##// END OF EJS Templates
Don't call #upcase on nil....
Jean-Philippe Lang -
r13612:2b124f1e61da
parent child
Show More
@@ -1,901 +1,901
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class QueryColumn
18 class QueryColumn
19 attr_accessor :name, :sortable, :groupable, :default_order
19 attr_accessor :name, :sortable, :groupable, :default_order
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 def initialize(name, options={})
22 def initialize(name, options={})
23 self.name = name
23 self.name = name
24 self.sortable = options[:sortable]
24 self.sortable = options[:sortable]
25 self.groupable = options[:groupable] || false
25 self.groupable = options[:groupable] || false
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}".to_sym
31 @caption_key = options[:caption] || "field_#{name}".to_sym
32 @frozen = options[:frozen]
32 @frozen = options[:frozen]
33 end
33 end
34
34
35 def caption
35 def caption
36 case @caption_key
36 case @caption_key
37 when Symbol
37 when Symbol
38 l(@caption_key)
38 l(@caption_key)
39 when Proc
39 when Proc
40 @caption_key.call
40 @caption_key.call
41 else
41 else
42 @caption_key
42 @caption_key
43 end
43 end
44 end
44 end
45
45
46 # Returns true if the column is sortable, otherwise false
46 # Returns true if the column is sortable, otherwise false
47 def sortable?
47 def sortable?
48 !@sortable.nil?
48 !@sortable.nil?
49 end
49 end
50
50
51 def sortable
51 def sortable
52 @sortable.is_a?(Proc) ? @sortable.call : @sortable
52 @sortable.is_a?(Proc) ? @sortable.call : @sortable
53 end
53 end
54
54
55 def inline?
55 def inline?
56 @inline
56 @inline
57 end
57 end
58
58
59 def frozen?
59 def frozen?
60 @frozen
60 @frozen
61 end
61 end
62
62
63 def value(object)
63 def value(object)
64 object.send name
64 object.send name
65 end
65 end
66
66
67 def value_object(object)
67 def value_object(object)
68 object.send name
68 object.send name
69 end
69 end
70
70
71 def css_classes
71 def css_classes
72 name
72 name
73 end
73 end
74 end
74 end
75
75
76 class QueryCustomFieldColumn < QueryColumn
76 class QueryCustomFieldColumn < QueryColumn
77
77
78 def initialize(custom_field)
78 def initialize(custom_field)
79 self.name = "cf_#{custom_field.id}".to_sym
79 self.name = "cf_#{custom_field.id}".to_sym
80 self.sortable = custom_field.order_statement || false
80 self.sortable = custom_field.order_statement || false
81 self.groupable = custom_field.group_statement || false
81 self.groupable = custom_field.group_statement || false
82 @inline = true
82 @inline = true
83 @cf = custom_field
83 @cf = custom_field
84 end
84 end
85
85
86 def caption
86 def caption
87 @cf.name
87 @cf.name
88 end
88 end
89
89
90 def custom_field
90 def custom_field
91 @cf
91 @cf
92 end
92 end
93
93
94 def value_object(object)
94 def value_object(object)
95 if custom_field.visible_by?(object.project, User.current)
95 if custom_field.visible_by?(object.project, User.current)
96 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
96 cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
97 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
97 cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first
98 else
98 else
99 nil
99 nil
100 end
100 end
101 end
101 end
102
102
103 def value(object)
103 def value(object)
104 raw = value_object(object)
104 raw = value_object(object)
105 if raw.is_a?(Array)
105 if raw.is_a?(Array)
106 raw.map {|r| @cf.cast_value(r.value)}
106 raw.map {|r| @cf.cast_value(r.value)}
107 elsif raw
107 elsif raw
108 @cf.cast_value(raw.value)
108 @cf.cast_value(raw.value)
109 else
109 else
110 nil
110 nil
111 end
111 end
112 end
112 end
113
113
114 def css_classes
114 def css_classes
115 @css_classes ||= "#{name} #{@cf.field_format}"
115 @css_classes ||= "#{name} #{@cf.field_format}"
116 end
116 end
117 end
117 end
118
118
119 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
119 class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn
120
120
121 def initialize(association, custom_field)
121 def initialize(association, custom_field)
122 super(custom_field)
122 super(custom_field)
123 self.name = "#{association}.cf_#{custom_field.id}".to_sym
123 self.name = "#{association}.cf_#{custom_field.id}".to_sym
124 # TODO: support sorting/grouping by association custom field
124 # TODO: support sorting/grouping by association custom field
125 self.sortable = false
125 self.sortable = false
126 self.groupable = false
126 self.groupable = false
127 @association = association
127 @association = association
128 end
128 end
129
129
130 def value_object(object)
130 def value_object(object)
131 if assoc = object.send(@association)
131 if assoc = object.send(@association)
132 super(assoc)
132 super(assoc)
133 end
133 end
134 end
134 end
135
135
136 def css_classes
136 def css_classes
137 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
137 @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}"
138 end
138 end
139 end
139 end
140
140
141 class Query < ActiveRecord::Base
141 class Query < ActiveRecord::Base
142 class StatementInvalid < ::ActiveRecord::StatementInvalid
142 class StatementInvalid < ::ActiveRecord::StatementInvalid
143 end
143 end
144
144
145 VISIBILITY_PRIVATE = 0
145 VISIBILITY_PRIVATE = 0
146 VISIBILITY_ROLES = 1
146 VISIBILITY_ROLES = 1
147 VISIBILITY_PUBLIC = 2
147 VISIBILITY_PUBLIC = 2
148
148
149 belongs_to :project
149 belongs_to :project
150 belongs_to :user
150 belongs_to :user
151 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
151 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id"
152 serialize :filters
152 serialize :filters
153 serialize :column_names
153 serialize :column_names
154 serialize :sort_criteria, Array
154 serialize :sort_criteria, Array
155 serialize :options, Hash
155 serialize :options, Hash
156
156
157 attr_protected :project_id, :user_id
157 attr_protected :project_id, :user_id
158
158
159 validates_presence_of :name
159 validates_presence_of :name
160 validates_length_of :name, :maximum => 255
160 validates_length_of :name, :maximum => 255
161 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
161 validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] }
162 validate :validate_query_filters
162 validate :validate_query_filters
163 validate do |query|
163 validate do |query|
164 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
164 errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank?
165 end
165 end
166
166
167 after_save do |query|
167 after_save do |query|
168 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
168 if query.visibility_changed? && query.visibility != VISIBILITY_ROLES
169 query.roles.clear
169 query.roles.clear
170 end
170 end
171 end
171 end
172
172
173 class_attribute :operators
173 class_attribute :operators
174 self.operators = {
174 self.operators = {
175 "=" => :label_equals,
175 "=" => :label_equals,
176 "!" => :label_not_equals,
176 "!" => :label_not_equals,
177 "o" => :label_open_issues,
177 "o" => :label_open_issues,
178 "c" => :label_closed_issues,
178 "c" => :label_closed_issues,
179 "!*" => :label_none,
179 "!*" => :label_none,
180 "*" => :label_any,
180 "*" => :label_any,
181 ">=" => :label_greater_or_equal,
181 ">=" => :label_greater_or_equal,
182 "<=" => :label_less_or_equal,
182 "<=" => :label_less_or_equal,
183 "><" => :label_between,
183 "><" => :label_between,
184 "<t+" => :label_in_less_than,
184 "<t+" => :label_in_less_than,
185 ">t+" => :label_in_more_than,
185 ">t+" => :label_in_more_than,
186 "><t+"=> :label_in_the_next_days,
186 "><t+"=> :label_in_the_next_days,
187 "t+" => :label_in,
187 "t+" => :label_in,
188 "t" => :label_today,
188 "t" => :label_today,
189 "ld" => :label_yesterday,
189 "ld" => :label_yesterday,
190 "w" => :label_this_week,
190 "w" => :label_this_week,
191 "lw" => :label_last_week,
191 "lw" => :label_last_week,
192 "l2w" => [:label_last_n_weeks, {:count => 2}],
192 "l2w" => [:label_last_n_weeks, {:count => 2}],
193 "m" => :label_this_month,
193 "m" => :label_this_month,
194 "lm" => :label_last_month,
194 "lm" => :label_last_month,
195 "y" => :label_this_year,
195 "y" => :label_this_year,
196 ">t-" => :label_less_than_ago,
196 ">t-" => :label_less_than_ago,
197 "<t-" => :label_more_than_ago,
197 "<t-" => :label_more_than_ago,
198 "><t-"=> :label_in_the_past_days,
198 "><t-"=> :label_in_the_past_days,
199 "t-" => :label_ago,
199 "t-" => :label_ago,
200 "~" => :label_contains,
200 "~" => :label_contains,
201 "!~" => :label_not_contains,
201 "!~" => :label_not_contains,
202 "=p" => :label_any_issues_in_project,
202 "=p" => :label_any_issues_in_project,
203 "=!p" => :label_any_issues_not_in_project,
203 "=!p" => :label_any_issues_not_in_project,
204 "!p" => :label_no_issues_in_project
204 "!p" => :label_no_issues_in_project
205 }
205 }
206
206
207 class_attribute :operators_by_filter_type
207 class_attribute :operators_by_filter_type
208 self.operators_by_filter_type = {
208 self.operators_by_filter_type = {
209 :list => [ "=", "!" ],
209 :list => [ "=", "!" ],
210 :list_status => [ "o", "=", "!", "c", "*" ],
210 :list_status => [ "o", "=", "!", "c", "*" ],
211 :list_optional => [ "=", "!", "!*", "*" ],
211 :list_optional => [ "=", "!", "!*", "*" ],
212 :list_subprojects => [ "*", "!*", "=" ],
212 :list_subprojects => [ "*", "!*", "=" ],
213 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
213 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
214 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
214 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
215 :string => [ "=", "~", "!", "!~", "!*", "*" ],
215 :string => [ "=", "~", "!", "!~", "!*", "*" ],
216 :text => [ "~", "!~", "!*", "*" ],
216 :text => [ "~", "!~", "!*", "*" ],
217 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
217 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
218 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
218 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
219 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
219 :relation => ["=", "=p", "=!p", "!p", "!*", "*"]
220 }
220 }
221
221
222 class_attribute :available_columns
222 class_attribute :available_columns
223 self.available_columns = []
223 self.available_columns = []
224
224
225 class_attribute :queried_class
225 class_attribute :queried_class
226
226
227 def queried_table_name
227 def queried_table_name
228 @queried_table_name ||= self.class.queried_class.table_name
228 @queried_table_name ||= self.class.queried_class.table_name
229 end
229 end
230
230
231 def initialize(attributes=nil, *args)
231 def initialize(attributes=nil, *args)
232 super attributes
232 super attributes
233 @is_for_all = project.nil?
233 @is_for_all = project.nil?
234 end
234 end
235
235
236 # Builds the query from the given params
236 # Builds the query from the given params
237 def build_from_params(params)
237 def build_from_params(params)
238 if params[:fields] || params[:f]
238 if params[:fields] || params[:f]
239 self.filters = {}
239 self.filters = {}
240 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
240 add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
241 else
241 else
242 available_filters.keys.each do |field|
242 available_filters.keys.each do |field|
243 add_short_filter(field, params[field]) if params[field]
243 add_short_filter(field, params[field]) if params[field]
244 end
244 end
245 end
245 end
246 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
246 self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
247 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
247 self.column_names = params[:c] || (params[:query] && params[:query][:column_names])
248 self
248 self
249 end
249 end
250
250
251 # Builds a new query from the given params and attributes
251 # Builds a new query from the given params and attributes
252 def self.build_from_params(params, attributes={})
252 def self.build_from_params(params, attributes={})
253 new(attributes).build_from_params(params)
253 new(attributes).build_from_params(params)
254 end
254 end
255
255
256 def validate_query_filters
256 def validate_query_filters
257 filters.each_key do |field|
257 filters.each_key do |field|
258 if values_for(field)
258 if values_for(field)
259 case type_for(field)
259 case type_for(field)
260 when :integer
260 when :integer
261 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
261 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
262 when :float
262 when :float
263 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
263 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
264 when :date, :date_past
264 when :date, :date_past
265 case operator_for(field)
265 case operator_for(field)
266 when "=", ">=", "<=", "><"
266 when "=", ">=", "<=", "><"
267 add_filter_error(field, :invalid) if values_for(field).detect {|v|
267 add_filter_error(field, :invalid) if values_for(field).detect {|v|
268 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
268 v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?)
269 }
269 }
270 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
270 when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-"
271 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
271 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
272 end
272 end
273 end
273 end
274 end
274 end
275
275
276 add_filter_error(field, :blank) unless
276 add_filter_error(field, :blank) unless
277 # filter requires one or more values
277 # filter requires one or more values
278 (values_for(field) and !values_for(field).first.blank?) or
278 (values_for(field) and !values_for(field).first.blank?) or
279 # filter doesn't require any value
279 # filter doesn't require any value
280 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y"].include? operator_for(field)
280 ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y"].include? operator_for(field)
281 end if filters
281 end if filters
282 end
282 end
283
283
284 def add_filter_error(field, message)
284 def add_filter_error(field, message)
285 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
285 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
286 errors.add(:base, m)
286 errors.add(:base, m)
287 end
287 end
288
288
289 def editable_by?(user)
289 def editable_by?(user)
290 return false unless user
290 return false unless user
291 # Admin can edit them all and regular users can edit their private queries
291 # Admin can edit them all and regular users can edit their private queries
292 return true if user.admin? || (is_private? && self.user_id == user.id)
292 return true if user.admin? || (is_private? && self.user_id == user.id)
293 # Members can not edit public queries that are for all project (only admin is allowed to)
293 # Members can not edit public queries that are for all project (only admin is allowed to)
294 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
294 is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
295 end
295 end
296
296
297 def trackers
297 def trackers
298 @trackers ||= project.nil? ? Tracker.sorted.to_a : project.rolled_up_trackers
298 @trackers ||= project.nil? ? Tracker.sorted.to_a : project.rolled_up_trackers
299 end
299 end
300
300
301 # Returns a hash of localized labels for all filter operators
301 # Returns a hash of localized labels for all filter operators
302 def self.operators_labels
302 def self.operators_labels
303 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
303 operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h}
304 end
304 end
305
305
306 # Returns a representation of the available filters for JSON serialization
306 # Returns a representation of the available filters for JSON serialization
307 def available_filters_as_json
307 def available_filters_as_json
308 json = {}
308 json = {}
309 available_filters.each do |field, options|
309 available_filters.each do |field, options|
310 json[field] = options.slice(:type, :name, :values).stringify_keys
310 json[field] = options.slice(:type, :name, :values).stringify_keys
311 end
311 end
312 json
312 json
313 end
313 end
314
314
315 def all_projects
315 def all_projects
316 @all_projects ||= Project.visible.to_a
316 @all_projects ||= Project.visible.to_a
317 end
317 end
318
318
319 def all_projects_values
319 def all_projects_values
320 return @all_projects_values if @all_projects_values
320 return @all_projects_values if @all_projects_values
321
321
322 values = []
322 values = []
323 Project.project_tree(all_projects) do |p, level|
323 Project.project_tree(all_projects) do |p, level|
324 prefix = (level > 0 ? ('--' * level + ' ') : '')
324 prefix = (level > 0 ? ('--' * level + ' ') : '')
325 values << ["#{prefix}#{p.name}", p.id.to_s]
325 values << ["#{prefix}#{p.name}", p.id.to_s]
326 end
326 end
327 @all_projects_values = values
327 @all_projects_values = values
328 end
328 end
329
329
330 # Adds available filters
330 # Adds available filters
331 def initialize_available_filters
331 def initialize_available_filters
332 # implemented by sub-classes
332 # implemented by sub-classes
333 end
333 end
334 protected :initialize_available_filters
334 protected :initialize_available_filters
335
335
336 # Adds an available filter
336 # Adds an available filter
337 def add_available_filter(field, options)
337 def add_available_filter(field, options)
338 @available_filters ||= ActiveSupport::OrderedHash.new
338 @available_filters ||= ActiveSupport::OrderedHash.new
339 @available_filters[field] = options
339 @available_filters[field] = options
340 @available_filters
340 @available_filters
341 end
341 end
342
342
343 # Removes an available filter
343 # Removes an available filter
344 def delete_available_filter(field)
344 def delete_available_filter(field)
345 if @available_filters
345 if @available_filters
346 @available_filters.delete(field)
346 @available_filters.delete(field)
347 end
347 end
348 end
348 end
349
349
350 # Return a hash of available filters
350 # Return a hash of available filters
351 def available_filters
351 def available_filters
352 unless @available_filters
352 unless @available_filters
353 initialize_available_filters
353 initialize_available_filters
354 @available_filters.each do |field, options|
354 @available_filters.each do |field, options|
355 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
355 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
356 end
356 end
357 end
357 end
358 @available_filters
358 @available_filters
359 end
359 end
360
360
361 def add_filter(field, operator, values=nil)
361 def add_filter(field, operator, values=nil)
362 # values must be an array
362 # values must be an array
363 return unless values.nil? || values.is_a?(Array)
363 return unless values.nil? || values.is_a?(Array)
364 # check if field is defined as an available filter
364 # check if field is defined as an available filter
365 if available_filters.has_key? field
365 if available_filters.has_key? field
366 filter_options = available_filters[field]
366 filter_options = available_filters[field]
367 filters[field] = {:operator => operator, :values => (values || [''])}
367 filters[field] = {:operator => operator, :values => (values || [''])}
368 end
368 end
369 end
369 end
370
370
371 def add_short_filter(field, expression)
371 def add_short_filter(field, expression)
372 return unless expression && available_filters.has_key?(field)
372 return unless expression && available_filters.has_key?(field)
373 field_type = available_filters[field][:type]
373 field_type = available_filters[field][:type]
374 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
374 operators_by_filter_type[field_type].sort.reverse.detect do |operator|
375 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
375 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
376 values = $1
376 values = $1
377 add_filter field, operator, values.present? ? values.split('|') : ['']
377 add_filter field, operator, values.present? ? values.split('|') : ['']
378 end || add_filter(field, '=', expression.split('|'))
378 end || add_filter(field, '=', expression.split('|'))
379 end
379 end
380
380
381 # Add multiple filters using +add_filter+
381 # Add multiple filters using +add_filter+
382 def add_filters(fields, operators, values)
382 def add_filters(fields, operators, values)
383 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
383 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
384 fields.each do |field|
384 fields.each do |field|
385 add_filter(field, operators[field], values && values[field])
385 add_filter(field, operators[field], values && values[field])
386 end
386 end
387 end
387 end
388 end
388 end
389
389
390 def has_filter?(field)
390 def has_filter?(field)
391 filters and filters[field]
391 filters and filters[field]
392 end
392 end
393
393
394 def type_for(field)
394 def type_for(field)
395 available_filters[field][:type] if available_filters.has_key?(field)
395 available_filters[field][:type] if available_filters.has_key?(field)
396 end
396 end
397
397
398 def operator_for(field)
398 def operator_for(field)
399 has_filter?(field) ? filters[field][:operator] : nil
399 has_filter?(field) ? filters[field][:operator] : nil
400 end
400 end
401
401
402 def values_for(field)
402 def values_for(field)
403 has_filter?(field) ? filters[field][:values] : nil
403 has_filter?(field) ? filters[field][:values] : nil
404 end
404 end
405
405
406 def value_for(field, index=0)
406 def value_for(field, index=0)
407 (values_for(field) || [])[index]
407 (values_for(field) || [])[index]
408 end
408 end
409
409
410 def label_for(field)
410 def label_for(field)
411 label = available_filters[field][:name] if available_filters.has_key?(field)
411 label = available_filters[field][:name] if available_filters.has_key?(field)
412 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
412 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
413 end
413 end
414
414
415 def self.add_available_column(column)
415 def self.add_available_column(column)
416 self.available_columns << (column) if column.is_a?(QueryColumn)
416 self.available_columns << (column) if column.is_a?(QueryColumn)
417 end
417 end
418
418
419 # Returns an array of columns that can be used to group the results
419 # Returns an array of columns that can be used to group the results
420 def groupable_columns
420 def groupable_columns
421 available_columns.select {|c| c.groupable}
421 available_columns.select {|c| c.groupable}
422 end
422 end
423
423
424 # Returns a Hash of columns and the key for sorting
424 # Returns a Hash of columns and the key for sorting
425 def sortable_columns
425 def sortable_columns
426 available_columns.inject({}) {|h, column|
426 available_columns.inject({}) {|h, column|
427 h[column.name.to_s] = column.sortable
427 h[column.name.to_s] = column.sortable
428 h
428 h
429 }
429 }
430 end
430 end
431
431
432 def columns
432 def columns
433 # preserve the column_names order
433 # preserve the column_names order
434 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
434 cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
435 available_columns.find { |col| col.name == name }
435 available_columns.find { |col| col.name == name }
436 end.compact
436 end.compact
437 available_columns.select(&:frozen?) | cols
437 available_columns.select(&:frozen?) | cols
438 end
438 end
439
439
440 def inline_columns
440 def inline_columns
441 columns.select(&:inline?)
441 columns.select(&:inline?)
442 end
442 end
443
443
444 def block_columns
444 def block_columns
445 columns.reject(&:inline?)
445 columns.reject(&:inline?)
446 end
446 end
447
447
448 def available_inline_columns
448 def available_inline_columns
449 available_columns.select(&:inline?)
449 available_columns.select(&:inline?)
450 end
450 end
451
451
452 def available_block_columns
452 def available_block_columns
453 available_columns.reject(&:inline?)
453 available_columns.reject(&:inline?)
454 end
454 end
455
455
456 def default_columns_names
456 def default_columns_names
457 []
457 []
458 end
458 end
459
459
460 def column_names=(names)
460 def column_names=(names)
461 if names
461 if names
462 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
462 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
463 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
463 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
464 # Set column_names to nil if default columns
464 # Set column_names to nil if default columns
465 if names == default_columns_names
465 if names == default_columns_names
466 names = nil
466 names = nil
467 end
467 end
468 end
468 end
469 write_attribute(:column_names, names)
469 write_attribute(:column_names, names)
470 end
470 end
471
471
472 def has_column?(column)
472 def has_column?(column)
473 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
473 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
474 end
474 end
475
475
476 def has_custom_field_column?
476 def has_custom_field_column?
477 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
477 columns.any? {|column| column.is_a? QueryCustomFieldColumn}
478 end
478 end
479
479
480 def has_default_columns?
480 def has_default_columns?
481 column_names.nil? || column_names.empty?
481 column_names.nil? || column_names.empty?
482 end
482 end
483
483
484 def sort_criteria=(arg)
484 def sort_criteria=(arg)
485 c = []
485 c = []
486 if arg.is_a?(Hash)
486 if arg.is_a?(Hash)
487 arg = arg.keys.sort.collect {|k| arg[k]}
487 arg = arg.keys.sort.collect {|k| arg[k]}
488 end
488 end
489 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
489 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']}
490 write_attribute(:sort_criteria, c)
490 write_attribute(:sort_criteria, c)
491 end
491 end
492
492
493 def sort_criteria
493 def sort_criteria
494 read_attribute(:sort_criteria) || []
494 read_attribute(:sort_criteria) || []
495 end
495 end
496
496
497 def sort_criteria_key(arg)
497 def sort_criteria_key(arg)
498 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
498 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
499 end
499 end
500
500
501 def sort_criteria_order(arg)
501 def sort_criteria_order(arg)
502 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
502 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
503 end
503 end
504
504
505 def sort_criteria_order_for(key)
505 def sort_criteria_order_for(key)
506 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
506 sort_criteria.detect {|k, order| key.to_s == k}.try(:last)
507 end
507 end
508
508
509 # Returns the SQL sort order that should be prepended for grouping
509 # Returns the SQL sort order that should be prepended for grouping
510 def group_by_sort_order
510 def group_by_sort_order
511 if grouped? && (column = group_by_column)
511 if grouped? && (column = group_by_column)
512 order = (sort_criteria_order_for(column.name) || column.default_order).upcase
512 order = (sort_criteria_order_for(column.name) || column.default_order).try(:upcase)
513 column.sortable.is_a?(Array) ?
513 column.sortable.is_a?(Array) ?
514 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
514 column.sortable.collect {|s| "#{s} #{order}"}.join(',') :
515 "#{column.sortable} #{order}"
515 "#{column.sortable} #{order}"
516 end
516 end
517 end
517 end
518
518
519 # Returns true if the query is a grouped query
519 # Returns true if the query is a grouped query
520 def grouped?
520 def grouped?
521 !group_by_column.nil?
521 !group_by_column.nil?
522 end
522 end
523
523
524 def group_by_column
524 def group_by_column
525 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
525 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
526 end
526 end
527
527
528 def group_by_statement
528 def group_by_statement
529 group_by_column.try(:groupable)
529 group_by_column.try(:groupable)
530 end
530 end
531
531
532 def project_statement
532 def project_statement
533 project_clauses = []
533 project_clauses = []
534 if project && !project.descendants.active.empty?
534 if project && !project.descendants.active.empty?
535 ids = [project.id]
535 ids = [project.id]
536 if has_filter?("subproject_id")
536 if has_filter?("subproject_id")
537 case operator_for("subproject_id")
537 case operator_for("subproject_id")
538 when '='
538 when '='
539 # include the selected subprojects
539 # include the selected subprojects
540 ids += values_for("subproject_id").each(&:to_i)
540 ids += values_for("subproject_id").each(&:to_i)
541 when '!*'
541 when '!*'
542 # main project only
542 # main project only
543 else
543 else
544 # all subprojects
544 # all subprojects
545 ids += project.descendants.collect(&:id)
545 ids += project.descendants.collect(&:id)
546 end
546 end
547 elsif Setting.display_subprojects_issues?
547 elsif Setting.display_subprojects_issues?
548 ids += project.descendants.collect(&:id)
548 ids += project.descendants.collect(&:id)
549 end
549 end
550 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
550 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
551 elsif project
551 elsif project
552 project_clauses << "#{Project.table_name}.id = %d" % project.id
552 project_clauses << "#{Project.table_name}.id = %d" % project.id
553 end
553 end
554 project_clauses.any? ? project_clauses.join(' AND ') : nil
554 project_clauses.any? ? project_clauses.join(' AND ') : nil
555 end
555 end
556
556
557 def statement
557 def statement
558 # filters clauses
558 # filters clauses
559 filters_clauses = []
559 filters_clauses = []
560 filters.each_key do |field|
560 filters.each_key do |field|
561 next if field == "subproject_id"
561 next if field == "subproject_id"
562 v = values_for(field).clone
562 v = values_for(field).clone
563 next unless v and !v.empty?
563 next unless v and !v.empty?
564 operator = operator_for(field)
564 operator = operator_for(field)
565
565
566 # "me" value substitution
566 # "me" value substitution
567 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
567 if %w(assigned_to_id author_id user_id watcher_id).include?(field)
568 if v.delete("me")
568 if v.delete("me")
569 if User.current.logged?
569 if User.current.logged?
570 v.push(User.current.id.to_s)
570 v.push(User.current.id.to_s)
571 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
571 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
572 else
572 else
573 v.push("0")
573 v.push("0")
574 end
574 end
575 end
575 end
576 end
576 end
577
577
578 if field == 'project_id'
578 if field == 'project_id'
579 if v.delete('mine')
579 if v.delete('mine')
580 v += User.current.memberships.map(&:project_id).map(&:to_s)
580 v += User.current.memberships.map(&:project_id).map(&:to_s)
581 end
581 end
582 end
582 end
583
583
584 if field =~ /cf_(\d+)$/
584 if field =~ /cf_(\d+)$/
585 # custom field
585 # custom field
586 filters_clauses << sql_for_custom_field(field, operator, v, $1)
586 filters_clauses << sql_for_custom_field(field, operator, v, $1)
587 elsif respond_to?("sql_for_#{field}_field")
587 elsif respond_to?("sql_for_#{field}_field")
588 # specific statement
588 # specific statement
589 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
589 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
590 else
590 else
591 # regular field
591 # regular field
592 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
592 filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')'
593 end
593 end
594 end if filters and valid?
594 end if filters and valid?
595
595
596 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
596 if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn)
597 # Excludes results for which the grouped custom field is not visible
597 # Excludes results for which the grouped custom field is not visible
598 filters_clauses << c.custom_field.visibility_by_project_condition
598 filters_clauses << c.custom_field.visibility_by_project_condition
599 end
599 end
600
600
601 filters_clauses << project_statement
601 filters_clauses << project_statement
602 filters_clauses.reject!(&:blank?)
602 filters_clauses.reject!(&:blank?)
603
603
604 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
604 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
605 end
605 end
606
606
607 private
607 private
608
608
609 def sql_for_custom_field(field, operator, value, custom_field_id)
609 def sql_for_custom_field(field, operator, value, custom_field_id)
610 db_table = CustomValue.table_name
610 db_table = CustomValue.table_name
611 db_field = 'value'
611 db_field = 'value'
612 filter = @available_filters[field]
612 filter = @available_filters[field]
613 return nil unless filter
613 return nil unless filter
614 if filter[:field].format.target_class && filter[:field].format.target_class <= User
614 if filter[:field].format.target_class && filter[:field].format.target_class <= User
615 if value.delete('me')
615 if value.delete('me')
616 value.push User.current.id.to_s
616 value.push User.current.id.to_s
617 end
617 end
618 end
618 end
619 not_in = nil
619 not_in = nil
620 if operator == '!'
620 if operator == '!'
621 # Makes ! operator work for custom fields with multiple values
621 # Makes ! operator work for custom fields with multiple values
622 operator = '='
622 operator = '='
623 not_in = 'NOT'
623 not_in = 'NOT'
624 end
624 end
625 customized_key = "id"
625 customized_key = "id"
626 customized_class = queried_class
626 customized_class = queried_class
627 if field =~ /^(.+)\.cf_/
627 if field =~ /^(.+)\.cf_/
628 assoc = $1
628 assoc = $1
629 customized_key = "#{assoc}_id"
629 customized_key = "#{assoc}_id"
630 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
630 customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
631 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
631 raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class
632 end
632 end
633 where = sql_for_field(field, operator, value, db_table, db_field, true)
633 where = sql_for_field(field, operator, value, db_table, db_field, true)
634 if operator =~ /[<>]/
634 if operator =~ /[<>]/
635 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
635 where = "(#{where}) AND #{db_table}.#{db_field} <> ''"
636 end
636 end
637 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
637 "#{queried_table_name}.#{customized_key} #{not_in} IN (" +
638 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
638 "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" +
639 " 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}" +
639 " 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}" +
640 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
640 " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))"
641 end
641 end
642
642
643 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
643 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
644 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
644 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
645 sql = ''
645 sql = ''
646 case operator
646 case operator
647 when "="
647 when "="
648 if value.any?
648 if value.any?
649 case type_for(field)
649 case type_for(field)
650 when :date, :date_past
650 when :date, :date_past
651 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
651 sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter)
652 when :integer
652 when :integer
653 if is_custom_filter
653 if is_custom_filter
654 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})"
654 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})"
655 else
655 else
656 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
656 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
657 end
657 end
658 when :float
658 when :float
659 if is_custom_filter
659 if is_custom_filter
660 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})"
660 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})"
661 else
661 else
662 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
662 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
663 end
663 end
664 else
664 else
665 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")"
665 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")"
666 end
666 end
667 else
667 else
668 # IN an empty set
668 # IN an empty set
669 sql = "1=0"
669 sql = "1=0"
670 end
670 end
671 when "!"
671 when "!"
672 if value.any?
672 if value.any?
673 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + "))"
673 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + "))"
674 else
674 else
675 # NOT IN an empty set
675 # NOT IN an empty set
676 sql = "1=1"
676 sql = "1=1"
677 end
677 end
678 when "!*"
678 when "!*"
679 sql = "#{db_table}.#{db_field} IS NULL"
679 sql = "#{db_table}.#{db_field} IS NULL"
680 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
680 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
681 when "*"
681 when "*"
682 sql = "#{db_table}.#{db_field} IS NOT NULL"
682 sql = "#{db_table}.#{db_field} IS NOT NULL"
683 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
683 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
684 when ">="
684 when ">="
685 if [:date, :date_past].include?(type_for(field))
685 if [:date, :date_past].include?(type_for(field))
686 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
686 sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter)
687 else
687 else
688 if is_custom_filter
688 if is_custom_filter
689 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})"
689 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})"
690 else
690 else
691 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
691 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
692 end
692 end
693 end
693 end
694 when "<="
694 when "<="
695 if [:date, :date_past].include?(type_for(field))
695 if [:date, :date_past].include?(type_for(field))
696 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
696 sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter)
697 else
697 else
698 if is_custom_filter
698 if is_custom_filter
699 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})"
699 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})"
700 else
700 else
701 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
701 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
702 end
702 end
703 end
703 end
704 when "><"
704 when "><"
705 if [:date, :date_past].include?(type_for(field))
705 if [:date, :date_past].include?(type_for(field))
706 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
706 sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter)
707 else
707 else
708 if is_custom_filter
708 if is_custom_filter
709 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})"
709 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})"
710 else
710 else
711 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
711 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
712 end
712 end
713 end
713 end
714 when "o"
714 when "o"
715 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false})" if field == "status_id"
715 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false})" if field == "status_id"
716 when "c"
716 when "c"
717 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_true})" if field == "status_id"
717 sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_true})" if field == "status_id"
718 when "><t-"
718 when "><t-"
719 # between today - n days and today
719 # between today - n days and today
720 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
720 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter)
721 when ">t-"
721 when ">t-"
722 # >= today - n days
722 # >= today - n days
723 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
723 sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter)
724 when "<t-"
724 when "<t-"
725 # <= today - n days
725 # <= today - n days
726 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
726 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter)
727 when "t-"
727 when "t-"
728 # = n days in past
728 # = n days in past
729 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
729 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter)
730 when "><t+"
730 when "><t+"
731 # between today and today + n days
731 # between today and today + n days
732 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
732 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter)
733 when ">t+"
733 when ">t+"
734 # >= today + n days
734 # >= today + n days
735 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
735 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter)
736 when "<t+"
736 when "<t+"
737 # <= today + n days
737 # <= today + n days
738 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
738 sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter)
739 when "t+"
739 when "t+"
740 # = today + n days
740 # = today + n days
741 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
741 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter)
742 when "t"
742 when "t"
743 # = today
743 # = today
744 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
744 sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter)
745 when "ld"
745 when "ld"
746 # = yesterday
746 # = yesterday
747 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
747 sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter)
748 when "w"
748 when "w"
749 # = this week
749 # = this week
750 first_day_of_week = l(:general_first_day_of_week).to_i
750 first_day_of_week = l(:general_first_day_of_week).to_i
751 day_of_week = Date.today.cwday
751 day_of_week = Date.today.cwday
752 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
752 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
753 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
753 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter)
754 when "lw"
754 when "lw"
755 # = last week
755 # = last week
756 first_day_of_week = l(:general_first_day_of_week).to_i
756 first_day_of_week = l(:general_first_day_of_week).to_i
757 day_of_week = Date.today.cwday
757 day_of_week = Date.today.cwday
758 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
758 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
759 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
759 sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter)
760 when "l2w"
760 when "l2w"
761 # = last 2 weeks
761 # = last 2 weeks
762 first_day_of_week = l(:general_first_day_of_week).to_i
762 first_day_of_week = l(:general_first_day_of_week).to_i
763 day_of_week = Date.today.cwday
763 day_of_week = Date.today.cwday
764 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
764 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
765 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
765 sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter)
766 when "m"
766 when "m"
767 # = this month
767 # = this month
768 date = Date.today
768 date = Date.today
769 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
769 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
770 when "lm"
770 when "lm"
771 # = last month
771 # = last month
772 date = Date.today.prev_month
772 date = Date.today.prev_month
773 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
773 sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter)
774 when "y"
774 when "y"
775 # = this year
775 # = this year
776 date = Date.today
776 date = Date.today
777 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
777 sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter)
778 when "~"
778 when "~"
779 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{self.class.connection.quote_string(value.first.to_s.downcase)}%'"
779 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{self.class.connection.quote_string(value.first.to_s.downcase)}%'"
780 when "!~"
780 when "!~"
781 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{self.class.connection.quote_string(value.first.to_s.downcase)}%'"
781 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{self.class.connection.quote_string(value.first.to_s.downcase)}%'"
782 else
782 else
783 raise "Unknown query operator #{operator}"
783 raise "Unknown query operator #{operator}"
784 end
784 end
785
785
786 return sql
786 return sql
787 end
787 end
788
788
789 # Adds a filter for the given custom field
789 # Adds a filter for the given custom field
790 def add_custom_field_filter(field, assoc=nil)
790 def add_custom_field_filter(field, assoc=nil)
791 options = field.format.query_filter_options(field, self)
791 options = field.format.query_filter_options(field, self)
792 if field.format.target_class && field.format.target_class <= User
792 if field.format.target_class && field.format.target_class <= User
793 if options[:values].is_a?(Array) && User.current.logged?
793 if options[:values].is_a?(Array) && User.current.logged?
794 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
794 options[:values].unshift ["<< #{l(:label_me)} >>", "me"]
795 end
795 end
796 end
796 end
797
797
798 filter_id = "cf_#{field.id}"
798 filter_id = "cf_#{field.id}"
799 filter_name = field.name
799 filter_name = field.name
800 if assoc.present?
800 if assoc.present?
801 filter_id = "#{assoc}.#{filter_id}"
801 filter_id = "#{assoc}.#{filter_id}"
802 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
802 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
803 end
803 end
804 add_available_filter filter_id, options.merge({
804 add_available_filter filter_id, options.merge({
805 :name => filter_name,
805 :name => filter_name,
806 :field => field
806 :field => field
807 })
807 })
808 end
808 end
809
809
810 # Adds filters for the given custom fields scope
810 # Adds filters for the given custom fields scope
811 def add_custom_fields_filters(scope, assoc=nil)
811 def add_custom_fields_filters(scope, assoc=nil)
812 scope.visible.where(:is_filter => true).sorted.each do |field|
812 scope.visible.where(:is_filter => true).sorted.each do |field|
813 add_custom_field_filter(field, assoc)
813 add_custom_field_filter(field, assoc)
814 end
814 end
815 end
815 end
816
816
817 # Adds filters for the given associations custom fields
817 # Adds filters for the given associations custom fields
818 def add_associations_custom_fields_filters(*associations)
818 def add_associations_custom_fields_filters(*associations)
819 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
819 fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class)
820 associations.each do |assoc|
820 associations.each do |assoc|
821 association_klass = queried_class.reflect_on_association(assoc).klass
821 association_klass = queried_class.reflect_on_association(assoc).klass
822 fields_by_class.each do |field_class, fields|
822 fields_by_class.each do |field_class, fields|
823 if field_class.customized_class <= association_klass
823 if field_class.customized_class <= association_klass
824 fields.sort.each do |field|
824 fields.sort.each do |field|
825 add_custom_field_filter(field, assoc)
825 add_custom_field_filter(field, assoc)
826 end
826 end
827 end
827 end
828 end
828 end
829 end
829 end
830 end
830 end
831
831
832 def quoted_time(time, is_custom_filter)
832 def quoted_time(time, is_custom_filter)
833 if is_custom_filter
833 if is_custom_filter
834 # Custom field values are stored as strings in the DB
834 # Custom field values are stored as strings in the DB
835 # using this format that does not depend on DB date representation
835 # using this format that does not depend on DB date representation
836 time.strftime("%Y-%m-%d %H:%M:%S")
836 time.strftime("%Y-%m-%d %H:%M:%S")
837 else
837 else
838 self.class.connection.quoted_date(time)
838 self.class.connection.quoted_date(time)
839 end
839 end
840 end
840 end
841
841
842 # Returns a SQL clause for a date or datetime field.
842 # Returns a SQL clause for a date or datetime field.
843 def date_clause(table, field, from, to, is_custom_filter)
843 def date_clause(table, field, from, to, is_custom_filter)
844 s = []
844 s = []
845 if from
845 if from
846 if from.is_a?(Date)
846 if from.is_a?(Date)
847 from = Time.local(from.year, from.month, from.day).yesterday.end_of_day
847 from = Time.local(from.year, from.month, from.day).yesterday.end_of_day
848 else
848 else
849 from = from - 1 # second
849 from = from - 1 # second
850 end
850 end
851 if self.class.default_timezone == :utc
851 if self.class.default_timezone == :utc
852 from = from.utc
852 from = from.utc
853 end
853 end
854 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
854 s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)])
855 end
855 end
856 if to
856 if to
857 if to.is_a?(Date)
857 if to.is_a?(Date)
858 to = Time.local(to.year, to.month, to.day).end_of_day
858 to = Time.local(to.year, to.month, to.day).end_of_day
859 end
859 end
860 if self.class.default_timezone == :utc
860 if self.class.default_timezone == :utc
861 to = to.utc
861 to = to.utc
862 end
862 end
863 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
863 s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)])
864 end
864 end
865 s.join(' AND ')
865 s.join(' AND ')
866 end
866 end
867
867
868 # Returns a SQL clause for a date or datetime field using relative dates.
868 # Returns a SQL clause for a date or datetime field using relative dates.
869 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
869 def relative_date_clause(table, field, days_from, days_to, is_custom_filter)
870 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil), is_custom_filter)
870 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil), is_custom_filter)
871 end
871 end
872
872
873 # Returns a Date or Time from the given filter value
873 # Returns a Date or Time from the given filter value
874 def parse_date(arg)
874 def parse_date(arg)
875 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
875 if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
876 Time.parse(arg) rescue nil
876 Time.parse(arg) rescue nil
877 else
877 else
878 Date.parse(arg) rescue nil
878 Date.parse(arg) rescue nil
879 end
879 end
880 end
880 end
881
881
882 # Additional joins required for the given sort options
882 # Additional joins required for the given sort options
883 def joins_for_order_statement(order_options)
883 def joins_for_order_statement(order_options)
884 joins = []
884 joins = []
885
885
886 if order_options
886 if order_options
887 if order_options.include?('authors')
887 if order_options.include?('authors')
888 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
888 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id"
889 end
889 end
890 order_options.scan(/cf_\d+/).uniq.each do |name|
890 order_options.scan(/cf_\d+/).uniq.each do |name|
891 column = available_columns.detect {|c| c.name.to_s == name}
891 column = available_columns.detect {|c| c.name.to_s == name}
892 join = column && column.custom_field.join_for_order_statement
892 join = column && column.custom_field.join_for_order_statement
893 if join
893 if join
894 joins << join
894 joins << join
895 end
895 end
896 end
896 end
897 end
897 end
898
898
899 joins.any? ? joins.join(' ') : nil
899 joins.any? ? joins.join(' ') : nil
900 end
900 end
901 end
901 end
General Comments 0
You need to be logged in to leave comments. Login now