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