comparison app/models/custom_field.rb @ 1464:261b3d9a4903 redmine-2.4

Update to Redmine 2.4 branch rev 12663
author Chris Cannam
date Tue, 14 Jan 2014 14:37:42 +0000
parents 433d4f72a19b
children e248c7af89ec
comparison
equal deleted inserted replaced
1296:038ba2d95de8 1464:261b3d9a4903
1 # Redmine - project management software 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 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.
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 acts_as_list :scope => 'type = \'#{self.class}\'' 23 acts_as_list :scope => 'type = \'#{self.class}\''
23 serialize :possible_values 24 serialize :possible_values
24 25
25 validates_presence_of :name, :field_format 26 validates_presence_of :name, :field_format
26 validates_uniqueness_of :name, :scope => :type 27 validates_uniqueness_of :name, :scope => :type
27 validates_length_of :name, :maximum => 30 28 validates_length_of :name, :maximum => 30
28 validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats 29 validates_inclusion_of :field_format, :in => Proc.new { Redmine::CustomFieldFormat.available_formats }
29
30 validate :validate_custom_field 30 validate :validate_custom_field
31
31 before_validation :set_searchable 32 before_validation :set_searchable
32 33 after_save :handle_multiplicity_change
33 CUSTOM_FIELDS_TABS = [ 34 after_save do |field|
34 {:name => 'IssueCustomField', :partial => 'custom_fields/index', 35 if field.visible_changed? && field.visible
35 :label => :label_issue_plural}, 36 field.roles.clear
36 {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index', 37 end
37 :label => :label_spent_time}, 38 end
38 {:name => 'ProjectCustomField', :partial => 'custom_fields/index', 39
39 :label => :label_project_plural}, 40 scope :sorted, lambda { order("#{table_name}.position ASC") }
40 {:name => 'VersionCustomField', :partial => 'custom_fields/index', 41 scope :visible, lambda {|*args|
41 :label => :label_version_plural}, 42 user = args.shift || User.current
42 {:name => 'UserCustomField', :partial => 'custom_fields/index', 43 if user.admin?
43 :label => :label_user_plural}, 44 # nop
44 {:name => 'GroupCustomField', :partial => 'custom_fields/index', 45 elsif user.memberships.any?
45 :label => :label_group_plural}, 46 where("#{table_name}.visible = ? OR #{table_name}.id IN (SELECT DISTINCT cfr.custom_field_id FROM #{Member.table_name} m" +
46 {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index', 47 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
47 :label => TimeEntryActivity::OptionName}, 48 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
48 {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index', 49 " WHERE m.user_id = ?)",
49 :label => IssuePriority::OptionName}, 50 true, user.id)
50 {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index', 51 else
51 :label => DocumentCategory::OptionName} 52 where(:visible => true)
52 ] 53 end
53 54 }
54 CUSTOM_FIELDS_NAMES = CUSTOM_FIELDS_TABS.collect{|v| v[:name]} 55
56 def visible_by?(project, user=User.current)
57 visible? || user.admin?
58 end
55 59
56 def field_format=(arg) 60 def field_format=(arg)
57 # cannot change format of a saved custom field 61 # cannot change format of a saved custom field
58 super if new_record? 62 super if new_record?
59 end 63 end
117 values = super() 121 values = super()
118 if values.is_a?(Array) 122 if values.is_a?(Array)
119 values.each do |value| 123 values.each do |value|
120 value.force_encoding('UTF-8') if value.respond_to?(:force_encoding) 124 value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
121 end 125 end
122 end 126 values
123 values || [] 127 else
128 []
129 end
124 end 130 end
125 end 131 end
126 132
127 # Makes possible_values accept a multiline string 133 # Makes possible_values accept a multiline string
128 def possible_values=(arg) 134 def possible_values=(arg)
167 end 173 end
168 else 174 else
169 keyword 175 keyword
170 end 176 end
171 end 177 end
172 178
173 # Returns a ORDER BY clause that can used to sort customized 179 # Returns a ORDER BY clause that can used to sort customized
174 # objects by their value of the custom field. 180 # objects by their value of the custom field.
175 # Returns nil if the custom field can not be used for sorting. 181 # Returns nil if the custom field can not be used for sorting.
176 def order_statement 182 def order_statement
177 return nil if multiple? 183 return nil if multiple?
178 case field_format 184 case field_format
179 when 'string', 'text', 'list', 'date', 'bool' 185 when 'string', 'text', 'list', 'date', 'bool'
180 # COALESCE is here to make sure that blank and NULL values are sorted equally 186 # COALESCE is here to make sure that blank and NULL values are sorted equally
181 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" + 187 "COALESCE(#{join_alias}.value, '')"
182 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
183 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
184 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
185 when 'int', 'float' 188 when 'int', 'float'
186 # Make the database cast values into numeric 189 # Make the database cast values into numeric
187 # Postgresql will raise an error if a value can not be casted! 190 # Postgresql will raise an error if a value can not be casted!
188 # CustomValue validations should ensure that it doesn't occur 191 # CustomValue validations should ensure that it doesn't occur
189 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" + 192 "CAST(CASE #{join_alias}.value WHEN '' THEN '0' ELSE #{join_alias}.value END AS decimal(30,3))"
190 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
191 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
192 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
193 when 'user', 'version' 193 when 'user', 'version'
194 value_class.fields_for_order_statement(value_join_alias) 194 value_class.fields_for_order_statement(value_join_alias)
195 else 195 else
196 nil 196 nil
197 end 197 end
198 end 198 end
199 199
200 # Returns a GROUP BY clause that can used to group by custom value 200 # 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. 201 # Returns nil if the custom field can not be used for grouping.
202 def group_statement 202 def group_statement
203 return nil if multiple? 203 return nil if multiple?
204 case field_format 204 case field_format
205 when 'list', 'date', 'bool', 'int' 205 when 'list', 'date', 'bool', 'int'
206 order_statement 206 order_statement
207 when 'user', 'version' 207 when 'user', 'version'
208 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" + 208 "COALESCE(#{join_alias}.value, '')"
209 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
210 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
211 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
212 else 209 else
213 nil 210 nil
214 end 211 end
215 end 212 end
216 213
219 when 'user', 'version' 216 when 'user', 'version'
220 "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" + 217 "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" +
221 " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" + 218 " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
222 " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" + 219 " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
223 " AND #{join_alias}.custom_field_id = #{id}" + 220 " AND #{join_alias}.custom_field_id = #{id}" +
221 " AND (#{visibility_by_project_condition})" +
224 " AND #{join_alias}.value <> ''" + 222 " AND #{join_alias}.value <> ''" +
225 " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" + 223 " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
226 " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" + 224 " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
227 " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" + 225 " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
228 " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)" + 226 " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)" +
229 " LEFT OUTER JOIN #{value_class.table_name} #{value_join_alias}" + 227 " LEFT OUTER JOIN #{value_class.table_name} #{value_join_alias}" +
230 " ON CAST(#{join_alias}.value as decimal(60,0)) = #{value_join_alias}.id" 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)"
231 else 250 else
232 nil 251 nil
233 end 252 end
234 end 253 end
235 254
239 258
240 def value_join_alias 259 def value_join_alias
241 join_alias + "_" + field_format 260 join_alias + "_" + field_format
242 end 261 end
243 262
263 def visibility_by_project_condition(project_key=nil, user=User.current)
264 if visible? || user.admin?
265 "1=1"
266 elsif user.anonymous?
267 "1=0"
268 else
269 project_key ||= "#{self.class.customized_class.table_name}.project_id"
270 "#{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" +
272 " 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})"
274 end
275 end
276
277 def self.visibility_condition
278 if user.admin?
279 "1=1"
280 elsif user.anonymous?
281 "#{table_name}.visible"
282 else
283 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
284 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
285 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
286 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
287 end
288 end
289
244 def <=>(field) 290 def <=>(field)
245 position <=> field.position 291 position <=> field.position
246 end 292 end
247 293
248 # Returns the class that values represent 294 # Returns the class that values represent
255 end 301 end
256 end 302 end
257 303
258 def self.customized_class 304 def self.customized_class
259 self.name =~ /^(.+)CustomField$/ 305 self.name =~ /^(.+)CustomField$/
260 begin; $1.constantize; rescue nil; end 306 $1.constantize rescue nil
261 end 307 end
262 308
263 # to move in project_custom_field 309 # to move in project_custom_field
264 def self.for_all 310 def self.for_all
265 find(:all, :conditions => ["is_for_all=?", true], :order => 'position') 311 where(:is_for_all => true).order('position').all
266 end 312 end
267 313
268 def type_name 314 def type_name
269 nil 315 nil
270 end 316 end
321 errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value) 367 errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value)
322 end 368 end
323 end 369 end
324 errs 370 errs
325 end 371 end
372
373 # 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
375 def handle_multiplicity_change
376 if !new_record? && multiple_was && !multiple
377 ids = custom_values.
378 where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" +
379 " AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" +
380 " AND cve.id > #{CustomValue.table_name}.id)").
381 pluck(:id)
382
383 if ids.any?
384 custom_values.where(:id => ids).delete_all
385 end
386 end
387 end
326 end 388 end