diff --git a/app/controllers/my_profile/profile_design_controller.rb b/app/controllers/my_profile/profile_design_controller.rb index cd4fc38..ad666e2 100644 --- a/app/controllers/my_profile/profile_design_controller.rb +++ b/app/controllers/my_profile/profile_design_controller.rb @@ -5,7 +5,7 @@ class ProfileDesignController < BoxOrganizerController protect 'edit_profile_design', :profile def available_blocks - blocks = [ ArticleBlock, TagsBlock, RecentDocumentsBlock, ProfileInfoBlock, LinkListBlock, MyNetworkBlock, FeedReaderBlock, ProfileImageBlock, LocationBlock, SlideshowBlock ] + blocks = [ ArticleBlock, TagsBlock, RecentDocumentsBlock, ProfileInfoBlock, LinkListBlock, MyNetworkBlock, FeedReaderBlock, ProfileImageBlock, LocationBlock, SlideshowBlock, ProfileSearchBlock ] # blocks exclusive for organizations if profile.has_members? diff --git a/app/controllers/public/profile_search_controller.rb b/app/controllers/public/profile_search_controller.rb new file mode 100644 index 0000000..a960fa3 --- /dev/null +++ b/app/controllers/public/profile_search_controller.rb @@ -0,0 +1,26 @@ +class ProfileSearchController < PublicController + + include SearchHelper + + needs_profile + before_filter :check_access_to_profile + + def index + @q = params[:q].blank? ? '' : params[:q] + @filtered_query = remove_stop_words(@q) + if params[:where] == 'environment' + redirect_to :controller => 'search', :query => @q + else + @results = profile.articles.published.find_by_contents(@filtered_query).paginate(:per_page => 10, :page => params[:page]) + end + end + + protected + + def check_access_to_profile + unless profile.display_info_to?(user) + redirect_to :controller => 'profile', :action => 'index' + end + end + +end diff --git a/app/helpers/folder_helper.rb b/app/helpers/folder_helper.rb index abeff3c..3b9a672 100644 --- a/app/helpers/folder_helper.rb +++ b/app/helpers/folder_helper.rb @@ -44,7 +44,11 @@ module FolderHelper if (icon =~ /\//) icon else - 'icon icon-' + icon + klasses = 'icon icon-' + icon + if article.kind_of?(UploadedFile) + klasses += ' icon-upload-file' + end + klasses end end diff --git a/app/helpers/forms_helper.rb b/app/helpers/forms_helper.rb index d636aef..6b3511f 100644 --- a/app/helpers/forms_helper.rb +++ b/app/helpers/forms_helper.rb @@ -41,8 +41,7 @@ module FormsHelper the_class << ' ' << html_options[:class] end - # FIXME: should be in stylesheet - bt_submit = submit_tag(label, html_options.merge(:style => 'height:28px; cursor:pointer', :class => the_class)) + bt_submit = submit_tag(label, html_options.merge(:class => the_class)) bt_submit + bt_cancel end diff --git a/app/models/article.rb b/app/models/article.rb index 43ffc10..318bf7e 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -473,6 +473,10 @@ class Article < ActiveRecord::Base abstract.blank? ? first_paragraph : abstract end + def short_lead + truncate sanitize_html(self.lead), 170, '...' + end + def creator creator_id = versions[0][:last_changed_by_id] creator_id && Profile.find(creator_id) @@ -493,4 +497,9 @@ class Article < ActiveRecord::Base tag_name.gsub(/[<>]/, '') end + def sanitize_html(text) + sanitizer = HTML::FullSanitizer.new + sanitizer.sanitize(text) + end + end diff --git a/app/models/profile_search_block.rb b/app/models/profile_search_block.rb new file mode 100644 index 0000000..eae7518 --- /dev/null +++ b/app/models/profile_search_block.rb @@ -0,0 +1,22 @@ +class ProfileSearchBlock < Block + + def self.description + _('Display a form to search the profile') + end + + def default_title + _('Profile search') + end + + def content + title = self.title + lambda do + render :file => 'blocks/profile_search', :locals => { :title => title } + end + end + + def editable? + true + end + +end diff --git a/app/models/text_article.rb b/app/models/text_article.rb index 379135f..e07e787 100644 --- a/app/models/text_article.rb +++ b/app/models/text_article.rb @@ -4,4 +4,13 @@ class TextArticle < Article xss_terminate :only => [ :name, :abstract, :body ], :on => 'validation' include Noosfero::TranslatableContent + + def self.icon_name(article = nil) + if article && !article.parent.nil? && article.parent.kind_of?(Blog) + Blog.icon_name + else + Article.icon_name + end + end + end diff --git a/app/models/uploaded_file.rb b/app/models/uploaded_file.rb index 45c3dcc..ef23258 100644 --- a/app/models/uploaded_file.rb +++ b/app/models/uploaded_file.rb @@ -47,8 +47,8 @@ class UploadedFile < Article delay_attachment_fu_thumbnails def self.icon_name(article = nil) - if article && article.image? - article.public_filename(:icon) + if article + article.image? ? article.public_filename(:icon) : article.mime_type.gsub(/[\/+.]/, '-') else 'upload-file' end diff --git a/app/views/blocks/profile_search.rhtml b/app/views/blocks/profile_search.rhtml new file mode 100644 index 0000000..d597920 --- /dev/null +++ b/app/views/blocks/profile_search.rhtml @@ -0,0 +1,3 @@ +<%= block_title(title) %> + +<%= render :partial => 'shared/profile_search_form' %> diff --git a/app/views/profile/index.rhtml b/app/views/profile/index.rhtml index 3560123..be3f86d 100644 --- a/app/views/profile/index.rhtml +++ b/app/views/profile/index.rhtml @@ -10,6 +10,9 @@ <%= profile.description %> <% end %> + <% end %> diff --git a/app/views/profile_search/_article.rhtml b/app/views/profile_search/_article.rhtml new file mode 100644 index 0000000..45fea5c --- /dev/null +++ b/app/views/profile_search/_article.rhtml @@ -0,0 +1,5 @@ +
  • + <%= link_to article.title, article.view_url, :class => 'result-title ' + icon_for_article(article) %> +

    <%= link_to article.short_lead, article.url, {:class => 'article-details'} %>

    +
    <%= link_to url_for(article.url), article.url, :class => 'article-url' %>
    +
  • diff --git a/app/views/profile_search/_folder.rhtml b/app/views/profile_search/_folder.rhtml new file mode 100644 index 0000000..c9bfd26 --- /dev/null +++ b/app/views/profile_search/_folder.rhtml @@ -0,0 +1,5 @@ +
  • + <%= link_to article.title, article.view_url, :class => 'result-title ' + icon_for_article(article) %> +

    <%= link_to article.body.to_s, article.url, {:class => 'article-details'} %>

    +
    <%= link_to url_for(article.url), article.url, :class => 'article-url' %>
    +
  • diff --git a/app/views/profile_search/_uploaded_file.rhtml b/app/views/profile_search/_uploaded_file.rhtml new file mode 100644 index 0000000..9e7ae4b --- /dev/null +++ b/app/views/profile_search/_uploaded_file.rhtml @@ -0,0 +1,11 @@ +<% if article.image? %> +
  • + <%= link_to image_tag(article.public_filename(:thumb), :alt => article.display_title), article.view_url, :class => 'article-details' %> +
  • +<% else %> +
  • + <%= link_to article.title, article.view_url, :class => 'result-title ' + icon_for_article(article) %> +

    <%= link_to article.title, article.short_lead, :class => 'article-details' %>

    +
    <%= link_to url_for(article.url), article.url, :class => 'article-url' %>
    +
  • +<% end %> diff --git a/app/views/profile_search/index.rhtml b/app/views/profile_search/index.rhtml new file mode 100644 index 0000000..84ac4fa --- /dev/null +++ b/app/views/profile_search/index.rhtml @@ -0,0 +1,18 @@ +
    +

    <%= _("Search results on %s's profile") % profile.short_name %>

    + + <%= render :partial => 'shared/profile_search_form' %> + +
    + <%= _("%s results found") % @results.total_entries %> +
    + + + + <%= pagination_links @results %> + +
    diff --git a/app/views/shared/_profile_search_form.rhtml b/app/views/shared/_profile_search_form.rhtml new file mode 100644 index 0000000..d7b720e --- /dev/null +++ b/app/views/shared/_profile_search_form.rhtml @@ -0,0 +1,12 @@ +<% form_tag( { :controller => 'profile_search', :profile => profile.identifier}, :method => 'get', :class => 'search_form' ) do %> +
    + + <%= text_field_tag 'q', @q, :title => _("Find %s's content") % profile.short_name %> + + <%= submit_button(:search, _('Search')) %> +
    + <%= labelled_radio_button __('on profile'), 'where', 'profile', true %> + <%= labelled_radio_button _('on %s') % environment.name, 'where', 'environment', false %> +
    +
    +<% end %> diff --git a/config/routes.rb b/config/routes.rb index 8410e12..d3e2e44 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -75,6 +75,9 @@ ActionController::Routing::Routes.draw do |map| map.tag 'profile/:profile/tags/:id', :controller => 'profile', :action => 'content_tagged', :id => /.+/, :profile => /#{Noosfero.identifier_format}/ map.tag 'profile/:profile/tags', :controller => 'profile', :action => 'tags', :profile => /#{Noosfero.identifier_format}/ + # profile search + map.profile_search 'profile/:profile/search', :controller => 'profile_search', :action => 'index', :profile => /#{Noosfero.identifier_format}/ + # public profile information map.profile 'profile/:profile/:action/:id', :controller => 'profile', :action => 'index', :id => /.*/, :profile => /#{Noosfero.identifier_format}/ diff --git a/features/profile_search.feature b/features/profile_search.feature new file mode 100644 index 0000000..bb8fc2a --- /dev/null +++ b/features/profile_search.feature @@ -0,0 +1,66 @@ +Feature: search inside a profile + As a noosfero user + I want to search + In order to find stuff from a profile + + Background: + Given the following users + | login | name | + | joaosilva | Joao Silva | + And the following articles + | owner | name | body | + | joaosilva | bees and butterflies | this is an article about bees and butterflies | + | joaosilva | whales and dolphins | this is an article about whales and dolphins | + + Scenario: search on profile + Given I go to Joao Silva's profile + And I fill in "q" with "bees" + And I press "Search" + Then I should see "bees and butterflies" within ".main-block" + And I should not see "whales and dolphins" within ".main-block" + + Scenario: search for event on profile + Given the following events + | owner | name | start_date | + | joaosilva | Group meeting | 2009-10-01 | + | joaosilva | John Doe's birthday | 2009-09-01 | + When I go to Joao Silva's profile + And I fill in "q" with "birthday" + And I press "Search" + Then I should see "John Doe's birthday" within ".main-block" + And I should not see "Group meeting" within ".main-block" + + Scenario: simple search for event on profile search block + Given the following blocks + | owner | type | + | joaosilva | ProfileSearchBlock | + When I go to Joao Silva's profile + And I fill in "q" with "bees" within ".profile-search-block" + And I press "Search" + Then I should see "bees and butterflies" within ".main-block" + + Scenario: not display unpublished articles + Given the following articles + | owner | name | body | published | + | joaosilva | published article | this is a public article | true | + | joaosilva | unpublished article | this is a private article | false | + And I go to Joao Silva's profile + And I fill in "q" with "article" + And I press "Search" + Then I should see "public article" within ".main-block" + And I should not see "private article" within ".main-block" + + Scenario: search on environment + Given I go to Joao Silva's profile + And I fill in "q" with "bees" + And I choose "on Colivre.net" + And I press "Search" + Then I should be on the search page + And I should see "bees and butterflies" within "#search-page" + + Scenario: not display search on private profiles + Given the following users + | login | name | public_profile | + | mariasilva | Maria Silva | false | + And I go to /profile/mariasilva/search + Then I should see "friends only" diff --git a/public/designs/icons/tango/style.css b/public/designs/icons/tango/style.css index 0f5b9d6..c711478 100644 --- a/public/designs/icons/tango/style.css +++ b/public/designs/icons/tango/style.css @@ -69,7 +69,19 @@ .icon-newupload-file { background-image: url(Tango/16x16/actions/filesave.png) } .icon-slideshow { background-image: url(Tango/16x16/mimetypes/x-office-presentation.png) } .icon-photos { background-image: url(Tango/16x16/devices/camera-photo.png) } + .icon-text-html { background-image: url(Tango/16x16/mimetypes/text-html.png) } +.icon-text-plain { background-image: url(Tango/16x16/mimetypes/text-x-generic.png) } +.icon-image-svg-xml { background-image: url(Tango/16x16/mimetypes/image-x-generic.png) } +.icon-application-octet-stream { background-image: url(Tango/16x16/mimetypes/binary.png) } +.icon-application-x-gzip { background-image: url(Tango/16x16/mimetypes/gnome-mime-application-x-gzip.png) } +.icon-application-postscript { background-image: url(Tango/16x16/mimetypes/gnome-mime-application-postscript.png) } +.icon-application-pdf { background-image: url(Tango/16x16/mimetypes/gnome-mime-application-pdf.png) } +.icon-application-ogg { background-image: url(Tango/16x16/mimetypes/gnome-mime-application-ogg.png) } +.icon-video-mpeg { background-image: url(Tango/16x16/mimetypes/video-x-generic.png) } +.icon-application-vnd-oasis-opendocument-text { background-image: url(Tango/16x16/mimetypes/gnome-mime-application-vnd.oasis.opendocument.text.png) } +.icon-application-vnd-oasis-opendocument-spreadsheet { background-image: url(Tango/16x16/mimetypes/gnome-mime-application-vnd.oasis.opendocument.spreadsheet.png) } +.icon-application-vnd-oasis-opendocument-presentation { background-image: url(Tango/16x16/mimetypes/gnome-mime-application-vnd.oasis.opendocument.presentation.png) } .icon-media-pause { background-image: url(Tango/16x16/actions/media-playback-pause.png) } .icon-media-play { background-image: url(Tango/16x16/actions/media-playback-start.png) } diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index dc0e86f..1b17688 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -1567,6 +1567,10 @@ input.button { input.button { background-position: 2px 50%; } +#content form input.button.submit { + height: 28px; + cursor: pointer; +} .button span { display: none; @@ -4372,6 +4376,98 @@ h1#agenda-title { text-align: left; } +/* * * Profile search * * * * * * * */ + +#public-profile-search, +#profile-search-results form, +.profile-search-block form { + padding: 10px; + margin-bottom: 15px; + background-color: #E6E6E6; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; +} + +#public-profile-search .formfield input { + width: 400px; +} + +/* * * Profile Search Results * * * * * * * */ + +#profile-search-results ul { + padding-left: 0px; +} + +#profile-search-results form .formfield input { + width: 395px; +} + +#profile-search-results form .search_form .button { + margin-top: 5px; +} + +#profile-search-results li { + list-style: none; + margin-bottom: 20px; + clear: left; +} + +#profile-search-results li.result-image { + float: left; + clear: none; + height: 150px; + margin-right: 10px; + margin-left: 10px; +} + +#profile-search-results .result-title { + font-size: 18px; +} + +#profile-search-results p { + margin-top: 5px; + margin-bottom: 0px; +} + +#profile-search-results .article-details { + color: #000; + text-decoration: none; +} + +#profile-search-results a.article-url { + text-decoration: none; + color: #77AA44; +} + +#profile-search-results a.article-url:hover { + text-decoration: underline; +} + +#profile-search-results .results-list .icon { + background-repeat: no-repeat; + background-position: left; + padding: 0px 0px 3px 20px; + border: none; +} + +#profile-search-results .results-list .icon:hover { + background-color: transparent; +} + +#profile-search-results .results-found-message { + margin-top: 10px; + font-style: italic; +} + +/* * * Profile search block * * * * * * * */ + +.profile-search-block .formfield input { + width: 100%; +} + +.profile-search-block .button.icon-search { + display: none; +} /* * * Sub-category stuff * * * * * * * */ diff --git a/test/functional/profile_design_controller_test.rb b/test/functional/profile_design_controller_test.rb index c9ed20a..1d607e3 100644 --- a/test/functional/profile_design_controller_test.rb +++ b/test/functional/profile_design_controller_test.rb @@ -5,7 +5,7 @@ class ProfileDesignController; def rescue_action(e) raise e end; end class ProfileDesignControllerTest < Test::Unit::TestCase - COMMOM_BLOCKS = [ ArticleBlock, TagsBlock, RecentDocumentsBlock, ProfileInfoBlock, LinkListBlock, MyNetworkBlock, FeedReaderBlock, ProfileImageBlock, LocationBlock, SlideshowBlock] + COMMOM_BLOCKS = [ ArticleBlock, TagsBlock, RecentDocumentsBlock, ProfileInfoBlock, LinkListBlock, MyNetworkBlock, FeedReaderBlock, ProfileImageBlock, LocationBlock, SlideshowBlock, ProfileSearchBlock ] PERSON_BLOCKS = COMMOM_BLOCKS + [FriendsBlock, FavoriteEnterprisesBlock, CommunitiesBlock, EnterprisesBlock ] PERSON_BLOCKS_WITH_MEMBERS = PERSON_BLOCKS + [MembersBlock] PERSON_BLOCKS_WITH_BLOG = PERSON_BLOCKS + [BlogArchivesBlock] diff --git a/test/functional/profile_search_controller_test.rb b/test/functional/profile_search_controller_test.rb new file mode 100644 index 0000000..0feb6f9 --- /dev/null +++ b/test/functional/profile_search_controller_test.rb @@ -0,0 +1,106 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'profile_search_controller' + +# Re-raise errors caught by the controller. +class ProfileSearchController; def rescue_action(e) raise e end; end + +class ProfileSearchControllerTest < Test::Unit::TestCase + def setup + @controller = ProfileSearchController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + + @person = fast_create(Person) + end + attr_reader :person + + should 'filter stop words' do + @controller.expects(:locale).returns('en').at_least_once + get 'index', :profile => person.identifier, :q => 'an article about something' + assert_response :success + assert_template 'index' + assert_equal 'article something', assigns('filtered_query') + end + + should 'espape xss attack' do + @controller.expects(:profile).returns(person).at_least_once + get 'index', :profile => person.identifier, :q => '' + assert_no_tag :tag => 'wslite' + end + + should 'render success in search' do + get :index, :profile => person.identifier, :q => 'something not important' + assert_response :success + end + + should 'search for articles' do + article = TextileArticle.create(:name => 'My article', :body => 'Article to test profile search', :profile => person) + + get 'index', :profile => person.identifier, :q => 'article profile' + assert_includes assigns(:results), article + end + + should 'display search results' do + article1 = fast_create(Article, :body => '

    Article to test profile search

    ', :profile_id => person.id) + article2 = fast_create(Article, :body => '

    Another article to test profile search

    ', :profile_id => person.id) + + get 'index', :profile => person.identifier, :q => 'article' + + [article1, article2].each do |article| + assert_tag :tag => 'li', :descendant => { :tag => 'a', :content => article.short_lead, :attributes => { :class => /article-details/ }} + end + end + + should 'paginate results listing' do + (1..11).each do |i| + TextileArticle.create!(:name => "Article #{i}", :profile => person, :language => 'en') + end + + get 'index', :profile => person.identifier, :q => 'Article' + + assert_equal 10, assigns(:results).size + assert_tag :tag => 'a', :attributes => { :href => "/profile/#{person.identifier}/search?page=2&q=Article", :rel => 'next' } + end + + should 'display abstract if given' do + article1 = TextileArticle.create(:name => 'Article 1', :abstract => 'Abstract to test', :body => 'Article to test profile search', :profile => person) + article2 = TextileArticle.create(:name => 'Article 2', :body => 'Another article to test profile search', :profile => person) + + get 'index', :profile => person.identifier, :q => 'article profile' + + assert_tag :tag => 'li', :descendant => { :tag => 'a', :content => article1.abstract, :attributes => { :class => /article-details/ }} + assert_no_tag :tag => 'li', :descendant => { :tag => 'a', :content => article1.body, :attributes => { :class => /article-details/ }} + + assert_tag :tag => 'li', :descendant => { :tag => 'a', :content => article2.body, :attributes => { :class => /article-details/ }} + end + + should 'display nothing if search is blank' do + article1 = TextileArticle.create(:name => 'Article 1', :body => 'Article to test profile search', :profile => person) + article2 = TextileArticle.create(:name => 'Article 2', :body => 'Another article to test profile search', :profile => person) + + get 'index', :profile => person.identifier, :q => '' + + assert_no_tag :tag => 'ul', :attributes => { :id => 'profile-search-results'}, :descendant => { :tag => 'li' } + end + + should 'not display private articles' do + article1 = TextileArticle.create(:name => 'Article 1', :body => 'Article to test profile search', :profile => person, :published => false) + article2 = TextileArticle.create(:name => 'Article 2', :body => 'Another article to test profile search', :profile => person) + + get 'index', :profile => person.identifier, :q => 'article profile' + + assert_no_tag :tag => 'li', :descendant => { :tag => 'a', :content => article1.body, :attributes => { :class => /article-details/ }} + + assert_tag :tag => 'li', :descendant => { :tag => 'a', :content => article2.body, :attributes => { :class => /article-details/ }} + end + + should 'display number of results found' do + article1 = TextileArticle.create(:name => 'Article 1', :body => 'Article to test profile search', :body => 'Article to test profile search', :profile => person) + article2 = TextileArticle.create(:name => 'Article 2', :body => 'Another article to test profile search', :profile => person) + + get 'index', :profile => person.identifier, :q => 'article profile' + + assert_tag :tag => 'div', :attributes => { :class => 'results-found-message' }, :content => /2 results found/ + end + +end diff --git a/test/unit/article_test.rb b/test/unit/article_test.rb index a41f41b..a26c8e9 100644 --- a/test/unit/article_test.rb +++ b/test/unit/article_test.rb @@ -921,6 +921,16 @@ class ArticleTest < Test::Unit::TestCase assert_equal '', a.lead end + should 'have short lead' do + a = fast_create(TinyMceArticle, :body => '

    ' + ('a' *180) + '

    ') + assert_equal 170, a.short_lead.length + end + + should 'remove html from short lead' do + a = Article.new(:body => "

    an article with html that should be removed

    ") + assert_equal 'an article with html that should be removed', a.short_lead + end + should 'track action when a published article is created outside a community' do article = TinyMceArticle.create! :name => 'Tracked Article', :profile_id => profile.id assert article.published? diff --git a/test/unit/profile_search_block_test.rb b/test/unit/profile_search_block_test.rb new file mode 100644 index 0000000..c4f4a53 --- /dev/null +++ b/test/unit/profile_search_block_test.rb @@ -0,0 +1,32 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class ProfileSearchBlockTest < Test::Unit::TestCase + + should 'describe itself' do + assert_not_equal Block.description, ProfileSearchBlock.description + end + + should 'provide a default title' do + assert_not_equal Block.new.default_title, ProfileSearchBlock.new.default_title + end + + should 'render profile search' do + person = fast_create(Person) + + block = ProfileSearchBlock.new + block.stubs(:owner).returns(person) + + self.expects(:render).with(:file => 'blocks/profile_search', :locals => { :title => block.title}) + instance_eval(& block.content) + end + + should 'provide view_title' do + person = fast_create(Person) + person.boxes << Box.new + block = ProfileSearchBlock.new(:title => 'Title from block') + person.boxes.first.blocks << block + block.save! + assert_equal 'Title from block', block.view_title + end + +end diff --git a/test/unit/text_article_test.rb b/test/unit/text_article_test.rb index e4ded47..34c5da7 100644 --- a/test/unit/text_article_test.rb +++ b/test/unit/text_article_test.rb @@ -43,4 +43,14 @@ class TextArticleTest < Test::Unit::TestCase assert_kind_of Noosfero::TranslatableContent, TextArticle.new end + should 'return article icon name' do + assert_equal Article.icon_name, TextArticle.icon_name + end + + should 'return blog icon name if the article is a blog post' do + blog = fast_create(Blog) + article = TextArticle.new(:parent => blog) + assert_equal Blog.icon_name, TextArticle.icon_name(article) + end + end -- libgit2 0.21.2