]> source.dussan.org Git - redmine.git/commitdiff
Ability to search all projects or the projects the user belongs to (#791).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Sun, 18 May 2008 16:15:22 +0000 (16:15 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Sun, 18 May 2008 16:15:22 +0000 (16:15 +0000)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1435 e93f8b46-1217-0410-a6f0-8f06a7374b81

14 files changed:
app/controllers/search_controller.rb
app/helpers/search_helper.rb
app/models/changeset.rb
app/models/document.rb
app/models/issue.rb
app/models/journal.rb
app/models/message.rb
app/models/news.rb
app/models/project.rb
app/models/wiki_page.rb
app/views/search/index.rhtml
public/stylesheets/application.css
test/functional/search_controller_test.rb
vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb

index f15653b634fb181b6062e2cbb7cfa6d1df8e636a..d93c638084563bd4e0ce31364a63ad66f94dda32 100644 (file)
@@ -29,6 +29,16 @@ class SearchController < ApplicationController
     @all_words = params[:all_words] || (params[:submit] ? false : true)
     @titles_only = !params[:titles_only].nil?
     
+    projects_to_search =
+      case params[:projects]
+      when 'all'
+        nil
+      when 'my_projects'
+        User.current.memberships.collect(&:project)
+      else
+        @project
+      end
+          
     offset = nil
     begin; offset = params[:offset].to_time if params[:offset]; rescue; end
     
@@ -38,16 +48,16 @@ class SearchController < ApplicationController
       return
     end
     
-    if @project
+    @object_types = %w(issues news documents changesets wiki_pages messages projects)
+    if projects_to_search.is_a? Project
+      # don't search projects
+      @object_types.delete('projects')
       # only show what the user is allowed to view
-      @object_types = %w(issues news documents changesets wiki_pages messages)
-      @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)}
-      
-      @scope = @object_types.select {|t| params[t]}
-      @scope = @object_types if @scope.empty?
-    else
-      @object_types = @scope = %w(projects)
+      @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
     end
+      
+    @scope = @object_types.select {|t| params[t]}
+    @scope = @object_types if @scope.empty?
     
     # extract tokens from the question
     # eg. hello "bye bye" => ["hello", "bye bye"]
@@ -62,37 +72,27 @@ class SearchController < ApplicationController
       like_tokens = @tokens.collect {|w| "%#{w.downcase}%"}      
       @results = []
       limit = 10
-      if @project        
-        @scope.each do |s|
-          @results += s.singularize.camelcase.constantize.search(like_tokens, @project,
-            :all_words => @all_words,
-            :titles_only => @titles_only,
-            :limit => (limit+1),
-            :offset => offset,
-            :before => params[:previous].nil?)
-        end
-        @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
-        if params[:previous].nil?
-          @pagination_previous_date = @results[0].event_datetime if offset && @results[0]
-          if @results.size > limit
-            @pagination_next_date = @results[limit-1].event_datetime 
-            @results = @results[0, limit]
-          end
-        else
-          @pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
-          if @results.size > limit
-            @pagination_previous_date = @results[-(limit)].event_datetime 
-            @results = @results[-(limit), limit]
-          end
+      @scope.each do |s|
+        @results += s.singularize.camelcase.constantize.search(like_tokens, projects_to_search,
+          :all_words => @all_words,
+          :titles_only => @titles_only,
+          :limit => (limit+1),
+          :offset => offset,
+          :before => params[:previous].nil?)
+      end
+      @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
+      if params[:previous].nil?
+        @pagination_previous_date = @results[0].event_datetime if offset && @results[0]
+        if @results.size > limit
+          @pagination_next_date = @results[limit-1].event_datetime 
+          @results = @results[0, limit]
         end
       else
-        operator = @all_words ? ' AND ' : ' OR '
-        @results += Project.find(:all, 
-                                 :limit => limit,
-                                 :conditions => [ (["(#{Project.visible_by(User.current)}) AND (LOWER(name) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort]
-                                 ) if @scope.include? 'projects'
-        # if only one project is found, user is redirected to its overview
-        redirect_to :controller => 'projects', :action => 'show', :id => @results.first and return if @results.size == 1
+        @pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
+        if @results.size > limit
+          @pagination_previous_date = @results[-(limit)].event_datetime 
+          @results = @results[-(limit), limit]
+        end
       end
     else
       @question = ""
index ed2f40b69f1e79a7f6b64ab3542acd2099ed0429..6b5a2cede90a97e2f1a353a38a8e87c95e8f25a6 100644 (file)
@@ -35,4 +35,11 @@ module SearchHelper
     end
     result
   end
+  
+  def project_select_tag
+    options = [[l(:label_project_all), 'all']]
+    options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty?
+    options << [@project.name, ''] unless @project.nil?
+    select_tag('projects', options_for_select(options, params[:projects].to_s)) if options.size > 1
+  end
 end
index 3e95ce111ce6bc352f311f7fe9d90d547bf4be50..9a05e6a6810163a2ed9e47179a22891ec1c323ec 100644 (file)
@@ -27,7 +27,7 @@ class Changeset < ActiveRecord::Base
                 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
                 
   acts_as_searchable :columns => 'comments',
-                     :include => :repository,
+                     :include => {:repository => :project},
                      :project_key => "#{Repository.table_name}.project_id",
                      :date_column => 'committed_on'
   
index 7a432b46bad2e03369719cce5600001dad2e8acb..9e2818fc700e090289eef387b6533f5881b03204 100644 (file)
@@ -20,7 +20,7 @@ class Document < ActiveRecord::Base
   belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
   has_many :attachments, :as => :container, :dependent => :destroy
 
-  acts_as_searchable :columns => ['title', 'description']
+  acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
   acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
                 :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
                 :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
index 8082e43b798733f6ff2097d15c3b7122590eb0a6..0618b0f0a57338452d8a0e4f22f381ae90a9c220 100644 (file)
@@ -36,7 +36,7 @@ class Issue < ActiveRecord::Base
   has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
   
   acts_as_watchable
-  acts_as_searchable :columns => ['subject', 'description'], :with => {:journal => :issue}
+  acts_as_searchable :columns => ['subject', "#{table_name}.description"], :include => :project, :with => {:journal => :issue}
   acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
                 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}                
   
index 1376d349e3f0eb56c0473708ba197d000af586cd..edf261e6d114591316e55f33bcc36f301de5a2e3 100644 (file)
@@ -26,7 +26,7 @@ class Journal < ActiveRecord::Base
   attr_accessor :indice
   
   acts_as_searchable :columns => 'notes',
-                     :include => :issue,
+                     :include => {:issue => :project},
                      :project_key => "#{Issue.table_name}.project_id",
                      :date_column => "#{Issue.table_name}.created_on"
   
index a18d126c9add21d1a2e557fafd81b450e3827adf..f57b909853b276dac38978fcbd5232d278263478 100644 (file)
@@ -23,9 +23,9 @@ class Message < ActiveRecord::Base
   belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
   
   acts_as_searchable :columns => ['subject', 'content'],
-                     :include => :board,
+                     :include => {:board, :project},
                      :project_key => 'project_id',
-                     :date_column => 'created_on'
+                     :date_column => "#{table_name}.created_on"
   acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
                 :description => :content,
                 :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
index 3d8c4d661fbe857d23d18960946079262ed746c6..71e2a2d5e2646ba910bfce9e1febca57c8ff7b63 100644 (file)
@@ -24,7 +24,7 @@ class News < ActiveRecord::Base
   validates_length_of :title, :maximum => 60
   validates_length_of :summary, :maximum => 255
 
-  acts_as_searchable :columns => ['title', 'description']
+  acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
   acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
 
   # returns latest news for projects visible by user
index 8c32c8562b1459518d7447d68c1cbd16361c1cc0..2f2937fd9f86a25a1637e02700c550c5aeb4661e 100644 (file)
@@ -46,7 +46,7 @@ class Project < ActiveRecord::Base
                           
   acts_as_tree :order => "name", :counter_cache => true
 
-  acts_as_searchable :columns => ['name', 'description'], :project_key => 'id'
+  acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
   acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
                 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}}
 
@@ -202,6 +202,10 @@ class Project < ActiveRecord::Base
     @all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq
   end
   
+  def project
+    self
+  end
+  
   def <=>(project)
     name.downcase <=> project.name.downcase
   end
index 95750f37bcf62d8f57886746f9d67807a2c2180b..65fc1f68cd94e40fafcc0d744330fe98798a4e7b 100644 (file)
@@ -29,7 +29,7 @@ class WikiPage < ActiveRecord::Base
                 :url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}}
 
   acts_as_searchable :columns => ['title', 'text'],
-                     :include => [:wiki, :content],
+                     :include => [{:wiki => :project}, :content],
                      :project_key => "#{Wiki.table_name}.project_id"
 
   attr_accessor :redirect_existing_links
index 29c604a218b680b8ad0573abf280691cfd1450ce..0be97a504326ece09af5ce3f25ee91e7090b63bc 100644 (file)
@@ -4,23 +4,25 @@
 <% form_tag({}, :method => :get) do %>\r
 <p><%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %>\r
 <%= javascript_tag "Field.focus('search-input')" %>\r
-\r
+<%= project_select_tag %>\r
+<label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label>\r
+<label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label>\r
+</p>\r
+<p>\r
 <% @object_types.each do |t| %>\r
 <label><%= check_box_tag t, 1, @scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label>\r
 <% end %>\r
-<br />\r
-<label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label>\r
-<label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label>\r
 </p>\r
-<%= submit_tag l(:button_submit), :name => 'submit' %>\r
+\r
+<p><%= submit_tag l(:button_submit), :name => 'submit' %></p>\r
 <% end %>\r
 </div>\r
 \r
 <% if @results %>\r
     <h3><%= l(:label_result_plural) %></h3>\r
-    <ul>\r
+    <ul id="search-results">\r
       <% @results.each do |e| %>\r
-        <li><p><%= link_to highlight_tokens(truncate(e.event_title, 255), @tokens), e.event_url %><br />\r
+        <li><p><%= content_tag('span', h(e.project), :class => 'project') unless @project == e.project %> <%= link_to highlight_tokens(truncate(e.event_title, 255), @tokens), e.event_url %><br />\r
         <%= highlight_tokens(e.event_description, @tokens) %><br />\r
         <span class="author"><%= format_time(e.event_datetime) %></span></p></li>\r
       <% end %>\r
index 8e4bf995cee6aecdf9e37cc1db824908f49f6b3d..bdcf5615ca5c12ae7cd32335caace5588f6620c5 100644 (file)
@@ -177,7 +177,7 @@ div#activity dd { margin-bottom: 1em; padding-left: 18px; }
 div#activity dt { margin-bottom: 1px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
 div#activity dt .time { color: #777; font-size: 80%; }
 div#activity dd .description { font-style: italic; }
-div#activity span.project:after { content: " -"; }
+div#activity span.project:after, #search-results span.project:after { content: " -"; }
 div#activity dt.issue { background-image: url(../images/ticket.png); }
 div#activity dt.issue-edit { background-image: url(../images/ticket_edit.png); }
 div#activity dt.issue-closed { background-image: url(../images/ticket_checked.png); }
index 49004c7e677fe6f3a6e18717f397b56b70e51991..b02f0779365fec0aaafefacf55016e55cf64ea29 100644 (file)
@@ -5,7 +5,10 @@ require 'search_controller'
 class SearchController; def rescue_action(e) raise e end; end
 
 class SearchControllerTest < Test::Unit::TestCase
-  fixtures :projects, :enabled_modules, :issues, :custom_fields, :custom_values
+  fixtures :projects, :enabled_modules, :roles, :users,
+           :issues, :trackers, :issue_statuses,
+           :custom_fields, :custom_values,
+           :repositories, :changesets
   
   def setup
     @controller = SearchController.new
@@ -25,6 +28,15 @@ class SearchControllerTest < Test::Unit::TestCase
     assert assigns(:results).include?(Project.find(1))
   end
   
+  def test_search_all_projects
+    get :index, :q => 'recipe subproject commit', :submit => 'Search'
+    assert_response :success
+    assert_template 'index'
+    assert assigns(:results).include?(Issue.find(2))
+    assert assigns(:results).include?(Issue.find(5))
+    assert assigns(:results).include?(Changeset.find(101))
+  end
+  
   def test_search_without_searchable_custom_fields
     CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}"
     
index dff76b91314902e4116b8769734d1ef84785fc7a..1eb64c30f081939f81f66ad48abff40ef2f68469 100644 (file)
@@ -49,6 +49,9 @@ module Redmine
             raise 'No date column defined defined.'
           end
           
+          # Permission needed to search this model
+          searchable_options[:permission] = "view_#{self.name.underscore.pluralize}".to_sym unless searchable_options.has_key?(:permission)
+          
           # Should we search custom fields on this model ?
           searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil?
           
@@ -62,8 +65,12 @@ module Redmine
         end
 
         module ClassMethods
-          def search(tokens, project, options={})
+          # Search the model for the given tokens
+          # projects argument can be either nil (will search all projects), a project or an array of projects
+          def search(tokens, projects, options={})
             tokens = [] << tokens unless tokens.is_a?(Array)
+            projects = [] << projects unless projects.nil? || projects.is_a?(Array)
+            
             find_options = {:include => searchable_options[:include]}
             find_options[:limit] = options[:limit] if options[:limit]
             find_options[:order] = "#{searchable_options[:date_column]} " + (options[:before] ? 'DESC' : 'ASC')
@@ -92,12 +99,17 @@ module Redmine
             end
             find_options[:conditions] = [sql, * (tokens * token_clauses.size).sort]
             
-            results = with_scope(:find => {:conditions => ["#{searchable_options[:project_key]} = ?", project.id]}) do
+            project_conditions = []
+            project_conditions << (searchable_options[:permission].nil? ? Project.visible_by(User.current) :
+                                                 Project.allowed_to_condition(User.current, searchable_options[:permission]))
+            project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
+            
+            results = with_scope(:find => {:conditions => project_conditions.join(' AND ')}) do
               find(:all, find_options)
             end            
             if searchable_options[:with] && !options[:titles_only]
               searchable_options[:with].each do |model, assoc|
-                results += model.to_s.camelcase.constantize.search(tokens, project, options).collect {|r| r.send assoc}
+                results += model.to_s.camelcase.constantize.search(tokens, projects, options).collect {|r| r.send assoc}
               end
               results.uniq!
             end