@@ -46,13 +46,8 class Issue < ActiveRecord::Base | |||
|
46 | 46 | acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed |
|
47 | 47 | acts_as_customizable |
|
48 | 48 | acts_as_watchable |
|
49 |
acts_as_searchable :columns => ['subject', "#{table_name}.description" |
|
|
50 |
:preload => [:project, :status, :tracker] |
|
|
51 | :scope => lambda { joins(:project). | |
|
52 | joins("LEFT OUTER JOIN #{Journal.table_name} ON #{Journal.table_name}.journalized_type='Issue'" + | |
|
53 | " AND #{Journal.table_name}.journalized_id = #{Issue.table_name}.id" + | |
|
54 | " AND (#{Journal.table_name}.private_notes = #{connection.quoted_false}" + | |
|
55 | " OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))") } | |
|
49 | acts_as_searchable :columns => ['subject', "#{table_name}.description"], | |
|
50 | :preload => [:project, :status, :tracker] | |
|
56 | 51 | |
|
57 | 52 | acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"}, |
|
58 | 53 | :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}, |
@@ -48,8 +48,9 module Redmine | |||
|
48 | 48 | searchable_options[:project_key] ||= "#{table_name}.project_id" |
|
49 | 49 | searchable_options[:date_column] ||= :created_on |
|
50 | 50 | |
|
51 |
# Should we search |
|
|
52 |
searchable_options[:search_custom_fields] = |
|
|
51 | # Should we search additional associations on this model ? | |
|
52 | searchable_options[:search_custom_fields] = reflect_on_association(:custom_values).present? | |
|
53 | searchable_options[:search_journals] = reflect_on_association(:journals).present? | |
|
53 | 54 | |
|
54 | 55 | send :include, Redmine::Acts::Searchable::InstanceMethods |
|
55 | 56 | end |
@@ -75,11 +76,6 module Redmine | |||
|
75 | 76 | # Issue.search_result_ranks_and_ids("foo") |
|
76 | 77 | # # => [[1419595329, 69], [1419595622, 123]] |
|
77 | 78 | def search_result_ranks_and_ids(tokens, user=User.current, projects=nil, options={}) |
|
78 | if projects.is_a?(Array) && projects.empty? | |
|
79 | # no results | |
|
80 | return [] | |
|
81 | end | |
|
82 | ||
|
83 | 79 | tokens = [] << tokens unless tokens.is_a?(Array) |
|
84 | 80 | projects = [] << projects if projects.is_a?(Project) |
|
85 | 81 | |
@@ -87,36 +83,63 module Redmine | |||
|
87 | 83 | columns = columns[0..0] if options[:titles_only] |
|
88 | 84 | |
|
89 | 85 | token_clauses = columns.collect {|column| "(#{search_token_match_statement(column)})"} |
|
86 | sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') | |
|
87 | tokens_conditions = [sql, * (tokens.collect {|w| "%#{w}%"} * token_clauses.size).sort] | |
|
88 | ||
|
89 | r = fetch_ranks_and_ids(search_scope(user, projects).where(tokens_conditions), options[:limit]) | |
|
90 | sort_and_limit_results = false | |
|
90 | 91 | |
|
91 | 92 | if !options[:titles_only] && searchable_options[:search_custom_fields] |
|
92 | searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true) | |
|
93 | searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true).to_a | |
|
94 | ||
|
95 | if searchable_custom_fields.any? | |
|
93 | 96 | fields_by_visibility = searchable_custom_fields.group_by {|field| |
|
94 |
field.visibility_by_project_condition(searchable_options[:project_key], user, " |
|
|
97 | field.visibility_by_project_condition(searchable_options[:project_key], user, "#{CustomValue.table_name}.custom_field_id") | |
|
95 | 98 | } |
|
96 | # only 1 subquery for all custom fields with the same visibility statement | |
|
99 | clauses = [] | |
|
97 | 100 | fields_by_visibility.each do |visibility, fields| |
|
98 | ids = fields.map(&:id).join(',') | |
|
99 | sql = "#{table_name}.id IN (SELECT cfs.customized_id FROM #{CustomValue.table_name} cfs" + | |
|
100 | " WHERE cfs.customized_type='#{self.name}' AND cfs.customized_id=#{table_name}.id" + | |
|
101 | " AND cfs.custom_field_id IN (#{ids})" + | |
|
102 | " AND #{search_token_match_statement('cfs.value')}" + | |
|
103 | " AND #{visibility})" | |
|
104 | token_clauses << sql | |
|
105 | end | |
|
101 | clauses << "(#{CustomValue.table_name}.custom_field_id IN (#{fields.map(&:id).join(',')}) AND (#{visibility}))" | |
|
106 | 102 | end |
|
103 | visibility = clauses.join(' OR ') | |
|
107 | 104 | |
|
108 |
sql = ([ |
|
|
105 | sql = ([search_token_match_statement("#{CustomValue.table_name}.value")] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') | |
|
106 | tokens_conditions = [sql, * tokens.collect {|w| "%#{w}%"}.sort] | |
|
109 | 107 | |
|
110 | tokens_conditions = [sql, * (tokens.collect {|w| "%#{w}%"} * token_clauses.size).sort] | |
|
108 | r |= fetch_ranks_and_ids( | |
|
109 | search_scope(user, projects). | |
|
110 | joins(:custom_values). | |
|
111 | where(visibility). | |
|
112 | where(tokens_conditions), | |
|
113 | options[:limit] | |
|
114 | ) | |
|
115 | ||
|
116 | sort_and_limit_results = true | |
|
117 | end | |
|
118 | end | |
|
111 | 119 | |
|
120 | if !options[:titles_only] && searchable_options[:search_journals] | |
|
121 | sql = ([search_token_match_statement("#{Journal.table_name}.notes")] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ') | |
|
122 | tokens_conditions = [sql, * tokens.collect {|w| "%#{w}%"}.sort] | |
|
123 | ||
|
124 | r |= fetch_ranks_and_ids( | |
|
112 | 125 | search_scope(user, projects). |
|
113 | reorder(searchable_options[:date_column] => :desc, :id => :desc). | |
|
114 | where(tokens_conditions). | |
|
115 | limit(options[:limit]). | |
|
116 |
|
|
|
117 | pluck(searchable_options[:date_column], :id). | |
|
118 | # converts timestamps to integers for faster sort | |
|
119 | map {|timestamp, id| [timestamp.to_i, id]} | |
|
126 | joins(:journals). | |
|
127 | where("#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(user, :view_private_notes)})", false). | |
|
128 | where(tokens_conditions), | |
|
129 | options[:limit] | |
|
130 | ) | |
|
131 | ||
|
132 | sort_and_limit_results = true | |
|
133 | end | |
|
134 | ||
|
135 | if sort_and_limit_results | |
|
136 | r = r.sort.reverse | |
|
137 | if options[:limit] && r.size > options[:limit] | |
|
138 | r = r[0, options[:limit]] | |
|
139 | end | |
|
140 | end | |
|
141 | ||
|
142 | r | |
|
120 | 143 | end |
|
121 | 144 | |
|
122 | 145 | def search_token_match_statement(column, value='?') |
@@ -129,8 +152,24 module Redmine | |||
|
129 | 152 | end |
|
130 | 153 | private :search_token_match_statement |
|
131 | 154 | |
|
155 | def fetch_ranks_and_ids(scope, limit) | |
|
156 | scope. | |
|
157 | reorder(searchable_options[:date_column] => :desc, :id => :desc). | |
|
158 | limit(limit). | |
|
159 | uniq. | |
|
160 | pluck(searchable_options[:date_column], :id). | |
|
161 | # converts timestamps to integers for faster sort | |
|
162 | map {|timestamp, id| [timestamp.to_i, id]} | |
|
163 | end | |
|
164 | private :fetch_ranks_and_ids | |
|
165 | ||
|
132 | 166 | # Returns the search scope for user and projects |
|
133 | 167 | def search_scope(user, projects) |
|
168 | if projects.is_a?(Array) && projects.empty? | |
|
169 | # no results | |
|
170 | return none | |
|
171 | end | |
|
172 | ||
|
134 | 173 | scope = (searchable_options[:scope] || self) |
|
135 | 174 | if scope.is_a? Proc |
|
136 | 175 | scope = scope.call |
General Comments 0
You need to be logged in to leave comments.
Login now