##// END OF EJS Templates
Fixed that non-empty blank strings as custom field values are not properly validated (#16169)....
Jean-Philippe Lang -
r12663:4c7a76785d9f
parent child
Show More
@@ -1,283 +1,281
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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, id_column=nil)
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 id_column ||= id
184 "#{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" +
185 " 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" +
186 " 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" +
187 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id_column})"
187 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id_column})"
188 end
188 end
189 end
189 end
190
190
191 def self.visibility_condition
191 def self.visibility_condition
192 if user.admin?
192 if user.admin?
193 "1=1"
193 "1=1"
194 elsif user.anonymous?
194 elsif user.anonymous?
195 "#{table_name}.visible"
195 "#{table_name}.visible"
196 else
196 else
197 "#{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" +
198 " 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" +
199 " 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" +
200 " 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})"
201 end
201 end
202 end
202 end
203
203
204 def <=>(field)
204 def <=>(field)
205 position <=> field.position
205 position <=> field.position
206 end
206 end
207
207
208 # Returns the class that values represent
208 # Returns the class that values represent
209 def value_class
209 def value_class
210 format.target_class if format.respond_to?(:target_class)
210 format.target_class if format.respond_to?(:target_class)
211 end
211 end
212
212
213 def self.customized_class
213 def self.customized_class
214 self.name =~ /^(.+)CustomField$/
214 self.name =~ /^(.+)CustomField$/
215 $1.constantize rescue nil
215 $1.constantize rescue nil
216 end
216 end
217
217
218 # to move in project_custom_field
218 # to move in project_custom_field
219 def self.for_all
219 def self.for_all
220 where(:is_for_all => true).order('position').all
220 where(:is_for_all => true).order('position').all
221 end
221 end
222
222
223 def type_name
223 def type_name
224 nil
224 nil
225 end
225 end
226
226
227 # Returns the error messages for the given value
227 # Returns the error messages for the given value
228 # 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
229 def validate_custom_value(custom_value)
229 def validate_custom_value(custom_value)
230 value = custom_value.value
230 value = custom_value.value
231 errs = []
231 errs = []
232 if value.is_a?(Array)
232 if value.is_a?(Array)
233 if !multiple?
233 if !multiple?
234 errs << ::I18n.t('activerecord.errors.messages.invalid')
234 errs << ::I18n.t('activerecord.errors.messages.invalid')
235 end
235 end
236 if is_required? && value.detect(&:present?).nil?
236 if is_required? && value.detect(&:present?).nil?
237 errs << ::I18n.t('activerecord.errors.messages.blank')
237 errs << ::I18n.t('activerecord.errors.messages.blank')
238 end
238 end
239 else
239 else
240 if is_required? && value.blank?
240 if is_required? && value.blank?
241 errs << ::I18n.t('activerecord.errors.messages.blank')
241 errs << ::I18n.t('activerecord.errors.messages.blank')
242 end
242 end
243 end
243 end
244 if custom_value.value.present?
244 errs += format.validate_custom_value(custom_value)
245 errs += format.validate_custom_value(custom_value)
246 end
247 errs
245 errs
248 end
246 end
249
247
250 # Returns the error messages for the default custom field value
248 # Returns the error messages for the default custom field value
251 def validate_field_value(value)
249 def validate_field_value(value)
252 validate_custom_value(CustomValue.new(:custom_field => self, :value => value))
250 validate_custom_value(CustomValue.new(:custom_field => self, :value => value))
253 end
251 end
254
252
255 # Returns true if value is a valid value for the custom field
253 # Returns true if value is a valid value for the custom field
256 def valid_field_value?(value)
254 def valid_field_value?(value)
257 validate_field_value(value).empty?
255 validate_field_value(value).empty?
258 end
256 end
259
257
260 def format_in?(*args)
258 def format_in?(*args)
261 args.include?(field_format)
259 args.include?(field_format)
262 end
260 end
263
261
264 protected
262 protected
265
263
266 # Removes multiple values for the custom field after setting the multiple attribute to false
264 # Removes multiple values for the custom field after setting the multiple attribute to false
267 # We kepp the value with the highest id for each customized object
265 # We kepp the value with the highest id for each customized object
268 def handle_multiplicity_change
266 def handle_multiplicity_change
269 if !new_record? && multiple_was && !multiple
267 if !new_record? && multiple_was && !multiple
270 ids = custom_values.
268 ids = custom_values.
271 where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" +
269 where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" +
272 " AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" +
270 " AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" +
273 " AND cve.id > #{CustomValue.table_name}.id)").
271 " AND cve.id > #{CustomValue.table_name}.id)").
274 pluck(:id)
272 pluck(:id)
275
273
276 if ids.any?
274 if ids.any?
277 custom_values.where(:id => ids).delete_all
275 custom_values.where(:id => ids).delete_all
278 end
276 end
279 end
277 end
280 end
278 end
281 end
279 end
282
280
283 require_dependency 'redmine/field_format'
281 require_dependency 'redmine/field_format'
@@ -1,683 +1,684
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 FieldFormat
19 module FieldFormat
20 def self.add(name, klass)
20 def self.add(name, klass)
21 all[name.to_s] = klass.instance
21 all[name.to_s] = klass.instance
22 end
22 end
23
23
24 def self.delete(name)
24 def self.delete(name)
25 all.delete(name.to_s)
25 all.delete(name.to_s)
26 end
26 end
27
27
28 def self.all
28 def self.all
29 @formats ||= Hash.new(Base.instance)
29 @formats ||= Hash.new(Base.instance)
30 end
30 end
31
31
32 def self.available_formats
32 def self.available_formats
33 all.keys
33 all.keys
34 end
34 end
35
35
36 def self.find(name)
36 def self.find(name)
37 all[name.to_s]
37 all[name.to_s]
38 end
38 end
39
39
40 # Return an array of custom field formats which can be used in select_tag
40 # Return an array of custom field formats which can be used in select_tag
41 def self.as_select(class_name=nil)
41 def self.as_select(class_name=nil)
42 formats = all.values.select do |format|
42 formats = all.values.select do |format|
43 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
43 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
44 end
44 end
45 formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
45 formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
46 end
46 end
47
47
48 class Base
48 class Base
49 include Singleton
49 include Singleton
50 include Redmine::I18n
50 include Redmine::I18n
51 include ERB::Util
51 include ERB::Util
52
52
53 class_attribute :format_name
53 class_attribute :format_name
54 self.format_name = nil
54 self.format_name = nil
55
55
56 # Set this to true if the format supports multiple values
56 # Set this to true if the format supports multiple values
57 class_attribute :multiple_supported
57 class_attribute :multiple_supported
58 self.multiple_supported = false
58 self.multiple_supported = false
59
59
60 # Set this to true if the format supports textual search on custom values
60 # Set this to true if the format supports textual search on custom values
61 class_attribute :searchable_supported
61 class_attribute :searchable_supported
62 self.searchable_supported = false
62 self.searchable_supported = false
63
63
64 # Restricts the classes that the custom field can be added to
64 # Restricts the classes that the custom field can be added to
65 # Set to nil for no restrictions
65 # Set to nil for no restrictions
66 class_attribute :customized_class_names
66 class_attribute :customized_class_names
67 self.customized_class_names = nil
67 self.customized_class_names = nil
68
68
69 # Name of the partial for editing the custom field
69 # Name of the partial for editing the custom field
70 class_attribute :form_partial
70 class_attribute :form_partial
71 self.form_partial = nil
71 self.form_partial = nil
72
72
73 def self.add(name)
73 def self.add(name)
74 self.format_name = name
74 self.format_name = name
75 Redmine::FieldFormat.add(name, self)
75 Redmine::FieldFormat.add(name, self)
76 end
76 end
77 private_class_method :add
77 private_class_method :add
78
78
79 def self.field_attributes(*args)
79 def self.field_attributes(*args)
80 CustomField.store_accessor :format_store, *args
80 CustomField.store_accessor :format_store, *args
81 end
81 end
82
82
83 field_attributes :url_pattern
83 field_attributes :url_pattern
84
84
85 def name
85 def name
86 self.class.format_name
86 self.class.format_name
87 end
87 end
88
88
89 def label
89 def label
90 "label_#{name}"
90 "label_#{name}"
91 end
91 end
92
92
93 def cast_custom_value(custom_value)
93 def cast_custom_value(custom_value)
94 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
94 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
95 end
95 end
96
96
97 def cast_value(custom_field, value, customized=nil)
97 def cast_value(custom_field, value, customized=nil)
98 if value.blank?
98 if value.blank?
99 nil
99 nil
100 elsif value.is_a?(Array)
100 elsif value.is_a?(Array)
101 value.map do |v|
101 value.map do |v|
102 cast_single_value(custom_field, v, customized)
102 cast_single_value(custom_field, v, customized)
103 end.sort
103 end.sort
104 else
104 else
105 cast_single_value(custom_field, value, customized)
105 cast_single_value(custom_field, value, customized)
106 end
106 end
107 end
107 end
108
108
109 def cast_single_value(custom_field, value, customized=nil)
109 def cast_single_value(custom_field, value, customized=nil)
110 value.to_s
110 value.to_s
111 end
111 end
112
112
113 def target_class
113 def target_class
114 nil
114 nil
115 end
115 end
116
116
117 def possible_custom_value_options(custom_value)
117 def possible_custom_value_options(custom_value)
118 possible_values_options(custom_value.custom_field, custom_value.customized)
118 possible_values_options(custom_value.custom_field, custom_value.customized)
119 end
119 end
120
120
121 def possible_values_options(custom_field, object=nil)
121 def possible_values_options(custom_field, object=nil)
122 []
122 []
123 end
123 end
124
124
125 # Returns the validation errors for custom_field
125 # Returns the validation errors for custom_field
126 # Should return an empty array if custom_field is valid
126 # Should return an empty array if custom_field is valid
127 def validate_custom_field(custom_field)
127 def validate_custom_field(custom_field)
128 []
128 []
129 end
129 end
130
130
131 # Returns the validation error messages for custom_value
131 # Returns the validation error messages for custom_value
132 # Should return an empty array if custom_value is valid
132 # Should return an empty array if custom_value is valid
133 def validate_custom_value(custom_value)
133 def validate_custom_value(custom_value)
134 errors = Array.wrap(custom_value.value).reject(&:blank?).map do |value|
134 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
135 errors = values.map do |value|
135 validate_single_value(custom_value.custom_field, value, custom_value.customized)
136 validate_single_value(custom_value.custom_field, value, custom_value.customized)
136 end
137 end
137 errors.flatten.uniq
138 errors.flatten.uniq
138 end
139 end
139
140
140 def validate_single_value(custom_field, value, customized=nil)
141 def validate_single_value(custom_field, value, customized=nil)
141 []
142 []
142 end
143 end
143
144
144 def formatted_custom_value(view, custom_value, html=false)
145 def formatted_custom_value(view, custom_value, html=false)
145 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
146 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
146 end
147 end
147
148
148 def formatted_value(view, custom_field, value, customized=nil, html=false)
149 def formatted_value(view, custom_field, value, customized=nil, html=false)
149 casted = cast_value(custom_field, value, customized)
150 casted = cast_value(custom_field, value, customized)
150 if custom_field.url_pattern.present?
151 if custom_field.url_pattern.present?
151 texts_and_urls = Array.wrap(casted).map do |single_value|
152 texts_and_urls = Array.wrap(casted).map do |single_value|
152 text = view.format_object(single_value, false).to_s
153 text = view.format_object(single_value, false).to_s
153 url = url_from_pattern(custom_field, single_value, customized)
154 url = url_from_pattern(custom_field, single_value, customized)
154 [text, url]
155 [text, url]
155 end
156 end
156 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to text, url}
157 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to text, url}
157 links.join(', ').html_safe
158 links.join(', ').html_safe
158 else
159 else
159 casted
160 casted
160 end
161 end
161 end
162 end
162
163
163 # Returns an URL generated with the custom field URL pattern
164 # Returns an URL generated with the custom field URL pattern
164 # and variables substitution:
165 # and variables substitution:
165 # %value% => the custom field value
166 # %value% => the custom field value
166 # %id% => id of the customized object
167 # %id% => id of the customized object
167 # %project_id% => id of the project of the customized object if defined
168 # %project_id% => id of the project of the customized object if defined
168 # %project_identifier% => identifier of the project of the customized object if defined
169 # %project_identifier% => identifier of the project of the customized object if defined
169 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
170 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
170 def url_from_pattern(custom_field, value, customized)
171 def url_from_pattern(custom_field, value, customized)
171 url = custom_field.url_pattern.to_s.dup
172 url = custom_field.url_pattern.to_s.dup
172 url.gsub!('%value%') {value.to_s}
173 url.gsub!('%value%') {value.to_s}
173 url.gsub!('%id%') {customized.id.to_s}
174 url.gsub!('%id%') {customized.id.to_s}
174 url.gsub!('%project_id%') {(customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
175 url.gsub!('%project_id%') {(customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
175 url.gsub!('%project_identifier%') {(customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
176 url.gsub!('%project_identifier%') {(customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
176 if custom_field.regexp.present?
177 if custom_field.regexp.present?
177 url.gsub!(%r{%m(\d+)%}) do
178 url.gsub!(%r{%m(\d+)%}) do
178 m = $1.to_i
179 m = $1.to_i
179 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
180 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
180 matches[m].to_s
181 matches[m].to_s
181 end
182 end
182 end
183 end
183 end
184 end
184 url
185 url
185 end
186 end
186 protected :url_from_pattern
187 protected :url_from_pattern
187
188
188 def edit_tag(view, tag_id, tag_name, custom_value, options={})
189 def edit_tag(view, tag_id, tag_name, custom_value, options={})
189 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
190 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
190 end
191 end
191
192
192 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
193 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
193 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
194 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
194 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
195 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
195 end
196 end
196
197
197 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
198 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
198 if custom_field.is_required?
199 if custom_field.is_required?
199 ''.html_safe
200 ''.html_safe
200 else
201 else
201 view.content_tag('label',
202 view.content_tag('label',
202 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
203 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
203 :class => 'inline'
204 :class => 'inline'
204 )
205 )
205 end
206 end
206 end
207 end
207 protected :bulk_clear_tag
208 protected :bulk_clear_tag
208
209
209 def query_filter_options(custom_field, query)
210 def query_filter_options(custom_field, query)
210 {:type => :string}
211 {:type => :string}
211 end
212 end
212
213
213 def before_custom_field_save(custom_field)
214 def before_custom_field_save(custom_field)
214 end
215 end
215
216
216 # Returns a ORDER BY clause that can used to sort customized
217 # Returns a ORDER BY clause that can used to sort customized
217 # objects by their value of the custom field.
218 # objects by their value of the custom field.
218 # Returns nil if the custom field can not be used for sorting.
219 # Returns nil if the custom field can not be used for sorting.
219 def order_statement(custom_field)
220 def order_statement(custom_field)
220 # COALESCE is here to make sure that blank and NULL values are sorted equally
221 # COALESCE is here to make sure that blank and NULL values are sorted equally
221 "COALESCE(#{join_alias custom_field}.value, '')"
222 "COALESCE(#{join_alias custom_field}.value, '')"
222 end
223 end
223
224
224 # Returns a GROUP BY clause that can used to group by custom value
225 # Returns a GROUP BY clause that can used to group by custom value
225 # Returns nil if the custom field can not be used for grouping.
226 # Returns nil if the custom field can not be used for grouping.
226 def group_statement(custom_field)
227 def group_statement(custom_field)
227 nil
228 nil
228 end
229 end
229
230
230 # Returns a JOIN clause that is added to the query when sorting by custom values
231 # Returns a JOIN clause that is added to the query when sorting by custom values
231 def join_for_order_statement(custom_field)
232 def join_for_order_statement(custom_field)
232 alias_name = join_alias(custom_field)
233 alias_name = join_alias(custom_field)
233
234
234 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
235 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
235 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
236 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
236 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
237 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
237 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
238 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
238 " AND (#{custom_field.visibility_by_project_condition})" +
239 " AND (#{custom_field.visibility_by_project_condition})" +
239 " AND #{alias_name}.value <> ''" +
240 " AND #{alias_name}.value <> ''" +
240 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
241 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
241 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
242 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
242 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
243 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
243 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
244 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
244 end
245 end
245
246
246 def join_alias(custom_field)
247 def join_alias(custom_field)
247 "cf_#{custom_field.id}"
248 "cf_#{custom_field.id}"
248 end
249 end
249 protected :join_alias
250 protected :join_alias
250 end
251 end
251
252
252 class Unbounded < Base
253 class Unbounded < Base
253 def validate_single_value(custom_field, value, customized=nil)
254 def validate_single_value(custom_field, value, customized=nil)
254 errs = super
255 errs = super
255 if value.present?
256 value = value.to_s
256 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
257 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
257 errs << ::I18n.t('activerecord.errors.messages.invalid')
258 errs << ::I18n.t('activerecord.errors.messages.invalid')
258 end
259 end
259 if custom_field.min_length && value.length < custom_field.min_length
260 if custom_field.min_length && value.length < custom_field.min_length
260 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
261 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
261 end
262 end
262 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
263 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
263 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
264 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
264 end
265 end
265 end
266 errs
266 errs
267 end
267 end
268 end
268 end
269
269
270 class StringFormat < Unbounded
270 class StringFormat < Unbounded
271 add 'string'
271 add 'string'
272 self.searchable_supported = true
272 self.searchable_supported = true
273 self.form_partial = 'custom_fields/formats/string'
273 self.form_partial = 'custom_fields/formats/string'
274 field_attributes :text_formatting
274 field_attributes :text_formatting
275
275
276 def formatted_value(view, custom_field, value, customized=nil, html=false)
276 def formatted_value(view, custom_field, value, customized=nil, html=false)
277 if html
277 if html
278 if custom_field.url_pattern.present?
278 if custom_field.url_pattern.present?
279 super
279 super
280 elsif custom_field.text_formatting == 'full'
280 elsif custom_field.text_formatting == 'full'
281 view.textilizable(value, :object => customized)
281 view.textilizable(value, :object => customized)
282 else
282 else
283 value.to_s
283 value.to_s
284 end
284 end
285 else
285 else
286 value.to_s
286 value.to_s
287 end
287 end
288 end
288 end
289 end
289 end
290
290
291 class TextFormat < Unbounded
291 class TextFormat < Unbounded
292 add 'text'
292 add 'text'
293 self.searchable_supported = true
293 self.searchable_supported = true
294 self.form_partial = 'custom_fields/formats/text'
294 self.form_partial = 'custom_fields/formats/text'
295
295
296 def formatted_value(view, custom_field, value, customized=nil, html=false)
296 def formatted_value(view, custom_field, value, customized=nil, html=false)
297 if html
297 if html
298 if custom_field.text_formatting == 'full'
298 if custom_field.text_formatting == 'full'
299 view.textilizable(value, :object => customized)
299 view.textilizable(value, :object => customized)
300 else
300 else
301 view.simple_format(html_escape(value))
301 view.simple_format(html_escape(value))
302 end
302 end
303 else
303 else
304 value.to_s
304 value.to_s
305 end
305 end
306 end
306 end
307
307
308 def edit_tag(view, tag_id, tag_name, custom_value, options={})
308 def edit_tag(view, tag_id, tag_name, custom_value, options={})
309 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
309 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
310 end
310 end
311
311
312 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
312 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
313 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
313 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
314 '<br />'.html_safe +
314 '<br />'.html_safe +
315 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
315 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
316 end
316 end
317
317
318 def query_filter_options(custom_field, query)
318 def query_filter_options(custom_field, query)
319 {:type => :text}
319 {:type => :text}
320 end
320 end
321 end
321 end
322
322
323 class LinkFormat < StringFormat
323 class LinkFormat < StringFormat
324 add 'link'
324 add 'link'
325 self.searchable_supported = false
325 self.searchable_supported = false
326 self.form_partial = 'custom_fields/formats/link'
326 self.form_partial = 'custom_fields/formats/link'
327
327
328 def formatted_value(view, custom_field, value, customized=nil, html=false)
328 def formatted_value(view, custom_field, value, customized=nil, html=false)
329 if html
329 if html
330 if custom_field.url_pattern.present?
330 if custom_field.url_pattern.present?
331 url = url_from_pattern(custom_field, value, customized)
331 url = url_from_pattern(custom_field, value, customized)
332 else
332 else
333 url = value.to_s
333 url = value.to_s
334 unless url =~ %r{\A[a-z]+://}i
334 unless url =~ %r{\A[a-z]+://}i
335 # no protocol found, use http by default
335 # no protocol found, use http by default
336 url = "http://" + url
336 url = "http://" + url
337 end
337 end
338 end
338 end
339 view.link_to value.to_s, url
339 view.link_to value.to_s, url
340 else
340 else
341 value.to_s
341 value.to_s
342 end
342 end
343 end
343 end
344 end
344 end
345
345
346 class Numeric < Unbounded
346 class Numeric < Unbounded
347 self.form_partial = 'custom_fields/formats/numeric'
347 self.form_partial = 'custom_fields/formats/numeric'
348
348
349 def order_statement(custom_field)
349 def order_statement(custom_field)
350 # Make the database cast values into numeric
350 # Make the database cast values into numeric
351 # Postgresql will raise an error if a value can not be casted!
351 # Postgresql will raise an error if a value can not be casted!
352 # CustomValue validations should ensure that it doesn't occur
352 # CustomValue validations should ensure that it doesn't occur
353 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
353 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
354 end
354 end
355 end
355 end
356
356
357 class IntFormat < Numeric
357 class IntFormat < Numeric
358 add 'int'
358 add 'int'
359
359
360 def label
360 def label
361 "label_integer"
361 "label_integer"
362 end
362 end
363
363
364 def cast_single_value(custom_field, value, customized=nil)
364 def cast_single_value(custom_field, value, customized=nil)
365 value.to_i
365 value.to_i
366 end
366 end
367
367
368 def validate_single_value(custom_field, value, customized=nil)
368 def validate_single_value(custom_field, value, customized=nil)
369 errs = super
369 errs = super
370 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
370 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
371 errs
371 errs
372 end
372 end
373
373
374 def query_filter_options(custom_field, query)
374 def query_filter_options(custom_field, query)
375 {:type => :integer}
375 {:type => :integer}
376 end
376 end
377
377
378 def group_statement(custom_field)
378 def group_statement(custom_field)
379 order_statement(custom_field)
379 order_statement(custom_field)
380 end
380 end
381 end
381 end
382
382
383 class FloatFormat < Numeric
383 class FloatFormat < Numeric
384 add 'float'
384 add 'float'
385
385
386 def cast_single_value(custom_field, value, customized=nil)
386 def cast_single_value(custom_field, value, customized=nil)
387 value.to_f
387 value.to_f
388 end
388 end
389
389
390 def validate_single_value(custom_field, value, customized=nil)
390 def validate_single_value(custom_field, value, customized=nil)
391 errs = super
391 errs = super
392 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
392 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
393 errs
393 errs
394 end
394 end
395
395
396 def query_filter_options(custom_field, query)
396 def query_filter_options(custom_field, query)
397 {:type => :float}
397 {:type => :float}
398 end
398 end
399 end
399 end
400
400
401 class DateFormat < Unbounded
401 class DateFormat < Unbounded
402 add 'date'
402 add 'date'
403 self.form_partial = 'custom_fields/formats/date'
403 self.form_partial = 'custom_fields/formats/date'
404
404
405 def cast_single_value(custom_field, value, customized=nil)
405 def cast_single_value(custom_field, value, customized=nil)
406 value.to_date rescue nil
406 value.to_date rescue nil
407 end
407 end
408
408
409 def validate_single_value(custom_field, value, customized=nil)
409 def validate_single_value(custom_field, value, customized=nil)
410 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
410 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
411 []
411 []
412 else
412 else
413 [::I18n.t('activerecord.errors.messages.not_a_date')]
413 [::I18n.t('activerecord.errors.messages.not_a_date')]
414 end
414 end
415 end
415 end
416
416
417 def edit_tag(view, tag_id, tag_name, custom_value, options={})
417 def edit_tag(view, tag_id, tag_name, custom_value, options={})
418 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
418 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
419 view.calendar_for(tag_id)
419 view.calendar_for(tag_id)
420 end
420 end
421
421
422 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
422 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
423 view.text_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
423 view.text_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
424 view.calendar_for(tag_id) +
424 view.calendar_for(tag_id) +
425 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
425 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
426 end
426 end
427
427
428 def query_filter_options(custom_field, query)
428 def query_filter_options(custom_field, query)
429 {:type => :date}
429 {:type => :date}
430 end
430 end
431
431
432 def group_statement(custom_field)
432 def group_statement(custom_field)
433 order_statement(custom_field)
433 order_statement(custom_field)
434 end
434 end
435 end
435 end
436
436
437 class List < Base
437 class List < Base
438 self.multiple_supported = true
438 self.multiple_supported = true
439 field_attributes :edit_tag_style
439 field_attributes :edit_tag_style
440
440
441 def edit_tag(view, tag_id, tag_name, custom_value, options={})
441 def edit_tag(view, tag_id, tag_name, custom_value, options={})
442 if custom_value.custom_field.edit_tag_style == 'check_box'
442 if custom_value.custom_field.edit_tag_style == 'check_box'
443 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
443 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
444 else
444 else
445 select_edit_tag(view, tag_id, tag_name, custom_value, options)
445 select_edit_tag(view, tag_id, tag_name, custom_value, options)
446 end
446 end
447 end
447 end
448
448
449 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
449 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
450 opts = []
450 opts = []
451 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
451 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
452 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
452 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
453 opts += possible_values_options(custom_field, objects)
453 opts += possible_values_options(custom_field, objects)
454 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
454 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
455 end
455 end
456
456
457 def query_filter_options(custom_field, query)
457 def query_filter_options(custom_field, query)
458 {:type => :list_optional, :values => possible_values_options(custom_field, query.project)}
458 {:type => :list_optional, :values => possible_values_options(custom_field, query.project)}
459 end
459 end
460
460
461 protected
461 protected
462
462
463 # Renders the edit tag as a select tag
463 # Renders the edit tag as a select tag
464 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
464 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
465 blank_option = ''.html_safe
465 blank_option = ''.html_safe
466 unless custom_value.custom_field.multiple?
466 unless custom_value.custom_field.multiple?
467 if custom_value.custom_field.is_required?
467 if custom_value.custom_field.is_required?
468 unless custom_value.custom_field.default_value.present?
468 unless custom_value.custom_field.default_value.present?
469 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
469 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
470 end
470 end
471 else
471 else
472 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
472 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
473 end
473 end
474 end
474 end
475 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
475 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
476 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
476 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
477 if custom_value.custom_field.multiple?
477 if custom_value.custom_field.multiple?
478 s << view.hidden_field_tag(tag_name, '')
478 s << view.hidden_field_tag(tag_name, '')
479 end
479 end
480 s
480 s
481 end
481 end
482
482
483 # Renders the edit tag as check box or radio tags
483 # Renders the edit tag as check box or radio tags
484 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
484 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
485 opts = []
485 opts = []
486 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
486 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
487 opts << ["(#{l(:label_none)})", '']
487 opts << ["(#{l(:label_none)})", '']
488 end
488 end
489 opts += possible_custom_value_options(custom_value)
489 opts += possible_custom_value_options(custom_value)
490 s = ''.html_safe
490 s = ''.html_safe
491 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
491 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
492 opts.each do |label, value|
492 opts.each do |label, value|
493 value ||= label
493 value ||= label
494 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
494 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
495 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
495 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
496 # set the id on the first tag only
496 # set the id on the first tag only
497 tag_id = nil
497 tag_id = nil
498 s << view.content_tag('label', tag + ' ' + label)
498 s << view.content_tag('label', tag + ' ' + label)
499 end
499 end
500 css = "#{options[:class]} check_box_group"
500 css = "#{options[:class]} check_box_group"
501 view.content_tag('span', s, options.merge(:class => css))
501 view.content_tag('span', s, options.merge(:class => css))
502 end
502 end
503 end
503 end
504
504
505 class ListFormat < List
505 class ListFormat < List
506 add 'list'
506 add 'list'
507 self.searchable_supported = true
507 self.searchable_supported = true
508 self.form_partial = 'custom_fields/formats/list'
508 self.form_partial = 'custom_fields/formats/list'
509
509
510 def possible_custom_value_options(custom_value)
510 def possible_custom_value_options(custom_value)
511 options = possible_values_options(custom_value.custom_field)
511 options = possible_values_options(custom_value.custom_field)
512 missing = [custom_value.value].flatten.reject(&:blank?) - options
512 missing = [custom_value.value].flatten.reject(&:blank?) - options
513 if missing.any?
513 if missing.any?
514 options += missing
514 options += missing
515 end
515 end
516 options
516 options
517 end
517 end
518
518
519 def possible_values_options(custom_field, object=nil)
519 def possible_values_options(custom_field, object=nil)
520 custom_field.possible_values
520 custom_field.possible_values
521 end
521 end
522
522
523 def validate_custom_field(custom_field)
523 def validate_custom_field(custom_field)
524 errors = []
524 errors = []
525 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
525 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
526 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
526 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
527 errors
527 errors
528 end
528 end
529
529
530 def validate_custom_value(custom_value)
530 def validate_custom_value(custom_value)
531 invalid_values = Array.wrap(custom_value.value) - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
531 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
532 if invalid_values.select(&:present?).any?
532 invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
533 if invalid_values.any?
533 [::I18n.t('activerecord.errors.messages.inclusion')]
534 [::I18n.t('activerecord.errors.messages.inclusion')]
534 else
535 else
535 []
536 []
536 end
537 end
537 end
538 end
538
539
539 def group_statement(custom_field)
540 def group_statement(custom_field)
540 order_statement(custom_field)
541 order_statement(custom_field)
541 end
542 end
542 end
543 end
543
544
544 class BoolFormat < List
545 class BoolFormat < List
545 add 'bool'
546 add 'bool'
546 self.multiple_supported = false
547 self.multiple_supported = false
547 self.form_partial = 'custom_fields/formats/bool'
548 self.form_partial = 'custom_fields/formats/bool'
548
549
549 def label
550 def label
550 "label_boolean"
551 "label_boolean"
551 end
552 end
552
553
553 def cast_single_value(custom_field, value, customized=nil)
554 def cast_single_value(custom_field, value, customized=nil)
554 value == '1' ? true : false
555 value == '1' ? true : false
555 end
556 end
556
557
557 def possible_values_options(custom_field, object=nil)
558 def possible_values_options(custom_field, object=nil)
558 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
559 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
559 end
560 end
560
561
561 def group_statement(custom_field)
562 def group_statement(custom_field)
562 order_statement(custom_field)
563 order_statement(custom_field)
563 end
564 end
564 end
565 end
565
566
566 class RecordList < List
567 class RecordList < List
567 self.customized_class_names = %w(Issue TimeEntry Version Project)
568 self.customized_class_names = %w(Issue TimeEntry Version Project)
568
569
569 def cast_single_value(custom_field, value, customized=nil)
570 def cast_single_value(custom_field, value, customized=nil)
570 target_class.find_by_id(value.to_i) if value.present?
571 target_class.find_by_id(value.to_i) if value.present?
571 end
572 end
572
573
573 def target_class
574 def target_class
574 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
575 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
575 end
576 end
576
577
577 def possible_custom_value_options(custom_value)
578 def possible_custom_value_options(custom_value)
578 options = possible_values_options(custom_value.custom_field, custom_value.customized)
579 options = possible_values_options(custom_value.custom_field, custom_value.customized)
579 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
580 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
580 if missing.any?
581 if missing.any?
581 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
582 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
582 #TODO: use #sort_by! when ruby1.8 support is dropped
583 #TODO: use #sort_by! when ruby1.8 support is dropped
583 options = options.sort_by(&:first)
584 options = options.sort_by(&:first)
584 end
585 end
585 options
586 options
586 end
587 end
587
588
588 def order_statement(custom_field)
589 def order_statement(custom_field)
589 if target_class.respond_to?(:fields_for_order_statement)
590 if target_class.respond_to?(:fields_for_order_statement)
590 target_class.fields_for_order_statement(value_join_alias(custom_field))
591 target_class.fields_for_order_statement(value_join_alias(custom_field))
591 end
592 end
592 end
593 end
593
594
594 def group_statement(custom_field)
595 def group_statement(custom_field)
595 "COALESCE(#{join_alias custom_field}.value, '')"
596 "COALESCE(#{join_alias custom_field}.value, '')"
596 end
597 end
597
598
598 def join_for_order_statement(custom_field)
599 def join_for_order_statement(custom_field)
599 alias_name = join_alias(custom_field)
600 alias_name = join_alias(custom_field)
600
601
601 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
602 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
602 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
603 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
603 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
604 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
604 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
605 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
605 " AND (#{custom_field.visibility_by_project_condition})" +
606 " AND (#{custom_field.visibility_by_project_condition})" +
606 " AND #{alias_name}.value <> ''" +
607 " AND #{alias_name}.value <> ''" +
607 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
608 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
608 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
609 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
609 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
610 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
610 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
611 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
611 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
612 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
612 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
613 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
613 end
614 end
614
615
615 def value_join_alias(custom_field)
616 def value_join_alias(custom_field)
616 join_alias(custom_field) + "_" + custom_field.field_format
617 join_alias(custom_field) + "_" + custom_field.field_format
617 end
618 end
618 protected :value_join_alias
619 protected :value_join_alias
619 end
620 end
620
621
621 class UserFormat < RecordList
622 class UserFormat < RecordList
622 add 'user'
623 add 'user'
623 self.form_partial = 'custom_fields/formats/user'
624 self.form_partial = 'custom_fields/formats/user'
624 field_attributes :user_role
625 field_attributes :user_role
625
626
626 def possible_values_options(custom_field, object=nil)
627 def possible_values_options(custom_field, object=nil)
627 if object.is_a?(Array)
628 if object.is_a?(Array)
628 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
629 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
629 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
630 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
630 elsif object.respond_to?(:project) && object.project
631 elsif object.respond_to?(:project) && object.project
631 scope = object.project.users
632 scope = object.project.users
632 if custom_field.user_role.is_a?(Array)
633 if custom_field.user_role.is_a?(Array)
633 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
634 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
634 if role_ids.any?
635 if role_ids.any?
635 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
636 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
636 end
637 end
637 end
638 end
638 scope.sorted.collect {|u| [u.to_s, u.id.to_s]}
639 scope.sorted.collect {|u| [u.to_s, u.id.to_s]}
639 else
640 else
640 []
641 []
641 end
642 end
642 end
643 end
643
644
644 def before_custom_field_save(custom_field)
645 def before_custom_field_save(custom_field)
645 super
646 super
646 if custom_field.user_role.is_a?(Array)
647 if custom_field.user_role.is_a?(Array)
647 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
648 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
648 end
649 end
649 end
650 end
650 end
651 end
651
652
652 class VersionFormat < RecordList
653 class VersionFormat < RecordList
653 add 'version'
654 add 'version'
654 self.form_partial = 'custom_fields/formats/version'
655 self.form_partial = 'custom_fields/formats/version'
655 field_attributes :version_status
656 field_attributes :version_status
656
657
657 def possible_values_options(custom_field, object=nil)
658 def possible_values_options(custom_field, object=nil)
658 if object.is_a?(Array)
659 if object.is_a?(Array)
659 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
660 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
660 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
661 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
661 elsif object.respond_to?(:project) && object.project
662 elsif object.respond_to?(:project) && object.project
662 scope = object.project.shared_versions
663 scope = object.project.shared_versions
663 if custom_field.version_status.is_a?(Array)
664 if custom_field.version_status.is_a?(Array)
664 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
665 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
665 if statuses.any?
666 if statuses.any?
666 scope = scope.where(:status => statuses.map(&:to_s))
667 scope = scope.where(:status => statuses.map(&:to_s))
667 end
668 end
668 end
669 end
669 scope.sort.collect {|u| [u.to_s, u.id.to_s]}
670 scope.sort.collect {|u| [u.to_s, u.id.to_s]}
670 else
671 else
671 []
672 []
672 end
673 end
673 end
674 end
674
675
675 def before_custom_field_save(custom_field)
676 def before_custom_field_save(custom_field)
676 super
677 super
677 if custom_field.version_status.is_a?(Array)
678 if custom_field.version_status.is_a?(Array)
678 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
679 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
679 end
680 end
680 end
681 end
681 end
682 end
682 end
683 end
683 end
684 end
@@ -1,310 +1,318
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class CustomFieldTest < ActiveSupport::TestCase
20 class CustomFieldTest < ActiveSupport::TestCase
21 fixtures :custom_fields
21 fixtures :custom_fields
22
22
23 def test_create
23 def test_create
24 field = UserCustomField.new(:name => 'Money money money', :field_format => 'float')
24 field = UserCustomField.new(:name => 'Money money money', :field_format => 'float')
25 assert field.save
25 assert field.save
26 end
26 end
27
27
28 def test_before_validation
28 def test_before_validation
29 field = CustomField.new(:name => 'test_before_validation', :field_format => 'int')
29 field = CustomField.new(:name => 'test_before_validation', :field_format => 'int')
30 field.searchable = true
30 field.searchable = true
31 assert field.save
31 assert field.save
32 assert_equal false, field.searchable
32 assert_equal false, field.searchable
33 field.searchable = true
33 field.searchable = true
34 assert field.save
34 assert field.save
35 assert_equal false, field.searchable
35 assert_equal false, field.searchable
36 end
36 end
37
37
38 def test_regexp_validation
38 def test_regexp_validation
39 field = IssueCustomField.new(:name => 'regexp', :field_format => 'text', :regexp => '[a-z0-9')
39 field = IssueCustomField.new(:name => 'regexp', :field_format => 'text', :regexp => '[a-z0-9')
40 assert !field.save
40 assert !field.save
41 assert_include I18n.t('activerecord.errors.messages.invalid'),
41 assert_include I18n.t('activerecord.errors.messages.invalid'),
42 field.errors[:regexp]
42 field.errors[:regexp]
43 field.regexp = '[a-z0-9]'
43 field.regexp = '[a-z0-9]'
44 assert field.save
44 assert field.save
45 end
45 end
46
46
47 def test_default_value_should_be_validated
47 def test_default_value_should_be_validated
48 field = CustomField.new(:name => 'Test', :field_format => 'int')
48 field = CustomField.new(:name => 'Test', :field_format => 'int')
49 field.default_value = 'abc'
49 field.default_value = 'abc'
50 assert !field.valid?
50 assert !field.valid?
51 field.default_value = '6'
51 field.default_value = '6'
52 assert field.valid?
52 assert field.valid?
53 end
53 end
54
54
55 def test_default_value_should_not_be_validated_when_blank
55 def test_default_value_should_not_be_validated_when_blank
56 field = CustomField.new(:name => 'Test', :field_format => 'list', :possible_values => ['a', 'b'], :is_required => true, :default_value => '')
56 field = CustomField.new(:name => 'Test', :field_format => 'list', :possible_values => ['a', 'b'], :is_required => true, :default_value => '')
57 assert field.valid?
57 assert field.valid?
58 end
58 end
59
59
60 def test_field_format_should_be_validated
60 def test_field_format_should_be_validated
61 field = CustomField.new(:name => 'Test', :field_format => 'foo')
61 field = CustomField.new(:name => 'Test', :field_format => 'foo')
62 assert !field.valid?
62 assert !field.valid?
63 end
63 end
64
64
65 def test_field_format_validation_should_accept_formats_added_at_runtime
65 def test_field_format_validation_should_accept_formats_added_at_runtime
66 Redmine::FieldFormat.add 'foobar', Class.new(Redmine::FieldFormat::Base)
66 Redmine::FieldFormat.add 'foobar', Class.new(Redmine::FieldFormat::Base)
67
67
68 field = CustomField.new(:name => 'Some Custom Field', :field_format => 'foobar')
68 field = CustomField.new(:name => 'Some Custom Field', :field_format => 'foobar')
69 assert field.valid?, 'field should be valid'
69 assert field.valid?, 'field should be valid'
70 ensure
70 ensure
71 Redmine::FieldFormat.delete 'foobar'
71 Redmine::FieldFormat.delete 'foobar'
72 end
72 end
73
73
74 def test_should_not_change_field_format_of_existing_custom_field
74 def test_should_not_change_field_format_of_existing_custom_field
75 field = CustomField.find(1)
75 field = CustomField.find(1)
76 field.field_format = 'int'
76 field.field_format = 'int'
77 assert_equal 'list', field.field_format
77 assert_equal 'list', field.field_format
78 end
78 end
79
79
80 def test_possible_values_should_accept_an_array
80 def test_possible_values_should_accept_an_array
81 field = CustomField.new
81 field = CustomField.new
82 field.possible_values = ["One value", ""]
82 field.possible_values = ["One value", ""]
83 assert_equal ["One value"], field.possible_values
83 assert_equal ["One value"], field.possible_values
84 end
84 end
85
85
86 def test_possible_values_should_accept_a_string
86 def test_possible_values_should_accept_a_string
87 field = CustomField.new
87 field = CustomField.new
88 field.possible_values = "One value"
88 field.possible_values = "One value"
89 assert_equal ["One value"], field.possible_values
89 assert_equal ["One value"], field.possible_values
90 end
90 end
91
91
92 def test_possible_values_should_accept_a_multiline_string
92 def test_possible_values_should_accept_a_multiline_string
93 field = CustomField.new
93 field = CustomField.new
94 field.possible_values = "One value\nAnd another one \r\n \n"
94 field.possible_values = "One value\nAnd another one \r\n \n"
95 assert_equal ["One value", "And another one"], field.possible_values
95 assert_equal ["One value", "And another one"], field.possible_values
96 end
96 end
97
97
98 if "string".respond_to?(:encoding)
98 if "string".respond_to?(:encoding)
99 def test_possible_values_stored_as_binary_should_be_utf8_encoded
99 def test_possible_values_stored_as_binary_should_be_utf8_encoded
100 field = CustomField.find(11)
100 field = CustomField.find(11)
101 assert_kind_of Array, field.possible_values
101 assert_kind_of Array, field.possible_values
102 assert field.possible_values.size > 0
102 assert field.possible_values.size > 0
103 field.possible_values.each do |value|
103 field.possible_values.each do |value|
104 assert_equal "UTF-8", value.encoding.name
104 assert_equal "UTF-8", value.encoding.name
105 end
105 end
106 end
106 end
107 end
107 end
108
108
109 def test_destroy
109 def test_destroy
110 field = CustomField.find(1)
110 field = CustomField.find(1)
111 assert field.destroy
111 assert field.destroy
112 end
112 end
113
113
114 def test_new_subclass_instance_should_return_an_instance
114 def test_new_subclass_instance_should_return_an_instance
115 f = CustomField.new_subclass_instance('IssueCustomField')
115 f = CustomField.new_subclass_instance('IssueCustomField')
116 assert_kind_of IssueCustomField, f
116 assert_kind_of IssueCustomField, f
117 end
117 end
118
118
119 def test_new_subclass_instance_should_set_attributes
119 def test_new_subclass_instance_should_set_attributes
120 f = CustomField.new_subclass_instance('IssueCustomField', :name => 'Test')
120 f = CustomField.new_subclass_instance('IssueCustomField', :name => 'Test')
121 assert_kind_of IssueCustomField, f
121 assert_kind_of IssueCustomField, f
122 assert_equal 'Test', f.name
122 assert_equal 'Test', f.name
123 end
123 end
124
124
125 def test_new_subclass_instance_with_invalid_class_name_should_return_nil
125 def test_new_subclass_instance_with_invalid_class_name_should_return_nil
126 assert_nil CustomField.new_subclass_instance('WrongClassName')
126 assert_nil CustomField.new_subclass_instance('WrongClassName')
127 end
127 end
128
128
129 def test_new_subclass_instance_with_non_subclass_name_should_return_nil
129 def test_new_subclass_instance_with_non_subclass_name_should_return_nil
130 assert_nil CustomField.new_subclass_instance('Project')
130 assert_nil CustomField.new_subclass_instance('Project')
131 end
131 end
132
132
133 def test_string_field_validation_with_blank_value
133 def test_string_field_validation_with_blank_value
134 f = CustomField.new(:field_format => 'string')
134 f = CustomField.new(:field_format => 'string')
135
135
136 assert f.valid_field_value?(nil)
136 assert f.valid_field_value?(nil)
137 assert f.valid_field_value?('')
137 assert f.valid_field_value?('')
138
138
139 f.is_required = true
139 f.is_required = true
140 assert !f.valid_field_value?(nil)
140 assert !f.valid_field_value?(nil)
141 assert !f.valid_field_value?('')
141 assert !f.valid_field_value?('')
142 end
142 end
143
143
144 def test_string_field_validation_with_min_and_max_lengths
144 def test_string_field_validation_with_min_and_max_lengths
145 f = CustomField.new(:field_format => 'string', :min_length => 2, :max_length => 5)
145 f = CustomField.new(:field_format => 'string', :min_length => 2, :max_length => 5)
146
146
147 assert f.valid_field_value?(nil)
147 assert f.valid_field_value?(nil)
148 assert f.valid_field_value?('')
148 assert f.valid_field_value?('')
149 assert !f.valid_field_value?(' ')
149 assert f.valid_field_value?('a' * 2)
150 assert f.valid_field_value?('a' * 2)
150 assert !f.valid_field_value?('a')
151 assert !f.valid_field_value?('a')
151 assert !f.valid_field_value?('a' * 6)
152 assert !f.valid_field_value?('a' * 6)
152 end
153 end
153
154
154 def test_string_field_validation_with_regexp
155 def test_string_field_validation_with_regexp
155 f = CustomField.new(:field_format => 'string', :regexp => '^[A-Z0-9]*$')
156 f = CustomField.new(:field_format => 'string', :regexp => '^[A-Z0-9]*$')
156
157
157 assert f.valid_field_value?(nil)
158 assert f.valid_field_value?(nil)
158 assert f.valid_field_value?('')
159 assert f.valid_field_value?('')
160 assert !f.valid_field_value?(' ')
159 assert f.valid_field_value?('ABC')
161 assert f.valid_field_value?('ABC')
160 assert !f.valid_field_value?('abc')
162 assert !f.valid_field_value?('abc')
161 end
163 end
162
164
163 def test_date_field_validation
165 def test_date_field_validation
164 f = CustomField.new(:field_format => 'date')
166 f = CustomField.new(:field_format => 'date')
165
167
166 assert f.valid_field_value?(nil)
168 assert f.valid_field_value?(nil)
167 assert f.valid_field_value?('')
169 assert f.valid_field_value?('')
170 assert !f.valid_field_value?(' ')
168 assert f.valid_field_value?('1975-07-14')
171 assert f.valid_field_value?('1975-07-14')
169 assert !f.valid_field_value?('1975-07-33')
172 assert !f.valid_field_value?('1975-07-33')
170 assert !f.valid_field_value?('abc')
173 assert !f.valid_field_value?('abc')
171 end
174 end
172
175
173 def test_list_field_validation
176 def test_list_field_validation
174 f = CustomField.new(:field_format => 'list', :possible_values => ['value1', 'value2'])
177 f = CustomField.new(:field_format => 'list', :possible_values => ['value1', 'value2'])
175
178
176 assert f.valid_field_value?(nil)
179 assert f.valid_field_value?(nil)
177 assert f.valid_field_value?('')
180 assert f.valid_field_value?('')
181 assert !f.valid_field_value?(' ')
178 assert f.valid_field_value?('value2')
182 assert f.valid_field_value?('value2')
179 assert !f.valid_field_value?('abc')
183 assert !f.valid_field_value?('abc')
180 end
184 end
181
185
182 def test_int_field_validation
186 def test_int_field_validation
183 f = CustomField.new(:field_format => 'int')
187 f = CustomField.new(:field_format => 'int')
184
188
185 assert f.valid_field_value?(nil)
189 assert f.valid_field_value?(nil)
186 assert f.valid_field_value?('')
190 assert f.valid_field_value?('')
191 assert !f.valid_field_value?(' ')
187 assert f.valid_field_value?('123')
192 assert f.valid_field_value?('123')
188 assert f.valid_field_value?('+123')
193 assert f.valid_field_value?('+123')
189 assert f.valid_field_value?('-123')
194 assert f.valid_field_value?('-123')
190 assert !f.valid_field_value?('6abc')
195 assert !f.valid_field_value?('6abc')
191 end
196 end
192
197
193 def test_float_field_validation
198 def test_float_field_validation
194 f = CustomField.new(:field_format => 'float')
199 f = CustomField.new(:field_format => 'float')
195
200
196 assert f.valid_field_value?(nil)
201 assert f.valid_field_value?(nil)
197 assert f.valid_field_value?('')
202 assert f.valid_field_value?('')
203 assert !f.valid_field_value?(' ')
198 assert f.valid_field_value?('11.2')
204 assert f.valid_field_value?('11.2')
199 assert f.valid_field_value?('-6.250')
205 assert f.valid_field_value?('-6.250')
200 assert f.valid_field_value?('5')
206 assert f.valid_field_value?('5')
201 assert !f.valid_field_value?('6abc')
207 assert !f.valid_field_value?('6abc')
202 end
208 end
203
209
204 def test_multi_field_validation
210 def test_multi_field_validation
205 f = CustomField.new(:field_format => 'list', :multiple => 'true', :possible_values => ['value1', 'value2'])
211 f = CustomField.new(:field_format => 'list', :multiple => 'true', :possible_values => ['value1', 'value2'])
206
212
207 assert f.valid_field_value?(nil)
213 assert f.valid_field_value?(nil)
208 assert f.valid_field_value?('')
214 assert f.valid_field_value?('')
215 assert !f.valid_field_value?(' ')
209 assert f.valid_field_value?([])
216 assert f.valid_field_value?([])
210 assert f.valid_field_value?([nil])
217 assert f.valid_field_value?([nil])
211 assert f.valid_field_value?([''])
218 assert f.valid_field_value?([''])
219 assert !f.valid_field_value?([' '])
212
220
213 assert f.valid_field_value?('value2')
221 assert f.valid_field_value?('value2')
214 assert !f.valid_field_value?('abc')
222 assert !f.valid_field_value?('abc')
215
223
216 assert f.valid_field_value?(['value2'])
224 assert f.valid_field_value?(['value2'])
217 assert !f.valid_field_value?(['abc'])
225 assert !f.valid_field_value?(['abc'])
218
226
219 assert f.valid_field_value?(['', 'value2'])
227 assert f.valid_field_value?(['', 'value2'])
220 assert !f.valid_field_value?(['', 'abc'])
228 assert !f.valid_field_value?(['', 'abc'])
221
229
222 assert f.valid_field_value?(['value1', 'value2'])
230 assert f.valid_field_value?(['value1', 'value2'])
223 assert !f.valid_field_value?(['value1', 'abc'])
231 assert !f.valid_field_value?(['value1', 'abc'])
224 end
232 end
225
233
226 def test_changing_multiple_to_false_should_delete_multiple_values
234 def test_changing_multiple_to_false_should_delete_multiple_values
227 field = ProjectCustomField.create!(:name => 'field', :field_format => 'list', :multiple => 'true', :possible_values => ['field1', 'field2'])
235 field = ProjectCustomField.create!(:name => 'field', :field_format => 'list', :multiple => 'true', :possible_values => ['field1', 'field2'])
228 other = ProjectCustomField.create!(:name => 'other', :field_format => 'list', :multiple => 'true', :possible_values => ['other1', 'other2'])
236 other = ProjectCustomField.create!(:name => 'other', :field_format => 'list', :multiple => 'true', :possible_values => ['other1', 'other2'])
229
237
230 item_with_multiple_values = Project.generate!(:custom_field_values => {field.id => ['field1', 'field2'], other.id => ['other1', 'other2']})
238 item_with_multiple_values = Project.generate!(:custom_field_values => {field.id => ['field1', 'field2'], other.id => ['other1', 'other2']})
231 item_with_single_values = Project.generate!(:custom_field_values => {field.id => ['field1'], other.id => ['other2']})
239 item_with_single_values = Project.generate!(:custom_field_values => {field.id => ['field1'], other.id => ['other2']})
232
240
233 assert_difference 'CustomValue.count', -1 do
241 assert_difference 'CustomValue.count', -1 do
234 field.multiple = false
242 field.multiple = false
235 field.save!
243 field.save!
236 end
244 end
237
245
238 item_with_multiple_values = Project.find(item_with_multiple_values.id)
246 item_with_multiple_values = Project.find(item_with_multiple_values.id)
239 assert_kind_of String, item_with_multiple_values.custom_field_value(field)
247 assert_kind_of String, item_with_multiple_values.custom_field_value(field)
240 assert_kind_of Array, item_with_multiple_values.custom_field_value(other)
248 assert_kind_of Array, item_with_multiple_values.custom_field_value(other)
241 assert_equal 2, item_with_multiple_values.custom_field_value(other).size
249 assert_equal 2, item_with_multiple_values.custom_field_value(other).size
242 end
250 end
243
251
244 def test_value_class_should_return_the_class_used_for_fields_values
252 def test_value_class_should_return_the_class_used_for_fields_values
245 assert_equal User, CustomField.new(:field_format => 'user').value_class
253 assert_equal User, CustomField.new(:field_format => 'user').value_class
246 assert_equal Version, CustomField.new(:field_format => 'version').value_class
254 assert_equal Version, CustomField.new(:field_format => 'version').value_class
247 end
255 end
248
256
249 def test_value_class_should_return_nil_for_other_fields
257 def test_value_class_should_return_nil_for_other_fields
250 assert_nil CustomField.new(:field_format => 'text').value_class
258 assert_nil CustomField.new(:field_format => 'text').value_class
251 assert_nil CustomField.new.value_class
259 assert_nil CustomField.new.value_class
252 end
260 end
253
261
254 def test_value_from_keyword_for_list_custom_field
262 def test_value_from_keyword_for_list_custom_field
255 field = CustomField.find(1)
263 field = CustomField.find(1)
256 assert_equal 'PostgreSQL', field.value_from_keyword('postgresql', Issue.find(1))
264 assert_equal 'PostgreSQL', field.value_from_keyword('postgresql', Issue.find(1))
257 end
265 end
258
266
259 def test_visibile_scope_with_admin_should_return_all_custom_fields
267 def test_visibile_scope_with_admin_should_return_all_custom_fields
260 CustomField.delete_all
268 CustomField.delete_all
261 fields = [
269 fields = [
262 CustomField.generate!(:visible => true),
270 CustomField.generate!(:visible => true),
263 CustomField.generate!(:visible => false),
271 CustomField.generate!(:visible => false),
264 CustomField.generate!(:visible => false, :role_ids => [1, 3]),
272 CustomField.generate!(:visible => false, :role_ids => [1, 3]),
265 CustomField.generate!(:visible => false, :role_ids => [1, 2]),
273 CustomField.generate!(:visible => false, :role_ids => [1, 2]),
266 ]
274 ]
267
275
268 assert_equal 4, CustomField.visible(User.find(1)).count
276 assert_equal 4, CustomField.visible(User.find(1)).count
269 end
277 end
270
278
271 def test_visibile_scope_with_non_admin_user_should_return_visible_custom_fields
279 def test_visibile_scope_with_non_admin_user_should_return_visible_custom_fields
272 CustomField.delete_all
280 CustomField.delete_all
273 fields = [
281 fields = [
274 CustomField.generate!(:visible => true),
282 CustomField.generate!(:visible => true),
275 CustomField.generate!(:visible => false),
283 CustomField.generate!(:visible => false),
276 CustomField.generate!(:visible => false, :role_ids => [1, 3]),
284 CustomField.generate!(:visible => false, :role_ids => [1, 3]),
277 CustomField.generate!(:visible => false, :role_ids => [1, 2]),
285 CustomField.generate!(:visible => false, :role_ids => [1, 2]),
278 ]
286 ]
279 user = User.generate!
287 user = User.generate!
280 User.add_to_project(user, Project.first, Role.find(3))
288 User.add_to_project(user, Project.first, Role.find(3))
281
289
282 assert_equal [fields[0], fields[2]], CustomField.visible(user).order("id").to_a
290 assert_equal [fields[0], fields[2]], CustomField.visible(user).order("id").to_a
283 end
291 end
284
292
285 def test_visibile_scope_with_anonymous_user_should_return_visible_custom_fields
293 def test_visibile_scope_with_anonymous_user_should_return_visible_custom_fields
286 CustomField.delete_all
294 CustomField.delete_all
287 fields = [
295 fields = [
288 CustomField.generate!(:visible => true),
296 CustomField.generate!(:visible => true),
289 CustomField.generate!(:visible => false),
297 CustomField.generate!(:visible => false),
290 CustomField.generate!(:visible => false, :role_ids => [1, 3]),
298 CustomField.generate!(:visible => false, :role_ids => [1, 3]),
291 CustomField.generate!(:visible => false, :role_ids => [1, 2]),
299 CustomField.generate!(:visible => false, :role_ids => [1, 2]),
292 ]
300 ]
293
301
294 assert_equal [fields[0]], CustomField.visible(User.anonymous).order("id").to_a
302 assert_equal [fields[0]], CustomField.visible(User.anonymous).order("id").to_a
295 end
303 end
296
304
297 def test_float_cast_blank_value_should_return_nil
305 def test_float_cast_blank_value_should_return_nil
298 field = CustomField.new(:field_format => 'float')
306 field = CustomField.new(:field_format => 'float')
299 assert_equal nil, field.cast_value(nil)
307 assert_equal nil, field.cast_value(nil)
300 assert_equal nil, field.cast_value('')
308 assert_equal nil, field.cast_value('')
301 end
309 end
302
310
303 def test_float_cast_valid_value_should_return_float
311 def test_float_cast_valid_value_should_return_float
304 field = CustomField.new(:field_format => 'float')
312 field = CustomField.new(:field_format => 'float')
305 assert_equal 12.0, field.cast_value('12')
313 assert_equal 12.0, field.cast_value('12')
306 assert_equal 12.5, field.cast_value('12.5')
314 assert_equal 12.5, field.cast_value('12.5')
307 assert_equal 12.5, field.cast_value('+12.5')
315 assert_equal 12.5, field.cast_value('+12.5')
308 assert_equal -12.5, field.cast_value('-12.5')
316 assert_equal -12.5, field.cast_value('-12.5')
309 end
317 end
310 end
318 end
General Comments 0
You need to be logged in to leave comments. Login now