##// END OF EJS Templates
move CustomFieldsHelper tabs variable to model constant for common use (#12018)...
Toshi MARUYAMA -
r10343:fb4210b6d969
parent child
Show More
@@ -1,158 +1,149
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module CustomFieldsHelper
21 21
22 22 def custom_fields_tabs
23 tabs = [{:name => 'IssueCustomField', :partial => 'custom_fields/index', :label => :label_issue_plural},
24 {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index', :label => :label_spent_time},
25 {:name => 'ProjectCustomField', :partial => 'custom_fields/index', :label => :label_project_plural},
26 {:name => 'VersionCustomField', :partial => 'custom_fields/index', :label => :label_version_plural},
27 {:name => 'UserCustomField', :partial => 'custom_fields/index', :label => :label_user_plural},
28 {:name => 'GroupCustomField', :partial => 'custom_fields/index', :label => :label_group_plural},
29 {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index', :label => TimeEntryActivity::OptionName},
30 {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index', :label => IssuePriority::OptionName},
31 {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index', :label => DocumentCategory::OptionName}
32 ]
23 CustomField::CUSTOM_FIELDS_TABS
33 24 end
34 25
35 26 # Return custom field html tag corresponding to its format
36 27 def custom_field_tag(name, custom_value)
37 28 custom_field = custom_value.custom_field
38 29 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
39 30 field_name << "[]" if custom_field.multiple?
40 31 field_id = "#{name}_custom_field_values_#{custom_field.id}"
41 32
42 33 tag_options = {:id => field_id, :class => "#{custom_field.field_format}_cf"}
43 34
44 35 field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
45 36 case field_format.try(:edit_as)
46 37 when "date"
47 38 text_field_tag(field_name, custom_value.value, tag_options.merge(:size => 10)) +
48 39 calendar_for(field_id)
49 40 when "text"
50 41 text_area_tag(field_name, custom_value.value, tag_options.merge(:rows => 3))
51 42 when "bool"
52 43 hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, tag_options)
53 44 when "list"
54 45 blank_option = ''.html_safe
55 46 unless custom_field.multiple?
56 47 if custom_field.is_required?
57 48 unless custom_field.default_value.present?
58 49 blank_option = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
59 50 end
60 51 else
61 52 blank_option = content_tag('option')
62 53 end
63 54 end
64 55 s = select_tag(field_name, blank_option + options_for_select(custom_field.possible_values_options(custom_value.customized), custom_value.value),
65 56 tag_options.merge(:multiple => custom_field.multiple?))
66 57 if custom_field.multiple?
67 58 s << hidden_field_tag(field_name, '')
68 59 end
69 60 s
70 61 else
71 62 text_field_tag(field_name, custom_value.value, tag_options)
72 63 end
73 64 end
74 65
75 66 # Return custom field label tag
76 67 def custom_field_label_tag(name, custom_value, options={})
77 68 required = options[:required] || custom_value.custom_field.is_required?
78 69
79 70 content_tag "label", h(custom_value.custom_field.name) +
80 71 (required ? " <span class=\"required\">*</span>".html_safe : ""),
81 72 :for => "#{name}_custom_field_values_#{custom_value.custom_field.id}"
82 73 end
83 74
84 75 # Return custom field tag with its label tag
85 76 def custom_field_tag_with_label(name, custom_value, options={})
86 77 custom_field_label_tag(name, custom_value, options) + custom_field_tag(name, custom_value)
87 78 end
88 79
89 80 def custom_field_tag_for_bulk_edit(name, custom_field, projects=nil)
90 81 field_name = "#{name}[custom_field_values][#{custom_field.id}]"
91 82 field_name << "[]" if custom_field.multiple?
92 83 field_id = "#{name}_custom_field_values_#{custom_field.id}"
93 84
94 85 tag_options = {:id => field_id, :class => "#{custom_field.field_format}_cf"}
95 86
96 87 field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
97 88 case field_format.try(:edit_as)
98 89 when "date"
99 90 text_field_tag(field_name, '', tag_options.merge(:size => 10)) +
100 91 calendar_for(field_id)
101 92 when "text"
102 93 text_area_tag(field_name, '', tag_options.merge(:rows => 3))
103 94 when "bool"
104 95 select_tag(field_name, options_for_select([[l(:label_no_change_option), ''],
105 96 [l(:general_text_yes), '1'],
106 97 [l(:general_text_no), '0']]), tag_options)
107 98 when "list"
108 99 options = []
109 100 options << [l(:label_no_change_option), ''] unless custom_field.multiple?
110 101 options << [l(:label_none), '__none__'] unless custom_field.is_required?
111 102 options += custom_field.possible_values_options(projects)
112 103 select_tag(field_name, options_for_select(options), tag_options.merge(:multiple => custom_field.multiple?))
113 104 else
114 105 text_field_tag(field_name, '', tag_options)
115 106 end
116 107 end
117 108
118 109 # Return a string used to display a custom value
119 110 def show_value(custom_value)
120 111 return "" unless custom_value
121 112 format_value(custom_value.value, custom_value.custom_field.field_format)
122 113 end
123 114
124 115 # Return a string used to display a custom value
125 116 def format_value(value, field_format)
126 117 if value.is_a?(Array)
127 118 value.collect {|v| format_value(v, field_format)}.compact.sort.join(', ')
128 119 else
129 120 Redmine::CustomFieldFormat.format_value(value, field_format)
130 121 end
131 122 end
132 123
133 124 # Return an array of custom field formats which can be used in select_tag
134 125 def custom_field_formats_for_select(custom_field)
135 126 Redmine::CustomFieldFormat.as_select(custom_field.class.customized_class.name)
136 127 end
137 128
138 129 # Renders the custom_values in api views
139 130 def render_api_custom_values(custom_values, api)
140 131 api.array :custom_fields do
141 132 custom_values.each do |custom_value|
142 133 attrs = {:id => custom_value.custom_field_id, :name => custom_value.custom_field.name}
143 134 attrs.merge!(:multiple => true) if custom_value.custom_field.multiple?
144 135 api.custom_field attrs do
145 136 if custom_value.value.is_a?(Array)
146 137 api.array :value do
147 138 custom_value.value.each do |value|
148 139 api.value value unless value.blank?
149 140 end
150 141 end
151 142 else
152 143 api.value custom_value.value
153 144 end
154 145 end
155 146 end
156 147 end unless custom_values.empty?
157 148 end
158 149 end
@@ -1,292 +1,313
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 acts_as_list :scope => 'type = \'#{self.class}\''
23 23 serialize :possible_values
24 24
25 25 validates_presence_of :name, :field_format
26 26 validates_uniqueness_of :name, :scope => :type
27 27 validates_length_of :name, :maximum => 30
28 28 validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
29 29
30 30 validate :validate_custom_field
31 31 before_validation :set_searchable
32 32
33 CUSTOM_FIELDS_TABS = [
34 {:name => 'IssueCustomField', :partial => 'custom_fields/index',
35 :label => :label_issue_plural},
36 {:name => 'TimeEntryCustomField', :partial => 'custom_fields/index',
37 :label => :label_spent_time},
38 {:name => 'ProjectCustomField', :partial => 'custom_fields/index',
39 :label => :label_project_plural},
40 {:name => 'VersionCustomField', :partial => 'custom_fields/index',
41 :label => :label_version_plural},
42 {:name => 'UserCustomField', :partial => 'custom_fields/index',
43 :label => :label_user_plural},
44 {:name => 'GroupCustomField', :partial => 'custom_fields/index',
45 :label => :label_group_plural},
46 {:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index',
47 :label => TimeEntryActivity::OptionName},
48 {:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index',
49 :label => IssuePriority::OptionName},
50 {:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index',
51 :label => DocumentCategory::OptionName}
52 ]
53
33 54 def set_searchable
34 55 # make sure these fields are not searchable
35 56 self.searchable = false if %w(int float date bool).include?(field_format)
36 57 # make sure only these fields can have multiple values
37 58 self.multiple = false unless %w(list user version).include?(field_format)
38 59 true
39 60 end
40 61
41 62 def validate_custom_field
42 63 if self.field_format == "list"
43 64 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
44 65 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
45 66 end
46 67
47 68 if regexp.present?
48 69 begin
49 70 Regexp.new(regexp)
50 71 rescue
51 72 errors.add(:regexp, :invalid)
52 73 end
53 74 end
54 75
55 76 if default_value.present? && !valid_field_value?(default_value)
56 77 errors.add(:default_value, :invalid)
57 78 end
58 79 end
59 80
60 81 def possible_values_options(obj=nil)
61 82 case field_format
62 83 when 'user', 'version'
63 84 if obj.respond_to?(:project) && obj.project
64 85 case field_format
65 86 when 'user'
66 87 obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
67 88 when 'version'
68 89 obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]}
69 90 end
70 91 elsif obj.is_a?(Array)
71 92 obj.collect {|o| possible_values_options(o)}.reduce(:&)
72 93 else
73 94 []
74 95 end
75 96 when 'bool'
76 97 [[l(:general_text_Yes), '1'], [l(:general_text_No), '0']]
77 98 else
78 99 possible_values || []
79 100 end
80 101 end
81 102
82 103 def possible_values(obj=nil)
83 104 case field_format
84 105 when 'user', 'version'
85 106 possible_values_options(obj).collect(&:last)
86 107 when 'bool'
87 108 ['1', '0']
88 109 else
89 110 values = super()
90 111 if values.is_a?(Array)
91 112 values.each do |value|
92 113 value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
93 114 end
94 115 end
95 116 values || []
96 117 end
97 118 end
98 119
99 120 # Makes possible_values accept a multiline string
100 121 def possible_values=(arg)
101 122 if arg.is_a?(Array)
102 123 super(arg.compact.collect(&:strip).select {|v| !v.blank?})
103 124 else
104 125 self.possible_values = arg.to_s.split(/[\n\r]+/)
105 126 end
106 127 end
107 128
108 129 def cast_value(value)
109 130 casted = nil
110 131 unless value.blank?
111 132 case field_format
112 133 when 'string', 'text', 'list'
113 134 casted = value
114 135 when 'date'
115 136 casted = begin; value.to_date; rescue; nil end
116 137 when 'bool'
117 138 casted = (value == '1' ? true : false)
118 139 when 'int'
119 140 casted = value.to_i
120 141 when 'float'
121 142 casted = value.to_f
122 143 when 'user', 'version'
123 144 casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
124 145 end
125 146 end
126 147 casted
127 148 end
128 149
129 150 def value_from_keyword(keyword, customized)
130 151 possible_values_options = possible_values_options(customized)
131 152 if possible_values_options.present?
132 153 keyword = keyword.to_s.downcase
133 154 possible_values_options.detect {|text, id| text.downcase == keyword}.try(:last)
134 155 else
135 156 keyword
136 157 end
137 158 end
138 159
139 160 # Returns a ORDER BY clause that can used to sort customized
140 161 # objects by their value of the custom field.
141 162 # Returns nil if the custom field can not be used for sorting.
142 163 def order_statement
143 164 return nil if multiple?
144 165 case field_format
145 166 when 'string', 'text', 'list', 'date', 'bool'
146 167 # COALESCE is here to make sure that blank and NULL values are sorted equally
147 168 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
148 169 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
149 170 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
150 171 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
151 172 when 'int', 'float'
152 173 # Make the database cast values into numeric
153 174 # Postgresql will raise an error if a value can not be casted!
154 175 # CustomValue validations should ensure that it doesn't occur
155 176 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
156 177 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
157 178 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
158 179 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
159 180 when 'user', 'version'
160 181 value_class.fields_for_order_statement(value_join_alias)
161 182 else
162 183 nil
163 184 end
164 185 end
165 186
166 187 # Returns a GROUP BY clause that can used to group by custom value
167 188 # Returns nil if the custom field can not be used for grouping.
168 189 def group_statement
169 190 return nil if multiple?
170 191 case field_format
171 192 when 'list', 'date', 'bool', 'int'
172 193 order_statement
173 194 when 'user', 'version'
174 195 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
175 196 " WHERE cv_sort.customized_type='#{self.class.customized_class.base_class.name}'" +
176 197 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
177 198 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
178 199 else
179 200 nil
180 201 end
181 202 end
182 203
183 204 def join_for_order_statement
184 205 case field_format
185 206 when 'user', 'version'
186 207 "LEFT OUTER JOIN #{CustomValue.table_name} #{join_alias}" +
187 208 " ON #{join_alias}.customized_type = '#{self.class.customized_class.base_class.name}'" +
188 209 " AND #{join_alias}.customized_id = #{self.class.customized_class.table_name}.id" +
189 210 " AND #{join_alias}.custom_field_id = #{id}" +
190 211 " AND #{join_alias}.value <> ''" +
191 212 " AND #{join_alias}.id = (SELECT max(#{join_alias}_2.id) FROM #{CustomValue.table_name} #{join_alias}_2" +
192 213 " WHERE #{join_alias}_2.customized_type = #{join_alias}.customized_type" +
193 214 " AND #{join_alias}_2.customized_id = #{join_alias}.customized_id" +
194 215 " AND #{join_alias}_2.custom_field_id = #{join_alias}.custom_field_id)" +
195 216 " LEFT OUTER JOIN #{value_class.table_name} #{value_join_alias}" +
196 217 " ON CAST(#{join_alias}.value as decimal(60,0)) = #{value_join_alias}.id"
197 218 else
198 219 nil
199 220 end
200 221 end
201 222
202 223 def join_alias
203 224 "cf_#{id}"
204 225 end
205 226
206 227 def value_join_alias
207 228 join_alias + "_" + field_format
208 229 end
209 230
210 231 def <=>(field)
211 232 position <=> field.position
212 233 end
213 234
214 235 # Returns the class that values represent
215 236 def value_class
216 237 case field_format
217 238 when 'user', 'version'
218 239 field_format.classify.constantize
219 240 else
220 241 nil
221 242 end
222 243 end
223 244
224 245 def self.customized_class
225 246 self.name =~ /^(.+)CustomField$/
226 247 begin; $1.constantize; rescue nil; end
227 248 end
228 249
229 250 # to move in project_custom_field
230 251 def self.for_all
231 252 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
232 253 end
233 254
234 255 def type_name
235 256 nil
236 257 end
237 258
238 259 # Returns the error messages for the given value
239 260 # or an empty array if value is a valid value for the custom field
240 261 def validate_field_value(value)
241 262 errs = []
242 263 if value.is_a?(Array)
243 264 if !multiple?
244 265 errs << ::I18n.t('activerecord.errors.messages.invalid')
245 266 end
246 267 if is_required? && value.detect(&:present?).nil?
247 268 errs << ::I18n.t('activerecord.errors.messages.blank')
248 269 end
249 270 value.each {|v| errs += validate_field_value_format(v)}
250 271 else
251 272 if is_required? && value.blank?
252 273 errs << ::I18n.t('activerecord.errors.messages.blank')
253 274 end
254 275 errs += validate_field_value_format(value)
255 276 end
256 277 errs
257 278 end
258 279
259 280 # Returns true if value is a valid value for the custom field
260 281 def valid_field_value?(value)
261 282 validate_field_value(value).empty?
262 283 end
263 284
264 285 def format_in?(*args)
265 286 args.include?(field_format)
266 287 end
267 288
268 289 protected
269 290
270 291 # Returns the error message for the given value regarding its format
271 292 def validate_field_value_format(value)
272 293 errs = []
273 294 if value.present?
274 295 errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp)
275 296 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length > 0 and value.length < min_length
276 297 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length > 0 and value.length > max_length
277 298
278 299 # Format specific validations
279 300 case field_format
280 301 when 'int'
281 302 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
282 303 when 'float'
283 304 begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end
284 305 when 'date'
285 306 errs << ::I18n.t('activerecord.errors.messages.not_a_date') unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end
286 307 when 'list'
287 308 errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value)
288 309 end
289 310 end
290 311 errs
291 312 end
292 313 end
General Comments 0
You need to be logged in to leave comments. Login now