##// END OF EJS Templates
Don't preload custom field filter values (#24787)....
Jean-Philippe Lang -
r15791:309c6cec861b
parent child
Show More
@@ -1,1231 +1,1226
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 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 QueryAssociationColumn < QueryColumn
78 78
79 79 def initialize(association, attribute, options={})
80 80 @association = association
81 81 @attribute = attribute
82 82 name_with_assoc = "#{association}.#{attribute}".to_sym
83 83 super(name_with_assoc, options)
84 84 end
85 85
86 86 def value_object(object)
87 87 if assoc = object.send(@association)
88 88 assoc.send @attribute
89 89 end
90 90 end
91 91
92 92 def css_classes
93 93 @css_classes ||= "#{@association}-#{@attribute}"
94 94 end
95 95 end
96 96
97 97 class QueryCustomFieldColumn < QueryColumn
98 98
99 99 def initialize(custom_field, options={})
100 100 self.name = "cf_#{custom_field.id}".to_sym
101 101 self.sortable = custom_field.order_statement || false
102 102 self.groupable = custom_field.group_statement || false
103 103 self.totalable = options.key?(:totalable) ? !!options[:totalable] : custom_field.totalable?
104 104 @inline = true
105 105 @cf = custom_field
106 106 end
107 107
108 108 def caption
109 109 @cf.name
110 110 end
111 111
112 112 def custom_field
113 113 @cf
114 114 end
115 115
116 116 def value_object(object)
117 117 if custom_field.visible_by?(object.project, User.current)
118 118 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
119 119 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
120 120 else
121 121 nil
122 122 end
123 123 end
124 124
125 125 def value(object)
126 126 raw = value_object(object)
127 127 if raw.is_a?(Array)
128 128 raw.map {|r| @cf.cast_value(r.value)}
129 129 elsif raw
130 130 @cf.cast_value(raw.value)
131 131 else
132 132 nil
133 133 end
134 134 end
135 135
136 136 def css_classes
137 137 @css_classes ||= "#{name} #{@cf.field_format}"
138 138 end
139 139 end
140 140
141 141 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
142 142
143 143 def initialize(association, custom_field, options={})
144 144 super(custom_field, options)
145 145 self.name = "#{association}.cf_#{custom_field.id}".to_sym
146 146 # TODO: support sorting/grouping by association custom field
147 147 self.sortable = false
148 148 self.groupable = false
149 149 @association = association
150 150 end
151 151
152 152 def value_object(object)
153 153 if assoc = object.send(@association)
154 154 super(assoc)
155 155 end
156 156 end
157 157
158 158 def css_classes
159 159 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
160 160 end
161 161 end
162 162
163 163 class QueryFilter
164 164 include Redmine::I18n
165 165
166 166 def initialize(field, options)
167 167 @field = field.to_s
168 168 @options = options
169 169 @options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
170 170 # Consider filters with a Proc for values as remote by default
171 171 @remote = options.key?(:remote) ? options[:remote] : options[:values].is_a?(Proc)
172 172 end
173 173
174 174 def [](arg)
175 175 if arg == :values
176 176 values
177 177 else
178 178 @options[arg]
179 179 end
180 180 end
181 181
182 182 def values
183 183 @values ||= begin
184 184 values = @options[:values]
185 185 if values.is_a?(Proc)
186 186 values = values.call
187 187 end
188 188 values
189 189 end
190 190 end
191 191
192 192 def remote
193 193 @remote
194 194 end
195 195 end
196 196
197 197 class Query < ActiveRecord::Base
198 198 class StatementInvalid < ::ActiveRecord::StatementInvalid
199 199 end
200 200
201 201 include Redmine::SubclassFactory
202 202
203 203 VISIBILITY_PRIVATE = 0
204 204 VISIBILITY_ROLES = 1
205 205 VISIBILITY_PUBLIC = 2
206 206
207 207 belongs_to :project
208 208 belongs_to :user
209 209 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
210 210 serialize :filters
211 211 serialize :column_names
212 212 serialize :sort_criteria, Array
213 213 serialize :options, Hash
214 214
215 215 attr_protected :project_id, :user_id
216 216
217 217 validates_presence_of :name
218 218 validates_length_of :name, :maximum => 255
219 219 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
220 220 validate :validate_query_filters
221 221 validate do |query|
222 222 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
223 223 end
224 224
225 225 after_save do |query|
226 226 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
227 227 query.roles.clear
228 228 end
229 229 end
230 230
231 231 class_attribute :operators
232 232 self.operators = {
233 233 "=" => :label_equals,
234 234 "!" => :label_not_equals,
235 235 "o" => :label_open_issues,
236 236 "c" => :label_closed_issues,
237 237 "!*" => :label_none,
238 238 "*" => :label_any,
239 239 ">=" => :label_greater_or_equal,
240 240 "<=" => :label_less_or_equal,
241 241 "><" => :label_between,
242 242 "<t+" => :label_in_less_than,
243 243 ">t+" => :label_in_more_than,
244 244 "><t+"=> :label_in_the_next_days,
245 245 "t+" => :label_in,
246 246 "t" => :label_today,
247 247 "ld" => :label_yesterday,
248 248 "w" => :label_this_week,
249 249 "lw" => :label_last_week,
250 250 "l2w" => [:label_last_n_weeks, {:count => 2}],
251 251 "m" => :label_this_month,
252 252 "lm" => :label_last_month,
253 253 "y" => :label_this_year,
254 254 ">t-" => :label_less_than_ago,
255 255 "<t-" => :label_more_than_ago,
256 256 "><t-"=> :label_in_the_past_days,
257 257 "t-" => :label_ago,
258 258 "~" => :label_contains,
259 259 "!~" => :label_not_contains,
260 260 "=p" => :label_any_issues_in_project,
261 261 "=!p" => :label_any_issues_not_in_project,
262 262 "!p" => :label_no_issues_in_project,
263 263 "*o" => :label_any_open_issues,
264 264 "!o" => :label_no_open_issues
265 265 }
266 266
267 267 class_attribute :operators_by_filter_type
268 268 self.operators_by_filter_type = {
269 269 :list => [ "=", "!" ],
270 270 :list_status => [ "o", "=", "!", "c", "*" ],
271 271 :list_optional => [ "=", "!", "!*", "*" ],
272 272 :list_subprojects => [ "*", "!*", "=" ],
273 273 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
274 274 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
275 275 :string => [ "=", "~", "!", "!~", "!*", "*" ],
276 276 :text => [ "~", "!~", "!*", "*" ],
277 277 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
278 278 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
279 279 :relation => ["=", "=p", "=!p", "!p", "*o", "!o", "!*", "*"],
280 280 :tree => ["=", "~", "!*", "*"]
281 281 }
282 282
283 283 class_attribute :available_columns
284 284 self.available_columns = []
285 285
286 286 class_attribute :queried_class
287 287
288 288 # Permission required to view the queries, set on subclasses.
289 289 class_attribute :view_permission
290 290
291 291 # Scope of queries that are global or on the given project
292 292 scope :global_or_on_project, lambda {|project|
293 293 where(:project_id => (project.nil? ? nil : [nil, project.id]))
294 294 }
295 295
296 296 scope :sorted, lambda {order(:name, :id)}
297 297
298 298 # Scope of visible queries, can be used from subclasses only.
299 299 # Unlike other visible scopes, a class methods is used as it
300 300 # let handle inheritance more nicely than scope DSL.
301 301 def self.visible(*args)
302 302 if self == ::Query
303 303 # Visibility depends on permissions for each subclass,
304 304 # raise an error if the scope is called from Query (eg. Query.visible)
305 305 raise Exception.new("Cannot call .visible scope from the base Query class, but from subclasses only.")
306 306 end
307 307
308 308 user = args.shift || User.current
309 309 base = Project.allowed_to_condition(user, view_permission, *args)
310 310 scope = joins("LEFT OUTER JOIN #{Project.table_name} ON #{table_name}.project_id = #{Project.table_name}.id").
311 311 where("#{table_name}.project_id IS NULL OR (#{base})")
312 312
313 313 if user.admin?
314 314 scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
315 315 elsif user.memberships.any?
316 316 scope.where("#{table_name}.visibility = ?" +
317 317 " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
318 318 "SELECT DISTINCT q.id FROM #{table_name} q" +
319 319 " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
320 320 " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
321 321 " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
322 322 " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
323 323 " OR #{table_name}.user_id = ?",
324 324 VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
325 325 elsif user.logged?
326 326 scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
327 327 else
328 328 scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
329 329 end
330 330 end
331 331
332 332 # Returns true if the query is visible to +user+ or the current user.
333 333 def visible?(user=User.current)
334 334 return true if user.admin?
335 335 return false unless project.nil? || user.allowed_to?(self.class.view_permission, project)
336 336 case visibility
337 337 when VISIBILITY_PUBLIC
338 338 true
339 339 when VISIBILITY_ROLES
340 340 if project
341 341 (user.roles_for_project(project) & roles).any?
342 342 else
343 343 Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
344 344 end
345 345 else
346 346 user == self.user
347 347 end
348 348 end
349 349
350 350 def is_private?
351 351 visibility == VISIBILITY_PRIVATE
352 352 end
353 353
354 354 def is_public?
355 355 !is_private?
356 356 end
357 357
358 358 def queried_table_name
359 359 @queried_table_name ||= self.class.queried_class.table_name
360 360 end
361 361
362 362 def initialize(attributes=nil, *args)
363 363 super attributes
364 364 @is_for_all = project.nil?
365 365 end
366 366
367 367 # Builds the query from the given params
368 368 def build_from_params(params)
369 369 if params[:fields] || params[:f]
370 370 self.filters = {}
371 371 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
372 372 else
373 373 available_filters.keys.each do |field|
374 374 add_short_filter(field, params[field]) if params[field]
375 375 end
376 376 end
377 377 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
378 378 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
379 379 self.totalable_names = params[:t] || (params[:query] && params[:query][:totalable_names])
380 380 self
381 381 end
382 382
383 383 # Builds a new query from the given params and attributes
384 384 def self.build_from_params(params, attributes={})
385 385 new(attributes).build_from_params(params)
386 386 end
387 387
388 388 def validate_query_filters
389 389 filters.each_key do |field|
390 390 if values_for(field)
391 391 case type_for(field)
392 392 when :integer
393 393 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(,[+-]?\d+)*\z/) }
394 394 when :float
395 395 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(\.\d*)?\z/) }
396 396 when :date, :date_past
397 397 case operator_for(field)
398 398 when "=", ">=", "<=", "><"
399 399 add_filter_error(field, :invalid) if values_for(field).detect {|v|
400 400 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?)
401 401 }
402 402 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
403 403 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
404 404 end
405 405 end
406 406 end
407 407
408 408 add_filter_error(field, :blank) unless
409 409 # filter requires one or more values
410 410 (values_for(field) and !values_for(field).first.blank?) or
411 411 # filter doesn't require any value
412 412 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "*o", "!o"].include? operator_for(field)
413 413 end if filters
414 414 end
415 415
416 416 def add_filter_error(field, message)
417 417 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
418 418 errors.add(:base, m)
419 419 end
420 420
421 421 def editable_by?(user)
422 422 return false unless user
423 423 # Admin can edit them all and regular users can edit their private queries
424 424 return true if user.admin? || (is_private? && self.user_id == user.id)
425 425 # Members can not edit public queries that are for all project (only admin is allowed to)
426 426 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
427 427 end
428 428
429 429 def trackers
430 430 @trackers ||= (project.nil? ? Tracker.all : project.rolled_up_trackers).visible.sorted
431 431 end
432 432
433 433 # Returns a hash of localized labels for all filter operators
434 434 def self.operators_labels
435 435 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
436 436 end
437 437
438 438 # Returns a representation of the available filters for JSON serialization
439 439 def available_filters_as_json
440 440 json = {}
441 441 available_filters.each do |field, filter|
442 442 options = {:type => filter[:type], :name => filter[:name]}
443 443 options[:remote] = true if filter.remote
444 444
445 445 if has_filter?(field) || !filter.remote
446 446 options[:values] = filter.values
447 447 if options[:values] && values_for(field)
448 448 missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last)
449 449 if missing.any? && respond_to?(method = "find_#{field}_filter_values")
450 450 options[:values] += send(method, missing)
451 451 end
452 452 end
453 453 end
454 454 json[field] = options.stringify_keys
455 455 end
456 456 json
457 457 end
458 458
459 459 def all_projects
460 460 @all_projects ||= Project.visible.to_a
461 461 end
462 462
463 463 def all_projects_values
464 464 return @all_projects_values if @all_projects_values
465 465
466 466 values = []
467 467 Project.project_tree(all_projects) do |p, level|
468 468 prefix = (level > 0 ? ('--' * level + ' ') : '')
469 469 values << ["#{prefix}#{p.name}", p.id.to_s]
470 470 end
471 471 @all_projects_values = values
472 472 end
473 473
474 474 def project_values
475 475 project_values = []
476 476 if User.current.logged? && User.current.memberships.any?
477 477 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
478 478 end
479 479 project_values += all_projects_values
480 480 project_values
481 481 end
482 482
483 483 def subproject_values
484 484 project.descendants.visible.collect{|s| [s.name, s.id.to_s] }
485 485 end
486 486
487 487 def principals
488 488 @principal ||= begin
489 489 principals = []
490 490 if project
491 491 principals += project.principals.visible
492 492 unless project.leaf?
493 493 principals += Principal.member_of(project.descendants.visible).visible
494 494 end
495 495 else
496 496 principals += Principal.member_of(all_projects).visible
497 497 end
498 498 principals.uniq!
499 499 principals.sort!
500 500 principals.reject! {|p| p.is_a?(GroupBuiltin)}
501 501 principals
502 502 end
503 503 end
504 504
505 505 def users
506 506 principals.select {|p| p.is_a?(User)}
507 507 end
508 508
509 509 def author_values
510 510 author_values = []
511 511 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
512 512 author_values += users.collect{|s| [s.name, s.id.to_s] }
513 513 author_values
514 514 end
515 515
516 516 def assigned_to_values
517 517 assigned_to_values = []
518 518 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
519 519 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
520 520 assigned_to_values
521 521 end
522 522
523 523 def fixed_version_values
524 524 versions = []
525 525 if project
526 526 versions = project.shared_versions.to_a
527 527 else
528 528 versions = Version.visible.where(:sharing => 'system').to_a
529 529 end
530 530 Version.sort_by_status(versions).collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s, l("version_status_#{s.status}")] }
531 531 end
532 532
533 533 # Adds available filters
534 534 def initialize_available_filters
535 535 # implemented by sub-classes
536 536 end
537 537 protected :initialize_available_filters
538 538
539 539 # Adds an available filter
540 540 def add_available_filter(field, options)
541 541 @available_filters ||= ActiveSupport::OrderedHash.new
542 542 @available_filters[field] = QueryFilter.new(field, options)
543 543 @available_filters
544 544 end
545 545
546 546 # Removes an available filter
547 547 def delete_available_filter(field)
548 548 if @available_filters
549 549 @available_filters.delete(field)
550 550 end
551 551 end
552 552
553 553 # Return a hash of available filters
554 554 def available_filters
555 555 unless @available_filters
556 556 initialize_available_filters
557 557 @available_filters ||= {}
558 558 end
559 559 @available_filters
560 560 end
561 561
562 562 def add_filter(field, operator, values=nil)
563 563 # values must be an array
564 564 return unless values.nil? || values.is_a?(Array)
565 565 # check if field is defined as an available filter
566 566 if available_filters.has_key? field
567 567 filter_options = available_filters[field]
568 568 filters[field] = {:operator => operator, :values => (values || [''])}
569 569 end
570 570 end
571 571
572 572 def add_short_filter(field, expression)
573 573 return unless expression && available_filters.has_key?(field)
574 574 field_type = available_filters[field][:type]
575 575 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
576 576 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
577 577 values = $1
578 578 add_filter field, operator, values.present? ? values.split('|') : ['']
579 579 end || add_filter(field, '=', expression.to_s.split('|'))
580 580 end
581 581
582 582 # Add multiple filters using +add_filter+
583 583 def add_filters(fields, operators, values)
584 584 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
585 585 fields.each do |field|
586 586 add_filter(field, operators[field], values && values[field])
587 587 end
588 588 end
589 589 end
590 590
591 591 def has_filter?(field)
592 592 filters and filters[field]
593 593 end
594 594
595 595 def type_for(field)
596 596 available_filters[field][:type] if available_filters.has_key?(field)
597 597 end
598 598
599 599 def operator_for(field)
600 600 has_filter?(field) ? filters[field][:operator] : nil
601 601 end
602 602
603 603 def values_for(field)
604 604 has_filter?(field) ? filters[field][:values] : nil
605 605 end
606 606
607 607 def value_for(field, index=0)
608 608 (values_for(field) || [])[index]
609 609 end
610 610
611 611 def label_for(field)
612 612 label = available_filters[field][:name] if available_filters.has_key?(field)
613 613 label ||= queried_class.human_attribute_name(field, :default => field)
614 614 end
615 615
616 616 def self.add_available_column(column)
617 617 self.available_columns << (column) if column.is_a?(QueryColumn)
618 618 end
619 619
620 620 # Returns an array of columns that can be used to group the results
621 621 def groupable_columns
622 622 available_columns.select {|c| c.groupable}
623 623 end
624 624
625 625 # Returns a Hash of columns and the key for sorting
626 626 def sortable_columns
627 627 available_columns.inject({}) {|h, column|
628 628 h[column.name.to_s] = column.sortable
629 629 h
630 630 }
631 631 end
632 632
633 633 def columns
634 634 # preserve the column_names order
635 635 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
636 636 available_columns.find { |col| col.name == name }
637 637 end.compact
638 638 available_columns.select(&:frozen?) | cols
639 639 end
640 640
641 641 def inline_columns
642 642 columns.select(&:inline?)
643 643 end
644 644
645 645 def block_columns
646 646 columns.reject(&:inline?)
647 647 end
648 648
649 649 def available_inline_columns
650 650 available_columns.select(&:inline?)
651 651 end
652 652
653 653 def available_block_columns
654 654 available_columns.reject(&:inline?)
655 655 end
656 656
657 657 def available_totalable_columns
658 658 available_columns.select(&:totalable)
659 659 end
660 660
661 661 def default_columns_names
662 662 []
663 663 end
664 664
665 665 def default_totalable_names
666 666 []
667 667 end
668 668
669 669 def column_names=(names)
670 670 if names
671 671 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
672 672 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
673 673 # Set column_names to nil if default columns
674 674 if names == default_columns_names
675 675 names = nil
676 676 end
677 677 end
678 678 write_attribute(:column_names, names)
679 679 end
680 680
681 681 def has_column?(column)
682 682 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
683 683 end
684 684
685 685 def has_custom_field_column?
686 686 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
687 687 end
688 688
689 689 def has_default_columns?
690 690 column_names.nil? || column_names.empty?
691 691 end
692 692
693 693 def totalable_columns
694 694 names = totalable_names
695 695 available_totalable_columns.select {|column| names.include?(column.name)}
696 696 end
697 697
698 698 def totalable_names=(names)
699 699 if names
700 700 names = names.select(&:present?).map {|n| n.is_a?(Symbol) ? n : n.to_sym}
701 701 end
702 702 options[:totalable_names] = names
703 703 end
704 704
705 705 def totalable_names
706 706 options[:totalable_names] || default_totalable_names || []
707 707 end
708 708
709 709 def sort_criteria=(arg)
710 710 c = []
711 711 if arg.is_a?(Hash)
712 712 arg = arg.keys.sort.collect {|k| arg[k]}
713 713 end
714 714 if arg
715 715 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
716 716 end
717 717 write_attribute(:sort_criteria, c)
718 718 end
719 719
720 720 def sort_criteria
721 721 read_attribute(:sort_criteria) || []
722 722 end
723 723
724 724 def sort_criteria_key(arg)
725 725 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
726 726 end
727 727
728 728 def sort_criteria_order(arg)
729 729 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
730 730 end
731 731
732 732 def sort_criteria_order_for(key)
733 733 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
734 734 end
735 735
736 736 # Returns the SQL sort order that should be prepended for grouping
737 737 def group_by_sort_order
738 738 if column = group_by_column
739 739 order = (sort_criteria_order_for(column.name) || column.default_order || 'asc').try(:upcase)
740 740 Array(column.sortable).map {|s| "#{s} #{order}"}
741 741 end
742 742 end
743 743
744 744 # Returns true if the query is a grouped query
745 745 def grouped?
746 746 !group_by_column.nil?
747 747 end
748 748
749 749 def group_by_column
750 750 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
751 751 end
752 752
753 753 def group_by_statement
754 754 group_by_column.try(:groupable)
755 755 end
756 756
757 757 def project_statement
758 758 project_clauses = []
759 759 if project && !project.descendants.active.empty?
760 760 if has_filter?("subproject_id")
761 761 case operator_for("subproject_id")
762 762 when '='
763 763 # include the selected subprojects
764 764 ids = [project.id] + values_for("subproject_id").each(&:to_i)
765 765 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
766 766 when '!*'
767 767 # main project only
768 768 project_clauses << "#{Project.table_name}.id = %d" % project.id
769 769 else
770 770 # all subprojects
771 771 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
772 772 end
773 773 elsif Setting.display_subprojects_issues?
774 774 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
775 775 else
776 776 project_clauses << "#{Project.table_name}.id = %d" % project.id
777 777 end
778 778 elsif project
779 779 project_clauses << "#{Project.table_name}.id = %d" % project.id
780 780 end
781 781 project_clauses.any? ? project_clauses.join(' AND ') : nil
782 782 end
783 783
784 784 def statement
785 785 # filters clauses
786 786 filters_clauses = []
787 787 filters.each_key do |field|
788 788 next if field == "subproject_id"
789 789 v = values_for(field).clone
790 790 next unless v and !v.empty?
791 791 operator = operator_for(field)
792 792
793 793 # "me" value substitution
794 794 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
795 795 if v.delete("me")
796 796 if User.current.logged?
797 797 v.push(User.current.id.to_s)
798 798 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
799 799 else
800 800 v.push("0")
801 801 end
802 802 end
803 803 end
804 804
805 805 if field == 'project_id'
806 806 if v.delete('mine')
807 807 v += User.current.memberships.map(&:project_id).map(&:to_s)
808 808 end
809 809 end
810 810
811 811 if field =~ /cf_(\d+)$/
812 812 # custom field
813 813 filters_clauses << sql_for_custom_field(field, operator, v, $1)
814 814 elsif respond_to?(method = "sql_for_#{field.gsub('.','_')}_field")
815 815 # specific statement
816 816 filters_clauses << send(method, field, operator, v)
817 817 else
818 818 # regular field
819 819 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
820 820 end
821 821 end if filters and valid?
822 822
823 823 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
824 824 # Excludes results for which the grouped custom field is not visible
825 825 filters_clauses << c.custom_field.visibility_by_project_condition
826 826 end
827 827
828 828 filters_clauses << project_statement
829 829 filters_clauses.reject!(&:blank?)
830 830
831 831 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
832 832 end
833 833
834 834 # Returns the sum of values for the given column
835 835 def total_for(column)
836 836 total_with_scope(column, base_scope)
837 837 end
838 838
839 839 # Returns a hash of the sum of the given column for each group,
840 840 # or nil if the query is not grouped
841 841 def total_by_group_for(column)
842 842 grouped_query do |scope|
843 843 total_with_scope(column, scope)
844 844 end
845 845 end
846 846
847 847 def totals
848 848 totals = totalable_columns.map {|column| [column, total_for(column)]}
849 849 yield totals if block_given?
850 850 totals
851 851 end
852 852
853 853 def totals_by_group
854 854 totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
855 855 yield totals if block_given?
856 856 totals
857 857 end
858 858
859 859 private
860 860
861 861 def grouped_query(&block)
862 862 r = nil
863 863 if grouped?
864 864 begin
865 865 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
866 866 r = yield base_group_scope
867 867 rescue ActiveRecord::RecordNotFound
868 868 r = {nil => yield(base_scope)}
869 869 end
870 870 c = group_by_column
871 871 if c.is_a?(QueryCustomFieldColumn)
872 872 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
873 873 end
874 874 end
875 875 r
876 876 rescue ::ActiveRecord::StatementInvalid => e
877 877 raise StatementInvalid.new(e.message)
878 878 end
879 879
880 880 def total_with_scope(column, scope)
881 881 unless column.is_a?(QueryColumn)
882 882 column = column.to_sym
883 883 column = available_totalable_columns.detect {|c| c.name == column}
884 884 end
885 885 if column.is_a?(QueryCustomFieldColumn)
886 886 custom_field = column.custom_field
887 887 send "total_for_custom_field", custom_field, scope
888 888 else
889 889 send "total_for_#{column.name}", scope
890 890 end
891 891 rescue ::ActiveRecord::StatementInvalid => e
892 892 raise StatementInvalid.new(e.message)
893 893 end
894 894
895 895 def base_scope
896 896 raise "unimplemented"
897 897 end
898 898
899 899 def base_group_scope
900 900 base_scope.
901 901 joins(joins_for_order_statement(group_by_statement)).
902 902 group(group_by_statement)
903 903 end
904 904
905 905 def total_for_custom_field(custom_field, scope, &block)
906 906 total = custom_field.format.total_for_scope(custom_field, scope)
907 907 total = map_total(total) {|t| custom_field.format.cast_total_value(custom_field, t)}
908 908 total
909 909 end
910 910
911 911 def map_total(total, &block)
912 912 if total.is_a?(Hash)
913 913 total.keys.each {|k| total[k] = yield total[k]}
914 914 else
915 915 total = yield total
916 916 end
917 917 total
918 918 end
919 919
920 920 def sql_for_custom_field(field, operator, value, custom_field_id)
921 921 db_table = CustomValue.table_name
922 922 db_field = 'value'
923 923 filter = @available_filters[field]
924 924 return nil unless filter
925 925 if filter[:field].format.target_class && filter[:field].format.target_class <= User
926 926 if value.delete('me')
927 927 value.push User.current.id.to_s
928 928 end
929 929 end
930 930 not_in = nil
931 931 if operator == '!'
932 932 # Makes ! operator work for custom fields with multiple values
933 933 operator = '='
934 934 not_in = 'NOT'
935 935 end
936 936 customized_key = "id"
937 937 customized_class = queried_class
938 938 if field =~ /^(.+)\.cf_/
939 939 assoc = $1
940 940 customized_key = "#{assoc}_id"
941 941 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
942 942 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
943 943 end
944 944 where = sql_for_field(field, operator, value, db_table, db_field, true)
945 945 if operator =~ /[<>]/
946 946 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
947 947 end
948 948 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
949 949 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
950 950 " 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}" +
951 951 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
952 952 end
953 953
954 954 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
955 955 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
956 956 sql = ''
957 957 case operator
958 958 when "="
959 959 if value.any?
960 960 case type_for(field)
961 961 when :date, :date_past
962 962 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
963 963 when :integer
964 964 int_values = value.first.to_s.scan(/[+-]?\d+/).map(&:to_i).join(",")
965 965 if int_values.present?
966 966 if is_custom_filter
967 967 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)) IN (#{int_values}))"
968 968 else
969 969 sql = "#{db_table}.#{db_field} IN (#{int_values})"
970 970 end
971 971 else
972 972 sql = "1=0"
973 973 end
974 974 when :float
975 975 if is_custom_filter
976 976 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})"
977 977 else
978 978 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
979 979 end
980 980 else
981 981 sql = queried_class.send(:sanitize_sql_for_conditions, ["#{db_table}.#{db_field} IN (?)", value])
982 982 end
983 983 else
984 984 # IN an empty set
985 985 sql = "1=0"
986 986 end
987 987 when "!"
988 988 if value.any?
989 989 sql = queried_class.send(:sanitize_sql_for_conditions, ["(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (?))", value])
990 990 else
991 991 # NOT IN an empty set
992 992 sql = "1=1"
993 993 end
994 994 when "!*"
995 995 sql = "#{db_table}.#{db_field} IS NULL"
996 996 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
997 997 when "*"
998 998 sql = "#{db_table}.#{db_field} IS NOT NULL"
999 999 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
1000 1000 when ">="
1001 1001 if [:date, :date_past].include?(type_for(field))
1002 1002 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
1003 1003 else
1004 1004 if is_custom_filter
1005 1005 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})"
1006 1006 else
1007 1007 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
1008 1008 end
1009 1009 end
1010 1010 when "<="
1011 1011 if [:date, :date_past].include?(type_for(field))
1012 1012 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
1013 1013 else
1014 1014 if is_custom_filter
1015 1015 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})"
1016 1016 else
1017 1017 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
1018 1018 end
1019 1019 end
1020 1020 when "><"
1021 1021 if [:date, :date_past].include?(type_for(field))
1022 1022 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
1023 1023 else
1024 1024 if is_custom_filter
1025 1025 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})"
1026 1026 else
1027 1027 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
1028 1028 end
1029 1029 end
1030 1030 when "o"
1031 1031 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"
1032 1032 when "c"
1033 1033 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"
1034 1034 when "><t-"
1035 1035 # between today - n days and today
1036 1036 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
1037 1037 when ">t-"
1038 1038 # >= today - n days
1039 1039 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
1040 1040 when "<t-"
1041 1041 # <= today - n days
1042 1042 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
1043 1043 when "t-"
1044 1044 # = n days in past
1045 1045 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
1046 1046 when "><t+"
1047 1047 # between today and today + n days
1048 1048 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
1049 1049 when ">t+"
1050 1050 # >= today + n days
1051 1051 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
1052 1052 when "<t+"
1053 1053 # <= today + n days
1054 1054 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
1055 1055 when "t+"
1056 1056 # = today + n days
1057 1057 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
1058 1058 when "t"
1059 1059 # = today
1060 1060 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
1061 1061 when "ld"
1062 1062 # = yesterday
1063 1063 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
1064 1064 when "w"
1065 1065 # = this week
1066 1066 first_day_of_week = l(:general_first_day_of_week).to_i
1067 1067 day_of_week = User.current.today.cwday
1068 1068 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
1069 1069 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
1070 1070 when "lw"
1071 1071 # = last week
1072 1072 first_day_of_week = l(:general_first_day_of_week).to_i
1073 1073 day_of_week = User.current.today.cwday
1074 1074 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
1075 1075 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
1076 1076 when "l2w"
1077 1077 # = last 2 weeks
1078 1078 first_day_of_week = l(:general_first_day_of_week).to_i
1079 1079 day_of_week = User.current.today.cwday
1080 1080 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
1081 1081 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
1082 1082 when "m"
1083 1083 # = this month
1084 1084 date = User.current.today
1085 1085 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
1086 1086 when "lm"
1087 1087 # = last month
1088 1088 date = User.current.today.prev_month
1089 1089 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
1090 1090 when "y"
1091 1091 # = this year
1092 1092 date = User.current.today
1093 1093 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
1094 1094 when "~"
1095 1095 sql = sql_contains("#{db_table}.#{db_field}", value.first)
1096 1096 when "!~"
1097 1097 sql = sql_contains("#{db_table}.#{db_field}", value.first, false)
1098 1098 else
1099 1099 raise "Unknown query operator #{operator}"
1100 1100 end
1101 1101
1102 1102 return sql
1103 1103 end
1104 1104
1105 1105 # Returns a SQL LIKE statement with wildcards
1106 1106 def sql_contains(db_field, value, match=true)
1107 1107 queried_class.send :sanitize_sql_for_conditions,
1108 1108 [Redmine::Database.like(db_field, '?', :match => match), "%#{value}%"]
1109 1109 end
1110 1110
1111 1111 # Adds a filter for the given custom field
1112 1112 def add_custom_field_filter(field, assoc=nil)
1113 1113 options = field.query_filter_options(self)
1114 if field.format.target_class && field.format.target_class <= User
1115 if options[:values].is_a?(Array) && User.current.logged?
1116 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
1117 end
1118 end
1119 1114
1120 1115 filter_id = "cf_#{field.id}"
1121 1116 filter_name = field.name
1122 1117 if assoc.present?
1123 1118 filter_id = "#{assoc}.#{filter_id}"
1124 1119 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1125 1120 end
1126 1121 add_available_filter filter_id, options.merge({
1127 1122 :name => filter_name,
1128 1123 :field => field
1129 1124 })
1130 1125 end
1131 1126
1132 1127 # Adds filters for the given custom fields scope
1133 1128 def add_custom_fields_filters(scope, assoc=nil)
1134 1129 scope.visible.where(:is_filter => true).sorted.each do |field|
1135 1130 add_custom_field_filter(field, assoc)
1136 1131 end
1137 1132 end
1138 1133
1139 1134 # Adds filters for the given associations custom fields
1140 1135 def add_associations_custom_fields_filters(*associations)
1141 1136 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
1142 1137 associations.each do |assoc|
1143 1138 association_klass = queried_class.reflect_on_association(assoc).klass
1144 1139 fields_by_class.each do |field_class, fields|
1145 1140 if field_class.customized_class <= association_klass
1146 1141 fields.sort.each do |field|
1147 1142 add_custom_field_filter(field, assoc)
1148 1143 end
1149 1144 end
1150 1145 end
1151 1146 end
1152 1147 end
1153 1148
1154 1149 def quoted_time(time, is_custom_filter)
1155 1150 if is_custom_filter
1156 1151 # Custom field values are stored as strings in the DB
1157 1152 # using this format that does not depend on DB date representation
1158 1153 time.strftime("%Y-%m-%d %H:%M:%S")
1159 1154 else
1160 1155 self.class.connection.quoted_date(time)
1161 1156 end
1162 1157 end
1163 1158
1164 1159 def date_for_user_time_zone(y, m, d)
1165 1160 if tz = User.current.time_zone
1166 1161 tz.local y, m, d
1167 1162 else
1168 1163 Time.local y, m, d
1169 1164 end
1170 1165 end
1171 1166
1172 1167 # Returns a SQL clause for a date or datetime field.
1173 1168 def date_clause(table, field, from, to, is_custom_filter)
1174 1169 s = []
1175 1170 if from
1176 1171 if from.is_a?(Date)
1177 1172 from = date_for_user_time_zone(from.year, from.month, from.day).yesterday.end_of_day
1178 1173 else
1179 1174 from = from - 1 # second
1180 1175 end
1181 1176 if self.class.default_timezone == :utc
1182 1177 from = from.utc
1183 1178 end
1184 1179 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
1185 1180 end
1186 1181 if to
1187 1182 if to.is_a?(Date)
1188 1183 to = date_for_user_time_zone(to.year, to.month, to.day).end_of_day
1189 1184 end
1190 1185 if self.class.default_timezone == :utc
1191 1186 to = to.utc
1192 1187 end
1193 1188 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
1194 1189 end
1195 1190 s.join(' AND ')
1196 1191 end
1197 1192
1198 1193 # Returns a SQL clause for a date or datetime field using relative dates.
1199 1194 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
1200 1195 date_clause(table, field, (days_from ? User.current.today + days_from : nil), (days_to ? User.current.today + days_to : nil), is_custom_filter)
1201 1196 end
1202 1197
1203 1198 # Returns a Date or Time from the given filter value
1204 1199 def parse_date(arg)
1205 1200 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
1206 1201 Time.parse(arg) rescue nil
1207 1202 else
1208 1203 Date.parse(arg) rescue nil
1209 1204 end
1210 1205 end
1211 1206
1212 1207 # Additional joins required for the given sort options
1213 1208 def joins_for_order_statement(order_options)
1214 1209 joins = []
1215 1210
1216 1211 if order_options
1217 1212 if order_options.include?('authors')
1218 1213 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
1219 1214 end
1220 1215 order_options.scan(/cf_\d+/).uniq.each do |name|
1221 1216 column = available_columns.detect {|c| c.name.to_s == name}
1222 1217 join = column && column.custom_field.join_for_order_statement
1223 1218 if join
1224 1219 joins << join
1225 1220 end
1226 1221 end
1227 1222 end
1228 1223
1229 1224 joins.any? ? joins.join(' ') : nil
1230 1225 end
1231 1226 end
@@ -1,979 +1,983
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'uri'
19 19
20 20 module Redmine
21 21 module FieldFormat
22 22 def self.add(name, klass)
23 23 all[name.to_s] = klass.instance
24 24 end
25 25
26 26 def self.delete(name)
27 27 all.delete(name.to_s)
28 28 end
29 29
30 30 def self.all
31 31 @formats ||= Hash.new(Base.instance)
32 32 end
33 33
34 34 def self.available_formats
35 35 all.keys
36 36 end
37 37
38 38 def self.find(name)
39 39 all[name.to_s]
40 40 end
41 41
42 42 # Return an array of custom field formats which can be used in select_tag
43 43 def self.as_select(class_name=nil)
44 44 formats = all.values.select do |format|
45 45 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
46 46 end
47 47 formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
48 48 end
49 49
50 50 # Returns an array of formats that can be used for a custom field class
51 51 def self.formats_for_custom_field_class(klass=nil)
52 52 all.values.select do |format|
53 53 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(klass.name)
54 54 end
55 55 end
56 56
57 57 class Base
58 58 include Singleton
59 59 include Redmine::I18n
60 60 include Redmine::Helpers::URL
61 61 include ERB::Util
62 62
63 63 class_attribute :format_name
64 64 self.format_name = nil
65 65
66 66 # Set this to true if the format supports multiple values
67 67 class_attribute :multiple_supported
68 68 self.multiple_supported = false
69 69
70 70 # Set this to true if the format supports filtering on custom values
71 71 class_attribute :is_filter_supported
72 72 self.is_filter_supported = true
73 73
74 74 # Set this to true if the format supports textual search on custom values
75 75 class_attribute :searchable_supported
76 76 self.searchable_supported = false
77 77
78 78 # Set this to true if field values can be summed up
79 79 class_attribute :totalable_supported
80 80 self.totalable_supported = false
81 81
82 82 # Set this to false if field cannot be bulk edited
83 83 class_attribute :bulk_edit_supported
84 84 self.bulk_edit_supported = true
85 85
86 86 # Restricts the classes that the custom field can be added to
87 87 # Set to nil for no restrictions
88 88 class_attribute :customized_class_names
89 89 self.customized_class_names = nil
90 90
91 91 # Name of the partial for editing the custom field
92 92 class_attribute :form_partial
93 93 self.form_partial = nil
94 94
95 95 class_attribute :change_as_diff
96 96 self.change_as_diff = false
97 97
98 98 class_attribute :change_no_details
99 99 self.change_no_details = false
100 100
101 101 def self.add(name)
102 102 self.format_name = name
103 103 Redmine::FieldFormat.add(name, self)
104 104 end
105 105 private_class_method :add
106 106
107 107 def self.field_attributes(*args)
108 108 CustomField.store_accessor :format_store, *args
109 109 end
110 110
111 111 field_attributes :url_pattern
112 112
113 113 def name
114 114 self.class.format_name
115 115 end
116 116
117 117 def label
118 118 "label_#{name}"
119 119 end
120 120
121 121 def set_custom_field_value(custom_field, custom_field_value, value)
122 122 if value.is_a?(Array)
123 123 value = value.map(&:to_s).reject{|v| v==''}.uniq
124 124 if value.empty?
125 125 value << ''
126 126 end
127 127 else
128 128 value = value.to_s
129 129 end
130 130
131 131 value
132 132 end
133 133
134 134 def cast_custom_value(custom_value)
135 135 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
136 136 end
137 137
138 138 def cast_value(custom_field, value, customized=nil)
139 139 if value.blank?
140 140 nil
141 141 elsif value.is_a?(Array)
142 142 casted = value.map do |v|
143 143 cast_single_value(custom_field, v, customized)
144 144 end
145 145 casted.compact.sort
146 146 else
147 147 cast_single_value(custom_field, value, customized)
148 148 end
149 149 end
150 150
151 151 def cast_single_value(custom_field, value, customized=nil)
152 152 value.to_s
153 153 end
154 154
155 155 def target_class
156 156 nil
157 157 end
158 158
159 159 def possible_custom_value_options(custom_value)
160 160 possible_values_options(custom_value.custom_field, custom_value.customized)
161 161 end
162 162
163 163 def possible_values_options(custom_field, object=nil)
164 164 []
165 165 end
166 166
167 167 def value_from_keyword(custom_field, keyword, object)
168 168 possible_values_options = possible_values_options(custom_field, object)
169 169 if possible_values_options.present?
170 170 keyword = keyword.to_s
171 171 if v = possible_values_options.detect {|text, id| keyword.casecmp(text) == 0}
172 172 if v.is_a?(Array)
173 173 v.last
174 174 else
175 175 v
176 176 end
177 177 end
178 178 else
179 179 keyword
180 180 end
181 181 end
182 182
183 183 # Returns the validation errors for custom_field
184 184 # Should return an empty array if custom_field is valid
185 185 def validate_custom_field(custom_field)
186 186 errors = []
187 187 pattern = custom_field.url_pattern
188 188 if pattern.present? && !uri_with_safe_scheme?(url_pattern_without_tokens(pattern))
189 189 errors << [:url_pattern, :invalid]
190 190 end
191 191 errors
192 192 end
193 193
194 194 # Returns the validation error messages for custom_value
195 195 # Should return an empty array if custom_value is valid
196 196 # custom_value is a CustomFieldValue.
197 197 def validate_custom_value(custom_value)
198 198 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
199 199 errors = values.map do |value|
200 200 validate_single_value(custom_value.custom_field, value, custom_value.customized)
201 201 end
202 202 errors.flatten.uniq
203 203 end
204 204
205 205 def validate_single_value(custom_field, value, customized=nil)
206 206 []
207 207 end
208 208
209 209 # CustomValue after_save callback
210 210 def after_save_custom_value(custom_field, custom_value)
211 211 end
212 212
213 213 def formatted_custom_value(view, custom_value, html=false)
214 214 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
215 215 end
216 216
217 217 def formatted_value(view, custom_field, value, customized=nil, html=false)
218 218 casted = cast_value(custom_field, value, customized)
219 219 if html && custom_field.url_pattern.present?
220 220 texts_and_urls = Array.wrap(casted).map do |single_value|
221 221 text = view.format_object(single_value, false).to_s
222 222 url = url_from_pattern(custom_field, single_value, customized)
223 223 [text, url]
224 224 end
225 225 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to_if uri_with_safe_scheme?(url), text, url}
226 226 links.join(', ').html_safe
227 227 else
228 228 casted
229 229 end
230 230 end
231 231
232 232 # Returns an URL generated with the custom field URL pattern
233 233 # and variables substitution:
234 234 # %value% => the custom field value
235 235 # %id% => id of the customized object
236 236 # %project_id% => id of the project of the customized object if defined
237 237 # %project_identifier% => identifier of the project of the customized object if defined
238 238 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
239 239 def url_from_pattern(custom_field, value, customized)
240 240 url = custom_field.url_pattern.to_s.dup
241 241 url.gsub!('%value%') {URI.encode value.to_s}
242 242 url.gsub!('%id%') {URI.encode customized.id.to_s}
243 243 url.gsub!('%project_id%') {URI.encode (customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
244 244 url.gsub!('%project_identifier%') {URI.encode (customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
245 245 if custom_field.regexp.present?
246 246 url.gsub!(%r{%m(\d+)%}) do
247 247 m = $1.to_i
248 248 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
249 249 URI.encode matches[m].to_s
250 250 end
251 251 end
252 252 end
253 253 url
254 254 end
255 255 protected :url_from_pattern
256 256
257 257 # Returns the URL pattern with substitution tokens removed,
258 258 # for validation purpose
259 259 def url_pattern_without_tokens(url_pattern)
260 260 url_pattern.to_s.gsub(/%(value|id|project_id|project_identifier|m\d+)%/, '')
261 261 end
262 262 protected :url_pattern_without_tokens
263 263
264 264 def edit_tag(view, tag_id, tag_name, custom_value, options={})
265 265 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
266 266 end
267 267
268 268 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
269 269 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
270 270 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
271 271 end
272 272
273 273 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
274 274 if custom_field.is_required?
275 275 ''.html_safe
276 276 else
277 277 view.content_tag('label',
278 278 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
279 279 :class => 'inline'
280 280 )
281 281 end
282 282 end
283 283 protected :bulk_clear_tag
284 284
285 285 def query_filter_options(custom_field, query)
286 286 {:type => :string}
287 287 end
288 288
289 289 def before_custom_field_save(custom_field)
290 290 end
291 291
292 292 # Returns a ORDER BY clause that can used to sort customized
293 293 # objects by their value of the custom field.
294 294 # Returns nil if the custom field can not be used for sorting.
295 295 def order_statement(custom_field)
296 296 # COALESCE is here to make sure that blank and NULL values are sorted equally
297 297 "COALESCE(#{join_alias custom_field}.value, '')"
298 298 end
299 299
300 300 # Returns a GROUP BY clause that can used to group by custom value
301 301 # Returns nil if the custom field can not be used for grouping.
302 302 def group_statement(custom_field)
303 303 nil
304 304 end
305 305
306 306 # Returns a JOIN clause that is added to the query when sorting by custom values
307 307 def join_for_order_statement(custom_field)
308 308 alias_name = join_alias(custom_field)
309 309
310 310 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
311 311 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
312 312 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
313 313 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
314 314 " AND (#{custom_field.visibility_by_project_condition})" +
315 315 " AND #{alias_name}.value <> ''" +
316 316 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
317 317 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
318 318 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
319 319 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
320 320 end
321 321
322 322 def join_alias(custom_field)
323 323 "cf_#{custom_field.id}"
324 324 end
325 325 protected :join_alias
326 326 end
327 327
328 328 class Unbounded < Base
329 329 def validate_single_value(custom_field, value, customized=nil)
330 330 errs = super
331 331 value = value.to_s
332 332 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
333 333 errs << ::I18n.t('activerecord.errors.messages.invalid')
334 334 end
335 335 if custom_field.min_length && value.length < custom_field.min_length
336 336 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
337 337 end
338 338 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
339 339 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
340 340 end
341 341 errs
342 342 end
343 343 end
344 344
345 345 class StringFormat < Unbounded
346 346 add 'string'
347 347 self.searchable_supported = true
348 348 self.form_partial = 'custom_fields/formats/string'
349 349 field_attributes :text_formatting
350 350
351 351 def formatted_value(view, custom_field, value, customized=nil, html=false)
352 352 if html
353 353 if custom_field.url_pattern.present?
354 354 super
355 355 elsif custom_field.text_formatting == 'full'
356 356 view.textilizable(value, :object => customized)
357 357 else
358 358 value.to_s
359 359 end
360 360 else
361 361 value.to_s
362 362 end
363 363 end
364 364 end
365 365
366 366 class TextFormat < Unbounded
367 367 add 'text'
368 368 self.searchable_supported = true
369 369 self.form_partial = 'custom_fields/formats/text'
370 370 self.change_as_diff = true
371 371
372 372 def formatted_value(view, custom_field, value, customized=nil, html=false)
373 373 if html
374 374 if value.present?
375 375 if custom_field.text_formatting == 'full'
376 376 view.textilizable(value, :object => customized)
377 377 else
378 378 view.simple_format(html_escape(value))
379 379 end
380 380 else
381 381 ''
382 382 end
383 383 else
384 384 value.to_s
385 385 end
386 386 end
387 387
388 388 def edit_tag(view, tag_id, tag_name, custom_value, options={})
389 389 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
390 390 end
391 391
392 392 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
393 393 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
394 394 '<br />'.html_safe +
395 395 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
396 396 end
397 397
398 398 def query_filter_options(custom_field, query)
399 399 {:type => :text}
400 400 end
401 401 end
402 402
403 403 class LinkFormat < StringFormat
404 404 add 'link'
405 405 self.searchable_supported = false
406 406 self.form_partial = 'custom_fields/formats/link'
407 407
408 408 def formatted_value(view, custom_field, value, customized=nil, html=false)
409 409 if html && value.present?
410 410 if custom_field.url_pattern.present?
411 411 url = url_from_pattern(custom_field, value, customized)
412 412 else
413 413 url = value.to_s
414 414 unless url =~ %r{\A[a-z]+://}i
415 415 # no protocol found, use http by default
416 416 url = "http://" + url
417 417 end
418 418 end
419 419 view.link_to value.to_s.truncate(40), url
420 420 else
421 421 value.to_s
422 422 end
423 423 end
424 424 end
425 425
426 426 class Numeric < Unbounded
427 427 self.form_partial = 'custom_fields/formats/numeric'
428 428 self.totalable_supported = true
429 429
430 430 def order_statement(custom_field)
431 431 # Make the database cast values into numeric
432 432 # Postgresql will raise an error if a value can not be casted!
433 433 # CustomValue validations should ensure that it doesn't occur
434 434 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
435 435 end
436 436
437 437 # Returns totals for the given scope
438 438 def total_for_scope(custom_field, scope)
439 439 scope.joins(:custom_values).
440 440 where(:custom_values => {:custom_field_id => custom_field.id}).
441 441 where.not(:custom_values => {:value => ''}).
442 442 sum("CAST(#{CustomValue.table_name}.value AS decimal(30,3))")
443 443 end
444 444
445 445 def cast_total_value(custom_field, value)
446 446 cast_single_value(custom_field, value)
447 447 end
448 448 end
449 449
450 450 class IntFormat < Numeric
451 451 add 'int'
452 452
453 453 def label
454 454 "label_integer"
455 455 end
456 456
457 457 def cast_single_value(custom_field, value, customized=nil)
458 458 value.to_i
459 459 end
460 460
461 461 def validate_single_value(custom_field, value, customized=nil)
462 462 errs = super
463 463 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s =~ /^[+-]?\d+$/
464 464 errs
465 465 end
466 466
467 467 def query_filter_options(custom_field, query)
468 468 {:type => :integer}
469 469 end
470 470
471 471 def group_statement(custom_field)
472 472 order_statement(custom_field)
473 473 end
474 474 end
475 475
476 476 class FloatFormat < Numeric
477 477 add 'float'
478 478
479 479 def cast_single_value(custom_field, value, customized=nil)
480 480 value.to_f
481 481 end
482 482
483 483 def cast_total_value(custom_field, value)
484 484 value.to_f.round(2)
485 485 end
486 486
487 487 def validate_single_value(custom_field, value, customized=nil)
488 488 errs = super
489 489 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
490 490 errs
491 491 end
492 492
493 493 def query_filter_options(custom_field, query)
494 494 {:type => :float}
495 495 end
496 496 end
497 497
498 498 class DateFormat < Unbounded
499 499 add 'date'
500 500 self.form_partial = 'custom_fields/formats/date'
501 501
502 502 def cast_single_value(custom_field, value, customized=nil)
503 503 value.to_date rescue nil
504 504 end
505 505
506 506 def validate_single_value(custom_field, value, customized=nil)
507 507 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
508 508 []
509 509 else
510 510 [::I18n.t('activerecord.errors.messages.not_a_date')]
511 511 end
512 512 end
513 513
514 514 def edit_tag(view, tag_id, tag_name, custom_value, options={})
515 515 view.date_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
516 516 view.calendar_for(tag_id)
517 517 end
518 518
519 519 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
520 520 view.date_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
521 521 view.calendar_for(tag_id) +
522 522 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
523 523 end
524 524
525 525 def query_filter_options(custom_field, query)
526 526 {:type => :date}
527 527 end
528 528
529 529 def group_statement(custom_field)
530 530 order_statement(custom_field)
531 531 end
532 532 end
533 533
534 534 class List < Base
535 535 self.multiple_supported = true
536 536 field_attributes :edit_tag_style
537 537
538 538 def edit_tag(view, tag_id, tag_name, custom_value, options={})
539 539 if custom_value.custom_field.edit_tag_style == 'check_box'
540 540 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
541 541 else
542 542 select_edit_tag(view, tag_id, tag_name, custom_value, options)
543 543 end
544 544 end
545 545
546 546 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
547 547 opts = []
548 548 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
549 549 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
550 550 opts += possible_values_options(custom_field, objects)
551 551 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
552 552 end
553 553
554 554 def query_filter_options(custom_field, query)
555 {:type => :list_optional, :values => query_filter_values(custom_field, query)}
555 {:type => :list_optional, :values => lambda { query_filter_values(custom_field, query) }}
556 556 end
557 557
558 558 protected
559 559
560 560 # Returns the values that are available in the field filter
561 561 def query_filter_values(custom_field, query)
562 562 possible_values_options(custom_field, query.project)
563 563 end
564 564
565 565 # Renders the edit tag as a select tag
566 566 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
567 567 blank_option = ''.html_safe
568 568 unless custom_value.custom_field.multiple?
569 569 if custom_value.custom_field.is_required?
570 570 unless custom_value.custom_field.default_value.present?
571 571 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
572 572 end
573 573 else
574 574 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
575 575 end
576 576 end
577 577 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
578 578 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
579 579 if custom_value.custom_field.multiple?
580 580 s << view.hidden_field_tag(tag_name, '')
581 581 end
582 582 s
583 583 end
584 584
585 585 # Renders the edit tag as check box or radio tags
586 586 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
587 587 opts = []
588 588 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
589 589 opts << ["(#{l(:label_none)})", '']
590 590 end
591 591 opts += possible_custom_value_options(custom_value)
592 592 s = ''.html_safe
593 593 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
594 594 opts.each do |label, value|
595 595 value ||= label
596 596 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
597 597 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
598 598 # set the id on the first tag only
599 599 tag_id = nil
600 600 s << view.content_tag('label', tag + ' ' + label)
601 601 end
602 602 if custom_value.custom_field.multiple?
603 603 s << view.hidden_field_tag(tag_name, '')
604 604 end
605 605 css = "#{options[:class]} check_box_group"
606 606 view.content_tag('span', s, options.merge(:class => css))
607 607 end
608 608 end
609 609
610 610 class ListFormat < List
611 611 add 'list'
612 612 self.searchable_supported = true
613 613 self.form_partial = 'custom_fields/formats/list'
614 614
615 615 def possible_custom_value_options(custom_value)
616 616 options = possible_values_options(custom_value.custom_field)
617 617 missing = [custom_value.value].flatten.reject(&:blank?) - options
618 618 if missing.any?
619 619 options += missing
620 620 end
621 621 options
622 622 end
623 623
624 624 def possible_values_options(custom_field, object=nil)
625 625 custom_field.possible_values
626 626 end
627 627
628 628 def validate_custom_field(custom_field)
629 629 errors = []
630 630 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
631 631 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
632 632 errors
633 633 end
634 634
635 635 def validate_custom_value(custom_value)
636 636 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
637 637 invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
638 638 if invalid_values.any?
639 639 [::I18n.t('activerecord.errors.messages.inclusion')]
640 640 else
641 641 []
642 642 end
643 643 end
644 644
645 645 def group_statement(custom_field)
646 646 order_statement(custom_field)
647 647 end
648 648 end
649 649
650 650 class BoolFormat < List
651 651 add 'bool'
652 652 self.multiple_supported = false
653 653 self.form_partial = 'custom_fields/formats/bool'
654 654
655 655 def label
656 656 "label_boolean"
657 657 end
658 658
659 659 def cast_single_value(custom_field, value, customized=nil)
660 660 value == '1' ? true : false
661 661 end
662 662
663 663 def possible_values_options(custom_field, object=nil)
664 664 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
665 665 end
666 666
667 667 def group_statement(custom_field)
668 668 order_statement(custom_field)
669 669 end
670 670
671 671 def edit_tag(view, tag_id, tag_name, custom_value, options={})
672 672 case custom_value.custom_field.edit_tag_style
673 673 when 'check_box'
674 674 single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
675 675 when 'radio'
676 676 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
677 677 else
678 678 select_edit_tag(view, tag_id, tag_name, custom_value, options)
679 679 end
680 680 end
681 681
682 682 # Renders the edit tag as a simple check box
683 683 def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
684 684 s = ''.html_safe
685 685 s << view.hidden_field_tag(tag_name, '0', :id => nil)
686 686 s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', :id => tag_id)
687 687 view.content_tag('span', s, options)
688 688 end
689 689 end
690 690
691 691 class RecordList < List
692 692 self.customized_class_names = %w(Issue TimeEntry Version Document Project)
693 693
694 694 def cast_single_value(custom_field, value, customized=nil)
695 695 target_class.find_by_id(value.to_i) if value.present?
696 696 end
697 697
698 698 def target_class
699 699 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
700 700 end
701 701
702 702 def reset_target_class
703 703 @target_class = nil
704 704 end
705 705
706 706 def possible_custom_value_options(custom_value)
707 707 options = possible_values_options(custom_value.custom_field, custom_value.customized)
708 708 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
709 709 if missing.any?
710 710 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
711 711 end
712 712 options
713 713 end
714 714
715 715 def order_statement(custom_field)
716 716 if target_class.respond_to?(:fields_for_order_statement)
717 717 target_class.fields_for_order_statement(value_join_alias(custom_field))
718 718 end
719 719 end
720 720
721 721 def group_statement(custom_field)
722 722 "COALESCE(#{join_alias custom_field}.value, '')"
723 723 end
724 724
725 725 def join_for_order_statement(custom_field)
726 726 alias_name = join_alias(custom_field)
727 727
728 728 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
729 729 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
730 730 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
731 731 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
732 732 " AND (#{custom_field.visibility_by_project_condition})" +
733 733 " AND #{alias_name}.value <> ''" +
734 734 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
735 735 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
736 736 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
737 737 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
738 738 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
739 739 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
740 740 end
741 741
742 742 def value_join_alias(custom_field)
743 743 join_alias(custom_field) + "_" + custom_field.field_format
744 744 end
745 745 protected :value_join_alias
746 746 end
747 747
748 748 class EnumerationFormat < RecordList
749 749 add 'enumeration'
750 750 self.form_partial = 'custom_fields/formats/enumeration'
751 751
752 752 def label
753 753 "label_field_format_enumeration"
754 754 end
755 755
756 756 def target_class
757 757 @target_class ||= CustomFieldEnumeration
758 758 end
759 759
760 760 def possible_values_options(custom_field, object=nil)
761 761 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
762 762 end
763 763
764 764 def possible_values_records(custom_field, object=nil)
765 765 custom_field.enumerations.active
766 766 end
767 767
768 768 def value_from_keyword(custom_field, keyword, object)
769 769 value = custom_field.enumerations.where("LOWER(name) LIKE LOWER(?)", keyword).first
770 770 value ? value.id : nil
771 771 end
772 772 end
773 773
774 774 class UserFormat < RecordList
775 775 add 'user'
776 776 self.form_partial = 'custom_fields/formats/user'
777 777 field_attributes :user_role
778 778
779 779 def possible_values_options(custom_field, object=nil)
780 780 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
781 781 end
782 782
783 783 def possible_values_records(custom_field, object=nil)
784 784 if object.is_a?(Array)
785 785 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
786 786 projects.map {|project| possible_values_records(custom_field, project)}.reduce(:&) || []
787 787 elsif object.respond_to?(:project) && object.project
788 788 scope = object.project.users
789 789 if custom_field.user_role.is_a?(Array)
790 790 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
791 791 if role_ids.any?
792 792 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
793 793 end
794 794 end
795 795 scope.sorted
796 796 else
797 797 []
798 798 end
799 799 end
800 800
801 801 def value_from_keyword(custom_field, keyword, object)
802 802 users = possible_values_records(custom_field, object).to_a
803 803 user = Principal.detect_by_keyword(users, keyword)
804 804 user ? user.id : nil
805 805 end
806 806
807 807 def before_custom_field_save(custom_field)
808 808 super
809 809 if custom_field.user_role.is_a?(Array)
810 810 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
811 811 end
812 812 end
813
814 def query_filter_values(*args)
815 [["<< #{l(:label_me)} >>", "me"]] + super
816 end
813 817 end
814 818
815 819 class VersionFormat < RecordList
816 820 add 'version'
817 821 self.form_partial = 'custom_fields/formats/version'
818 822 field_attributes :version_status
819 823
820 824 def possible_values_options(custom_field, object=nil)
821 825 versions_options(custom_field, object)
822 826 end
823 827
824 828 def before_custom_field_save(custom_field)
825 829 super
826 830 if custom_field.version_status.is_a?(Array)
827 831 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
828 832 end
829 833 end
830 834
831 835 protected
832 836
833 837 def query_filter_values(custom_field, query)
834 838 versions_options(custom_field, query.project, true)
835 839 end
836 840
837 841 def versions_options(custom_field, object, all_statuses=false)
838 842 if object.is_a?(Array)
839 843 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
840 844 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
841 845 elsif object.respond_to?(:project) && object.project
842 846 scope = object.project.shared_versions
843 847 filtered_versions_options(custom_field, scope, all_statuses)
844 848 elsif object.nil?
845 849 scope = ::Version.visible.where(:sharing => 'system')
846 850 filtered_versions_options(custom_field, scope, all_statuses)
847 851 else
848 852 []
849 853 end
850 854 end
851 855
852 856 def filtered_versions_options(custom_field, scope, all_statuses=false)
853 857 if !all_statuses && custom_field.version_status.is_a?(Array)
854 858 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
855 859 if statuses.any?
856 860 scope = scope.where(:status => statuses.map(&:to_s))
857 861 end
858 862 end
859 863 scope.sort.collect{|u| [u.to_s, u.id.to_s] }
860 864 end
861 865 end
862 866
863 867 class AttachmentFormat < Base
864 868 add 'attachment'
865 869 self.form_partial = 'custom_fields/formats/attachment'
866 870 self.is_filter_supported = false
867 871 self.change_no_details = true
868 872 self.bulk_edit_supported = false
869 873 field_attributes :extensions_allowed
870 874
871 875 def set_custom_field_value(custom_field, custom_field_value, value)
872 876 attachment_present = false
873 877
874 878 if value.is_a?(Hash)
875 879 attachment_present = true
876 880 value = value.except(:blank)
877 881
878 882 if value.values.any? && value.values.all? {|v| v.is_a?(Hash)}
879 883 value = value.values.first
880 884 end
881 885
882 886 if value.key?(:id)
883 887 value = set_custom_field_value_by_id(custom_field, custom_field_value, value[:id])
884 888 elsif value[:token].present?
885 889 if attachment = Attachment.find_by_token(value[:token])
886 890 value = attachment.id.to_s
887 891 else
888 892 value = ''
889 893 end
890 894 elsif value.key?(:file)
891 895 attachment = Attachment.new(:file => value[:file], :author => User.current)
892 896 if attachment.save
893 897 value = attachment.id.to_s
894 898 else
895 899 value = ''
896 900 end
897 901 else
898 902 attachment_present = false
899 903 value = ''
900 904 end
901 905 elsif value.is_a?(String)
902 906 value = set_custom_field_value_by_id(custom_field, custom_field_value, value)
903 907 end
904 908 custom_field_value.instance_variable_set "@attachment_present", attachment_present
905 909
906 910 value
907 911 end
908 912
909 913 def set_custom_field_value_by_id(custom_field, custom_field_value, id)
910 914 attachment = Attachment.find_by_id(id)
911 915 if attachment && attachment.container.is_a?(CustomValue) && attachment.container.customized == custom_field_value.customized
912 916 id.to_s
913 917 else
914 918 ''
915 919 end
916 920 end
917 921 private :set_custom_field_value_by_id
918 922
919 923 def cast_single_value(custom_field, value, customized=nil)
920 924 Attachment.find_by_id(value.to_i) if value.present? && value.respond_to?(:to_i)
921 925 end
922 926
923 927 def validate_custom_value(custom_value)
924 928 errors = []
925 929
926 930 if custom_value.value.blank?
927 931 if custom_value.instance_variable_get("@attachment_present")
928 932 errors << ::I18n.t('activerecord.errors.messages.invalid')
929 933 end
930 934 else
931 935 if custom_value.value.present?
932 936 attachment = Attachment.where(:id => custom_value.value.to_s).first
933 937 extensions = custom_value.custom_field.extensions_allowed
934 938 if attachment && extensions.present? && !attachment.extension_in?(extensions)
935 939 errors << "#{::I18n.t('activerecord.errors.messages.invalid')} (#{l(:setting_attachment_extensions_allowed)}: #{extensions})"
936 940 end
937 941 end
938 942 end
939 943
940 944 errors.uniq
941 945 end
942 946
943 947 def after_save_custom_value(custom_field, custom_value)
944 948 if custom_value.value_changed?
945 949 if custom_value.value.present?
946 950 attachment = Attachment.where(:id => custom_value.value.to_s).first
947 951 if attachment
948 952 attachment.container = custom_value
949 953 attachment.save!
950 954 end
951 955 end
952 956 if custom_value.value_was.present?
953 957 attachment = Attachment.where(:id => custom_value.value_was.to_s).first
954 958 if attachment
955 959 attachment.destroy
956 960 end
957 961 end
958 962 end
959 963 end
960 964
961 965 def edit_tag(view, tag_id, tag_name, custom_value, options={})
962 966 attachment = nil
963 967 if custom_value.value.present?
964 968 attachment = Attachment.find_by_id(custom_value.value)
965 969 end
966 970
967 971 view.hidden_field_tag("#{tag_name}[blank]", "") +
968 972 view.render(:partial => 'attachments/form',
969 973 :locals => {
970 974 :attachment_param => tag_name,
971 975 :multiple => false,
972 976 :description => false,
973 977 :saved_attachments => [attachment].compact,
974 978 :filedrop => false
975 979 })
976 980 end
977 981 end
978 982 end
979 983 end
@@ -1,88 +1,88
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../../../../test_helper', __FILE__)
19 19 require 'redmine/field_format'
20 20
21 21 class Redmine::VersionFieldFormatTest < ActionView::TestCase
22 22 fixtures :projects, :versions, :trackers,
23 23 :roles, :users, :members, :member_roles,
24 24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
25 25 :enumerations
26 26
27 27 def test_version_status_should_reject_blank_values
28 28 field = IssueCustomField.new(:name => 'Foo', :field_format => 'version', :version_status => ["open", ""])
29 29 field.save!
30 30 assert_equal ["open"], field.version_status
31 31 end
32 32
33 33 def test_existing_values_should_be_valid
34 34 field = IssueCustomField.create!(:name => 'Foo', :field_format => 'version', :is_for_all => true, :trackers => Tracker.all)
35 35 project = Project.generate!
36 36 version = Version.generate!(:project => project, :status => 'open')
37 37 issue = Issue.generate!(:project_id => project.id, :tracker_id => 1, :custom_field_values => {field.id => version.id})
38 38
39 39 field.version_status = ["open"]
40 40 field.save!
41 41
42 42 issue = Issue.order('id DESC').first
43 43 assert_include [version.name, version.id.to_s], field.possible_custom_value_options(issue.custom_value_for(field))
44 44 assert issue.valid?
45 45 end
46 46
47 47 def test_possible_values_options_should_return_project_versions
48 48 field = IssueCustomField.new(:field_format => 'version')
49 49 project = Project.find(1)
50 50 expected = project.shared_versions.sort.map(&:name)
51 51
52 52 assert_equal expected, field.possible_values_options(project).map(&:first)
53 53 end
54 54
55 55 def test_possible_values_options_should_return_system_shared_versions_without_project
56 56 field = IssueCustomField.new(:field_format => 'version')
57 57 version = Version.generate!(:project => Project.find(1), :status => 'open', :sharing => 'system')
58 58
59 59 expected = Version.visible.where(:sharing => 'system').sort.map(&:name)
60 60 assert_include version.name, expected
61 61 assert_equal expected, field.possible_values_options.map(&:first)
62 62 end
63 63
64 64 def test_possible_values_options_should_return_project_versions_with_selected_status
65 65 field = IssueCustomField.new(:field_format => 'version', :version_status => ["open"])
66 66 project = Project.find(1)
67 67 expected = project.shared_versions.sort.select {|v| v.status == "open"}.map(&:name)
68 68
69 69 assert_equal expected, field.possible_values_options(project).map(&:first)
70 70 end
71 71
72 72 def test_cast_value_should_not_raise_error_when_array_contains_value_casted_to_nil
73 73 field = IssueCustomField.new(:field_format => 'version')
74 74 assert_nothing_raised do
75 75 field.cast_value([1,2, 42])
76 76 end
77 77 end
78 78
79 79 def test_query_filter_options_should_include_versions_with_any_status
80 80 field = IssueCustomField.new(:field_format => 'version', :version_status => ["open"])
81 81 project = Project.find(1)
82 82 version = Version.generate!(:project => project, :status => 'locked')
83 83 query = Query.new(:project => project)
84 84
85 85 assert_not_include version.name, field.possible_values_options(project).map(&:first)
86 assert_include version.name, field.query_filter_options(query)[:values].map(&:first)
86 assert_include version.name, field.query_filter_options(query)[:values].call.map(&:first)
87 87 end
88 88 end
General Comments 0
You need to be logged in to leave comments. Login now