##// END OF EJS Templates
Search engine: display total results count (#906) and count by result type....
Jean-Philippe Lang -
r1664:be2b8a62f4d0
parent child
Show More
@@ -72,15 +72,20 class SearchController < ApplicationController
72 @tokens.slice! 5..-1 if @tokens.size > 5
72 @tokens.slice! 5..-1 if @tokens.size > 5
73 # strings used in sql like statement
73 # strings used in sql like statement
74 like_tokens = @tokens.collect {|w| "%#{w.downcase}%"}
74 like_tokens = @tokens.collect {|w| "%#{w.downcase}%"}
75
75 @results = []
76 @results = []
77 @results_by_type = Hash.new {|h,k| h[k] = 0}
78
76 limit = 10
79 limit = 10
77 @scope.each do |s|
80 @scope.each do |s|
78 @results += s.singularize.camelcase.constantize.search(like_tokens, projects_to_search,
81 r, c = s.singularize.camelcase.constantize.search(like_tokens, projects_to_search,
79 :all_words => @all_words,
82 :all_words => @all_words,
80 :titles_only => @titles_only,
83 :titles_only => @titles_only,
81 :limit => (limit+1),
84 :limit => (limit+1),
82 :offset => offset,
85 :offset => offset,
83 :before => params[:previous].nil?)
86 :before => params[:previous].nil?)
87 @results += r
88 @results_by_type[s] += c
84 end
89 end
85 @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
90 @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
86 if params[:previous].nil?
91 if params[:previous].nil?
@@ -36,6 +36,10 module SearchHelper
36 result
36 result
37 end
37 end
38
38
39 def type_label(t)
40 l("label_#{t.singularize}_plural")
41 end
42
39 def project_select_tag
43 def project_select_tag
40 options = [[l(:label_project_all), 'all']]
44 options = [[l(:label_project_all), 'all']]
41 options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty?
45 options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty?
@@ -43,4 +47,16 module SearchHelper
43 options << [@project.name, ''] unless @project.nil?
47 options << [@project.name, ''] unless @project.nil?
44 select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1
48 select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1
45 end
49 end
50
51 def render_results_by_type(results_by_type)
52 links = []
53 # Sorts types by results count
54 results_by_type.keys.sort {|a, b| results_by_type[b] <=> results_by_type[a]}.each do |t|
55 c = results_by_type[t]
56 next if c == 0
57 text = "#{type_label(t)} (#{c})"
58 links << link_to(text, :q => params[:q], :titles_only => params[:title_only], :all_words => params[:all_words], :scope => params[:scope], t => 1)
59 end
60 ('<ul>' + links.map {|link| content_tag('li', link)}.join(' ') + '</ul>') unless links.empty?
61 end
46 end
62 end
@@ -35,7 +35,10 class Issue < ActiveRecord::Base
35
35
36 acts_as_customizable
36 acts_as_customizable
37 acts_as_watchable
37 acts_as_watchable
38 acts_as_searchable :columns => ['subject', "#{table_name}.description"], :include => :project, :with => {:journal => :issue}
38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39 :include => [:project, :journals],
40 # sort by id so that limited eager loading doesn't break with postgresql
41 :order_column => "#{table_name}.id"
39 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"},
40 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}
41
44
@@ -25,12 +25,6 class Journal < ActiveRecord::Base
25 has_many :details, :class_name => "JournalDetail", :dependent => :delete_all
25 has_many :details, :class_name => "JournalDetail", :dependent => :delete_all
26 attr_accessor :indice
26 attr_accessor :indice
27
27
28 acts_as_searchable :columns => 'notes',
29 :include => {:issue => :project},
30 :project_key => "#{Issue.table_name}.project_id",
31 :date_column => "#{Issue.table_name}.created_on",
32 :permission => :view_issues
33
34 acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" },
28 acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" },
35 :description => :notes,
29 :description => :notes,
36 :author => :user,
30 :author => :user,
@@ -10,7 +10,7
10 </p>
10 </p>
11 <p>
11 <p>
12 <% @object_types.each do |t| %>
12 <% @object_types.each do |t| %>
13 <label><%= check_box_tag t, 1, @scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label>
13 <label><%= check_box_tag t, 1, @scope.include?(t) %> <%= type_label(t) %></label>
14 <% end %>
14 <% end %>
15 </p>
15 </p>
16
16
@@ -19,12 +19,16
19 </div>
19 </div>
20
20
21 <% if @results %>
21 <% if @results %>
22 <h3><%= l(:label_result_plural) %></h3>
22 <div id="search-results-counts">
23 <%= render_results_by_type(@results_by_type) unless @scope.size == 1 %>
24 </div>
25
26 <h3><%= l(:label_result_plural) %> (<%= @results_by_type.values.sum %>)</h3>
23 <dl id="search-results">
27 <dl id="search-results">
24 <% @results.each do |e| %>
28 <% @results.each do |e| %>
25 <dt class="<%= e.event_type %>"><%= 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 %></dt>
29 <dt class="<%= e.event_type %>"><%= 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 %></dt>
26 <dd><span class="description"><%= highlight_tokens(e.event_description, @tokens) %></span>
30 <dd><span class="description"><%= highlight_tokens(e.event_description, @tokens) %></span>
27 <span class="author"><%= format_time(e.event_datetime) %></span><dd>
31 <span class="author"><%= format_time(e.event_datetime) %></span></dd>
28 <% end %>
32 <% end %>
29 </dl>
33 </dl>
30 <% end %>
34 <% end %>
@@ -185,9 +185,13 div#activity dt.me .time { border-bottom: 1px solid #999; }
185 div#activity dt .time { color: #777; font-size: 80%; }
185 div#activity dt .time { color: #777; font-size: 80%; }
186 div#activity dd .description, #search-results dd .description { font-style: italic; }
186 div#activity dd .description, #search-results dd .description { font-style: italic; }
187 div#activity span.project:after, #search-results span.project:after { content: " -"; }
187 div#activity span.project:after, #search-results span.project:after { content: " -"; }
188 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px;}
189 div#activity dd span.description, #search-results dd span.description { display:block; }
188 div#activity dd span.description, #search-results dd span.description { display:block; }
190
189
190 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
191 div#search-results-counts {float:right;}
192 div#search-results-counts ul { margin-top: 0.5em; }
193 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
194
191 dt.issue { background-image: url(../images/ticket.png); }
195 dt.issue { background-image: url(../images/ticket.png); }
192 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
196 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
193 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
197 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
@@ -32,9 +32,17 class SearchControllerTest < Test::Unit::TestCase
32 get :index, :q => 'recipe subproject commit', :submit => 'Search'
32 get :index, :q => 'recipe subproject commit', :submit => 'Search'
33 assert_response :success
33 assert_response :success
34 assert_template 'index'
34 assert_template 'index'
35
35 assert assigns(:results).include?(Issue.find(2))
36 assert assigns(:results).include?(Issue.find(2))
36 assert assigns(:results).include?(Issue.find(5))
37 assert assigns(:results).include?(Issue.find(5))
37 assert assigns(:results).include?(Changeset.find(101))
38 assert assigns(:results).include?(Changeset.find(101))
39 assert_tag :dt, :attributes => { :class => /issue/ },
40 :child => { :tag => 'a', :content => /Add ingredients categories/ },
41 :sibling => { :tag => 'dd', :content => /should be classified by categories/ }
42
43 assert assigns(:results_by_type).is_a?(Hash)
44 assert_equal 4, assigns(:results_by_type)['changesets']
45 assert_tag :a, :content => 'Changesets (4)'
38 end
46 end
39
47
40 def test_search_project_and_subprojects
48 def test_search_project_and_subprojects
@@ -41,24 +41,24 class SearchTest < Test::Unit::TestCase
41 def test_search_by_anonymous
41 def test_search_by_anonymous
42 User.current = nil
42 User.current = nil
43
43
44 r = Issue.search(@issue_keyword)
44 r = Issue.search(@issue_keyword).first
45 assert r.include?(@issue)
45 assert r.include?(@issue)
46 r = Changeset.search(@changeset_keyword)
46 r = Changeset.search(@changeset_keyword).first
47 assert r.include?(@changeset)
47 assert r.include?(@changeset)
48
48
49 # Removes the :view_changesets permission from Anonymous role
49 # Removes the :view_changesets permission from Anonymous role
50 remove_permission Role.anonymous, :view_changesets
50 remove_permission Role.anonymous, :view_changesets
51
51
52 r = Issue.search(@issue_keyword)
52 r = Issue.search(@issue_keyword).first
53 assert r.include?(@issue)
53 assert r.include?(@issue)
54 r = Changeset.search(@changeset_keyword)
54 r = Changeset.search(@changeset_keyword).first
55 assert !r.include?(@changeset)
55 assert !r.include?(@changeset)
56
56
57 # Make the project private
57 # Make the project private
58 @project.update_attribute :is_public, false
58 @project.update_attribute :is_public, false
59 r = Issue.search(@issue_keyword)
59 r = Issue.search(@issue_keyword).first
60 assert !r.include?(@issue)
60 assert !r.include?(@issue)
61 r = Changeset.search(@changeset_keyword)
61 r = Changeset.search(@changeset_keyword).first
62 assert !r.include?(@changeset)
62 assert !r.include?(@changeset)
63 end
63 end
64
64
@@ -66,24 +66,24 class SearchTest < Test::Unit::TestCase
66 User.current = User.find_by_login('rhill')
66 User.current = User.find_by_login('rhill')
67 assert User.current.memberships.empty?
67 assert User.current.memberships.empty?
68
68
69 r = Issue.search(@issue_keyword)
69 r = Issue.search(@issue_keyword).first
70 assert r.include?(@issue)
70 assert r.include?(@issue)
71 r = Changeset.search(@changeset_keyword)
71 r = Changeset.search(@changeset_keyword).first
72 assert r.include?(@changeset)
72 assert r.include?(@changeset)
73
73
74 # Removes the :view_changesets permission from Non member role
74 # Removes the :view_changesets permission from Non member role
75 remove_permission Role.non_member, :view_changesets
75 remove_permission Role.non_member, :view_changesets
76
76
77 r = Issue.search(@issue_keyword)
77 r = Issue.search(@issue_keyword).first
78 assert r.include?(@issue)
78 assert r.include?(@issue)
79 r = Changeset.search(@changeset_keyword)
79 r = Changeset.search(@changeset_keyword).first
80 assert !r.include?(@changeset)
80 assert !r.include?(@changeset)
81
81
82 # Make the project private
82 # Make the project private
83 @project.update_attribute :is_public, false
83 @project.update_attribute :is_public, false
84 r = Issue.search(@issue_keyword)
84 r = Issue.search(@issue_keyword).first
85 assert !r.include?(@issue)
85 assert !r.include?(@issue)
86 r = Changeset.search(@changeset_keyword)
86 r = Changeset.search(@changeset_keyword).first
87 assert !r.include?(@changeset)
87 assert !r.include?(@changeset)
88 end
88 end
89
89
@@ -91,16 +91,16 class SearchTest < Test::Unit::TestCase
91 User.current = User.find_by_login('jsmith')
91 User.current = User.find_by_login('jsmith')
92 assert User.current.projects.include?(@project)
92 assert User.current.projects.include?(@project)
93
93
94 r = Issue.search(@issue_keyword)
94 r = Issue.search(@issue_keyword).first
95 assert r.include?(@issue)
95 assert r.include?(@issue)
96 r = Changeset.search(@changeset_keyword)
96 r = Changeset.search(@changeset_keyword).first
97 assert r.include?(@changeset)
97 assert r.include?(@changeset)
98
98
99 # Make the project private
99 # Make the project private
100 @project.update_attribute :is_public, false
100 @project.update_attribute :is_public, false
101 r = Issue.search(@issue_keyword)
101 r = Issue.search(@issue_keyword).first
102 assert r.include?(@issue)
102 assert r.include?(@issue)
103 r = Changeset.search(@changeset_keyword)
103 r = Changeset.search(@changeset_keyword).first
104 assert r.include?(@changeset)
104 assert r.include?(@changeset)
105 end
105 end
106
106
@@ -112,19 +112,28 class SearchTest < Test::Unit::TestCase
112 User.current = User.find_by_login('jsmith')
112 User.current = User.find_by_login('jsmith')
113 assert User.current.projects.include?(@project)
113 assert User.current.projects.include?(@project)
114
114
115 r = Issue.search(@issue_keyword)
115 r = Issue.search(@issue_keyword).first
116 assert r.include?(@issue)
116 assert r.include?(@issue)
117 r = Changeset.search(@changeset_keyword)
117 r = Changeset.search(@changeset_keyword).first
118 assert !r.include?(@changeset)
118 assert !r.include?(@changeset)
119
119
120 # Make the project private
120 # Make the project private
121 @project.update_attribute :is_public, false
121 @project.update_attribute :is_public, false
122 r = Issue.search(@issue_keyword)
122 r = Issue.search(@issue_keyword).first
123 assert r.include?(@issue)
123 assert r.include?(@issue)
124 r = Changeset.search(@changeset_keyword)
124 r = Changeset.search(@changeset_keyword).first
125 assert !r.include?(@changeset)
125 assert !r.include?(@changeset)
126 end
126 end
127
127
128 def test_search_issue_with_multiple_hits_in_journals
129 i = Issue.find(1)
130 assert_equal 2, i.journals.count(:all, :conditions => "notes LIKE '%notes%'")
131
132 r = Issue.search('%notes%').first
133 assert_equal 1, r.size
134 assert_equal i, r.first
135 end
136
128 private
137 private
129
138
130 def remove_permission(role, permission)
139 def remove_permission(role, permission)
@@ -23,6 +23,12 module Redmine
23 end
23 end
24
24
25 module ClassMethods
25 module ClassMethods
26 # Options:
27 # * :columns - a column or an array of columns to search
28 # * :project_key - project foreign key (default to project_id)
29 # * :date_column - name of the datetime column (default to created_on)
30 # * :sort_order - name of the column used to sort results (default to :date_column or created_on)
31 # * :permission - permission required to search the model (default to :view_"objects")
26 def acts_as_searchable(options = {})
32 def acts_as_searchable(options = {})
27 return if self.included_modules.include?(Redmine::Acts::Searchable::InstanceMethods)
33 return if self.included_modules.include?(Redmine::Acts::Searchable::InstanceMethods)
28
34
@@ -49,6 +55,8 module Redmine
49 raise 'No date column defined defined.'
55 raise 'No date column defined defined.'
50 end
56 end
51
57
58 searchable_options[:order_column] ||= searchable_options[:date_column]
59
52 # Permission needed to search this model
60 # Permission needed to search this model
53 searchable_options[:permission] = "view_#{self.name.underscore.pluralize}".to_sym unless searchable_options.has_key?(:permission)
61 searchable_options[:permission] = "view_#{self.name.underscore.pluralize}".to_sym unless searchable_options.has_key?(:permission)
54
62
@@ -65,15 +73,22 module Redmine
65 end
73 end
66
74
67 module ClassMethods
75 module ClassMethods
68 # Search the model for the given tokens
76 # Searches the model for the given tokens
69 # projects argument can be either nil (will search all projects), a project or an array of projects
77 # projects argument can be either nil (will search all projects), a project or an array of projects
78 # Returns the results and the results count
70 def search(tokens, projects=nil, options={})
79 def search(tokens, projects=nil, options={})
71 tokens = [] << tokens unless tokens.is_a?(Array)
80 tokens = [] << tokens unless tokens.is_a?(Array)
72 projects = [] << projects unless projects.nil? || projects.is_a?(Array)
81 projects = [] << projects unless projects.nil? || projects.is_a?(Array)
73
82
74 find_options = {:include => searchable_options[:include]}
83 find_options = {:include => searchable_options[:include]}
75 find_options[:limit] = options[:limit] if options[:limit]
84 find_options[:order] = "#{searchable_options[:order_column]} " + (options[:before] ? 'DESC' : 'ASC')
76 find_options[:order] = "#{searchable_options[:date_column]} " + (options[:before] ? 'DESC' : 'ASC')
85
86 limit_options = {}
87 limit_options[:limit] = options[:limit] if options[:limit]
88 if options[:offset]
89 limit_options[:conditions] = "(#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')"
90 end
91
77 columns = searchable_options[:columns]
92 columns = searchable_options[:columns]
78 columns.slice!(1..-1) if options[:titles_only]
93 columns.slice!(1..-1) if options[:titles_only]
79
94
@@ -94,9 +109,6 module Redmine
94
109
95 sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
110 sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
96
111
97 if options[:offset]
98 sql = "(#{sql}) AND (#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')"
99 end
100 find_options[:conditions] = [sql, * (tokens * token_clauses.size).sort]
112 find_options[:conditions] = [sql, * (tokens * token_clauses.size).sort]
101
113
102 project_conditions = []
114 project_conditions = []
@@ -104,16 +116,16 module Redmine
104 Project.allowed_to_condition(User.current, searchable_options[:permission]))
116 Project.allowed_to_condition(User.current, searchable_options[:permission]))
105 project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
117 project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
106
118
107 results = with_scope(:find => {:conditions => project_conditions.join(' AND ')}) do
119 results = []
108 find(:all, find_options)
120 results_count = 0
109 end
121
110 if searchable_options[:with] && !options[:titles_only]
122 with_scope(:find => {:conditions => project_conditions.join(' AND ')}) do
111 searchable_options[:with].each do |model, assoc|
123 with_scope(:find => find_options) do
112 results += model.to_s.camelcase.constantize.search(tokens, projects, options).collect {|r| r.send assoc}
124 results_count = count(:all)
125 results = find(:all, limit_options)
113 end
126 end
114 results.uniq!
115 end
127 end
116 results
128 [results, results_count]
117 end
129 end
118 end
130 end
119 end
131 end
General Comments 0
You need to be logged in to leave comments. Login now