##// END OF EJS Templates
Reduces the number of subqueries when searching with many custom fields set as searchable (#15781)....
Jean-Philippe Lang -
r12206:77ac3b7891d4
parent child
Show More
@@ -1,280 +1,281
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class CustomField < ActiveRecord::Base
18 class CustomField < ActiveRecord::Base
19 include Redmine::SubclassFactory
19 include Redmine::SubclassFactory
20
20
21 has_many :custom_values, :dependent => :delete_all
21 has_many :custom_values, :dependent => :delete_all
22 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
22 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
23 acts_as_list :scope => 'type = \'#{self.class}\''
23 acts_as_list :scope => 'type = \'#{self.class}\''
24 serialize :possible_values
24 serialize :possible_values
25 store :format_store
25 store :format_store
26
26
27 validates_presence_of :name, :field_format
27 validates_presence_of :name, :field_format
28 validates_uniqueness_of :name, :scope => :type
28 validates_uniqueness_of :name, :scope => :type
29 validates_length_of :name, :maximum => 30
29 validates_length_of :name, :maximum => 30
30 validates_inclusion_of :field_format, :in => Proc.new { Redmine::FieldFormat.available_formats }
30 validates_inclusion_of :field_format, :in => Proc.new { Redmine::FieldFormat.available_formats }
31 validate :validate_custom_field
31 validate :validate_custom_field
32
32
33 before_validation :set_searchable
33 before_validation :set_searchable
34 before_save do |field|
34 before_save do |field|
35 field.format.before_custom_field_save(field)
35 field.format.before_custom_field_save(field)
36 end
36 end
37 after_save :handle_multiplicity_change
37 after_save :handle_multiplicity_change
38 after_save do |field|
38 after_save do |field|
39 if field.visible_changed? && field.visible
39 if field.visible_changed? && field.visible
40 field.roles.clear
40 field.roles.clear
41 end
41 end
42 end
42 end
43
43
44 scope :sorted, lambda { order("#{table_name}.position ASC") }
44 scope :sorted, lambda { order("#{table_name}.position ASC") }
45 scope :visible, lambda {|*args|
45 scope :visible, lambda {|*args|
46 user = args.shift || User.current
46 user = args.shift || User.current
47 if user.admin?
47 if user.admin?
48 # nop
48 # nop
49 elsif user.memberships.any?
49 elsif user.memberships.any?
50 where("#{table_name}.visible = ? OR #{table_name}.id IN (SELECT DISTINCT cfr.custom_field_id FROM #{Member.table_name} m" +
50 where("#{table_name}.visible = ? OR #{table_name}.id IN (SELECT DISTINCT cfr.custom_field_id FROM #{Member.table_name} m" +
51 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
51 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
52 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
52 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
53 " WHERE m.user_id = ?)",
53 " WHERE m.user_id = ?)",
54 true, user.id)
54 true, user.id)
55 else
55 else
56 where(:visible => true)
56 where(:visible => true)
57 end
57 end
58 }
58 }
59
59
60 def visible_by?(project, user=User.current)
60 def visible_by?(project, user=User.current)
61 visible? || user.admin?
61 visible? || user.admin?
62 end
62 end
63
63
64 def format
64 def format
65 @format ||= Redmine::FieldFormat.find(field_format)
65 @format ||= Redmine::FieldFormat.find(field_format)
66 end
66 end
67
67
68 def field_format=(arg)
68 def field_format=(arg)
69 # cannot change format of a saved custom field
69 # cannot change format of a saved custom field
70 if new_record?
70 if new_record?
71 @format = nil
71 @format = nil
72 super
72 super
73 end
73 end
74 end
74 end
75
75
76 def set_searchable
76 def set_searchable
77 # make sure these fields are not searchable
77 # make sure these fields are not searchable
78 self.searchable = false unless format.class.searchable_supported
78 self.searchable = false unless format.class.searchable_supported
79 # make sure only these fields can have multiple values
79 # make sure only these fields can have multiple values
80 self.multiple = false unless format.class.multiple_supported
80 self.multiple = false unless format.class.multiple_supported
81 true
81 true
82 end
82 end
83
83
84 def validate_custom_field
84 def validate_custom_field
85 format.validate_custom_field(self).each do |attribute, message|
85 format.validate_custom_field(self).each do |attribute, message|
86 errors.add attribute, message
86 errors.add attribute, message
87 end
87 end
88
88
89 if regexp.present?
89 if regexp.present?
90 begin
90 begin
91 Regexp.new(regexp)
91 Regexp.new(regexp)
92 rescue
92 rescue
93 errors.add(:regexp, :invalid)
93 errors.add(:regexp, :invalid)
94 end
94 end
95 end
95 end
96
96
97 if default_value.present?
97 if default_value.present?
98 validate_field_value(default_value).each do |message|
98 validate_field_value(default_value).each do |message|
99 errors.add :default_value, message
99 errors.add :default_value, message
100 end
100 end
101 end
101 end
102 end
102 end
103
103
104 def possible_custom_value_options(custom_value)
104 def possible_custom_value_options(custom_value)
105 format.possible_custom_value_options(custom_value)
105 format.possible_custom_value_options(custom_value)
106 end
106 end
107
107
108 def possible_values_options(object=nil)
108 def possible_values_options(object=nil)
109 if object.is_a?(Array)
109 if object.is_a?(Array)
110 object.map {|o| format.possible_values_options(self, o)}.reduce(:&) || []
110 object.map {|o| format.possible_values_options(self, o)}.reduce(:&) || []
111 else
111 else
112 format.possible_values_options(self, object) || []
112 format.possible_values_options(self, object) || []
113 end
113 end
114 end
114 end
115
115
116 def possible_values
116 def possible_values
117 values = super()
117 values = super()
118 if values.is_a?(Array)
118 if values.is_a?(Array)
119 values.each do |value|
119 values.each do |value|
120 value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
120 value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
121 end
121 end
122 values
122 values
123 else
123 else
124 []
124 []
125 end
125 end
126 end
126 end
127
127
128 # Makes possible_values accept a multiline string
128 # Makes possible_values accept a multiline string
129 def possible_values=(arg)
129 def possible_values=(arg)
130 if arg.is_a?(Array)
130 if arg.is_a?(Array)
131 super(arg.compact.collect(&:strip).select {|v| !v.blank?})
131 super(arg.compact.collect(&:strip).select {|v| !v.blank?})
132 else
132 else
133 self.possible_values = arg.to_s.split(/[\n\r]+/)
133 self.possible_values = arg.to_s.split(/[\n\r]+/)
134 end
134 end
135 end
135 end
136
136
137 def cast_value(value)
137 def cast_value(value)
138 format.cast_value(self, value)
138 format.cast_value(self, value)
139 end
139 end
140
140
141 def value_from_keyword(keyword, customized)
141 def value_from_keyword(keyword, customized)
142 possible_values_options = possible_values_options(customized)
142 possible_values_options = possible_values_options(customized)
143 if possible_values_options.present?
143 if possible_values_options.present?
144 keyword = keyword.to_s.downcase
144 keyword = keyword.to_s.downcase
145 if v = possible_values_options.detect {|text, id| text.downcase == keyword}
145 if v = possible_values_options.detect {|text, id| text.downcase == keyword}
146 if v.is_a?(Array)
146 if v.is_a?(Array)
147 v.last
147 v.last
148 else
148 else
149 v
149 v
150 end
150 end
151 end
151 end
152 else
152 else
153 keyword
153 keyword
154 end
154 end
155 end
155 end
156
156
157 # Returns a ORDER BY clause that can used to sort customized
157 # Returns a ORDER BY clause that can used to sort customized
158 # objects by their value of the custom field.
158 # objects by their value of the custom field.
159 # Returns nil if the custom field can not be used for sorting.
159 # Returns nil if the custom field can not be used for sorting.
160 def order_statement
160 def order_statement
161 return nil if multiple?
161 return nil if multiple?
162 format.order_statement(self)
162 format.order_statement(self)
163 end
163 end
164
164
165 # Returns a GROUP BY clause that can used to group by custom value
165 # Returns a GROUP BY clause that can used to group by custom value
166 # Returns nil if the custom field can not be used for grouping.
166 # Returns nil if the custom field can not be used for grouping.
167 def group_statement
167 def group_statement
168 return nil if multiple?
168 return nil if multiple?
169 format.group_statement(self)
169 format.group_statement(self)
170 end
170 end
171
171
172 def join_for_order_statement
172 def join_for_order_statement
173 format.join_for_order_statement(self)
173 format.join_for_order_statement(self)
174 end
174 end
175
175
176 def visibility_by_project_condition(project_key=nil, user=User.current)
176 def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil)
177 if visible? || user.admin?
177 if visible? || user.admin?
178 "1=1"
178 "1=1"
179 elsif user.anonymous?
179 elsif user.anonymous?
180 "1=0"
180 "1=0"
181 else
181 else
182 project_key ||= "#{self.class.customized_class.table_name}.project_id"
182 project_key ||= "#{self.class.customized_class.table_name}.project_id"
183 id_column ||= id
183 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
184 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
184 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
185 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
185 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
186 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
186 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
187 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id_column})"
187 end
188 end
188 end
189 end
189
190
190 def self.visibility_condition
191 def self.visibility_condition
191 if user.admin?
192 if user.admin?
192 "1=1"
193 "1=1"
193 elsif user.anonymous?
194 elsif user.anonymous?
194 "#{table_name}.visible"
195 "#{table_name}.visible"
195 else
196 else
196 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
197 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
197 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
198 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
198 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
199 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
199 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
200 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
200 end
201 end
201 end
202 end
202
203
203 def <=>(field)
204 def <=>(field)
204 position <=> field.position
205 position <=> field.position
205 end
206 end
206
207
207 # Returns the class that values represent
208 # Returns the class that values represent
208 def value_class
209 def value_class
209 format.target_class if format.respond_to?(:target_class)
210 format.target_class if format.respond_to?(:target_class)
210 end
211 end
211
212
212 def self.customized_class
213 def self.customized_class
213 self.name =~ /^(.+)CustomField$/
214 self.name =~ /^(.+)CustomField$/
214 $1.constantize rescue nil
215 $1.constantize rescue nil
215 end
216 end
216
217
217 # to move in project_custom_field
218 # to move in project_custom_field
218 def self.for_all
219 def self.for_all
219 where(:is_for_all => true).order('position').all
220 where(:is_for_all => true).order('position').all
220 end
221 end
221
222
222 def type_name
223 def type_name
223 nil
224 nil
224 end
225 end
225
226
226 # Returns the error messages for the given value
227 # Returns the error messages for the given value
227 # or an empty array if value is a valid value for the custom field
228 # or an empty array if value is a valid value for the custom field
228 def validate_custom_value(custom_value)
229 def validate_custom_value(custom_value)
229 value = custom_value.value
230 value = custom_value.value
230 errs = []
231 errs = []
231 if value.is_a?(Array)
232 if value.is_a?(Array)
232 if !multiple?
233 if !multiple?
233 errs << ::I18n.t('activerecord.errors.messages.invalid')
234 errs << ::I18n.t('activerecord.errors.messages.invalid')
234 end
235 end
235 if is_required? && value.detect(&:present?).nil?
236 if is_required? && value.detect(&:present?).nil?
236 errs << ::I18n.t('activerecord.errors.messages.blank')
237 errs << ::I18n.t('activerecord.errors.messages.blank')
237 end
238 end
238 else
239 else
239 if is_required? && value.blank?
240 if is_required? && value.blank?
240 errs << ::I18n.t('activerecord.errors.messages.blank')
241 errs << ::I18n.t('activerecord.errors.messages.blank')
241 end
242 end
242 end
243 end
243 if custom_value.value.present?
244 if custom_value.value.present?
244 errs += format.validate_custom_value(custom_value)
245 errs += format.validate_custom_value(custom_value)
245 end
246 end
246 errs
247 errs
247 end
248 end
248
249
249 # Returns the error messages for the default custom field value
250 # Returns the error messages for the default custom field value
250 def validate_field_value(value)
251 def validate_field_value(value)
251 validate_custom_value(CustomValue.new(:custom_field => self, :value => value))
252 validate_custom_value(CustomValue.new(:custom_field => self, :value => value))
252 end
253 end
253
254
254 # Returns true if value is a valid value for the custom field
255 # Returns true if value is a valid value for the custom field
255 def valid_field_value?(value)
256 def valid_field_value?(value)
256 validate_field_value(value).empty?
257 validate_field_value(value).empty?
257 end
258 end
258
259
259 def format_in?(*args)
260 def format_in?(*args)
260 args.include?(field_format)
261 args.include?(field_format)
261 end
262 end
262
263
263 protected
264 protected
264
265
265 # Removes multiple values for the custom field after setting the multiple attribute to false
266 # Removes multiple values for the custom field after setting the multiple attribute to false
266 # We kepp the value with the highest id for each customized object
267 # We kepp the value with the highest id for each customized object
267 def handle_multiplicity_change
268 def handle_multiplicity_change
268 if !new_record? && multiple_was && !multiple
269 if !new_record? && multiple_was && !multiple
269 ids = custom_values.
270 ids = custom_values.
270 where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" +
271 where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" +
271 " AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" +
272 " AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" +
272 " AND cve.id > #{CustomValue.table_name}.id)").
273 " AND cve.id > #{CustomValue.table_name}.id)").
273 pluck(:id)
274 pluck(:id)
274
275
275 if ids.any?
276 if ids.any?
276 custom_values.where(:id => ids).delete_all
277 custom_values.where(:id => ids).delete_all
277 end
278 end
278 end
279 end
279 end
280 end
280 end
281 end
@@ -1,44 +1,45
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssueCustomField < CustomField
18 class IssueCustomField < CustomField
19 has_and_belongs_to_many :projects, :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :foreign_key => "custom_field_id"
19 has_and_belongs_to_many :projects, :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :foreign_key => "custom_field_id"
20 has_and_belongs_to_many :trackers, :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :foreign_key => "custom_field_id"
20 has_and_belongs_to_many :trackers, :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :foreign_key => "custom_field_id"
21 has_many :issues, :through => :issue_custom_values
21 has_many :issues, :through => :issue_custom_values
22
22
23 def type_name
23 def type_name
24 :label_issue_plural
24 :label_issue_plural
25 end
25 end
26
26
27 def visible_by?(project, user=User.current)
27 def visible_by?(project, user=User.current)
28 super || (roles & user.roles_for_project(project)).present?
28 super || (roles & user.roles_for_project(project)).present?
29 end
29 end
30
30
31 def visibility_by_project_condition(*args)
31 def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil)
32 sql = super
32 sql = super
33 additional_sql = "#{Issue.table_name}.tracker_id IN (SELECT tracker_id FROM #{table_name_prefix}custom_fields_trackers#{table_name_suffix} WHERE custom_field_id = #{id})"
33 id_column ||= id
34 unless is_for_all?
34 tracker_condition = "#{Issue.table_name}.tracker_id IN (SELECT tracker_id FROM #{table_name_prefix}custom_fields_trackers#{table_name_suffix} WHERE custom_field_id = #{id_column})"
35 additional_sql << " AND #{Issue.table_name}.project_id IN (SELECT project_id FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} WHERE custom_field_id = #{id})"
35 project_condition = "EXISTS (SELECT 1 FROM #{CustomField.table_name} ifa WHERE ifa.is_for_all = #{connection.quoted_true} AND ifa.id = #{id_column})" +
36 end
36 " OR #{Issue.table_name}.project_id IN (SELECT project_id FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} WHERE custom_field_id = #{id_column})"
37 "((#{sql}) AND (#{additional_sql}))"
37
38 "((#{sql}) AND (#{tracker_condition}) AND (#{project_condition}))"
38 end
39 end
39
40
40 def validate_custom_field
41 def validate_custom_field
41 super
42 super
42 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) unless visible? || roles.present?
43 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) unless visible? || roles.present?
43 end
44 end
44 end
45 end
@@ -1,135 +1,140
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module Redmine
18 module Redmine
19 module Acts
19 module Acts
20 module Searchable
20 module Searchable
21 def self.included(base)
21 def self.included(base)
22 base.extend ClassMethods
22 base.extend ClassMethods
23 end
23 end
24
24
25 module ClassMethods
25 module ClassMethods
26 # Options:
26 # Options:
27 # * :columns - a column or an array of columns to search
27 # * :columns - a column or an array of columns to search
28 # * :project_key - project foreign key (default to project_id)
28 # * :project_key - project foreign key (default to project_id)
29 # * :date_column - name of the datetime column (default to created_on)
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)
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")
31 # * :permission - permission required to search the model (default to :view_"objects")
32 def acts_as_searchable(options = {})
32 def acts_as_searchable(options = {})
33 return if self.included_modules.include?(Redmine::Acts::Searchable::InstanceMethods)
33 return if self.included_modules.include?(Redmine::Acts::Searchable::InstanceMethods)
34
34
35 cattr_accessor :searchable_options
35 cattr_accessor :searchable_options
36 self.searchable_options = options
36 self.searchable_options = options
37
37
38 if searchable_options[:columns].nil?
38 if searchable_options[:columns].nil?
39 raise 'No searchable column defined.'
39 raise 'No searchable column defined.'
40 elsif !searchable_options[:columns].is_a?(Array)
40 elsif !searchable_options[:columns].is_a?(Array)
41 searchable_options[:columns] = [] << searchable_options[:columns]
41 searchable_options[:columns] = [] << searchable_options[:columns]
42 end
42 end
43
43
44 searchable_options[:project_key] ||= "#{table_name}.project_id"
44 searchable_options[:project_key] ||= "#{table_name}.project_id"
45 searchable_options[:date_column] ||= "#{table_name}.created_on"
45 searchable_options[:date_column] ||= "#{table_name}.created_on"
46 searchable_options[:order_column] ||= searchable_options[:date_column]
46 searchable_options[:order_column] ||= searchable_options[:date_column]
47
47
48 # Should we search custom fields on this model ?
48 # Should we search custom fields on this model ?
49 searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil?
49 searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil?
50
50
51 send :include, Redmine::Acts::Searchable::InstanceMethods
51 send :include, Redmine::Acts::Searchable::InstanceMethods
52 end
52 end
53 end
53 end
54
54
55 module InstanceMethods
55 module InstanceMethods
56 def self.included(base)
56 def self.included(base)
57 base.extend ClassMethods
57 base.extend ClassMethods
58 end
58 end
59
59
60 module ClassMethods
60 module ClassMethods
61 # Searches the model for the given tokens
61 # Searches the model for the given tokens
62 # projects argument can be either nil (will search all projects), a project or an array of projects
62 # projects argument can be either nil (will search all projects), a project or an array of projects
63 # Returns the results and the results count
63 # Returns the results and the results count
64 def search(tokens, projects=nil, options={})
64 def search(tokens, projects=nil, options={})
65 if projects.is_a?(Array) && projects.empty?
65 if projects.is_a?(Array) && projects.empty?
66 # no results
66 # no results
67 return [[], 0]
67 return [[], 0]
68 end
68 end
69
69
70 # TODO: make user an argument
70 # TODO: make user an argument
71 user = User.current
71 user = User.current
72 tokens = [] << tokens unless tokens.is_a?(Array)
72 tokens = [] << tokens unless tokens.is_a?(Array)
73 projects = [] << projects unless projects.nil? || projects.is_a?(Array)
73 projects = [] << projects unless projects.nil? || projects.is_a?(Array)
74
74
75 limit_options = {}
75 limit_options = {}
76 limit_options[:limit] = options[:limit] if options[:limit]
76 limit_options[:limit] = options[:limit] if options[:limit]
77
77
78 columns = searchable_options[:columns]
78 columns = searchable_options[:columns]
79 columns = columns[0..0] if options[:titles_only]
79 columns = columns[0..0] if options[:titles_only]
80
80
81 token_clauses = columns.collect {|column| "(LOWER(#{column}) LIKE ?)"}
81 token_clauses = columns.collect {|column| "(LOWER(#{column}) LIKE ?)"}
82
82
83 if !options[:titles_only] && searchable_options[:search_custom_fields]
83 if !options[:titles_only] && searchable_options[:search_custom_fields]
84 searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true)
84 searchable_custom_fields = CustomField.where(:type => "#{self.name}CustomField", :searchable => true)
85 searchable_custom_fields.each do |field|
85 fields_by_visibility = searchable_custom_fields.group_by {|field|
86 sql = "#{table_name}.id IN (SELECT customized_id FROM #{CustomValue.table_name}" +
86 field.visibility_by_project_condition(searchable_options[:project_key], user, "cfs.custom_field_id")
87 " WHERE customized_type='#{self.name}' AND customized_id=#{table_name}.id AND LOWER(value) LIKE ?" +
87 }
88 " AND #{CustomValue.table_name}.custom_field_id = #{field.id})" +
88 # only 1 subquery for all custom fields with the same visibility statement
89 " AND #{field.visibility_by_project_condition(searchable_options[:project_key], user)}"
89 fields_by_visibility.each do |visibility, fields|
90 ids = fields.map(&:id).join(',')
91 sql = "#{table_name}.id IN (SELECT cfs.customized_id FROM #{CustomValue.table_name} cfs" +
92 " WHERE cfs.customized_type='#{self.name}' AND cfs.customized_id=#{table_name}.id AND LOWER(cfs.value) LIKE ?" +
93 " AND cfs.custom_field_id IN (#{ids})" +
94 " AND #{visibility})"
90 token_clauses << sql
95 token_clauses << sql
91 end
96 end
92 end
97 end
93
98
94 sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
99 sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
95
100
96 tokens_conditions = [sql, * (tokens.collect {|w| "%#{w.downcase}%"} * token_clauses.size).sort]
101 tokens_conditions = [sql, * (tokens.collect {|w| "%#{w.downcase}%"} * token_clauses.size).sort]
97
102
98 scope = self.scoped
103 scope = self.scoped
99 project_conditions = []
104 project_conditions = []
100 if searchable_options.has_key?(:permission)
105 if searchable_options.has_key?(:permission)
101 project_conditions << Project.allowed_to_condition(user, searchable_options[:permission] || :view_project)
106 project_conditions << Project.allowed_to_condition(user, searchable_options[:permission] || :view_project)
102 elsif respond_to?(:visible)
107 elsif respond_to?(:visible)
103 scope = scope.visible(user)
108 scope = scope.visible(user)
104 else
109 else
105 ActiveSupport::Deprecation.warn "acts_as_searchable with implicit :permission option is deprecated. Add a visible scope to the #{self.name} model or use explicit :permission option."
110 ActiveSupport::Deprecation.warn "acts_as_searchable with implicit :permission option is deprecated. Add a visible scope to the #{self.name} model or use explicit :permission option."
106 project_conditions << Project.allowed_to_condition(user, "view_#{self.name.underscore.pluralize}".to_sym)
111 project_conditions << Project.allowed_to_condition(user, "view_#{self.name.underscore.pluralize}".to_sym)
107 end
112 end
108 # TODO: use visible scope options instead
113 # TODO: use visible scope options instead
109 project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
114 project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
110 project_conditions = project_conditions.empty? ? nil : project_conditions.join(' AND ')
115 project_conditions = project_conditions.empty? ? nil : project_conditions.join(' AND ')
111
116
112 results = []
117 results = []
113 results_count = 0
118 results_count = 0
114
119
115 scope = scope.
120 scope = scope.
116 includes(searchable_options[:include]).
121 includes(searchable_options[:include]).
117 order("#{searchable_options[:order_column]} " + (options[:before] ? 'DESC' : 'ASC')).
122 order("#{searchable_options[:order_column]} " + (options[:before] ? 'DESC' : 'ASC')).
118 where(project_conditions).
123 where(project_conditions).
119 where(tokens_conditions)
124 where(tokens_conditions)
120
125
121 results_count = scope.count
126 results_count = scope.count
122
127
123 scope_with_limit = scope.limit(options[:limit])
128 scope_with_limit = scope.limit(options[:limit])
124 if options[:offset]
129 if options[:offset]
125 scope_with_limit = scope_with_limit.where("#{searchable_options[:date_column]} #{options[:before] ? '<' : '>'} ?", options[:offset])
130 scope_with_limit = scope_with_limit.where("#{searchable_options[:date_column]} #{options[:before] ? '<' : '>'} ?", options[:offset])
126 end
131 end
127 results = scope_with_limit.all
132 results = scope_with_limit.all
128
133
129 [results, results_count]
134 [results, results_count]
130 end
135 end
131 end
136 end
132 end
137 end
133 end
138 end
134 end
139 end
135 end
140 end
General Comments 0
You need to be logged in to leave comments. Login now