Mercurial > hg > soundsoftware-site
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 |
