##// END OF EJS Templates
Fixed: Can't filter for negative numeric custom field (#11307)....
Jean-Philippe Lang -
r9725:d00ba6d2bb7c
parent child
Show More
@@ -1,871 +1,871
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 sortable
42 def sortable
43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
44 end
44 end
45
45
46 def value(issue)
46 def value(issue)
47 issue.send name
47 issue.send name
48 end
48 end
49
49
50 def css_classes
50 def css_classes
51 name
51 name
52 end
52 end
53 end
53 end
54
54
55 class QueryCustomFieldColumn < QueryColumn
55 class QueryCustomFieldColumn < QueryColumn
56
56
57 def initialize(custom_field)
57 def initialize(custom_field)
58 self.name = "cf_#{custom_field.id}".to_sym
58 self.name = "cf_#{custom_field.id}".to_sym
59 self.sortable = custom_field.order_statement || false
59 self.sortable = custom_field.order_statement || false
60 if %w(list date bool int).include?(custom_field.field_format) && !custom_field.multiple?
60 if %w(list date bool int).include?(custom_field.field_format) && !custom_field.multiple?
61 self.groupable = custom_field.order_statement
61 self.groupable = custom_field.order_statement
62 end
62 end
63 self.groupable ||= false
63 self.groupable ||= false
64 @cf = custom_field
64 @cf = custom_field
65 end
65 end
66
66
67 def caption
67 def caption
68 @cf.name
68 @cf.name
69 end
69 end
70
70
71 def custom_field
71 def custom_field
72 @cf
72 @cf
73 end
73 end
74
74
75 def value(issue)
75 def value(issue)
76 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
76 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
77 cv.size > 1 ? cv : cv.first
77 cv.size > 1 ? cv : cv.first
78 end
78 end
79
79
80 def css_classes
80 def css_classes
81 @css_classes ||= "#{name} #{@cf.field_format}"
81 @css_classes ||= "#{name} #{@cf.field_format}"
82 end
82 end
83 end
83 end
84
84
85 class Query < ActiveRecord::Base
85 class Query < ActiveRecord::Base
86 class StatementInvalid < ::ActiveRecord::StatementInvalid
86 class StatementInvalid < ::ActiveRecord::StatementInvalid
87 end
87 end
88
88
89 belongs_to :project
89 belongs_to :project
90 belongs_to :user
90 belongs_to :user
91 serialize :filters
91 serialize :filters
92 serialize :column_names
92 serialize :column_names
93 serialize :sort_criteria, Array
93 serialize :sort_criteria, Array
94
94
95 attr_protected :project_id, :user_id
95 attr_protected :project_id, :user_id
96
96
97 validates_presence_of :name
97 validates_presence_of :name
98 validates_length_of :name, :maximum => 255
98 validates_length_of :name, :maximum => 255
99 validate :validate_query_filters
99 validate :validate_query_filters
100
100
101 @@operators = { "=" => :label_equals,
101 @@operators = { "=" => :label_equals,
102 "!" => :label_not_equals,
102 "!" => :label_not_equals,
103 "o" => :label_open_issues,
103 "o" => :label_open_issues,
104 "c" => :label_closed_issues,
104 "c" => :label_closed_issues,
105 "!*" => :label_none,
105 "!*" => :label_none,
106 "*" => :label_all,
106 "*" => :label_all,
107 ">=" => :label_greater_or_equal,
107 ">=" => :label_greater_or_equal,
108 "<=" => :label_less_or_equal,
108 "<=" => :label_less_or_equal,
109 "><" => :label_between,
109 "><" => :label_between,
110 "<t+" => :label_in_less_than,
110 "<t+" => :label_in_less_than,
111 ">t+" => :label_in_more_than,
111 ">t+" => :label_in_more_than,
112 "t+" => :label_in,
112 "t+" => :label_in,
113 "t" => :label_today,
113 "t" => :label_today,
114 "w" => :label_this_week,
114 "w" => :label_this_week,
115 ">t-" => :label_less_than_ago,
115 ">t-" => :label_less_than_ago,
116 "<t-" => :label_more_than_ago,
116 "<t-" => :label_more_than_ago,
117 "t-" => :label_ago,
117 "t-" => :label_ago,
118 "~" => :label_contains,
118 "~" => :label_contains,
119 "!~" => :label_not_contains }
119 "!~" => :label_not_contains }
120
120
121 cattr_reader :operators
121 cattr_reader :operators
122
122
123 @@operators_by_filter_type = { :list => [ "=", "!" ],
123 @@operators_by_filter_type = { :list => [ "=", "!" ],
124 :list_status => [ "o", "=", "!", "c", "*" ],
124 :list_status => [ "o", "=", "!", "c", "*" ],
125 :list_optional => [ "=", "!", "!*", "*" ],
125 :list_optional => [ "=", "!", "!*", "*" ],
126 :list_subprojects => [ "*", "!*", "=" ],
126 :list_subprojects => [ "*", "!*", "=" ],
127 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ],
127 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ],
128 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ],
128 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ],
129 :string => [ "=", "~", "!", "!~", "!*", "*" ],
129 :string => [ "=", "~", "!", "!~", "!*", "*" ],
130 :text => [ "~", "!~", "!*", "*" ],
130 :text => [ "~", "!~", "!*", "*" ],
131 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
131 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
132 :float => [ "=", ">=", "<=", "><", "!*", "*" ] }
132 :float => [ "=", ">=", "<=", "><", "!*", "*" ] }
133
133
134 cattr_reader :operators_by_filter_type
134 cattr_reader :operators_by_filter_type
135
135
136 @@available_columns = [
136 @@available_columns = [
137 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
137 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
138 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
138 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
139 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
139 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
140 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
140 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
141 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
141 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
142 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
142 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
143 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
143 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
144 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
144 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
145 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
145 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
146 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
146 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
147 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
147 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
148 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
148 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
149 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
149 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
150 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
150 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
151 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
151 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
152 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
152 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
153 ]
153 ]
154 cattr_reader :available_columns
154 cattr_reader :available_columns
155
155
156 scope :visible, lambda {|*args|
156 scope :visible, lambda {|*args|
157 user = args.shift || User.current
157 user = args.shift || User.current
158 base = Project.allowed_to_condition(user, :view_issues, *args)
158 base = Project.allowed_to_condition(user, :view_issues, *args)
159 user_id = user.logged? ? user.id : 0
159 user_id = user.logged? ? user.id : 0
160 {
160 {
161 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
161 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
162 :include => :project
162 :include => :project
163 }
163 }
164 }
164 }
165
165
166 def initialize(attributes=nil, *args)
166 def initialize(attributes=nil, *args)
167 super attributes
167 super attributes
168 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
168 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
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 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
177 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
178 when :float
178 when :float
179 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+(\.\d*)?$/) }
179 add_filter_error(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 add_filter_error(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 add_filter_error(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 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
185 add_filter_error(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 add_filter_error(field, :blank) unless
190 add_filter_error(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 def add_filter_error(field, message)
198 def add_filter_error(field, message)
199 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
199 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
200 errors.add(:base, m)
200 errors.add(:base, m)
201 end
201 end
202
202
203 # Returns true if the query is visible to +user+ or the current user.
203 # Returns true if the query is visible to +user+ or the current user.
204 def visible?(user=User.current)
204 def visible?(user=User.current)
205 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
205 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
206 end
206 end
207
207
208 def editable_by?(user)
208 def editable_by?(user)
209 return false unless user
209 return false unless user
210 # Admin can edit them all and regular users can edit their private queries
210 # Admin can edit them all and regular users can edit their private queries
211 return true if user.admin? || (!is_public && self.user_id == user.id)
211 return true if user.admin? || (!is_public && self.user_id == user.id)
212 # Members can not edit public queries that are for all project (only admin is allowed to)
212 # Members can not edit public queries that are for all project (only admin is allowed to)
213 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
213 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
214 end
214 end
215
215
216 def available_filters
216 def available_filters
217 return @available_filters if @available_filters
217 return @available_filters if @available_filters
218
218
219 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
219 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
220
220
221 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
221 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
222 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
222 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
223 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
223 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
224 "subject" => { :type => :text, :order => 8 },
224 "subject" => { :type => :text, :order => 8 },
225 "created_on" => { :type => :date_past, :order => 9 },
225 "created_on" => { :type => :date_past, :order => 9 },
226 "updated_on" => { :type => :date_past, :order => 10 },
226 "updated_on" => { :type => :date_past, :order => 10 },
227 "start_date" => { :type => :date, :order => 11 },
227 "start_date" => { :type => :date, :order => 11 },
228 "due_date" => { :type => :date, :order => 12 },
228 "due_date" => { :type => :date, :order => 12 },
229 "estimated_hours" => { :type => :float, :order => 13 },
229 "estimated_hours" => { :type => :float, :order => 13 },
230 "done_ratio" => { :type => :integer, :order => 14 }}
230 "done_ratio" => { :type => :integer, :order => 14 }}
231
231
232 principals = []
232 principals = []
233 if project
233 if project
234 principals += project.principals.sort
234 principals += project.principals.sort
235 unless project.leaf?
235 unless project.leaf?
236 subprojects = project.descendants.visible.all
236 subprojects = project.descendants.visible.all
237 if subprojects.any?
237 if subprojects.any?
238 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
238 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
239 principals += Principal.member_of(subprojects)
239 principals += Principal.member_of(subprojects)
240 end
240 end
241 end
241 end
242 else
242 else
243 all_projects = Project.visible.all
243 all_projects = Project.visible.all
244 if all_projects.any?
244 if all_projects.any?
245 # members of visible projects
245 # members of visible projects
246 principals += Principal.member_of(all_projects)
246 principals += Principal.member_of(all_projects)
247
247
248 # project filter
248 # project filter
249 project_values = []
249 project_values = []
250 if User.current.logged? && User.current.memberships.any?
250 if User.current.logged? && User.current.memberships.any?
251 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
251 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
252 end
252 end
253 Project.project_tree(all_projects) do |p, level|
253 Project.project_tree(all_projects) do |p, level|
254 prefix = (level > 0 ? ('--' * level + ' ') : '')
254 prefix = (level > 0 ? ('--' * level + ' ') : '')
255 project_values << ["#{prefix}#{p.name}", p.id.to_s]
255 project_values << ["#{prefix}#{p.name}", p.id.to_s]
256 end
256 end
257 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
257 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
258 end
258 end
259 end
259 end
260 principals.uniq!
260 principals.uniq!
261 principals.sort!
261 principals.sort!
262 users = principals.select {|p| p.is_a?(User)}
262 users = principals.select {|p| p.is_a?(User)}
263
263
264 assigned_to_values = []
264 assigned_to_values = []
265 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
265 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
266 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
266 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
267 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty?
267 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty?
268
268
269 author_values = []
269 author_values = []
270 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
270 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
271 author_values += users.collect{|s| [s.name, s.id.to_s] }
271 author_values += users.collect{|s| [s.name, s.id.to_s] }
272 @available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty?
272 @available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty?
273
273
274 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
274 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
275 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
275 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
276
276
277 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
277 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
278 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
278 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
279
279
280 if User.current.logged?
280 if User.current.logged?
281 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
281 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
282 end
282 end
283
283
284 if project
284 if project
285 # project specific filters
285 # project specific filters
286 categories = project.issue_categories.all
286 categories = project.issue_categories.all
287 unless categories.empty?
287 unless categories.empty?
288 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
288 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
289 end
289 end
290 versions = project.shared_versions.all
290 versions = project.shared_versions.all
291 unless versions.empty?
291 unless versions.empty?
292 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
292 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
293 end
293 end
294 add_custom_fields_filters(project.all_issue_custom_fields)
294 add_custom_fields_filters(project.all_issue_custom_fields)
295 else
295 else
296 # global filters for cross project issue list
296 # global filters for cross project issue list
297 system_shared_versions = Version.visible.find_all_by_sharing('system')
297 system_shared_versions = Version.visible.find_all_by_sharing('system')
298 unless system_shared_versions.empty?
298 unless system_shared_versions.empty?
299 @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] } }
299 @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] } }
300 end
300 end
301 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
301 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
302 end
302 end
303 @available_filters
303 @available_filters
304 end
304 end
305
305
306 def add_filter(field, operator, values)
306 def add_filter(field, operator, values)
307 # values must be an array
307 # values must be an array
308 return unless values.nil? || values.is_a?(Array)
308 return unless values.nil? || values.is_a?(Array)
309 # check if field is defined as an available filter
309 # check if field is defined as an available filter
310 if available_filters.has_key? field
310 if available_filters.has_key? field
311 filter_options = available_filters[field]
311 filter_options = available_filters[field]
312 # check if operator is allowed for that filter
312 # check if operator is allowed for that filter
313 #if @@operators_by_filter_type[filter_options[:type]].include? operator
313 #if @@operators_by_filter_type[filter_options[:type]].include? operator
314 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
314 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
315 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
315 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
316 #end
316 #end
317 filters[field] = {:operator => operator, :values => (values || [''])}
317 filters[field] = {:operator => operator, :values => (values || [''])}
318 end
318 end
319 end
319 end
320
320
321 def add_short_filter(field, expression)
321 def add_short_filter(field, expression)
322 return unless expression && available_filters.has_key?(field)
322 return unless expression && available_filters.has_key?(field)
323 field_type = available_filters[field][:type]
323 field_type = available_filters[field][:type]
324 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
324 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
325 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
325 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
326 add_filter field, operator, $1.present? ? $1.split('|') : ['']
326 add_filter field, operator, $1.present? ? $1.split('|') : ['']
327 end || add_filter(field, '=', expression.split('|'))
327 end || add_filter(field, '=', expression.split('|'))
328 end
328 end
329
329
330 # Add multiple filters using +add_filter+
330 # Add multiple filters using +add_filter+
331 def add_filters(fields, operators, values)
331 def add_filters(fields, operators, values)
332 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
332 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
333 fields.each do |field|
333 fields.each do |field|
334 add_filter(field, operators[field], values && values[field])
334 add_filter(field, operators[field], values && values[field])
335 end
335 end
336 end
336 end
337 end
337 end
338
338
339 def has_filter?(field)
339 def has_filter?(field)
340 filters and filters[field]
340 filters and filters[field]
341 end
341 end
342
342
343 def type_for(field)
343 def type_for(field)
344 available_filters[field][:type] if available_filters.has_key?(field)
344 available_filters[field][:type] if available_filters.has_key?(field)
345 end
345 end
346
346
347 def operator_for(field)
347 def operator_for(field)
348 has_filter?(field) ? filters[field][:operator] : nil
348 has_filter?(field) ? filters[field][:operator] : nil
349 end
349 end
350
350
351 def values_for(field)
351 def values_for(field)
352 has_filter?(field) ? filters[field][:values] : nil
352 has_filter?(field) ? filters[field][:values] : nil
353 end
353 end
354
354
355 def value_for(field, index=0)
355 def value_for(field, index=0)
356 (values_for(field) || [])[index]
356 (values_for(field) || [])[index]
357 end
357 end
358
358
359 def label_for(field)
359 def label_for(field)
360 label = available_filters[field][:name] if available_filters.has_key?(field)
360 label = available_filters[field][:name] if available_filters.has_key?(field)
361 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
361 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
362 end
362 end
363
363
364 def available_columns
364 def available_columns
365 return @available_columns if @available_columns
365 return @available_columns if @available_columns
366 @available_columns = ::Query.available_columns.dup
366 @available_columns = ::Query.available_columns.dup
367 @available_columns += (project ?
367 @available_columns += (project ?
368 project.all_issue_custom_fields :
368 project.all_issue_custom_fields :
369 IssueCustomField.find(:all)
369 IssueCustomField.find(:all)
370 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
370 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
371
371
372 if User.current.allowed_to?(:view_time_entries, project, :global => true)
372 if User.current.allowed_to?(:view_time_entries, project, :global => true)
373 index = nil
373 index = nil
374 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
374 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
375 index = (index ? index + 1 : -1)
375 index = (index ? index + 1 : -1)
376 # insert the column after estimated_hours or at the end
376 # insert the column after estimated_hours or at the end
377 @available_columns.insert index, QueryColumn.new(:spent_hours,
377 @available_columns.insert index, QueryColumn.new(:spent_hours,
378 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
378 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
379 :default_order => 'desc',
379 :default_order => 'desc',
380 :caption => :label_spent_time
380 :caption => :label_spent_time
381 )
381 )
382 end
382 end
383 @available_columns
383 @available_columns
384 end
384 end
385
385
386 def self.available_columns=(v)
386 def self.available_columns=(v)
387 self.available_columns = (v)
387 self.available_columns = (v)
388 end
388 end
389
389
390 def self.add_available_column(column)
390 def self.add_available_column(column)
391 self.available_columns << (column) if column.is_a?(QueryColumn)
391 self.available_columns << (column) if column.is_a?(QueryColumn)
392 end
392 end
393
393
394 # Returns an array of columns that can be used to group the results
394 # Returns an array of columns that can be used to group the results
395 def groupable_columns
395 def groupable_columns
396 available_columns.select {|c| c.groupable}
396 available_columns.select {|c| c.groupable}
397 end
397 end
398
398
399 # Returns a Hash of columns and the key for sorting
399 # Returns a Hash of columns and the key for sorting
400 def sortable_columns
400 def sortable_columns
401 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
401 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
402 h[column.name.to_s] = column.sortable
402 h[column.name.to_s] = column.sortable
403 h
403 h
404 })
404 })
405 end
405 end
406
406
407 def columns
407 def columns
408 # preserve the column_names order
408 # preserve the column_names order
409 (has_default_columns? ? default_columns_names : column_names).collect do |name|
409 (has_default_columns? ? default_columns_names : column_names).collect do |name|
410 available_columns.find { |col| col.name == name }
410 available_columns.find { |col| col.name == name }
411 end.compact
411 end.compact
412 end
412 end
413
413
414 def default_columns_names
414 def default_columns_names
415 @default_columns_names ||= begin
415 @default_columns_names ||= begin
416 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
416 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
417
417
418 project.present? ? default_columns : [:project] | default_columns
418 project.present? ? default_columns : [:project] | default_columns
419 end
419 end
420 end
420 end
421
421
422 def column_names=(names)
422 def column_names=(names)
423 if names
423 if names
424 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
424 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
425 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
425 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
426 # Set column_names to nil if default columns
426 # Set column_names to nil if default columns
427 if names == default_columns_names
427 if names == default_columns_names
428 names = nil
428 names = nil
429 end
429 end
430 end
430 end
431 write_attribute(:column_names, names)
431 write_attribute(:column_names, names)
432 end
432 end
433
433
434 def has_column?(column)
434 def has_column?(column)
435 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
435 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
436 end
436 end
437
437
438 def has_default_columns?
438 def has_default_columns?
439 column_names.nil? || column_names.empty?
439 column_names.nil? || column_names.empty?
440 end
440 end
441
441
442 def sort_criteria=(arg)
442 def sort_criteria=(arg)
443 c = []
443 c = []
444 if arg.is_a?(Hash)
444 if arg.is_a?(Hash)
445 arg = arg.keys.sort.collect {|k| arg[k]}
445 arg = arg.keys.sort.collect {|k| arg[k]}
446 end
446 end
447 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
447 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
448 write_attribute(:sort_criteria, c)
448 write_attribute(:sort_criteria, c)
449 end
449 end
450
450
451 def sort_criteria
451 def sort_criteria
452 read_attribute(:sort_criteria) || []
452 read_attribute(:sort_criteria) || []
453 end
453 end
454
454
455 def sort_criteria_key(arg)
455 def sort_criteria_key(arg)
456 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
456 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
457 end
457 end
458
458
459 def sort_criteria_order(arg)
459 def sort_criteria_order(arg)
460 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
460 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
461 end
461 end
462
462
463 # Returns the SQL sort order that should be prepended for grouping
463 # Returns the SQL sort order that should be prepended for grouping
464 def group_by_sort_order
464 def group_by_sort_order
465 if grouped? && (column = group_by_column)
465 if grouped? && (column = group_by_column)
466 column.sortable.is_a?(Array) ?
466 column.sortable.is_a?(Array) ?
467 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
467 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
468 "#{column.sortable} #{column.default_order}"
468 "#{column.sortable} #{column.default_order}"
469 end
469 end
470 end
470 end
471
471
472 # Returns true if the query is a grouped query
472 # Returns true if the query is a grouped query
473 def grouped?
473 def grouped?
474 !group_by_column.nil?
474 !group_by_column.nil?
475 end
475 end
476
476
477 def group_by_column
477 def group_by_column
478 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
478 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
479 end
479 end
480
480
481 def group_by_statement
481 def group_by_statement
482 group_by_column.try(:groupable)
482 group_by_column.try(:groupable)
483 end
483 end
484
484
485 def project_statement
485 def project_statement
486 project_clauses = []
486 project_clauses = []
487 if project && !project.descendants.active.empty?
487 if project && !project.descendants.active.empty?
488 ids = [project.id]
488 ids = [project.id]
489 if has_filter?("subproject_id")
489 if has_filter?("subproject_id")
490 case operator_for("subproject_id")
490 case operator_for("subproject_id")
491 when '='
491 when '='
492 # include the selected subprojects
492 # include the selected subprojects
493 ids += values_for("subproject_id").each(&:to_i)
493 ids += values_for("subproject_id").each(&:to_i)
494 when '!*'
494 when '!*'
495 # main project only
495 # main project only
496 else
496 else
497 # all subprojects
497 # all subprojects
498 ids += project.descendants.collect(&:id)
498 ids += project.descendants.collect(&:id)
499 end
499 end
500 elsif Setting.display_subprojects_issues?
500 elsif Setting.display_subprojects_issues?
501 ids += project.descendants.collect(&:id)
501 ids += project.descendants.collect(&:id)
502 end
502 end
503 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
503 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
504 elsif project
504 elsif project
505 project_clauses << "#{Project.table_name}.id = %d" % project.id
505 project_clauses << "#{Project.table_name}.id = %d" % project.id
506 end
506 end
507 project_clauses.any? ? project_clauses.join(' AND ') : nil
507 project_clauses.any? ? project_clauses.join(' AND ') : nil
508 end
508 end
509
509
510 def statement
510 def statement
511 # filters clauses
511 # filters clauses
512 filters_clauses = []
512 filters_clauses = []
513 filters.each_key do |field|
513 filters.each_key do |field|
514 next if field == "subproject_id"
514 next if field == "subproject_id"
515 v = values_for(field).clone
515 v = values_for(field).clone
516 next unless v and !v.empty?
516 next unless v and !v.empty?
517 operator = operator_for(field)
517 operator = operator_for(field)
518
518
519 # "me" value subsitution
519 # "me" value subsitution
520 if %w(assigned_to_id author_id watcher_id).include?(field)
520 if %w(assigned_to_id author_id watcher_id).include?(field)
521 if v.delete("me")
521 if v.delete("me")
522 if User.current.logged?
522 if User.current.logged?
523 v.push(User.current.id.to_s)
523 v.push(User.current.id.to_s)
524 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
524 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
525 else
525 else
526 v.push("0")
526 v.push("0")
527 end
527 end
528 end
528 end
529 end
529 end
530
530
531 if field == 'project_id'
531 if field == 'project_id'
532 if v.delete('mine')
532 if v.delete('mine')
533 v += User.current.memberships.map(&:project_id).map(&:to_s)
533 v += User.current.memberships.map(&:project_id).map(&:to_s)
534 end
534 end
535 end
535 end
536
536
537 if field =~ /^cf_(\d+)$/
537 if field =~ /^cf_(\d+)$/
538 # custom field
538 # custom field
539 filters_clauses << sql_for_custom_field(field, operator, v, $1)
539 filters_clauses << sql_for_custom_field(field, operator, v, $1)
540 elsif respond_to?("sql_for_#{field}_field")
540 elsif respond_to?("sql_for_#{field}_field")
541 # specific statement
541 # specific statement
542 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
542 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
543 else
543 else
544 # regular field
544 # regular field
545 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
545 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
546 end
546 end
547 end if filters and valid?
547 end if filters and valid?
548
548
549 filters_clauses << project_statement
549 filters_clauses << project_statement
550 filters_clauses.reject!(&:blank?)
550 filters_clauses.reject!(&:blank?)
551
551
552 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
552 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
553 end
553 end
554
554
555 # Returns the issue count
555 # Returns the issue count
556 def issue_count
556 def issue_count
557 Issue.visible.count(:include => [:status, :project], :conditions => statement)
557 Issue.visible.count(:include => [:status, :project], :conditions => statement)
558 rescue ::ActiveRecord::StatementInvalid => e
558 rescue ::ActiveRecord::StatementInvalid => e
559 raise StatementInvalid.new(e.message)
559 raise StatementInvalid.new(e.message)
560 end
560 end
561
561
562 # Returns the issue count by group or nil if query is not grouped
562 # Returns the issue count by group or nil if query is not grouped
563 def issue_count_by_group
563 def issue_count_by_group
564 r = nil
564 r = nil
565 if grouped?
565 if grouped?
566 begin
566 begin
567 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
567 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
568 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
568 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
569 rescue ActiveRecord::RecordNotFound
569 rescue ActiveRecord::RecordNotFound
570 r = {nil => issue_count}
570 r = {nil => issue_count}
571 end
571 end
572 c = group_by_column
572 c = group_by_column
573 if c.is_a?(QueryCustomFieldColumn)
573 if c.is_a?(QueryCustomFieldColumn)
574 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
574 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
575 end
575 end
576 end
576 end
577 r
577 r
578 rescue ::ActiveRecord::StatementInvalid => e
578 rescue ::ActiveRecord::StatementInvalid => e
579 raise StatementInvalid.new(e.message)
579 raise StatementInvalid.new(e.message)
580 end
580 end
581
581
582 # Returns the issues
582 # Returns the issues
583 # Valid options are :order, :offset, :limit, :include, :conditions
583 # Valid options are :order, :offset, :limit, :include, :conditions
584 def issues(options={})
584 def issues(options={})
585 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
585 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
586 order_option = nil if order_option.blank?
586 order_option = nil if order_option.blank?
587
587
588 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
588 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
589
589
590 issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
590 issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
591 :conditions => statement,
591 :conditions => statement,
592 :order => order_option,
592 :order => order_option,
593 :joins => joins,
593 :joins => joins,
594 :limit => options[:limit],
594 :limit => options[:limit],
595 :offset => options[:offset]
595 :offset => options[:offset]
596
596
597 if has_column?(:spent_hours)
597 if has_column?(:spent_hours)
598 Issue.load_visible_spent_hours(issues)
598 Issue.load_visible_spent_hours(issues)
599 end
599 end
600 issues
600 issues
601 rescue ::ActiveRecord::StatementInvalid => e
601 rescue ::ActiveRecord::StatementInvalid => e
602 raise StatementInvalid.new(e.message)
602 raise StatementInvalid.new(e.message)
603 end
603 end
604
604
605 # Returns the issues ids
605 # Returns the issues ids
606 def issue_ids(options={})
606 def issue_ids(options={})
607 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
607 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
608 order_option = nil if order_option.blank?
608 order_option = nil if order_option.blank?
609
609
610 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
610 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
611
611
612 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
612 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
613 :conditions => statement,
613 :conditions => statement,
614 :order => order_option,
614 :order => order_option,
615 :joins => joins,
615 :joins => joins,
616 :limit => options[:limit],
616 :limit => options[:limit],
617 :offset => options[:offset]).find_ids
617 :offset => options[:offset]).find_ids
618 rescue ::ActiveRecord::StatementInvalid => e
618 rescue ::ActiveRecord::StatementInvalid => e
619 raise StatementInvalid.new(e.message)
619 raise StatementInvalid.new(e.message)
620 end
620 end
621
621
622 # Returns the journals
622 # Returns the journals
623 # Valid options are :order, :offset, :limit
623 # Valid options are :order, :offset, :limit
624 def journals(options={})
624 def journals(options={})
625 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
625 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
626 :conditions => statement,
626 :conditions => statement,
627 :order => options[:order],
627 :order => options[:order],
628 :limit => options[:limit],
628 :limit => options[:limit],
629 :offset => options[:offset]
629 :offset => options[:offset]
630 rescue ::ActiveRecord::StatementInvalid => e
630 rescue ::ActiveRecord::StatementInvalid => e
631 raise StatementInvalid.new(e.message)
631 raise StatementInvalid.new(e.message)
632 end
632 end
633
633
634 # Returns the versions
634 # Returns the versions
635 # Valid options are :conditions
635 # Valid options are :conditions
636 def versions(options={})
636 def versions(options={})
637 Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
637 Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
638 rescue ::ActiveRecord::StatementInvalid => e
638 rescue ::ActiveRecord::StatementInvalid => e
639 raise StatementInvalid.new(e.message)
639 raise StatementInvalid.new(e.message)
640 end
640 end
641
641
642 def sql_for_watcher_id_field(field, operator, value)
642 def sql_for_watcher_id_field(field, operator, value)
643 db_table = Watcher.table_name
643 db_table = Watcher.table_name
644 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
644 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
645 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
645 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
646 end
646 end
647
647
648 def sql_for_member_of_group_field(field, operator, value)
648 def sql_for_member_of_group_field(field, operator, value)
649 if operator == '*' # Any group
649 if operator == '*' # Any group
650 groups = Group.all
650 groups = Group.all
651 operator = '=' # Override the operator since we want to find by assigned_to
651 operator = '=' # Override the operator since we want to find by assigned_to
652 elsif operator == "!*"
652 elsif operator == "!*"
653 groups = Group.all
653 groups = Group.all
654 operator = '!' # Override the operator since we want to find by assigned_to
654 operator = '!' # Override the operator since we want to find by assigned_to
655 else
655 else
656 groups = Group.find_all_by_id(value)
656 groups = Group.find_all_by_id(value)
657 end
657 end
658 groups ||= []
658 groups ||= []
659
659
660 members_of_groups = groups.inject([]) {|user_ids, group|
660 members_of_groups = groups.inject([]) {|user_ids, group|
661 if group && group.user_ids.present?
661 if group && group.user_ids.present?
662 user_ids << group.user_ids
662 user_ids << group.user_ids
663 end
663 end
664 user_ids.flatten.uniq.compact
664 user_ids.flatten.uniq.compact
665 }.sort.collect(&:to_s)
665 }.sort.collect(&:to_s)
666
666
667 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
667 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
668 end
668 end
669
669
670 def sql_for_assigned_to_role_field(field, operator, value)
670 def sql_for_assigned_to_role_field(field, operator, value)
671 case operator
671 case operator
672 when "*", "!*" # Member / Not member
672 when "*", "!*" # Member / Not member
673 sw = operator == "!*" ? 'NOT' : ''
673 sw = operator == "!*" ? 'NOT' : ''
674 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
674 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
675 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
675 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
676 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
676 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
677 when "=", "!"
677 when "=", "!"
678 role_cond = value.any? ?
678 role_cond = value.any? ?
679 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
679 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
680 "1=0"
680 "1=0"
681
681
682 sw = operator == "!" ? 'NOT' : ''
682 sw = operator == "!" ? 'NOT' : ''
683 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
683 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
684 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
684 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
685 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
685 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
686 end
686 end
687 end
687 end
688
688
689 private
689 private
690
690
691 def sql_for_custom_field(field, operator, value, custom_field_id)
691 def sql_for_custom_field(field, operator, value, custom_field_id)
692 db_table = CustomValue.table_name
692 db_table = CustomValue.table_name
693 db_field = 'value'
693 db_field = 'value'
694 filter = @available_filters[field]
694 filter = @available_filters[field]
695 if filter && filter[:format] == 'user'
695 if filter && filter[:format] == 'user'
696 if value.delete('me')
696 if value.delete('me')
697 value.push User.current.id.to_s
697 value.push User.current.id.to_s
698 end
698 end
699 end
699 end
700 not_in = nil
700 not_in = nil
701 if operator == '!'
701 if operator == '!'
702 # Makes ! operator work for custom fields with multiple values
702 # Makes ! operator work for custom fields with multiple values
703 operator = '='
703 operator = '='
704 not_in = 'NOT'
704 not_in = 'NOT'
705 end
705 end
706 "#{Issue.table_name}.id #{not_in} 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 " +
706 "#{Issue.table_name}.id #{not_in} 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 " +
707 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
707 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
708 end
708 end
709
709
710 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
710 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
711 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
711 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
712 sql = ''
712 sql = ''
713 case operator
713 case operator
714 when "="
714 when "="
715 if value.any?
715 if value.any?
716 case type_for(field)
716 case type_for(field)
717 when :date, :date_past
717 when :date, :date_past
718 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
718 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
719 when :integer
719 when :integer
720 if is_custom_filter
720 if is_custom_filter
721 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
721 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
722 else
722 else
723 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
723 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
724 end
724 end
725 when :float
725 when :float
726 if is_custom_filter
726 if is_custom_filter
727 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
727 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
728 else
728 else
729 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
729 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
730 end
730 end
731 else
731 else
732 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
732 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
733 end
733 end
734 else
734 else
735 # IN an empty set
735 # IN an empty set
736 sql = "1=0"
736 sql = "1=0"
737 end
737 end
738 when "!"
738 when "!"
739 if value.any?
739 if value.any?
740 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
740 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
741 else
741 else
742 # NOT IN an empty set
742 # NOT IN an empty set
743 sql = "1=1"
743 sql = "1=1"
744 end
744 end
745 when "!*"
745 when "!*"
746 sql = "#{db_table}.#{db_field} IS NULL"
746 sql = "#{db_table}.#{db_field} IS NULL"
747 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
747 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
748 when "*"
748 when "*"
749 sql = "#{db_table}.#{db_field} IS NOT NULL"
749 sql = "#{db_table}.#{db_field} IS NOT NULL"
750 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
750 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
751 when ">="
751 when ">="
752 if [:date, :date_past].include?(type_for(field))
752 if [:date, :date_past].include?(type_for(field))
753 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
753 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
754 else
754 else
755 if is_custom_filter
755 if is_custom_filter
756 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
756 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
757 else
757 else
758 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
758 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
759 end
759 end
760 end
760 end
761 when "<="
761 when "<="
762 if [:date, :date_past].include?(type_for(field))
762 if [:date, :date_past].include?(type_for(field))
763 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
763 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
764 else
764 else
765 if is_custom_filter
765 if is_custom_filter
766 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
766 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
767 else
767 else
768 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
768 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
769 end
769 end
770 end
770 end
771 when "><"
771 when "><"
772 if [:date, :date_past].include?(type_for(field))
772 if [:date, :date_past].include?(type_for(field))
773 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
773 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
774 else
774 else
775 if is_custom_filter
775 if is_custom_filter
776 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
776 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
777 else
777 else
778 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
778 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
779 end
779 end
780 end
780 end
781 when "o"
781 when "o"
782 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
782 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
783 when "c"
783 when "c"
784 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
784 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
785 when ">t-"
785 when ">t-"
786 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
786 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
787 when "<t-"
787 when "<t-"
788 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
788 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
789 when "t-"
789 when "t-"
790 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
790 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
791 when ">t+"
791 when ">t+"
792 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
792 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
793 when "<t+"
793 when "<t+"
794 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
794 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
795 when "t+"
795 when "t+"
796 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
796 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
797 when "t"
797 when "t"
798 sql = relative_date_clause(db_table, db_field, 0, 0)
798 sql = relative_date_clause(db_table, db_field, 0, 0)
799 when "w"
799 when "w"
800 first_day_of_week = l(:general_first_day_of_week).to_i
800 first_day_of_week = l(:general_first_day_of_week).to_i
801 day_of_week = Date.today.cwday
801 day_of_week = Date.today.cwday
802 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
802 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
803 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
803 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
804 when "~"
804 when "~"
805 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
805 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
806 when "!~"
806 when "!~"
807 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
807 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
808 else
808 else
809 raise "Unknown query operator #{operator}"
809 raise "Unknown query operator #{operator}"
810 end
810 end
811
811
812 return sql
812 return sql
813 end
813 end
814
814
815 def add_custom_fields_filters(custom_fields)
815 def add_custom_fields_filters(custom_fields)
816 @available_filters ||= {}
816 @available_filters ||= {}
817
817
818 custom_fields.select(&:is_filter?).each do |field|
818 custom_fields.select(&:is_filter?).each do |field|
819 case field.field_format
819 case field.field_format
820 when "text"
820 when "text"
821 options = { :type => :text, :order => 20 }
821 options = { :type => :text, :order => 20 }
822 when "list"
822 when "list"
823 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
823 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
824 when "date"
824 when "date"
825 options = { :type => :date, :order => 20 }
825 options = { :type => :date, :order => 20 }
826 when "bool"
826 when "bool"
827 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
827 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
828 when "int"
828 when "int"
829 options = { :type => :integer, :order => 20 }
829 options = { :type => :integer, :order => 20 }
830 when "float"
830 when "float"
831 options = { :type => :float, :order => 20 }
831 options = { :type => :float, :order => 20 }
832 when "user", "version"
832 when "user", "version"
833 next unless project
833 next unless project
834 values = field.possible_values_options(project)
834 values = field.possible_values_options(project)
835 if User.current.logged? && field.field_format == 'user'
835 if User.current.logged? && field.field_format == 'user'
836 values.unshift ["<< #{l(:label_me)} >>", "me"]
836 values.unshift ["<< #{l(:label_me)} >>", "me"]
837 end
837 end
838 options = { :type => :list_optional, :values => values, :order => 20}
838 options = { :type => :list_optional, :values => values, :order => 20}
839 else
839 else
840 options = { :type => :string, :order => 20 }
840 options = { :type => :string, :order => 20 }
841 end
841 end
842 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name, :format => field.field_format })
842 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name, :format => field.field_format })
843 end
843 end
844 end
844 end
845
845
846 # Returns a SQL clause for a date or datetime field.
846 # Returns a SQL clause for a date or datetime field.
847 def date_clause(table, field, from, to)
847 def date_clause(table, field, from, to)
848 s = []
848 s = []
849 if from
849 if from
850 from_yesterday = from - 1
850 from_yesterday = from - 1
851 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
851 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
852 if self.class.default_timezone == :utc
852 if self.class.default_timezone == :utc
853 from_yesterday_time = from_yesterday_time.utc
853 from_yesterday_time = from_yesterday_time.utc
854 end
854 end
855 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
855 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
856 end
856 end
857 if to
857 if to
858 to_time = Time.local(to.year, to.month, to.day)
858 to_time = Time.local(to.year, to.month, to.day)
859 if self.class.default_timezone == :utc
859 if self.class.default_timezone == :utc
860 to_time = to_time.utc
860 to_time = to_time.utc
861 end
861 end
862 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
862 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
863 end
863 end
864 s.join(' AND ')
864 s.join(' AND ')
865 end
865 end
866
866
867 # Returns a SQL clause for a date or datetime field using relative dates.
867 # Returns a SQL clause for a date or datetime field using relative dates.
868 def relative_date_clause(table, field, days_from, days_to)
868 def relative_date_clause(table, field, days_from, days_to)
869 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
869 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
870 end
870 end
871 end
871 end
@@ -1,1001 +1,1029
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 :custom_fields_trackers
27 :custom_fields_trackers
28
28
29 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
29 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
30 query = Query.new(:project => nil, :name => '_')
30 query = Query.new(:project => nil, :name => '_')
31 assert query.available_filters.has_key?('cf_1')
31 assert query.available_filters.has_key?('cf_1')
32 assert !query.available_filters.has_key?('cf_3')
32 assert !query.available_filters.has_key?('cf_3')
33 end
33 end
34
34
35 def test_system_shared_versions_should_be_available_in_global_queries
35 def test_system_shared_versions_should_be_available_in_global_queries
36 Version.find(2).update_attribute :sharing, 'system'
36 Version.find(2).update_attribute :sharing, 'system'
37 query = Query.new(:project => nil, :name => '_')
37 query = Query.new(:project => nil, :name => '_')
38 assert query.available_filters.has_key?('fixed_version_id')
38 assert query.available_filters.has_key?('fixed_version_id')
39 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
39 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
40 end
40 end
41
41
42 def test_project_filter_in_global_queries
42 def test_project_filter_in_global_queries
43 query = Query.new(:project => nil, :name => '_')
43 query = Query.new(:project => nil, :name => '_')
44 project_filter = query.available_filters["project_id"]
44 project_filter = query.available_filters["project_id"]
45 assert_not_nil project_filter
45 assert_not_nil project_filter
46 project_ids = project_filter[:values].map{|p| p[1]}
46 project_ids = project_filter[:values].map{|p| p[1]}
47 assert project_ids.include?("1") #public project
47 assert project_ids.include?("1") #public project
48 assert !project_ids.include?("2") #private project user cannot see
48 assert !project_ids.include?("2") #private project user cannot see
49 end
49 end
50
50
51 def find_issues_with_query(query)
51 def find_issues_with_query(query)
52 Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
52 Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
53 query.statement
53 query.statement
54 ).all
54 ).all
55 end
55 end
56
56
57 def assert_find_issues_with_query_is_successful(query)
57 def assert_find_issues_with_query_is_successful(query)
58 assert_nothing_raised do
58 assert_nothing_raised do
59 find_issues_with_query(query)
59 find_issues_with_query(query)
60 end
60 end
61 end
61 end
62
62
63 def assert_query_statement_includes(query, condition)
63 def assert_query_statement_includes(query, condition)
64 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
64 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
65 end
65 end
66
66
67 def assert_query_result(expected, query)
67 def assert_query_result(expected, query)
68 assert_nothing_raised do
68 assert_nothing_raised do
69 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
69 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
70 assert_equal expected.size, query.issue_count
70 assert_equal expected.size, query.issue_count
71 end
71 end
72 end
72 end
73
73
74 def test_query_should_allow_shared_versions_for_a_project_query
74 def test_query_should_allow_shared_versions_for_a_project_query
75 subproject_version = Version.find(4)
75 subproject_version = Version.find(4)
76 query = Query.new(:project => Project.find(1), :name => '_')
76 query = Query.new(:project => Project.find(1), :name => '_')
77 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
77 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
78
78
79 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
79 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
80 end
80 end
81
81
82 def test_query_with_multiple_custom_fields
82 def test_query_with_multiple_custom_fields
83 query = Query.find(1)
83 query = Query.find(1)
84 assert query.valid?
84 assert query.valid?
85 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
85 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
86 issues = find_issues_with_query(query)
86 issues = find_issues_with_query(query)
87 assert_equal 1, issues.length
87 assert_equal 1, issues.length
88 assert_equal Issue.find(3), issues.first
88 assert_equal Issue.find(3), issues.first
89 end
89 end
90
90
91 def test_operator_none
91 def test_operator_none
92 query = Query.new(:project => Project.find(1), :name => '_')
92 query = Query.new(:project => Project.find(1), :name => '_')
93 query.add_filter('fixed_version_id', '!*', [''])
93 query.add_filter('fixed_version_id', '!*', [''])
94 query.add_filter('cf_1', '!*', [''])
94 query.add_filter('cf_1', '!*', [''])
95 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
95 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
96 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
96 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
97 find_issues_with_query(query)
97 find_issues_with_query(query)
98 end
98 end
99
99
100 def test_operator_none_for_integer
100 def test_operator_none_for_integer
101 query = Query.new(:project => Project.find(1), :name => '_')
101 query = Query.new(:project => Project.find(1), :name => '_')
102 query.add_filter('estimated_hours', '!*', [''])
102 query.add_filter('estimated_hours', '!*', [''])
103 issues = find_issues_with_query(query)
103 issues = find_issues_with_query(query)
104 assert !issues.empty?
104 assert !issues.empty?
105 assert issues.all? {|i| !i.estimated_hours}
105 assert issues.all? {|i| !i.estimated_hours}
106 end
106 end
107
107
108 def test_operator_none_for_date
108 def test_operator_none_for_date
109 query = Query.new(:project => Project.find(1), :name => '_')
109 query = Query.new(:project => Project.find(1), :name => '_')
110 query.add_filter('start_date', '!*', [''])
110 query.add_filter('start_date', '!*', [''])
111 issues = find_issues_with_query(query)
111 issues = find_issues_with_query(query)
112 assert !issues.empty?
112 assert !issues.empty?
113 assert issues.all? {|i| i.start_date.nil?}
113 assert issues.all? {|i| i.start_date.nil?}
114 end
114 end
115
115
116 def test_operator_none_for_string_custom_field
116 def test_operator_none_for_string_custom_field
117 query = Query.new(:project => Project.find(1), :name => '_')
117 query = Query.new(:project => Project.find(1), :name => '_')
118 query.add_filter('cf_2', '!*', [''])
118 query.add_filter('cf_2', '!*', [''])
119 assert query.has_filter?('cf_2')
119 assert query.has_filter?('cf_2')
120 issues = find_issues_with_query(query)
120 issues = find_issues_with_query(query)
121 assert !issues.empty?
121 assert !issues.empty?
122 assert issues.all? {|i| i.custom_field_value(2).blank?}
122 assert issues.all? {|i| i.custom_field_value(2).blank?}
123 end
123 end
124
124
125 def test_operator_all
125 def test_operator_all
126 query = Query.new(:project => Project.find(1), :name => '_')
126 query = Query.new(:project => Project.find(1), :name => '_')
127 query.add_filter('fixed_version_id', '*', [''])
127 query.add_filter('fixed_version_id', '*', [''])
128 query.add_filter('cf_1', '*', [''])
128 query.add_filter('cf_1', '*', [''])
129 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
129 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
130 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
130 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
131 find_issues_with_query(query)
131 find_issues_with_query(query)
132 end
132 end
133
133
134 def test_operator_all_for_date
134 def test_operator_all_for_date
135 query = Query.new(:project => Project.find(1), :name => '_')
135 query = Query.new(:project => Project.find(1), :name => '_')
136 query.add_filter('start_date', '*', [''])
136 query.add_filter('start_date', '*', [''])
137 issues = find_issues_with_query(query)
137 issues = find_issues_with_query(query)
138 assert !issues.empty?
138 assert !issues.empty?
139 assert issues.all? {|i| i.start_date.present?}
139 assert issues.all? {|i| i.start_date.present?}
140 end
140 end
141
141
142 def test_operator_all_for_string_custom_field
142 def test_operator_all_for_string_custom_field
143 query = Query.new(:project => Project.find(1), :name => '_')
143 query = Query.new(:project => Project.find(1), :name => '_')
144 query.add_filter('cf_2', '*', [''])
144 query.add_filter('cf_2', '*', [''])
145 assert query.has_filter?('cf_2')
145 assert query.has_filter?('cf_2')
146 issues = find_issues_with_query(query)
146 issues = find_issues_with_query(query)
147 assert !issues.empty?
147 assert !issues.empty?
148 assert issues.all? {|i| i.custom_field_value(2).present?}
148 assert issues.all? {|i| i.custom_field_value(2).present?}
149 end
149 end
150
150
151 def test_numeric_filter_should_not_accept_non_numeric_values
151 def test_numeric_filter_should_not_accept_non_numeric_values
152 query = Query.new(:name => '_')
152 query = Query.new(:name => '_')
153 query.add_filter('estimated_hours', '=', ['a'])
153 query.add_filter('estimated_hours', '=', ['a'])
154
154
155 assert query.has_filter?('estimated_hours')
155 assert query.has_filter?('estimated_hours')
156 assert !query.valid?
156 assert !query.valid?
157 end
157 end
158
158
159 def test_operator_is_on_float
159 def test_operator_is_on_float
160 Issue.update_all("estimated_hours = 171.2", "id=2")
160 Issue.update_all("estimated_hours = 171.2", "id=2")
161
161
162 query = Query.new(:name => '_')
162 query = Query.new(:name => '_')
163 query.add_filter('estimated_hours', '=', ['171.20'])
163 query.add_filter('estimated_hours', '=', ['171.20'])
164 issues = find_issues_with_query(query)
164 issues = find_issues_with_query(query)
165 assert_equal 1, issues.size
165 assert_equal 1, issues.size
166 assert_equal 2, issues.first.id
166 assert_equal 2, issues.first.id
167 end
167 end
168
168
169 def test_operator_is_on_integer_custom_field
169 def test_operator_is_on_integer_custom_field
170 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
170 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
171 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
171 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
172 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
172 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
173 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
173 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
174
174
175 query = Query.new(:name => '_')
175 query = Query.new(:name => '_')
176 query.add_filter("cf_#{f.id}", '=', ['12'])
176 query.add_filter("cf_#{f.id}", '=', ['12'])
177 issues = find_issues_with_query(query)
177 issues = find_issues_with_query(query)
178 assert_equal 1, issues.size
178 assert_equal 1, issues.size
179 assert_equal 2, issues.first.id
179 assert_equal 2, issues.first.id
180 end
180 end
181
181
182 def test_operator_is_on_integer_custom_field_should_accept_negative_value
183 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
184 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
185 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
186 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
187
188 query = Query.new(:name => '_')
189 query.add_filter("cf_#{f.id}", '=', ['-12'])
190 assert query.valid?
191 issues = find_issues_with_query(query)
192 assert_equal 1, issues.size
193 assert_equal 2, issues.first.id
194 end
195
182 def test_operator_is_on_float_custom_field
196 def test_operator_is_on_float_custom_field
183 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
197 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
184 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
198 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
185 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
199 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
186 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
200 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
187
201
188 query = Query.new(:name => '_')
202 query = Query.new(:name => '_')
189 query.add_filter("cf_#{f.id}", '=', ['12.7'])
203 query.add_filter("cf_#{f.id}", '=', ['12.7'])
190 issues = find_issues_with_query(query)
204 issues = find_issues_with_query(query)
191 assert_equal 1, issues.size
205 assert_equal 1, issues.size
192 assert_equal 2, issues.first.id
206 assert_equal 2, issues.first.id
193 end
207 end
194
208
209 def test_operator_is_on_float_custom_field_should_accept_negative_value
210 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
211 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
212 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
213 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
214
215 query = Query.new(:name => '_')
216 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
217 assert query.valid?
218 issues = find_issues_with_query(query)
219 assert_equal 1, issues.size
220 assert_equal 2, issues.first.id
221 end
222
195 def test_operator_is_on_multi_list_custom_field
223 def test_operator_is_on_multi_list_custom_field
196 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
224 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
197 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
225 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
198 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
226 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
199 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
227 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
200 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
228 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
201
229
202 query = Query.new(:name => '_')
230 query = Query.new(:name => '_')
203 query.add_filter("cf_#{f.id}", '=', ['value1'])
231 query.add_filter("cf_#{f.id}", '=', ['value1'])
204 issues = find_issues_with_query(query)
232 issues = find_issues_with_query(query)
205 assert_equal [1, 3], issues.map(&:id).sort
233 assert_equal [1, 3], issues.map(&:id).sort
206
234
207 query = Query.new(:name => '_')
235 query = Query.new(:name => '_')
208 query.add_filter("cf_#{f.id}", '=', ['value2'])
236 query.add_filter("cf_#{f.id}", '=', ['value2'])
209 issues = find_issues_with_query(query)
237 issues = find_issues_with_query(query)
210 assert_equal [1], issues.map(&:id).sort
238 assert_equal [1], issues.map(&:id).sort
211 end
239 end
212
240
213 def test_operator_is_not_on_multi_list_custom_field
241 def test_operator_is_not_on_multi_list_custom_field
214 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
242 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
215 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
243 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
216 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
244 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
217 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
245 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
218 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
246 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
219
247
220 query = Query.new(:name => '_')
248 query = Query.new(:name => '_')
221 query.add_filter("cf_#{f.id}", '!', ['value1'])
249 query.add_filter("cf_#{f.id}", '!', ['value1'])
222 issues = find_issues_with_query(query)
250 issues = find_issues_with_query(query)
223 assert !issues.map(&:id).include?(1)
251 assert !issues.map(&:id).include?(1)
224 assert !issues.map(&:id).include?(3)
252 assert !issues.map(&:id).include?(3)
225
253
226 query = Query.new(:name => '_')
254 query = Query.new(:name => '_')
227 query.add_filter("cf_#{f.id}", '!', ['value2'])
255 query.add_filter("cf_#{f.id}", '!', ['value2'])
228 issues = find_issues_with_query(query)
256 issues = find_issues_with_query(query)
229 assert !issues.map(&:id).include?(1)
257 assert !issues.map(&:id).include?(1)
230 assert issues.map(&:id).include?(3)
258 assert issues.map(&:id).include?(3)
231 end
259 end
232
260
233 def test_operator_greater_than
261 def test_operator_greater_than
234 query = Query.new(:project => Project.find(1), :name => '_')
262 query = Query.new(:project => Project.find(1), :name => '_')
235 query.add_filter('done_ratio', '>=', ['40'])
263 query.add_filter('done_ratio', '>=', ['40'])
236 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
264 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
237 find_issues_with_query(query)
265 find_issues_with_query(query)
238 end
266 end
239
267
240 def test_operator_greater_than_a_float
268 def test_operator_greater_than_a_float
241 query = Query.new(:project => Project.find(1), :name => '_')
269 query = Query.new(:project => Project.find(1), :name => '_')
242 query.add_filter('estimated_hours', '>=', ['40.5'])
270 query.add_filter('estimated_hours', '>=', ['40.5'])
243 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
271 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
244 find_issues_with_query(query)
272 find_issues_with_query(query)
245 end
273 end
246
274
247 def test_operator_greater_than_on_int_custom_field
275 def test_operator_greater_than_on_int_custom_field
248 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
276 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
249 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
277 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
250 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
278 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
251 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
279 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
252
280
253 query = Query.new(:project => Project.find(1), :name => '_')
281 query = Query.new(:project => Project.find(1), :name => '_')
254 query.add_filter("cf_#{f.id}", '>=', ['8'])
282 query.add_filter("cf_#{f.id}", '>=', ['8'])
255 issues = find_issues_with_query(query)
283 issues = find_issues_with_query(query)
256 assert_equal 1, issues.size
284 assert_equal 1, issues.size
257 assert_equal 2, issues.first.id
285 assert_equal 2, issues.first.id
258 end
286 end
259
287
260 def test_operator_lesser_than
288 def test_operator_lesser_than
261 query = Query.new(:project => Project.find(1), :name => '_')
289 query = Query.new(:project => Project.find(1), :name => '_')
262 query.add_filter('done_ratio', '<=', ['30'])
290 query.add_filter('done_ratio', '<=', ['30'])
263 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
291 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
264 find_issues_with_query(query)
292 find_issues_with_query(query)
265 end
293 end
266
294
267 def test_operator_lesser_than_on_custom_field
295 def test_operator_lesser_than_on_custom_field
268 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
296 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
269 query = Query.new(:project => Project.find(1), :name => '_')
297 query = Query.new(:project => Project.find(1), :name => '_')
270 query.add_filter("cf_#{f.id}", '<=', ['30'])
298 query.add_filter("cf_#{f.id}", '<=', ['30'])
271 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) <= 30.0")
299 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) <= 30.0")
272 find_issues_with_query(query)
300 find_issues_with_query(query)
273 end
301 end
274
302
275 def test_operator_between
303 def test_operator_between
276 query = Query.new(:project => Project.find(1), :name => '_')
304 query = Query.new(:project => Project.find(1), :name => '_')
277 query.add_filter('done_ratio', '><', ['30', '40'])
305 query.add_filter('done_ratio', '><', ['30', '40'])
278 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
306 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
279 find_issues_with_query(query)
307 find_issues_with_query(query)
280 end
308 end
281
309
282 def test_operator_between_on_custom_field
310 def test_operator_between_on_custom_field
283 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
311 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
284 query = Query.new(:project => Project.find(1), :name => '_')
312 query = Query.new(:project => Project.find(1), :name => '_')
285 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
313 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
286 assert_include "CAST(custom_values.value AS decimal(60,3)) BETWEEN 30.0 AND 40.0", query.statement
314 assert_include "CAST(custom_values.value AS decimal(60,3)) BETWEEN 30.0 AND 40.0", query.statement
287 find_issues_with_query(query)
315 find_issues_with_query(query)
288 end
316 end
289
317
290 def test_date_filter_should_not_accept_non_date_values
318 def test_date_filter_should_not_accept_non_date_values
291 query = Query.new(:name => '_')
319 query = Query.new(:name => '_')
292 query.add_filter('created_on', '=', ['a'])
320 query.add_filter('created_on', '=', ['a'])
293
321
294 assert query.has_filter?('created_on')
322 assert query.has_filter?('created_on')
295 assert !query.valid?
323 assert !query.valid?
296 end
324 end
297
325
298 def test_date_filter_should_not_accept_invalid_date_values
326 def test_date_filter_should_not_accept_invalid_date_values
299 query = Query.new(:name => '_')
327 query = Query.new(:name => '_')
300 query.add_filter('created_on', '=', ['2011-01-34'])
328 query.add_filter('created_on', '=', ['2011-01-34'])
301
329
302 assert query.has_filter?('created_on')
330 assert query.has_filter?('created_on')
303 assert !query.valid?
331 assert !query.valid?
304 end
332 end
305
333
306 def test_relative_date_filter_should_not_accept_non_integer_values
334 def test_relative_date_filter_should_not_accept_non_integer_values
307 query = Query.new(:name => '_')
335 query = Query.new(:name => '_')
308 query.add_filter('created_on', '>t-', ['a'])
336 query.add_filter('created_on', '>t-', ['a'])
309
337
310 assert query.has_filter?('created_on')
338 assert query.has_filter?('created_on')
311 assert !query.valid?
339 assert !query.valid?
312 end
340 end
313
341
314 def test_operator_date_equals
342 def test_operator_date_equals
315 query = Query.new(:name => '_')
343 query = Query.new(:name => '_')
316 query.add_filter('due_date', '=', ['2011-07-10'])
344 query.add_filter('due_date', '=', ['2011-07-10'])
317 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
345 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
318 find_issues_with_query(query)
346 find_issues_with_query(query)
319 end
347 end
320
348
321 def test_operator_date_lesser_than
349 def test_operator_date_lesser_than
322 query = Query.new(:name => '_')
350 query = Query.new(:name => '_')
323 query.add_filter('due_date', '<=', ['2011-07-10'])
351 query.add_filter('due_date', '<=', ['2011-07-10'])
324 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
352 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
325 find_issues_with_query(query)
353 find_issues_with_query(query)
326 end
354 end
327
355
328 def test_operator_date_greater_than
356 def test_operator_date_greater_than
329 query = Query.new(:name => '_')
357 query = Query.new(:name => '_')
330 query.add_filter('due_date', '>=', ['2011-07-10'])
358 query.add_filter('due_date', '>=', ['2011-07-10'])
331 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
359 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
332 find_issues_with_query(query)
360 find_issues_with_query(query)
333 end
361 end
334
362
335 def test_operator_date_between
363 def test_operator_date_between
336 query = Query.new(:name => '_')
364 query = Query.new(:name => '_')
337 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
365 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
338 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
366 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
339 find_issues_with_query(query)
367 find_issues_with_query(query)
340 end
368 end
341
369
342 def test_operator_in_more_than
370 def test_operator_in_more_than
343 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
371 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
344 query = Query.new(:project => Project.find(1), :name => '_')
372 query = Query.new(:project => Project.find(1), :name => '_')
345 query.add_filter('due_date', '>t+', ['15'])
373 query.add_filter('due_date', '>t+', ['15'])
346 issues = find_issues_with_query(query)
374 issues = find_issues_with_query(query)
347 assert !issues.empty?
375 assert !issues.empty?
348 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
376 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
349 end
377 end
350
378
351 def test_operator_in_less_than
379 def test_operator_in_less_than
352 query = Query.new(:project => Project.find(1), :name => '_')
380 query = Query.new(:project => Project.find(1), :name => '_')
353 query.add_filter('due_date', '<t+', ['15'])
381 query.add_filter('due_date', '<t+', ['15'])
354 issues = find_issues_with_query(query)
382 issues = find_issues_with_query(query)
355 assert !issues.empty?
383 assert !issues.empty?
356 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
384 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
357 end
385 end
358
386
359 def test_operator_less_than_ago
387 def test_operator_less_than_ago
360 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
388 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
361 query = Query.new(:project => Project.find(1), :name => '_')
389 query = Query.new(:project => Project.find(1), :name => '_')
362 query.add_filter('due_date', '>t-', ['3'])
390 query.add_filter('due_date', '>t-', ['3'])
363 issues = find_issues_with_query(query)
391 issues = find_issues_with_query(query)
364 assert !issues.empty?
392 assert !issues.empty?
365 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
393 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
366 end
394 end
367
395
368 def test_operator_more_than_ago
396 def test_operator_more_than_ago
369 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
397 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
370 query = Query.new(:project => Project.find(1), :name => '_')
398 query = Query.new(:project => Project.find(1), :name => '_')
371 query.add_filter('due_date', '<t-', ['10'])
399 query.add_filter('due_date', '<t-', ['10'])
372 assert query.statement.include?("#{Issue.table_name}.due_date <=")
400 assert query.statement.include?("#{Issue.table_name}.due_date <=")
373 issues = find_issues_with_query(query)
401 issues = find_issues_with_query(query)
374 assert !issues.empty?
402 assert !issues.empty?
375 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
403 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
376 end
404 end
377
405
378 def test_operator_in
406 def test_operator_in
379 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
407 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
380 query = Query.new(:project => Project.find(1), :name => '_')
408 query = Query.new(:project => Project.find(1), :name => '_')
381 query.add_filter('due_date', 't+', ['2'])
409 query.add_filter('due_date', 't+', ['2'])
382 issues = find_issues_with_query(query)
410 issues = find_issues_with_query(query)
383 assert !issues.empty?
411 assert !issues.empty?
384 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
412 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
385 end
413 end
386
414
387 def test_operator_ago
415 def test_operator_ago
388 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
416 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
389 query = Query.new(:project => Project.find(1), :name => '_')
417 query = Query.new(:project => Project.find(1), :name => '_')
390 query.add_filter('due_date', 't-', ['3'])
418 query.add_filter('due_date', 't-', ['3'])
391 issues = find_issues_with_query(query)
419 issues = find_issues_with_query(query)
392 assert !issues.empty?
420 assert !issues.empty?
393 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
421 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
394 end
422 end
395
423
396 def test_operator_today
424 def test_operator_today
397 query = Query.new(:project => Project.find(1), :name => '_')
425 query = Query.new(:project => Project.find(1), :name => '_')
398 query.add_filter('due_date', 't', [''])
426 query.add_filter('due_date', 't', [''])
399 issues = find_issues_with_query(query)
427 issues = find_issues_with_query(query)
400 assert !issues.empty?
428 assert !issues.empty?
401 issues.each {|issue| assert_equal Date.today, issue.due_date}
429 issues.each {|issue| assert_equal Date.today, issue.due_date}
402 end
430 end
403
431
404 def test_operator_this_week_on_date
432 def test_operator_this_week_on_date
405 query = Query.new(:project => Project.find(1), :name => '_')
433 query = Query.new(:project => Project.find(1), :name => '_')
406 query.add_filter('due_date', 'w', [''])
434 query.add_filter('due_date', 'w', [''])
407 find_issues_with_query(query)
435 find_issues_with_query(query)
408 end
436 end
409
437
410 def test_operator_this_week_on_datetime
438 def test_operator_this_week_on_datetime
411 query = Query.new(:project => Project.find(1), :name => '_')
439 query = Query.new(:project => Project.find(1), :name => '_')
412 query.add_filter('created_on', 'w', [''])
440 query.add_filter('created_on', 'w', [''])
413 find_issues_with_query(query)
441 find_issues_with_query(query)
414 end
442 end
415
443
416 def test_operator_contains
444 def test_operator_contains
417 query = Query.new(:project => Project.find(1), :name => '_')
445 query = Query.new(:project => Project.find(1), :name => '_')
418 query.add_filter('subject', '~', ['uNable'])
446 query.add_filter('subject', '~', ['uNable'])
419 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
447 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
420 result = find_issues_with_query(query)
448 result = find_issues_with_query(query)
421 assert result.empty?
449 assert result.empty?
422 result.each {|issue| assert issue.subject.downcase.include?('unable') }
450 result.each {|issue| assert issue.subject.downcase.include?('unable') }
423 end
451 end
424
452
425 def test_range_for_this_week_with_week_starting_on_monday
453 def test_range_for_this_week_with_week_starting_on_monday
426 I18n.locale = :fr
454 I18n.locale = :fr
427 assert_equal '1', I18n.t(:general_first_day_of_week)
455 assert_equal '1', I18n.t(:general_first_day_of_week)
428
456
429 Date.stubs(:today).returns(Date.parse('2011-04-29'))
457 Date.stubs(:today).returns(Date.parse('2011-04-29'))
430
458
431 query = Query.new(:project => Project.find(1), :name => '_')
459 query = Query.new(:project => Project.find(1), :name => '_')
432 query.add_filter('due_date', 'w', [''])
460 query.add_filter('due_date', 'w', [''])
433 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}"
461 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}"
434 I18n.locale = :en
462 I18n.locale = :en
435 end
463 end
436
464
437 def test_range_for_this_week_with_week_starting_on_sunday
465 def test_range_for_this_week_with_week_starting_on_sunday
438 I18n.locale = :en
466 I18n.locale = :en
439 assert_equal '7', I18n.t(:general_first_day_of_week)
467 assert_equal '7', I18n.t(:general_first_day_of_week)
440
468
441 Date.stubs(:today).returns(Date.parse('2011-04-29'))
469 Date.stubs(:today).returns(Date.parse('2011-04-29'))
442
470
443 query = Query.new(:project => Project.find(1), :name => '_')
471 query = Query.new(:project => Project.find(1), :name => '_')
444 query.add_filter('due_date', 'w', [''])
472 query.add_filter('due_date', 'w', [''])
445 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}"
473 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}"
446 end
474 end
447
475
448 def test_operator_does_not_contains
476 def test_operator_does_not_contains
449 query = Query.new(:project => Project.find(1), :name => '_')
477 query = Query.new(:project => Project.find(1), :name => '_')
450 query.add_filter('subject', '!~', ['uNable'])
478 query.add_filter('subject', '!~', ['uNable'])
451 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
479 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
452 find_issues_with_query(query)
480 find_issues_with_query(query)
453 end
481 end
454
482
455 def test_filter_assigned_to_me
483 def test_filter_assigned_to_me
456 user = User.find(2)
484 user = User.find(2)
457 group = Group.find(10)
485 group = Group.find(10)
458 User.current = user
486 User.current = user
459 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
487 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
460 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
488 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
461 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
489 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
462 group.users << user
490 group.users << user
463
491
464 query = Query.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
492 query = Query.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
465 result = query.issues
493 result = query.issues
466 assert_equal Issue.visible.all(:conditions => {:assigned_to_id => ([2] + user.reload.group_ids)}).sort_by(&:id), result.sort_by(&:id)
494 assert_equal Issue.visible.all(:conditions => {:assigned_to_id => ([2] + user.reload.group_ids)}).sort_by(&:id), result.sort_by(&:id)
467
495
468 assert result.include?(i1)
496 assert result.include?(i1)
469 assert result.include?(i2)
497 assert result.include?(i2)
470 assert !result.include?(i3)
498 assert !result.include?(i3)
471 end
499 end
472
500
473 def test_user_custom_field_filtered_on_me
501 def test_user_custom_field_filtered_on_me
474 User.current = User.find(2)
502 User.current = User.find(2)
475 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
503 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
476 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
504 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
477 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
505 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
478
506
479 query = Query.new(:name => '_', :project => Project.find(1))
507 query = Query.new(:name => '_', :project => Project.find(1))
480 filter = query.available_filters["cf_#{cf.id}"]
508 filter = query.available_filters["cf_#{cf.id}"]
481 assert_not_nil filter
509 assert_not_nil filter
482 assert_include 'me', filter[:values].map{|v| v[1]}
510 assert_include 'me', filter[:values].map{|v| v[1]}
483
511
484 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
512 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
485 result = query.issues
513 result = query.issues
486 assert_equal 1, result.size
514 assert_equal 1, result.size
487 assert_equal issue1, result.first
515 assert_equal issue1, result.first
488 end
516 end
489
517
490 def test_filter_my_projects
518 def test_filter_my_projects
491 User.current = User.find(2)
519 User.current = User.find(2)
492 query = Query.new(:name => '_')
520 query = Query.new(:name => '_')
493 filter = query.available_filters['project_id']
521 filter = query.available_filters['project_id']
494 assert_not_nil filter
522 assert_not_nil filter
495 assert_include 'mine', filter[:values].map{|v| v[1]}
523 assert_include 'mine', filter[:values].map{|v| v[1]}
496
524
497 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
525 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
498 result = query.issues
526 result = query.issues
499 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
527 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
500 end
528 end
501
529
502 def test_filter_watched_issues
530 def test_filter_watched_issues
503 User.current = User.find(1)
531 User.current = User.find(1)
504 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
532 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
505 result = find_issues_with_query(query)
533 result = find_issues_with_query(query)
506 assert_not_nil result
534 assert_not_nil result
507 assert !result.empty?
535 assert !result.empty?
508 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
536 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
509 User.current = nil
537 User.current = nil
510 end
538 end
511
539
512 def test_filter_unwatched_issues
540 def test_filter_unwatched_issues
513 User.current = User.find(1)
541 User.current = User.find(1)
514 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
542 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
515 result = find_issues_with_query(query)
543 result = find_issues_with_query(query)
516 assert_not_nil result
544 assert_not_nil result
517 assert !result.empty?
545 assert !result.empty?
518 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
546 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
519 User.current = nil
547 User.current = nil
520 end
548 end
521
549
522 def test_statement_should_be_nil_with_no_filters
550 def test_statement_should_be_nil_with_no_filters
523 q = Query.new(:name => '_')
551 q = Query.new(:name => '_')
524 q.filters = {}
552 q.filters = {}
525
553
526 assert q.valid?
554 assert q.valid?
527 assert_nil q.statement
555 assert_nil q.statement
528 end
556 end
529
557
530 def test_default_columns
558 def test_default_columns
531 q = Query.new
559 q = Query.new
532 assert !q.columns.empty?
560 assert !q.columns.empty?
533 end
561 end
534
562
535 def test_set_column_names
563 def test_set_column_names
536 q = Query.new
564 q = Query.new
537 q.column_names = ['tracker', :subject, '', 'unknonw_column']
565 q.column_names = ['tracker', :subject, '', 'unknonw_column']
538 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
566 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
539 c = q.columns.first
567 c = q.columns.first
540 assert q.has_column?(c)
568 assert q.has_column?(c)
541 end
569 end
542
570
543 def test_query_should_preload_spent_hours
571 def test_query_should_preload_spent_hours
544 q = Query.new(:name => '_', :column_names => [:subject, :spent_hours])
572 q = Query.new(:name => '_', :column_names => [:subject, :spent_hours])
545 assert q.has_column?(:spent_hours)
573 assert q.has_column?(:spent_hours)
546 issues = q.issues
574 issues = q.issues
547 assert_not_nil issues.first.instance_variable_get("@spent_hours")
575 assert_not_nil issues.first.instance_variable_get("@spent_hours")
548 end
576 end
549
577
550 def test_groupable_columns_should_include_custom_fields
578 def test_groupable_columns_should_include_custom_fields
551 q = Query.new
579 q = Query.new
552 column = q.groupable_columns.detect {|c| c.name == :cf_1}
580 column = q.groupable_columns.detect {|c| c.name == :cf_1}
553 assert_not_nil column
581 assert_not_nil column
554 assert_kind_of QueryCustomFieldColumn, column
582 assert_kind_of QueryCustomFieldColumn, column
555 end
583 end
556
584
557 def test_groupable_columns_should_not_include_multi_custom_fields
585 def test_groupable_columns_should_not_include_multi_custom_fields
558 field = CustomField.find(1)
586 field = CustomField.find(1)
559 field.update_attribute :multiple, true
587 field.update_attribute :multiple, true
560
588
561 q = Query.new
589 q = Query.new
562 column = q.groupable_columns.detect {|c| c.name == :cf_1}
590 column = q.groupable_columns.detect {|c| c.name == :cf_1}
563 assert_nil column
591 assert_nil column
564 end
592 end
565
593
566 def test_grouped_with_valid_column
594 def test_grouped_with_valid_column
567 q = Query.new(:group_by => 'status')
595 q = Query.new(:group_by => 'status')
568 assert q.grouped?
596 assert q.grouped?
569 assert_not_nil q.group_by_column
597 assert_not_nil q.group_by_column
570 assert_equal :status, q.group_by_column.name
598 assert_equal :status, q.group_by_column.name
571 assert_not_nil q.group_by_statement
599 assert_not_nil q.group_by_statement
572 assert_equal 'status', q.group_by_statement
600 assert_equal 'status', q.group_by_statement
573 end
601 end
574
602
575 def test_grouped_with_invalid_column
603 def test_grouped_with_invalid_column
576 q = Query.new(:group_by => 'foo')
604 q = Query.new(:group_by => 'foo')
577 assert !q.grouped?
605 assert !q.grouped?
578 assert_nil q.group_by_column
606 assert_nil q.group_by_column
579 assert_nil q.group_by_statement
607 assert_nil q.group_by_statement
580 end
608 end
581
609
582 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
610 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
583 with_settings :user_format => 'lastname_coma_firstname' do
611 with_settings :user_format => 'lastname_coma_firstname' do
584 q = Query.new
612 q = Query.new
585 assert q.sortable_columns.has_key?('assigned_to')
613 assert q.sortable_columns.has_key?('assigned_to')
586 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
614 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
587 end
615 end
588 end
616 end
589
617
590 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
618 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
591 with_settings :user_format => 'lastname_coma_firstname' do
619 with_settings :user_format => 'lastname_coma_firstname' do
592 q = Query.new
620 q = Query.new
593 assert q.sortable_columns.has_key?('author')
621 assert q.sortable_columns.has_key?('author')
594 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
622 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
595 end
623 end
596 end
624 end
597
625
598 def test_sortable_columns_should_include_custom_field
626 def test_sortable_columns_should_include_custom_field
599 q = Query.new
627 q = Query.new
600 assert q.sortable_columns['cf_1']
628 assert q.sortable_columns['cf_1']
601 end
629 end
602
630
603 def test_sortable_columns_should_not_include_multi_custom_field
631 def test_sortable_columns_should_not_include_multi_custom_field
604 field = CustomField.find(1)
632 field = CustomField.find(1)
605 field.update_attribute :multiple, true
633 field.update_attribute :multiple, true
606
634
607 q = Query.new
635 q = Query.new
608 assert !q.sortable_columns['cf_1']
636 assert !q.sortable_columns['cf_1']
609 end
637 end
610
638
611 def test_default_sort
639 def test_default_sort
612 q = Query.new
640 q = Query.new
613 assert_equal [], q.sort_criteria
641 assert_equal [], q.sort_criteria
614 end
642 end
615
643
616 def test_set_sort_criteria_with_hash
644 def test_set_sort_criteria_with_hash
617 q = Query.new
645 q = Query.new
618 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
646 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
619 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
647 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
620 end
648 end
621
649
622 def test_set_sort_criteria_with_array
650 def test_set_sort_criteria_with_array
623 q = Query.new
651 q = Query.new
624 q.sort_criteria = [['priority', 'desc'], 'tracker']
652 q.sort_criteria = [['priority', 'desc'], 'tracker']
625 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
653 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
626 end
654 end
627
655
628 def test_create_query_with_sort
656 def test_create_query_with_sort
629 q = Query.new(:name => 'Sorted')
657 q = Query.new(:name => 'Sorted')
630 q.sort_criteria = [['priority', 'desc'], 'tracker']
658 q.sort_criteria = [['priority', 'desc'], 'tracker']
631 assert q.save
659 assert q.save
632 q.reload
660 q.reload
633 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
661 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
634 end
662 end
635
663
636 def test_sort_by_string_custom_field_asc
664 def test_sort_by_string_custom_field_asc
637 q = Query.new
665 q = Query.new
638 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
666 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
639 assert c
667 assert c
640 assert c.sortable
668 assert c.sortable
641 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
669 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
642 q.statement
670 q.statement
643 ).order("#{c.sortable} ASC").all
671 ).order("#{c.sortable} ASC").all
644 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
672 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
645 assert !values.empty?
673 assert !values.empty?
646 assert_equal values.sort, values
674 assert_equal values.sort, values
647 end
675 end
648
676
649 def test_sort_by_string_custom_field_desc
677 def test_sort_by_string_custom_field_desc
650 q = Query.new
678 q = Query.new
651 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
679 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
652 assert c
680 assert c
653 assert c.sortable
681 assert c.sortable
654 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
682 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
655 q.statement
683 q.statement
656 ).order("#{c.sortable} DESC").all
684 ).order("#{c.sortable} DESC").all
657 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
685 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
658 assert !values.empty?
686 assert !values.empty?
659 assert_equal values.sort.reverse, values
687 assert_equal values.sort.reverse, values
660 end
688 end
661
689
662 def test_sort_by_float_custom_field_asc
690 def test_sort_by_float_custom_field_asc
663 q = Query.new
691 q = Query.new
664 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
692 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
665 assert c
693 assert c
666 assert c.sortable
694 assert c.sortable
667 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
695 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
668 q.statement
696 q.statement
669 ).order("#{c.sortable} ASC").all
697 ).order("#{c.sortable} ASC").all
670 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
698 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
671 assert !values.empty?
699 assert !values.empty?
672 assert_equal values.sort, values
700 assert_equal values.sort, values
673 end
701 end
674
702
675 def test_invalid_query_should_raise_query_statement_invalid_error
703 def test_invalid_query_should_raise_query_statement_invalid_error
676 q = Query.new
704 q = Query.new
677 assert_raise Query::StatementInvalid do
705 assert_raise Query::StatementInvalid do
678 q.issues(:conditions => "foo = 1")
706 q.issues(:conditions => "foo = 1")
679 end
707 end
680 end
708 end
681
709
682 def test_issue_count
710 def test_issue_count
683 q = Query.new(:name => '_')
711 q = Query.new(:name => '_')
684 issue_count = q.issue_count
712 issue_count = q.issue_count
685 assert_equal q.issues.size, issue_count
713 assert_equal q.issues.size, issue_count
686 end
714 end
687
715
688 def test_issue_count_with_archived_issues
716 def test_issue_count_with_archived_issues
689 p = Project.generate! do |project|
717 p = Project.generate! do |project|
690 project.status = Project::STATUS_ARCHIVED
718 project.status = Project::STATUS_ARCHIVED
691 end
719 end
692 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
720 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
693 assert !i.visible?
721 assert !i.visible?
694
722
695 test_issue_count
723 test_issue_count
696 end
724 end
697
725
698 def test_issue_count_by_association_group
726 def test_issue_count_by_association_group
699 q = Query.new(:name => '_', :group_by => 'assigned_to')
727 q = Query.new(:name => '_', :group_by => 'assigned_to')
700 count_by_group = q.issue_count_by_group
728 count_by_group = q.issue_count_by_group
701 assert_kind_of Hash, count_by_group
729 assert_kind_of Hash, count_by_group
702 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
730 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
703 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
731 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
704 assert count_by_group.has_key?(User.find(3))
732 assert count_by_group.has_key?(User.find(3))
705 end
733 end
706
734
707 def test_issue_count_by_list_custom_field_group
735 def test_issue_count_by_list_custom_field_group
708 q = Query.new(:name => '_', :group_by => 'cf_1')
736 q = Query.new(:name => '_', :group_by => 'cf_1')
709 count_by_group = q.issue_count_by_group
737 count_by_group = q.issue_count_by_group
710 assert_kind_of Hash, count_by_group
738 assert_kind_of Hash, count_by_group
711 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
739 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
712 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
740 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
713 assert count_by_group.has_key?('MySQL')
741 assert count_by_group.has_key?('MySQL')
714 end
742 end
715
743
716 def test_issue_count_by_date_custom_field_group
744 def test_issue_count_by_date_custom_field_group
717 q = Query.new(:name => '_', :group_by => 'cf_8')
745 q = Query.new(:name => '_', :group_by => 'cf_8')
718 count_by_group = q.issue_count_by_group
746 count_by_group = q.issue_count_by_group
719 assert_kind_of Hash, count_by_group
747 assert_kind_of Hash, count_by_group
720 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
748 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
721 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
749 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
722 end
750 end
723
751
724 def test_issue_count_with_nil_group_only
752 def test_issue_count_with_nil_group_only
725 Issue.update_all("assigned_to_id = NULL")
753 Issue.update_all("assigned_to_id = NULL")
726
754
727 q = Query.new(:name => '_', :group_by => 'assigned_to')
755 q = Query.new(:name => '_', :group_by => 'assigned_to')
728 count_by_group = q.issue_count_by_group
756 count_by_group = q.issue_count_by_group
729 assert_kind_of Hash, count_by_group
757 assert_kind_of Hash, count_by_group
730 assert_equal 1, count_by_group.keys.size
758 assert_equal 1, count_by_group.keys.size
731 assert_nil count_by_group.keys.first
759 assert_nil count_by_group.keys.first
732 end
760 end
733
761
734 def test_issue_ids
762 def test_issue_ids
735 q = Query.new(:name => '_')
763 q = Query.new(:name => '_')
736 order = "issues.subject, issues.id"
764 order = "issues.subject, issues.id"
737 issues = q.issues(:order => order)
765 issues = q.issues(:order => order)
738 assert_equal issues.map(&:id), q.issue_ids(:order => order)
766 assert_equal issues.map(&:id), q.issue_ids(:order => order)
739 end
767 end
740
768
741 def test_label_for
769 def test_label_for
742 q = Query.new
770 q = Query.new
743 assert_equal 'Assignee', q.label_for('assigned_to_id')
771 assert_equal 'Assignee', q.label_for('assigned_to_id')
744 end
772 end
745
773
746 def test_editable_by
774 def test_editable_by
747 admin = User.find(1)
775 admin = User.find(1)
748 manager = User.find(2)
776 manager = User.find(2)
749 developer = User.find(3)
777 developer = User.find(3)
750
778
751 # Public query on project 1
779 # Public query on project 1
752 q = Query.find(1)
780 q = Query.find(1)
753 assert q.editable_by?(admin)
781 assert q.editable_by?(admin)
754 assert q.editable_by?(manager)
782 assert q.editable_by?(manager)
755 assert !q.editable_by?(developer)
783 assert !q.editable_by?(developer)
756
784
757 # Private query on project 1
785 # Private query on project 1
758 q = Query.find(2)
786 q = Query.find(2)
759 assert q.editable_by?(admin)
787 assert q.editable_by?(admin)
760 assert !q.editable_by?(manager)
788 assert !q.editable_by?(manager)
761 assert q.editable_by?(developer)
789 assert q.editable_by?(developer)
762
790
763 # Private query for all projects
791 # Private query for all projects
764 q = Query.find(3)
792 q = Query.find(3)
765 assert q.editable_by?(admin)
793 assert q.editable_by?(admin)
766 assert !q.editable_by?(manager)
794 assert !q.editable_by?(manager)
767 assert q.editable_by?(developer)
795 assert q.editable_by?(developer)
768
796
769 # Public query for all projects
797 # Public query for all projects
770 q = Query.find(4)
798 q = Query.find(4)
771 assert q.editable_by?(admin)
799 assert q.editable_by?(admin)
772 assert !q.editable_by?(manager)
800 assert !q.editable_by?(manager)
773 assert !q.editable_by?(developer)
801 assert !q.editable_by?(developer)
774 end
802 end
775
803
776 def test_visible_scope
804 def test_visible_scope
777 query_ids = Query.visible(User.anonymous).map(&:id)
805 query_ids = Query.visible(User.anonymous).map(&:id)
778
806
779 assert query_ids.include?(1), 'public query on public project was not visible'
807 assert query_ids.include?(1), 'public query on public project was not visible'
780 assert query_ids.include?(4), 'public query for all projects was not visible'
808 assert query_ids.include?(4), 'public query for all projects was not visible'
781 assert !query_ids.include?(2), 'private query on public project was visible'
809 assert !query_ids.include?(2), 'private query on public project was visible'
782 assert !query_ids.include?(3), 'private query for all projects was visible'
810 assert !query_ids.include?(3), 'private query for all projects was visible'
783 assert !query_ids.include?(7), 'public query on private project was visible'
811 assert !query_ids.include?(7), 'public query on private project was visible'
784 end
812 end
785
813
786 context "#available_filters" do
814 context "#available_filters" do
787 setup do
815 setup do
788 @query = Query.new(:name => "_")
816 @query = Query.new(:name => "_")
789 end
817 end
790
818
791 should "include users of visible projects in cross-project view" do
819 should "include users of visible projects in cross-project view" do
792 users = @query.available_filters["assigned_to_id"]
820 users = @query.available_filters["assigned_to_id"]
793 assert_not_nil users
821 assert_not_nil users
794 assert users[:values].map{|u|u[1]}.include?("3")
822 assert users[:values].map{|u|u[1]}.include?("3")
795 end
823 end
796
824
797 should "include users of subprojects" do
825 should "include users of subprojects" do
798 user1 = User.generate!
826 user1 = User.generate!
799 user2 = User.generate!
827 user2 = User.generate!
800 project = Project.find(1)
828 project = Project.find(1)
801 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
829 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
802 @query.project = project
830 @query.project = project
803
831
804 users = @query.available_filters["assigned_to_id"]
832 users = @query.available_filters["assigned_to_id"]
805 assert_not_nil users
833 assert_not_nil users
806 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
834 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
807 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
835 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
808 end
836 end
809
837
810 should "include visible projects in cross-project view" do
838 should "include visible projects in cross-project view" do
811 projects = @query.available_filters["project_id"]
839 projects = @query.available_filters["project_id"]
812 assert_not_nil projects
840 assert_not_nil projects
813 assert projects[:values].map{|u|u[1]}.include?("1")
841 assert projects[:values].map{|u|u[1]}.include?("1")
814 end
842 end
815
843
816 context "'member_of_group' filter" do
844 context "'member_of_group' filter" do
817 should "be present" do
845 should "be present" do
818 assert @query.available_filters.keys.include?("member_of_group")
846 assert @query.available_filters.keys.include?("member_of_group")
819 end
847 end
820
848
821 should "be an optional list" do
849 should "be an optional list" do
822 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
850 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
823 end
851 end
824
852
825 should "have a list of the groups as values" do
853 should "have a list of the groups as values" do
826 Group.destroy_all # No fixtures
854 Group.destroy_all # No fixtures
827 group1 = Group.generate!.reload
855 group1 = Group.generate!.reload
828 group2 = Group.generate!.reload
856 group2 = Group.generate!.reload
829
857
830 expected_group_list = [
858 expected_group_list = [
831 [group1.name, group1.id.to_s],
859 [group1.name, group1.id.to_s],
832 [group2.name, group2.id.to_s]
860 [group2.name, group2.id.to_s]
833 ]
861 ]
834 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
862 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
835 end
863 end
836
864
837 end
865 end
838
866
839 context "'assigned_to_role' filter" do
867 context "'assigned_to_role' filter" do
840 should "be present" do
868 should "be present" do
841 assert @query.available_filters.keys.include?("assigned_to_role")
869 assert @query.available_filters.keys.include?("assigned_to_role")
842 end
870 end
843
871
844 should "be an optional list" do
872 should "be an optional list" do
845 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
873 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
846 end
874 end
847
875
848 should "have a list of the Roles as values" do
876 should "have a list of the Roles as values" do
849 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
877 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
850 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
878 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
851 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
879 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
852 end
880 end
853
881
854 should "not include the built in Roles as values" do
882 should "not include the built in Roles as values" do
855 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
883 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
856 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
884 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
857 end
885 end
858
886
859 end
887 end
860
888
861 end
889 end
862
890
863 context "#statement" do
891 context "#statement" do
864 context "with 'member_of_group' filter" do
892 context "with 'member_of_group' filter" do
865 setup do
893 setup do
866 Group.destroy_all # No fixtures
894 Group.destroy_all # No fixtures
867 @user_in_group = User.generate!
895 @user_in_group = User.generate!
868 @second_user_in_group = User.generate!
896 @second_user_in_group = User.generate!
869 @user_in_group2 = User.generate!
897 @user_in_group2 = User.generate!
870 @user_not_in_group = User.generate!
898 @user_not_in_group = User.generate!
871
899
872 @group = Group.generate!.reload
900 @group = Group.generate!.reload
873 @group.users << @user_in_group
901 @group.users << @user_in_group
874 @group.users << @second_user_in_group
902 @group.users << @second_user_in_group
875
903
876 @group2 = Group.generate!.reload
904 @group2 = Group.generate!.reload
877 @group2.users << @user_in_group2
905 @group2.users << @user_in_group2
878
906
879 end
907 end
880
908
881 should "search assigned to for users in the group" do
909 should "search assigned to for users in the group" do
882 @query = Query.new(:name => '_')
910 @query = Query.new(:name => '_')
883 @query.add_filter('member_of_group', '=', [@group.id.to_s])
911 @query.add_filter('member_of_group', '=', [@group.id.to_s])
884
912
885 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
913 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
886 assert_find_issues_with_query_is_successful @query
914 assert_find_issues_with_query_is_successful @query
887 end
915 end
888
916
889 should "search not assigned to any group member (none)" do
917 should "search not assigned to any group member (none)" do
890 @query = Query.new(:name => '_')
918 @query = Query.new(:name => '_')
891 @query.add_filter('member_of_group', '!*', [''])
919 @query.add_filter('member_of_group', '!*', [''])
892
920
893 # Users not in a group
921 # Users not in a group
894 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}')"
922 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}')"
895 assert_find_issues_with_query_is_successful @query
923 assert_find_issues_with_query_is_successful @query
896 end
924 end
897
925
898 should "search assigned to any group member (all)" do
926 should "search assigned to any group member (all)" do
899 @query = Query.new(:name => '_')
927 @query = Query.new(:name => '_')
900 @query.add_filter('member_of_group', '*', [''])
928 @query.add_filter('member_of_group', '*', [''])
901
929
902 # Only users in a group
930 # Only users in a group
903 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}')"
931 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}')"
904 assert_find_issues_with_query_is_successful @query
932 assert_find_issues_with_query_is_successful @query
905 end
933 end
906
934
907 should "return an empty set with = empty group" do
935 should "return an empty set with = empty group" do
908 @empty_group = Group.generate!
936 @empty_group = Group.generate!
909 @query = Query.new(:name => '_')
937 @query = Query.new(:name => '_')
910 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
938 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
911
939
912 assert_equal [], find_issues_with_query(@query)
940 assert_equal [], find_issues_with_query(@query)
913 end
941 end
914
942
915 should "return issues with ! empty group" do
943 should "return issues with ! empty group" do
916 @empty_group = Group.generate!
944 @empty_group = Group.generate!
917 @query = Query.new(:name => '_')
945 @query = Query.new(:name => '_')
918 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
946 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
919
947
920 assert_find_issues_with_query_is_successful @query
948 assert_find_issues_with_query_is_successful @query
921 end
949 end
922 end
950 end
923
951
924 context "with 'assigned_to_role' filter" do
952 context "with 'assigned_to_role' filter" do
925 setup do
953 setup do
926 @manager_role = Role.find_by_name('Manager')
954 @manager_role = Role.find_by_name('Manager')
927 @developer_role = Role.find_by_name('Developer')
955 @developer_role = Role.find_by_name('Developer')
928
956
929 @project = Project.generate!
957 @project = Project.generate!
930 @manager = User.generate!
958 @manager = User.generate!
931 @developer = User.generate!
959 @developer = User.generate!
932 @boss = User.generate!
960 @boss = User.generate!
933 @guest = User.generate!
961 @guest = User.generate!
934 User.add_to_project(@manager, @project, @manager_role)
962 User.add_to_project(@manager, @project, @manager_role)
935 User.add_to_project(@developer, @project, @developer_role)
963 User.add_to_project(@developer, @project, @developer_role)
936 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
964 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
937
965
938 @issue1 = Issue.generate_for_project!(@project, :assigned_to_id => @manager.id)
966 @issue1 = Issue.generate_for_project!(@project, :assigned_to_id => @manager.id)
939 @issue2 = Issue.generate_for_project!(@project, :assigned_to_id => @developer.id)
967 @issue2 = Issue.generate_for_project!(@project, :assigned_to_id => @developer.id)
940 @issue3 = Issue.generate_for_project!(@project, :assigned_to_id => @boss.id)
968 @issue3 = Issue.generate_for_project!(@project, :assigned_to_id => @boss.id)
941 @issue4 = Issue.generate_for_project!(@project, :assigned_to_id => @guest.id)
969 @issue4 = Issue.generate_for_project!(@project, :assigned_to_id => @guest.id)
942 @issue5 = Issue.generate_for_project!(@project)
970 @issue5 = Issue.generate_for_project!(@project)
943 end
971 end
944
972
945 should "search assigned to for users with the Role" do
973 should "search assigned to for users with the Role" do
946 @query = Query.new(:name => '_', :project => @project)
974 @query = Query.new(:name => '_', :project => @project)
947 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
975 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
948
976
949 assert_query_result [@issue1, @issue3], @query
977 assert_query_result [@issue1, @issue3], @query
950 end
978 end
951
979
952 should "search assigned to for users with the Role on the issue project" do
980 should "search assigned to for users with the Role on the issue project" do
953 other_project = Project.generate!
981 other_project = Project.generate!
954 User.add_to_project(@developer, other_project, @manager_role)
982 User.add_to_project(@developer, other_project, @manager_role)
955
983
956 @query = Query.new(:name => '_', :project => @project)
984 @query = Query.new(:name => '_', :project => @project)
957 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
985 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
958
986
959 assert_query_result [@issue1, @issue3], @query
987 assert_query_result [@issue1, @issue3], @query
960 end
988 end
961
989
962 should "return an empty set with empty role" do
990 should "return an empty set with empty role" do
963 @empty_role = Role.generate!
991 @empty_role = Role.generate!
964 @query = Query.new(:name => '_', :project => @project)
992 @query = Query.new(:name => '_', :project => @project)
965 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
993 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
966
994
967 assert_query_result [], @query
995 assert_query_result [], @query
968 end
996 end
969
997
970 should "search assigned to for users without the Role" do
998 should "search assigned to for users without the Role" do
971 @query = Query.new(:name => '_', :project => @project)
999 @query = Query.new(:name => '_', :project => @project)
972 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
1000 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
973
1001
974 assert_query_result [@issue2, @issue4, @issue5], @query
1002 assert_query_result [@issue2, @issue4, @issue5], @query
975 end
1003 end
976
1004
977 should "search assigned to for users not assigned to any Role (none)" do
1005 should "search assigned to for users not assigned to any Role (none)" do
978 @query = Query.new(:name => '_', :project => @project)
1006 @query = Query.new(:name => '_', :project => @project)
979 @query.add_filter('assigned_to_role', '!*', [''])
1007 @query.add_filter('assigned_to_role', '!*', [''])
980
1008
981 assert_query_result [@issue4, @issue5], @query
1009 assert_query_result [@issue4, @issue5], @query
982 end
1010 end
983
1011
984 should "search assigned to for users assigned to any Role (all)" do
1012 should "search assigned to for users assigned to any Role (all)" do
985 @query = Query.new(:name => '_', :project => @project)
1013 @query = Query.new(:name => '_', :project => @project)
986 @query.add_filter('assigned_to_role', '*', [''])
1014 @query.add_filter('assigned_to_role', '*', [''])
987
1015
988 assert_query_result [@issue1, @issue2, @issue3], @query
1016 assert_query_result [@issue1, @issue2, @issue3], @query
989 end
1017 end
990
1018
991 should "return issues with ! empty role" do
1019 should "return issues with ! empty role" do
992 @empty_role = Role.generate!
1020 @empty_role = Role.generate!
993 @query = Query.new(:name => '_', :project => @project)
1021 @query = Query.new(:name => '_', :project => @project)
994 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1022 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
995
1023
996 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1024 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
997 end
1025 end
998 end
1026 end
999 end
1027 end
1000
1028
1001 end
1029 end
General Comments 0
You need to be logged in to leave comments. Login now