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