##// END OF EJS Templates
Use inheritable class attributes in Query model....
Jean-Philippe Lang -
r10736:fb9a87f53ecd
parent child
Show More
@@ -1,1107 +1,1100
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 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 @inline = options.key?(:inline) ? options[:inline] : true
31 31 @caption_key = options[:caption] || "field_#{name}"
32 32 end
33 33
34 34 def caption
35 35 l(@caption_key)
36 36 end
37 37
38 38 # Returns true if the column is sortable, otherwise false
39 39 def sortable?
40 40 !@sortable.nil?
41 41 end
42 42
43 43 def sortable
44 44 @sortable.is_a?(Proc) ? @sortable.call : @sortable
45 45 end
46 46
47 47 def inline?
48 48 @inline
49 49 end
50 50
51 51 def value(issue)
52 52 issue.send name
53 53 end
54 54
55 55 def css_classes
56 56 name
57 57 end
58 58 end
59 59
60 60 class QueryCustomFieldColumn < QueryColumn
61 61
62 62 def initialize(custom_field)
63 63 self.name = "cf_#{custom_field.id}".to_sym
64 64 self.sortable = custom_field.order_statement || false
65 65 self.groupable = custom_field.group_statement || false
66 66 @inline = true
67 67 @cf = custom_field
68 68 end
69 69
70 70 def caption
71 71 @cf.name
72 72 end
73 73
74 74 def custom_field
75 75 @cf
76 76 end
77 77
78 78 def value(issue)
79 79 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
80 80 cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first
81 81 end
82 82
83 83 def css_classes
84 84 @css_classes ||= "#{name} #{@cf.field_format}"
85 85 end
86 86 end
87 87
88 88 class Query < ActiveRecord::Base
89 89 class StatementInvalid < ::ActiveRecord::StatementInvalid
90 90 end
91 91
92 92 belongs_to :project
93 93 belongs_to :user
94 94 serialize :filters
95 95 serialize :column_names
96 96 serialize :sort_criteria, Array
97 97
98 98 attr_protected :project_id, :user_id
99 99
100 100 validates_presence_of :name
101 101 validates_length_of :name, :maximum => 255
102 102 validate :validate_query_filters
103 103
104 @@operators = { "=" => :label_equals,
105 "!" => :label_not_equals,
106 "o" => :label_open_issues,
107 "c" => :label_closed_issues,
108 "!*" => :label_none,
109 "*" => :label_any,
110 ">=" => :label_greater_or_equal,
111 "<=" => :label_less_or_equal,
112 "><" => :label_between,
113 "<t+" => :label_in_less_than,
114 ">t+" => :label_in_more_than,
115 "><t+"=> :label_in_the_next_days,
116 "t+" => :label_in,
117 "t" => :label_today,
118 "w" => :label_this_week,
119 ">t-" => :label_less_than_ago,
120 "<t-" => :label_more_than_ago,
121 "><t-"=> :label_in_the_past_days,
122 "t-" => :label_ago,
123 "~" => :label_contains,
124 "!~" => :label_not_contains,
125 "=p" => :label_any_issues_in_project,
126 "=!p" => :label_any_issues_not_in_project,
127 "!p" => :label_no_issues_in_project}
128
129 cattr_reader :operators
130
131 @@operators_by_filter_type = { :list => [ "=", "!" ],
132 :list_status => [ "o", "=", "!", "c", "*" ],
133 :list_optional => [ "=", "!", "!*", "*" ],
134 :list_subprojects => [ "*", "!*", "=" ],
135 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "w", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
136 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "w", "!*", "*" ],
137 :string => [ "=", "~", "!", "!~", "!*", "*" ],
138 :text => [ "~", "!~", "!*", "*" ],
139 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
140 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
141 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]}
142
143 cattr_reader :operators_by_filter_type
144
145 @@available_columns = [
104 class_attribute :operators
105 self.operators = {
106 "=" => :label_equals,
107 "!" => :label_not_equals,
108 "o" => :label_open_issues,
109 "c" => :label_closed_issues,
110 "!*" => :label_none,
111 "*" => :label_any,
112 ">=" => :label_greater_or_equal,
113 "<=" => :label_less_or_equal,
114 "><" => :label_between,
115 "<t+" => :label_in_less_than,
116 ">t+" => :label_in_more_than,
117 "><t+"=> :label_in_the_next_days,
118 "t+" => :label_in,
119 "t" => :label_today,
120 "w" => :label_this_week,
121 ">t-" => :label_less_than_ago,
122 "<t-" => :label_more_than_ago,
123 "><t-"=> :label_in_the_past_days,
124 "t-" => :label_ago,
125 "~" => :label_contains,
126 "!~" => :label_not_contains,
127 "=p" => :label_any_issues_in_project,
128 "=!p" => :label_any_issues_not_in_project,
129 "!p" => :label_no_issues_in_project
130 }
131
132 class_attribute :operators_by_filter_type
133 self.operators_by_filter_type = {
134 :list => [ "=", "!" ],
135 :list_status => [ "o", "=", "!", "c", "*" ],
136 :list_optional => [ "=", "!", "!*", "*" ],
137 :list_subprojects => [ "*", "!*", "=" ],
138 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "w", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
139 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "w", "!*", "*" ],
140 :string => [ "=", "~", "!", "!~", "!*", "*" ],
141 :text => [ "~", "!~", "!*", "*" ],
142 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
143 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
144 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
145 }
146
147 class_attribute :available_columns
148 self.available_columns = [
146 149 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
147 150 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
148 151 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
149 152 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
150 153 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
151 154 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
152 155 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
153 156 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
154 157 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
155 158 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
156 159 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
157 160 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
158 161 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
159 162 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
160 163 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
161 164 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
162 165 QueryColumn.new(:relations, :caption => :label_related_issues),
163 166 QueryColumn.new(:description, :inline => false)
164 167 ]
165 cattr_reader :available_columns
166 168
167 169 scope :visible, lambda {|*args|
168 170 user = args.shift || User.current
169 171 base = Project.allowed_to_condition(user, :view_issues, *args)
170 172 user_id = user.logged? ? user.id : 0
171 173
172 174 includes(:project).where("(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id)
173 175 }
174 176
175 177 def initialize(attributes=nil, *args)
176 178 super attributes
177 179 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
178 180 @is_for_all = project.nil?
179 181 end
180 182
181 183 def validate_query_filters
182 184 filters.each_key do |field|
183 185 if values_for(field)
184 186 case type_for(field)
185 187 when :integer
186 188 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
187 189 when :float
188 190 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
189 191 when :date, :date_past
190 192 case operator_for(field)
191 193 when "=", ">=", "<=", "><"
192 194 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
193 195 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
194 196 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
195 197 end
196 198 end
197 199 end
198 200
199 201 add_filter_error(field, :blank) unless
200 202 # filter requires one or more values
201 203 (values_for(field) and !values_for(field).first.blank?) or
202 204 # filter doesn't require any value
203 205 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
204 206 end if filters
205 207 end
206 208
207 209 def add_filter_error(field, message)
208 210 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
209 211 errors.add(:base, m)
210 212 end
211 213
212 214 # Returns true if the query is visible to +user+ or the current user.
213 215 def visible?(user=User.current)
214 216 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
215 217 end
216 218
217 219 def editable_by?(user)
218 220 return false unless user
219 221 # Admin can edit them all and regular users can edit their private queries
220 222 return true if user.admin? || (!is_public && self.user_id == user.id)
221 223 # Members can not edit public queries that are for all project (only admin is allowed to)
222 224 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
223 225 end
224 226
225 227 def trackers
226 228 @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers
227 229 end
228 230
229 231 # Returns a hash of localized labels for all filter operators
230 232 def self.operators_labels
231 233 operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
232 234 end
233 235
234 236 def available_filters
235 237 return @available_filters if @available_filters
236 238 @available_filters = {
237 239 "status_id" => {
238 240 :type => :list_status, :order => 0,
239 241 :values => IssueStatus.sorted.all.collect{|s| [s.name, s.id.to_s] }
240 242 },
241 243 "tracker_id" => {
242 244 :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] }
243 245 },
244 246 "priority_id" => {
245 247 :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
246 248 },
247 249 "subject" => { :type => :text, :order => 8 },
248 250 "created_on" => { :type => :date_past, :order => 9 },
249 251 "updated_on" => { :type => :date_past, :order => 10 },
250 252 "start_date" => { :type => :date, :order => 11 },
251 253 "due_date" => { :type => :date, :order => 12 },
252 254 "estimated_hours" => { :type => :float, :order => 13 },
253 255 "done_ratio" => { :type => :integer, :order => 14 }
254 256 }
255 257 IssueRelation::TYPES.each do |relation_type, options|
256 258 @available_filters[relation_type] = {
257 259 :type => :relation, :order => @available_filters.size + 100,
258 260 :label => options[:name]
259 261 }
260 262 end
261 263 principals = []
262 264 if project
263 265 principals += project.principals.sort
264 266 unless project.leaf?
265 267 subprojects = project.descendants.visible.all
266 268 if subprojects.any?
267 269 @available_filters["subproject_id"] = {
268 270 :type => :list_subprojects, :order => 13,
269 271 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
270 272 }
271 273 principals += Principal.member_of(subprojects)
272 274 end
273 275 end
274 276 else
275 277 if all_projects.any?
276 278 # members of visible projects
277 279 principals += Principal.member_of(all_projects)
278 280 # project filter
279 281 project_values = []
280 282 if User.current.logged? && User.current.memberships.any?
281 283 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
282 284 end
283 285 project_values += all_projects_values
284 286 @available_filters["project_id"] = {
285 287 :type => :list, :order => 1, :values => project_values
286 288 } unless project_values.empty?
287 289 end
288 290 end
289 291 principals.uniq!
290 292 principals.sort!
291 293 users = principals.select {|p| p.is_a?(User)}
292 294
293 295 assigned_to_values = []
294 296 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
295 297 assigned_to_values += (Setting.issue_group_assignment? ?
296 298 principals : users).collect{|s| [s.name, s.id.to_s] }
297 299 @available_filters["assigned_to_id"] = {
298 300 :type => :list_optional, :order => 4, :values => assigned_to_values
299 301 } unless assigned_to_values.empty?
300 302
301 303 author_values = []
302 304 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
303 305 author_values += users.collect{|s| [s.name, s.id.to_s] }
304 306 @available_filters["author_id"] = {
305 307 :type => :list, :order => 5, :values => author_values
306 308 } unless author_values.empty?
307 309
308 310 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
309 311 @available_filters["member_of_group"] = {
310 312 :type => :list_optional, :order => 6, :values => group_values
311 313 } unless group_values.empty?
312 314
313 315 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
314 316 @available_filters["assigned_to_role"] = {
315 317 :type => :list_optional, :order => 7, :values => role_values
316 318 } unless role_values.empty?
317 319
318 320 if User.current.logged?
319 321 @available_filters["watcher_id"] = {
320 322 :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]]
321 323 }
322 324 end
323 325
324 326 if project
325 327 # project specific filters
326 328 categories = project.issue_categories.all
327 329 unless categories.empty?
328 330 @available_filters["category_id"] = {
329 331 :type => :list_optional, :order => 6,
330 332 :values => categories.collect{|s| [s.name, s.id.to_s] }
331 333 }
332 334 end
333 335 versions = project.shared_versions.all
334 336 unless versions.empty?
335 337 @available_filters["fixed_version_id"] = {
336 338 :type => :list_optional, :order => 7,
337 339 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
338 340 }
339 341 end
340 342 add_custom_fields_filters(project.all_issue_custom_fields)
341 343 else
342 344 # global filters for cross project issue list
343 345 system_shared_versions = Version.visible.find_all_by_sharing('system')
344 346 unless system_shared_versions.empty?
345 347 @available_filters["fixed_version_id"] = {
346 348 :type => :list_optional, :order => 7,
347 349 :values => system_shared_versions.sort.collect{|s|
348 350 ["#{s.project.name} - #{s.name}", s.id.to_s]
349 351 }
350 352 }
351 353 end
352 354 add_custom_fields_filters(IssueCustomField.where(:is_filter => true, :is_for_all => true).all)
353 355 end
354 356 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
355 357 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
356 358 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
357 359 @available_filters["is_private"] = {
358 360 :type => :list, :order => 16,
359 361 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
360 362 }
361 363 end
362 364 Tracker.disabled_core_fields(trackers).each {|field|
363 365 @available_filters.delete field
364 366 }
365 367 @available_filters.each do |field, options|
366 368 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
367 369 end
368 370 @available_filters
369 371 end
370 372
371 373 # Returns a representation of the available filters for JSON serialization
372 374 def available_filters_as_json
373 375 json = {}
374 376 available_filters.each do |field, options|
375 377 json[field] = options.slice(:type, :name, :values).stringify_keys
376 378 end
377 379 json
378 380 end
379 381
380 382 def all_projects
381 383 @all_projects ||= Project.visible.all
382 384 end
383 385
384 386 def all_projects_values
385 387 return @all_projects_values if @all_projects_values
386 388
387 389 values = []
388 390 Project.project_tree(all_projects) do |p, level|
389 391 prefix = (level > 0 ? ('--' * level + ' ') : '')
390 392 values << ["#{prefix}#{p.name}", p.id.to_s]
391 393 end
392 394 @all_projects_values = values
393 395 end
394 396
395 397 def add_filter(field, operator, values)
396 398 # values must be an array
397 399 return unless values.nil? || values.is_a?(Array)
398 400 # check if field is defined as an available filter
399 401 if available_filters.has_key? field
400 402 filter_options = available_filters[field]
401 # check if operator is allowed for that filter
402 #if @@operators_by_filter_type[filter_options[:type]].include? operator
403 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
404 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
405 #end
406 403 filters[field] = {:operator => operator, :values => (values || [''])}
407 404 end
408 405 end
409 406
410 407 def add_short_filter(field, expression)
411 408 return unless expression && available_filters.has_key?(field)
412 409 field_type = available_filters[field][:type]
413 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
410 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
414 411 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
415 412 add_filter field, operator, $1.present? ? $1.split('|') : ['']
416 413 end || add_filter(field, '=', expression.split('|'))
417 414 end
418 415
419 416 # Add multiple filters using +add_filter+
420 417 def add_filters(fields, operators, values)
421 418 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
422 419 fields.each do |field|
423 420 add_filter(field, operators[field], values && values[field])
424 421 end
425 422 end
426 423 end
427 424
428 425 def has_filter?(field)
429 426 filters and filters[field]
430 427 end
431 428
432 429 def type_for(field)
433 430 available_filters[field][:type] if available_filters.has_key?(field)
434 431 end
435 432
436 433 def operator_for(field)
437 434 has_filter?(field) ? filters[field][:operator] : nil
438 435 end
439 436
440 437 def values_for(field)
441 438 has_filter?(field) ? filters[field][:values] : nil
442 439 end
443 440
444 441 def value_for(field, index=0)
445 442 (values_for(field) || [])[index]
446 443 end
447 444
448 445 def label_for(field)
449 446 label = available_filters[field][:name] if available_filters.has_key?(field)
450 447 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
451 448 end
452 449
453 450 def available_columns
454 451 return @available_columns if @available_columns
455 452 @available_columns = ::Query.available_columns.dup
456 453 @available_columns += (project ?
457 454 project.all_issue_custom_fields :
458 455 IssueCustomField.all
459 456 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
460 457
461 458 if User.current.allowed_to?(:view_time_entries, project, :global => true)
462 459 index = nil
463 460 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
464 461 index = (index ? index + 1 : -1)
465 462 # insert the column after estimated_hours or at the end
466 463 @available_columns.insert index, QueryColumn.new(:spent_hours,
467 464 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
468 465 :default_order => 'desc',
469 466 :caption => :label_spent_time
470 467 )
471 468 end
472 469
473 470 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
474 471 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
475 472 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
476 473 end
477 474
478 475 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
479 476 @available_columns.reject! {|column|
480 477 disabled_fields.include?(column.name.to_s)
481 478 }
482 479
483 480 @available_columns
484 481 end
485 482
486 def self.available_columns=(v)
487 self.available_columns = (v)
488 end
489
490 483 def self.add_available_column(column)
491 484 self.available_columns << (column) if column.is_a?(QueryColumn)
492 485 end
493 486
494 487 # Returns an array of columns that can be used to group the results
495 488 def groupable_columns
496 489 available_columns.select {|c| c.groupable}
497 490 end
498 491
499 492 # Returns a Hash of columns and the key for sorting
500 493 def sortable_columns
501 494 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
502 495 h[column.name.to_s] = column.sortable
503 496 h
504 497 })
505 498 end
506 499
507 500 def columns
508 501 # preserve the column_names order
509 502 (has_default_columns? ? default_columns_names : column_names).collect do |name|
510 503 available_columns.find { |col| col.name == name }
511 504 end.compact
512 505 end
513 506
514 507 def inline_columns
515 508 columns.select(&:inline?)
516 509 end
517 510
518 511 def block_columns
519 512 columns.reject(&:inline?)
520 513 end
521 514
522 515 def available_inline_columns
523 516 available_columns.select(&:inline?)
524 517 end
525 518
526 519 def available_block_columns
527 520 available_columns.reject(&:inline?)
528 521 end
529 522
530 523 def default_columns_names
531 524 @default_columns_names ||= begin
532 525 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
533 526
534 527 project.present? ? default_columns : [:project] | default_columns
535 528 end
536 529 end
537 530
538 531 def column_names=(names)
539 532 if names
540 533 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
541 534 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
542 535 # Set column_names to nil if default columns
543 536 if names == default_columns_names
544 537 names = nil
545 538 end
546 539 end
547 540 write_attribute(:column_names, names)
548 541 end
549 542
550 543 def has_column?(column)
551 544 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
552 545 end
553 546
554 547 def has_default_columns?
555 548 column_names.nil? || column_names.empty?
556 549 end
557 550
558 551 def sort_criteria=(arg)
559 552 c = []
560 553 if arg.is_a?(Hash)
561 554 arg = arg.keys.sort.collect {|k| arg[k]}
562 555 end
563 556 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
564 557 write_attribute(:sort_criteria, c)
565 558 end
566 559
567 560 def sort_criteria
568 561 read_attribute(:sort_criteria) || []
569 562 end
570 563
571 564 def sort_criteria_key(arg)
572 565 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
573 566 end
574 567
575 568 def sort_criteria_order(arg)
576 569 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
577 570 end
578 571
579 572 def sort_criteria_order_for(key)
580 573 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
581 574 end
582 575
583 576 # Returns the SQL sort order that should be prepended for grouping
584 577 def group_by_sort_order
585 578 if grouped? && (column = group_by_column)
586 579 order = sort_criteria_order_for(column.name) || column.default_order
587 580 column.sortable.is_a?(Array) ?
588 581 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
589 582 "#{column.sortable} #{order}"
590 583 end
591 584 end
592 585
593 586 # Returns true if the query is a grouped query
594 587 def grouped?
595 588 !group_by_column.nil?
596 589 end
597 590
598 591 def group_by_column
599 592 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
600 593 end
601 594
602 595 def group_by_statement
603 596 group_by_column.try(:groupable)
604 597 end
605 598
606 599 def project_statement
607 600 project_clauses = []
608 601 if project && !project.descendants.active.empty?
609 602 ids = [project.id]
610 603 if has_filter?("subproject_id")
611 604 case operator_for("subproject_id")
612 605 when '='
613 606 # include the selected subprojects
614 607 ids += values_for("subproject_id").each(&:to_i)
615 608 when '!*'
616 609 # main project only
617 610 else
618 611 # all subprojects
619 612 ids += project.descendants.collect(&:id)
620 613 end
621 614 elsif Setting.display_subprojects_issues?
622 615 ids += project.descendants.collect(&:id)
623 616 end
624 617 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
625 618 elsif project
626 619 project_clauses << "#{Project.table_name}.id = %d" % project.id
627 620 end
628 621 project_clauses.any? ? project_clauses.join(' AND ') : nil
629 622 end
630 623
631 624 def statement
632 625 # filters clauses
633 626 filters_clauses = []
634 627 filters.each_key do |field|
635 628 next if field == "subproject_id"
636 629 v = values_for(field).clone
637 630 next unless v and !v.empty?
638 631 operator = operator_for(field)
639 632
640 633 # "me" value subsitution
641 634 if %w(assigned_to_id author_id watcher_id).include?(field)
642 635 if v.delete("me")
643 636 if User.current.logged?
644 637 v.push(User.current.id.to_s)
645 638 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
646 639 else
647 640 v.push("0")
648 641 end
649 642 end
650 643 end
651 644
652 645 if field == 'project_id'
653 646 if v.delete('mine')
654 647 v += User.current.memberships.map(&:project_id).map(&:to_s)
655 648 end
656 649 end
657 650
658 651 if field =~ /cf_(\d+)$/
659 652 # custom field
660 653 filters_clauses << sql_for_custom_field(field, operator, v, $1)
661 654 elsif respond_to?("sql_for_#{field}_field")
662 655 # specific statement
663 656 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
664 657 else
665 658 # regular field
666 659 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
667 660 end
668 661 end if filters and valid?
669 662
670 663 filters_clauses << project_statement
671 664 filters_clauses.reject!(&:blank?)
672 665
673 666 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
674 667 end
675 668
676 669 # Returns the issue count
677 670 def issue_count
678 671 Issue.visible.count(:include => [:status, :project], :conditions => statement)
679 672 rescue ::ActiveRecord::StatementInvalid => e
680 673 raise StatementInvalid.new(e.message)
681 674 end
682 675
683 676 # Returns the issue count by group or nil if query is not grouped
684 677 def issue_count_by_group
685 678 r = nil
686 679 if grouped?
687 680 begin
688 681 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
689 682 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
690 683 rescue ActiveRecord::RecordNotFound
691 684 r = {nil => issue_count}
692 685 end
693 686 c = group_by_column
694 687 if c.is_a?(QueryCustomFieldColumn)
695 688 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
696 689 end
697 690 end
698 691 r
699 692 rescue ::ActiveRecord::StatementInvalid => e
700 693 raise StatementInvalid.new(e.message)
701 694 end
702 695
703 696 # Returns the issues
704 697 # Valid options are :order, :offset, :limit, :include, :conditions
705 698 def issues(options={})
706 699 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
707 700 order_option = nil if order_option.blank?
708 701
709 702 issues = Issue.visible.where(options[:conditions]).all(
710 703 :include => ([:status, :project] + (options[:include] || [])).uniq,
711 704 :conditions => statement,
712 705 :order => order_option,
713 706 :joins => joins_for_order_statement(order_option),
714 707 :limit => options[:limit],
715 708 :offset => options[:offset]
716 709 )
717 710
718 711 if has_column?(:spent_hours)
719 712 Issue.load_visible_spent_hours(issues)
720 713 end
721 714 if has_column?(:relations)
722 715 Issue.load_visible_relations(issues)
723 716 end
724 717 issues
725 718 rescue ::ActiveRecord::StatementInvalid => e
726 719 raise StatementInvalid.new(e.message)
727 720 end
728 721
729 722 # Returns the issues ids
730 723 def issue_ids(options={})
731 724 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
732 725 order_option = nil if order_option.blank?
733 726
734 727 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
735 728 :conditions => statement,
736 729 :order => order_option,
737 730 :joins => joins_for_order_statement(order_option),
738 731 :limit => options[:limit],
739 732 :offset => options[:offset]).find_ids
740 733 rescue ::ActiveRecord::StatementInvalid => e
741 734 raise StatementInvalid.new(e.message)
742 735 end
743 736
744 737 # Returns the journals
745 738 # Valid options are :order, :offset, :limit
746 739 def journals(options={})
747 740 Journal.visible.all(
748 741 :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
749 742 :conditions => statement,
750 743 :order => options[:order],
751 744 :limit => options[:limit],
752 745 :offset => options[:offset]
753 746 )
754 747 rescue ::ActiveRecord::StatementInvalid => e
755 748 raise StatementInvalid.new(e.message)
756 749 end
757 750
758 751 # Returns the versions
759 752 # Valid options are :conditions
760 753 def versions(options={})
761 754 Version.visible.where(options[:conditions]).all(
762 755 :include => :project,
763 756 :conditions => project_statement
764 757 )
765 758 rescue ::ActiveRecord::StatementInvalid => e
766 759 raise StatementInvalid.new(e.message)
767 760 end
768 761
769 762 def sql_for_watcher_id_field(field, operator, value)
770 763 db_table = Watcher.table_name
771 764 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
772 765 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
773 766 end
774 767
775 768 def sql_for_member_of_group_field(field, operator, value)
776 769 if operator == '*' # Any group
777 770 groups = Group.all
778 771 operator = '=' # Override the operator since we want to find by assigned_to
779 772 elsif operator == "!*"
780 773 groups = Group.all
781 774 operator = '!' # Override the operator since we want to find by assigned_to
782 775 else
783 776 groups = Group.find_all_by_id(value)
784 777 end
785 778 groups ||= []
786 779
787 780 members_of_groups = groups.inject([]) {|user_ids, group|
788 781 if group && group.user_ids.present?
789 782 user_ids << group.user_ids
790 783 end
791 784 user_ids.flatten.uniq.compact
792 785 }.sort.collect(&:to_s)
793 786
794 787 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
795 788 end
796 789
797 790 def sql_for_assigned_to_role_field(field, operator, value)
798 791 case operator
799 792 when "*", "!*" # Member / Not member
800 793 sw = operator == "!*" ? 'NOT' : ''
801 794 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
802 795 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
803 796 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
804 797 when "=", "!"
805 798 role_cond = value.any? ?
806 799 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
807 800 "1=0"
808 801
809 802 sw = operator == "!" ? 'NOT' : ''
810 803 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
811 804 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
812 805 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
813 806 end
814 807 end
815 808
816 809 def sql_for_is_private_field(field, operator, value)
817 810 op = (operator == "=" ? 'IN' : 'NOT IN')
818 811 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
819 812
820 813 "#{Issue.table_name}.is_private #{op} (#{va})"
821 814 end
822 815
823 816 def sql_for_relations(field, operator, value, options={})
824 817 relation_options = IssueRelation::TYPES[field]
825 818 return relation_options unless relation_options
826 819
827 820 relation_type = field
828 821 join_column, target_join_column = "issue_from_id", "issue_to_id"
829 822 if relation_options[:reverse] || options[:reverse]
830 823 relation_type = relation_options[:reverse] || relation_type
831 824 join_column, target_join_column = target_join_column, join_column
832 825 end
833 826
834 827 sql = case operator
835 828 when "*", "!*"
836 829 op = (operator == "*" ? 'IN' : 'NOT IN')
837 830 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}')"
838 831 when "=", "!"
839 832 op = (operator == "=" ? 'IN' : 'NOT IN')
840 833 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
841 834 when "=p", "=!p", "!p"
842 835 op = (operator == "!p" ? 'NOT IN' : 'IN')
843 836 comp = (operator == "=!p" ? '<>' : '=')
844 837 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
845 838 end
846 839
847 840 if relation_options[:sym] == field && !options[:reverse]
848 841 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
849 842 sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
850 843 else
851 844 sql
852 845 end
853 846 end
854 847
855 848 IssueRelation::TYPES.keys.each do |relation_type|
856 849 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
857 850 end
858 851
859 852 private
860 853
861 854 def sql_for_custom_field(field, operator, value, custom_field_id)
862 855 db_table = CustomValue.table_name
863 856 db_field = 'value'
864 857 filter = @available_filters[field]
865 858 return nil unless filter
866 859 if filter[:format] == 'user'
867 860 if value.delete('me')
868 861 value.push User.current.id.to_s
869 862 end
870 863 end
871 864 not_in = nil
872 865 if operator == '!'
873 866 # Makes ! operator work for custom fields with multiple values
874 867 operator = '='
875 868 not_in = 'NOT'
876 869 end
877 870 customized_key = "id"
878 871 customized_class = Issue
879 872 if field =~ /^(.+)\.cf_/
880 873 assoc = $1
881 874 customized_key = "#{assoc}_id"
882 875 customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
883 876 raise "Unknown Issue association #{assoc}" unless customized_class
884 877 end
885 878 "#{Issue.table_name}.#{customized_key} #{not_in} IN (SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
886 879 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
887 880 end
888 881
889 882 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
890 883 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
891 884 sql = ''
892 885 case operator
893 886 when "="
894 887 if value.any?
895 888 case type_for(field)
896 889 when :date, :date_past
897 890 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
898 891 when :integer
899 892 if is_custom_filter
900 893 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
901 894 else
902 895 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
903 896 end
904 897 when :float
905 898 if is_custom_filter
906 899 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
907 900 else
908 901 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
909 902 end
910 903 else
911 904 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
912 905 end
913 906 else
914 907 # IN an empty set
915 908 sql = "1=0"
916 909 end
917 910 when "!"
918 911 if value.any?
919 912 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
920 913 else
921 914 # NOT IN an empty set
922 915 sql = "1=1"
923 916 end
924 917 when "!*"
925 918 sql = "#{db_table}.#{db_field} IS NULL"
926 919 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
927 920 when "*"
928 921 sql = "#{db_table}.#{db_field} IS NOT NULL"
929 922 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
930 923 when ">="
931 924 if [:date, :date_past].include?(type_for(field))
932 925 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
933 926 else
934 927 if is_custom_filter
935 928 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
936 929 else
937 930 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
938 931 end
939 932 end
940 933 when "<="
941 934 if [:date, :date_past].include?(type_for(field))
942 935 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
943 936 else
944 937 if is_custom_filter
945 938 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
946 939 else
947 940 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
948 941 end
949 942 end
950 943 when "><"
951 944 if [:date, :date_past].include?(type_for(field))
952 945 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
953 946 else
954 947 if is_custom_filter
955 948 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
956 949 else
957 950 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
958 951 end
959 952 end
960 953 when "o"
961 954 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
962 955 when "c"
963 956 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
964 957 when "><t-"
965 958 # between today - n days and today
966 959 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
967 960 when ">t-"
968 961 # >= today - n days
969 962 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
970 963 when "<t-"
971 964 # <= today - n days
972 965 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
973 966 when "t-"
974 967 # = n days in past
975 968 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
976 969 when "><t+"
977 970 # between today and today + n days
978 971 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
979 972 when ">t+"
980 973 # >= today + n days
981 974 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
982 975 when "<t+"
983 976 # <= today + n days
984 977 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
985 978 when "t+"
986 979 # = today + n days
987 980 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
988 981 when "t"
989 982 # = today
990 983 sql = relative_date_clause(db_table, db_field, 0, 0)
991 984 when "w"
992 985 # = this week
993 986 first_day_of_week = l(:general_first_day_of_week).to_i
994 987 day_of_week = Date.today.cwday
995 988 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
996 989 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
997 990 when "~"
998 991 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
999 992 when "!~"
1000 993 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
1001 994 else
1002 995 raise "Unknown query operator #{operator}"
1003 996 end
1004 997
1005 998 return sql
1006 999 end
1007 1000
1008 1001 def add_custom_fields_filters(custom_fields, assoc=nil)
1009 1002 return unless custom_fields.present?
1010 1003 @available_filters ||= {}
1011 1004
1012 1005 custom_fields.select(&:is_filter?).each do |field|
1013 1006 case field.field_format
1014 1007 when "text"
1015 1008 options = { :type => :text, :order => 20 }
1016 1009 when "list"
1017 1010 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
1018 1011 when "date"
1019 1012 options = { :type => :date, :order => 20 }
1020 1013 when "bool"
1021 1014 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
1022 1015 when "int"
1023 1016 options = { :type => :integer, :order => 20 }
1024 1017 when "float"
1025 1018 options = { :type => :float, :order => 20 }
1026 1019 when "user", "version"
1027 1020 next unless project
1028 1021 values = field.possible_values_options(project)
1029 1022 if User.current.logged? && field.field_format == 'user'
1030 1023 values.unshift ["<< #{l(:label_me)} >>", "me"]
1031 1024 end
1032 1025 options = { :type => :list_optional, :values => values, :order => 20}
1033 1026 else
1034 1027 options = { :type => :string, :order => 20 }
1035 1028 end
1036 1029 filter_id = "cf_#{field.id}"
1037 1030 filter_name = field.name
1038 1031 if assoc.present?
1039 1032 filter_id = "#{assoc}.#{filter_id}"
1040 1033 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
1041 1034 end
1042 1035 @available_filters[filter_id] = options.merge({
1043 1036 :name => filter_name,
1044 1037 :format => field.field_format,
1045 1038 :field => field
1046 1039 })
1047 1040 end
1048 1041 end
1049 1042
1050 1043 def add_associations_custom_fields_filters(*associations)
1051 1044 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
1052 1045 associations.each do |assoc|
1053 1046 association_klass = Issue.reflect_on_association(assoc).klass
1054 1047 fields_by_class.each do |field_class, fields|
1055 1048 if field_class.customized_class <= association_klass
1056 1049 add_custom_fields_filters(fields, assoc)
1057 1050 end
1058 1051 end
1059 1052 end
1060 1053 end
1061 1054
1062 1055 # Returns a SQL clause for a date or datetime field.
1063 1056 def date_clause(table, field, from, to)
1064 1057 s = []
1065 1058 if from
1066 1059 from_yesterday = from - 1
1067 1060 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
1068 1061 if self.class.default_timezone == :utc
1069 1062 from_yesterday_time = from_yesterday_time.utc
1070 1063 end
1071 1064 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
1072 1065 end
1073 1066 if to
1074 1067 to_time = Time.local(to.year, to.month, to.day)
1075 1068 if self.class.default_timezone == :utc
1076 1069 to_time = to_time.utc
1077 1070 end
1078 1071 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
1079 1072 end
1080 1073 s.join(' AND ')
1081 1074 end
1082 1075
1083 1076 # Returns a SQL clause for a date or datetime field using relative dates.
1084 1077 def relative_date_clause(table, field, days_from, days_to)
1085 1078 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
1086 1079 end
1087 1080
1088 1081 # Additional joins required for the given sort options
1089 1082 def joins_for_order_statement(order_options)
1090 1083 joins = []
1091 1084
1092 1085 if order_options
1093 1086 if order_options.include?('authors')
1094 1087 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id"
1095 1088 end
1096 1089 order_options.scan(/cf_\d+/).uniq.each do |name|
1097 1090 column = available_columns.detect {|c| c.name.to_s == name}
1098 1091 join = column && column.custom_field.join_for_order_statement
1099 1092 if join
1100 1093 joins << join
1101 1094 end
1102 1095 end
1103 1096 end
1104 1097
1105 1098 joins.any? ? joins.join(' ') : nil
1106 1099 end
1107 1100 end
General Comments 0
You need to be logged in to leave comments. Login now