##// END OF EJS Templates
Ability to search all projects or the projects the user belongs to (#791)....
Jean-Philippe Lang -
r1420:073818f8bc46
parent child
Show More
@@ -29,6 +29,16 class SearchController < ApplicationController
29 29 @all_words = params[:all_words] || (params[:submit] ? false : true)
30 30 @titles_only = !params[:titles_only].nil?
31 31
32 projects_to_search =
33 case params[:projects]
34 when 'all'
35 nil
36 when 'my_projects'
37 User.current.memberships.collect(&:project)
38 else
39 @project
40 end
41
32 42 offset = nil
33 43 begin; offset = params[:offset].to_time if params[:offset]; rescue; end
34 44
@@ -38,16 +48,16 class SearchController < ApplicationController
38 48 return
39 49 end
40 50
41 if @project
51 @object_types = %w(issues news documents changesets wiki_pages messages projects)
52 if projects_to_search.is_a? Project
53 # don't search projects
54 @object_types.delete('projects')
42 55 # only show what the user is allowed to view
43 @object_types = %w(issues news documents changesets wiki_pages messages)
44 @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, @project)}
45
46 @scope = @object_types.select {|t| params[t]}
47 @scope = @object_types if @scope.empty?
48 else
49 @object_types = @scope = %w(projects)
56 @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
50 57 end
58
59 @scope = @object_types.select {|t| params[t]}
60 @scope = @object_types if @scope.empty?
51 61
52 62 # extract tokens from the question
53 63 # eg. hello "bye bye" => ["hello", "bye bye"]
@@ -62,37 +72,27 class SearchController < ApplicationController
62 72 like_tokens = @tokens.collect {|w| "%#{w.downcase}%"}
63 73 @results = []
64 74 limit = 10
65 if @project
66 @scope.each do |s|
67 @results += s.singularize.camelcase.constantize.search(like_tokens, @project,
68 :all_words => @all_words,
69 :titles_only => @titles_only,
70 :limit => (limit+1),
71 :offset => offset,
72 :before => params[:previous].nil?)
73 end
74 @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
75 if params[:previous].nil?
76 @pagination_previous_date = @results[0].event_datetime if offset && @results[0]
77 if @results.size > limit
78 @pagination_next_date = @results[limit-1].event_datetime
79 @results = @results[0, limit]
80 end
81 else
82 @pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
83 if @results.size > limit
84 @pagination_previous_date = @results[-(limit)].event_datetime
85 @results = @results[-(limit), limit]
86 end
75 @scope.each do |s|
76 @results += s.singularize.camelcase.constantize.search(like_tokens, projects_to_search,
77 :all_words => @all_words,
78 :titles_only => @titles_only,
79 :limit => (limit+1),
80 :offset => offset,
81 :before => params[:previous].nil?)
82 end
83 @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
84 if params[:previous].nil?
85 @pagination_previous_date = @results[0].event_datetime if offset && @results[0]
86 if @results.size > limit
87 @pagination_next_date = @results[limit-1].event_datetime
88 @results = @results[0, limit]
87 89 end
88 90 else
89 operator = @all_words ? ' AND ' : ' OR '
90 @results += Project.find(:all,
91 :limit => limit,
92 :conditions => [ (["(#{Project.visible_by(User.current)}) AND (LOWER(name) like ? OR LOWER(description) like ?)"] * like_tokens.size).join(operator), * (like_tokens * 2).sort]
93 ) if @scope.include? 'projects'
94 # if only one project is found, user is redirected to its overview
95 redirect_to :controller => 'projects', :action => 'show', :id => @results.first and return if @results.size == 1
91 @pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
92 if @results.size > limit
93 @pagination_previous_date = @results[-(limit)].event_datetime
94 @results = @results[-(limit), limit]
95 end
96 96 end
97 97 else
98 98 @question = ""
@@ -35,4 +35,11 module SearchHelper
35 35 end
36 36 result
37 37 end
38
39 def project_select_tag
40 options = [[l(:label_project_all), 'all']]
41 options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty?
42 options << [@project.name, ''] unless @project.nil?
43 select_tag('projects', options_for_select(options, params[:projects].to_s)) if options.size > 1
44 end
38 45 end
@@ -27,7 +27,7 class Changeset < ActiveRecord::Base
27 27 :url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project_id, :rev => o.revision}}
28 28
29 29 acts_as_searchable :columns => 'comments',
30 :include => :repository,
30 :include => {:repository => :project},
31 31 :project_key => "#{Repository.table_name}.project_id",
32 32 :date_column => 'committed_on'
33 33
@@ -20,7 +20,7 class Document < ActiveRecord::Base
20 20 belongs_to :category, :class_name => "Enumeration", :foreign_key => "category_id"
21 21 has_many :attachments, :as => :container, :dependent => :destroy
22 22
23 acts_as_searchable :columns => ['title', 'description']
23 acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
24 24 acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
25 25 :author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
26 26 :url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
@@ -36,7 +36,7 class Issue < ActiveRecord::Base
36 36 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
37 37
38 38 acts_as_watchable
39 acts_as_searchable :columns => ['subject', 'description'], :with => {:journal => :issue}
39 acts_as_searchable :columns => ['subject', "#{table_name}.description"], :include => :project, :with => {:journal => :issue}
40 40 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
41 41 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
42 42
@@ -26,7 +26,7 class Journal < ActiveRecord::Base
26 26 attr_accessor :indice
27 27
28 28 acts_as_searchable :columns => 'notes',
29 :include => :issue,
29 :include => {:issue => :project},
30 30 :project_key => "#{Issue.table_name}.project_id",
31 31 :date_column => "#{Issue.table_name}.created_on"
32 32
@@ -23,9 +23,9 class Message < ActiveRecord::Base
23 23 belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
24 24
25 25 acts_as_searchable :columns => ['subject', 'content'],
26 :include => :board,
26 :include => {:board, :project},
27 27 :project_key => 'project_id',
28 :date_column => 'created_on'
28 :date_column => "#{table_name}.created_on"
29 29 acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
30 30 :description => :content,
31 31 :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
@@ -24,7 +24,7 class News < ActiveRecord::Base
24 24 validates_length_of :title, :maximum => 60
25 25 validates_length_of :summary, :maximum => 255
26 26
27 acts_as_searchable :columns => ['title', 'description']
27 acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
28 28 acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
29 29
30 30 # returns latest news for projects visible by user
@@ -46,7 +46,7 class Project < ActiveRecord::Base
46 46
47 47 acts_as_tree :order => "name", :counter_cache => true
48 48
49 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id'
49 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
50 50 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
51 51 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}}
52 52
@@ -202,6 +202,10 class Project < ActiveRecord::Base
202 202 @all_custom_fields ||= (IssueCustomField.for_all + custom_fields).uniq
203 203 end
204 204
205 def project
206 self
207 end
208
205 209 def <=>(project)
206 210 name.downcase <=> project.name.downcase
207 211 end
@@ -29,7 +29,7 class WikiPage < ActiveRecord::Base
29 29 :url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}}
30 30
31 31 acts_as_searchable :columns => ['title', 'text'],
32 :include => [:wiki, :content],
32 :include => [{:wiki => :project}, :content],
33 33 :project_key => "#{Wiki.table_name}.project_id"
34 34
35 35 attr_accessor :redirect_existing_links
@@ -4,23 +4,25
4 4 <% form_tag({}, :method => :get) do %>
5 5 <p><%= text_field_tag 'q', @question, :size => 60, :id => 'search-input' %>
6 6 <%= javascript_tag "Field.focus('search-input')" %>
7
7 <%= project_select_tag %>
8 <label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label>
9 <label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label>
10 </p>
11 <p>
8 12 <% @object_types.each do |t| %>
9 13 <label><%= check_box_tag t, 1, @scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label>
10 14 <% end %>
11 <br />
12 <label><%= check_box_tag 'all_words', 1, @all_words %> <%= l(:label_all_words) %></label>
13 <label><%= check_box_tag 'titles_only', 1, @titles_only %> <%= l(:label_search_titles_only) %></label>
14 15 </p>
15 <%= submit_tag l(:button_submit), :name => 'submit' %>
16
17 <p><%= submit_tag l(:button_submit), :name => 'submit' %></p>
16 18 <% end %>
17 19 </div>
18 20
19 21 <% if @results %>
20 22 <h3><%= l(:label_result_plural) %></h3>
21 <ul>
23 <ul id="search-results">
22 24 <% @results.each do |e| %>
23 <li><p><%= link_to highlight_tokens(truncate(e.event_title, 255), @tokens), e.event_url %><br />
25 <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 />
24 26 <%= highlight_tokens(e.event_description, @tokens) %><br />
25 27 <span class="author"><%= format_time(e.event_datetime) %></span></p></li>
26 28 <% end %>
@@ -177,7 +177,7 div#activity dd { margin-bottom: 1em; padding-left: 18px; }
177 177 div#activity dt { margin-bottom: 1px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
178 178 div#activity dt .time { color: #777; font-size: 80%; }
179 179 div#activity dd .description { font-style: italic; }
180 div#activity span.project:after { content: " -"; }
180 div#activity span.project:after, #search-results span.project:after { content: " -"; }
181 181 div#activity dt.issue { background-image: url(../images/ticket.png); }
182 182 div#activity dt.issue-edit { background-image: url(../images/ticket_edit.png); }
183 183 div#activity dt.issue-closed { background-image: url(../images/ticket_checked.png); }
@@ -5,7 +5,10 require 'search_controller'
5 5 class SearchController; def rescue_action(e) raise e end; end
6 6
7 7 class SearchControllerTest < Test::Unit::TestCase
8 fixtures :projects, :enabled_modules, :issues, :custom_fields, :custom_values
8 fixtures :projects, :enabled_modules, :roles, :users,
9 :issues, :trackers, :issue_statuses,
10 :custom_fields, :custom_values,
11 :repositories, :changesets
9 12
10 13 def setup
11 14 @controller = SearchController.new
@@ -25,6 +28,15 class SearchControllerTest < Test::Unit::TestCase
25 28 assert assigns(:results).include?(Project.find(1))
26 29 end
27 30
31 def test_search_all_projects
32 get :index, :q => 'recipe subproject commit', :submit => 'Search'
33 assert_response :success
34 assert_template 'index'
35 assert assigns(:results).include?(Issue.find(2))
36 assert assigns(:results).include?(Issue.find(5))
37 assert assigns(:results).include?(Changeset.find(101))
38 end
39
28 40 def test_search_without_searchable_custom_fields
29 41 CustomField.update_all "searchable = #{ActiveRecord::Base.connection.quoted_false}"
30 42
@@ -49,6 +49,9 module Redmine
49 49 raise 'No date column defined defined.'
50 50 end
51 51
52 # Permission needed to search this model
53 searchable_options[:permission] = "view_#{self.name.underscore.pluralize}".to_sym unless searchable_options.has_key?(:permission)
54
52 55 # Should we search custom fields on this model ?
53 56 searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil?
54 57
@@ -62,8 +65,12 module Redmine
62 65 end
63 66
64 67 module ClassMethods
65 def search(tokens, project, options={})
68 # Search the model for the given tokens
69 # projects argument can be either nil (will search all projects), a project or an array of projects
70 def search(tokens, projects, options={})
66 71 tokens = [] << tokens unless tokens.is_a?(Array)
72 projects = [] << projects unless projects.nil? || projects.is_a?(Array)
73
67 74 find_options = {:include => searchable_options[:include]}
68 75 find_options[:limit] = options[:limit] if options[:limit]
69 76 find_options[:order] = "#{searchable_options[:date_column]} " + (options[:before] ? 'DESC' : 'ASC')
@@ -92,12 +99,17 module Redmine
92 99 end
93 100 find_options[:conditions] = [sql, * (tokens * token_clauses.size).sort]
94 101
95 results = with_scope(:find => {:conditions => ["#{searchable_options[:project_key]} = ?", project.id]}) do
102 project_conditions = []
103 project_conditions << (searchable_options[:permission].nil? ? Project.visible_by(User.current) :
104 Project.allowed_to_condition(User.current, searchable_options[:permission]))
105 project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
106
107 results = with_scope(:find => {:conditions => project_conditions.join(' AND ')}) do
96 108 find(:all, find_options)
97 109 end
98 110 if searchable_options[:with] && !options[:titles_only]
99 111 searchable_options[:with].each do |model, assoc|
100 results += model.to_s.camelcase.constantize.search(tokens, project, options).collect {|r| r.send assoc}
112 results += model.to_s.camelcase.constantize.search(tokens, projects, options).collect {|r| r.send assoc}
101 113 end
102 114 results.uniq!
103 115 end
General Comments 0
You need to be logged in to leave comments. Login now