To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / app / models / query.rb @ 437:102056ec2de9

History | View | Annotate | Download (25.5 KB)

1
# Redmine - project management software
2
# Copyright (C) 2006-2008  Jean-Philippe Lang
3
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8
# 
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
# 
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17

    
18
class QueryColumn  
19
  attr_accessor :name, :sortable, :groupable, :default_order
20
  include Redmine::I18n
21
  
22
  def initialize(name, options={})
23
    self.name = name
24
    self.sortable = options[:sortable]
25
    self.groupable = options[:groupable] || false
26
    if groupable == true
27
      self.groupable = name.to_s
28
    end
29
    self.default_order = options[:default_order]
30
    @caption_key = options[:caption] || "field_#{name}"
31
  end
32
  
33
  def caption
34
    l(@caption_key)
35
  end
36
  
37
  # Returns true if the column is sortable, otherwise false
38
  def sortable?
39
    !sortable.nil?
40
  end
41
  
42
  def value(issue)
43
    issue.send name
44
  end
45
end
46

    
47
class QueryCustomFieldColumn < QueryColumn
48

    
49
  def initialize(custom_field)
50
    self.name = "cf_#{custom_field.id}".to_sym
51
    self.sortable = custom_field.order_statement || false
52
    if %w(list date bool int).include?(custom_field.field_format)
53
      self.groupable = custom_field.order_statement
54
    end
55
    self.groupable ||= false
56
    @cf = custom_field
57
  end
58
  
59
  def caption
60
    @cf.name
61
  end
62
  
63
  def custom_field
64
    @cf
65
  end
66
  
67
  def value(issue)
68
    cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
69
    cv && @cf.cast_value(cv.value)
70
  end
71
end
72

    
73
class Query < ActiveRecord::Base
74
  class StatementInvalid < ::ActiveRecord::StatementInvalid
75
  end
76
  
77
  belongs_to :project
78
  belongs_to :user
79
  serialize :filters
80
  serialize :column_names
81
  serialize :sort_criteria, Array
82
  
83
  attr_protected :project_id, :user_id
84
  
85
  validates_presence_of :name, :on => :save
86
  validates_length_of :name, :maximum => 255
87
    
88
  @@operators = { "="   => :label_equals, 
89
                  "!"   => :label_not_equals,
90
                  "o"   => :label_open_issues,
91
                  "c"   => :label_closed_issues,
92
                  "!*"  => :label_none,
93
                  "*"   => :label_all,
94
                  ">="  => :label_greater_or_equal,
95
                  "<="  => :label_less_or_equal,
96
                  "<t+" => :label_in_less_than,
97
                  ">t+" => :label_in_more_than,
98
                  "t+"  => :label_in,
99
                  "t"   => :label_today,
100
                  "w"   => :label_this_week,
101
                  ">t-" => :label_less_than_ago,
102
                  "<t-" => :label_more_than_ago,
103
                  "t-"  => :label_ago,
104
                  "~"   => :label_contains,
105
                  "!~"  => :label_not_contains }
106

    
107
  cattr_reader :operators
108
    
109
  @@operators_by_filter_type = { :list => [ "=", "!" ],
110
                                 :list_status => [ "o", "=", "!", "c", "*" ],
111
                                 :list_optional => [ "=", "!", "!*", "*" ],
112
                                 :list_subprojects => [ "*", "!*", "=" ],
113
                                 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
114
                                 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
115
                                 :string => [ "=", "~", "!", "!~" ],
116
                                 :text => [  "~", "!~" ],
117
                                 :integer => [ "=", ">=", "<=", "!*", "*" ] }
118

    
119
  cattr_reader :operators_by_filter_type
120

    
121
  @@available_columns = [
122
    QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
123
    QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
124
    QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
125
    QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
126
    QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
127
    QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
128
    QueryColumn.new(:author),
129
    QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
130
    QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
131
    QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
132
    QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
133
    QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
134
    QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
135
    QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
136
    QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
137
    QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
138
  ]
139
  cattr_reader :available_columns
140
  
141
  def initialize(attributes = nil)
142
    super attributes
143
    self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
144
  end
145
  
146
  def after_initialize
147
    # Store the fact that project is nil (used in #editable_by?)
148
    @is_for_all = project.nil?
149
  end
150
  
151
  def validate
152
    filters.each_key do |field|
153
      errors.add label_for(field), :blank unless 
154
          # filter requires one or more values
155
          (values_for(field) and !values_for(field).first.blank?) or 
156
          # filter doesn't require any value
157
          ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
158
    end if filters
159
  end
160
  
161
  def editable_by?(user)
162
    return false unless user
163
    # Admin can edit them all and regular users can edit their private queries
164
    return true if user.admin? || (!is_public && self.user_id == user.id)
165
    # Members can not edit public queries that are for all project (only admin is allowed to)
166
    is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
167
  end
168
  
169
  def available_filters
170
    return @available_filters if @available_filters
171
    
172
    trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
173
    
174
    @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },       
175
                           "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },                                                                                                                
176
                           "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
177
                           "subject" => { :type => :text, :order => 8 },  
178
                           "created_on" => { :type => :date_past, :order => 9 },                        
179
                           "updated_on" => { :type => :date_past, :order => 10 },
180
                           "start_date" => { :type => :date, :order => 11 },
181
                           "due_date" => { :type => :date, :order => 12 },
182
                           "estimated_hours" => { :type => :integer, :order => 13 },
183
                           "done_ratio" =>  { :type => :integer, :order => 14 }}
184
    
185
    user_values = []
186
    user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
187
    if project
188
      user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
189
    else
190
      project_ids = Project.all(:conditions => Project.visible_by(User.current)).collect(&:id)
191
      if project_ids.any?
192
        # members of the user's projects
193
        user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", project_ids]).sort.collect{|s| [s.name, s.id.to_s] }
194
      end
195
    end
196
    @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
197
    @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
198

    
199
    group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
200
    @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
201

    
202
    role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
203
    @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
204
    
205
    if User.current.logged?
206
      @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
207
    end
208
  
209
    if project
210
      # project specific filters
211
      unless @project.issue_categories.empty?
212
        @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
213
      end
214
      unless @project.shared_versions.empty?
215
        @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
216
      end
217
      unless @project.descendants.active.empty?
218
        @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
219
      end
220
      add_custom_fields_filters(@project.all_issue_custom_fields)
221
    else
222
      # global filters for cross project issue list
223
      system_shared_versions = Version.visible.find_all_by_sharing('system')
224
      unless system_shared_versions.empty?
225
        @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
226
      end
227
      add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
228
      # project filter
229
      project_values = Project.all(:conditions => Project.visible_by(User.current), :order => 'lft').map do |p|
230
        pre = (p.level > 0 ? ('--' * p.level + ' ') : '')
231
        ["#{pre}#{p.name}",p.id.to_s]
232
      end
233
      @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values}
234
    end
235
    @available_filters
236
  end
237
  
238
  def add_filter(field, operator, values)
239
    # values must be an array
240
    return unless values and values.is_a? Array # and !values.first.empty?
241
    # check if field is defined as an available filter
242
    if available_filters.has_key? field
243
      filter_options = available_filters[field]
244
      # check if operator is allowed for that filter
245
      #if @@operators_by_filter_type[filter_options[:type]].include? operator
246
      #  allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
247
      #  filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
248
      #end
249
      filters[field] = {:operator => operator, :values => values }
250
    end
251
  end
252
  
253
  def add_short_filter(field, expression)
254
    return unless expression
255
    parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
256
    add_filter field, (parms[0] || "="), [parms[1] || ""]
257
  end
258

    
259
  # Add multiple filters using +add_filter+
260
  def add_filters(fields, operators, values)
261
    if fields.is_a?(Array) && operators.is_a?(Hash) && values.is_a?(Hash)
262
      fields.each do |field|
263
        add_filter(field, operators[field], values[field])
264
      end
265
    end
266
  end
267
  
268
  def has_filter?(field)
269
    filters and filters[field]
270
  end
271
  
272
  def operator_for(field)
273
    has_filter?(field) ? filters[field][:operator] : nil
274
  end
275
  
276
  def values_for(field)
277
    has_filter?(field) ? filters[field][:values] : nil
278
  end
279
  
280
  def label_for(field)
281
    label = available_filters[field][:name] if available_filters.has_key?(field)
282
    label ||= field.gsub(/\_id$/, "")
283
  end
284

    
285
  def available_columns
286
    return @available_columns if @available_columns
287
    @available_columns = Query.available_columns
288
    @available_columns += (project ? 
289
                            project.all_issue_custom_fields :
290
                            IssueCustomField.find(:all)
291
                           ).collect {|cf| QueryCustomFieldColumn.new(cf) }      
292
  end
293

    
294
  def self.available_columns=(v)
295
    self.available_columns = (v)
296
  end
297
  
298
  def self.add_available_column(column)
299
    self.available_columns << (column) if column.is_a?(QueryColumn)
300
  end
301
  
302
  # Returns an array of columns that can be used to group the results
303
  def groupable_columns
304
    available_columns.select {|c| c.groupable}
305
  end
306

    
307
  # Returns a Hash of columns and the key for sorting
308
  def sortable_columns
309
    {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
310
                                               h[column.name.to_s] = column.sortable
311
                                               h
312
                                             })
313
  end
314
  
315
  def columns
316
    if has_default_columns?
317
      available_columns.select do |c|
318
        # Adds the project column by default for cross-project lists
319
        Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
320
      end
321
    else
322
      # preserve the column_names order
323
      column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
324
    end
325
  end
326
  
327
  def column_names=(names)
328
    if names
329
      names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
330
      names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
331
      # Set column_names to nil if default columns
332
      if names.map(&:to_s) == Setting.issue_list_default_columns
333
        names = nil
334
      end
335
    end
336
    write_attribute(:column_names, names)
337
  end
338
  
339
  def has_column?(column)
340
    column_names && column_names.include?(column.name)
341
  end
342
  
343
  def has_default_columns?
344
    column_names.nil? || column_names.empty?
345
  end
346
  
347
  def sort_criteria=(arg)
348
    c = []
349
    if arg.is_a?(Hash)
350
      arg = arg.keys.sort.collect {|k| arg[k]}
351
    end
352
    c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
353
    write_attribute(:sort_criteria, c)
354
  end
355
  
356
  def sort_criteria
357
    read_attribute(:sort_criteria) || []
358
  end
359
  
360
  def sort_criteria_key(arg)
361
    sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
362
  end
363
  
364
  def sort_criteria_order(arg)
365
    sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
366
  end
367
  
368
  # Returns the SQL sort order that should be prepended for grouping
369
  def group_by_sort_order
370
    if grouped? && (column = group_by_column)
371
      column.sortable.is_a?(Array) ?
372
        column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
373
        "#{column.sortable} #{column.default_order}"
374
    end
375
  end
376
  
377
  # Returns true if the query is a grouped query
378
  def grouped?
379
    !group_by.blank?
380
  end
381
  
382
  def group_by_column
383
    groupable_columns.detect {|c| c.name.to_s == group_by}
384
  end
385
  
386
  def group_by_statement
387
    group_by_column.groupable
388
  end
389
  
390
  def project_statement
391
    project_clauses = []
392
    if project && !@project.descendants.active.empty?
393
      ids = [project.id]
394
      if has_filter?("subproject_id")
395
        case operator_for("subproject_id")
396
        when '='
397
          # include the selected subprojects
398
          ids += values_for("subproject_id").each(&:to_i)
399
        when '!*'
400
          # main project only
401
        else
402
          # all subprojects
403
          ids += project.descendants.collect(&:id)
404
        end
405
      elsif Setting.display_subprojects_issues?
406
        ids += project.descendants.collect(&:id)
407
      end
408
      project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
409
    elsif project
410
      project_clauses << "#{Project.table_name}.id = %d" % project.id
411
    end
412
    project_clauses <<  Project.allowed_to_condition(User.current, :view_issues)
413
    project_clauses.join(' AND ')
414
  end
415

    
416
  def statement
417
    # filters clauses
418
    filters_clauses = []
419
    filters.each_key do |field|
420
      next if field == "subproject_id"
421
      v = values_for(field).clone
422
      next unless v and !v.empty?
423
      operator = operator_for(field)
424
      
425
      # "me" value subsitution
426
      if %w(assigned_to_id author_id watcher_id).include?(field)
427
        v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
428
      end
429
      
430
      sql = ''
431
      if field =~ /^cf_(\d+)$/
432
        # custom field
433
        db_table = CustomValue.table_name
434
        db_field = 'value'
435
        is_custom_filter = true
436
        sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
437
        sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
438
      elsif field == 'watcher_id'
439
        db_table = Watcher.table_name
440
        db_field = 'user_id'
441
        sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
442
        sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
443
      elsif field == "member_of_group" # named field
444
        if operator == '*' # Any group
445
          groups = Group.all
446
          operator = '=' # Override the operator since we want to find by assigned_to
447
        elsif operator == "!*"
448
          groups = Group.all
449
          operator = '!' # Override the operator since we want to find by assigned_to
450
        else
451
          groups = Group.find_all_by_id(v)
452
        end
453
        groups ||= []
454

    
455
        members_of_groups = groups.inject([]) {|user_ids, group|
456
          if group && group.user_ids.present?
457
            user_ids << group.user_ids
458
          end
459
          user_ids.flatten.uniq.compact
460
        }.sort.collect(&:to_s)
461
        
462
        sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
463

    
464
      elsif field == "assigned_to_role" # named field
465
        if operator == "*" # Any Role
466
          roles = Role.givable
467
          operator = '=' # Override the operator since we want to find by assigned_to
468
        elsif operator == "!*" # No role
469
          roles = Role.givable
470
          operator = '!' # Override the operator since we want to find by assigned_to
471
        else
472
          roles = Role.givable.find_all_by_id(v)
473
        end
474
        roles ||= []
475
        
476
        members_of_roles = roles.inject([]) {|user_ids, role|
477
          if role && role.members
478
            user_ids << role.members.collect(&:user_id)
479
          end
480
          user_ids.flatten.uniq.compact
481
        }.sort.collect(&:to_s)
482
        
483
        sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
484
      else
485
        # regular field
486
        db_table = Issue.table_name
487
        db_field = field
488
        sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
489
      end
490
      filters_clauses << sql
491
      
492
    end if filters and valid?
493
    
494
    (filters_clauses << project_statement).join(' AND ')
495
  end
496
  
497
  # Returns the issue count
498
  def issue_count
499
    Issue.count(:include => [:status, :project], :conditions => statement)
500
  rescue ::ActiveRecord::StatementInvalid => e
501
    raise StatementInvalid.new(e.message)
502
  end
503
  
504
  # Returns the issue count by group or nil if query is not grouped
505
  def issue_count_by_group
506
    r = nil
507
    if grouped?
508
      begin
509
        # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
510
        r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
511
      rescue ActiveRecord::RecordNotFound
512
        r = {nil => issue_count}
513
      end
514
      c = group_by_column
515
      if c.is_a?(QueryCustomFieldColumn)
516
        r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
517
      end
518
    end
519
    r
520
  rescue ::ActiveRecord::StatementInvalid => e
521
    raise StatementInvalid.new(e.message)
522
  end
523
  
524
  # Returns the issues
525
  # Valid options are :order, :offset, :limit, :include, :conditions
526
  def issues(options={})
527
    order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
528
    order_option = nil if order_option.blank?
529
    
530
    Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
531
                     :conditions => Query.merge_conditions(statement, options[:conditions]),
532
                     :order => order_option,
533
                     :limit  => options[:limit],
534
                     :offset => options[:offset]
535
  rescue ::ActiveRecord::StatementInvalid => e
536
    raise StatementInvalid.new(e.message)
537
  end
538

    
539
  # Returns the journals
540
  # Valid options are :order, :offset, :limit
541
  def journals(options={})
542
    Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
543
                       :conditions => statement,
544
                       :order => options[:order],
545
                       :limit => options[:limit],
546
                       :offset => options[:offset]
547
  rescue ::ActiveRecord::StatementInvalid => e
548
    raise StatementInvalid.new(e.message)
549
  end
550
  
551
  # Returns the versions
552
  # Valid options are :conditions
553
  def versions(options={})
554
    Version.find :all, :include => :project,
555
                       :conditions => Query.merge_conditions(project_statement, options[:conditions])
556
  rescue ::ActiveRecord::StatementInvalid => e
557
    raise StatementInvalid.new(e.message)
558
  end
559
  
560
  private
561
  
562
  # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
563
  def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
564
    sql = ''
565
    case operator
566
    when "="
567
      sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
568
    when "!"
569
      sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
570
    when "!*"
571
      sql = "#{db_table}.#{db_field} IS NULL"
572
      sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
573
    when "*"
574
      sql = "#{db_table}.#{db_field} IS NOT NULL"
575
      sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
576
    when ">="
577
      sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
578
    when "<="
579
      sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
580
    when "o"
581
      sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
582
    when "c"
583
      sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
584
    when ">t-"
585
      sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
586
    when "<t-"
587
      sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
588
    when "t-"
589
      sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
590
    when ">t+"
591
      sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
592
    when "<t+"
593
      sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
594
    when "t+"
595
      sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
596
    when "t"
597
      sql = date_range_clause(db_table, db_field, 0, 0)
598
    when "w"
599
      from = l(:general_first_day_of_week) == '7' ?
600
      # week starts on sunday
601
      ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
602
        # week starts on monday (Rails default)
603
        Time.now.at_beginning_of_week
604
      sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
605
    when "~"
606
      sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
607
    when "!~"
608
      sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
609
    end
610
    
611
    return sql
612
  end
613
  
614
  def add_custom_fields_filters(custom_fields)
615
    @available_filters ||= {}
616
    
617
    custom_fields.select(&:is_filter?).each do |field|
618
      case field.field_format
619
      when "text"
620
        options = { :type => :text, :order => 20 }
621
      when "list"
622
        options = { :type => :list_optional, :values => field.possible_values, :order => 20}
623
      when "date"
624
        options = { :type => :date, :order => 20 }
625
      when "bool"
626
        options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
627
      else
628
        options = { :type => :string, :order => 20 }
629
      end
630
      @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
631
    end
632
  end
633
  
634
  # Returns a SQL clause for a date or datetime field.
635
  def date_range_clause(table, field, from, to)
636
    s = []
637
    if from
638
      s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
639
    end
640
    if to
641
      s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
642
    end
643
    s.join(' AND ')
644
  end
645
end