Class: Query

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
app/models/query.rb

Direct Known Subclasses

IssueQuery, TimeEntryQuery

Defined Under Namespace

Classes: StatementInvalid

Constant Summary collapse

VISIBILITY_PRIVATE =
0
VISIBILITY_ROLES =
1
VISIBILITY_PUBLIC =
2

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(attributes = nil, *args) ⇒ Query

Returns a new instance of Query.


231
232
233
234
# File 'app/models/query.rb', line 231

def initialize(attributes=nil, *args)
  super attributes
  @is_for_all = project.nil?
end

Class Method Details

.add_available_column(column) ⇒ Object


415
416
417
# File 'app/models/query.rb', line 415

def self.add_available_column(column)
  self.available_columns << (column) if column.is_a?(QueryColumn)
end

.build_from_params(params, attributes = {}) ⇒ Object

Builds a new query from the given params and attributes


252
253
254
# File 'app/models/query.rb', line 252

def self.build_from_params(params, attributes={})
  new(attributes).build_from_params(params)
end

.operators_labelsObject

Returns a hash of localized labels for all filter operators


302
303
304
# File 'app/models/query.rb', line 302

def self.operators_labels
  operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
end

Instance Method Details

#add_available_filter(field, options) ⇒ Object

Adds an available filter


337
338
339
340
341
# File 'app/models/query.rb', line 337

def add_available_filter(field, options)
  @available_filters ||= ActiveSupport::OrderedHash.new
  @available_filters[field] = options
  @available_filters
end

#add_filter(field, operator, values = nil) ⇒ Object


361
362
363
364
365
366
367
368
369
# File 'app/models/query.rb', line 361

def add_filter(field, operator, values=nil)
  # values must be an array
  return unless values.nil? || values.is_a?(Array)  # check if field is defined as an available filter

  if available_filters.has_key? field
    filter_options = available_filters[field]
    filters[field] = {:operator => operator, :values => (values || [''])}
  end
end

#add_filter_error(field, message) ⇒ Object


284
285
286
287
# File 'app/models/query.rb', line 284

def add_filter_error(field, message)
  m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
  errors.add(:base, m)
end

#add_filters(fields, operators, values) ⇒ Object

Add multiple filters using add_filter


382
383
384
385
386
387
388
# File 'app/models/query.rb', line 382

def add_filters(fields, operators, values)
  if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
    fields.each do |field|
      add_filter(field, operators[field], values && values[field])
    end
  end
end

#add_short_filter(field, expression) ⇒ Object


371
372
373
374
375
376
377
378
379
# File 'app/models/query.rb', line 371

def add_short_filter(field, expression)
  return unless expression && available_filters.has_key?(field)
  field_type = available_filters[field][:type]
  operators_by_filter_type[field_type].sort.reverse.detect do |operator|
    next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
    values = $1
    add_filter field, operator, values.present? ? values.split('|') : ['']
  end || add_filter(field, '=', expression.split('|'))
end

#all_projectsObject


315
316
317
# File 'app/models/query.rb', line 315

def all_projects
  @all_projects ||= Project.visible.to_a
end

#all_projects_valuesObject


319
320
321
322
323
324
325
326
327
328
# File 'app/models/query.rb', line 319

def all_projects_values
  return @all_projects_values if @all_projects_values

  values = []
  Project.project_tree(all_projects) do |p, level|
    prefix = (level > 0 ? ('--' * level + ' ') : '')
    values << ["#{prefix}#{p.name}", p.id.to_s]
  end
  @all_projects_values = values
end

#available_block_columnsObject


452
453
454
# File 'app/models/query.rb', line 452

def available_block_columns
  available_columns.reject(&:inline?)
end

#available_filtersObject

Return a hash of available filters


351
352
353
354
355
356
357
358
359
# File 'app/models/query.rb', line 351

def available_filters
  unless @available_filters
    initialize_available_filters
    @available_filters.each do |field, options|
      options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
    end
  end
  @available_filters
end

#available_filters_as_jsonObject

Returns a representation of the available filters for JSON serialization


307
308
309
310
311
312
313
# File 'app/models/query.rb', line 307

def available_filters_as_json
  json = {}
  available_filters.each do |field, options|
    json[field] = options.slice(:type, :name, :values).stringify_keys
  end
  json
end

#available_inline_columnsObject


448
449
450
# File 'app/models/query.rb', line 448

def available_inline_columns
  available_columns.select(&:inline?)
end

#block_columnsObject


444
445
446
# File 'app/models/query.rb', line 444

def block_columns
  columns.reject(&:inline?)
end

#build_from_params(params) ⇒ Object

Builds the query from the given params


237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'app/models/query.rb', line 237

def build_from_params(params)
  if params[:fields] || params[:f]
    self.filters = {}
    add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
  else
    available_filters.keys.each do |field|
      add_short_filter(field, params[field]) if params[field]
    end
  end
  self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
  self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
  self
end

#column_names=(names) ⇒ Object


460
461
462
463
464
465
466
467
468
469
470
# File 'app/models/query.rb', line 460

def column_names=(names)
  if names
    names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
    names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }    # Set column_names to nil if default columns

    if names == default_columns_names
      names = nil
    end
  end
  write_attribute(:column_names, names)
end

#columnsObject


432
433
434
435
436
437
438
# File 'app/models/query.rb', line 432

def columns  # preserve the column_names order

  cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
     available_columns.find { |col| col.name == name }
  end.compact
  available_columns.select(&:frozen?) | cols
end

#default_columns_namesObject


456
457
458
# File 'app/models/query.rb', line 456

def default_columns_names
  []
end

#delete_available_filter(field) ⇒ Object

Removes an available filter


344
345
346
347
348
# File 'app/models/query.rb', line 344

def delete_available_filter(field)
  if @available_filters
    @available_filters.delete(field)
  end
end

#editable_by?(user) ⇒ Boolean

Returns:

  • (Boolean)

289
290
291
292
293
294
295
# File 'app/models/query.rb', line 289

def editable_by?(user)
  return false unless user  # Admin can edit them all and regular users can edit their private queries

  return true if user.admin? || (is_private? && self.user_id == user.id)  # Members can not edit public queries that are for all project (only admin is allowed to)

  is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
end

#group_by_columnObject


524
525
526
# File 'app/models/query.rb', line 524

def group_by_column
  groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
end

#group_by_sort_orderObject

Returns the SQL sort order that should be prepended for grouping


510
511
512
513
514
515
516
517
# File 'app/models/query.rb', line 510

def group_by_sort_order
  if grouped? && (column = group_by_column)
    order = sort_criteria_order_for(column.name) || column.default_order
    column.sortable.is_a?(Array) ?
      column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
      "#{column.sortable} #{order}"
  end
end

#group_by_statementObject


528
529
530
# File 'app/models/query.rb', line 528

def group_by_statement
  group_by_column.try(:groupable)
end

#groupable_columnsObject

Returns an array of columns that can be used to group the results


420
421
422
# File 'app/models/query.rb', line 420

def groupable_columns
  available_columns.select {|c| c.groupable}
end

#grouped?Boolean

Returns true if the query is a grouped query

Returns:

  • (Boolean)

520
521
522
# File 'app/models/query.rb', line 520

def grouped?
  !group_by_column.nil?
end

#has_column?(column) ⇒ Boolean

Returns:

  • (Boolean)

472
473
474
# File 'app/models/query.rb', line 472

def has_column?(column)
  column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
end

#has_custom_field_column?Boolean

Returns:

  • (Boolean)

476
477
478
# File 'app/models/query.rb', line 476

def has_custom_field_column?
  columns.any? {|column| column.is_a? QueryCustomFieldColumn}
end

#has_default_columns?Boolean

Returns:

  • (Boolean)

480
481
482
# File 'app/models/query.rb', line 480

def has_default_columns?
  column_names.nil? || column_names.empty?
end

#has_filter?(field) ⇒ Boolean

Returns:

  • (Boolean)

390
391
392
# File 'app/models/query.rb', line 390

def has_filter?(field)
  filters and filters[field]
end

#inline_columnsObject


440
441
442
# File 'app/models/query.rb', line 440

def inline_columns
  columns.select(&:inline?)
end

#label_for(field) ⇒ Object


410
411
412
413
# File 'app/models/query.rb', line 410

def label_for(field)
  label = available_filters[field][:name] if available_filters.has_key?(field)
  label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
end

#operator_for(field) ⇒ Object


398
399
400
# File 'app/models/query.rb', line 398

def operator_for(field)
  has_filter?(field) ? filters[field][:operator] : nil
end

#project_statementObject


532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
# File 'app/models/query.rb', line 532

def project_statement
  project_clauses = []
  if project && !project.descendants.active.empty?
    ids = [project.id]
    if has_filter?("subproject_id")
      case operator_for("subproject_id")
      when '='        # include the selected subprojects

        ids += values_for("subproject_id").each(&:to_i)
      when '!*'        # main project only

      else
        # all subprojects
        ids += project.descendants.collect(&:id)
      end
    elsif Setting.display_subprojects_issues?
      ids += project.descendants.collect(&:id)
    end
    project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
  elsif project
    project_clauses << "#{Project.table_name}.id = %d" % project.id
  end
  project_clauses.any? ? project_clauses.join(' AND ') : nil
end

#queried_table_nameObject


227
228
229
# File 'app/models/query.rb', line 227

def queried_table_name
  @queried_table_name ||= self.class.queried_class.table_name
end

#sort_criteriaObject


493
494
495
# File 'app/models/query.rb', line 493

def sort_criteria
  read_attribute(:sort_criteria) || []
end

#sort_criteria=(arg) ⇒ Object


484
485
486
487
488
489
490
491
# File 'app/models/query.rb', line 484

def sort_criteria=(arg)
  c = []
  if arg.is_a?(Hash)
    arg = arg.keys.sort.collect {|k| arg[k]}
  end
  c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
  write_attribute(:sort_criteria, c)
end

#sort_criteria_key(arg) ⇒ Object


497
498
499
# File 'app/models/query.rb', line 497

def sort_criteria_key(arg)
  sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
end

#sort_criteria_order(arg) ⇒ Object


501
502
503
# File 'app/models/query.rb', line 501

def sort_criteria_order(arg)
  sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
end

#sort_criteria_order_for(key) ⇒ Object


505
506
507
# File 'app/models/query.rb', line 505

def sort_criteria_order_for(key)
  sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
end

#sortable_columnsObject

Returns a Hash of columns and the key for sorting


425
426
427
428
429
430
# File 'app/models/query.rb', line 425

def sortable_columns
  available_columns.inject({}) {|h, column|
    h[column.name.to_s] = column.sortable
    h
  }
end

#statementObject


557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
# File 'app/models/query.rb', line 557

def statement  # filters clauses

  filters_clauses = []
  filters.each_key do |field|
    next if field == "subproject_id"
    v = values_for(field).clone
    next unless v and !v.empty?
    operator = operator_for(field)

    # "me" value substitution
    if %w(assigned_to_id author_id user_id watcher_id).include?(field)
      if v.delete("me")
        if User.current.logged?
          v.push(User.current.id.to_s)
          v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
        else
          v.push("0")
        end
      end
    end

    if field == 'project_id'
      if v.delete('mine')
        v += User.current.memberships.map(&:project_id).map(&:to_s)
      end
    end

    if field =~ /cf_(\d+)$/      # custom field

      filters_clauses << sql_for_custom_field(field, operator, v, $1)
    elsif respond_to?("sql_for_#{field}_field")      # specific statement

      filters_clauses << send("sql_for_#{field}_field", field, operator, v)
    else
      # regular field
      filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
    end
  end if filters and valid?

  if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)    # Excludes results for which the grouped custom field is not visible

    filters_clauses << c.custom_field.visibility_by_project_condition
  end

  filters_clauses << project_statement
  filters_clauses.reject!(&:blank?)

  filters_clauses.any? ? filters_clauses.join(' AND ') : nil
end

#trackersObject


297
298
299
# File 'app/models/query.rb', line 297

def trackers
  @trackers ||= project.nil? ? Tracker.sorted.to_a : project.rolled_up_trackers
end

#type_for(field) ⇒ Object


394
395
396
# File 'app/models/query.rb', line 394

def type_for(field)
  available_filters[field][:type] if available_filters.has_key?(field)
end

#validate_query_filtersObject


256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'app/models/query.rb', line 256

def validate_query_filters
  filters.each_key do |field|
    if values_for(field)
      case type_for(field)
      when :integer
        add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
      when :float
        add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
      when :date, :date_past
        case operator_for(field)
        when "=", ">=", "<=", "><"
          add_filter_error(field, :invalid) if values_for(field).detect {|v|
            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?)
          }
        when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
          add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
        end
      end
    end

    add_filter_error(field, :blank) unless
        # filter requires one or more values
        (values_for(field) and !values_for(field).first.blank?) or
        # filter doesn't require any value
        ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y"].include? operator_for(field)
  end if filters
end

#value_for(field, index = 0) ⇒ Object


406
407
408
# File 'app/models/query.rb', line 406

def value_for(field, index=0)
  (values_for(field) || [])[index]
end

#values_for(field) ⇒ Object


402
403
404
# File 'app/models/query.rb', line 402

def values_for(field)
  has_filter?(field) ? filters[field][:values] : nil
end