diff options
-rw-r--r-- | app/controllers/search_controller.rb | 74 | ||||
-rw-r--r-- | app/helpers/search_helper.rb | 7 | ||||
-rw-r--r-- | app/models/changeset.rb | 2 | ||||
-rw-r--r-- | app/models/document.rb | 2 | ||||
-rw-r--r-- | app/models/issue.rb | 2 | ||||
-rw-r--r-- | app/models/journal.rb | 2 | ||||
-rw-r--r-- | app/models/message.rb | 4 | ||||
-rw-r--r-- | app/models/news.rb | 2 | ||||
-rw-r--r-- | app/models/project.rb | 6 | ||||
-rw-r--r-- | app/models/wiki_page.rb | 2 | ||||
-rw-r--r-- | app/views/search/index.rhtml | 16 | ||||
-rw-r--r-- | public/stylesheets/application.css | 2 | ||||
-rw-r--r-- | test/functional/search_controller_test.rb | 14 | ||||
-rw-r--r-- | vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb | 18 |
14 files changed, 95 insertions, 58 deletions
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index f15653b63..d93c63808 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -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 = "" diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index ed2f40b69..6b5a2cede 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -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 diff --git a/app/models/changeset.rb b/app/models/changeset.rb index 3e95ce111..9a05e6a68 100644 --- a/app/models/changeset.rb +++ b/app/models/changeset.rb @@ -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' diff --git a/app/models/document.rb b/app/models/document.rb index 7a432b46b..9e2818fc7 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -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}} diff --git a/app/models/issue.rb b/app/models/issue.rb index 8082e43b7..0618b0f0a 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -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}} diff --git a/app/models/journal.rb b/app/models/journal.rb index 1376d349e..edf261e6d 100644 --- a/app/models/journal.rb +++ b/app/models/journal.rb @@ -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" diff --git a/app/models/message.rb b/app/models/message.rb index a18d126c9..f57b90985 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -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'}, diff --git a/app/models/news.rb b/app/models/news.rb index 3d8c4d661..71e2a2d5e 100644 --- a/app/models/news.rb +++ b/app/models/news.rb @@ -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 diff --git a/app/models/project.rb b/app/models/project.rb index 8c32c8562..2f2937fd9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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 diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 95750f37b..65fc1f68c 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -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 diff --git a/app/views/search/index.rhtml b/app/views/search/index.rhtml index 29c604a21..0be97a504 100644 --- a/app/views/search/index.rhtml +++ b/app/views/search/index.rhtml @@ -4,23 +4,25 @@ <% form_tag({}, :method => :get) do %>
<p><%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %>
<%= javascript_tag "Field.focus('search-input')" %>
-
+<%= project_select_tag %>
+<label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label>
+<label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label>
+</p>
+<p>
<% @object_types.each do |t| %>
<label><%= check_box_tag t, 1, @scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label>
<% end %>
-<br />
-<label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label>
-<label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label>
</p>
-<%= submit_tag l(:button_submit), :name => 'submit' %>
+
+<p><%= submit_tag l(:button_submit), :name => 'submit' %></p>
<% end %>
</div>
<% if @results %>
<h3><%= l(:label_result_plural) %></h3>
- <ul>
+ <ul id="search-results">
<% @results.each do |e| %>
- <li><p><%= link_to highlight_tokens(truncate(e.event_title, 255), @tokens), e.event_url %><br />
+ <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 />
<%= highlight_tokens(e.event_description, @tokens) %><br />
<span class="author"><%= format_time(e.event_datetime) %></span></p></li>
<% end %>
diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 8e4bf995c..bdcf5615c 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -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); } diff --git a/test/functional/search_controller_test.rb b/test/functional/search_controller_test.rb index 49004c7e6..b02f07793 100644 --- a/test/functional/search_controller_test.rb +++ b/test/functional/search_controller_test.rb @@ -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}" diff --git a/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb b/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb index dff76b913..1eb64c30f 100644 --- a/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb +++ b/vendor/plugins/acts_as_searchable/lib/acts_as_searchable.rb @@ -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 |