##// END OF EJS Templates
Makes 'This week' filter work with any starting day of week (#7097)....
Jean-Philippe Lang -
r5476:57f63d513c2b
parent child
Show More
@@ -1,675 +1,673
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 "<t+" => :label_in_less_than,
104 "<t+" => :label_in_less_than,
105 ">t+" => :label_in_more_than,
105 ">t+" => :label_in_more_than,
106 "t+" => :label_in,
106 "t+" => :label_in,
107 "t" => :label_today,
107 "t" => :label_today,
108 "w" => :label_this_week,
108 "w" => :label_this_week,
109 ">t-" => :label_less_than_ago,
109 ">t-" => :label_less_than_ago,
110 "<t-" => :label_more_than_ago,
110 "<t-" => :label_more_than_ago,
111 "t-" => :label_ago,
111 "t-" => :label_ago,
112 "~" => :label_contains,
112 "~" => :label_contains,
113 "!~" => :label_not_contains }
113 "!~" => :label_not_contains }
114
114
115 cattr_reader :operators
115 cattr_reader :operators
116
116
117 @@operators_by_filter_type = { :list => [ "=", "!" ],
117 @@operators_by_filter_type = { :list => [ "=", "!" ],
118 :list_status => [ "o", "=", "!", "c", "*" ],
118 :list_status => [ "o", "=", "!", "c", "*" ],
119 :list_optional => [ "=", "!", "!*", "*" ],
119 :list_optional => [ "=", "!", "!*", "*" ],
120 :list_subprojects => [ "*", "!*", "=" ],
120 :list_subprojects => [ "*", "!*", "=" ],
121 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
121 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
122 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
122 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
123 :string => [ "=", "~", "!", "!~" ],
123 :string => [ "=", "~", "!", "!~" ],
124 :text => [ "~", "!~" ],
124 :text => [ "~", "!~" ],
125 :integer => [ "=", ">=", "<=", "!*", "*" ] }
125 :integer => [ "=", ">=", "<=", "!*", "*" ] }
126
126
127 cattr_reader :operators_by_filter_type
127 cattr_reader :operators_by_filter_type
128
128
129 @@available_columns = [
129 @@available_columns = [
130 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
130 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
131 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
131 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
132 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
132 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
133 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
133 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
134 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
134 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
135 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
135 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
136 QueryColumn.new(:author),
136 QueryColumn.new(:author),
137 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
137 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
138 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
138 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
139 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
139 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
140 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
140 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
141 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
141 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
142 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
142 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
143 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
143 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
144 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
144 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
145 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
145 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
146 ]
146 ]
147 cattr_reader :available_columns
147 cattr_reader :available_columns
148
148
149 def initialize(attributes = nil)
149 def initialize(attributes = nil)
150 super attributes
150 super attributes
151 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
151 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
152 end
152 end
153
153
154 def after_initialize
154 def after_initialize
155 # Store the fact that project is nil (used in #editable_by?)
155 # Store the fact that project is nil (used in #editable_by?)
156 @is_for_all = project.nil?
156 @is_for_all = project.nil?
157 end
157 end
158
158
159 def validate
159 def validate
160 filters.each_key do |field|
160 filters.each_key do |field|
161 errors.add label_for(field), :blank unless
161 errors.add label_for(field), :blank unless
162 # filter requires one or more values
162 # filter requires one or more values
163 (values_for(field) and !values_for(field).first.blank?) or
163 (values_for(field) and !values_for(field).first.blank?) or
164 # filter doesn't require any value
164 # filter doesn't require any value
165 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
165 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
166 end if filters
166 end if filters
167 end
167 end
168
168
169 def editable_by?(user)
169 def editable_by?(user)
170 return false unless user
170 return false unless user
171 # Admin can edit them all and regular users can edit their private queries
171 # Admin can edit them all and regular users can edit their private queries
172 return true if user.admin? || (!is_public && self.user_id == user.id)
172 return true if user.admin? || (!is_public && self.user_id == user.id)
173 # Members can not edit public queries that are for all project (only admin is allowed to)
173 # Members can not edit public queries that are for all project (only admin is allowed to)
174 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
174 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
175 end
175 end
176
176
177 def available_filters
177 def available_filters
178 return @available_filters if @available_filters
178 return @available_filters if @available_filters
179
179
180 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
180 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
181
181
182 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
182 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
183 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
183 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
184 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
184 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
185 "subject" => { :type => :text, :order => 8 },
185 "subject" => { :type => :text, :order => 8 },
186 "created_on" => { :type => :date_past, :order => 9 },
186 "created_on" => { :type => :date_past, :order => 9 },
187 "updated_on" => { :type => :date_past, :order => 10 },
187 "updated_on" => { :type => :date_past, :order => 10 },
188 "start_date" => { :type => :date, :order => 11 },
188 "start_date" => { :type => :date, :order => 11 },
189 "due_date" => { :type => :date, :order => 12 },
189 "due_date" => { :type => :date, :order => 12 },
190 "estimated_hours" => { :type => :integer, :order => 13 },
190 "estimated_hours" => { :type => :integer, :order => 13 },
191 "done_ratio" => { :type => :integer, :order => 14 }}
191 "done_ratio" => { :type => :integer, :order => 14 }}
192
192
193 user_values = []
193 user_values = []
194 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
194 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
195 if project
195 if project
196 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
196 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
197 else
197 else
198 all_projects = Project.visible.all
198 all_projects = Project.visible.all
199 if all_projects.any?
199 if all_projects.any?
200 # members of visible projects
200 # members of visible projects
201 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] }
201 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] }
202
202
203 # project filter
203 # project filter
204 project_values = []
204 project_values = []
205 Project.project_tree(all_projects) do |p, level|
205 Project.project_tree(all_projects) do |p, level|
206 prefix = (level > 0 ? ('--' * level + ' ') : '')
206 prefix = (level > 0 ? ('--' * level + ' ') : '')
207 project_values << ["#{prefix}#{p.name}", p.id.to_s]
207 project_values << ["#{prefix}#{p.name}", p.id.to_s]
208 end
208 end
209 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
209 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
210 end
210 end
211 end
211 end
212 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
212 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
213 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
213 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
214
214
215 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
215 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
216 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
216 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
217
217
218 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
218 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
219 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
219 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
220
220
221 if User.current.logged?
221 if User.current.logged?
222 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
222 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
223 end
223 end
224
224
225 if project
225 if project
226 # project specific filters
226 # project specific filters
227 categories = @project.issue_categories.all
227 categories = @project.issue_categories.all
228 unless categories.empty?
228 unless categories.empty?
229 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
229 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
230 end
230 end
231 versions = @project.shared_versions.all
231 versions = @project.shared_versions.all
232 unless versions.empty?
232 unless versions.empty?
233 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
233 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
234 end
234 end
235 unless @project.leaf?
235 unless @project.leaf?
236 subprojects = @project.descendants.visible.all
236 subprojects = @project.descendants.visible.all
237 unless subprojects.empty?
237 unless subprojects.empty?
238 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
238 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
239 end
239 end
240 end
240 end
241 add_custom_fields_filters(@project.all_issue_custom_fields)
241 add_custom_fields_filters(@project.all_issue_custom_fields)
242 else
242 else
243 # global filters for cross project issue list
243 # global filters for cross project issue list
244 system_shared_versions = Version.visible.find_all_by_sharing('system')
244 system_shared_versions = Version.visible.find_all_by_sharing('system')
245 unless system_shared_versions.empty?
245 unless system_shared_versions.empty?
246 @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] } }
246 @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] } }
247 end
247 end
248 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
248 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
249 end
249 end
250 @available_filters
250 @available_filters
251 end
251 end
252
252
253 def add_filter(field, operator, values)
253 def add_filter(field, operator, values)
254 # values must be an array
254 # values must be an array
255 return unless values and values.is_a? Array # and !values.first.empty?
255 return unless values and values.is_a? Array # and !values.first.empty?
256 # check if field is defined as an available filter
256 # check if field is defined as an available filter
257 if available_filters.has_key? field
257 if available_filters.has_key? field
258 filter_options = available_filters[field]
258 filter_options = available_filters[field]
259 # check if operator is allowed for that filter
259 # check if operator is allowed for that filter
260 #if @@operators_by_filter_type[filter_options[:type]].include? operator
260 #if @@operators_by_filter_type[filter_options[:type]].include? operator
261 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
261 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
262 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
262 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
263 #end
263 #end
264 filters[field] = {:operator => operator, :values => values }
264 filters[field] = {:operator => operator, :values => values }
265 end
265 end
266 end
266 end
267
267
268 def add_short_filter(field, expression)
268 def add_short_filter(field, expression)
269 return unless expression
269 return unless expression
270 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
270 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
271 add_filter field, (parms[0] || "="), [parms[1] || ""]
271 add_filter field, (parms[0] || "="), [parms[1] || ""]
272 end
272 end
273
273
274 # Add multiple filters using +add_filter+
274 # Add multiple filters using +add_filter+
275 def add_filters(fields, operators, values)
275 def add_filters(fields, operators, values)
276 if fields.is_a?(Array) && operators.is_a?(Hash) && values.is_a?(Hash)
276 if fields.is_a?(Array) && operators.is_a?(Hash) && values.is_a?(Hash)
277 fields.each do |field|
277 fields.each do |field|
278 add_filter(field, operators[field], values[field])
278 add_filter(field, operators[field], values[field])
279 end
279 end
280 end
280 end
281 end
281 end
282
282
283 def has_filter?(field)
283 def has_filter?(field)
284 filters and filters[field]
284 filters and filters[field]
285 end
285 end
286
286
287 def operator_for(field)
287 def operator_for(field)
288 has_filter?(field) ? filters[field][:operator] : nil
288 has_filter?(field) ? filters[field][:operator] : nil
289 end
289 end
290
290
291 def values_for(field)
291 def values_for(field)
292 has_filter?(field) ? filters[field][:values] : nil
292 has_filter?(field) ? filters[field][:values] : nil
293 end
293 end
294
294
295 def label_for(field)
295 def label_for(field)
296 label = available_filters[field][:name] if available_filters.has_key?(field)
296 label = available_filters[field][:name] if available_filters.has_key?(field)
297 label ||= field.gsub(/\_id$/, "")
297 label ||= field.gsub(/\_id$/, "")
298 end
298 end
299
299
300 def available_columns
300 def available_columns
301 return @available_columns if @available_columns
301 return @available_columns if @available_columns
302 @available_columns = Query.available_columns
302 @available_columns = Query.available_columns
303 @available_columns += (project ?
303 @available_columns += (project ?
304 project.all_issue_custom_fields :
304 project.all_issue_custom_fields :
305 IssueCustomField.find(:all)
305 IssueCustomField.find(:all)
306 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
306 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
307 end
307 end
308
308
309 def self.available_columns=(v)
309 def self.available_columns=(v)
310 self.available_columns = (v)
310 self.available_columns = (v)
311 end
311 end
312
312
313 def self.add_available_column(column)
313 def self.add_available_column(column)
314 self.available_columns << (column) if column.is_a?(QueryColumn)
314 self.available_columns << (column) if column.is_a?(QueryColumn)
315 end
315 end
316
316
317 # Returns an array of columns that can be used to group the results
317 # Returns an array of columns that can be used to group the results
318 def groupable_columns
318 def groupable_columns
319 available_columns.select {|c| c.groupable}
319 available_columns.select {|c| c.groupable}
320 end
320 end
321
321
322 # Returns a Hash of columns and the key for sorting
322 # Returns a Hash of columns and the key for sorting
323 def sortable_columns
323 def sortable_columns
324 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
324 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
325 h[column.name.to_s] = column.sortable
325 h[column.name.to_s] = column.sortable
326 h
326 h
327 })
327 })
328 end
328 end
329
329
330 def columns
330 def columns
331 if has_default_columns?
331 if has_default_columns?
332 available_columns.select do |c|
332 available_columns.select do |c|
333 # Adds the project column by default for cross-project lists
333 # Adds the project column by default for cross-project lists
334 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
334 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
335 end
335 end
336 else
336 else
337 # preserve the column_names order
337 # preserve the column_names order
338 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
338 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
339 end
339 end
340 end
340 end
341
341
342 def column_names=(names)
342 def column_names=(names)
343 if names
343 if names
344 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
344 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
345 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
345 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
346 # Set column_names to nil if default columns
346 # Set column_names to nil if default columns
347 if names.map(&:to_s) == Setting.issue_list_default_columns
347 if names.map(&:to_s) == Setting.issue_list_default_columns
348 names = nil
348 names = nil
349 end
349 end
350 end
350 end
351 write_attribute(:column_names, names)
351 write_attribute(:column_names, names)
352 end
352 end
353
353
354 def has_column?(column)
354 def has_column?(column)
355 column_names && column_names.include?(column.name)
355 column_names && column_names.include?(column.name)
356 end
356 end
357
357
358 def has_default_columns?
358 def has_default_columns?
359 column_names.nil? || column_names.empty?
359 column_names.nil? || column_names.empty?
360 end
360 end
361
361
362 def sort_criteria=(arg)
362 def sort_criteria=(arg)
363 c = []
363 c = []
364 if arg.is_a?(Hash)
364 if arg.is_a?(Hash)
365 arg = arg.keys.sort.collect {|k| arg[k]}
365 arg = arg.keys.sort.collect {|k| arg[k]}
366 end
366 end
367 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
367 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
368 write_attribute(:sort_criteria, c)
368 write_attribute(:sort_criteria, c)
369 end
369 end
370
370
371 def sort_criteria
371 def sort_criteria
372 read_attribute(:sort_criteria) || []
372 read_attribute(:sort_criteria) || []
373 end
373 end
374
374
375 def sort_criteria_key(arg)
375 def sort_criteria_key(arg)
376 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
376 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
377 end
377 end
378
378
379 def sort_criteria_order(arg)
379 def sort_criteria_order(arg)
380 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
380 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
381 end
381 end
382
382
383 # Returns the SQL sort order that should be prepended for grouping
383 # Returns the SQL sort order that should be prepended for grouping
384 def group_by_sort_order
384 def group_by_sort_order
385 if grouped? && (column = group_by_column)
385 if grouped? && (column = group_by_column)
386 column.sortable.is_a?(Array) ?
386 column.sortable.is_a?(Array) ?
387 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
387 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
388 "#{column.sortable} #{column.default_order}"
388 "#{column.sortable} #{column.default_order}"
389 end
389 end
390 end
390 end
391
391
392 # Returns true if the query is a grouped query
392 # Returns true if the query is a grouped query
393 def grouped?
393 def grouped?
394 !group_by_column.nil?
394 !group_by_column.nil?
395 end
395 end
396
396
397 def group_by_column
397 def group_by_column
398 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
398 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
399 end
399 end
400
400
401 def group_by_statement
401 def group_by_statement
402 group_by_column.try(:groupable)
402 group_by_column.try(:groupable)
403 end
403 end
404
404
405 def project_statement
405 def project_statement
406 project_clauses = []
406 project_clauses = []
407 if project && !@project.descendants.active.empty?
407 if project && !@project.descendants.active.empty?
408 ids = [project.id]
408 ids = [project.id]
409 if has_filter?("subproject_id")
409 if has_filter?("subproject_id")
410 case operator_for("subproject_id")
410 case operator_for("subproject_id")
411 when '='
411 when '='
412 # include the selected subprojects
412 # include the selected subprojects
413 ids += values_for("subproject_id").each(&:to_i)
413 ids += values_for("subproject_id").each(&:to_i)
414 when '!*'
414 when '!*'
415 # main project only
415 # main project only
416 else
416 else
417 # all subprojects
417 # all subprojects
418 ids += project.descendants.collect(&:id)
418 ids += project.descendants.collect(&:id)
419 end
419 end
420 elsif Setting.display_subprojects_issues?
420 elsif Setting.display_subprojects_issues?
421 ids += project.descendants.collect(&:id)
421 ids += project.descendants.collect(&:id)
422 end
422 end
423 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
423 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
424 elsif project
424 elsif project
425 project_clauses << "#{Project.table_name}.id = %d" % project.id
425 project_clauses << "#{Project.table_name}.id = %d" % project.id
426 end
426 end
427 project_clauses.any? ? project_clauses.join(' AND ') : nil
427 project_clauses.any? ? project_clauses.join(' AND ') : nil
428 end
428 end
429
429
430 def statement
430 def statement
431 # filters clauses
431 # filters clauses
432 filters_clauses = []
432 filters_clauses = []
433 filters.each_key do |field|
433 filters.each_key do |field|
434 next if field == "subproject_id"
434 next if field == "subproject_id"
435 v = values_for(field).clone
435 v = values_for(field).clone
436 next unless v and !v.empty?
436 next unless v and !v.empty?
437 operator = operator_for(field)
437 operator = operator_for(field)
438
438
439 # "me" value subsitution
439 # "me" value subsitution
440 if %w(assigned_to_id author_id watcher_id).include?(field)
440 if %w(assigned_to_id author_id watcher_id).include?(field)
441 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
441 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
442 end
442 end
443
443
444 sql = ''
444 sql = ''
445 if field =~ /^cf_(\d+)$/
445 if field =~ /^cf_(\d+)$/
446 # custom field
446 # custom field
447 db_table = CustomValue.table_name
447 db_table = CustomValue.table_name
448 db_field = 'value'
448 db_field = 'value'
449 is_custom_filter = true
449 is_custom_filter = true
450 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 "
450 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 "
451 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
451 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
452 elsif field == 'watcher_id'
452 elsif field == 'watcher_id'
453 db_table = Watcher.table_name
453 db_table = Watcher.table_name
454 db_field = 'user_id'
454 db_field = 'user_id'
455 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
455 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
456 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
456 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
457 elsif field == "member_of_group" # named field
457 elsif field == "member_of_group" # named field
458 if operator == '*' # Any group
458 if operator == '*' # Any group
459 groups = Group.all
459 groups = Group.all
460 operator = '=' # Override the operator since we want to find by assigned_to
460 operator = '=' # Override the operator since we want to find by assigned_to
461 elsif operator == "!*"
461 elsif operator == "!*"
462 groups = Group.all
462 groups = Group.all
463 operator = '!' # Override the operator since we want to find by assigned_to
463 operator = '!' # Override the operator since we want to find by assigned_to
464 else
464 else
465 groups = Group.find_all_by_id(v)
465 groups = Group.find_all_by_id(v)
466 end
466 end
467 groups ||= []
467 groups ||= []
468
468
469 members_of_groups = groups.inject([]) {|user_ids, group|
469 members_of_groups = groups.inject([]) {|user_ids, group|
470 if group && group.user_ids.present?
470 if group && group.user_ids.present?
471 user_ids << group.user_ids
471 user_ids << group.user_ids
472 end
472 end
473 user_ids.flatten.uniq.compact
473 user_ids.flatten.uniq.compact
474 }.sort.collect(&:to_s)
474 }.sort.collect(&:to_s)
475
475
476 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
476 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
477
477
478 elsif field == "assigned_to_role" # named field
478 elsif field == "assigned_to_role" # named field
479 if operator == "*" # Any Role
479 if operator == "*" # Any Role
480 roles = Role.givable
480 roles = Role.givable
481 operator = '=' # Override the operator since we want to find by assigned_to
481 operator = '=' # Override the operator since we want to find by assigned_to
482 elsif operator == "!*" # No role
482 elsif operator == "!*" # No role
483 roles = Role.givable
483 roles = Role.givable
484 operator = '!' # Override the operator since we want to find by assigned_to
484 operator = '!' # Override the operator since we want to find by assigned_to
485 else
485 else
486 roles = Role.givable.find_all_by_id(v)
486 roles = Role.givable.find_all_by_id(v)
487 end
487 end
488 roles ||= []
488 roles ||= []
489
489
490 members_of_roles = roles.inject([]) {|user_ids, role|
490 members_of_roles = roles.inject([]) {|user_ids, role|
491 if role && role.members
491 if role && role.members
492 user_ids << role.members.collect(&:user_id)
492 user_ids << role.members.collect(&:user_id)
493 end
493 end
494 user_ids.flatten.uniq.compact
494 user_ids.flatten.uniq.compact
495 }.sort.collect(&:to_s)
495 }.sort.collect(&:to_s)
496
496
497 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
497 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
498 else
498 else
499 # regular field
499 # regular field
500 db_table = Issue.table_name
500 db_table = Issue.table_name
501 db_field = field
501 db_field = field
502 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
502 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
503 end
503 end
504 filters_clauses << sql
504 filters_clauses << sql
505
505
506 end if filters and valid?
506 end if filters and valid?
507
507
508 filters_clauses << project_statement
508 filters_clauses << project_statement
509 filters_clauses.reject!(&:blank?)
509 filters_clauses.reject!(&:blank?)
510
510
511 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
511 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
512 end
512 end
513
513
514 # Returns the issue count
514 # Returns the issue count
515 def issue_count
515 def issue_count
516 Issue.count(:include => [:status, :project], :conditions => statement)
516 Issue.count(:include => [:status, :project], :conditions => statement)
517 rescue ::ActiveRecord::StatementInvalid => e
517 rescue ::ActiveRecord::StatementInvalid => e
518 raise StatementInvalid.new(e.message)
518 raise StatementInvalid.new(e.message)
519 end
519 end
520
520
521 # Returns the issue count by group or nil if query is not grouped
521 # Returns the issue count by group or nil if query is not grouped
522 def issue_count_by_group
522 def issue_count_by_group
523 r = nil
523 r = nil
524 if grouped?
524 if grouped?
525 begin
525 begin
526 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
526 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
527 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
527 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
528 rescue ActiveRecord::RecordNotFound
528 rescue ActiveRecord::RecordNotFound
529 r = {nil => issue_count}
529 r = {nil => issue_count}
530 end
530 end
531 c = group_by_column
531 c = group_by_column
532 if c.is_a?(QueryCustomFieldColumn)
532 if c.is_a?(QueryCustomFieldColumn)
533 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
533 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
534 end
534 end
535 end
535 end
536 r
536 r
537 rescue ::ActiveRecord::StatementInvalid => e
537 rescue ::ActiveRecord::StatementInvalid => e
538 raise StatementInvalid.new(e.message)
538 raise StatementInvalid.new(e.message)
539 end
539 end
540
540
541 # Returns the issues
541 # Returns the issues
542 # Valid options are :order, :offset, :limit, :include, :conditions
542 # Valid options are :order, :offset, :limit, :include, :conditions
543 def issues(options={})
543 def issues(options={})
544 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
544 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
545 order_option = nil if order_option.blank?
545 order_option = nil if order_option.blank?
546
546
547 Issue.visible.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
547 Issue.visible.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
548 :conditions => Query.merge_conditions(statement, options[:conditions]),
548 :conditions => Query.merge_conditions(statement, options[:conditions]),
549 :order => order_option,
549 :order => order_option,
550 :limit => options[:limit],
550 :limit => options[:limit],
551 :offset => options[:offset]
551 :offset => options[:offset]
552 rescue ::ActiveRecord::StatementInvalid => e
552 rescue ::ActiveRecord::StatementInvalid => e
553 raise StatementInvalid.new(e.message)
553 raise StatementInvalid.new(e.message)
554 end
554 end
555
555
556 # Returns the journals
556 # Returns the journals
557 # Valid options are :order, :offset, :limit
557 # Valid options are :order, :offset, :limit
558 def journals(options={})
558 def journals(options={})
559 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
559 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
560 :conditions => statement,
560 :conditions => statement,
561 :order => options[:order],
561 :order => options[:order],
562 :limit => options[:limit],
562 :limit => options[:limit],
563 :offset => options[:offset]
563 :offset => options[:offset]
564 rescue ::ActiveRecord::StatementInvalid => e
564 rescue ::ActiveRecord::StatementInvalid => e
565 raise StatementInvalid.new(e.message)
565 raise StatementInvalid.new(e.message)
566 end
566 end
567
567
568 # Returns the versions
568 # Returns the versions
569 # Valid options are :conditions
569 # Valid options are :conditions
570 def versions(options={})
570 def versions(options={})
571 Version.visible.find :all, :include => :project,
571 Version.visible.find :all, :include => :project,
572 :conditions => Query.merge_conditions(project_statement, options[:conditions])
572 :conditions => Query.merge_conditions(project_statement, options[:conditions])
573 rescue ::ActiveRecord::StatementInvalid => e
573 rescue ::ActiveRecord::StatementInvalid => e
574 raise StatementInvalid.new(e.message)
574 raise StatementInvalid.new(e.message)
575 end
575 end
576
576
577 private
577 private
578
578
579 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
579 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
580 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
580 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
581 sql = ''
581 sql = ''
582 case operator
582 case operator
583 when "="
583 when "="
584 if value.any?
584 if value.any?
585 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
585 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
586 else
586 else
587 # IN an empty set
587 # IN an empty set
588 sql = "1=0"
588 sql = "1=0"
589 end
589 end
590 when "!"
590 when "!"
591 if value.any?
591 if value.any?
592 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
592 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
593 else
593 else
594 # NOT IN an empty set
594 # NOT IN an empty set
595 sql = "1=1"
595 sql = "1=1"
596 end
596 end
597 when "!*"
597 when "!*"
598 sql = "#{db_table}.#{db_field} IS NULL"
598 sql = "#{db_table}.#{db_field} IS NULL"
599 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
599 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
600 when "*"
600 when "*"
601 sql = "#{db_table}.#{db_field} IS NOT NULL"
601 sql = "#{db_table}.#{db_field} IS NOT NULL"
602 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
602 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
603 when ">="
603 when ">="
604 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
604 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
605 when "<="
605 when "<="
606 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
606 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
607 when "o"
607 when "o"
608 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
608 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
609 when "c"
609 when "c"
610 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
610 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
611 when ">t-"
611 when ">t-"
612 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
612 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
613 when "<t-"
613 when "<t-"
614 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
614 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
615 when "t-"
615 when "t-"
616 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
616 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
617 when ">t+"
617 when ">t+"
618 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
618 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
619 when "<t+"
619 when "<t+"
620 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
620 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
621 when "t+"
621 when "t+"
622 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
622 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
623 when "t"
623 when "t"
624 sql = date_range_clause(db_table, db_field, 0, 0)
624 sql = date_range_clause(db_table, db_field, 0, 0)
625 when "w"
625 when "w"
626 from = l(:general_first_day_of_week) == '7' ?
626 first_day_of_week = l(:general_first_day_of_week).to_i
627 # week starts on sunday
627 day_of_week = Date.today.cwday
628 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
628 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
629 # week starts on monday (Rails default)
629 sql = date_range_clause(db_table, db_field, - days_ago, - days_ago + 6)
630 Time.now.at_beginning_of_week
631 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
632 when "~"
630 when "~"
633 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
631 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
634 when "!~"
632 when "!~"
635 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
633 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
636 end
634 end
637
635
638 return sql
636 return sql
639 end
637 end
640
638
641 def add_custom_fields_filters(custom_fields)
639 def add_custom_fields_filters(custom_fields)
642 @available_filters ||= {}
640 @available_filters ||= {}
643
641
644 custom_fields.select(&:is_filter?).each do |field|
642 custom_fields.select(&:is_filter?).each do |field|
645 case field.field_format
643 case field.field_format
646 when "text"
644 when "text"
647 options = { :type => :text, :order => 20 }
645 options = { :type => :text, :order => 20 }
648 when "list"
646 when "list"
649 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
647 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
650 when "date"
648 when "date"
651 options = { :type => :date, :order => 20 }
649 options = { :type => :date, :order => 20 }
652 when "bool"
650 when "bool"
653 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
651 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
654 when "user", "version"
652 when "user", "version"
655 next unless project
653 next unless project
656 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20}
654 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20}
657 else
655 else
658 options = { :type => :string, :order => 20 }
656 options = { :type => :string, :order => 20 }
659 end
657 end
660 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
658 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
661 end
659 end
662 end
660 end
663
661
664 # Returns a SQL clause for a date or datetime field.
662 # Returns a SQL clause for a date or datetime field.
665 def date_range_clause(table, field, from, to)
663 def date_range_clause(table, field, from, to)
666 s = []
664 s = []
667 if from
665 if from
668 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
666 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
669 end
667 end
670 if to
668 if to
671 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
669 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
672 end
670 end
673 s.join(' AND ')
671 s.join(' AND ')
674 end
672 end
675 end
673 end
@@ -1,584 +1,607
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_operator_greater_than
104 def test_operator_greater_than
105 query = Query.new(:project => Project.find(1), :name => '_')
105 query = Query.new(:project => Project.find(1), :name => '_')
106 query.add_filter('done_ratio', '>=', ['40'])
106 query.add_filter('done_ratio', '>=', ['40'])
107 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
107 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
108 find_issues_with_query(query)
108 find_issues_with_query(query)
109 end
109 end
110
110
111 def test_operator_in_more_than
111 def test_operator_in_more_than
112 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
112 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
113 query = Query.new(:project => Project.find(1), :name => '_')
113 query = Query.new(:project => Project.find(1), :name => '_')
114 query.add_filter('due_date', '>t+', ['15'])
114 query.add_filter('due_date', '>t+', ['15'])
115 issues = find_issues_with_query(query)
115 issues = find_issues_with_query(query)
116 assert !issues.empty?
116 assert !issues.empty?
117 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
117 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
118 end
118 end
119
119
120 def test_operator_in_less_than
120 def test_operator_in_less_than
121 query = Query.new(:project => Project.find(1), :name => '_')
121 query = Query.new(:project => Project.find(1), :name => '_')
122 query.add_filter('due_date', '<t+', ['15'])
122 query.add_filter('due_date', '<t+', ['15'])
123 issues = find_issues_with_query(query)
123 issues = find_issues_with_query(query)
124 assert !issues.empty?
124 assert !issues.empty?
125 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
125 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
126 end
126 end
127
127
128 def test_operator_less_than_ago
128 def test_operator_less_than_ago
129 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
129 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
130 query = Query.new(:project => Project.find(1), :name => '_')
130 query = Query.new(:project => Project.find(1), :name => '_')
131 query.add_filter('due_date', '>t-', ['3'])
131 query.add_filter('due_date', '>t-', ['3'])
132 issues = find_issues_with_query(query)
132 issues = find_issues_with_query(query)
133 assert !issues.empty?
133 assert !issues.empty?
134 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
134 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
135 end
135 end
136
136
137 def test_operator_more_than_ago
137 def test_operator_more_than_ago
138 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
138 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
139 query = Query.new(:project => Project.find(1), :name => '_')
139 query = Query.new(:project => Project.find(1), :name => '_')
140 query.add_filter('due_date', '<t-', ['10'])
140 query.add_filter('due_date', '<t-', ['10'])
141 assert query.statement.include?("#{Issue.table_name}.due_date <=")
141 assert query.statement.include?("#{Issue.table_name}.due_date <=")
142 issues = find_issues_with_query(query)
142 issues = find_issues_with_query(query)
143 assert !issues.empty?
143 assert !issues.empty?
144 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
144 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
145 end
145 end
146
146
147 def test_operator_in
147 def test_operator_in
148 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
148 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
149 query = Query.new(:project => Project.find(1), :name => '_')
149 query = Query.new(:project => Project.find(1), :name => '_')
150 query.add_filter('due_date', 't+', ['2'])
150 query.add_filter('due_date', 't+', ['2'])
151 issues = find_issues_with_query(query)
151 issues = find_issues_with_query(query)
152 assert !issues.empty?
152 assert !issues.empty?
153 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
153 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
154 end
154 end
155
155
156 def test_operator_ago
156 def test_operator_ago
157 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
157 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
158 query = Query.new(:project => Project.find(1), :name => '_')
158 query = Query.new(:project => Project.find(1), :name => '_')
159 query.add_filter('due_date', 't-', ['3'])
159 query.add_filter('due_date', 't-', ['3'])
160 issues = find_issues_with_query(query)
160 issues = find_issues_with_query(query)
161 assert !issues.empty?
161 assert !issues.empty?
162 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
162 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
163 end
163 end
164
164
165 def test_operator_today
165 def test_operator_today
166 query = Query.new(:project => Project.find(1), :name => '_')
166 query = Query.new(:project => Project.find(1), :name => '_')
167 query.add_filter('due_date', 't', [''])
167 query.add_filter('due_date', 't', [''])
168 issues = find_issues_with_query(query)
168 issues = find_issues_with_query(query)
169 assert !issues.empty?
169 assert !issues.empty?
170 issues.each {|issue| assert_equal Date.today, issue.due_date}
170 issues.each {|issue| assert_equal Date.today, issue.due_date}
171 end
171 end
172
172
173 def test_operator_this_week_on_date
173 def test_operator_this_week_on_date
174 query = Query.new(:project => Project.find(1), :name => '_')
174 query = Query.new(:project => Project.find(1), :name => '_')
175 query.add_filter('due_date', 'w', [''])
175 query.add_filter('due_date', 'w', [''])
176 find_issues_with_query(query)
176 find_issues_with_query(query)
177 end
177 end
178
178
179 def test_operator_this_week_on_datetime
179 def test_operator_this_week_on_datetime
180 query = Query.new(:project => Project.find(1), :name => '_')
180 query = Query.new(:project => Project.find(1), :name => '_')
181 query.add_filter('created_on', 'w', [''])
181 query.add_filter('created_on', 'w', [''])
182 find_issues_with_query(query)
182 find_issues_with_query(query)
183 end
183 end
184
184
185 def test_operator_contains
185 def test_operator_contains
186 query = Query.new(:project => Project.find(1), :name => '_')
186 query = Query.new(:project => Project.find(1), :name => '_')
187 query.add_filter('subject', '~', ['uNable'])
187 query.add_filter('subject', '~', ['uNable'])
188 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
188 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
189 result = find_issues_with_query(query)
189 result = find_issues_with_query(query)
190 assert result.empty?
190 assert result.empty?
191 result.each {|issue| assert issue.subject.downcase.include?('unable') }
191 result.each {|issue| assert issue.subject.downcase.include?('unable') }
192 end
192 end
193
193
194 def test_range_for_this_week_with_week_starting_on_monday
195 I18n.locale = :fr
196 assert_equal '1', I18n.t(:general_first_day_of_week)
197
198 Date.stubs(:today).returns(Date.parse('2011-04-29'))
199
200 query = Query.new(:project => Project.find(1), :name => '_')
201 query.add_filter('due_date', 'w', [''])
202 assert query.statement.include?("issues.due_date > '2011-04-24 23:59:59' AND issues.due_date <= '2011-05-01 23:59:59")
203 I18n.locale = :en
204 end
205
206 def test_range_for_this_week_with_week_starting_on_sunday
207 I18n.locale = :en
208 assert_equal '7', I18n.t(:general_first_day_of_week)
209
210 Date.stubs(:today).returns(Date.parse('2011-04-29'))
211
212 query = Query.new(:project => Project.find(1), :name => '_')
213 query.add_filter('due_date', 'w', [''])
214 assert query.statement.include?("issues.due_date > '2011-04-23 23:59:59' AND issues.due_date <= '2011-04-30 23:59:59")
215 end
216
194 def test_operator_does_not_contains
217 def test_operator_does_not_contains
195 query = Query.new(:project => Project.find(1), :name => '_')
218 query = Query.new(:project => Project.find(1), :name => '_')
196 query.add_filter('subject', '!~', ['uNable'])
219 query.add_filter('subject', '!~', ['uNable'])
197 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
220 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
198 find_issues_with_query(query)
221 find_issues_with_query(query)
199 end
222 end
200
223
201 def test_filter_watched_issues
224 def test_filter_watched_issues
202 User.current = User.find(1)
225 User.current = User.find(1)
203 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
226 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
204 result = find_issues_with_query(query)
227 result = find_issues_with_query(query)
205 assert_not_nil result
228 assert_not_nil result
206 assert !result.empty?
229 assert !result.empty?
207 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
230 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
208 User.current = nil
231 User.current = nil
209 end
232 end
210
233
211 def test_filter_unwatched_issues
234 def test_filter_unwatched_issues
212 User.current = User.find(1)
235 User.current = User.find(1)
213 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
236 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
214 result = find_issues_with_query(query)
237 result = find_issues_with_query(query)
215 assert_not_nil result
238 assert_not_nil result
216 assert !result.empty?
239 assert !result.empty?
217 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
240 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
218 User.current = nil
241 User.current = nil
219 end
242 end
220
243
221 def test_statement_should_be_nil_with_no_filters
244 def test_statement_should_be_nil_with_no_filters
222 q = Query.new(:name => '_')
245 q = Query.new(:name => '_')
223 q.filters = {}
246 q.filters = {}
224
247
225 assert q.valid?
248 assert q.valid?
226 assert_nil q.statement
249 assert_nil q.statement
227 end
250 end
228
251
229 def test_default_columns
252 def test_default_columns
230 q = Query.new
253 q = Query.new
231 assert !q.columns.empty?
254 assert !q.columns.empty?
232 end
255 end
233
256
234 def test_set_column_names
257 def test_set_column_names
235 q = Query.new
258 q = Query.new
236 q.column_names = ['tracker', :subject, '', 'unknonw_column']
259 q.column_names = ['tracker', :subject, '', 'unknonw_column']
237 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
260 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
238 c = q.columns.first
261 c = q.columns.first
239 assert q.has_column?(c)
262 assert q.has_column?(c)
240 end
263 end
241
264
242 def test_groupable_columns_should_include_custom_fields
265 def test_groupable_columns_should_include_custom_fields
243 q = Query.new
266 q = Query.new
244 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
267 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
245 end
268 end
246
269
247 def test_grouped_with_valid_column
270 def test_grouped_with_valid_column
248 q = Query.new(:group_by => 'status')
271 q = Query.new(:group_by => 'status')
249 assert q.grouped?
272 assert q.grouped?
250 assert_not_nil q.group_by_column
273 assert_not_nil q.group_by_column
251 assert_equal :status, q.group_by_column.name
274 assert_equal :status, q.group_by_column.name
252 assert_not_nil q.group_by_statement
275 assert_not_nil q.group_by_statement
253 assert_equal 'status', q.group_by_statement
276 assert_equal 'status', q.group_by_statement
254 end
277 end
255
278
256 def test_grouped_with_invalid_column
279 def test_grouped_with_invalid_column
257 q = Query.new(:group_by => 'foo')
280 q = Query.new(:group_by => 'foo')
258 assert !q.grouped?
281 assert !q.grouped?
259 assert_nil q.group_by_column
282 assert_nil q.group_by_column
260 assert_nil q.group_by_statement
283 assert_nil q.group_by_statement
261 end
284 end
262
285
263 def test_default_sort
286 def test_default_sort
264 q = Query.new
287 q = Query.new
265 assert_equal [], q.sort_criteria
288 assert_equal [], q.sort_criteria
266 end
289 end
267
290
268 def test_set_sort_criteria_with_hash
291 def test_set_sort_criteria_with_hash
269 q = Query.new
292 q = Query.new
270 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
293 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
271 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
294 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
272 end
295 end
273
296
274 def test_set_sort_criteria_with_array
297 def test_set_sort_criteria_with_array
275 q = Query.new
298 q = Query.new
276 q.sort_criteria = [['priority', 'desc'], 'tracker']
299 q.sort_criteria = [['priority', 'desc'], 'tracker']
277 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
300 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
278 end
301 end
279
302
280 def test_create_query_with_sort
303 def test_create_query_with_sort
281 q = Query.new(:name => 'Sorted')
304 q = Query.new(:name => 'Sorted')
282 q.sort_criteria = [['priority', 'desc'], 'tracker']
305 q.sort_criteria = [['priority', 'desc'], 'tracker']
283 assert q.save
306 assert q.save
284 q.reload
307 q.reload
285 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
308 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
286 end
309 end
287
310
288 def test_sort_by_string_custom_field_asc
311 def test_sort_by_string_custom_field_asc
289 q = Query.new
312 q = Query.new
290 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
313 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
291 assert c
314 assert c
292 assert c.sortable
315 assert c.sortable
293 issues = Issue.find :all,
316 issues = Issue.find :all,
294 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
317 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
295 :conditions => q.statement,
318 :conditions => q.statement,
296 :order => "#{c.sortable} ASC"
319 :order => "#{c.sortable} ASC"
297 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
320 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
298 assert !values.empty?
321 assert !values.empty?
299 assert_equal values.sort, values
322 assert_equal values.sort, values
300 end
323 end
301
324
302 def test_sort_by_string_custom_field_desc
325 def test_sort_by_string_custom_field_desc
303 q = Query.new
326 q = Query.new
304 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
327 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
305 assert c
328 assert c
306 assert c.sortable
329 assert c.sortable
307 issues = Issue.find :all,
330 issues = Issue.find :all,
308 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
331 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
309 :conditions => q.statement,
332 :conditions => q.statement,
310 :order => "#{c.sortable} DESC"
333 :order => "#{c.sortable} DESC"
311 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
334 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
312 assert !values.empty?
335 assert !values.empty?
313 assert_equal values.sort.reverse, values
336 assert_equal values.sort.reverse, values
314 end
337 end
315
338
316 def test_sort_by_float_custom_field_asc
339 def test_sort_by_float_custom_field_asc
317 q = Query.new
340 q = Query.new
318 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
341 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
319 assert c
342 assert c
320 assert c.sortable
343 assert c.sortable
321 issues = Issue.find :all,
344 issues = Issue.find :all,
322 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
345 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
323 :conditions => q.statement,
346 :conditions => q.statement,
324 :order => "#{c.sortable} ASC"
347 :order => "#{c.sortable} ASC"
325 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
348 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
326 assert !values.empty?
349 assert !values.empty?
327 assert_equal values.sort, values
350 assert_equal values.sort, values
328 end
351 end
329
352
330 def test_invalid_query_should_raise_query_statement_invalid_error
353 def test_invalid_query_should_raise_query_statement_invalid_error
331 q = Query.new
354 q = Query.new
332 assert_raise Query::StatementInvalid do
355 assert_raise Query::StatementInvalid do
333 q.issues(:conditions => "foo = 1")
356 q.issues(:conditions => "foo = 1")
334 end
357 end
335 end
358 end
336
359
337 def test_issue_count_by_association_group
360 def test_issue_count_by_association_group
338 q = Query.new(:name => '_', :group_by => 'assigned_to')
361 q = Query.new(:name => '_', :group_by => 'assigned_to')
339 count_by_group = q.issue_count_by_group
362 count_by_group = q.issue_count_by_group
340 assert_kind_of Hash, count_by_group
363 assert_kind_of Hash, count_by_group
341 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
364 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
342 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
365 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
343 assert count_by_group.has_key?(User.find(3))
366 assert count_by_group.has_key?(User.find(3))
344 end
367 end
345
368
346 def test_issue_count_by_list_custom_field_group
369 def test_issue_count_by_list_custom_field_group
347 q = Query.new(:name => '_', :group_by => 'cf_1')
370 q = Query.new(:name => '_', :group_by => 'cf_1')
348 count_by_group = q.issue_count_by_group
371 count_by_group = q.issue_count_by_group
349 assert_kind_of Hash, count_by_group
372 assert_kind_of Hash, count_by_group
350 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
373 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
351 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
374 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
352 assert count_by_group.has_key?('MySQL')
375 assert count_by_group.has_key?('MySQL')
353 end
376 end
354
377
355 def test_issue_count_by_date_custom_field_group
378 def test_issue_count_by_date_custom_field_group
356 q = Query.new(:name => '_', :group_by => 'cf_8')
379 q = Query.new(:name => '_', :group_by => 'cf_8')
357 count_by_group = q.issue_count_by_group
380 count_by_group = q.issue_count_by_group
358 assert_kind_of Hash, count_by_group
381 assert_kind_of Hash, count_by_group
359 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
382 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
360 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
383 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
361 end
384 end
362
385
363 def test_label_for
386 def test_label_for
364 q = Query.new
387 q = Query.new
365 assert_equal 'assigned_to', q.label_for('assigned_to_id')
388 assert_equal 'assigned_to', q.label_for('assigned_to_id')
366 end
389 end
367
390
368 def test_editable_by
391 def test_editable_by
369 admin = User.find(1)
392 admin = User.find(1)
370 manager = User.find(2)
393 manager = User.find(2)
371 developer = User.find(3)
394 developer = User.find(3)
372
395
373 # Public query on project 1
396 # Public query on project 1
374 q = Query.find(1)
397 q = Query.find(1)
375 assert q.editable_by?(admin)
398 assert q.editable_by?(admin)
376 assert q.editable_by?(manager)
399 assert q.editable_by?(manager)
377 assert !q.editable_by?(developer)
400 assert !q.editable_by?(developer)
378
401
379 # Private query on project 1
402 # Private query on project 1
380 q = Query.find(2)
403 q = Query.find(2)
381 assert q.editable_by?(admin)
404 assert q.editable_by?(admin)
382 assert !q.editable_by?(manager)
405 assert !q.editable_by?(manager)
383 assert q.editable_by?(developer)
406 assert q.editable_by?(developer)
384
407
385 # Private query for all projects
408 # Private query for all projects
386 q = Query.find(3)
409 q = Query.find(3)
387 assert q.editable_by?(admin)
410 assert q.editable_by?(admin)
388 assert !q.editable_by?(manager)
411 assert !q.editable_by?(manager)
389 assert q.editable_by?(developer)
412 assert q.editable_by?(developer)
390
413
391 # Public query for all projects
414 # Public query for all projects
392 q = Query.find(4)
415 q = Query.find(4)
393 assert q.editable_by?(admin)
416 assert q.editable_by?(admin)
394 assert !q.editable_by?(manager)
417 assert !q.editable_by?(manager)
395 assert !q.editable_by?(developer)
418 assert !q.editable_by?(developer)
396 end
419 end
397
420
398 context "#available_filters" do
421 context "#available_filters" do
399 setup do
422 setup do
400 @query = Query.new(:name => "_")
423 @query = Query.new(:name => "_")
401 end
424 end
402
425
403 should "include users of visible projects in cross-project view" do
426 should "include users of visible projects in cross-project view" do
404 users = @query.available_filters["assigned_to_id"]
427 users = @query.available_filters["assigned_to_id"]
405 assert_not_nil users
428 assert_not_nil users
406 assert users[:values].map{|u|u[1]}.include?("3")
429 assert users[:values].map{|u|u[1]}.include?("3")
407 end
430 end
408
431
409 should "include visible projects in cross-project view" do
432 should "include visible projects in cross-project view" do
410 projects = @query.available_filters["project_id"]
433 projects = @query.available_filters["project_id"]
411 assert_not_nil projects
434 assert_not_nil projects
412 assert projects[:values].map{|u|u[1]}.include?("1")
435 assert projects[:values].map{|u|u[1]}.include?("1")
413 end
436 end
414
437
415 context "'member_of_group' filter" do
438 context "'member_of_group' filter" do
416 should "be present" do
439 should "be present" do
417 assert @query.available_filters.keys.include?("member_of_group")
440 assert @query.available_filters.keys.include?("member_of_group")
418 end
441 end
419
442
420 should "be an optional list" do
443 should "be an optional list" do
421 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
444 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
422 end
445 end
423
446
424 should "have a list of the groups as values" do
447 should "have a list of the groups as values" do
425 Group.destroy_all # No fixtures
448 Group.destroy_all # No fixtures
426 group1 = Group.generate!.reload
449 group1 = Group.generate!.reload
427 group2 = Group.generate!.reload
450 group2 = Group.generate!.reload
428
451
429 expected_group_list = [
452 expected_group_list = [
430 [group1.name, group1.id.to_s],
453 [group1.name, group1.id.to_s],
431 [group2.name, group2.id.to_s]
454 [group2.name, group2.id.to_s]
432 ]
455 ]
433 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
456 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
434 end
457 end
435
458
436 end
459 end
437
460
438 context "'assigned_to_role' filter" do
461 context "'assigned_to_role' filter" do
439 should "be present" do
462 should "be present" do
440 assert @query.available_filters.keys.include?("assigned_to_role")
463 assert @query.available_filters.keys.include?("assigned_to_role")
441 end
464 end
442
465
443 should "be an optional list" do
466 should "be an optional list" do
444 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
467 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
445 end
468 end
446
469
447 should "have a list of the Roles as values" do
470 should "have a list of the Roles as values" do
448 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
471 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
449 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
472 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
450 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
473 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
451 end
474 end
452
475
453 should "not include the built in Roles as values" do
476 should "not include the built in Roles as values" do
454 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
477 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
455 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
478 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
456 end
479 end
457
480
458 end
481 end
459
482
460 end
483 end
461
484
462 context "#statement" do
485 context "#statement" do
463 context "with 'member_of_group' filter" do
486 context "with 'member_of_group' filter" do
464 setup do
487 setup do
465 Group.destroy_all # No fixtures
488 Group.destroy_all # No fixtures
466 @user_in_group = User.generate!
489 @user_in_group = User.generate!
467 @second_user_in_group = User.generate!
490 @second_user_in_group = User.generate!
468 @user_in_group2 = User.generate!
491 @user_in_group2 = User.generate!
469 @user_not_in_group = User.generate!
492 @user_not_in_group = User.generate!
470
493
471 @group = Group.generate!.reload
494 @group = Group.generate!.reload
472 @group.users << @user_in_group
495 @group.users << @user_in_group
473 @group.users << @second_user_in_group
496 @group.users << @second_user_in_group
474
497
475 @group2 = Group.generate!.reload
498 @group2 = Group.generate!.reload
476 @group2.users << @user_in_group2
499 @group2.users << @user_in_group2
477
500
478 end
501 end
479
502
480 should "search assigned to for users in the group" do
503 should "search assigned to for users in the group" do
481 @query = Query.new(:name => '_')
504 @query = Query.new(:name => '_')
482 @query.add_filter('member_of_group', '=', [@group.id.to_s])
505 @query.add_filter('member_of_group', '=', [@group.id.to_s])
483
506
484 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
507 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
485 assert_find_issues_with_query_is_successful @query
508 assert_find_issues_with_query_is_successful @query
486 end
509 end
487
510
488 should "search not assigned to any group member (none)" do
511 should "search not assigned to any group member (none)" do
489 @query = Query.new(:name => '_')
512 @query = Query.new(:name => '_')
490 @query.add_filter('member_of_group', '!*', [''])
513 @query.add_filter('member_of_group', '!*', [''])
491
514
492 # Users not in a group
515 # Users not in a group
493 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}')"
516 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}')"
494 assert_find_issues_with_query_is_successful @query
517 assert_find_issues_with_query_is_successful @query
495 end
518 end
496
519
497 should "search assigned to any group member (all)" do
520 should "search assigned to any group member (all)" do
498 @query = Query.new(:name => '_')
521 @query = Query.new(:name => '_')
499 @query.add_filter('member_of_group', '*', [''])
522 @query.add_filter('member_of_group', '*', [''])
500
523
501 # Only users in a group
524 # Only users in a group
502 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}')"
525 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}')"
503 assert_find_issues_with_query_is_successful @query
526 assert_find_issues_with_query_is_successful @query
504 end
527 end
505
528
506 should "return an empty set with = empty group" do
529 should "return an empty set with = empty group" do
507 @empty_group = Group.generate!
530 @empty_group = Group.generate!
508 @query = Query.new(:name => '_')
531 @query = Query.new(:name => '_')
509 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
532 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
510
533
511 assert_equal [], find_issues_with_query(@query)
534 assert_equal [], find_issues_with_query(@query)
512 end
535 end
513
536
514 should "return issues with ! empty group" do
537 should "return issues with ! empty group" do
515 @empty_group = Group.generate!
538 @empty_group = Group.generate!
516 @query = Query.new(:name => '_')
539 @query = Query.new(:name => '_')
517 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
540 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
518
541
519 assert_find_issues_with_query_is_successful @query
542 assert_find_issues_with_query_is_successful @query
520 end
543 end
521 end
544 end
522
545
523 context "with 'assigned_to_role' filter" do
546 context "with 'assigned_to_role' filter" do
524 setup do
547 setup do
525 # No fixtures
548 # No fixtures
526 MemberRole.delete_all
549 MemberRole.delete_all
527 Member.delete_all
550 Member.delete_all
528 Role.delete_all
551 Role.delete_all
529
552
530 @manager_role = Role.generate!(:name => 'Manager')
553 @manager_role = Role.generate!(:name => 'Manager')
531 @developer_role = Role.generate!(:name => 'Developer')
554 @developer_role = Role.generate!(:name => 'Developer')
532
555
533 @project = Project.generate!
556 @project = Project.generate!
534 @manager = User.generate!
557 @manager = User.generate!
535 @developer = User.generate!
558 @developer = User.generate!
536 @boss = User.generate!
559 @boss = User.generate!
537 User.add_to_project(@manager, @project, @manager_role)
560 User.add_to_project(@manager, @project, @manager_role)
538 User.add_to_project(@developer, @project, @developer_role)
561 User.add_to_project(@developer, @project, @developer_role)
539 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
562 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
540 end
563 end
541
564
542 should "search assigned to for users with the Role" do
565 should "search assigned to for users with the Role" do
543 @query = Query.new(:name => '_')
566 @query = Query.new(:name => '_')
544 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
567 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
545
568
546 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@boss.id}')"
569 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@boss.id}')"
547 assert_find_issues_with_query_is_successful @query
570 assert_find_issues_with_query_is_successful @query
548 end
571 end
549
572
550 should "search assigned to for users not assigned to any Role (none)" do
573 should "search assigned to for users not assigned to any Role (none)" do
551 @query = Query.new(:name => '_')
574 @query = Query.new(:name => '_')
552 @query.add_filter('assigned_to_role', '!*', [''])
575 @query.add_filter('assigned_to_role', '!*', [''])
553
576
554 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}')"
577 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}')"
555 assert_find_issues_with_query_is_successful @query
578 assert_find_issues_with_query_is_successful @query
556 end
579 end
557
580
558 should "search assigned to for users assigned to any Role (all)" do
581 should "search assigned to for users assigned to any Role (all)" do
559 @query = Query.new(:name => '_')
582 @query = Query.new(:name => '_')
560 @query.add_filter('assigned_to_role', '*', [''])
583 @query.add_filter('assigned_to_role', '*', [''])
561
584
562 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@developer.id}','#{@boss.id}')"
585 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@manager.id}','#{@developer.id}','#{@boss.id}')"
563 assert_find_issues_with_query_is_successful @query
586 assert_find_issues_with_query_is_successful @query
564 end
587 end
565
588
566 should "return an empty set with empty role" do
589 should "return an empty set with empty role" do
567 @empty_role = Role.generate!
590 @empty_role = Role.generate!
568 @query = Query.new(:name => '_')
591 @query = Query.new(:name => '_')
569 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
592 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
570
593
571 assert_equal [], find_issues_with_query(@query)
594 assert_equal [], find_issues_with_query(@query)
572 end
595 end
573
596
574 should "return issues with ! empty role" do
597 should "return issues with ! empty role" do
575 @empty_role = Role.generate!
598 @empty_role = Role.generate!
576 @query = Query.new(:name => '_')
599 @query = Query.new(:name => '_')
577 @query.add_filter('member_of_group', '!', [@empty_role.id.to_s])
600 @query.add_filter('member_of_group', '!', [@empty_role.id.to_s])
578
601
579 assert_find_issues_with_query_is_successful @query
602 assert_find_issues_with_query_is_successful @query
580 end
603 end
581 end
604 end
582 end
605 end
583
606
584 end
607 end
General Comments 0
You need to be logged in to leave comments. Login now