##// END OF EJS Templates
Always return an array....
Jean-Philippe Lang -
r15035:ada650ce7f4b
parent child
Show More
@@ -1,1041 +1,1039
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 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 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(/\A[+-]?\d+(,[+-]?\d+)*\z/) }
268 268 when :float
269 269 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/\A[+-]?\d+(\.\d*)?\z/) }
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 if grouped? && (column = group_by_column)
546 if column = group_by_column
547 547 order = (sort_criteria_order_for(column.name) || column.default_order).try(:upcase)
548 column.sortable.is_a?(Array) ?
549 column.sortable.collect {|s| "#{s} #{order}"} :
550 "#{column.sortable} #{order}"
548 Array(column.sortable).map {|s| "#{s} #{order}"}
551 549 end
552 550 end
553 551
554 552 # Returns true if the query is a grouped query
555 553 def grouped?
556 554 !group_by_column.nil?
557 555 end
558 556
559 557 def group_by_column
560 558 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
561 559 end
562 560
563 561 def group_by_statement
564 562 group_by_column.try(:groupable)
565 563 end
566 564
567 565 def project_statement
568 566 project_clauses = []
569 567 if project && !project.descendants.active.empty?
570 568 if has_filter?("subproject_id")
571 569 case operator_for("subproject_id")
572 570 when '='
573 571 # include the selected subprojects
574 572 ids = [project.id] + values_for("subproject_id").each(&:to_i)
575 573 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
576 574 when '!*'
577 575 # main project only
578 576 project_clauses << "#{Project.table_name}.id = %d" % project.id
579 577 else
580 578 # all subprojects
581 579 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
582 580 end
583 581 elsif Setting.display_subprojects_issues?
584 582 project_clauses << "#{Project.table_name}.lft >= #{project.lft} AND #{Project.table_name}.rgt <= #{project.rgt}"
585 583 else
586 584 project_clauses << "#{Project.table_name}.id = %d" % project.id
587 585 end
588 586 elsif project
589 587 project_clauses << "#{Project.table_name}.id = %d" % project.id
590 588 end
591 589 project_clauses.any? ? project_clauses.join(' AND ') : nil
592 590 end
593 591
594 592 def statement
595 593 # filters clauses
596 594 filters_clauses = []
597 595 filters.each_key do |field|
598 596 next if field == "subproject_id"
599 597 v = values_for(field).clone
600 598 next unless v and !v.empty?
601 599 operator = operator_for(field)
602 600
603 601 # "me" value substitution
604 602 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
605 603 if v.delete("me")
606 604 if User.current.logged?
607 605 v.push(User.current.id.to_s)
608 606 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
609 607 else
610 608 v.push("0")
611 609 end
612 610 end
613 611 end
614 612
615 613 if field == 'project_id'
616 614 if v.delete('mine')
617 615 v += User.current.memberships.map(&:project_id).map(&:to_s)
618 616 end
619 617 end
620 618
621 619 if field =~ /cf_(\d+)$/
622 620 # custom field
623 621 filters_clauses << sql_for_custom_field(field, operator, v, $1)
624 622 elsif respond_to?("sql_for_#{field}_field")
625 623 # specific statement
626 624 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
627 625 else
628 626 # regular field
629 627 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
630 628 end
631 629 end if filters and valid?
632 630
633 631 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
634 632 # Excludes results for which the grouped custom field is not visible
635 633 filters_clauses << c.custom_field.visibility_by_project_condition
636 634 end
637 635
638 636 filters_clauses << project_statement
639 637 filters_clauses.reject!(&:blank?)
640 638
641 639 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
642 640 end
643 641
644 642 # Returns the sum of values for the given column
645 643 def total_for(column)
646 644 total_with_scope(column, base_scope)
647 645 end
648 646
649 647 # Returns a hash of the sum of the given column for each group,
650 648 # or nil if the query is not grouped
651 649 def total_by_group_for(column)
652 650 grouped_query do |scope|
653 651 total_with_scope(column, scope)
654 652 end
655 653 end
656 654
657 655 def totals
658 656 totals = totalable_columns.map {|column| [column, total_for(column)]}
659 657 yield totals if block_given?
660 658 totals
661 659 end
662 660
663 661 def totals_by_group
664 662 totals = totalable_columns.map {|column| [column, total_by_group_for(column)]}
665 663 yield totals if block_given?
666 664 totals
667 665 end
668 666
669 667 private
670 668
671 669 def grouped_query(&block)
672 670 r = nil
673 671 if grouped?
674 672 begin
675 673 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
676 674 r = yield base_group_scope
677 675 rescue ActiveRecord::RecordNotFound
678 676 r = {nil => yield(base_scope)}
679 677 end
680 678 c = group_by_column
681 679 if c.is_a?(QueryCustomFieldColumn)
682 680 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
683 681 end
684 682 end
685 683 r
686 684 rescue ::ActiveRecord::StatementInvalid => e
687 685 raise StatementInvalid.new(e.message)
688 686 end
689 687
690 688 def total_with_scope(column, scope)
691 689 unless column.is_a?(QueryColumn)
692 690 column = column.to_sym
693 691 column = available_totalable_columns.detect {|c| c.name == column}
694 692 end
695 693 if column.is_a?(QueryCustomFieldColumn)
696 694 custom_field = column.custom_field
697 695 send "total_for_custom_field", custom_field, scope
698 696 else
699 697 send "total_for_#{column.name}", scope
700 698 end
701 699 rescue ::ActiveRecord::StatementInvalid => e
702 700 raise StatementInvalid.new(e.message)
703 701 end
704 702
705 703 def base_scope
706 704 raise "unimplemented"
707 705 end
708 706
709 707 def base_group_scope
710 708 base_scope.
711 709 joins(joins_for_order_statement(group_by_statement)).
712 710 group(group_by_statement)
713 711 end
714 712
715 713 def total_for_custom_field(custom_field, scope, &block)
716 714 total = custom_field.format.total_for_scope(custom_field, scope)
717 715 total = map_total(total) {|t| custom_field.format.cast_total_value(custom_field, t)}
718 716 total
719 717 end
720 718
721 719 def map_total(total, &block)
722 720 if total.is_a?(Hash)
723 721 total.keys.each {|k| total[k] = yield total[k]}
724 722 else
725 723 total = yield total
726 724 end
727 725 total
728 726 end
729 727
730 728 def sql_for_custom_field(field, operator, value, custom_field_id)
731 729 db_table = CustomValue.table_name
732 730 db_field = 'value'
733 731 filter = @available_filters[field]
734 732 return nil unless filter
735 733 if filter[:field].format.target_class && filter[:field].format.target_class <= User
736 734 if value.delete('me')
737 735 value.push User.current.id.to_s
738 736 end
739 737 end
740 738 not_in = nil
741 739 if operator == '!'
742 740 # Makes ! operator work for custom fields with multiple values
743 741 operator = '='
744 742 not_in = 'NOT'
745 743 end
746 744 customized_key = "id"
747 745 customized_class = queried_class
748 746 if field =~ /^(.+)\.cf_/
749 747 assoc = $1
750 748 customized_key = "#{assoc}_id"
751 749 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
752 750 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
753 751 end
754 752 where = sql_for_field(field, operator, value, db_table, db_field, true)
755 753 if operator =~ /[<>]/
756 754 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
757 755 end
758 756 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
759 757 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
760 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}" +
761 759 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
762 760 end
763 761
764 762 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
765 763 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
766 764 sql = ''
767 765 case operator
768 766 when "="
769 767 if value.any?
770 768 case type_for(field)
771 769 when :date, :date_past
772 770 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
773 771 when :integer
774 772 int_values = value.first.to_s.scan(/[+-]?\d+/).map(&:to_i).join(",")
775 773 if int_values.present?
776 774 if is_custom_filter
777 775 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}))"
778 776 else
779 777 sql = "#{db_table}.#{db_field} IN (#{int_values})"
780 778 end
781 779 else
782 780 sql = "1=0"
783 781 end
784 782 when :float
785 783 if is_custom_filter
786 784 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})"
787 785 else
788 786 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
789 787 end
790 788 else
791 789 sql = queried_class.send(:sanitize_sql_for_conditions, ["#{db_table}.#{db_field} IN (?)", value])
792 790 end
793 791 else
794 792 # IN an empty set
795 793 sql = "1=0"
796 794 end
797 795 when "!"
798 796 if value.any?
799 797 sql = queried_class.send(:sanitize_sql_for_conditions, ["(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (?))", value])
800 798 else
801 799 # NOT IN an empty set
802 800 sql = "1=1"
803 801 end
804 802 when "!*"
805 803 sql = "#{db_table}.#{db_field} IS NULL"
806 804 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
807 805 when "*"
808 806 sql = "#{db_table}.#{db_field} IS NOT NULL"
809 807 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
810 808 when ">="
811 809 if [:date, :date_past].include?(type_for(field))
812 810 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
813 811 else
814 812 if is_custom_filter
815 813 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})"
816 814 else
817 815 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
818 816 end
819 817 end
820 818 when "<="
821 819 if [:date, :date_past].include?(type_for(field))
822 820 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
823 821 else
824 822 if is_custom_filter
825 823 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})"
826 824 else
827 825 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
828 826 end
829 827 end
830 828 when "><"
831 829 if [:date, :date_past].include?(type_for(field))
832 830 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
833 831 else
834 832 if is_custom_filter
835 833 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})"
836 834 else
837 835 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
838 836 end
839 837 end
840 838 when "o"
841 839 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"
842 840 when "c"
843 841 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"
844 842 when "><t-"
845 843 # between today - n days and today
846 844 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
847 845 when ">t-"
848 846 # >= today - n days
849 847 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
850 848 when "<t-"
851 849 # <= today - n days
852 850 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
853 851 when "t-"
854 852 # = n days in past
855 853 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
856 854 when "><t+"
857 855 # between today and today + n days
858 856 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
859 857 when ">t+"
860 858 # >= today + n days
861 859 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
862 860 when "<t+"
863 861 # <= today + n days
864 862 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
865 863 when "t+"
866 864 # = today + n days
867 865 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
868 866 when "t"
869 867 # = today
870 868 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
871 869 when "ld"
872 870 # = yesterday
873 871 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
874 872 when "w"
875 873 # = this week
876 874 first_day_of_week = l(:general_first_day_of_week).to_i
877 875 day_of_week = User.current.today.cwday
878 876 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
879 877 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
880 878 when "lw"
881 879 # = last week
882 880 first_day_of_week = l(:general_first_day_of_week).to_i
883 881 day_of_week = User.current.today.cwday
884 882 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
885 883 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
886 884 when "l2w"
887 885 # = last 2 weeks
888 886 first_day_of_week = l(:general_first_day_of_week).to_i
889 887 day_of_week = User.current.today.cwday
890 888 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
891 889 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
892 890 when "m"
893 891 # = this month
894 892 date = User.current.today
895 893 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
896 894 when "lm"
897 895 # = last month
898 896 date = User.current.today.prev_month
899 897 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
900 898 when "y"
901 899 # = this year
902 900 date = User.current.today
903 901 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
904 902 when "~"
905 903 sql = sql_contains("#{db_table}.#{db_field}", value.first)
906 904 when "!~"
907 905 sql = sql_contains("#{db_table}.#{db_field}", value.first, false)
908 906 else
909 907 raise "Unknown query operator #{operator}"
910 908 end
911 909
912 910 return sql
913 911 end
914 912
915 913 # Returns a SQL LIKE statement with wildcards
916 914 def sql_contains(db_field, value, match=true)
917 915 queried_class.send :sanitize_sql_for_conditions,
918 916 [Redmine::Database.like(db_field, '?', :match => match), "%#{value}%"]
919 917 end
920 918
921 919 # Adds a filter for the given custom field
922 920 def add_custom_field_filter(field, assoc=nil)
923 921 options = field.query_filter_options(self)
924 922 if field.format.target_class && field.format.target_class <= User
925 923 if options[:values].is_a?(Array) && User.current.logged?
926 924 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
927 925 end
928 926 end
929 927
930 928 filter_id = "cf_#{field.id}"
931 929 filter_name = field.name
932 930 if assoc.present?
933 931 filter_id = "#{assoc}.#{filter_id}"
934 932 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
935 933 end
936 934 add_available_filter filter_id, options.merge({
937 935 :name => filter_name,
938 936 :field => field
939 937 })
940 938 end
941 939
942 940 # Adds filters for the given custom fields scope
943 941 def add_custom_fields_filters(scope, assoc=nil)
944 942 scope.visible.where(:is_filter => true).sorted.each do |field|
945 943 add_custom_field_filter(field, assoc)
946 944 end
947 945 end
948 946
949 947 # Adds filters for the given associations custom fields
950 948 def add_associations_custom_fields_filters(*associations)
951 949 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
952 950 associations.each do |assoc|
953 951 association_klass = queried_class.reflect_on_association(assoc).klass
954 952 fields_by_class.each do |field_class, fields|
955 953 if field_class.customized_class <= association_klass
956 954 fields.sort.each do |field|
957 955 add_custom_field_filter(field, assoc)
958 956 end
959 957 end
960 958 end
961 959 end
962 960 end
963 961
964 962 def quoted_time(time, is_custom_filter)
965 963 if is_custom_filter
966 964 # Custom field values are stored as strings in the DB
967 965 # using this format that does not depend on DB date representation
968 966 time.strftime("%Y-%m-%d %H:%M:%S")
969 967 else
970 968 self.class.connection.quoted_date(time)
971 969 end
972 970 end
973 971
974 972 def date_for_user_time_zone(y, m, d)
975 973 if tz = User.current.time_zone
976 974 tz.local y, m, d
977 975 else
978 976 Time.local y, m, d
979 977 end
980 978 end
981 979
982 980 # Returns a SQL clause for a date or datetime field.
983 981 def date_clause(table, field, from, to, is_custom_filter)
984 982 s = []
985 983 if from
986 984 if from.is_a?(Date)
987 985 from = date_for_user_time_zone(from.year, from.month, from.day).yesterday.end_of_day
988 986 else
989 987 from = from - 1 # second
990 988 end
991 989 if self.class.default_timezone == :utc
992 990 from = from.utc
993 991 end
994 992 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
995 993 end
996 994 if to
997 995 if to.is_a?(Date)
998 996 to = date_for_user_time_zone(to.year, to.month, to.day).end_of_day
999 997 end
1000 998 if self.class.default_timezone == :utc
1001 999 to = to.utc
1002 1000 end
1003 1001 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
1004 1002 end
1005 1003 s.join(' AND ')
1006 1004 end
1007 1005
1008 1006 # Returns a SQL clause for a date or datetime field using relative dates.
1009 1007 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
1010 1008 date_clause(table, field, (days_from ? User.current.today + days_from : nil), (days_to ? User.current.today + days_to : nil), is_custom_filter)
1011 1009 end
1012 1010
1013 1011 # Returns a Date or Time from the given filter value
1014 1012 def parse_date(arg)
1015 1013 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
1016 1014 Time.parse(arg) rescue nil
1017 1015 else
1018 1016 Date.parse(arg) rescue nil
1019 1017 end
1020 1018 end
1021 1019
1022 1020 # Additional joins required for the given sort options
1023 1021 def joins_for_order_statement(order_options)
1024 1022 joins = []
1025 1023
1026 1024 if order_options
1027 1025 if order_options.include?('authors')
1028 1026 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
1029 1027 end
1030 1028 order_options.scan(/cf_\d+/).uniq.each do |name|
1031 1029 column = available_columns.detect {|c| c.name.to_s == name}
1032 1030 join = column && column.custom_field.join_for_order_statement
1033 1031 if join
1034 1032 joins << join
1035 1033 end
1036 1034 end
1037 1035 end
1038 1036
1039 1037 joins.any? ? joins.join(' ') : nil
1040 1038 end
1041 1039 end
General Comments 0
You need to be logged in to leave comments. Login now