comparison app/models/custom_field.rb @ 1517:dffacf8a6908 redmine-2.5

Update to Redmine SVN revision 13367 on 2.5-stable branch
author Chris Cannam
date Tue, 09 Sep 2014 09:29:00 +0100
parents b450a9d58aed
children
comparison
equal deleted inserted replaced
1516:b450a9d58aed 1517:dffacf8a6908
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 26
26 validates_presence_of :name, :field_format 27 validates_presence_of :name, :field_format
27 validates_uniqueness_of :name, :scope => :type 28 validates_uniqueness_of :name, :scope => :type
28 validates_length_of :name, :maximum => 30 29 validates_length_of :name, :maximum => 30
29 validates_inclusion_of :field_format, :in => Proc.new { Redmine::CustomFieldFormat.available_formats } 30 validates_inclusion_of :field_format, :in => Proc.new { Redmine::FieldFormat.available_formats }
30 validate :validate_custom_field 31 validate :validate_custom_field
31 32
32 before_validation :set_searchable 33 before_validation :set_searchable
34 before_save do |field|
35 field.format.before_custom_field_save(field)
36 end
33 after_save :handle_multiplicity_change 37 after_save :handle_multiplicity_change
34 after_save do |field| 38 after_save do |field|
35 if field.visible_changed? && field.visible 39 if field.visible_changed? && field.visible
36 field.roles.clear 40 field.roles.clear
37 end 41 end
55 59
56 def visible_by?(project, user=User.current) 60 def visible_by?(project, user=User.current)
57 visible? || user.admin? 61 visible? || user.admin?
58 end 62 end
59 63
64 def format
65 @format ||= Redmine::FieldFormat.find(field_format)
66 end
67
60 def field_format=(arg) 68 def field_format=(arg)
61 # cannot change format of a saved custom field 69 # cannot change format of a saved custom field
62 super if new_record? 70 if new_record?
71 @format = nil
72 super
73 end
63 end 74 end
64 75
65 def set_searchable 76 def set_searchable
66 # make sure these fields are not searchable 77 # make sure these fields are not searchable
67 self.searchable = false if %w(int float date bool).include?(field_format) 78 self.searchable = false unless format.class.searchable_supported
68 # make sure only these fields can have multiple values 79 # make sure only these fields can have multiple values
69 self.multiple = false unless %w(list user version).include?(field_format) 80 self.multiple = false unless format.class.multiple_supported
70 true 81 true
71 end 82 end
72 83
73 def validate_custom_field 84 def validate_custom_field
74 if self.field_format == "list" 85 format.validate_custom_field(self).each do |attribute, message|
75 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty? 86 errors.add attribute, message
76 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
77 end 87 end
78 88
79 if regexp.present? 89 if regexp.present?
80 begin 90 begin
81 Regexp.new(regexp) 91 Regexp.new(regexp)
82 rescue 92 rescue
83 errors.add(:regexp, :invalid) 93 errors.add(:regexp, :invalid)
84 end 94 end
85 end 95 end
86 96
87 if default_value.present? && !valid_field_value?(default_value) 97 if default_value.present?
88 errors.add(:default_value, :invalid) 98 validate_field_value(default_value).each do |message|
89 end 99 errors.add :default_value, message
90 end 100 end
91 101 end
92 def possible_values_options(obj=nil) 102 end
93 case field_format 103
94 when 'user', 'version' 104 def possible_custom_value_options(custom_value)
95 if obj.respond_to?(:project) && obj.project 105 format.possible_custom_value_options(custom_value)
96 case field_format 106 end
97 when 'user' 107
98 obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]} 108 def possible_values_options(object=nil)
99 when 'version' 109 if object.is_a?(Array)
100 obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]} 110 object.map {|o| format.possible_values_options(self, o)}.reduce(:&) || []
101 end 111 else
102 elsif obj.is_a?(Array) 112 format.possible_values_options(self, object) || []
103 obj.collect {|o| possible_values_options(o)}.reduce(:&) 113 end
104 else 114 end
105 [] 115
106 end 116 def possible_values
107 when 'bool' 117 values = read_attribute(:possible_values)
108 [[l(:general_text_Yes), '1'], [l(:general_text_No), '0']] 118 if values.is_a?(Array)
109 else 119 values.each do |value|
110 possible_values || [] 120 value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
111 end 121 end
112 end 122 values
113 123 else
114 def possible_values(obj=nil) 124 []
115 case field_format
116 when 'user', 'version'
117 possible_values_options(obj).collect(&:last)
118 when 'bool'
119 ['1', '0']
120 else
121 values = super()
122 if values.is_a?(Array)
123 values.each do |value|
124 value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
125 end
126 values
127 else
128 []
129 end
130 end 125 end
131 end 126 end
132 127
133 # Makes possible_values accept a multiline string 128 # Makes possible_values accept a multiline string
134 def possible_values=(arg) 129 def possible_values=(arg)
135 if arg.is_a?(Array) 130 if arg.is_a?(Array)
136 super(arg.compact.collect(&:strip).select {|v| !v.blank?}) 131 values = arg.compact.collect(&:strip).select {|v| !v.blank?}
132 write_attribute(:possible_values, values)
137 else 133 else
138 self.possible_values = arg.to_s.split(/[\n\r]+/) 134 self.possible_values = arg.to_s.split(/[\n\r]+/)
139 end 135 end
140 end 136 end
141 137
142 def cast_value(value) 138 def cast_value(value)
143 casted = nil 139 format.cast_value(self, value)
144 unless value.blank?
145 case field_format
146 when 'string', 'text', 'list'
147 casted = value
148 when 'date'
149 casted = begin; value.to_date; rescue; nil end
150 when 'bool'
151 casted = (value == '1' ? true : false)
152 when 'int'
153 casted = value.to_i
154 when 'float'
155 casted = value.to_f
156 when 'user', 'version'
157 casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
158 end
159 end
160 casted
161 end 140 end
162 141
163 def value_from_keyword(keyword, customized) 142 def value_from_keyword(keyword, customized)
164 possible_values_options = possible_values_options(customized) 143 possible_values_options = possible_values_options(customized)
165 if possible_values_options.present? 144 if possible_values_options.present?
179 # Returns a ORDER BY clause that can used to sort customized 158 # Returns a ORDER BY clause that can used to sort customized
180 # objects by their value of the custom field. 159 # objects by their value of the custom field.
181 # Returns nil if the custom field can not be used for sorting. 160 # Returns nil if the custom field can not be used for sorting.
182 def order_statement 161 def order_statement
183 return nil if multiple? 162 return nil if multiple?
184 case field_format 163 format.order_statement(self)
185 when 'string', 'text', 'list', 'date', 'bool'
186 # COALESCE is here to make sure that blank and NULL values are sorted equally
187 "COALESCE(#{join_alias}.value, '')"
188 when 'int', 'float'
189 # Make the database cast values into numeric
190 # Postgresql will raise an error if a value can not be casted!
191 # CustomValue validations should ensure that it doesn't occur
192 "CAST(CASE #{join_alias}.value WHEN '' THEN '0' ELSE #{join_alias}.value END AS decimal(30,3))"
193 when 'user', 'version'
194 value_class.fields_for_order_statement(value_join_alias)
195 else
196 nil
197 end
198 end 164 end
199 165
200 # Returns a GROUP BY clause that can used to group by custom value 166 # Returns a GROUP BY clause that can used to group by custom value
201 # Returns nil if the custom field can not be used for grouping. 167 # Returns nil if the custom field can not be used for grouping.
202 def group_statement 168 def group_statement
203 return nil if multiple? 169 return nil if multiple?
204 case field_format 170 format.group_statement(self)
205 when 'list', 'date', 'bool', 'int'
206 order_statement
207 when 'user', 'version'
208 "COALESCE(#{join_alias}.value, '')"
209 else
210 nil
211 end
212 end 171 end
213 172
214 def join_for_order_statement 173 def join_for_order_statement
215 case field_format 174 format.join_for_order_statement(self)
216 when 'user', 'version' 175 end
217 "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" + 176
218 " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" + 177 def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil)
219 " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
220 " AND #{join_alias}.custom_field_id = #{id}" +
221 " AND (#{visibility_by_project_condition})" +
222 " AND #{join_alias}.value <> ''" +
223 " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
224 " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
225 " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
226 " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)" +
227 " LEFT OUTER JOIN #{value_class.table_name} #{value_join_alias}" +
228 " ON CAST(CASE #{join_alias}.value WHEN '' THEN '0' ELSE #{join_alias}.value END AS decimal(30,0)) = #{value_join_alias}.id"
229 when 'int', 'float'
230 "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" +
231 " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
232 " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
233 " AND #{join_alias}.custom_field_id = #{id}" +
234 " AND (#{visibility_by_project_condition})" +
235 " AND #{join_alias}.value <> ''" +
236 " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
237 " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
238 " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
239 " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)"
240 when 'string', 'text', 'list', 'date', 'bool'
241 "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" +
242 " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
243 " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
244 " AND #{join_alias}.custom_field_id = #{id}" +
245 " AND (#{visibility_by_project_condition})" +
246 " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
247 " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
248 " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
249 " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)"
250 else
251 nil
252 end
253 end
254
255 def join_alias
256 "cf_#{id}"
257 end
258
259 def value_join_alias
260 join_alias + "_" + field_format
261 end
262
263 def visibility_by_project_condition(project_key=nil, user=User.current)
264 if visible? || user.admin? 178 if visible? || user.admin?
265 "1=1" 179 "1=1"
266 elsif user.anonymous? 180 elsif user.anonymous?
267 "1=0" 181 "1=0"
268 else 182 else
269 project_key ||= "#{self.class.customized_class.table_name}.project_id" 183 project_key ||= "#{self.class.customized_class.table_name}.project_id"
184 id_column ||= id
270 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" + 185 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
271 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" + 186 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
272 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" + 187 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
273 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})" 188 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id_column})"
274 end 189 end
275 end 190 end
276 191
277 def self.visibility_condition 192 def self.visibility_condition
278 if user.admin? 193 if user.admin?
291 position <=> field.position 206 position <=> field.position
292 end 207 end
293 208
294 # Returns the class that values represent 209 # Returns the class that values represent
295 def value_class 210 def value_class
296 case field_format 211 format.target_class if format.respond_to?(:target_class)
297 when 'user', 'version'
298 field_format.classify.constantize
299 else
300 nil
301 end
302 end 212 end
303 213
304 def self.customized_class 214 def self.customized_class
305 self.name =~ /^(.+)CustomField$/ 215 self.name =~ /^(.+)CustomField$/
306 $1.constantize rescue nil 216 $1.constantize rescue nil
315 nil 225 nil
316 end 226 end
317 227
318 # Returns the error messages for the given value 228 # Returns the error messages for the given value
319 # or an empty array if value is a valid value for the custom field 229 # or an empty array if value is a valid value for the custom field
320 def validate_field_value(value) 230 def validate_custom_value(custom_value)
231 value = custom_value.value
321 errs = [] 232 errs = []
322 if value.is_a?(Array) 233 if value.is_a?(Array)
323 if !multiple? 234 if !multiple?
324 errs << ::I18n.t('activerecord.errors.messages.invalid') 235 errs << ::I18n.t('activerecord.errors.messages.invalid')
325 end 236 end
326 if is_required? && value.detect(&:present?).nil? 237 if is_required? && value.detect(&:present?).nil?
327 errs << ::I18n.t('activerecord.errors.messages.blank') 238 errs << ::I18n.t('activerecord.errors.messages.blank')
328 end 239 end
329 value.each {|v| errs += validate_field_value_format(v)}
330 else 240 else
331 if is_required? && value.blank? 241 if is_required? && value.blank?
332 errs << ::I18n.t('activerecord.errors.messages.blank') 242 errs << ::I18n.t('activerecord.errors.messages.blank')
333 end 243 end
334 errs += validate_field_value_format(value) 244 end
335 end 245 errs += format.validate_custom_value(custom_value)
336 errs 246 errs
247 end
248
249 # Returns the error messages for the default custom field value
250 def validate_field_value(value)
251 validate_custom_value(CustomValue.new(:custom_field => self, :value => value))
337 end 252 end
338 253
339 # Returns true if value is a valid value for the custom field 254 # Returns true if value is a valid value for the custom field
340 def valid_field_value?(value) 255 def valid_field_value?(value)
341 validate_field_value(value).empty? 256 validate_field_value(value).empty?
344 def format_in?(*args) 259 def format_in?(*args)
345 args.include?(field_format) 260 args.include?(field_format)
346 end 261 end
347 262
348 protected 263 protected
349
350 # Returns the error message for the given value regarding its format
351 def validate_field_value_format(value)
352 errs = []
353 unless value.to_s == ''
354 errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp)
355 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length && min_length > 0 && value.length < min_length
356 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length && max_length > 0 && value.length > max_length
357
358 # Format specific validations
359 case field_format
360 when 'int'
361 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
362 when 'float'
363 begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end
364 when 'date'
365 errs << ::I18n.t('activerecord.errors.messages.not_a_date') unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end
366 when 'list'
367 errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value)
368 end
369 end
370 errs
371 end
372 264
373 # Removes multiple values for the custom field after setting the multiple attribute to false 265 # Removes multiple values for the custom field after setting the multiple attribute to false
374 # We kepp the value with the highest id for each customized object 266 # We kepp the value with the highest id for each customized object
375 def handle_multiplicity_change 267 def handle_multiplicity_change
376 if !new_record? && multiple_was && !multiple 268 if !new_record? && multiple_was && !multiple
384 custom_values.where(:id => ids).delete_all 276 custom_values.where(:id => ids).delete_all
385 end 277 end
386 end 278 end
387 end 279 end
388 end 280 end
281
282 require_dependency 'redmine/field_format'