@@ -72,15 +72,20 class SearchController < ApplicationController | |||
|
72 | 72 | @tokens.slice! 5..-1 if @tokens.size > 5 |
|
73 | 73 | # strings used in sql like statement |
|
74 | 74 | like_tokens = @tokens.collect {|w| "%#{w.downcase}%"} |
|
75 | ||
|
75 | 76 | @results = [] |
|
77 | @results_by_type = Hash.new {|h,k| h[k] = 0} | |
|
78 | ||
|
76 | 79 | limit = 10 |
|
77 | 80 | @scope.each do |s| |
|
78 |
|
|
|
81 | r, c = s.singularize.camelcase.constantize.search(like_tokens, projects_to_search, | |
|
79 | 82 | :all_words => @all_words, |
|
80 | 83 | :titles_only => @titles_only, |
|
81 | 84 | :limit => (limit+1), |
|
82 | 85 | :offset => offset, |
|
83 | 86 | :before => params[:previous].nil?) |
|
87 | @results += r | |
|
88 | @results_by_type[s] += c | |
|
84 | 89 | end |
|
85 | 90 | @results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime} |
|
86 | 91 | if params[:previous].nil? |
@@ -36,6 +36,10 module SearchHelper | |||
|
36 | 36 | result |
|
37 | 37 | end |
|
38 | 38 | |
|
39 | def type_label(t) | |
|
40 | l("label_#{t.singularize}_plural") | |
|
41 | end | |
|
42 | ||
|
39 | 43 | def project_select_tag |
|
40 | 44 | options = [[l(:label_project_all), 'all']] |
|
41 | 45 | options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty? |
@@ -43,4 +47,16 module SearchHelper | |||
|
43 | 47 | options << [@project.name, ''] unless @project.nil? |
|
44 | 48 | select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1 |
|
45 | 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 | 62 | end |
@@ -35,7 +35,10 class Issue < ActiveRecord::Base | |||
|
35 | 35 | |
|
36 | 36 | acts_as_customizable |
|
37 | 37 | acts_as_watchable |
|
38 |
acts_as_searchable :columns => ['subject', "#{table_name}.description"], |
|
|
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 | 42 | acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id}: #{o.subject}"}, |
|
40 | 43 | :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}} |
|
41 | 44 |
@@ -25,12 +25,6 class Journal < ActiveRecord::Base | |||
|
25 | 25 | has_many :details, :class_name => "JournalDetail", :dependent => :delete_all |
|
26 | 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 | 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 | 29 | :description => :notes, |
|
36 | 30 | :author => :user, |
@@ -10,7 +10,7 | |||
|
10 | 10 | </p> |
|
11 | 11 | <p> |
|
12 | 12 | <% @object_types.each do |t| %> |
|
13 |
<label><%= check_box_tag t, 1, @scope.include?(t) %> <%= l( |
|
|
13 | <label><%= check_box_tag t, 1, @scope.include?(t) %> <%= type_label(t) %></label> | |
|
14 | 14 | <% end %> |
|
15 | 15 | </p> |
|
16 | 16 | |
@@ -19,12 +19,16 | |||
|
19 | 19 | </div> |
|
20 | 20 | |
|
21 | 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 | 27 | <dl id="search-results"> |
|
24 | 28 | <% @results.each do |e| %> |
|
25 | 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 | 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 | 32 | <% end %> |
|
29 | 33 | </dl> |
|
30 | 34 | <% end %> |
@@ -185,9 +185,13 div#activity dt.me .time { border-bottom: 1px solid #999; } | |||
|
185 | 185 | div#activity dt .time { color: #777; font-size: 80%; } |
|
186 | 186 | div#activity dd .description, #search-results dd .description { font-style: italic; } |
|
187 | 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 | 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 | 195 | dt.issue { background-image: url(../images/ticket.png); } |
|
192 | 196 | dt.issue-edit { background-image: url(../images/ticket_edit.png); } |
|
193 | 197 | dt.issue-closed { background-image: url(../images/ticket_checked.png); } |
@@ -32,9 +32,17 class SearchControllerTest < Test::Unit::TestCase | |||
|
32 | 32 | get :index, :q => 'recipe subproject commit', :submit => 'Search' |
|
33 | 33 | assert_response :success |
|
34 | 34 | assert_template 'index' |
|
35 | ||
|
35 | 36 | assert assigns(:results).include?(Issue.find(2)) |
|
36 | 37 | assert assigns(:results).include?(Issue.find(5)) |
|
37 | 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 | 46 | end |
|
39 | 47 | |
|
40 | 48 | def test_search_project_and_subprojects |
@@ -41,24 +41,24 class SearchTest < Test::Unit::TestCase | |||
|
41 | 41 | def test_search_by_anonymous |
|
42 | 42 | User.current = nil |
|
43 | 43 | |
|
44 | r = Issue.search(@issue_keyword) | |
|
44 | r = Issue.search(@issue_keyword).first | |
|
45 | 45 | assert r.include?(@issue) |
|
46 | r = Changeset.search(@changeset_keyword) | |
|
46 | r = Changeset.search(@changeset_keyword).first | |
|
47 | 47 | assert r.include?(@changeset) |
|
48 | 48 | |
|
49 | 49 | # Removes the :view_changesets permission from Anonymous role |
|
50 | 50 | remove_permission Role.anonymous, :view_changesets |
|
51 | 51 | |
|
52 | r = Issue.search(@issue_keyword) | |
|
52 | r = Issue.search(@issue_keyword).first | |
|
53 | 53 | assert r.include?(@issue) |
|
54 | r = Changeset.search(@changeset_keyword) | |
|
54 | r = Changeset.search(@changeset_keyword).first | |
|
55 | 55 | assert !r.include?(@changeset) |
|
56 | 56 | |
|
57 | 57 | # Make the project private |
|
58 | 58 | @project.update_attribute :is_public, false |
|
59 | r = Issue.search(@issue_keyword) | |
|
59 | r = Issue.search(@issue_keyword).first | |
|
60 | 60 | assert !r.include?(@issue) |
|
61 | r = Changeset.search(@changeset_keyword) | |
|
61 | r = Changeset.search(@changeset_keyword).first | |
|
62 | 62 | assert !r.include?(@changeset) |
|
63 | 63 | end |
|
64 | 64 | |
@@ -66,24 +66,24 class SearchTest < Test::Unit::TestCase | |||
|
66 | 66 | User.current = User.find_by_login('rhill') |
|
67 | 67 | assert User.current.memberships.empty? |
|
68 | 68 | |
|
69 | r = Issue.search(@issue_keyword) | |
|
69 | r = Issue.search(@issue_keyword).first | |
|
70 | 70 | assert r.include?(@issue) |
|
71 | r = Changeset.search(@changeset_keyword) | |
|
71 | r = Changeset.search(@changeset_keyword).first | |
|
72 | 72 | assert r.include?(@changeset) |
|
73 | 73 | |
|
74 | 74 | # Removes the :view_changesets permission from Non member role |
|
75 | 75 | remove_permission Role.non_member, :view_changesets |
|
76 | 76 | |
|
77 | r = Issue.search(@issue_keyword) | |
|
77 | r = Issue.search(@issue_keyword).first | |
|
78 | 78 | assert r.include?(@issue) |
|
79 | r = Changeset.search(@changeset_keyword) | |
|
79 | r = Changeset.search(@changeset_keyword).first | |
|
80 | 80 | assert !r.include?(@changeset) |
|
81 | 81 | |
|
82 | 82 | # Make the project private |
|
83 | 83 | @project.update_attribute :is_public, false |
|
84 | r = Issue.search(@issue_keyword) | |
|
84 | r = Issue.search(@issue_keyword).first | |
|
85 | 85 | assert !r.include?(@issue) |
|
86 | r = Changeset.search(@changeset_keyword) | |
|
86 | r = Changeset.search(@changeset_keyword).first | |
|
87 | 87 | assert !r.include?(@changeset) |
|
88 | 88 | end |
|
89 | 89 | |
@@ -91,16 +91,16 class SearchTest < Test::Unit::TestCase | |||
|
91 | 91 | User.current = User.find_by_login('jsmith') |
|
92 | 92 | assert User.current.projects.include?(@project) |
|
93 | 93 | |
|
94 | r = Issue.search(@issue_keyword) | |
|
94 | r = Issue.search(@issue_keyword).first | |
|
95 | 95 | assert r.include?(@issue) |
|
96 | r = Changeset.search(@changeset_keyword) | |
|
96 | r = Changeset.search(@changeset_keyword).first | |
|
97 | 97 | assert r.include?(@changeset) |
|
98 | 98 | |
|
99 | 99 | # Make the project private |
|
100 | 100 | @project.update_attribute :is_public, false |
|
101 | r = Issue.search(@issue_keyword) | |
|
101 | r = Issue.search(@issue_keyword).first | |
|
102 | 102 | assert r.include?(@issue) |
|
103 | r = Changeset.search(@changeset_keyword) | |
|
103 | r = Changeset.search(@changeset_keyword).first | |
|
104 | 104 | assert r.include?(@changeset) |
|
105 | 105 | end |
|
106 | 106 | |
@@ -112,19 +112,28 class SearchTest < Test::Unit::TestCase | |||
|
112 | 112 | User.current = User.find_by_login('jsmith') |
|
113 | 113 | assert User.current.projects.include?(@project) |
|
114 | 114 | |
|
115 | r = Issue.search(@issue_keyword) | |
|
115 | r = Issue.search(@issue_keyword).first | |
|
116 | 116 | assert r.include?(@issue) |
|
117 | r = Changeset.search(@changeset_keyword) | |
|
117 | r = Changeset.search(@changeset_keyword).first | |
|
118 | 118 | assert !r.include?(@changeset) |
|
119 | 119 | |
|
120 | 120 | # Make the project private |
|
121 | 121 | @project.update_attribute :is_public, false |
|
122 | r = Issue.search(@issue_keyword) | |
|
122 | r = Issue.search(@issue_keyword).first | |
|
123 | 123 | assert r.include?(@issue) |
|
124 | r = Changeset.search(@changeset_keyword) | |
|
124 | r = Changeset.search(@changeset_keyword).first | |
|
125 | 125 | assert !r.include?(@changeset) |
|
126 | 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 | 137 | private |
|
129 | 138 | |
|
130 | 139 | def remove_permission(role, permission) |
@@ -23,6 +23,12 module Redmine | |||
|
23 | 23 | end |
|
24 | 24 | |
|
25 | 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 | 32 | def acts_as_searchable(options = {}) |
|
27 | 33 | return if self.included_modules.include?(Redmine::Acts::Searchable::InstanceMethods) |
|
28 | 34 | |
@@ -49,6 +55,8 module Redmine | |||
|
49 | 55 | raise 'No date column defined defined.' |
|
50 | 56 | end |
|
51 | 57 | |
|
58 | searchable_options[:order_column] ||= searchable_options[:date_column] | |
|
59 | ||
|
52 | 60 | # Permission needed to search this model |
|
53 | 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 | 73 | end |
|
66 | 74 | |
|
67 | 75 | module ClassMethods |
|
68 | # Search the model for the given tokens | |
|
76 | # Searches the model for the given tokens | |
|
69 | 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 | 79 | def search(tokens, projects=nil, options={}) |
|
71 | 80 | tokens = [] << tokens unless tokens.is_a?(Array) |
|
72 | 81 | projects = [] << projects unless projects.nil? || projects.is_a?(Array) |
|
73 | 82 | |
|
74 | 83 | find_options = {:include => searchable_options[:include]} |
|
75 | find_options[:limit] = options[:limit] if options[:limit] | |
|
76 | find_options[:order] = "#{searchable_options[:date_column]} " + (options[:before] ? 'DESC' : 'ASC') | |
|
84 | find_options[:order] = "#{searchable_options[:order_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 | 92 | columns = searchable_options[:columns] |
|
78 | 93 | columns.slice!(1..-1) if options[:titles_only] |
|
79 | 94 | |
@@ -94,9 +109,6 module Redmine | |||
|
94 | 109 | |
|
95 | 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 | 112 | find_options[:conditions] = [sql, * (tokens * token_clauses.size).sort] |
|
101 | 113 | |
|
102 | 114 | project_conditions = [] |
@@ -104,16 +116,16 module Redmine | |||
|
104 | 116 | Project.allowed_to_condition(User.current, searchable_options[:permission])) |
|
105 | 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 | |
|
108 | find(:all, find_options) | |
|
109 |
|
|
|
110 | if searchable_options[:with] && !options[:titles_only] | |
|
111 | searchable_options[:with].each do |model, assoc| | |
|
112 | results += model.to_s.camelcase.constantize.search(tokens, projects, options).collect {|r| r.send assoc} | |
|
119 | results = [] | |
|
120 | results_count = 0 | |
|
121 | ||
|
122 | with_scope(:find => {:conditions => project_conditions.join(' AND ')}) do | |
|
123 | with_scope(:find => find_options) do | |
|
124 | results_count = count(:all) | |
|
125 | results = find(:all, limit_options) | |
|
113 | 126 | end |
|
114 | results.uniq! | |
|
115 | 127 | end |
|
116 | results | |
|
128 | [results, results_count] | |
|
117 | 129 | end |
|
118 | 130 | end |
|
119 | 131 | end |
General Comments 0
You need to be logged in to leave comments.
Login now