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