##// END OF EJS Templates
Merged r14079 (#19316)....
Jean-Philippe Lang -
r13712:97fc28da3a56
parent child
Show More
@@ -1,283 +1,283
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class CustomField < ActiveRecord::Base
19 19 include Redmine::SubclassFactory
20 20
21 21 has_many :custom_values, :dependent => :delete_all
22 22 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
23 23 acts_as_list :scope => 'type = \'#{self.class}\''
24 24 serialize :possible_values
25 25 store :format_store
26 26
27 27 validates_presence_of :name, :field_format
28 28 validates_uniqueness_of :name, :scope => :type
29 29 validates_length_of :name, :maximum => 30
30 30 validates_inclusion_of :field_format, :in => Proc.new { Redmine::FieldFormat.available_formats }
31 31 validate :validate_custom_field
32 32 attr_protected :id
33 33
34 34 before_validation :set_searchable
35 35 before_save do |field|
36 36 field.format.before_custom_field_save(field)
37 37 end
38 38 after_save :handle_multiplicity_change
39 39 after_save do |field|
40 40 if field.visible_changed? && field.visible
41 41 field.roles.clear
42 42 end
43 43 end
44 44
45 45 scope :sorted, lambda { order(:position) }
46 46 scope :visible, lambda {|*args|
47 47 user = args.shift || User.current
48 48 if user.admin?
49 49 # nop
50 50 elsif user.memberships.any?
51 51 where("#{table_name}.visible = ? OR #{table_name}.id IN (SELECT DISTINCT cfr.custom_field_id FROM #{Member.table_name} m" +
52 52 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
53 53 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
54 54 " WHERE m.user_id = ?)",
55 55 true, user.id)
56 56 else
57 57 where(:visible => true)
58 58 end
59 59 }
60 60
61 61 def visible_by?(project, user=User.current)
62 62 visible? || user.admin?
63 63 end
64 64
65 65 def format
66 66 @format ||= Redmine::FieldFormat.find(field_format)
67 67 end
68 68
69 69 def field_format=(arg)
70 70 # cannot change format of a saved custom field
71 71 if new_record?
72 72 @format = nil
73 73 super
74 74 end
75 75 end
76 76
77 77 def set_searchable
78 78 # make sure these fields are not searchable
79 79 self.searchable = false unless format.class.searchable_supported
80 80 # make sure only these fields can have multiple values
81 81 self.multiple = false unless format.class.multiple_supported
82 82 true
83 83 end
84 84
85 85 def validate_custom_field
86 86 format.validate_custom_field(self).each do |attribute, message|
87 87 errors.add attribute, message
88 88 end
89 89
90 90 if regexp.present?
91 91 begin
92 92 Regexp.new(regexp)
93 93 rescue
94 94 errors.add(:regexp, :invalid)
95 95 end
96 96 end
97 97
98 98 if default_value.present?
99 99 validate_field_value(default_value).each do |message|
100 100 errors.add :default_value, message
101 101 end
102 102 end
103 103 end
104 104
105 105 def possible_custom_value_options(custom_value)
106 106 format.possible_custom_value_options(custom_value)
107 107 end
108 108
109 109 def possible_values_options(object=nil)
110 110 if object.is_a?(Array)
111 111 object.map {|o| format.possible_values_options(self, o)}.reduce(:&) || []
112 112 else
113 113 format.possible_values_options(self, object) || []
114 114 end
115 115 end
116 116
117 117 def possible_values
118 118 values = read_attribute(:possible_values)
119 119 if values.is_a?(Array)
120 120 values.each do |value|
121 value.force_encoding('UTF-8')
121 value.to_s.force_encoding('UTF-8')
122 122 end
123 123 values
124 124 else
125 125 []
126 126 end
127 127 end
128 128
129 129 # Makes possible_values accept a multiline string
130 130 def possible_values=(arg)
131 131 if arg.is_a?(Array)
132 values = arg.compact.collect(&:strip).select {|v| !v.blank?}
132 values = arg.compact.map {|a| a.to_s.strip}.reject(&:blank?)
133 133 write_attribute(:possible_values, values)
134 134 else
135 135 self.possible_values = arg.to_s.split(/[\n\r]+/)
136 136 end
137 137 end
138 138
139 139 def cast_value(value)
140 140 format.cast_value(self, value)
141 141 end
142 142
143 143 def value_from_keyword(keyword, customized)
144 144 possible_values_options = possible_values_options(customized)
145 145 if possible_values_options.present?
146 146 keyword = keyword.to_s.downcase
147 147 if v = possible_values_options.detect {|text, id| text.downcase == keyword}
148 148 if v.is_a?(Array)
149 149 v.last
150 150 else
151 151 v
152 152 end
153 153 end
154 154 else
155 155 keyword
156 156 end
157 157 end
158 158
159 159 # Returns a ORDER BY clause that can used to sort customized
160 160 # objects by their value of the custom field.
161 161 # Returns nil if the custom field can not be used for sorting.
162 162 def order_statement
163 163 return nil if multiple?
164 164 format.order_statement(self)
165 165 end
166 166
167 167 # Returns a GROUP BY clause that can used to group by custom value
168 168 # Returns nil if the custom field can not be used for grouping.
169 169 def group_statement
170 170 return nil if multiple?
171 171 format.group_statement(self)
172 172 end
173 173
174 174 def join_for_order_statement
175 175 format.join_for_order_statement(self)
176 176 end
177 177
178 178 def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil)
179 179 if visible? || user.admin?
180 180 "1=1"
181 181 elsif user.anonymous?
182 182 "1=0"
183 183 else
184 184 project_key ||= "#{self.class.customized_class.table_name}.project_id"
185 185 id_column ||= id
186 186 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
187 187 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
188 188 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
189 189 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id_column})"
190 190 end
191 191 end
192 192
193 193 def self.visibility_condition
194 194 if user.admin?
195 195 "1=1"
196 196 elsif user.anonymous?
197 197 "#{table_name}.visible"
198 198 else
199 199 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
200 200 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
201 201 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
202 202 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
203 203 end
204 204 end
205 205
206 206 def <=>(field)
207 207 position <=> field.position
208 208 end
209 209
210 210 # Returns the class that values represent
211 211 def value_class
212 212 format.target_class if format.respond_to?(:target_class)
213 213 end
214 214
215 215 def self.customized_class
216 216 self.name =~ /^(.+)CustomField$/
217 217 $1.constantize rescue nil
218 218 end
219 219
220 220 # to move in project_custom_field
221 221 def self.for_all
222 222 where(:is_for_all => true).order('position').to_a
223 223 end
224 224
225 225 def type_name
226 226 nil
227 227 end
228 228
229 229 # Returns the error messages for the given value
230 230 # or an empty array if value is a valid value for the custom field
231 231 def validate_custom_value(custom_value)
232 232 value = custom_value.value
233 233 errs = []
234 234 if value.is_a?(Array)
235 235 if !multiple?
236 236 errs << ::I18n.t('activerecord.errors.messages.invalid')
237 237 end
238 238 if is_required? && value.detect(&:present?).nil?
239 239 errs << ::I18n.t('activerecord.errors.messages.blank')
240 240 end
241 241 else
242 242 if is_required? && value.blank?
243 243 errs << ::I18n.t('activerecord.errors.messages.blank')
244 244 end
245 245 end
246 246 errs += format.validate_custom_value(custom_value)
247 247 errs
248 248 end
249 249
250 250 # Returns the error messages for the default custom field value
251 251 def validate_field_value(value)
252 252 validate_custom_value(CustomFieldValue.new(:custom_field => self, :value => value))
253 253 end
254 254
255 255 # Returns true if value is a valid value for the custom field
256 256 def valid_field_value?(value)
257 257 validate_field_value(value).empty?
258 258 end
259 259
260 260 def format_in?(*args)
261 261 args.include?(field_format)
262 262 end
263 263
264 264 protected
265 265
266 266 # Removes multiple values for the custom field after setting the multiple attribute to false
267 267 # We kepp the value with the highest id for each customized object
268 268 def handle_multiplicity_change
269 269 if !new_record? && multiple_was && !multiple
270 270 ids = custom_values.
271 271 where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" +
272 272 " AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" +
273 273 " AND cve.id > #{CustomValue.table_name}.id)").
274 274 pluck(:id)
275 275
276 276 if ids.any?
277 277 custom_values.where(:id => ids).delete_all
278 278 end
279 279 end
280 280 end
281 281 end
282 282
283 283 require_dependency 'redmine/field_format'
@@ -1,321 +1,335
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class CustomFieldTest < ActiveSupport::TestCase
21 21 fixtures :custom_fields, :roles, :projects,
22 22 :trackers, :issue_statuses,
23 23 :issues
24 24
25 25 def test_create
26 26 field = UserCustomField.new(:name => 'Money money money', :field_format => 'float')
27 27 assert field.save
28 28 end
29 29
30 30 def test_before_validation
31 31 field = CustomField.new(:name => 'test_before_validation', :field_format => 'int')
32 32 field.searchable = true
33 33 assert field.save
34 34 assert_equal false, field.searchable
35 35 field.searchable = true
36 36 assert field.save
37 37 assert_equal false, field.searchable
38 38 end
39 39
40 40 def test_regexp_validation
41 41 field = IssueCustomField.new(:name => 'regexp', :field_format => 'text', :regexp => '[a-z0-9')
42 42 assert !field.save
43 43 assert_include I18n.t('activerecord.errors.messages.invalid'),
44 44 field.errors[:regexp]
45 45 field.regexp = '[a-z0-9]'
46 46 assert field.save
47 47 end
48 48
49 49 def test_default_value_should_be_validated
50 50 field = CustomField.new(:name => 'Test', :field_format => 'int')
51 51 field.default_value = 'abc'
52 52 assert !field.valid?
53 53 field.default_value = '6'
54 54 assert field.valid?
55 55 end
56 56
57 57 def test_default_value_should_not_be_validated_when_blank
58 58 field = CustomField.new(:name => 'Test', :field_format => 'list', :possible_values => ['a', 'b'], :is_required => true, :default_value => '')
59 59 assert field.valid?
60 60 end
61 61
62 62 def test_field_format_should_be_validated
63 63 field = CustomField.new(:name => 'Test', :field_format => 'foo')
64 64 assert !field.valid?
65 65 end
66 66
67 67 def test_field_format_validation_should_accept_formats_added_at_runtime
68 68 Redmine::FieldFormat.add 'foobar', Class.new(Redmine::FieldFormat::Base)
69 69
70 70 field = CustomField.new(:name => 'Some Custom Field', :field_format => 'foobar')
71 71 assert field.valid?, 'field should be valid'
72 72 ensure
73 73 Redmine::FieldFormat.delete 'foobar'
74 74 end
75 75
76 76 def test_should_not_change_field_format_of_existing_custom_field
77 77 field = CustomField.find(1)
78 78 field.field_format = 'int'
79 79 assert_equal 'list', field.field_format
80 80 end
81 81
82 82 def test_possible_values_should_accept_an_array
83 83 field = CustomField.new
84 84 field.possible_values = ["One value", ""]
85 85 assert_equal ["One value"], field.possible_values
86 86 end
87 87
88 def test_possible_values_should_stringify_values
89 field = CustomField.new
90 field.possible_values = [1, 2]
91 assert_equal ["1", "2"], field.possible_values
92 end
93
88 94 def test_possible_values_should_accept_a_string
89 95 field = CustomField.new
90 96 field.possible_values = "One value"
91 97 assert_equal ["One value"], field.possible_values
92 98 end
93 99
100 def test_possible_values_should_return_utf8_encoded_strings
101 field = CustomField.new
102 s = "Value".force_encoding('BINARY')
103 field.possible_values = s
104 assert_equal [s], field.possible_values
105 assert_equal 'UTF-8', field.possible_values.first.encoding.name
106 end
107
94 108 def test_possible_values_should_accept_a_multiline_string
95 109 field = CustomField.new
96 110 field.possible_values = "One value\nAnd another one \r\n \n"
97 111 assert_equal ["One value", "And another one"], field.possible_values
98 112 end
99 113
100 114 def test_possible_values_stored_as_binary_should_be_utf8_encoded
101 115 field = CustomField.find(11)
102 116 assert_kind_of Array, field.possible_values
103 117 assert field.possible_values.size > 0
104 118 field.possible_values.each do |value|
105 119 assert_equal "UTF-8", value.encoding.name
106 120 end
107 121 end
108 122
109 123 def test_destroy
110 124 field = CustomField.find(1)
111 125 assert field.destroy
112 126 end
113 127
114 128 def test_new_subclass_instance_should_return_an_instance
115 129 f = CustomField.new_subclass_instance('IssueCustomField')
116 130 assert_kind_of IssueCustomField, f
117 131 end
118 132
119 133 def test_new_subclass_instance_should_set_attributes
120 134 f = CustomField.new_subclass_instance('IssueCustomField', :name => 'Test')
121 135 assert_kind_of IssueCustomField, f
122 136 assert_equal 'Test', f.name
123 137 end
124 138
125 139 def test_new_subclass_instance_with_invalid_class_name_should_return_nil
126 140 assert_nil CustomField.new_subclass_instance('WrongClassName')
127 141 end
128 142
129 143 def test_new_subclass_instance_with_non_subclass_name_should_return_nil
130 144 assert_nil CustomField.new_subclass_instance('Project')
131 145 end
132 146
133 147 def test_string_field_validation_with_blank_value
134 148 f = CustomField.new(:field_format => 'string')
135 149
136 150 assert f.valid_field_value?(nil)
137 151 assert f.valid_field_value?('')
138 152
139 153 f.is_required = true
140 154 assert !f.valid_field_value?(nil)
141 155 assert !f.valid_field_value?('')
142 156 end
143 157
144 158 def test_string_field_validation_with_min_and_max_lengths
145 159 f = CustomField.new(:field_format => 'string', :min_length => 2, :max_length => 5)
146 160
147 161 assert f.valid_field_value?(nil)
148 162 assert f.valid_field_value?('')
149 163 assert !f.valid_field_value?(' ')
150 164 assert f.valid_field_value?('a' * 2)
151 165 assert !f.valid_field_value?('a')
152 166 assert !f.valid_field_value?('a' * 6)
153 167 end
154 168
155 169 def test_string_field_validation_with_regexp
156 170 f = CustomField.new(:field_format => 'string', :regexp => '^[A-Z0-9]*$')
157 171
158 172 assert f.valid_field_value?(nil)
159 173 assert f.valid_field_value?('')
160 174 assert !f.valid_field_value?(' ')
161 175 assert f.valid_field_value?('ABC')
162 176 assert !f.valid_field_value?('abc')
163 177 end
164 178
165 179 def test_date_field_validation
166 180 f = CustomField.new(:field_format => 'date')
167 181
168 182 assert f.valid_field_value?(nil)
169 183 assert f.valid_field_value?('')
170 184 assert !f.valid_field_value?(' ')
171 185 assert f.valid_field_value?('1975-07-14')
172 186 assert !f.valid_field_value?('1975-07-33')
173 187 assert !f.valid_field_value?('abc')
174 188 end
175 189
176 190 def test_list_field_validation
177 191 f = CustomField.new(:field_format => 'list', :possible_values => ['value1', 'value2'])
178 192
179 193 assert f.valid_field_value?(nil)
180 194 assert f.valid_field_value?('')
181 195 assert !f.valid_field_value?(' ')
182 196 assert f.valid_field_value?('value2')
183 197 assert !f.valid_field_value?('abc')
184 198 end
185 199
186 200 def test_int_field_validation
187 201 f = CustomField.new(:field_format => 'int')
188 202
189 203 assert f.valid_field_value?(nil)
190 204 assert f.valid_field_value?('')
191 205 assert !f.valid_field_value?(' ')
192 206 assert f.valid_field_value?('123')
193 207 assert f.valid_field_value?('+123')
194 208 assert f.valid_field_value?('-123')
195 209 assert !f.valid_field_value?('6abc')
196 210 assert f.valid_field_value?(123)
197 211 end
198 212
199 213 def test_float_field_validation
200 214 f = CustomField.new(:field_format => 'float')
201 215
202 216 assert f.valid_field_value?(nil)
203 217 assert f.valid_field_value?('')
204 218 assert !f.valid_field_value?(' ')
205 219 assert f.valid_field_value?('11.2')
206 220 assert f.valid_field_value?('-6.250')
207 221 assert f.valid_field_value?('5')
208 222 assert !f.valid_field_value?('6abc')
209 223 assert f.valid_field_value?(11.2)
210 224 end
211 225
212 226 def test_multi_field_validation
213 227 f = CustomField.new(:field_format => 'list', :multiple => 'true', :possible_values => ['value1', 'value2'])
214 228
215 229 assert f.valid_field_value?(nil)
216 230 assert f.valid_field_value?('')
217 231 assert !f.valid_field_value?(' ')
218 232 assert f.valid_field_value?([])
219 233 assert f.valid_field_value?([nil])
220 234 assert f.valid_field_value?([''])
221 235 assert !f.valid_field_value?([' '])
222 236
223 237 assert f.valid_field_value?('value2')
224 238 assert !f.valid_field_value?('abc')
225 239
226 240 assert f.valid_field_value?(['value2'])
227 241 assert !f.valid_field_value?(['abc'])
228 242
229 243 assert f.valid_field_value?(['', 'value2'])
230 244 assert !f.valid_field_value?(['', 'abc'])
231 245
232 246 assert f.valid_field_value?(['value1', 'value2'])
233 247 assert !f.valid_field_value?(['value1', 'abc'])
234 248 end
235 249
236 250 def test_changing_multiple_to_false_should_delete_multiple_values
237 251 field = ProjectCustomField.create!(:name => 'field', :field_format => 'list', :multiple => 'true', :possible_values => ['field1', 'field2'])
238 252 other = ProjectCustomField.create!(:name => 'other', :field_format => 'list', :multiple => 'true', :possible_values => ['other1', 'other2'])
239 253
240 254 item_with_multiple_values = Project.generate!(:custom_field_values => {field.id => ['field1', 'field2'], other.id => ['other1', 'other2']})
241 255 item_with_single_values = Project.generate!(:custom_field_values => {field.id => ['field1'], other.id => ['other2']})
242 256
243 257 assert_difference 'CustomValue.count', -1 do
244 258 field.multiple = false
245 259 field.save!
246 260 end
247 261
248 262 item_with_multiple_values = Project.find(item_with_multiple_values.id)
249 263 assert_kind_of String, item_with_multiple_values.custom_field_value(field)
250 264 assert_kind_of Array, item_with_multiple_values.custom_field_value(other)
251 265 assert_equal 2, item_with_multiple_values.custom_field_value(other).size
252 266 end
253 267
254 268 def test_value_class_should_return_the_class_used_for_fields_values
255 269 assert_equal User, CustomField.new(:field_format => 'user').value_class
256 270 assert_equal Version, CustomField.new(:field_format => 'version').value_class
257 271 end
258 272
259 273 def test_value_class_should_return_nil_for_other_fields
260 274 assert_nil CustomField.new(:field_format => 'text').value_class
261 275 assert_nil CustomField.new.value_class
262 276 end
263 277
264 278 def test_value_from_keyword_for_list_custom_field
265 279 field = CustomField.find(1)
266 280 assert_equal 'PostgreSQL', field.value_from_keyword('postgresql', Issue.find(1))
267 281 end
268 282
269 283 def test_visibile_scope_with_admin_should_return_all_custom_fields
270 284 admin = User.generate! {|user| user.admin = true}
271 285 CustomField.delete_all
272 286 fields = [
273 287 CustomField.generate!(:visible => true),
274 288 CustomField.generate!(:visible => false),
275 289 CustomField.generate!(:visible => false, :role_ids => [1, 3]),
276 290 CustomField.generate!(:visible => false, :role_ids => [1, 2]),
277 291 ]
278 292
279 293 assert_equal 4, CustomField.visible(admin).count
280 294 end
281 295
282 296 def test_visibile_scope_with_non_admin_user_should_return_visible_custom_fields
283 297 CustomField.delete_all
284 298 fields = [
285 299 CustomField.generate!(:visible => true),
286 300 CustomField.generate!(:visible => false),
287 301 CustomField.generate!(:visible => false, :role_ids => [1, 3]),
288 302 CustomField.generate!(:visible => false, :role_ids => [1, 2]),
289 303 ]
290 304 user = User.generate!
291 305 User.add_to_project(user, Project.first, Role.find(3))
292 306
293 307 assert_equal [fields[0], fields[2]], CustomField.visible(user).order("id").to_a
294 308 end
295 309
296 310 def test_visibile_scope_with_anonymous_user_should_return_visible_custom_fields
297 311 CustomField.delete_all
298 312 fields = [
299 313 CustomField.generate!(:visible => true),
300 314 CustomField.generate!(:visible => false),
301 315 CustomField.generate!(:visible => false, :role_ids => [1, 3]),
302 316 CustomField.generate!(:visible => false, :role_ids => [1, 2]),
303 317 ]
304 318
305 319 assert_equal [fields[0]], CustomField.visible(User.anonymous).order("id").to_a
306 320 end
307 321
308 322 def test_float_cast_blank_value_should_return_nil
309 323 field = CustomField.new(:field_format => 'float')
310 324 assert_equal nil, field.cast_value(nil)
311 325 assert_equal nil, field.cast_value('')
312 326 end
313 327
314 328 def test_float_cast_valid_value_should_return_float
315 329 field = CustomField.new(:field_format => 'float')
316 330 assert_equal 12.0, field.cast_value('12')
317 331 assert_equal 12.5, field.cast_value('12.5')
318 332 assert_equal 12.5, field.cast_value('+12.5')
319 333 assert_equal -12.5, field.cast_value('-12.5')
320 334 end
321 335 end
General Comments 0
You need to be logged in to leave comments. Login now