##// END OF EJS Templates
Merged r14944 (#21413)....
Jean-Philippe Lang -
r14563:f2fd7905557d
parent child
Show More
@@ -1,280 +1,284
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 :enumerations,
22 22 lambda { order(:position) },
23 23 :class_name => 'CustomFieldEnumeration',
24 24 :dependent => :delete_all
25 25 has_many :custom_values, :dependent => :delete_all
26 26 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
27 27 acts_as_list :scope => 'type = \'#{self.class}\''
28 28 serialize :possible_values
29 29 store :format_store
30 30
31 31 validates_presence_of :name, :field_format
32 32 validates_uniqueness_of :name, :scope => :type
33 33 validates_length_of :name, :maximum => 30
34 34 validates_inclusion_of :field_format, :in => Proc.new { Redmine::FieldFormat.available_formats }
35 35 validate :validate_custom_field
36 36 attr_protected :id
37 37
38 38 before_validation :set_searchable
39 39 before_save do |field|
40 40 field.format.before_custom_field_save(field)
41 41 end
42 42 after_save :handle_multiplicity_change
43 43 after_save do |field|
44 44 if field.visible_changed? && field.visible
45 45 field.roles.clear
46 46 end
47 47 end
48 48
49 49 scope :sorted, lambda { order(:position) }
50 50 scope :visible, lambda {|*args|
51 51 user = args.shift || User.current
52 52 if user.admin?
53 53 # nop
54 54 elsif user.memberships.any?
55 55 where("#{table_name}.visible = ? OR #{table_name}.id IN (SELECT DISTINCT cfr.custom_field_id FROM #{Member.table_name} m" +
56 56 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
57 57 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
58 58 " WHERE m.user_id = ?)",
59 59 true, user.id)
60 60 else
61 61 where(:visible => true)
62 62 end
63 63 }
64 64
65 65 def visible_by?(project, user=User.current)
66 66 visible? || user.admin?
67 67 end
68 68
69 69 def format
70 70 @format ||= Redmine::FieldFormat.find(field_format)
71 71 end
72 72
73 73 def field_format=(arg)
74 74 # cannot change format of a saved custom field
75 75 if new_record?
76 76 @format = nil
77 77 super
78 78 end
79 79 end
80 80
81 81 def set_searchable
82 82 # make sure these fields are not searchable
83 83 self.searchable = false unless format.class.searchable_supported
84 84 # make sure only these fields can have multiple values
85 85 self.multiple = false unless format.class.multiple_supported
86 86 true
87 87 end
88 88
89 89 def validate_custom_field
90 90 format.validate_custom_field(self).each do |attribute, message|
91 91 errors.add attribute, message
92 92 end
93 93
94 94 if regexp.present?
95 95 begin
96 96 Regexp.new(regexp)
97 97 rescue
98 98 errors.add(:regexp, :invalid)
99 99 end
100 100 end
101 101
102 102 if default_value.present?
103 103 validate_field_value(default_value).each do |message|
104 104 errors.add :default_value, message
105 105 end
106 106 end
107 107 end
108 108
109 109 def possible_custom_value_options(custom_value)
110 110 format.possible_custom_value_options(custom_value)
111 111 end
112 112
113 113 def possible_values_options(object=nil)
114 114 if object.is_a?(Array)
115 115 object.map {|o| format.possible_values_options(self, o)}.reduce(:&) || []
116 116 else
117 117 format.possible_values_options(self, object) || []
118 118 end
119 119 end
120 120
121 121 def possible_values
122 122 values = read_attribute(:possible_values)
123 123 if values.is_a?(Array)
124 124 values.each do |value|
125 125 value.to_s.force_encoding('UTF-8')
126 126 end
127 127 values
128 128 else
129 129 []
130 130 end
131 131 end
132 132
133 133 # Makes possible_values accept a multiline string
134 134 def possible_values=(arg)
135 135 if arg.is_a?(Array)
136 136 values = arg.compact.map {|a| a.to_s.strip}.reject(&:blank?)
137 137 write_attribute(:possible_values, values)
138 138 else
139 139 self.possible_values = arg.to_s.split(/[\n\r]+/)
140 140 end
141 141 end
142 142
143 143 def cast_value(value)
144 144 format.cast_value(self, value)
145 145 end
146 146
147 147 def value_from_keyword(keyword, customized)
148 148 format.value_from_keyword(self, keyword, customized)
149 149 end
150 150
151 151 # Returns the options hash used to build a query filter for the field
152 152 def query_filter_options(query)
153 153 format.query_filter_options(self, query)
154 154 end
155 155
156 def totalable?
157 format.totalable_supported
158 end
159
156 160 # Returns a ORDER BY clause that can used to sort customized
157 161 # objects by their value of the custom field.
158 162 # Returns nil if the custom field can not be used for sorting.
159 163 def order_statement
160 164 return nil if multiple?
161 165 format.order_statement(self)
162 166 end
163 167
164 168 # Returns a GROUP BY clause that can used to group by custom value
165 169 # Returns nil if the custom field can not be used for grouping.
166 170 def group_statement
167 171 return nil if multiple?
168 172 format.group_statement(self)
169 173 end
170 174
171 175 def join_for_order_statement
172 176 format.join_for_order_statement(self)
173 177 end
174 178
175 179 def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil)
176 180 if visible? || user.admin?
177 181 "1=1"
178 182 elsif user.anonymous?
179 183 "1=0"
180 184 else
181 185 project_key ||= "#{self.class.customized_class.table_name}.project_id"
182 186 id_column ||= id
183 187 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
184 188 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
185 189 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
186 190 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id_column})"
187 191 end
188 192 end
189 193
190 194 def self.visibility_condition
191 195 if user.admin?
192 196 "1=1"
193 197 elsif user.anonymous?
194 198 "#{table_name}.visible"
195 199 else
196 200 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
197 201 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
198 202 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
199 203 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
200 204 end
201 205 end
202 206
203 207 def <=>(field)
204 208 position <=> field.position
205 209 end
206 210
207 211 # Returns the class that values represent
208 212 def value_class
209 213 format.target_class if format.respond_to?(:target_class)
210 214 end
211 215
212 216 def self.customized_class
213 217 self.name =~ /^(.+)CustomField$/
214 218 $1.constantize rescue nil
215 219 end
216 220
217 221 # to move in project_custom_field
218 222 def self.for_all
219 223 where(:is_for_all => true).order('position').to_a
220 224 end
221 225
222 226 def type_name
223 227 nil
224 228 end
225 229
226 230 # Returns the error messages for the given value
227 231 # or an empty array if value is a valid value for the custom field
228 232 def validate_custom_value(custom_value)
229 233 value = custom_value.value
230 234 errs = []
231 235 if value.is_a?(Array)
232 236 if !multiple?
233 237 errs << ::I18n.t('activerecord.errors.messages.invalid')
234 238 end
235 239 if is_required? && value.detect(&:present?).nil?
236 240 errs << ::I18n.t('activerecord.errors.messages.blank')
237 241 end
238 242 else
239 243 if is_required? && value.blank?
240 244 errs << ::I18n.t('activerecord.errors.messages.blank')
241 245 end
242 246 end
243 247 errs += format.validate_custom_value(custom_value)
244 248 errs
245 249 end
246 250
247 251 # Returns the error messages for the default custom field value
248 252 def validate_field_value(value)
249 253 validate_custom_value(CustomFieldValue.new(:custom_field => self, :value => value))
250 254 end
251 255
252 256 # Returns true if value is a valid value for the custom field
253 257 def valid_field_value?(value)
254 258 validate_field_value(value).empty?
255 259 end
256 260
257 261 def format_in?(*args)
258 262 args.include?(field_format)
259 263 end
260 264
261 265 protected
262 266
263 267 # Removes multiple values for the custom field after setting the multiple attribute to false
264 268 # We kepp the value with the highest id for each customized object
265 269 def handle_multiplicity_change
266 270 if !new_record? && multiple_was && !multiple
267 271 ids = custom_values.
268 272 where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" +
269 273 " AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" +
270 274 " AND cve.id > #{CustomValue.table_name}.id)").
271 275 pluck(:id)
272 276
273 277 if ids.any?
274 278 custom_values.where(:id => ids).delete_all
275 279 end
276 280 end
277 281 end
278 282 end
279 283
280 284 require_dependency 'redmine/field_format'
@@ -1,1038 +1,1026
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 QueryColumn
19 19 attr_accessor :name, :sortable, :groupable, :totalable, :default_order
20 20 include Redmine::I18n
21 21
22 22 def initialize(name, options={})
23 23 self.name = name
24 24 self.sortable = options[:sortable]
25 25 self.groupable = options[:groupable] || false
26 26 if groupable == true
27 27 self.groupable = name.to_s
28 28 end
29 29 self.totalable = options[:totalable] || false
30 30 self.default_order = options[:default_order]
31 31 @inline = options.key?(:inline) ? options[:inline] : true
32 32 @caption_key = options[:caption] || "field_#{name}".to_sym
33 33 @frozen = options[:frozen]
34 34 end
35 35
36 36 def caption
37 37 case @caption_key
38 38 when Symbol
39 39 l(@caption_key)
40 40 when Proc
41 41 @caption_key.call
42 42 else
43 43 @caption_key
44 44 end
45 45 end
46 46
47 47 # Returns true if the column is sortable, otherwise false
48 48 def sortable?
49 49 !@sortable.nil?
50 50 end
51 51
52 52 def sortable
53 53 @sortable.is_a?(Proc) ? @sortable.call : @sortable
54 54 end
55 55
56 56 def inline?
57 57 @inline
58 58 end
59 59
60 60 def frozen?
61 61 @frozen
62 62 end
63 63
64 64 def value(object)
65 65 object.send name
66 66 end
67 67
68 68 def value_object(object)
69 69 object.send name
70 70 end
71 71
72 72 def css_classes
73 73 name
74 74 end
75 75 end
76 76
77 77 class QueryCustomFieldColumn < QueryColumn
78 78
79 79 def initialize(custom_field)
80 80 self.name = "cf_#{custom_field.id}".to_sym
81 81 self.sortable = custom_field.order_statement || false
82 82 self.groupable = custom_field.group_statement || false
83 self.totalable = ['int', 'float'].include?(custom_field.field_format)
83 self.totalable = custom_field.totalable?
84 84 @inline = true
85 85 @cf = custom_field
86 86 end
87 87
88 88 def caption
89 89 @cf.name
90 90 end
91 91
92 92 def custom_field
93 93 @cf
94 94 end
95 95
96 96 def value_object(object)
97 97 if custom_field.visible_by?(object.project, User.current)
98 98 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
99 99 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
100 100 else
101 101 nil
102 102 end
103 103 end
104 104
105 105 def value(object)
106 106 raw = value_object(object)
107 107 if raw.is_a?(Array)
108 108 raw.map {|r| @cf.cast_value(r.value)}
109 109 elsif raw
110 110 @cf.cast_value(raw.value)
111 111 else
112 112 nil
113 113 end
114 114 end
115 115
116 116 def css_classes
117 117 @css_classes ||= "#{name} #{@cf.field_format}"
118 118 end
119 119 end
120 120
121 121 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
122 122
123 123 def initialize(association, custom_field)
124 124 super(custom_field)
125 125 self.name = "#{association}.cf_#{custom_field.id}".to_sym
126 126 # TODO: support sorting/grouping by association custom field
127 127 self.sortable = false
128 128 self.groupable = false
129 129 @association = association
130 130 end
131 131
132 132 def value_object(object)
133 133 if assoc = object.send(@association)
134 134 super(assoc)
135 135 end
136 136 end
137 137
138 138 def css_classes
139 139 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
140 140 end
141 141 end
142 142
143 143 class Query < ActiveRecord::Base
144 144 class StatementInvalid < ::ActiveRecord::StatementInvalid
145 145 end
146 146
147 147 VISIBILITY_PRIVATE = 0
148 148 VISIBILITY_ROLES = 1
149 149 VISIBILITY_PUBLIC = 2
150 150
151 151 belongs_to :project
152 152 belongs_to :user
153 153 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
154 154 serialize :filters
155 155 serialize :column_names
156 156 serialize :sort_criteria, Array
157 157 serialize :options, Hash
158 158
159 159 attr_protected :project_id, :user_id
160 160
161 161 validates_presence_of :name
162 162 validates_length_of :name, :maximum => 255
163 163 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
164 164 validate :validate_query_filters
165 165 validate do |query|
166 166 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
167 167 end
168 168
169 169 after_save do |query|
170 170 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
171 171 query.roles.clear
172 172 end
173 173 end
174 174
175 175 class_attribute :operators
176 176 self.operators = {
177 177 "=" => :label_equals,
178 178 "!" => :label_not_equals,
179 179 "o" => :label_open_issues,
180 180 "c" => :label_closed_issues,
181 181 "!*" => :label_none,
182 182 "*" => :label_any,
183 183 ">=" => :label_greater_or_equal,
184 184 "<=" => :label_less_or_equal,
185 185 "><" => :label_between,
186 186 "<t+" => :label_in_less_than,
187 187 ">t+" => :label_in_more_than,
188 188 "><t+"=> :label_in_the_next_days,
189 189 "t+" => :label_in,
190 190 "t" => :label_today,
191 191 "ld" => :label_yesterday,
192 192 "w" => :label_this_week,
193 193 "lw" => :label_last_week,
194 194 "l2w" => [:label_last_n_weeks, {:count => 2}],
195 195 "m" => :label_this_month,
196 196 "lm" => :label_last_month,
197 197 "y" => :label_this_year,
198 198 ">t-" => :label_less_than_ago,
199 199 "<t-" => :label_more_than_ago,
200 200 "><t-"=> :label_in_the_past_days,
201 201 "t-" => :label_ago,
202 202 "~" => :label_contains,
203 203 "!~" => :label_not_contains,
204 204 "=p" => :label_any_issues_in_project,
205 205 "=!p" => :label_any_issues_not_in_project,
206 206 "!p" => :label_no_issues_in_project,
207 207 "*o" => :label_any_open_issues,
208 208 "!o" => :label_no_open_issues
209 209 }
210 210
211 211 class_attribute :operators_by_filter_type
212 212 self.operators_by_filter_type = {
213 213 :list => [ "=", "!" ],
214 214 :list_status => [ "o", "=", "!", "c", "*" ],
215 215 :list_optional => [ "=", "!", "!*", "*" ],
216 216 :list_subprojects => [ "*", "!*", "=" ],
217 217 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
218 218 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
219 219 :string => [ "=", "~", "!", "!~", "!*", "*" ],
220 220 :text => [ "~", "!~", "!*", "*" ],
221 221 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
222 222 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
223 223 :relation => ["=", "=p", "=!p", "!p", "*o", "!o", "!*", "*"],
224 224 :tree => ["=", "~", "!*", "*"]
225 225 }
226 226
227 227 class_attribute :available_columns
228 228 self.available_columns = []
229 229
230 230 class_attribute :queried_class
231 231
232 232 def queried_table_name
233 233 @queried_table_name ||= self.class.queried_class.table_name
234 234 end
235 235
236 236 def initialize(attributes=nil, *args)
237 237 super attributes
238 238 @is_for_all = project.nil?
239 239 end
240 240
241 241 # Builds the query from the given params
242 242 def build_from_params(params)
243 243 if params[:fields] || params[:f]
244 244 self.filters = {}
245 245 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
246 246 else
247 247 available_filters.keys.each do |field|
248 248 add_short_filter(field, params[field]) if params[field]
249 249 end
250 250 end
251 251 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
252 252 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
253 253 self.totalable_names = params[:t] || (params[:query] && params[:query][:totalable_names])
254 254 self
255 255 end
256 256
257 257 # Builds a new query from the given params and attributes
258 258 def self.build_from_params(params, attributes={})
259 259 new(attributes).build_from_params(params)
260 260 end
261 261
262 262 def validate_query_filters
263 263 filters.each_key do |field|
264 264 if values_for(field)
265 265 case type_for(field)
266 266 when :integer
267 267 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
268 268 when :float
269 269 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
270 270 when :date, :date_past
271 271 case operator_for(field)
272 272 when "=", ">=", "<=", "><"
273 273 add_filter_error(field, :invalid) if values_for(field).detect {|v|
274 274 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
275 275 }
276 276 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
277 277 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
278 278 end
279 279 end
280 280 end
281 281
282 282 add_filter_error(field, :blank) unless
283 283 # filter requires one or more values
284 284 (values_for(field) and !values_for(field).first.blank?) or
285 285 # filter doesn't require any value
286 286 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "*o", "!o"].include? operator_for(field)
287 287 end if filters
288 288 end
289 289
290 290 def add_filter_error(field, message)
291 291 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
292 292 errors.add(:base, m)
293 293 end
294 294
295 295 def editable_by?(user)
296 296 return false unless user
297 297 # Admin can edit them all and regular users can edit their private queries
298 298 return true if user.admin? || (is_private? && self.user_id == user.id)
299 299 # Members can not edit public queries that are for all project (only admin is allowed to)
300 300 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
301 301 end
302 302
303 303 def trackers
304 304 @trackers ||= project.nil? ? Tracker.sorted.to_a : project.rolled_up_trackers
305 305 end
306 306
307 307 # Returns a hash of localized labels for all filter operators
308 308 def self.operators_labels
309 309 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
310 310 end
311 311
312 312 # Returns a representation of the available filters for JSON serialization
313 313 def available_filters_as_json
314 314 json = {}
315 315 available_filters.each do |field, options|
316 316 options = options.slice(:type, :name, :values)
317 317 if options[:values] && values_for(field)
318 318 missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
319 319 if missing.any? && respond_to?(method = "find_#{field}_filter_values")
320 320 options[:values] += send(method, missing)
321 321 end
322 322 end
323 323 json[field] = options.stringify_keys
324 324 end
325 325 json
326 326 end
327 327
328 328 def all_projects
329 329 @all_projects ||= Project.visible.to_a
330 330 end
331 331
332 332 def all_projects_values
333 333 return @all_projects_values if @all_projects_values
334 334
335 335 values = []
336 336 Project.project_tree(all_projects) do |p, level|
337 337 prefix = (level > 0 ? ('--' * level + ' ') : '')
338 338 values << ["#{prefix}#{p.name}", p.id.to_s]
339 339 end
340 340 @all_projects_values = values
341 341 end
342 342
343 343 # Adds available filters
344 344 def initialize_available_filters
345 345 # implemented by sub-classes
346 346 end
347 347 protected :initialize_available_filters
348 348
349 349 # Adds an available filter
350 350 def add_available_filter(field, options)
351 351 @available_filters ||= ActiveSupport::OrderedHash.new
352 352 @available_filters[field] = options
353 353 @available_filters
354 354 end
355 355
356 356 # Removes an available filter
357 357 def delete_available_filter(field)
358 358 if @available_filters
359 359 @available_filters.delete(field)
360 360 end
361 361 end
362 362
363 363 # Return a hash of available filters
364 364 def available_filters
365 365 unless @available_filters
366 366 initialize_available_filters
367 367 @available_filters.each do |field, options|
368 368 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
369 369 end
370 370 end
371 371 @available_filters
372 372 end
373 373
374 374 def add_filter(field, operator, values=nil)
375 375 # values must be an array
376 376 return unless values.nil? || values.is_a?(Array)
377 377 # check if field is defined as an available filter
378 378 if available_filters.has_key? field
379 379 filter_options = available_filters[field]
380 380 filters[field] = {:operator => operator, :values => (values || [''])}
381 381 end
382 382 end
383 383
384 384 def add_short_filter(field, expression)
385 385 return unless expression && available_filters.has_key?(field)
386 386 field_type = available_filters[field][:type]
387 387 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
388 388 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
389 389 values = $1
390 390 add_filter field, operator, values.present? ? values.split('|') : ['']
391 391 end || add_filter(field, '=', expression.split('|'))
392 392 end
393 393
394 394 # Add multiple filters using +add_filter+
395 395 def add_filters(fields, operators, values)
396 396 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
397 397 fields.each do |field|
398 398 add_filter(field, operators[field], values && values[field])
399 399 end
400 400 end
401 401 end
402 402
403 403 def has_filter?(field)
404 404 filters and filters[field]
405 405 end
406 406
407 407 def type_for(field)
408 408 available_filters[field][:type] if available_filters.has_key?(field)
409 409 end
410 410
411 411 def operator_for(field)
412 412 has_filter?(field) ? filters[field][:operator] : nil
413 413 end
414 414
415 415 def values_for(field)
416 416 has_filter?(field) ? filters[field][:values] : nil
417 417 end
418 418
419 419 def value_for(field, index=0)
420 420 (values_for(field) || [])[index]
421 421 end
422 422
423 423 def label_for(field)
424 424 label = available_filters[field][:name] if available_filters.has_key?(field)
425 425 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
426 426 end
427 427
428 428 def self.add_available_column(column)
429 429 self.available_columns << (column) if column.is_a?(QueryColumn)
430 430 end
431 431
432 432 # Returns an array of columns that can be used to group the results
433 433 def groupable_columns
434 434 available_columns.select {|c| c.groupable}
435 435 end
436 436
437 437 # Returns a Hash of columns and the key for sorting
438 438 def sortable_columns
439 439 available_columns.inject({}) {|h, column|
440 440 h[column.name.to_s] = column.sortable
441 441 h
442 442 }
443 443 end
444 444
445 445 def columns
446 446 # preserve the column_names order
447 447 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
448 448 available_columns.find { |col| col.name == name }
449 449 end.compact
450 450 available_columns.select(&:frozen?) | cols
451 451 end
452 452
453 453 def inline_columns
454 454 columns.select(&:inline?)
455 455 end
456 456
457 457 def block_columns
458 458 columns.reject(&:inline?)
459 459 end
460 460
461 461 def available_inline_columns
462 462 available_columns.select(&:inline?)
463 463 end
464 464
465 465 def available_block_columns
466 466 available_columns.reject(&:inline?)
467 467 end
468 468
469 469 def available_totalable_columns
470 470 available_columns.select(&:totalable)
471 471 end
472 472
473 473 def default_columns_names
474 474 []
475 475 end
476 476
477 477 def column_names=(names)
478 478 if names
479 479 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
480 480 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
481 481 # Set column_names to nil if default columns
482 482 if names == default_columns_names
483 483 names = nil
484 484 end
485 485 end
486 486 write_attribute(:column_names, names)
487 487 end
488 488
489 489 def has_column?(column)
490 490 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
491 491 end
492 492
493 493 def has_custom_field_column?
494 494 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
495 495 end
496 496
497 497 def has_default_columns?
498 498 column_names.nil? || column_names.empty?
499 499 end
500 500
501 501 def totalable_columns
502 502 names = totalable_names
503 503 available_totalable_columns.select {|column| names.include?(column.name)}
504 504 end
505 505
506 506 def totalable_names=(names)
507 507 if names
508 508 names = names.select(&:present?).map {|n| n.is_a?(Symbol) ? n : n.to_sym}
509 509 end
510 510 options[:totalable_names] = names
511 511 end
512 512
513 513 def totalable_names
514 514 options[:totalable_names] || Setting.issue_list_default_totals.map(&:to_sym) || []
515 515 end
516 516
517 517 def sort_criteria=(arg)
518 518 c = []
519 519 if arg.is_a?(Hash)
520 520 arg = arg.keys.sort.collect {|k| arg[k]}
521 521 end
522 522 if arg
523 523 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
524 524 end
525 525 write_attribute(:sort_criteria, c)
526 526 end
527 527
528 528 def sort_criteria
529 529 read_attribute(:sort_criteria) || []
530 530 end
531 531
532 532 def sort_criteria_key(arg)
533 533 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
534 534 end
535 535
536 536 def sort_criteria_order(arg)
537 537 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
538 538 end
539 539
540 540 def sort_criteria_order_for(key)
541 541 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
542 542 end
543 543
544 544 # Returns the SQL sort order that should be prepended for grouping
545 545 def group_by_sort_order
546 546 if grouped? && (column = group_by_column)
547 547 order = (sort_criteria_order_for(column.name) || column.default_order).try(:upcase)
548 548 column.sortable.is_a?(Array) ?
549 549 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
550 550 "#{column.sortable} #{order}"
551 551 end
552 552 end
553 553
554 554 # Returns true if the query is a grouped query
555 555 def grouped?
556 556 !group_by_column.nil?
557 557 end
558 558
559 559 def group_by_column
560 560 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
561 561 end
562 562
563 563 def group_by_statement
564 564 group_by_column.try(:groupable)
565 565 end
566 566
567 567 def project_statement
568 568 project_clauses = []
569 569 if project && !project.descendants.active.empty?
570 570 ids = [project.id]
571 571 if has_filter?("subproject_id")
572 572 case operator_for("subproject_id")
573 573 when '='
574 574 # include the selected subprojects
575 575 ids += values_for("subproject_id").each(&:to_i)
576 576 when '!*'
577 577 # main project only
578 578 else
579 579 # all subprojects
580 580 ids += project.descendants.collect(&:id)
581 581 end
582 582 elsif Setting.display_subprojects_issues?
583 583 ids += project.descendants.collect(&:id)
584 584 end
585 585 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
586 586 elsif project
587 587 project_clauses << "#{Project.table_name}.id = %d" % project.id
588 588 end
589 589 project_clauses.any? ? project_clauses.join(' AND ') : nil
590 590 end
591 591
592 592 def statement
593 593 # filters clauses
594 594 filters_clauses = []
595 595 filters.each_key do |field|
596 596 next if field == "subproject_id"
597 597 v = values_for(field).clone
598 598 next unless v and !v.empty?
599 599 operator = operator_for(field)
600 600
601 601 # "me" value substitution
602 602 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
603 603 if v.delete("me")
604 604 if User.current.logged?
605 605 v.push(User.current.id.to_s)
606 606 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
607 607 else
608 608 v.push("0")
609 609 end
610 610 end
611 611 end
612 612
613 613 if field == 'project_id'
614 614 if v.delete('mine')
615 615 v += User.current.memberships.map(&:project_id).map(&:to_s)
616 616 end
617 617 end
618 618
619 619 if field =~ /cf_(\d+)$/
620 620 # custom field
621 621 filters_clauses << sql_for_custom_field(field, operator, v, $1)
622 622 elsif respond_to?("sql_for_#{field}_field")
623 623 # specific statement
624 624 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
625 625 else
626 626 # regular field
627 627 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
628 628 end
629 629 end if filters and valid?
630 630
631 631 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
632 632 # Excludes results for which the grouped custom field is not visible
633 633 filters_clauses << c.custom_field.visibility_by_project_condition
634 634 end
635 635
636 636 filters_clauses << project_statement
637 637 filters_clauses.reject!(&:blank?)
638 638
639 639 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
640 640 end
641 641
642 642 # Returns the sum of values for the given column
643 643 def total_for(column)
644 644 total_with_scope(column, base_scope)
645 645 end
646 646
647 647 # Returns a hash of the sum of the given column for each group,
648 648 # or nil if the query is not grouped
649 649 def total_by_group_for(column)
650 650 grouped_query do |scope|
651 651 total_with_scope(column, scope)
652 652 end
653 653 end
654 654
655 655 def totals
656 656 totals = totalable_columns.map {|column| [column, total_for(column)]}
657 657 yield totals if block_given?
658 658 totals
659 659 end
660 660
661 661 def totals_by_group
662 662 totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
663 663 yield totals if block_given?
664 664 totals
665 665 end
666 666
667 667 private
668 668
669 669 def grouped_query(&block)
670 670 r = nil
671 671 if grouped?
672 672 begin
673 673 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
674 674 r = yield base_group_scope
675 675 rescue ActiveRecord::RecordNotFound
676 676 r = {nil => yield(base_scope)}
677 677 end
678 678 c = group_by_column
679 679 if c.is_a?(QueryCustomFieldColumn)
680 680 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
681 681 end
682 682 end
683 683 r
684 684 rescue ::ActiveRecord::StatementInvalid => e
685 685 raise StatementInvalid.new(e.message)
686 686 end
687 687
688 688 def total_with_scope(column, scope)
689 689 unless column.is_a?(QueryColumn)
690 690 column = column.to_sym
691 691 column = available_totalable_columns.detect {|c| c.name == column}
692 692 end
693 693 if column.is_a?(QueryCustomFieldColumn)
694 694 custom_field = column.custom_field
695 send "total_for_#{custom_field.field_format}_custom_field", custom_field, scope
695 send "total_for_custom_field", custom_field, scope
696 696 else
697 697 send "total_for_#{column.name}", scope
698 698 end
699 699 rescue ::ActiveRecord::StatementInvalid => e
700 700 raise StatementInvalid.new(e.message)
701 701 end
702 702
703 703 def base_scope
704 704 raise "unimplemented"
705 705 end
706 706
707 707 def base_group_scope
708 708 base_scope.
709 709 joins(joins_for_order_statement(group_by_statement)).
710 710 group(group_by_statement)
711 711 end
712 712
713 def total_for_float_custom_field(custom_field, scope)
714 total_for_custom_field(custom_field, scope) {|t| t.to_f.round(2)}
715 end
716
717 def total_for_int_custom_field(custom_field, scope)
718 total_for_custom_field(custom_field, scope) {|t| t.to_i}
719 end
720
721 713 def total_for_custom_field(custom_field, scope, &block)
722 total = scope.joins(:custom_values).
723 where(:custom_values => {:custom_field_id => custom_field.id}).
724 where.not(:custom_values => {:value => ''}).
725 sum("CAST(#{CustomValue.table_name}.value AS decimal(30,3))")
726
727 total = map_total(total, &block) if block_given?
714 total = custom_field.format.total_for_scope(custom_field, scope)
715 total = map_total(total) {|t| custom_field.format.cast_total_value(custom_field, t)}
728 716 total
729 717 end
730 718
731 719 def map_total(total, &block)
732 720 if total.is_a?(Hash)
733 721 total.keys.each {|k| total[k] = yield total[k]}
734 722 else
735 723 total = yield total
736 724 end
737 725 total
738 726 end
739 727
740 728 def sql_for_custom_field(field, operator, value, custom_field_id)
741 729 db_table = CustomValue.table_name
742 730 db_field = 'value'
743 731 filter = @available_filters[field]
744 732 return nil unless filter
745 733 if filter[:field].format.target_class && filter[:field].format.target_class <= User
746 734 if value.delete('me')
747 735 value.push User.current.id.to_s
748 736 end
749 737 end
750 738 not_in = nil
751 739 if operator == '!'
752 740 # Makes ! operator work for custom fields with multiple values
753 741 operator = '='
754 742 not_in = 'NOT'
755 743 end
756 744 customized_key = "id"
757 745 customized_class = queried_class
758 746 if field =~ /^(.+)\.cf_/
759 747 assoc = $1
760 748 customized_key = "#{assoc}_id"
761 749 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
762 750 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
763 751 end
764 752 where = sql_for_field(field, operator, value, db_table, db_field, true)
765 753 if operator =~ /[<>]/
766 754 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
767 755 end
768 756 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
769 757 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
770 758 " LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id}" +
771 759 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
772 760 end
773 761
774 762 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
775 763 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
776 764 sql = ''
777 765 case operator
778 766 when "="
779 767 if value.any?
780 768 case type_for(field)
781 769 when :date, :date_past
782 770 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
783 771 when :integer
784 772 if is_custom_filter
785 773 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) = #{value.first.to_i})"
786 774 else
787 775 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
788 776 end
789 777 when :float
790 778 if is_custom_filter
791 779 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
792 780 else
793 781 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
794 782 end
795 783 else
796 784 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")"
797 785 end
798 786 else
799 787 # IN an empty set
800 788 sql = "1=0"
801 789 end
802 790 when "!"
803 791 if value.any?
804 792 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + "))"
805 793 else
806 794 # NOT IN an empty set
807 795 sql = "1=1"
808 796 end
809 797 when "!*"
810 798 sql = "#{db_table}.#{db_field} IS NULL"
811 799 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
812 800 when "*"
813 801 sql = "#{db_table}.#{db_field} IS NOT NULL"
814 802 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
815 803 when ">="
816 804 if [:date, :date_past].include?(type_for(field))
817 805 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
818 806 else
819 807 if is_custom_filter
820 808 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})"
821 809 else
822 810 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
823 811 end
824 812 end
825 813 when "<="
826 814 if [:date, :date_past].include?(type_for(field))
827 815 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
828 816 else
829 817 if is_custom_filter
830 818 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})"
831 819 else
832 820 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
833 821 end
834 822 end
835 823 when "><"
836 824 if [:date, :date_past].include?(type_for(field))
837 825 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
838 826 else
839 827 if is_custom_filter
840 828 sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
841 829 else
842 830 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
843 831 end
844 832 end
845 833 when "o"
846 834 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false})" if field == "status_id"
847 835 when "c"
848 836 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_true})" if field == "status_id"
849 837 when "><t-"
850 838 # between today - n days and today
851 839 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
852 840 when ">t-"
853 841 # >= today - n days
854 842 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
855 843 when "<t-"
856 844 # <= today - n days
857 845 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
858 846 when "t-"
859 847 # = n days in past
860 848 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
861 849 when "><t+"
862 850 # between today and today + n days
863 851 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
864 852 when ">t+"
865 853 # >= today + n days
866 854 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
867 855 when "<t+"
868 856 # <= today + n days
869 857 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
870 858 when "t+"
871 859 # = today + n days
872 860 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
873 861 when "t"
874 862 # = today
875 863 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
876 864 when "ld"
877 865 # = yesterday
878 866 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
879 867 when "w"
880 868 # = this week
881 869 first_day_of_week = l(:general_first_day_of_week).to_i
882 870 day_of_week = Date.today.cwday
883 871 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
884 872 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
885 873 when "lw"
886 874 # = last week
887 875 first_day_of_week = l(:general_first_day_of_week).to_i
888 876 day_of_week = Date.today.cwday
889 877 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
890 878 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
891 879 when "l2w"
892 880 # = last 2 weeks
893 881 first_day_of_week = l(:general_first_day_of_week).to_i
894 882 day_of_week = Date.today.cwday
895 883 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
896 884 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
897 885 when "m"
898 886 # = this month
899 887 date = Date.today
900 888 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
901 889 when "lm"
902 890 # = last month
903 891 date = Date.today.prev_month
904 892 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
905 893 when "y"
906 894 # = this year
907 895 date = Date.today
908 896 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
909 897 when "~"
910 898 sql = sql_contains("#{db_table}.#{db_field}", value.first)
911 899 when "!~"
912 900 sql = sql_contains("#{db_table}.#{db_field}", value.first, false)
913 901 else
914 902 raise "Unknown query operator #{operator}"
915 903 end
916 904
917 905 return sql
918 906 end
919 907
920 908 # Returns a SQL LIKE statement with wildcards
921 909 def sql_contains(db_field, value, match=true)
922 910 value = "'%#{self.class.connection.quote_string(value.to_s)}%'"
923 911 Redmine::Database.like(db_field, value, :match => match)
924 912 end
925 913
926 914 # Adds a filter for the given custom field
927 915 def add_custom_field_filter(field, assoc=nil)
928 916 options = field.query_filter_options(self)
929 917 if field.format.target_class && field.format.target_class <= User
930 918 if options[:values].is_a?(Array) && User.current.logged?
931 919 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
932 920 end
933 921 end
934 922
935 923 filter_id = "cf_#{field.id}"
936 924 filter_name = field.name
937 925 if assoc.present?
938 926 filter_id = "#{assoc}.#{filter_id}"
939 927 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
940 928 end
941 929 add_available_filter filter_id, options.merge({
942 930 :name => filter_name,
943 931 :field => field
944 932 })
945 933 end
946 934
947 935 # Adds filters for the given custom fields scope
948 936 def add_custom_fields_filters(scope, assoc=nil)
949 937 scope.visible.where(:is_filter => true).sorted.each do |field|
950 938 add_custom_field_filter(field, assoc)
951 939 end
952 940 end
953 941
954 942 # Adds filters for the given associations custom fields
955 943 def add_associations_custom_fields_filters(*associations)
956 944 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
957 945 associations.each do |assoc|
958 946 association_klass = queried_class.reflect_on_association(assoc).klass
959 947 fields_by_class.each do |field_class, fields|
960 948 if field_class.customized_class <= association_klass
961 949 fields.sort.each do |field|
962 950 add_custom_field_filter(field, assoc)
963 951 end
964 952 end
965 953 end
966 954 end
967 955 end
968 956
969 957 def quoted_time(time, is_custom_filter)
970 958 if is_custom_filter
971 959 # Custom field values are stored as strings in the DB
972 960 # using this format that does not depend on DB date representation
973 961 time.strftime("%Y-%m-%d %H:%M:%S")
974 962 else
975 963 self.class.connection.quoted_date(time)
976 964 end
977 965 end
978 966
979 967 # Returns a SQL clause for a date or datetime field.
980 968 def date_clause(table, field, from, to, is_custom_filter)
981 969 s = []
982 970 if from
983 971 if from.is_a?(Date)
984 972 from = Time.local(from.year, from.month, from.day).yesterday.end_of_day
985 973 else
986 974 from = from - 1 # second
987 975 end
988 976 if self.class.default_timezone == :utc
989 977 from = from.utc
990 978 end
991 979 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
992 980 end
993 981 if to
994 982 if to.is_a?(Date)
995 983 to = Time.local(to.year, to.month, to.day).end_of_day
996 984 end
997 985 if self.class.default_timezone == :utc
998 986 to = to.utc
999 987 end
1000 988 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
1001 989 end
1002 990 s.join(' AND ')
1003 991 end
1004 992
1005 993 # Returns a SQL clause for a date or datetime field using relative dates.
1006 994 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
1007 995 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil), is_custom_filter)
1008 996 end
1009 997
1010 998 # Returns a Date or Time from the given filter value
1011 999 def parse_date(arg)
1012 1000 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
1013 1001 Time.parse(arg) rescue nil
1014 1002 else
1015 1003 Date.parse(arg) rescue nil
1016 1004 end
1017 1005 end
1018 1006
1019 1007 # Additional joins required for the given sort options
1020 1008 def joins_for_order_statement(order_options)
1021 1009 joins = []
1022 1010
1023 1011 if order_options
1024 1012 if order_options.include?('authors')
1025 1013 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
1026 1014 end
1027 1015 order_options.scan(/cf_\d+/).uniq.each do |name|
1028 1016 column = available_columns.detect {|c| c.name.to_s == name}
1029 1017 join = column && column.custom_field.join_for_order_statement
1030 1018 if join
1031 1019 joins << join
1032 1020 end
1033 1021 end
1034 1022 end
1035 1023
1036 1024 joins.any? ? joins.join(' ') : nil
1037 1025 end
1038 1026 end
@@ -1,784 +1,805
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 module Redmine
19 19 module FieldFormat
20 20 def self.add(name, klass)
21 21 all[name.to_s] = klass.instance
22 22 end
23 23
24 24 def self.delete(name)
25 25 all.delete(name.to_s)
26 26 end
27 27
28 28 def self.all
29 29 @formats ||= Hash.new(Base.instance)
30 30 end
31 31
32 32 def self.available_formats
33 33 all.keys
34 34 end
35 35
36 36 def self.find(name)
37 37 all[name.to_s]
38 38 end
39 39
40 40 # Return an array of custom field formats which can be used in select_tag
41 41 def self.as_select(class_name=nil)
42 42 formats = all.values.select do |format|
43 43 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
44 44 end
45 45 formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
46 46 end
47 47
48 48 class Base
49 49 include Singleton
50 50 include Redmine::I18n
51 51 include ERB::Util
52 52
53 53 class_attribute :format_name
54 54 self.format_name = nil
55 55
56 56 # Set this to true if the format supports multiple values
57 57 class_attribute :multiple_supported
58 58 self.multiple_supported = false
59 59
60 60 # Set this to true if the format supports textual search on custom values
61 61 class_attribute :searchable_supported
62 62 self.searchable_supported = false
63 63
64 # Set this to true if field values can be summed up
65 class_attribute :totalable_supported
66 self.totalable_supported = false
67
64 68 # Restricts the classes that the custom field can be added to
65 69 # Set to nil for no restrictions
66 70 class_attribute :customized_class_names
67 71 self.customized_class_names = nil
68 72
69 73 # Name of the partial for editing the custom field
70 74 class_attribute :form_partial
71 75 self.form_partial = nil
72 76
73 77 class_attribute :change_as_diff
74 78 self.change_as_diff = false
75 79
76 80 def self.add(name)
77 81 self.format_name = name
78 82 Redmine::FieldFormat.add(name, self)
79 83 end
80 84 private_class_method :add
81 85
82 86 def self.field_attributes(*args)
83 87 CustomField.store_accessor :format_store, *args
84 88 end
85 89
86 90 field_attributes :url_pattern
87 91
88 92 def name
89 93 self.class.format_name
90 94 end
91 95
92 96 def label
93 97 "label_#{name}"
94 98 end
95 99
96 100 def cast_custom_value(custom_value)
97 101 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
98 102 end
99 103
100 104 def cast_value(custom_field, value, customized=nil)
101 105 if value.blank?
102 106 nil
103 107 elsif value.is_a?(Array)
104 108 casted = value.map do |v|
105 109 cast_single_value(custom_field, v, customized)
106 110 end
107 111 casted.compact.sort
108 112 else
109 113 cast_single_value(custom_field, value, customized)
110 114 end
111 115 end
112 116
113 117 def cast_single_value(custom_field, value, customized=nil)
114 118 value.to_s
115 119 end
116 120
117 121 def target_class
118 122 nil
119 123 end
120 124
121 125 def possible_custom_value_options(custom_value)
122 126 possible_values_options(custom_value.custom_field, custom_value.customized)
123 127 end
124 128
125 129 def possible_values_options(custom_field, object=nil)
126 130 []
127 131 end
128 132
129 133 def value_from_keyword(custom_field, keyword, object)
130 134 possible_values_options = possible_values_options(custom_field, object)
131 135 if possible_values_options.present?
132 136 keyword = keyword.to_s
133 137 if v = possible_values_options.detect {|text, id| keyword.casecmp(text) == 0}
134 138 if v.is_a?(Array)
135 139 v.last
136 140 else
137 141 v
138 142 end
139 143 end
140 144 else
141 145 keyword
142 146 end
143 147 end
144 148
145 149 # Returns the validation errors for custom_field
146 150 # Should return an empty array if custom_field is valid
147 151 def validate_custom_field(custom_field)
148 152 []
149 153 end
150 154
151 155 # Returns the validation error messages for custom_value
152 156 # Should return an empty array if custom_value is valid
153 157 def validate_custom_value(custom_value)
154 158 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
155 159 errors = values.map do |value|
156 160 validate_single_value(custom_value.custom_field, value, custom_value.customized)
157 161 end
158 162 errors.flatten.uniq
159 163 end
160 164
161 165 def validate_single_value(custom_field, value, customized=nil)
162 166 []
163 167 end
164 168
165 169 def formatted_custom_value(view, custom_value, html=false)
166 170 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
167 171 end
168 172
169 173 def formatted_value(view, custom_field, value, customized=nil, html=false)
170 174 casted = cast_value(custom_field, value, customized)
171 175 if html && custom_field.url_pattern.present?
172 176 texts_and_urls = Array.wrap(casted).map do |single_value|
173 177 text = view.format_object(single_value, false).to_s
174 178 url = url_from_pattern(custom_field, single_value, customized)
175 179 [text, url]
176 180 end
177 181 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to text, url}
178 182 links.join(', ').html_safe
179 183 else
180 184 casted
181 185 end
182 186 end
183 187
184 188 # Returns an URL generated with the custom field URL pattern
185 189 # and variables substitution:
186 190 # %value% => the custom field value
187 191 # %id% => id of the customized object
188 192 # %project_id% => id of the project of the customized object if defined
189 193 # %project_identifier% => identifier of the project of the customized object if defined
190 194 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
191 195 def url_from_pattern(custom_field, value, customized)
192 196 url = custom_field.url_pattern.to_s.dup
193 197 url.gsub!('%value%') {value.to_s}
194 198 url.gsub!('%id%') {customized.id.to_s}
195 199 url.gsub!('%project_id%') {(customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
196 200 url.gsub!('%project_identifier%') {(customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
197 201 if custom_field.regexp.present?
198 202 url.gsub!(%r{%m(\d+)%}) do
199 203 m = $1.to_i
200 204 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
201 205 matches[m].to_s
202 206 end
203 207 end
204 208 end
205 209 url
206 210 end
207 211 protected :url_from_pattern
208 212
209 213 def edit_tag(view, tag_id, tag_name, custom_value, options={})
210 214 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
211 215 end
212 216
213 217 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
214 218 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
215 219 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
216 220 end
217 221
218 222 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
219 223 if custom_field.is_required?
220 224 ''.html_safe
221 225 else
222 226 view.content_tag('label',
223 227 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
224 228 :class => 'inline'
225 229 )
226 230 end
227 231 end
228 232 protected :bulk_clear_tag
229 233
230 234 def query_filter_options(custom_field, query)
231 235 {:type => :string}
232 236 end
233 237
234 238 def before_custom_field_save(custom_field)
235 239 end
236 240
237 241 # Returns a ORDER BY clause that can used to sort customized
238 242 # objects by their value of the custom field.
239 243 # Returns nil if the custom field can not be used for sorting.
240 244 def order_statement(custom_field)
241 245 # COALESCE is here to make sure that blank and NULL values are sorted equally
242 246 "COALESCE(#{join_alias custom_field}.value, '')"
243 247 end
244 248
245 249 # Returns a GROUP BY clause that can used to group by custom value
246 250 # Returns nil if the custom field can not be used for grouping.
247 251 def group_statement(custom_field)
248 252 nil
249 253 end
250 254
251 255 # Returns a JOIN clause that is added to the query when sorting by custom values
252 256 def join_for_order_statement(custom_field)
253 257 alias_name = join_alias(custom_field)
254 258
255 259 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
256 260 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
257 261 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
258 262 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
259 263 " AND (#{custom_field.visibility_by_project_condition})" +
260 264 " AND #{alias_name}.value <> ''" +
261 265 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
262 266 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
263 267 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
264 268 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
265 269 end
266 270
267 271 def join_alias(custom_field)
268 272 "cf_#{custom_field.id}"
269 273 end
270 274 protected :join_alias
271 275 end
272 276
273 277 class Unbounded < Base
274 278 def validate_single_value(custom_field, value, customized=nil)
275 279 errs = super
276 280 value = value.to_s
277 281 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
278 282 errs << ::I18n.t('activerecord.errors.messages.invalid')
279 283 end
280 284 if custom_field.min_length && value.length < custom_field.min_length
281 285 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
282 286 end
283 287 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
284 288 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
285 289 end
286 290 errs
287 291 end
288 292 end
289 293
290 294 class StringFormat < Unbounded
291 295 add 'string'
292 296 self.searchable_supported = true
293 297 self.form_partial = 'custom_fields/formats/string'
294 298 field_attributes :text_formatting
295 299
296 300 def formatted_value(view, custom_field, value, customized=nil, html=false)
297 301 if html
298 302 if custom_field.url_pattern.present?
299 303 super
300 304 elsif custom_field.text_formatting == 'full'
301 305 view.textilizable(value, :object => customized)
302 306 else
303 307 value.to_s
304 308 end
305 309 else
306 310 value.to_s
307 311 end
308 312 end
309 313 end
310 314
311 315 class TextFormat < Unbounded
312 316 add 'text'
313 317 self.searchable_supported = true
314 318 self.form_partial = 'custom_fields/formats/text'
315 319 self.change_as_diff = true
316 320
317 321 def formatted_value(view, custom_field, value, customized=nil, html=false)
318 322 if html
319 323 if value.present?
320 324 if custom_field.text_formatting == 'full'
321 325 view.textilizable(value, :object => customized)
322 326 else
323 327 view.simple_format(html_escape(value))
324 328 end
325 329 else
326 330 ''
327 331 end
328 332 else
329 333 value.to_s
330 334 end
331 335 end
332 336
333 337 def edit_tag(view, tag_id, tag_name, custom_value, options={})
334 338 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
335 339 end
336 340
337 341 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
338 342 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
339 343 '<br />'.html_safe +
340 344 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
341 345 end
342 346
343 347 def query_filter_options(custom_field, query)
344 348 {:type => :text}
345 349 end
346 350 end
347 351
348 352 class LinkFormat < StringFormat
349 353 add 'link'
350 354 self.searchable_supported = false
351 355 self.form_partial = 'custom_fields/formats/link'
352 356
353 357 def formatted_value(view, custom_field, value, customized=nil, html=false)
354 358 if html
355 359 if custom_field.url_pattern.present?
356 360 url = url_from_pattern(custom_field, value, customized)
357 361 else
358 362 url = value.to_s
359 363 unless url =~ %r{\A[a-z]+://}i
360 364 # no protocol found, use http by default
361 365 url = "http://" + url
362 366 end
363 367 end
364 368 view.link_to value.to_s.truncate(40), url
365 369 else
366 370 value.to_s
367 371 end
368 372 end
369 373 end
370 374
371 375 class Numeric < Unbounded
372 376 self.form_partial = 'custom_fields/formats/numeric'
377 self.totalable_supported = true
373 378
374 379 def order_statement(custom_field)
375 380 # Make the database cast values into numeric
376 381 # Postgresql will raise an error if a value can not be casted!
377 382 # CustomValue validations should ensure that it doesn't occur
378 383 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
379 384 end
385
386 # Returns totals for the given scope
387 def total_for_scope(custom_field, scope)
388 scope.joins(:custom_values).
389 where(:custom_values => {:custom_field_id => custom_field.id}).
390 where.not(:custom_values => {:value => ''}).
391 sum("CAST(#{CustomValue.table_name}.value AS decimal(30,3))")
392 end
393
394 def cast_total_value(custom_field, value)
395 cast_single_value(custom_field, value)
396 end
380 397 end
381 398
382 399 class IntFormat < Numeric
383 400 add 'int'
384 401
385 402 def label
386 403 "label_integer"
387 404 end
388 405
389 406 def cast_single_value(custom_field, value, customized=nil)
390 407 value.to_i
391 408 end
392 409
393 410 def validate_single_value(custom_field, value, customized=nil)
394 411 errs = super
395 412 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s =~ /^[+-]?\d+$/
396 413 errs
397 414 end
398 415
399 416 def query_filter_options(custom_field, query)
400 417 {:type => :integer}
401 418 end
402 419
403 420 def group_statement(custom_field)
404 421 order_statement(custom_field)
405 422 end
406 423 end
407 424
408 425 class FloatFormat < Numeric
409 426 add 'float'
410 427
411 428 def cast_single_value(custom_field, value, customized=nil)
412 429 value.to_f
413 430 end
414 431
432 def cast_total_value(custom_field, value)
433 value.to_f.round(2)
434 end
435
415 436 def validate_single_value(custom_field, value, customized=nil)
416 437 errs = super
417 438 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
418 439 errs
419 440 end
420 441
421 442 def query_filter_options(custom_field, query)
422 443 {:type => :float}
423 444 end
424 445 end
425 446
426 447 class DateFormat < Unbounded
427 448 add 'date'
428 449 self.form_partial = 'custom_fields/formats/date'
429 450
430 451 def cast_single_value(custom_field, value, customized=nil)
431 452 value.to_date rescue nil
432 453 end
433 454
434 455 def validate_single_value(custom_field, value, customized=nil)
435 456 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
436 457 []
437 458 else
438 459 [::I18n.t('activerecord.errors.messages.not_a_date')]
439 460 end
440 461 end
441 462
442 463 def edit_tag(view, tag_id, tag_name, custom_value, options={})
443 464 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
444 465 view.calendar_for(tag_id)
445 466 end
446 467
447 468 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
448 469 view.text_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
449 470 view.calendar_for(tag_id) +
450 471 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
451 472 end
452 473
453 474 def query_filter_options(custom_field, query)
454 475 {:type => :date}
455 476 end
456 477
457 478 def group_statement(custom_field)
458 479 order_statement(custom_field)
459 480 end
460 481 end
461 482
462 483 class List < Base
463 484 self.multiple_supported = true
464 485 field_attributes :edit_tag_style
465 486
466 487 def edit_tag(view, tag_id, tag_name, custom_value, options={})
467 488 if custom_value.custom_field.edit_tag_style == 'check_box'
468 489 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
469 490 else
470 491 select_edit_tag(view, tag_id, tag_name, custom_value, options)
471 492 end
472 493 end
473 494
474 495 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
475 496 opts = []
476 497 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
477 498 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
478 499 opts += possible_values_options(custom_field, objects)
479 500 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
480 501 end
481 502
482 503 def query_filter_options(custom_field, query)
483 504 {:type => :list_optional, :values => query_filter_values(custom_field, query)}
484 505 end
485 506
486 507 protected
487 508
488 509 # Returns the values that are available in the field filter
489 510 def query_filter_values(custom_field, query)
490 511 possible_values_options(custom_field, query.project)
491 512 end
492 513
493 514 # Renders the edit tag as a select tag
494 515 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
495 516 blank_option = ''.html_safe
496 517 unless custom_value.custom_field.multiple?
497 518 if custom_value.custom_field.is_required?
498 519 unless custom_value.custom_field.default_value.present?
499 520 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
500 521 end
501 522 else
502 523 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
503 524 end
504 525 end
505 526 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
506 527 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
507 528 if custom_value.custom_field.multiple?
508 529 s << view.hidden_field_tag(tag_name, '')
509 530 end
510 531 s
511 532 end
512 533
513 534 # Renders the edit tag as check box or radio tags
514 535 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
515 536 opts = []
516 537 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
517 538 opts << ["(#{l(:label_none)})", '']
518 539 end
519 540 opts += possible_custom_value_options(custom_value)
520 541 s = ''.html_safe
521 542 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
522 543 opts.each do |label, value|
523 544 value ||= label
524 545 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
525 546 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
526 547 # set the id on the first tag only
527 548 tag_id = nil
528 549 s << view.content_tag('label', tag + ' ' + label)
529 550 end
530 551 if custom_value.custom_field.multiple?
531 552 s << view.hidden_field_tag(tag_name, '')
532 553 end
533 554 css = "#{options[:class]} check_box_group"
534 555 view.content_tag('span', s, options.merge(:class => css))
535 556 end
536 557 end
537 558
538 559 class ListFormat < List
539 560 add 'list'
540 561 self.searchable_supported = true
541 562 self.form_partial = 'custom_fields/formats/list'
542 563
543 564 def possible_custom_value_options(custom_value)
544 565 options = possible_values_options(custom_value.custom_field)
545 566 missing = [custom_value.value].flatten.reject(&:blank?) - options
546 567 if missing.any?
547 568 options += missing
548 569 end
549 570 options
550 571 end
551 572
552 573 def possible_values_options(custom_field, object=nil)
553 574 custom_field.possible_values
554 575 end
555 576
556 577 def validate_custom_field(custom_field)
557 578 errors = []
558 579 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
559 580 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
560 581 errors
561 582 end
562 583
563 584 def validate_custom_value(custom_value)
564 585 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
565 586 invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
566 587 if invalid_values.any?
567 588 [::I18n.t('activerecord.errors.messages.inclusion')]
568 589 else
569 590 []
570 591 end
571 592 end
572 593
573 594 def group_statement(custom_field)
574 595 order_statement(custom_field)
575 596 end
576 597 end
577 598
578 599 class BoolFormat < List
579 600 add 'bool'
580 601 self.multiple_supported = false
581 602 self.form_partial = 'custom_fields/formats/bool'
582 603
583 604 def label
584 605 "label_boolean"
585 606 end
586 607
587 608 def cast_single_value(custom_field, value, customized=nil)
588 609 value == '1' ? true : false
589 610 end
590 611
591 612 def possible_values_options(custom_field, object=nil)
592 613 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
593 614 end
594 615
595 616 def group_statement(custom_field)
596 617 order_statement(custom_field)
597 618 end
598 619
599 620 def edit_tag(view, tag_id, tag_name, custom_value, options={})
600 621 case custom_value.custom_field.edit_tag_style
601 622 when 'check_box'
602 623 single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
603 624 when 'radio'
604 625 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
605 626 else
606 627 select_edit_tag(view, tag_id, tag_name, custom_value, options)
607 628 end
608 629 end
609 630
610 631 # Renders the edit tag as a simple check box
611 632 def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
612 633 s = ''.html_safe
613 634 s << view.hidden_field_tag(tag_name, '0', :id => nil)
614 635 s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', :id => tag_id)
615 636 view.content_tag('span', s, options)
616 637 end
617 638 end
618 639
619 640 class RecordList < List
620 641 self.customized_class_names = %w(Issue TimeEntry Version Document Project)
621 642
622 643 def cast_single_value(custom_field, value, customized=nil)
623 644 target_class.find_by_id(value.to_i) if value.present?
624 645 end
625 646
626 647 def target_class
627 648 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
628 649 end
629 650
630 651 def reset_target_class
631 652 @target_class = nil
632 653 end
633 654
634 655 def possible_custom_value_options(custom_value)
635 656 options = possible_values_options(custom_value.custom_field, custom_value.customized)
636 657 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
637 658 if missing.any?
638 659 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
639 660 end
640 661 options
641 662 end
642 663
643 664 def order_statement(custom_field)
644 665 if target_class.respond_to?(:fields_for_order_statement)
645 666 target_class.fields_for_order_statement(value_join_alias(custom_field))
646 667 end
647 668 end
648 669
649 670 def group_statement(custom_field)
650 671 "COALESCE(#{join_alias custom_field}.value, '')"
651 672 end
652 673
653 674 def join_for_order_statement(custom_field)
654 675 alias_name = join_alias(custom_field)
655 676
656 677 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
657 678 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
658 679 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
659 680 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
660 681 " AND (#{custom_field.visibility_by_project_condition})" +
661 682 " AND #{alias_name}.value <> ''" +
662 683 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
663 684 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
664 685 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
665 686 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
666 687 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
667 688 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
668 689 end
669 690
670 691 def value_join_alias(custom_field)
671 692 join_alias(custom_field) + "_" + custom_field.field_format
672 693 end
673 694 protected :value_join_alias
674 695 end
675 696
676 697 class EnumerationFormat < RecordList
677 698 add 'enumeration'
678 699 self.form_partial = 'custom_fields/formats/enumeration'
679 700
680 701 def label
681 702 "label_field_format_enumeration"
682 703 end
683 704
684 705 def target_class
685 706 @target_class ||= CustomFieldEnumeration
686 707 end
687 708
688 709 def possible_values_options(custom_field, object=nil)
689 710 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
690 711 end
691 712
692 713 def possible_values_records(custom_field, object=nil)
693 714 custom_field.enumerations.active
694 715 end
695 716
696 717 def value_from_keyword(custom_field, keyword, object)
697 718 value = custom_field.enumerations.where("LOWER(name) LIKE LOWER(?)", keyword)
698 719 value ? value.id : nil
699 720 end
700 721 end
701 722
702 723 class UserFormat < RecordList
703 724 add 'user'
704 725 self.form_partial = 'custom_fields/formats/user'
705 726 field_attributes :user_role
706 727
707 728 def possible_values_options(custom_field, object=nil)
708 729 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
709 730 end
710 731
711 732 def possible_values_records(custom_field, object=nil)
712 733 if object.is_a?(Array)
713 734 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
714 735 projects.map {|project| possible_values_records(custom_field, project)}.reduce(:&) || []
715 736 elsif object.respond_to?(:project) && object.project
716 737 scope = object.project.users
717 738 if custom_field.user_role.is_a?(Array)
718 739 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
719 740 if role_ids.any?
720 741 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
721 742 end
722 743 end
723 744 scope.sorted
724 745 else
725 746 []
726 747 end
727 748 end
728 749
729 750 def value_from_keyword(custom_field, keyword, object)
730 751 users = possible_values_records(custom_field, object).to_a
731 752 user = Principal.detect_by_keyword(users, keyword)
732 753 user ? user.id : nil
733 754 end
734 755
735 756 def before_custom_field_save(custom_field)
736 757 super
737 758 if custom_field.user_role.is_a?(Array)
738 759 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
739 760 end
740 761 end
741 762 end
742 763
743 764 class VersionFormat < RecordList
744 765 add 'version'
745 766 self.form_partial = 'custom_fields/formats/version'
746 767 field_attributes :version_status
747 768
748 769 def possible_values_options(custom_field, object=nil)
749 770 versions_options(custom_field, object)
750 771 end
751 772
752 773 def before_custom_field_save(custom_field)
753 774 super
754 775 if custom_field.version_status.is_a?(Array)
755 776 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
756 777 end
757 778 end
758 779
759 780 protected
760 781
761 782 def query_filter_values(custom_field, query)
762 783 versions_options(custom_field, query.project, true)
763 784 end
764 785
765 786 def versions_options(custom_field, object, all_statuses=false)
766 787 if object.is_a?(Array)
767 788 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
768 789 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
769 790 elsif object.respond_to?(:project) && object.project
770 791 scope = object.project.shared_versions
771 792 if !all_statuses && custom_field.version_status.is_a?(Array)
772 793 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
773 794 if statuses.any?
774 795 scope = scope.where(:status => statuses.map(&:to_s))
775 796 end
776 797 end
777 798 scope.sort.collect {|u| [u.to_s, u.id.to_s]}
778 799 else
779 800 []
780 801 end
781 802 end
782 803 end
783 804 end
784 805 end
General Comments 0
You need to be logged in to leave comments. Login now