##// END OF EJS Templates
Validate date filters values....
Jean-Philippe Lang -
r6144:308a922d8033
parent child
Show More
@@ -1,753 +1,760
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 @caption_key = options[:caption] || "field_#{name}"
30 @caption_key = options[:caption] || "field_#{name}"
31 end
31 end
32
32
33 def caption
33 def caption
34 l(@caption_key)
34 l(@caption_key)
35 end
35 end
36
36
37 # Returns true if the column is sortable, otherwise false
37 # Returns true if the column is sortable, otherwise false
38 def sortable?
38 def sortable?
39 !sortable.nil?
39 !sortable.nil?
40 end
40 end
41
41
42 def value(issue)
42 def value(issue)
43 issue.send name
43 issue.send name
44 end
44 end
45
45
46 def css_classes
46 def css_classes
47 name
47 name
48 end
48 end
49 end
49 end
50
50
51 class QueryCustomFieldColumn < QueryColumn
51 class QueryCustomFieldColumn < QueryColumn
52
52
53 def initialize(custom_field)
53 def initialize(custom_field)
54 self.name = "cf_#{custom_field.id}".to_sym
54 self.name = "cf_#{custom_field.id}".to_sym
55 self.sortable = custom_field.order_statement || false
55 self.sortable = custom_field.order_statement || false
56 if %w(list date bool int).include?(custom_field.field_format)
56 if %w(list date bool int).include?(custom_field.field_format)
57 self.groupable = custom_field.order_statement
57 self.groupable = custom_field.order_statement
58 end
58 end
59 self.groupable ||= false
59 self.groupable ||= false
60 @cf = custom_field
60 @cf = custom_field
61 end
61 end
62
62
63 def caption
63 def caption
64 @cf.name
64 @cf.name
65 end
65 end
66
66
67 def custom_field
67 def custom_field
68 @cf
68 @cf
69 end
69 end
70
70
71 def value(issue)
71 def value(issue)
72 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
72 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
73 cv && @cf.cast_value(cv.value)
73 cv && @cf.cast_value(cv.value)
74 end
74 end
75
75
76 def css_classes
76 def css_classes
77 @css_classes ||= "#{name} #{@cf.field_format}"
77 @css_classes ||= "#{name} #{@cf.field_format}"
78 end
78 end
79 end
79 end
80
80
81 class Query < ActiveRecord::Base
81 class Query < ActiveRecord::Base
82 class StatementInvalid < ::ActiveRecord::StatementInvalid
82 class StatementInvalid < ::ActiveRecord::StatementInvalid
83 end
83 end
84
84
85 belongs_to :project
85 belongs_to :project
86 belongs_to :user
86 belongs_to :user
87 serialize :filters
87 serialize :filters
88 serialize :column_names
88 serialize :column_names
89 serialize :sort_criteria, Array
89 serialize :sort_criteria, Array
90
90
91 attr_protected :project_id, :user_id
91 attr_protected :project_id, :user_id
92
92
93 validates_presence_of :name, :on => :save
93 validates_presence_of :name, :on => :save
94 validates_length_of :name, :maximum => 255
94 validates_length_of :name, :maximum => 255
95
95
96 @@operators = { "=" => :label_equals,
96 @@operators = { "=" => :label_equals,
97 "!" => :label_not_equals,
97 "!" => :label_not_equals,
98 "o" => :label_open_issues,
98 "o" => :label_open_issues,
99 "c" => :label_closed_issues,
99 "c" => :label_closed_issues,
100 "!*" => :label_none,
100 "!*" => :label_none,
101 "*" => :label_all,
101 "*" => :label_all,
102 ">=" => :label_greater_or_equal,
102 ">=" => :label_greater_or_equal,
103 "<=" => :label_less_or_equal,
103 "<=" => :label_less_or_equal,
104 "><" => :label_between,
104 "><" => :label_between,
105 "<t+" => :label_in_less_than,
105 "<t+" => :label_in_less_than,
106 ">t+" => :label_in_more_than,
106 ">t+" => :label_in_more_than,
107 "t+" => :label_in,
107 "t+" => :label_in,
108 "t" => :label_today,
108 "t" => :label_today,
109 "w" => :label_this_week,
109 "w" => :label_this_week,
110 ">t-" => :label_less_than_ago,
110 ">t-" => :label_less_than_ago,
111 "<t-" => :label_more_than_ago,
111 "<t-" => :label_more_than_ago,
112 "t-" => :label_ago,
112 "t-" => :label_ago,
113 "~" => :label_contains,
113 "~" => :label_contains,
114 "!~" => :label_not_contains }
114 "!~" => :label_not_contains }
115
115
116 cattr_reader :operators
116 cattr_reader :operators
117
117
118 @@operators_by_filter_type = { :list => [ "=", "!" ],
118 @@operators_by_filter_type = { :list => [ "=", "!" ],
119 :list_status => [ "o", "=", "!", "c", "*" ],
119 :list_status => [ "o", "=", "!", "c", "*" ],
120 :list_optional => [ "=", "!", "!*", "*" ],
120 :list_optional => [ "=", "!", "!*", "*" ],
121 :list_subprojects => [ "*", "!*", "=" ],
121 :list_subprojects => [ "*", "!*", "=" ],
122 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
122 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
123 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w" ],
123 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w" ],
124 :string => [ "=", "~", "!", "!~" ],
124 :string => [ "=", "~", "!", "!~" ],
125 :text => [ "~", "!~" ],
125 :text => [ "~", "!~" ],
126 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
126 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
127 :float => [ "=", ">=", "<=", "><", "!*", "*" ] }
127 :float => [ "=", ">=", "<=", "><", "!*", "*" ] }
128
128
129 cattr_reader :operators_by_filter_type
129 cattr_reader :operators_by_filter_type
130
130
131 @@available_columns = [
131 @@available_columns = [
132 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
132 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
133 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
133 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
134 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
134 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
135 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
135 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
136 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
136 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
137 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
137 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
138 QueryColumn.new(:author),
138 QueryColumn.new(:author),
139 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
139 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
140 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
140 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
141 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
141 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
142 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
142 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
143 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
143 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
144 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
144 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
145 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
145 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
146 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
146 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
147 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
147 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
148 ]
148 ]
149 cattr_reader :available_columns
149 cattr_reader :available_columns
150
150
151 named_scope :visible, lambda {|*args|
151 named_scope :visible, lambda {|*args|
152 user = args.shift || User.current
152 user = args.shift || User.current
153 base = Project.allowed_to_condition(user, :view_issues, *args)
153 base = Project.allowed_to_condition(user, :view_issues, *args)
154 user_id = user.logged? ? user.id : 0
154 user_id = user.logged? ? user.id : 0
155 {
155 {
156 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
156 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
157 :include => :project
157 :include => :project
158 }
158 }
159 }
159 }
160
160
161 def initialize(attributes = nil)
161 def initialize(attributes = nil)
162 super attributes
162 super attributes
163 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
163 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
164 end
164 end
165
165
166 def after_initialize
166 def after_initialize
167 # Store the fact that project is nil (used in #editable_by?)
167 # Store the fact that project is nil (used in #editable_by?)
168 @is_for_all = project.nil?
168 @is_for_all = project.nil?
169 end
169 end
170
170
171 def validate
171 def validate
172 filters.each_key do |field|
172 filters.each_key do |field|
173 if values_for(field)
173 if values_for(field)
174 case type_for(field)
174 case type_for(field)
175 when :integer
175 when :integer
176 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
176 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
177 when :float
177 when :float
178 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+(\.\d*)?$/) }
178 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+(\.\d*)?$/) }
179 when :date, :date_past
180 case operator_for(field)
181 when "=", ">=", "<=", "><"
182 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d{4}-\d{2}-\d{2}$/) }
183 when ">t-", "<t-", "t-"
184 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
185 end
179 end
186 end
180 end
187 end
181
188
182 errors.add label_for(field), :blank unless
189 errors.add label_for(field), :blank unless
183 # filter requires one or more values
190 # filter requires one or more values
184 (values_for(field) and !values_for(field).first.blank?) or
191 (values_for(field) and !values_for(field).first.blank?) or
185 # filter doesn't require any value
192 # filter doesn't require any value
186 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
193 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
187 end if filters
194 end if filters
188 end
195 end
189
196
190 # Returns true if the query is visible to +user+ or the current user.
197 # Returns true if the query is visible to +user+ or the current user.
191 def visible?(user=User.current)
198 def visible?(user=User.current)
192 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
199 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
193 end
200 end
194
201
195 def editable_by?(user)
202 def editable_by?(user)
196 return false unless user
203 return false unless user
197 # Admin can edit them all and regular users can edit their private queries
204 # Admin can edit them all and regular users can edit their private queries
198 return true if user.admin? || (!is_public && self.user_id == user.id)
205 return true if user.admin? || (!is_public && self.user_id == user.id)
199 # Members can not edit public queries that are for all project (only admin is allowed to)
206 # Members can not edit public queries that are for all project (only admin is allowed to)
200 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
207 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
201 end
208 end
202
209
203 def available_filters
210 def available_filters
204 return @available_filters if @available_filters
211 return @available_filters if @available_filters
205
212
206 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
213 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
207
214
208 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
215 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
209 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
216 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
210 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
217 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
211 "subject" => { :type => :text, :order => 8 },
218 "subject" => { :type => :text, :order => 8 },
212 "created_on" => { :type => :date_past, :order => 9 },
219 "created_on" => { :type => :date_past, :order => 9 },
213 "updated_on" => { :type => :date_past, :order => 10 },
220 "updated_on" => { :type => :date_past, :order => 10 },
214 "start_date" => { :type => :date, :order => 11 },
221 "start_date" => { :type => :date, :order => 11 },
215 "due_date" => { :type => :date, :order => 12 },
222 "due_date" => { :type => :date, :order => 12 },
216 "estimated_hours" => { :type => :float, :order => 13 },
223 "estimated_hours" => { :type => :float, :order => 13 },
217 "done_ratio" => { :type => :integer, :order => 14 }}
224 "done_ratio" => { :type => :integer, :order => 14 }}
218
225
219 user_values = []
226 user_values = []
220 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
227 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
221 if project
228 if project
222 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
229 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
223 else
230 else
224 all_projects = Project.visible.all
231 all_projects = Project.visible.all
225 if all_projects.any?
232 if all_projects.any?
226 # members of visible projects
233 # members of visible projects
227 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort.collect{|s| [s.name, s.id.to_s] }
234 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort.collect{|s| [s.name, s.id.to_s] }
228
235
229 # project filter
236 # project filter
230 project_values = []
237 project_values = []
231 Project.project_tree(all_projects) do |p, level|
238 Project.project_tree(all_projects) do |p, level|
232 prefix = (level > 0 ? ('--' * level + ' ') : '')
239 prefix = (level > 0 ? ('--' * level + ' ') : '')
233 project_values << ["#{prefix}#{p.name}", p.id.to_s]
240 project_values << ["#{prefix}#{p.name}", p.id.to_s]
234 end
241 end
235 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
242 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
236 end
243 end
237 end
244 end
238 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
245 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
239 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
246 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
240
247
241 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
248 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
242 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
249 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
243
250
244 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
251 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
245 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
252 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
246
253
247 if User.current.logged?
254 if User.current.logged?
248 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
255 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
249 end
256 end
250
257
251 if project
258 if project
252 # project specific filters
259 # project specific filters
253 categories = @project.issue_categories.all
260 categories = @project.issue_categories.all
254 unless categories.empty?
261 unless categories.empty?
255 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
262 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
256 end
263 end
257 versions = @project.shared_versions.all
264 versions = @project.shared_versions.all
258 unless versions.empty?
265 unless versions.empty?
259 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
266 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
260 end
267 end
261 unless @project.leaf?
268 unless @project.leaf?
262 subprojects = @project.descendants.visible.all
269 subprojects = @project.descendants.visible.all
263 unless subprojects.empty?
270 unless subprojects.empty?
264 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
271 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
265 end
272 end
266 end
273 end
267 add_custom_fields_filters(@project.all_issue_custom_fields)
274 add_custom_fields_filters(@project.all_issue_custom_fields)
268 else
275 else
269 # global filters for cross project issue list
276 # global filters for cross project issue list
270 system_shared_versions = Version.visible.find_all_by_sharing('system')
277 system_shared_versions = Version.visible.find_all_by_sharing('system')
271 unless system_shared_versions.empty?
278 unless system_shared_versions.empty?
272 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
279 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
273 end
280 end
274 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
281 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
275 end
282 end
276 @available_filters
283 @available_filters
277 end
284 end
278
285
279 def add_filter(field, operator, values)
286 def add_filter(field, operator, values)
280 # values must be an array
287 # values must be an array
281 return unless values.nil? || values.is_a?(Array)
288 return unless values.nil? || values.is_a?(Array)
282 # check if field is defined as an available filter
289 # check if field is defined as an available filter
283 if available_filters.has_key? field
290 if available_filters.has_key? field
284 filter_options = available_filters[field]
291 filter_options = available_filters[field]
285 # check if operator is allowed for that filter
292 # check if operator is allowed for that filter
286 #if @@operators_by_filter_type[filter_options[:type]].include? operator
293 #if @@operators_by_filter_type[filter_options[:type]].include? operator
287 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
294 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
288 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
295 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
289 #end
296 #end
290 filters[field] = {:operator => operator, :values => (values || [''])}
297 filters[field] = {:operator => operator, :values => (values || [''])}
291 end
298 end
292 end
299 end
293
300
294 def add_short_filter(field, expression)
301 def add_short_filter(field, expression)
295 return unless expression
302 return unless expression
296 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
303 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
297 add_filter field, (parms[0] || "="), [parms[1] || ""]
304 add_filter field, (parms[0] || "="), [parms[1] || ""]
298 end
305 end
299
306
300 # Add multiple filters using +add_filter+
307 # Add multiple filters using +add_filter+
301 def add_filters(fields, operators, values)
308 def add_filters(fields, operators, values)
302 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
309 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
303 fields.each do |field|
310 fields.each do |field|
304 add_filter(field, operators[field], values && values[field])
311 add_filter(field, operators[field], values && values[field])
305 end
312 end
306 end
313 end
307 end
314 end
308
315
309 def has_filter?(field)
316 def has_filter?(field)
310 filters and filters[field]
317 filters and filters[field]
311 end
318 end
312
319
313 def type_for(field)
320 def type_for(field)
314 available_filters[field][:type] if available_filters.has_key?(field)
321 available_filters[field][:type] if available_filters.has_key?(field)
315 end
322 end
316
323
317 def operator_for(field)
324 def operator_for(field)
318 has_filter?(field) ? filters[field][:operator] : nil
325 has_filter?(field) ? filters[field][:operator] : nil
319 end
326 end
320
327
321 def values_for(field)
328 def values_for(field)
322 has_filter?(field) ? filters[field][:values] : nil
329 has_filter?(field) ? filters[field][:values] : nil
323 end
330 end
324
331
325 def value_for(field, index=0)
332 def value_for(field, index=0)
326 (values_for(field) || [])[index]
333 (values_for(field) || [])[index]
327 end
334 end
328
335
329 def label_for(field)
336 def label_for(field)
330 label = available_filters[field][:name] if available_filters.has_key?(field)
337 label = available_filters[field][:name] if available_filters.has_key?(field)
331 label ||= field.gsub(/\_id$/, "")
338 label ||= field.gsub(/\_id$/, "")
332 end
339 end
333
340
334 def available_columns
341 def available_columns
335 return @available_columns if @available_columns
342 return @available_columns if @available_columns
336 @available_columns = Query.available_columns
343 @available_columns = Query.available_columns
337 @available_columns += (project ?
344 @available_columns += (project ?
338 project.all_issue_custom_fields :
345 project.all_issue_custom_fields :
339 IssueCustomField.find(:all)
346 IssueCustomField.find(:all)
340 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
347 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
341 end
348 end
342
349
343 def self.available_columns=(v)
350 def self.available_columns=(v)
344 self.available_columns = (v)
351 self.available_columns = (v)
345 end
352 end
346
353
347 def self.add_available_column(column)
354 def self.add_available_column(column)
348 self.available_columns << (column) if column.is_a?(QueryColumn)
355 self.available_columns << (column) if column.is_a?(QueryColumn)
349 end
356 end
350
357
351 # Returns an array of columns that can be used to group the results
358 # Returns an array of columns that can be used to group the results
352 def groupable_columns
359 def groupable_columns
353 available_columns.select {|c| c.groupable}
360 available_columns.select {|c| c.groupable}
354 end
361 end
355
362
356 # Returns a Hash of columns and the key for sorting
363 # Returns a Hash of columns and the key for sorting
357 def sortable_columns
364 def sortable_columns
358 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
365 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
359 h[column.name.to_s] = column.sortable
366 h[column.name.to_s] = column.sortable
360 h
367 h
361 })
368 })
362 end
369 end
363
370
364 def columns
371 def columns
365 if has_default_columns?
372 if has_default_columns?
366 available_columns.select do |c|
373 available_columns.select do |c|
367 # Adds the project column by default for cross-project lists
374 # Adds the project column by default for cross-project lists
368 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
375 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
369 end
376 end
370 else
377 else
371 # preserve the column_names order
378 # preserve the column_names order
372 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
379 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
373 end
380 end
374 end
381 end
375
382
376 def column_names=(names)
383 def column_names=(names)
377 if names
384 if names
378 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
385 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
379 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
386 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
380 # Set column_names to nil if default columns
387 # Set column_names to nil if default columns
381 if names.map(&:to_s) == Setting.issue_list_default_columns
388 if names.map(&:to_s) == Setting.issue_list_default_columns
382 names = nil
389 names = nil
383 end
390 end
384 end
391 end
385 write_attribute(:column_names, names)
392 write_attribute(:column_names, names)
386 end
393 end
387
394
388 def has_column?(column)
395 def has_column?(column)
389 column_names && column_names.include?(column.name)
396 column_names && column_names.include?(column.name)
390 end
397 end
391
398
392 def has_default_columns?
399 def has_default_columns?
393 column_names.nil? || column_names.empty?
400 column_names.nil? || column_names.empty?
394 end
401 end
395
402
396 def sort_criteria=(arg)
403 def sort_criteria=(arg)
397 c = []
404 c = []
398 if arg.is_a?(Hash)
405 if arg.is_a?(Hash)
399 arg = arg.keys.sort.collect {|k| arg[k]}
406 arg = arg.keys.sort.collect {|k| arg[k]}
400 end
407 end
401 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
408 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
402 write_attribute(:sort_criteria, c)
409 write_attribute(:sort_criteria, c)
403 end
410 end
404
411
405 def sort_criteria
412 def sort_criteria
406 read_attribute(:sort_criteria) || []
413 read_attribute(:sort_criteria) || []
407 end
414 end
408
415
409 def sort_criteria_key(arg)
416 def sort_criteria_key(arg)
410 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
417 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
411 end
418 end
412
419
413 def sort_criteria_order(arg)
420 def sort_criteria_order(arg)
414 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
421 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
415 end
422 end
416
423
417 # Returns the SQL sort order that should be prepended for grouping
424 # Returns the SQL sort order that should be prepended for grouping
418 def group_by_sort_order
425 def group_by_sort_order
419 if grouped? && (column = group_by_column)
426 if grouped? && (column = group_by_column)
420 column.sortable.is_a?(Array) ?
427 column.sortable.is_a?(Array) ?
421 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
428 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
422 "#{column.sortable} #{column.default_order}"
429 "#{column.sortable} #{column.default_order}"
423 end
430 end
424 end
431 end
425
432
426 # Returns true if the query is a grouped query
433 # Returns true if the query is a grouped query
427 def grouped?
434 def grouped?
428 !group_by_column.nil?
435 !group_by_column.nil?
429 end
436 end
430
437
431 def group_by_column
438 def group_by_column
432 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
439 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
433 end
440 end
434
441
435 def group_by_statement
442 def group_by_statement
436 group_by_column.try(:groupable)
443 group_by_column.try(:groupable)
437 end
444 end
438
445
439 def project_statement
446 def project_statement
440 project_clauses = []
447 project_clauses = []
441 if project && !@project.descendants.active.empty?
448 if project && !@project.descendants.active.empty?
442 ids = [project.id]
449 ids = [project.id]
443 if has_filter?("subproject_id")
450 if has_filter?("subproject_id")
444 case operator_for("subproject_id")
451 case operator_for("subproject_id")
445 when '='
452 when '='
446 # include the selected subprojects
453 # include the selected subprojects
447 ids += values_for("subproject_id").each(&:to_i)
454 ids += values_for("subproject_id").each(&:to_i)
448 when '!*'
455 when '!*'
449 # main project only
456 # main project only
450 else
457 else
451 # all subprojects
458 # all subprojects
452 ids += project.descendants.collect(&:id)
459 ids += project.descendants.collect(&:id)
453 end
460 end
454 elsif Setting.display_subprojects_issues?
461 elsif Setting.display_subprojects_issues?
455 ids += project.descendants.collect(&:id)
462 ids += project.descendants.collect(&:id)
456 end
463 end
457 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
464 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
458 elsif project
465 elsif project
459 project_clauses << "#{Project.table_name}.id = %d" % project.id
466 project_clauses << "#{Project.table_name}.id = %d" % project.id
460 end
467 end
461 project_clauses.any? ? project_clauses.join(' AND ') : nil
468 project_clauses.any? ? project_clauses.join(' AND ') : nil
462 end
469 end
463
470
464 def statement
471 def statement
465 # filters clauses
472 # filters clauses
466 filters_clauses = []
473 filters_clauses = []
467 filters.each_key do |field|
474 filters.each_key do |field|
468 next if field == "subproject_id"
475 next if field == "subproject_id"
469 v = values_for(field).clone
476 v = values_for(field).clone
470 next unless v and !v.empty?
477 next unless v and !v.empty?
471 operator = operator_for(field)
478 operator = operator_for(field)
472
479
473 # "me" value subsitution
480 # "me" value subsitution
474 if %w(assigned_to_id author_id watcher_id).include?(field)
481 if %w(assigned_to_id author_id watcher_id).include?(field)
475 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
482 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
476 end
483 end
477
484
478 sql = ''
485 sql = ''
479 if field =~ /^cf_(\d+)$/
486 if field =~ /^cf_(\d+)$/
480 # custom field
487 # custom field
481 db_table = CustomValue.table_name
488 db_table = CustomValue.table_name
482 db_field = 'value'
489 db_field = 'value'
483 is_custom_filter = true
490 is_custom_filter = true
484 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
491 sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
485 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
492 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
486 elsif field == 'watcher_id'
493 elsif field == 'watcher_id'
487 db_table = Watcher.table_name
494 db_table = Watcher.table_name
488 db_field = 'user_id'
495 db_field = 'user_id'
489 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
496 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
490 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
497 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
491 elsif field == "member_of_group" # named field
498 elsif field == "member_of_group" # named field
492 if operator == '*' # Any group
499 if operator == '*' # Any group
493 groups = Group.all
500 groups = Group.all
494 operator = '=' # Override the operator since we want to find by assigned_to
501 operator = '=' # Override the operator since we want to find by assigned_to
495 elsif operator == "!*"
502 elsif operator == "!*"
496 groups = Group.all
503 groups = Group.all
497 operator = '!' # Override the operator since we want to find by assigned_to
504 operator = '!' # Override the operator since we want to find by assigned_to
498 else
505 else
499 groups = Group.find_all_by_id(v)
506 groups = Group.find_all_by_id(v)
500 end
507 end
501 groups ||= []
508 groups ||= []
502
509
503 members_of_groups = groups.inject([]) {|user_ids, group|
510 members_of_groups = groups.inject([]) {|user_ids, group|
504 if group && group.user_ids.present?
511 if group && group.user_ids.present?
505 user_ids << group.user_ids
512 user_ids << group.user_ids
506 end
513 end
507 user_ids.flatten.uniq.compact
514 user_ids.flatten.uniq.compact
508 }.sort.collect(&:to_s)
515 }.sort.collect(&:to_s)
509
516
510 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
517 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
511
518
512 elsif field == "assigned_to_role" # named field
519 elsif field == "assigned_to_role" # named field
513 if operator == "*" # Any Role
520 if operator == "*" # Any Role
514 roles = Role.givable
521 roles = Role.givable
515 operator = '=' # Override the operator since we want to find by assigned_to
522 operator = '=' # Override the operator since we want to find by assigned_to
516 elsif operator == "!*" # No role
523 elsif operator == "!*" # No role
517 roles = Role.givable
524 roles = Role.givable
518 operator = '!' # Override the operator since we want to find by assigned_to
525 operator = '!' # Override the operator since we want to find by assigned_to
519 else
526 else
520 roles = Role.givable.find_all_by_id(v)
527 roles = Role.givable.find_all_by_id(v)
521 end
528 end
522 roles ||= []
529 roles ||= []
523
530
524 members_of_roles = roles.inject([]) {|user_ids, role|
531 members_of_roles = roles.inject([]) {|user_ids, role|
525 if role && role.members
532 if role && role.members
526 user_ids << role.members.collect(&:user_id)
533 user_ids << role.members.collect(&:user_id)
527 end
534 end
528 user_ids.flatten.uniq.compact
535 user_ids.flatten.uniq.compact
529 }.sort.collect(&:to_s)
536 }.sort.collect(&:to_s)
530
537
531 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
538 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
532 else
539 else
533 # regular field
540 # regular field
534 db_table = Issue.table_name
541 db_table = Issue.table_name
535 db_field = field
542 db_field = field
536 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
543 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
537 end
544 end
538 filters_clauses << sql
545 filters_clauses << sql
539
546
540 end if filters and valid?
547 end if filters and valid?
541
548
542 filters_clauses << project_statement
549 filters_clauses << project_statement
543 filters_clauses.reject!(&:blank?)
550 filters_clauses.reject!(&:blank?)
544
551
545 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
552 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
546 end
553 end
547
554
548 # Returns the issue count
555 # Returns the issue count
549 def issue_count
556 def issue_count
550 Issue.count(:include => [:status, :project], :conditions => statement)
557 Issue.count(:include => [:status, :project], :conditions => statement)
551 rescue ::ActiveRecord::StatementInvalid => e
558 rescue ::ActiveRecord::StatementInvalid => e
552 raise StatementInvalid.new(e.message)
559 raise StatementInvalid.new(e.message)
553 end
560 end
554
561
555 # Returns the issue count by group or nil if query is not grouped
562 # Returns the issue count by group or nil if query is not grouped
556 def issue_count_by_group
563 def issue_count_by_group
557 r = nil
564 r = nil
558 if grouped?
565 if grouped?
559 begin
566 begin
560 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
567 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
561 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
568 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
562 rescue ActiveRecord::RecordNotFound
569 rescue ActiveRecord::RecordNotFound
563 r = {nil => issue_count}
570 r = {nil => issue_count}
564 end
571 end
565 c = group_by_column
572 c = group_by_column
566 if c.is_a?(QueryCustomFieldColumn)
573 if c.is_a?(QueryCustomFieldColumn)
567 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
574 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
568 end
575 end
569 end
576 end
570 r
577 r
571 rescue ::ActiveRecord::StatementInvalid => e
578 rescue ::ActiveRecord::StatementInvalid => e
572 raise StatementInvalid.new(e.message)
579 raise StatementInvalid.new(e.message)
573 end
580 end
574
581
575 # Returns the issues
582 # Returns the issues
576 # Valid options are :order, :offset, :limit, :include, :conditions
583 # Valid options are :order, :offset, :limit, :include, :conditions
577 def issues(options={})
584 def issues(options={})
578 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
585 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
579 order_option = nil if order_option.blank?
586 order_option = nil if order_option.blank?
580
587
581 Issue.visible.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
588 Issue.visible.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
582 :conditions => Query.merge_conditions(statement, options[:conditions]),
589 :conditions => Query.merge_conditions(statement, options[:conditions]),
583 :order => order_option,
590 :order => order_option,
584 :limit => options[:limit],
591 :limit => options[:limit],
585 :offset => options[:offset]
592 :offset => options[:offset]
586 rescue ::ActiveRecord::StatementInvalid => e
593 rescue ::ActiveRecord::StatementInvalid => e
587 raise StatementInvalid.new(e.message)
594 raise StatementInvalid.new(e.message)
588 end
595 end
589
596
590 # Returns the journals
597 # Returns the journals
591 # Valid options are :order, :offset, :limit
598 # Valid options are :order, :offset, :limit
592 def journals(options={})
599 def journals(options={})
593 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
600 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
594 :conditions => statement,
601 :conditions => statement,
595 :order => options[:order],
602 :order => options[:order],
596 :limit => options[:limit],
603 :limit => options[:limit],
597 :offset => options[:offset]
604 :offset => options[:offset]
598 rescue ::ActiveRecord::StatementInvalid => e
605 rescue ::ActiveRecord::StatementInvalid => e
599 raise StatementInvalid.new(e.message)
606 raise StatementInvalid.new(e.message)
600 end
607 end
601
608
602 # Returns the versions
609 # Returns the versions
603 # Valid options are :conditions
610 # Valid options are :conditions
604 def versions(options={})
611 def versions(options={})
605 Version.visible.find :all, :include => :project,
612 Version.visible.find :all, :include => :project,
606 :conditions => Query.merge_conditions(project_statement, options[:conditions])
613 :conditions => Query.merge_conditions(project_statement, options[:conditions])
607 rescue ::ActiveRecord::StatementInvalid => e
614 rescue ::ActiveRecord::StatementInvalid => e
608 raise StatementInvalid.new(e.message)
615 raise StatementInvalid.new(e.message)
609 end
616 end
610
617
611 private
618 private
612
619
613 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
620 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
614 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
621 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
615 sql = ''
622 sql = ''
616 case operator
623 case operator
617 when "="
624 when "="
618 if value.any?
625 if value.any?
619 case type_for(field)
626 case type_for(field)
620 when :date, :date_past
627 when :date, :date_past
621 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
628 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
622 when :integer
629 when :integer
623 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
630 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
624 when :float
631 when :float
625 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
632 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
626 else
633 else
627 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
634 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
628 end
635 end
629 else
636 else
630 # IN an empty set
637 # IN an empty set
631 sql = "1=0"
638 sql = "1=0"
632 end
639 end
633 when "!"
640 when "!"
634 if value.any?
641 if value.any?
635 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
642 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
636 else
643 else
637 # NOT IN an empty set
644 # NOT IN an empty set
638 sql = "1=1"
645 sql = "1=1"
639 end
646 end
640 when "!*"
647 when "!*"
641 sql = "#{db_table}.#{db_field} IS NULL"
648 sql = "#{db_table}.#{db_field} IS NULL"
642 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
649 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
643 when "*"
650 when "*"
644 sql = "#{db_table}.#{db_field} IS NOT NULL"
651 sql = "#{db_table}.#{db_field} IS NOT NULL"
645 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
652 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
646 when ">="
653 when ">="
647 if [:date, :date_past].include?(type_for(field))
654 if [:date, :date_past].include?(type_for(field))
648 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
655 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
649 else
656 else
650 if is_custom_filter
657 if is_custom_filter
651 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f}"
658 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f}"
652 else
659 else
653 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
660 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
654 end
661 end
655 end
662 end
656 when "<="
663 when "<="
657 if [:date, :date_past].include?(type_for(field))
664 if [:date, :date_past].include?(type_for(field))
658 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
665 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
659 else
666 else
660 if is_custom_filter
667 if is_custom_filter
661 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f}"
668 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f}"
662 else
669 else
663 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
670 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
664 end
671 end
665 end
672 end
666 when "><"
673 when "><"
667 if [:date, :date_past].include?(type_for(field))
674 if [:date, :date_past].include?(type_for(field))
668 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
675 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
669 else
676 else
670 if is_custom_filter
677 if is_custom_filter
671 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
678 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
672 else
679 else
673 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
680 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
674 end
681 end
675 end
682 end
676 when "o"
683 when "o"
677 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
684 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
678 when "c"
685 when "c"
679 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
686 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
680 when ">t-"
687 when ">t-"
681 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
688 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
682 when "<t-"
689 when "<t-"
683 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
690 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
684 when "t-"
691 when "t-"
685 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
692 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
686 when ">t+"
693 when ">t+"
687 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
694 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
688 when "<t+"
695 when "<t+"
689 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
696 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
690 when "t+"
697 when "t+"
691 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
698 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
692 when "t"
699 when "t"
693 sql = relative_date_clause(db_table, db_field, 0, 0)
700 sql = relative_date_clause(db_table, db_field, 0, 0)
694 when "w"
701 when "w"
695 first_day_of_week = l(:general_first_day_of_week).to_i
702 first_day_of_week = l(:general_first_day_of_week).to_i
696 day_of_week = Date.today.cwday
703 day_of_week = Date.today.cwday
697 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
704 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
698 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
705 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
699 when "~"
706 when "~"
700 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
707 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
701 when "!~"
708 when "!~"
702 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
709 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
703 else
710 else
704 raise "Unknown query operator #{operator}"
711 raise "Unknown query operator #{operator}"
705 end
712 end
706
713
707 return sql
714 return sql
708 end
715 end
709
716
710 def add_custom_fields_filters(custom_fields)
717 def add_custom_fields_filters(custom_fields)
711 @available_filters ||= {}
718 @available_filters ||= {}
712
719
713 custom_fields.select(&:is_filter?).each do |field|
720 custom_fields.select(&:is_filter?).each do |field|
714 case field.field_format
721 case field.field_format
715 when "text"
722 when "text"
716 options = { :type => :text, :order => 20 }
723 options = { :type => :text, :order => 20 }
717 when "list"
724 when "list"
718 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
725 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
719 when "date"
726 when "date"
720 options = { :type => :date, :order => 20 }
727 options = { :type => :date, :order => 20 }
721 when "bool"
728 when "bool"
722 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
729 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
723 when "int"
730 when "int"
724 options = { :type => :integer, :order => 20 }
731 options = { :type => :integer, :order => 20 }
725 when "float"
732 when "float"
726 options = { :type => :float, :order => 20 }
733 options = { :type => :float, :order => 20 }
727 when "user", "version"
734 when "user", "version"
728 next unless project
735 next unless project
729 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20}
736 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20}
730 else
737 else
731 options = { :type => :string, :order => 20 }
738 options = { :type => :string, :order => 20 }
732 end
739 end
733 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
740 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
734 end
741 end
735 end
742 end
736
743
737 # Returns a SQL clause for a date or datetime field.
744 # Returns a SQL clause for a date or datetime field.
738 def date_clause(table, field, from, to)
745 def date_clause(table, field, from, to)
739 s = []
746 s = []
740 if from
747 if from
741 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((from - 1).to_time.end_of_day)])
748 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((from - 1).to_time.end_of_day)])
742 end
749 end
743 if to
750 if to
744 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to.to_time.end_of_day)])
751 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to.to_time.end_of_day)])
745 end
752 end
746 s.join(' AND ')
753 s.join(' AND ')
747 end
754 end
748
755
749 # Returns a SQL clause for a date or datetime field using relative dates.
756 # Returns a SQL clause for a date or datetime field using relative dates.
750 def relative_date_clause(table, field, days_from, days_to)
757 def relative_date_clause(table, field, days_from, days_to)
751 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
758 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
752 end
759 end
753 end
760 end
@@ -1,708 +1,724
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class QueryTest < ActiveSupport::TestCase
20 class QueryTest < ActiveSupport::TestCase
21 fixtures :projects, :enabled_modules, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :watchers, :custom_fields, :custom_values, :versions, :queries
21 fixtures :projects, :enabled_modules, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :watchers, :custom_fields, :custom_values, :versions, :queries
22
22
23 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
23 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
24 query = Query.new(:project => nil, :name => '_')
24 query = Query.new(:project => nil, :name => '_')
25 assert query.available_filters.has_key?('cf_1')
25 assert query.available_filters.has_key?('cf_1')
26 assert !query.available_filters.has_key?('cf_3')
26 assert !query.available_filters.has_key?('cf_3')
27 end
27 end
28
28
29 def test_system_shared_versions_should_be_available_in_global_queries
29 def test_system_shared_versions_should_be_available_in_global_queries
30 Version.find(2).update_attribute :sharing, 'system'
30 Version.find(2).update_attribute :sharing, 'system'
31 query = Query.new(:project => nil, :name => '_')
31 query = Query.new(:project => nil, :name => '_')
32 assert query.available_filters.has_key?('fixed_version_id')
32 assert query.available_filters.has_key?('fixed_version_id')
33 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
33 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
34 end
34 end
35
35
36 def test_project_filter_in_global_queries
36 def test_project_filter_in_global_queries
37 query = Query.new(:project => nil, :name => '_')
37 query = Query.new(:project => nil, :name => '_')
38 project_filter = query.available_filters["project_id"]
38 project_filter = query.available_filters["project_id"]
39 assert_not_nil project_filter
39 assert_not_nil project_filter
40 project_ids = project_filter[:values].map{|p| p[1]}
40 project_ids = project_filter[:values].map{|p| p[1]}
41 assert project_ids.include?("1") #public project
41 assert project_ids.include?("1") #public project
42 assert !project_ids.include?("2") #private project user cannot see
42 assert !project_ids.include?("2") #private project user cannot see
43 end
43 end
44
44
45 def find_issues_with_query(query)
45 def find_issues_with_query(query)
46 Issue.find :all,
46 Issue.find :all,
47 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
47 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
48 :conditions => query.statement
48 :conditions => query.statement
49 end
49 end
50
50
51 def assert_find_issues_with_query_is_successful(query)
51 def assert_find_issues_with_query_is_successful(query)
52 assert_nothing_raised do
52 assert_nothing_raised do
53 find_issues_with_query(query)
53 find_issues_with_query(query)
54 end
54 end
55 end
55 end
56
56
57 def assert_query_statement_includes(query, condition)
57 def assert_query_statement_includes(query, condition)
58 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
58 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
59 end
59 end
60
60
61 def test_query_should_allow_shared_versions_for_a_project_query
61 def test_query_should_allow_shared_versions_for_a_project_query
62 subproject_version = Version.find(4)
62 subproject_version = Version.find(4)
63 query = Query.new(:project => Project.find(1), :name => '_')
63 query = Query.new(:project => Project.find(1), :name => '_')
64 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
64 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
65
65
66 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
66 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
67 end
67 end
68
68
69 def test_query_with_multiple_custom_fields
69 def test_query_with_multiple_custom_fields
70 query = Query.find(1)
70 query = Query.find(1)
71 assert query.valid?
71 assert query.valid?
72 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
72 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
73 issues = find_issues_with_query(query)
73 issues = find_issues_with_query(query)
74 assert_equal 1, issues.length
74 assert_equal 1, issues.length
75 assert_equal Issue.find(3), issues.first
75 assert_equal Issue.find(3), issues.first
76 end
76 end
77
77
78 def test_operator_none
78 def test_operator_none
79 query = Query.new(:project => Project.find(1), :name => '_')
79 query = Query.new(:project => Project.find(1), :name => '_')
80 query.add_filter('fixed_version_id', '!*', [''])
80 query.add_filter('fixed_version_id', '!*', [''])
81 query.add_filter('cf_1', '!*', [''])
81 query.add_filter('cf_1', '!*', [''])
82 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
82 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
83 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
83 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
84 find_issues_with_query(query)
84 find_issues_with_query(query)
85 end
85 end
86
86
87 def test_operator_none_for_integer
87 def test_operator_none_for_integer
88 query = Query.new(:project => Project.find(1), :name => '_')
88 query = Query.new(:project => Project.find(1), :name => '_')
89 query.add_filter('estimated_hours', '!*', [''])
89 query.add_filter('estimated_hours', '!*', [''])
90 issues = find_issues_with_query(query)
90 issues = find_issues_with_query(query)
91 assert !issues.empty?
91 assert !issues.empty?
92 assert issues.all? {|i| !i.estimated_hours}
92 assert issues.all? {|i| !i.estimated_hours}
93 end
93 end
94
94
95 def test_operator_all
95 def test_operator_all
96 query = Query.new(:project => Project.find(1), :name => '_')
96 query = Query.new(:project => Project.find(1), :name => '_')
97 query.add_filter('fixed_version_id', '*', [''])
97 query.add_filter('fixed_version_id', '*', [''])
98 query.add_filter('cf_1', '*', [''])
98 query.add_filter('cf_1', '*', [''])
99 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
99 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
100 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
100 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
101 find_issues_with_query(query)
101 find_issues_with_query(query)
102 end
102 end
103
103
104 def test_numeric_filter_should_not_accept_non_numeric_values
104 def test_numeric_filter_should_not_accept_non_numeric_values
105 query = Query.new(:name => '_')
105 query = Query.new(:name => '_')
106 query.add_filter('estimated_hours', '=', ['a'])
106 query.add_filter('estimated_hours', '=', ['a'])
107
107
108 assert query.has_filter?('estimated_hours')
108 assert query.has_filter?('estimated_hours')
109 assert !query.valid?
109 assert !query.valid?
110 end
110 end
111
111
112 def test_operator_is_on_float
112 def test_operator_is_on_float
113 Issue.update_all("estimated_hours = 171.2", "id=2")
113 Issue.update_all("estimated_hours = 171.2", "id=2")
114
114
115 query = Query.new(:name => '_')
115 query = Query.new(:name => '_')
116 query.add_filter('estimated_hours', '=', ['171.20'])
116 query.add_filter('estimated_hours', '=', ['171.20'])
117 issues = find_issues_with_query(query)
117 issues = find_issues_with_query(query)
118 assert_equal 1, issues.size
118 assert_equal 1, issues.size
119 assert_equal 2, issues.first.id
119 assert_equal 2, issues.first.id
120 end
120 end
121
121
122 def test_operator_greater_than
122 def test_operator_greater_than
123 query = Query.new(:project => Project.find(1), :name => '_')
123 query = Query.new(:project => Project.find(1), :name => '_')
124 query.add_filter('done_ratio', '>=', ['40'])
124 query.add_filter('done_ratio', '>=', ['40'])
125 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
125 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
126 find_issues_with_query(query)
126 find_issues_with_query(query)
127 end
127 end
128
128
129 def test_operator_greater_than_a_float
129 def test_operator_greater_than_a_float
130 query = Query.new(:project => Project.find(1), :name => '_')
130 query = Query.new(:project => Project.find(1), :name => '_')
131 query.add_filter('estimated_hours', '>=', ['40.5'])
131 query.add_filter('estimated_hours', '>=', ['40.5'])
132 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
132 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
133 find_issues_with_query(query)
133 find_issues_with_query(query)
134 end
134 end
135
135
136 def test_operator_greater_than_on_custom_field
136 def test_operator_greater_than_on_custom_field
137 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
137 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
138 query = Query.new(:project => Project.find(1), :name => '_')
138 query = Query.new(:project => Project.find(1), :name => '_')
139 query.add_filter("cf_#{f.id}", '>=', ['40'])
139 query.add_filter("cf_#{f.id}", '>=', ['40'])
140 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) >= 40.0")
140 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) >= 40.0")
141 find_issues_with_query(query)
141 find_issues_with_query(query)
142 end
142 end
143
143
144 def test_operator_lesser_than
144 def test_operator_lesser_than
145 query = Query.new(:project => Project.find(1), :name => '_')
145 query = Query.new(:project => Project.find(1), :name => '_')
146 query.add_filter('done_ratio', '<=', ['30'])
146 query.add_filter('done_ratio', '<=', ['30'])
147 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
147 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
148 find_issues_with_query(query)
148 find_issues_with_query(query)
149 end
149 end
150
150
151 def test_operator_lesser_than_on_custom_field
151 def test_operator_lesser_than_on_custom_field
152 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
152 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
153 query = Query.new(:project => Project.find(1), :name => '_')
153 query = Query.new(:project => Project.find(1), :name => '_')
154 query.add_filter("cf_#{f.id}", '<=', ['30'])
154 query.add_filter("cf_#{f.id}", '<=', ['30'])
155 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) <= 30.0")
155 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) <= 30.0")
156 find_issues_with_query(query)
156 find_issues_with_query(query)
157 end
157 end
158
158
159 def test_operator_between
159 def test_operator_between
160 query = Query.new(:project => Project.find(1), :name => '_')
160 query = Query.new(:project => Project.find(1), :name => '_')
161 query.add_filter('done_ratio', '><', ['30', '40'])
161 query.add_filter('done_ratio', '><', ['30', '40'])
162 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
162 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
163 find_issues_with_query(query)
163 find_issues_with_query(query)
164 end
164 end
165
165
166 def test_operator_between_on_custom_field
166 def test_operator_between_on_custom_field
167 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
167 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
168 query = Query.new(:project => Project.find(1), :name => '_')
168 query = Query.new(:project => Project.find(1), :name => '_')
169 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
169 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
170 assert_include "CAST(custom_values.value AS decimal(60,3)) BETWEEN 30.0 AND 40.0", query.statement
170 assert_include "CAST(custom_values.value AS decimal(60,3)) BETWEEN 30.0 AND 40.0", query.statement
171 find_issues_with_query(query)
171 find_issues_with_query(query)
172 end
172 end
173
173
174 def test_date_filter_should_not_accept_non_date_values
175 query = Query.new(:name => '_')
176 query.add_filter('created_on', '=', ['a'])
177
178 assert query.has_filter?('created_on')
179 assert !query.valid?
180 end
181
182 def test_relative_date_filter_should_not_accept_non_integer_values
183 query = Query.new(:name => '_')
184 query.add_filter('created_on', '>t-', ['a'])
185
186 assert query.has_filter?('created_on')
187 assert !query.valid?
188 end
189
174 def test_operator_date_equals
190 def test_operator_date_equals
175 query = Query.new(:name => '_')
191 query = Query.new(:name => '_')
176 query.add_filter('due_date', '=', ['2011-07-10'])
192 query.add_filter('due_date', '=', ['2011-07-10'])
177 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
193 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
178 find_issues_with_query(query)
194 find_issues_with_query(query)
179 end
195 end
180
196
181 def test_operator_date_lesser_than
197 def test_operator_date_lesser_than
182 query = Query.new(:name => '_')
198 query = Query.new(:name => '_')
183 query.add_filter('due_date', '<=', ['2011-07-10'])
199 query.add_filter('due_date', '<=', ['2011-07-10'])
184 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
200 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
185 find_issues_with_query(query)
201 find_issues_with_query(query)
186 end
202 end
187
203
188 def test_operator_date_greater_than
204 def test_operator_date_greater_than
189 query = Query.new(:name => '_')
205 query = Query.new(:name => '_')
190 query.add_filter('due_date', '>=', ['2011-07-10'])
206 query.add_filter('due_date', '>=', ['2011-07-10'])
191 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
207 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
192 find_issues_with_query(query)
208 find_issues_with_query(query)
193 end
209 end
194
210
195 def test_operator_date_between
211 def test_operator_date_between
196 query = Query.new(:name => '_')
212 query = Query.new(:name => '_')
197 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
213 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
198 assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
214 assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
199 find_issues_with_query(query)
215 find_issues_with_query(query)
200 end
216 end
201
217
202 def test_operator_in_more_than
218 def test_operator_in_more_than
203 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
219 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
204 query = Query.new(:project => Project.find(1), :name => '_')
220 query = Query.new(:project => Project.find(1), :name => '_')
205 query.add_filter('due_date', '>t+', ['15'])
221 query.add_filter('due_date', '>t+', ['15'])
206 issues = find_issues_with_query(query)
222 issues = find_issues_with_query(query)
207 assert !issues.empty?
223 assert !issues.empty?
208 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
224 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
209 end
225 end
210
226
211 def test_operator_in_less_than
227 def test_operator_in_less_than
212 query = Query.new(:project => Project.find(1), :name => '_')
228 query = Query.new(:project => Project.find(1), :name => '_')
213 query.add_filter('due_date', '<t+', ['15'])
229 query.add_filter('due_date', '<t+', ['15'])
214 issues = find_issues_with_query(query)
230 issues = find_issues_with_query(query)
215 assert !issues.empty?
231 assert !issues.empty?
216 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
232 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
217 end
233 end
218
234
219 def test_operator_less_than_ago
235 def test_operator_less_than_ago
220 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
236 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
221 query = Query.new(:project => Project.find(1), :name => '_')
237 query = Query.new(:project => Project.find(1), :name => '_')
222 query.add_filter('due_date', '>t-', ['3'])
238 query.add_filter('due_date', '>t-', ['3'])
223 issues = find_issues_with_query(query)
239 issues = find_issues_with_query(query)
224 assert !issues.empty?
240 assert !issues.empty?
225 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
241 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
226 end
242 end
227
243
228 def test_operator_more_than_ago
244 def test_operator_more_than_ago
229 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
245 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
230 query = Query.new(:project => Project.find(1), :name => '_')
246 query = Query.new(:project => Project.find(1), :name => '_')
231 query.add_filter('due_date', '<t-', ['10'])
247 query.add_filter('due_date', '<t-', ['10'])
232 assert query.statement.include?("#{Issue.table_name}.due_date <=")
248 assert query.statement.include?("#{Issue.table_name}.due_date <=")
233 issues = find_issues_with_query(query)
249 issues = find_issues_with_query(query)
234 assert !issues.empty?
250 assert !issues.empty?
235 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
251 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
236 end
252 end
237
253
238 def test_operator_in
254 def test_operator_in
239 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
255 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
240 query = Query.new(:project => Project.find(1), :name => '_')
256 query = Query.new(:project => Project.find(1), :name => '_')
241 query.add_filter('due_date', 't+', ['2'])
257 query.add_filter('due_date', 't+', ['2'])
242 issues = find_issues_with_query(query)
258 issues = find_issues_with_query(query)
243 assert !issues.empty?
259 assert !issues.empty?
244 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
260 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
245 end
261 end
246
262
247 def test_operator_ago
263 def test_operator_ago
248 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
264 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
249 query = Query.new(:project => Project.find(1), :name => '_')
265 query = Query.new(:project => Project.find(1), :name => '_')
250 query.add_filter('due_date', 't-', ['3'])
266 query.add_filter('due_date', 't-', ['3'])
251 issues = find_issues_with_query(query)
267 issues = find_issues_with_query(query)
252 assert !issues.empty?
268 assert !issues.empty?
253 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
269 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
254 end
270 end
255
271
256 def test_operator_today
272 def test_operator_today
257 query = Query.new(:project => Project.find(1), :name => '_')
273 query = Query.new(:project => Project.find(1), :name => '_')
258 query.add_filter('due_date', 't', [''])
274 query.add_filter('due_date', 't', [''])
259 issues = find_issues_with_query(query)
275 issues = find_issues_with_query(query)
260 assert !issues.empty?
276 assert !issues.empty?
261 issues.each {|issue| assert_equal Date.today, issue.due_date}
277 issues.each {|issue| assert_equal Date.today, issue.due_date}
262 end
278 end
263
279
264 def test_operator_this_week_on_date
280 def test_operator_this_week_on_date
265 query = Query.new(:project => Project.find(1), :name => '_')
281 query = Query.new(:project => Project.find(1), :name => '_')
266 query.add_filter('due_date', 'w', [''])
282 query.add_filter('due_date', 'w', [''])
267 find_issues_with_query(query)
283 find_issues_with_query(query)
268 end
284 end
269
285
270 def test_operator_this_week_on_datetime
286 def test_operator_this_week_on_datetime
271 query = Query.new(:project => Project.find(1), :name => '_')
287 query = Query.new(:project => Project.find(1), :name => '_')
272 query.add_filter('created_on', 'w', [''])
288 query.add_filter('created_on', 'w', [''])
273 find_issues_with_query(query)
289 find_issues_with_query(query)
274 end
290 end
275
291
276 def test_operator_contains
292 def test_operator_contains
277 query = Query.new(:project => Project.find(1), :name => '_')
293 query = Query.new(:project => Project.find(1), :name => '_')
278 query.add_filter('subject', '~', ['uNable'])
294 query.add_filter('subject', '~', ['uNable'])
279 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
295 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
280 result = find_issues_with_query(query)
296 result = find_issues_with_query(query)
281 assert result.empty?
297 assert result.empty?
282 result.each {|issue| assert issue.subject.downcase.include?('unable') }
298 result.each {|issue| assert issue.subject.downcase.include?('unable') }
283 end
299 end
284
300
285 def test_range_for_this_week_with_week_starting_on_monday
301 def test_range_for_this_week_with_week_starting_on_monday
286 I18n.locale = :fr
302 I18n.locale = :fr
287 assert_equal '1', I18n.t(:general_first_day_of_week)
303 assert_equal '1', I18n.t(:general_first_day_of_week)
288
304
289 Date.stubs(:today).returns(Date.parse('2011-04-29'))
305 Date.stubs(:today).returns(Date.parse('2011-04-29'))
290
306
291 query = Query.new(:project => Project.find(1), :name => '_')
307 query = Query.new(:project => Project.find(1), :name => '_')
292 query.add_filter('due_date', 'w', [''])
308 query.add_filter('due_date', 'w', [''])
293 assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}"
309 assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}"
294 I18n.locale = :en
310 I18n.locale = :en
295 end
311 end
296
312
297 def test_range_for_this_week_with_week_starting_on_sunday
313 def test_range_for_this_week_with_week_starting_on_sunday
298 I18n.locale = :en
314 I18n.locale = :en
299 assert_equal '7', I18n.t(:general_first_day_of_week)
315 assert_equal '7', I18n.t(:general_first_day_of_week)
300
316
301 Date.stubs(:today).returns(Date.parse('2011-04-29'))
317 Date.stubs(:today).returns(Date.parse('2011-04-29'))
302
318
303 query = Query.new(:project => Project.find(1), :name => '_')
319 query = Query.new(:project => Project.find(1), :name => '_')
304 query.add_filter('due_date', 'w', [''])
320 query.add_filter('due_date', 'w', [''])
305 assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}"
321 assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}"
306 end
322 end
307
323
308 def test_operator_does_not_contains
324 def test_operator_does_not_contains
309 query = Query.new(:project => Project.find(1), :name => '_')
325 query = Query.new(:project => Project.find(1), :name => '_')
310 query.add_filter('subject', '!~', ['uNable'])
326 query.add_filter('subject', '!~', ['uNable'])
311 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
327 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
312 find_issues_with_query(query)
328 find_issues_with_query(query)
313 end
329 end
314
330
315 def test_filter_watched_issues
331 def test_filter_watched_issues
316 User.current = User.find(1)
332 User.current = User.find(1)
317 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
333 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
318 result = find_issues_with_query(query)
334 result = find_issues_with_query(query)
319 assert_not_nil result
335 assert_not_nil result
320 assert !result.empty?
336 assert !result.empty?
321 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
337 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
322 User.current = nil
338 User.current = nil
323 end
339 end
324
340
325 def test_filter_unwatched_issues
341 def test_filter_unwatched_issues
326 User.current = User.find(1)
342 User.current = User.find(1)
327 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
343 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
328 result = find_issues_with_query(query)
344 result = find_issues_with_query(query)
329 assert_not_nil result
345 assert_not_nil result
330 assert !result.empty?
346 assert !result.empty?
331 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
347 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
332 User.current = nil
348 User.current = nil
333 end
349 end
334
350
335 def test_statement_should_be_nil_with_no_filters
351 def test_statement_should_be_nil_with_no_filters
336 q = Query.new(:name => '_')
352 q = Query.new(:name => '_')
337 q.filters = {}
353 q.filters = {}
338
354
339 assert q.valid?
355 assert q.valid?
340 assert_nil q.statement
356 assert_nil q.statement
341 end
357 end
342
358
343 def test_default_columns
359 def test_default_columns
344 q = Query.new
360 q = Query.new
345 assert !q.columns.empty?
361 assert !q.columns.empty?
346 end
362 end
347
363
348 def test_set_column_names
364 def test_set_column_names
349 q = Query.new
365 q = Query.new
350 q.column_names = ['tracker', :subject, '', 'unknonw_column']
366 q.column_names = ['tracker', :subject, '', 'unknonw_column']
351 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
367 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
352 c = q.columns.first
368 c = q.columns.first
353 assert q.has_column?(c)
369 assert q.has_column?(c)
354 end
370 end
355
371
356 def test_groupable_columns_should_include_custom_fields
372 def test_groupable_columns_should_include_custom_fields
357 q = Query.new
373 q = Query.new
358 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
374 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
359 end
375 end
360
376
361 def test_grouped_with_valid_column
377 def test_grouped_with_valid_column
362 q = Query.new(:group_by => 'status')
378 q = Query.new(:group_by => 'status')
363 assert q.grouped?
379 assert q.grouped?
364 assert_not_nil q.group_by_column
380 assert_not_nil q.group_by_column
365 assert_equal :status, q.group_by_column.name
381 assert_equal :status, q.group_by_column.name
366 assert_not_nil q.group_by_statement
382 assert_not_nil q.group_by_statement
367 assert_equal 'status', q.group_by_statement
383 assert_equal 'status', q.group_by_statement
368 end
384 end
369
385
370 def test_grouped_with_invalid_column
386 def test_grouped_with_invalid_column
371 q = Query.new(:group_by => 'foo')
387 q = Query.new(:group_by => 'foo')
372 assert !q.grouped?
388 assert !q.grouped?
373 assert_nil q.group_by_column
389 assert_nil q.group_by_column
374 assert_nil q.group_by_statement
390 assert_nil q.group_by_statement
375 end
391 end
376
392
377 def test_default_sort
393 def test_default_sort
378 q = Query.new
394 q = Query.new
379 assert_equal [], q.sort_criteria
395 assert_equal [], q.sort_criteria
380 end
396 end
381
397
382 def test_set_sort_criteria_with_hash
398 def test_set_sort_criteria_with_hash
383 q = Query.new
399 q = Query.new
384 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
400 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
385 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
401 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
386 end
402 end
387
403
388 def test_set_sort_criteria_with_array
404 def test_set_sort_criteria_with_array
389 q = Query.new
405 q = Query.new
390 q.sort_criteria = [['priority', 'desc'], 'tracker']
406 q.sort_criteria = [['priority', 'desc'], 'tracker']
391 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
407 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
392 end
408 end
393
409
394 def test_create_query_with_sort
410 def test_create_query_with_sort
395 q = Query.new(:name => 'Sorted')
411 q = Query.new(:name => 'Sorted')
396 q.sort_criteria = [['priority', 'desc'], 'tracker']
412 q.sort_criteria = [['priority', 'desc'], 'tracker']
397 assert q.save
413 assert q.save
398 q.reload
414 q.reload
399 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
415 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
400 end
416 end
401
417
402 def test_sort_by_string_custom_field_asc
418 def test_sort_by_string_custom_field_asc
403 q = Query.new
419 q = Query.new
404 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
420 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
405 assert c
421 assert c
406 assert c.sortable
422 assert c.sortable
407 issues = Issue.find :all,
423 issues = Issue.find :all,
408 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
424 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
409 :conditions => q.statement,
425 :conditions => q.statement,
410 :order => "#{c.sortable} ASC"
426 :order => "#{c.sortable} ASC"
411 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
427 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
412 assert !values.empty?
428 assert !values.empty?
413 assert_equal values.sort, values
429 assert_equal values.sort, values
414 end
430 end
415
431
416 def test_sort_by_string_custom_field_desc
432 def test_sort_by_string_custom_field_desc
417 q = Query.new
433 q = Query.new
418 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
434 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
419 assert c
435 assert c
420 assert c.sortable
436 assert c.sortable
421 issues = Issue.find :all,
437 issues = Issue.find :all,
422 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
438 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
423 :conditions => q.statement,
439 :conditions => q.statement,
424 :order => "#{c.sortable} DESC"
440 :order => "#{c.sortable} DESC"
425 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
441 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
426 assert !values.empty?
442 assert !values.empty?
427 assert_equal values.sort.reverse, values
443 assert_equal values.sort.reverse, values
428 end
444 end
429
445
430 def test_sort_by_float_custom_field_asc
446 def test_sort_by_float_custom_field_asc
431 q = Query.new
447 q = Query.new
432 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
448 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
433 assert c
449 assert c
434 assert c.sortable
450 assert c.sortable
435 issues = Issue.find :all,
451 issues = Issue.find :all,
436 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
452 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
437 :conditions => q.statement,
453 :conditions => q.statement,
438 :order => "#{c.sortable} ASC"
454 :order => "#{c.sortable} ASC"
439 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
455 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
440 assert !values.empty?
456 assert !values.empty?
441 assert_equal values.sort, values
457 assert_equal values.sort, values
442 end
458 end
443
459
444 def test_invalid_query_should_raise_query_statement_invalid_error
460 def test_invalid_query_should_raise_query_statement_invalid_error
445 q = Query.new
461 q = Query.new
446 assert_raise Query::StatementInvalid do
462 assert_raise Query::StatementInvalid do
447 q.issues(:conditions => "foo = 1")
463 q.issues(:conditions => "foo = 1")
448 end
464 end
449 end
465 end
450
466
451 def test_issue_count_by_association_group
467 def test_issue_count_by_association_group
452 q = Query.new(:name => '_', :group_by => 'assigned_to')
468 q = Query.new(:name => '_', :group_by => 'assigned_to')
453 count_by_group = q.issue_count_by_group
469 count_by_group = q.issue_count_by_group
454 assert_kind_of Hash, count_by_group
470 assert_kind_of Hash, count_by_group
455 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
471 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
456 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
472 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
457 assert count_by_group.has_key?(User.find(3))
473 assert count_by_group.has_key?(User.find(3))
458 end
474 end
459
475
460 def test_issue_count_by_list_custom_field_group
476 def test_issue_count_by_list_custom_field_group
461 q = Query.new(:name => '_', :group_by => 'cf_1')
477 q = Query.new(:name => '_', :group_by => 'cf_1')
462 count_by_group = q.issue_count_by_group
478 count_by_group = q.issue_count_by_group
463 assert_kind_of Hash, count_by_group
479 assert_kind_of Hash, count_by_group
464 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
480 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
465 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
481 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
466 assert count_by_group.has_key?('MySQL')
482 assert count_by_group.has_key?('MySQL')
467 end
483 end
468
484
469 def test_issue_count_by_date_custom_field_group
485 def test_issue_count_by_date_custom_field_group
470 q = Query.new(:name => '_', :group_by => 'cf_8')
486 q = Query.new(:name => '_', :group_by => 'cf_8')
471 count_by_group = q.issue_count_by_group
487 count_by_group = q.issue_count_by_group
472 assert_kind_of Hash, count_by_group
488 assert_kind_of Hash, count_by_group
473 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
489 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
474 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
490 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
475 end
491 end
476
492
477 def test_label_for
493 def test_label_for
478 q = Query.new
494 q = Query.new
479 assert_equal 'assigned_to', q.label_for('assigned_to_id')
495 assert_equal 'assigned_to', q.label_for('assigned_to_id')
480 end
496 end
481
497
482 def test_editable_by
498 def test_editable_by
483 admin = User.find(1)
499 admin = User.find(1)
484 manager = User.find(2)
500 manager = User.find(2)
485 developer = User.find(3)
501 developer = User.find(3)
486
502
487 # Public query on project 1
503 # Public query on project 1
488 q = Query.find(1)
504 q = Query.find(1)
489 assert q.editable_by?(admin)
505 assert q.editable_by?(admin)
490 assert q.editable_by?(manager)
506 assert q.editable_by?(manager)
491 assert !q.editable_by?(developer)
507 assert !q.editable_by?(developer)
492
508
493 # Private query on project 1
509 # Private query on project 1
494 q = Query.find(2)
510 q = Query.find(2)
495 assert q.editable_by?(admin)
511 assert q.editable_by?(admin)
496 assert !q.editable_by?(manager)
512 assert !q.editable_by?(manager)
497 assert q.editable_by?(developer)
513 assert q.editable_by?(developer)
498
514
499 # Private query for all projects
515 # Private query for all projects
500 q = Query.find(3)
516 q = Query.find(3)
501 assert q.editable_by?(admin)
517 assert q.editable_by?(admin)
502 assert !q.editable_by?(manager)
518 assert !q.editable_by?(manager)
503 assert q.editable_by?(developer)
519 assert q.editable_by?(developer)
504
520
505 # Public query for all projects
521 # Public query for all projects
506 q = Query.find(4)
522 q = Query.find(4)
507 assert q.editable_by?(admin)
523 assert q.editable_by?(admin)
508 assert !q.editable_by?(manager)
524 assert !q.editable_by?(manager)
509 assert !q.editable_by?(developer)
525 assert !q.editable_by?(developer)
510 end
526 end
511
527
512 def test_visible_scope
528 def test_visible_scope
513 query_ids = Query.visible(User.anonymous).map(&:id)
529 query_ids = Query.visible(User.anonymous).map(&:id)
514
530
515 assert query_ids.include?(1), 'public query on public project was not visible'
531 assert query_ids.include?(1), 'public query on public project was not visible'
516 assert query_ids.include?(4), 'public query for all projects was not visible'
532 assert query_ids.include?(4), 'public query for all projects was not visible'
517 assert !query_ids.include?(2), 'private query on public project was visible'
533 assert !query_ids.include?(2), 'private query on public project was visible'
518 assert !query_ids.include?(3), 'private query for all projects was visible'
534 assert !query_ids.include?(3), 'private query for all projects was visible'
519 assert !query_ids.include?(7), 'public query on private project was visible'
535 assert !query_ids.include?(7), 'public query on private project was visible'
520 end
536 end
521
537
522 context "#available_filters" do
538 context "#available_filters" do
523 setup do
539 setup do
524 @query = Query.new(:name => "_")
540 @query = Query.new(:name => "_")
525 end
541 end
526
542
527 should "include users of visible projects in cross-project view" do
543 should "include users of visible projects in cross-project view" do
528 users = @query.available_filters["assigned_to_id"]
544 users = @query.available_filters["assigned_to_id"]
529 assert_not_nil users
545 assert_not_nil users
530 assert users[:values].map{|u|u[1]}.include?("3")
546 assert users[:values].map{|u|u[1]}.include?("3")
531 end
547 end
532
548
533 should "include visible projects in cross-project view" do
549 should "include visible projects in cross-project view" do
534 projects = @query.available_filters["project_id"]
550 projects = @query.available_filters["project_id"]
535 assert_not_nil projects
551 assert_not_nil projects
536 assert projects[:values].map{|u|u[1]}.include?("1")
552 assert projects[:values].map{|u|u[1]}.include?("1")
537 end
553 end
538
554
539 context "'member_of_group' filter" do
555 context "'member_of_group' filter" do
540 should "be present" do
556 should "be present" do
541 assert @query.available_filters.keys.include?("member_of_group")
557 assert @query.available_filters.keys.include?("member_of_group")
542 end
558 end
543
559
544 should "be an optional list" do
560 should "be an optional list" do
545 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
561 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
546 end
562 end
547
563
548 should "have a list of the groups as values" do
564 should "have a list of the groups as values" do
549 Group.destroy_all # No fixtures
565 Group.destroy_all # No fixtures
550 group1 = Group.generate!.reload
566 group1 = Group.generate!.reload
551 group2 = Group.generate!.reload
567 group2 = Group.generate!.reload
552
568
553 expected_group_list = [
569 expected_group_list = [
554 [group1.name, group1.id.to_s],
570 [group1.name, group1.id.to_s],
555 [group2.name, group2.id.to_s]
571 [group2.name, group2.id.to_s]
556 ]
572 ]
557 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
573 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
558 end
574 end
559
575
560 end
576 end
561
577
562 context "'assigned_to_role' filter" do
578 context "'assigned_to_role' filter" do
563 should "be present" do
579 should "be present" do
564 assert @query.available_filters.keys.include?("assigned_to_role")
580 assert @query.available_filters.keys.include?("assigned_to_role")
565 end
581 end
566
582
567 should "be an optional list" do
583 should "be an optional list" do
568 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
584 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
569 end
585 end
570
586
571 should "have a list of the Roles as values" do
587 should "have a list of the Roles as values" do
572 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
588 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
573 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
589 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
574 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
590 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
575 end
591 end
576
592
577 should "not include the built in Roles as values" do
593 should "not include the built in Roles as values" do
578 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
594 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
579 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
595 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
580 end
596 end
581
597
582 end
598 end
583
599
584 end
600 end
585
601
586 context "#statement" do
602 context "#statement" do
587 context "with 'member_of_group' filter" do
603 context "with 'member_of_group' filter" do
588 setup do
604 setup do
589 Group.destroy_all # No fixtures
605 Group.destroy_all # No fixtures
590 @user_in_group = User.generate!
606 @user_in_group = User.generate!
591 @second_user_in_group = User.generate!
607 @second_user_in_group = User.generate!
592 @user_in_group2 = User.generate!
608 @user_in_group2 = User.generate!
593 @user_not_in_group = User.generate!
609 @user_not_in_group = User.generate!
594
610
595 @group = Group.generate!.reload
611 @group = Group.generate!.reload
596 @group.users << @user_in_group
612 @group.users << @user_in_group
597 @group.users << @second_user_in_group
613 @group.users << @second_user_in_group
598
614
599 @group2 = Group.generate!.reload
615 @group2 = Group.generate!.reload
600 @group2.users << @user_in_group2
616 @group2.users << @user_in_group2
601
617
602 end
618 end
603
619
604 should "search assigned to for users in the group" do
620 should "search assigned to for users in the group" do
605 @query = Query.new(:name => '_')
621 @query = Query.new(:name => '_')
606 @query.add_filter('member_of_group', '=', [@group.id.to_s])
622 @query.add_filter('member_of_group', '=', [@group.id.to_s])
607
623
608 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
624 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
609 assert_find_issues_with_query_is_successful @query
625 assert_find_issues_with_query_is_successful @query
610 end
626 end
611
627
612 should "search not assigned to any group member (none)" do
628 should "search not assigned to any group member (none)" do
613 @query = Query.new(:name => '_')
629 @query = Query.new(:name => '_')
614 @query.add_filter('member_of_group', '!*', [''])
630 @query.add_filter('member_of_group', '!*', [''])
615
631
616 # Users not in a group
632 # Users not in a group
617 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')"
633 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')"
618 assert_find_issues_with_query_is_successful @query
634 assert_find_issues_with_query_is_successful @query
619 end
635 end
620
636
621 should "search assigned to any group member (all)" do
637 should "search assigned to any group member (all)" do
622 @query = Query.new(:name => '_')
638 @query = Query.new(:name => '_')
623 @query.add_filter('member_of_group', '*', [''])
639 @query.add_filter('member_of_group', '*', [''])
624
640
625 # Only users in a group
641 # Only users in a group
626 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')"
642 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')"
627 assert_find_issues_with_query_is_successful @query
643 assert_find_issues_with_query_is_successful @query
628 end
644 end
629
645
630 should "return an empty set with = empty group" do
646 should "return an empty set with = empty group" do
631 @empty_group = Group.generate!
647 @empty_group = Group.generate!
632 @query = Query.new(:name => '_')
648 @query = Query.new(:name => '_')
633 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
649 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
634
650
635 assert_equal [], find_issues_with_query(@query)
651 assert_equal [], find_issues_with_query(@query)
636 end
652 end
637
653
638 should "return issues with ! empty group" do
654 should "return issues with ! empty group" do
639 @empty_group = Group.generate!
655 @empty_group = Group.generate!
640 @query = Query.new(:name => '_')
656 @query = Query.new(:name => '_')
641 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
657 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
642
658
643 assert_find_issues_with_query_is_successful @query
659 assert_find_issues_with_query_is_successful @query
644 end
660 end
645 end
661 end
646
662
647 context "with 'assigned_to_role' filter" do
663 context "with 'assigned_to_role' filter" do
648 setup do
664 setup do
649 # No fixtures
665 # No fixtures
650 MemberRole.delete_all
666 MemberRole.delete_all
651 Member.delete_all
667 Member.delete_all
652 Role.delete_all
668 Role.delete_all
653
669
654 @manager_role = Role.generate!(:name => 'Manager')
670 @manager_role = Role.generate!(:name => 'Manager')
655 @developer_role = Role.generate!(:name => 'Developer')
671 @developer_role = Role.generate!(:name => 'Developer')
656
672
657 @project = Project.generate!
673 @project = Project.generate!
658 @manager = User.generate!
674 @manager = User.generate!
659 @developer = User.generate!
675 @developer = User.generate!
660 @boss = User.generate!
676 @boss = User.generate!
661 User.add_to_project(@manager, @project, @manager_role)
677 User.add_to_project(@manager, @project, @manager_role)
662 User.add_to_project(@developer, @project, @developer_role)
678 User.add_to_project(@developer, @project, @developer_role)
663 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
679 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
664 end
680 end
665
681
666 should "search assigned to for users with the Role" do
682 should "search assigned to for users with the Role" do
667 @query = Query.new(:name => '_')
683 @query = Query.new(:name => '_')
668 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
684 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
669
685
670 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@boss.id}')"
686 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@boss.id}')"
671 assert_find_issues_with_query_is_successful @query
687 assert_find_issues_with_query_is_successful @query
672 end
688 end
673
689
674 should "search assigned to for users not assigned to any Role (none)" do
690 should "search assigned to for users not assigned to any Role (none)" do
675 @query = Query.new(:name => '_')
691 @query = Query.new(:name => '_')
676 @query.add_filter('assigned_to_role', '!*', [''])
692 @query.add_filter('assigned_to_role', '!*', [''])
677
693
678 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@manager.id}','#{@developer.id}','#{@boss.id}')"
694 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@manager.id}','#{@developer.id}','#{@boss.id}')"
679 assert_find_issues_with_query_is_successful @query
695 assert_find_issues_with_query_is_successful @query
680 end
696 end
681
697
682 should "search assigned to for users assigned to any Role (all)" do
698 should "search assigned to for users assigned to any Role (all)" do
683 @query = Query.new(:name => '_')
699 @query = Query.new(:name => '_')
684 @query.add_filter('assigned_to_role', '*', [''])
700 @query.add_filter('assigned_to_role', '*', [''])
685
701
686 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@developer.id}','#{@boss.id}')"
702 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@developer.id}','#{@boss.id}')"
687 assert_find_issues_with_query_is_successful @query
703 assert_find_issues_with_query_is_successful @query
688 end
704 end
689
705
690 should "return an empty set with empty role" do
706 should "return an empty set with empty role" do
691 @empty_role = Role.generate!
707 @empty_role = Role.generate!
692 @query = Query.new(:name => '_')
708 @query = Query.new(:name => '_')
693 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
709 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
694
710
695 assert_equal [], find_issues_with_query(@query)
711 assert_equal [], find_issues_with_query(@query)
696 end
712 end
697
713
698 should "return issues with ! empty role" do
714 should "return issues with ! empty role" do
699 @empty_role = Role.generate!
715 @empty_role = Role.generate!
700 @query = Query.new(:name => '_')
716 @query = Query.new(:name => '_')
701 @query.add_filter('member_of_group', '!', [@empty_role.id.to_s])
717 @query.add_filter('member_of_group', '!', [@empty_role.id.to_s])
702
718
703 assert_find_issues_with_query_is_successful @query
719 assert_find_issues_with_query_is_successful @query
704 end
720 end
705 end
721 end
706 end
722 end
707
723
708 end
724 end
General Comments 0
You need to be logged in to leave comments. Login now