##// END OF EJS Templates
Numeric operators for custom fields in query filters (#6180)....
Jean-Philippe Lang -
r6093:0bd5e22c81ec
parent child
Show More
@@ -1,688 +1,690
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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, :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.default_order = options[:default_order]
30 30 @caption_key = options[:caption] || "field_#{name}"
31 31 end
32 32
33 33 def caption
34 34 l(@caption_key)
35 35 end
36 36
37 37 # Returns true if the column is sortable, otherwise false
38 38 def sortable?
39 39 !sortable.nil?
40 40 end
41 41
42 42 def value(issue)
43 43 issue.send name
44 44 end
45 45
46 46 def css_classes
47 47 name
48 48 end
49 49 end
50 50
51 51 class QueryCustomFieldColumn < QueryColumn
52 52
53 53 def initialize(custom_field)
54 54 self.name = "cf_#{custom_field.id}".to_sym
55 55 self.sortable = custom_field.order_statement || false
56 56 if %w(list date bool int).include?(custom_field.field_format)
57 57 self.groupable = custom_field.order_statement
58 58 end
59 59 self.groupable ||= false
60 60 @cf = custom_field
61 61 end
62 62
63 63 def caption
64 64 @cf.name
65 65 end
66 66
67 67 def custom_field
68 68 @cf
69 69 end
70 70
71 71 def value(issue)
72 72 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
73 73 cv && @cf.cast_value(cv.value)
74 74 end
75 75
76 76 def css_classes
77 77 @css_classes ||= "#{name} #{@cf.field_format}"
78 78 end
79 79 end
80 80
81 81 class Query < ActiveRecord::Base
82 82 class StatementInvalid < ::ActiveRecord::StatementInvalid
83 83 end
84 84
85 85 belongs_to :project
86 86 belongs_to :user
87 87 serialize :filters
88 88 serialize :column_names
89 89 serialize :sort_criteria, Array
90 90
91 91 attr_protected :project_id, :user_id
92 92
93 93 validates_presence_of :name, :on => :save
94 94 validates_length_of :name, :maximum => 255
95 95
96 96 @@operators = { "=" => :label_equals,
97 97 "!" => :label_not_equals,
98 98 "o" => :label_open_issues,
99 99 "c" => :label_closed_issues,
100 100 "!*" => :label_none,
101 101 "*" => :label_all,
102 102 ">=" => :label_greater_or_equal,
103 103 "<=" => :label_less_or_equal,
104 104 "<t+" => :label_in_less_than,
105 105 ">t+" => :label_in_more_than,
106 106 "t+" => :label_in,
107 107 "t" => :label_today,
108 108 "w" => :label_this_week,
109 109 ">t-" => :label_less_than_ago,
110 110 "<t-" => :label_more_than_ago,
111 111 "t-" => :label_ago,
112 112 "~" => :label_contains,
113 113 "!~" => :label_not_contains }
114 114
115 115 cattr_reader :operators
116 116
117 117 @@operators_by_filter_type = { :list => [ "=", "!" ],
118 118 :list_status => [ "o", "=", "!", "c", "*" ],
119 119 :list_optional => [ "=", "!", "!*", "*" ],
120 120 :list_subprojects => [ "*", "!*", "=" ],
121 121 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
122 122 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
123 123 :string => [ "=", "~", "!", "!~" ],
124 124 :text => [ "~", "!~" ],
125 125 :integer => [ "=", ">=", "<=", "!*", "*" ] }
126 126
127 127 cattr_reader :operators_by_filter_type
128 128
129 129 @@available_columns = [
130 130 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
131 131 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
132 132 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
133 133 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
134 134 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
135 135 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
136 136 QueryColumn.new(:author),
137 137 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
138 138 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
139 139 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
140 140 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
141 141 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
142 142 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
143 143 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
144 144 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
145 145 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
146 146 ]
147 147 cattr_reader :available_columns
148 148
149 149 named_scope :visible, lambda {|*args|
150 150 user = args.shift || User.current
151 151 base = Project.allowed_to_condition(user, :view_issues, *args)
152 152 user_id = user.logged? ? user.id : 0
153 153 {
154 154 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
155 155 :include => :project
156 156 }
157 157 }
158 158
159 159 def initialize(attributes = nil)
160 160 super attributes
161 161 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
162 162 end
163 163
164 164 def after_initialize
165 165 # Store the fact that project is nil (used in #editable_by?)
166 166 @is_for_all = project.nil?
167 167 end
168 168
169 169 def validate
170 170 filters.each_key do |field|
171 171 errors.add label_for(field), :blank unless
172 172 # filter requires one or more values
173 173 (values_for(field) and !values_for(field).first.blank?) or
174 174 # filter doesn't require any value
175 175 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
176 176 end if filters
177 177 end
178 178
179 179 # Returns true if the query is visible to +user+ or the current user.
180 180 def visible?(user=User.current)
181 181 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
182 182 end
183 183
184 184 def editable_by?(user)
185 185 return false unless user
186 186 # Admin can edit them all and regular users can edit their private queries
187 187 return true if user.admin? || (!is_public && self.user_id == user.id)
188 188 # Members can not edit public queries that are for all project (only admin is allowed to)
189 189 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
190 190 end
191 191
192 192 def available_filters
193 193 return @available_filters if @available_filters
194 194
195 195 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
196 196
197 197 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
198 198 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
199 199 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
200 200 "subject" => { :type => :text, :order => 8 },
201 201 "created_on" => { :type => :date_past, :order => 9 },
202 202 "updated_on" => { :type => :date_past, :order => 10 },
203 203 "start_date" => { :type => :date, :order => 11 },
204 204 "due_date" => { :type => :date, :order => 12 },
205 205 "estimated_hours" => { :type => :integer, :order => 13 },
206 206 "done_ratio" => { :type => :integer, :order => 14 }}
207 207
208 208 user_values = []
209 209 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
210 210 if project
211 211 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
212 212 else
213 213 all_projects = Project.visible.all
214 214 if all_projects.any?
215 215 # members of visible projects
216 216 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort.collect{|s| [s.name, s.id.to_s] }
217 217
218 218 # project filter
219 219 project_values = []
220 220 Project.project_tree(all_projects) do |p, level|
221 221 prefix = (level > 0 ? ('--' * level + ' ') : '')
222 222 project_values << ["#{prefix}#{p.name}", p.id.to_s]
223 223 end
224 224 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
225 225 end
226 226 end
227 227 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
228 228 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
229 229
230 230 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
231 231 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
232 232
233 233 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
234 234 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
235 235
236 236 if User.current.logged?
237 237 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
238 238 end
239 239
240 240 if project
241 241 # project specific filters
242 242 categories = @project.issue_categories.all
243 243 unless categories.empty?
244 244 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
245 245 end
246 246 versions = @project.shared_versions.all
247 247 unless versions.empty?
248 248 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
249 249 end
250 250 unless @project.leaf?
251 251 subprojects = @project.descendants.visible.all
252 252 unless subprojects.empty?
253 253 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
254 254 end
255 255 end
256 256 add_custom_fields_filters(@project.all_issue_custom_fields)
257 257 else
258 258 # global filters for cross project issue list
259 259 system_shared_versions = Version.visible.find_all_by_sharing('system')
260 260 unless system_shared_versions.empty?
261 261 @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] } }
262 262 end
263 263 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
264 264 end
265 265 @available_filters
266 266 end
267 267
268 268 def add_filter(field, operator, values)
269 269 # values must be an array
270 270 return unless values and values.is_a? Array # and !values.first.empty?
271 271 # check if field is defined as an available filter
272 272 if available_filters.has_key? field
273 273 filter_options = available_filters[field]
274 274 # check if operator is allowed for that filter
275 275 #if @@operators_by_filter_type[filter_options[:type]].include? operator
276 276 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
277 277 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
278 278 #end
279 279 filters[field] = {:operator => operator, :values => values }
280 280 end
281 281 end
282 282
283 283 def add_short_filter(field, expression)
284 284 return unless expression
285 285 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
286 286 add_filter field, (parms[0] || "="), [parms[1] || ""]
287 287 end
288 288
289 289 # Add multiple filters using +add_filter+
290 290 def add_filters(fields, operators, values)
291 291 if fields.is_a?(Array) && operators.is_a?(Hash) && values.is_a?(Hash)
292 292 fields.each do |field|
293 293 add_filter(field, operators[field], values[field])
294 294 end
295 295 end
296 296 end
297 297
298 298 def has_filter?(field)
299 299 filters and filters[field]
300 300 end
301 301
302 302 def operator_for(field)
303 303 has_filter?(field) ? filters[field][:operator] : nil
304 304 end
305 305
306 306 def values_for(field)
307 307 has_filter?(field) ? filters[field][:values] : nil
308 308 end
309 309
310 310 def label_for(field)
311 311 label = available_filters[field][:name] if available_filters.has_key?(field)
312 312 label ||= field.gsub(/\_id$/, "")
313 313 end
314 314
315 315 def available_columns
316 316 return @available_columns if @available_columns
317 317 @available_columns = Query.available_columns
318 318 @available_columns += (project ?
319 319 project.all_issue_custom_fields :
320 320 IssueCustomField.find(:all)
321 321 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
322 322 end
323 323
324 324 def self.available_columns=(v)
325 325 self.available_columns = (v)
326 326 end
327 327
328 328 def self.add_available_column(column)
329 329 self.available_columns << (column) if column.is_a?(QueryColumn)
330 330 end
331 331
332 332 # Returns an array of columns that can be used to group the results
333 333 def groupable_columns
334 334 available_columns.select {|c| c.groupable}
335 335 end
336 336
337 337 # Returns a Hash of columns and the key for sorting
338 338 def sortable_columns
339 339 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
340 340 h[column.name.to_s] = column.sortable
341 341 h
342 342 })
343 343 end
344 344
345 345 def columns
346 346 if has_default_columns?
347 347 available_columns.select do |c|
348 348 # Adds the project column by default for cross-project lists
349 349 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
350 350 end
351 351 else
352 352 # preserve the column_names order
353 353 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
354 354 end
355 355 end
356 356
357 357 def column_names=(names)
358 358 if names
359 359 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
360 360 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
361 361 # Set column_names to nil if default columns
362 362 if names.map(&:to_s) == Setting.issue_list_default_columns
363 363 names = nil
364 364 end
365 365 end
366 366 write_attribute(:column_names, names)
367 367 end
368 368
369 369 def has_column?(column)
370 370 column_names && column_names.include?(column.name)
371 371 end
372 372
373 373 def has_default_columns?
374 374 column_names.nil? || column_names.empty?
375 375 end
376 376
377 377 def sort_criteria=(arg)
378 378 c = []
379 379 if arg.is_a?(Hash)
380 380 arg = arg.keys.sort.collect {|k| arg[k]}
381 381 end
382 382 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
383 383 write_attribute(:sort_criteria, c)
384 384 end
385 385
386 386 def sort_criteria
387 387 read_attribute(:sort_criteria) || []
388 388 end
389 389
390 390 def sort_criteria_key(arg)
391 391 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
392 392 end
393 393
394 394 def sort_criteria_order(arg)
395 395 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
396 396 end
397 397
398 398 # Returns the SQL sort order that should be prepended for grouping
399 399 def group_by_sort_order
400 400 if grouped? && (column = group_by_column)
401 401 column.sortable.is_a?(Array) ?
402 402 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
403 403 "#{column.sortable} #{column.default_order}"
404 404 end
405 405 end
406 406
407 407 # Returns true if the query is a grouped query
408 408 def grouped?
409 409 !group_by_column.nil?
410 410 end
411 411
412 412 def group_by_column
413 413 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
414 414 end
415 415
416 416 def group_by_statement
417 417 group_by_column.try(:groupable)
418 418 end
419 419
420 420 def project_statement
421 421 project_clauses = []
422 422 if project && !@project.descendants.active.empty?
423 423 ids = [project.id]
424 424 if has_filter?("subproject_id")
425 425 case operator_for("subproject_id")
426 426 when '='
427 427 # include the selected subprojects
428 428 ids += values_for("subproject_id").each(&:to_i)
429 429 when '!*'
430 430 # main project only
431 431 else
432 432 # all subprojects
433 433 ids += project.descendants.collect(&:id)
434 434 end
435 435 elsif Setting.display_subprojects_issues?
436 436 ids += project.descendants.collect(&:id)
437 437 end
438 438 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
439 439 elsif project
440 440 project_clauses << "#{Project.table_name}.id = %d" % project.id
441 441 end
442 442 project_clauses.any? ? project_clauses.join(' AND ') : nil
443 443 end
444 444
445 445 def statement
446 446 # filters clauses
447 447 filters_clauses = []
448 448 filters.each_key do |field|
449 449 next if field == "subproject_id"
450 450 v = values_for(field).clone
451 451 next unless v and !v.empty?
452 452 operator = operator_for(field)
453 453
454 454 # "me" value subsitution
455 455 if %w(assigned_to_id author_id watcher_id).include?(field)
456 456 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
457 457 end
458 458
459 459 sql = ''
460 460 if field =~ /^cf_(\d+)$/
461 461 # custom field
462 462 db_table = CustomValue.table_name
463 463 db_field = 'value'
464 464 is_custom_filter = true
465 465 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 "
466 466 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
467 467 elsif field == 'watcher_id'
468 468 db_table = Watcher.table_name
469 469 db_field = 'user_id'
470 470 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
471 471 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
472 472 elsif field == "member_of_group" # named field
473 473 if operator == '*' # Any group
474 474 groups = Group.all
475 475 operator = '=' # Override the operator since we want to find by assigned_to
476 476 elsif operator == "!*"
477 477 groups = Group.all
478 478 operator = '!' # Override the operator since we want to find by assigned_to
479 479 else
480 480 groups = Group.find_all_by_id(v)
481 481 end
482 482 groups ||= []
483 483
484 484 members_of_groups = groups.inject([]) {|user_ids, group|
485 485 if group && group.user_ids.present?
486 486 user_ids << group.user_ids
487 487 end
488 488 user_ids.flatten.uniq.compact
489 489 }.sort.collect(&:to_s)
490 490
491 491 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
492 492
493 493 elsif field == "assigned_to_role" # named field
494 494 if operator == "*" # Any Role
495 495 roles = Role.givable
496 496 operator = '=' # Override the operator since we want to find by assigned_to
497 497 elsif operator == "!*" # No role
498 498 roles = Role.givable
499 499 operator = '!' # Override the operator since we want to find by assigned_to
500 500 else
501 501 roles = Role.givable.find_all_by_id(v)
502 502 end
503 503 roles ||= []
504 504
505 505 members_of_roles = roles.inject([]) {|user_ids, role|
506 506 if role && role.members
507 507 user_ids << role.members.collect(&:user_id)
508 508 end
509 509 user_ids.flatten.uniq.compact
510 510 }.sort.collect(&:to_s)
511 511
512 512 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
513 513 else
514 514 # regular field
515 515 db_table = Issue.table_name
516 516 db_field = field
517 517 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
518 518 end
519 519 filters_clauses << sql
520 520
521 521 end if filters and valid?
522 522
523 523 filters_clauses << project_statement
524 524 filters_clauses.reject!(&:blank?)
525 525
526 526 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
527 527 end
528 528
529 529 # Returns the issue count
530 530 def issue_count
531 531 Issue.count(:include => [:status, :project], :conditions => statement)
532 532 rescue ::ActiveRecord::StatementInvalid => e
533 533 raise StatementInvalid.new(e.message)
534 534 end
535 535
536 536 # Returns the issue count by group or nil if query is not grouped
537 537 def issue_count_by_group
538 538 r = nil
539 539 if grouped?
540 540 begin
541 541 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
542 542 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
543 543 rescue ActiveRecord::RecordNotFound
544 544 r = {nil => issue_count}
545 545 end
546 546 c = group_by_column
547 547 if c.is_a?(QueryCustomFieldColumn)
548 548 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
549 549 end
550 550 end
551 551 r
552 552 rescue ::ActiveRecord::StatementInvalid => e
553 553 raise StatementInvalid.new(e.message)
554 554 end
555 555
556 556 # Returns the issues
557 557 # Valid options are :order, :offset, :limit, :include, :conditions
558 558 def issues(options={})
559 559 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
560 560 order_option = nil if order_option.blank?
561 561
562 562 Issue.visible.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
563 563 :conditions => Query.merge_conditions(statement, options[:conditions]),
564 564 :order => order_option,
565 565 :limit => options[:limit],
566 566 :offset => options[:offset]
567 567 rescue ::ActiveRecord::StatementInvalid => e
568 568 raise StatementInvalid.new(e.message)
569 569 end
570 570
571 571 # Returns the journals
572 572 # Valid options are :order, :offset, :limit
573 573 def journals(options={})
574 574 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
575 575 :conditions => statement,
576 576 :order => options[:order],
577 577 :limit => options[:limit],
578 578 :offset => options[:offset]
579 579 rescue ::ActiveRecord::StatementInvalid => e
580 580 raise StatementInvalid.new(e.message)
581 581 end
582 582
583 583 # Returns the versions
584 584 # Valid options are :conditions
585 585 def versions(options={})
586 586 Version.visible.find :all, :include => :project,
587 587 :conditions => Query.merge_conditions(project_statement, options[:conditions])
588 588 rescue ::ActiveRecord::StatementInvalid => e
589 589 raise StatementInvalid.new(e.message)
590 590 end
591 591
592 592 private
593 593
594 594 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
595 595 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
596 596 sql = ''
597 597 case operator
598 598 when "="
599 599 if value.any?
600 600 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
601 601 else
602 602 # IN an empty set
603 603 sql = "1=0"
604 604 end
605 605 when "!"
606 606 if value.any?
607 607 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
608 608 else
609 609 # NOT IN an empty set
610 610 sql = "1=1"
611 611 end
612 612 when "!*"
613 613 sql = "#{db_table}.#{db_field} IS NULL"
614 614 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
615 615 when "*"
616 616 sql = "#{db_table}.#{db_field} IS NOT NULL"
617 617 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
618 618 when ">="
619 619 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
620 620 when "<="
621 621 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
622 622 when "o"
623 623 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
624 624 when "c"
625 625 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
626 626 when ">t-"
627 627 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
628 628 when "<t-"
629 629 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
630 630 when "t-"
631 631 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
632 632 when ">t+"
633 633 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
634 634 when "<t+"
635 635 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
636 636 when "t+"
637 637 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
638 638 when "t"
639 639 sql = date_range_clause(db_table, db_field, 0, 0)
640 640 when "w"
641 641 first_day_of_week = l(:general_first_day_of_week).to_i
642 642 day_of_week = Date.today.cwday
643 643 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
644 644 sql = date_range_clause(db_table, db_field, - days_ago, - days_ago + 6)
645 645 when "~"
646 646 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
647 647 when "!~"
648 648 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
649 649 end
650 650
651 651 return sql
652 652 end
653 653
654 654 def add_custom_fields_filters(custom_fields)
655 655 @available_filters ||= {}
656 656
657 657 custom_fields.select(&:is_filter?).each do |field|
658 658 case field.field_format
659 659 when "text"
660 660 options = { :type => :text, :order => 20 }
661 661 when "list"
662 662 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
663 663 when "date"
664 664 options = { :type => :date, :order => 20 }
665 665 when "bool"
666 666 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
667 when "int", "float"
668 options = { :type => :integer, :order => 20 }
667 669 when "user", "version"
668 670 next unless project
669 671 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20}
670 672 else
671 673 options = { :type => :string, :order => 20 }
672 674 end
673 675 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
674 676 end
675 677 end
676 678
677 679 # Returns a SQL clause for a date or datetime field.
678 680 def date_range_clause(table, field, from, to)
679 681 s = []
680 682 if from
681 683 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
682 684 end
683 685 if to
684 686 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
685 687 end
686 688 s.join(' AND ')
687 689 end
688 690 end
General Comments 0
You need to be logged in to leave comments. Login now