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