##// END OF EJS Templates
Merged r9908 from trunk....
Jean-Philippe Lang -
r9771:3cc0730f54b8
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,1000 +1,1028
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
27
28 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
28 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
29 query = Query.new(:project => nil, :name => '_')
29 query = Query.new(:project => nil, :name => '_')
30 assert query.available_filters.has_key?('cf_1')
30 assert query.available_filters.has_key?('cf_1')
31 assert !query.available_filters.has_key?('cf_3')
31 assert !query.available_filters.has_key?('cf_3')
32 end
32 end
33
33
34 def test_system_shared_versions_should_be_available_in_global_queries
34 def test_system_shared_versions_should_be_available_in_global_queries
35 Version.find(2).update_attribute :sharing, 'system'
35 Version.find(2).update_attribute :sharing, 'system'
36 query = Query.new(:project => nil, :name => '_')
36 query = Query.new(:project => nil, :name => '_')
37 assert query.available_filters.has_key?('fixed_version_id')
37 assert query.available_filters.has_key?('fixed_version_id')
38 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
38 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
39 end
39 end
40
40
41 def test_project_filter_in_global_queries
41 def test_project_filter_in_global_queries
42 query = Query.new(:project => nil, :name => '_')
42 query = Query.new(:project => nil, :name => '_')
43 project_filter = query.available_filters["project_id"]
43 project_filter = query.available_filters["project_id"]
44 assert_not_nil project_filter
44 assert_not_nil project_filter
45 project_ids = project_filter[:values].map{|p| p[1]}
45 project_ids = project_filter[:values].map{|p| p[1]}
46 assert project_ids.include?("1") #public project
46 assert project_ids.include?("1") #public project
47 assert !project_ids.include?("2") #private project user cannot see
47 assert !project_ids.include?("2") #private project user cannot see
48 end
48 end
49
49
50 def find_issues_with_query(query)
50 def find_issues_with_query(query)
51 Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
51 Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
52 query.statement
52 query.statement
53 ).all
53 ).all
54 end
54 end
55
55
56 def assert_find_issues_with_query_is_successful(query)
56 def assert_find_issues_with_query_is_successful(query)
57 assert_nothing_raised do
57 assert_nothing_raised do
58 find_issues_with_query(query)
58 find_issues_with_query(query)
59 end
59 end
60 end
60 end
61
61
62 def assert_query_statement_includes(query, condition)
62 def assert_query_statement_includes(query, condition)
63 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
63 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
64 end
64 end
65
65
66 def assert_query_result(expected, query)
66 def assert_query_result(expected, query)
67 assert_nothing_raised do
67 assert_nothing_raised do
68 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
68 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
69 assert_equal expected.size, query.issue_count
69 assert_equal expected.size, query.issue_count
70 end
70 end
71 end
71 end
72
72
73 def test_query_should_allow_shared_versions_for_a_project_query
73 def test_query_should_allow_shared_versions_for_a_project_query
74 subproject_version = Version.find(4)
74 subproject_version = Version.find(4)
75 query = Query.new(:project => Project.find(1), :name => '_')
75 query = Query.new(:project => Project.find(1), :name => '_')
76 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
76 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
77
77
78 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
78 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
79 end
79 end
80
80
81 def test_query_with_multiple_custom_fields
81 def test_query_with_multiple_custom_fields
82 query = Query.find(1)
82 query = Query.find(1)
83 assert query.valid?
83 assert query.valid?
84 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
84 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
85 issues = find_issues_with_query(query)
85 issues = find_issues_with_query(query)
86 assert_equal 1, issues.length
86 assert_equal 1, issues.length
87 assert_equal Issue.find(3), issues.first
87 assert_equal Issue.find(3), issues.first
88 end
88 end
89
89
90 def test_operator_none
90 def test_operator_none
91 query = Query.new(:project => Project.find(1), :name => '_')
91 query = Query.new(:project => Project.find(1), :name => '_')
92 query.add_filter('fixed_version_id', '!*', [''])
92 query.add_filter('fixed_version_id', '!*', [''])
93 query.add_filter('cf_1', '!*', [''])
93 query.add_filter('cf_1', '!*', [''])
94 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
94 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
95 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
95 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
96 find_issues_with_query(query)
96 find_issues_with_query(query)
97 end
97 end
98
98
99 def test_operator_none_for_integer
99 def test_operator_none_for_integer
100 query = Query.new(:project => Project.find(1), :name => '_')
100 query = Query.new(:project => Project.find(1), :name => '_')
101 query.add_filter('estimated_hours', '!*', [''])
101 query.add_filter('estimated_hours', '!*', [''])
102 issues = find_issues_with_query(query)
102 issues = find_issues_with_query(query)
103 assert !issues.empty?
103 assert !issues.empty?
104 assert issues.all? {|i| !i.estimated_hours}
104 assert issues.all? {|i| !i.estimated_hours}
105 end
105 end
106
106
107 def test_operator_none_for_date
107 def test_operator_none_for_date
108 query = Query.new(:project => Project.find(1), :name => '_')
108 query = Query.new(:project => Project.find(1), :name => '_')
109 query.add_filter('start_date', '!*', [''])
109 query.add_filter('start_date', '!*', [''])
110 issues = find_issues_with_query(query)
110 issues = find_issues_with_query(query)
111 assert !issues.empty?
111 assert !issues.empty?
112 assert issues.all? {|i| i.start_date.nil?}
112 assert issues.all? {|i| i.start_date.nil?}
113 end
113 end
114
114
115 def test_operator_none_for_string_custom_field
115 def test_operator_none_for_string_custom_field
116 query = Query.new(:project => Project.find(1), :name => '_')
116 query = Query.new(:project => Project.find(1), :name => '_')
117 query.add_filter('cf_2', '!*', [''])
117 query.add_filter('cf_2', '!*', [''])
118 assert query.has_filter?('cf_2')
118 assert query.has_filter?('cf_2')
119 issues = find_issues_with_query(query)
119 issues = find_issues_with_query(query)
120 assert !issues.empty?
120 assert !issues.empty?
121 assert issues.all? {|i| i.custom_field_value(2).blank?}
121 assert issues.all? {|i| i.custom_field_value(2).blank?}
122 end
122 end
123
123
124 def test_operator_all
124 def test_operator_all
125 query = Query.new(:project => Project.find(1), :name => '_')
125 query = Query.new(:project => Project.find(1), :name => '_')
126 query.add_filter('fixed_version_id', '*', [''])
126 query.add_filter('fixed_version_id', '*', [''])
127 query.add_filter('cf_1', '*', [''])
127 query.add_filter('cf_1', '*', [''])
128 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
128 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
129 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
129 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
130 find_issues_with_query(query)
130 find_issues_with_query(query)
131 end
131 end
132
132
133 def test_operator_all_for_date
133 def test_operator_all_for_date
134 query = Query.new(:project => Project.find(1), :name => '_')
134 query = Query.new(:project => Project.find(1), :name => '_')
135 query.add_filter('start_date', '*', [''])
135 query.add_filter('start_date', '*', [''])
136 issues = find_issues_with_query(query)
136 issues = find_issues_with_query(query)
137 assert !issues.empty?
137 assert !issues.empty?
138 assert issues.all? {|i| i.start_date.present?}
138 assert issues.all? {|i| i.start_date.present?}
139 end
139 end
140
140
141 def test_operator_all_for_string_custom_field
141 def test_operator_all_for_string_custom_field
142 query = Query.new(:project => Project.find(1), :name => '_')
142 query = Query.new(:project => Project.find(1), :name => '_')
143 query.add_filter('cf_2', '*', [''])
143 query.add_filter('cf_2', '*', [''])
144 assert query.has_filter?('cf_2')
144 assert query.has_filter?('cf_2')
145 issues = find_issues_with_query(query)
145 issues = find_issues_with_query(query)
146 assert !issues.empty?
146 assert !issues.empty?
147 assert issues.all? {|i| i.custom_field_value(2).present?}
147 assert issues.all? {|i| i.custom_field_value(2).present?}
148 end
148 end
149
149
150 def test_numeric_filter_should_not_accept_non_numeric_values
150 def test_numeric_filter_should_not_accept_non_numeric_values
151 query = Query.new(:name => '_')
151 query = Query.new(:name => '_')
152 query.add_filter('estimated_hours', '=', ['a'])
152 query.add_filter('estimated_hours', '=', ['a'])
153
153
154 assert query.has_filter?('estimated_hours')
154 assert query.has_filter?('estimated_hours')
155 assert !query.valid?
155 assert !query.valid?
156 end
156 end
157
157
158 def test_operator_is_on_float
158 def test_operator_is_on_float
159 Issue.update_all("estimated_hours = 171.2", "id=2")
159 Issue.update_all("estimated_hours = 171.2", "id=2")
160
160
161 query = Query.new(:name => '_')
161 query = Query.new(:name => '_')
162 query.add_filter('estimated_hours', '=', ['171.20'])
162 query.add_filter('estimated_hours', '=', ['171.20'])
163 issues = find_issues_with_query(query)
163 issues = find_issues_with_query(query)
164 assert_equal 1, issues.size
164 assert_equal 1, issues.size
165 assert_equal 2, issues.first.id
165 assert_equal 2, issues.first.id
166 end
166 end
167
167
168 def test_operator_is_on_integer_custom_field
168 def test_operator_is_on_integer_custom_field
169 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
169 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
170 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
170 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
171 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
171 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
172 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
172 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
173
173
174 query = Query.new(:name => '_')
174 query = Query.new(:name => '_')
175 query.add_filter("cf_#{f.id}", '=', ['12'])
175 query.add_filter("cf_#{f.id}", '=', ['12'])
176 issues = find_issues_with_query(query)
176 issues = find_issues_with_query(query)
177 assert_equal 1, issues.size
177 assert_equal 1, issues.size
178 assert_equal 2, issues.first.id
178 assert_equal 2, issues.first.id
179 end
179 end
180
180
181 def test_operator_is_on_integer_custom_field_should_accept_negative_value
182 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
183 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
184 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12')
185 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
186
187 query = Query.new(:name => '_')
188 query.add_filter("cf_#{f.id}", '=', ['-12'])
189 assert query.valid?
190 issues = find_issues_with_query(query)
191 assert_equal 1, issues.size
192 assert_equal 2, issues.first.id
193 end
194
181 def test_operator_is_on_float_custom_field
195 def test_operator_is_on_float_custom_field
182 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
196 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
183 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
197 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
184 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
198 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
185 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
199 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
186
200
187 query = Query.new(:name => '_')
201 query = Query.new(:name => '_')
188 query.add_filter("cf_#{f.id}", '=', ['12.7'])
202 query.add_filter("cf_#{f.id}", '=', ['12.7'])
189 issues = find_issues_with_query(query)
203 issues = find_issues_with_query(query)
190 assert_equal 1, issues.size
204 assert_equal 1, issues.size
191 assert_equal 2, issues.first.id
205 assert_equal 2, issues.first.id
192 end
206 end
193
207
208 def test_operator_is_on_float_custom_field_should_accept_negative_value
209 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
210 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
211 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '-12.7')
212 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
213
214 query = Query.new(:name => '_')
215 query.add_filter("cf_#{f.id}", '=', ['-12.7'])
216 assert query.valid?
217 issues = find_issues_with_query(query)
218 assert_equal 1, issues.size
219 assert_equal 2, issues.first.id
220 end
221
194 def test_operator_is_on_multi_list_custom_field
222 def test_operator_is_on_multi_list_custom_field
195 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
223 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
196 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
224 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
197 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
225 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
198 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
226 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
199 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
227 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
200
228
201 query = Query.new(:name => '_')
229 query = Query.new(:name => '_')
202 query.add_filter("cf_#{f.id}", '=', ['value1'])
230 query.add_filter("cf_#{f.id}", '=', ['value1'])
203 issues = find_issues_with_query(query)
231 issues = find_issues_with_query(query)
204 assert_equal [1, 3], issues.map(&:id).sort
232 assert_equal [1, 3], issues.map(&:id).sort
205
233
206 query = Query.new(:name => '_')
234 query = Query.new(:name => '_')
207 query.add_filter("cf_#{f.id}", '=', ['value2'])
235 query.add_filter("cf_#{f.id}", '=', ['value2'])
208 issues = find_issues_with_query(query)
236 issues = find_issues_with_query(query)
209 assert_equal [1], issues.map(&:id).sort
237 assert_equal [1], issues.map(&:id).sort
210 end
238 end
211
239
212 def test_operator_is_not_on_multi_list_custom_field
240 def test_operator_is_not_on_multi_list_custom_field
213 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
241 f = IssueCustomField.create!(:name => 'filter', :field_format => 'list', :is_filter => true, :is_for_all => true,
214 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
242 :possible_values => ['value1', 'value2', 'value3'], :multiple => true)
215 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
243 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value1')
216 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
244 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => 'value2')
217 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
245 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => 'value1')
218
246
219 query = Query.new(:name => '_')
247 query = Query.new(:name => '_')
220 query.add_filter("cf_#{f.id}", '!', ['value1'])
248 query.add_filter("cf_#{f.id}", '!', ['value1'])
221 issues = find_issues_with_query(query)
249 issues = find_issues_with_query(query)
222 assert !issues.map(&:id).include?(1)
250 assert !issues.map(&:id).include?(1)
223 assert !issues.map(&:id).include?(3)
251 assert !issues.map(&:id).include?(3)
224
252
225 query = Query.new(:name => '_')
253 query = Query.new(:name => '_')
226 query.add_filter("cf_#{f.id}", '!', ['value2'])
254 query.add_filter("cf_#{f.id}", '!', ['value2'])
227 issues = find_issues_with_query(query)
255 issues = find_issues_with_query(query)
228 assert !issues.map(&:id).include?(1)
256 assert !issues.map(&:id).include?(1)
229 assert issues.map(&:id).include?(3)
257 assert issues.map(&:id).include?(3)
230 end
258 end
231
259
232 def test_operator_greater_than
260 def test_operator_greater_than
233 query = Query.new(:project => Project.find(1), :name => '_')
261 query = Query.new(:project => Project.find(1), :name => '_')
234 query.add_filter('done_ratio', '>=', ['40'])
262 query.add_filter('done_ratio', '>=', ['40'])
235 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
263 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
236 find_issues_with_query(query)
264 find_issues_with_query(query)
237 end
265 end
238
266
239 def test_operator_greater_than_a_float
267 def test_operator_greater_than_a_float
240 query = Query.new(:project => Project.find(1), :name => '_')
268 query = Query.new(:project => Project.find(1), :name => '_')
241 query.add_filter('estimated_hours', '>=', ['40.5'])
269 query.add_filter('estimated_hours', '>=', ['40.5'])
242 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
270 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
243 find_issues_with_query(query)
271 find_issues_with_query(query)
244 end
272 end
245
273
246 def test_operator_greater_than_on_int_custom_field
274 def test_operator_greater_than_on_int_custom_field
247 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
275 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
248 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
276 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
249 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
277 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
250 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
278 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
251
279
252 query = Query.new(:project => Project.find(1), :name => '_')
280 query = Query.new(:project => Project.find(1), :name => '_')
253 query.add_filter("cf_#{f.id}", '>=', ['8'])
281 query.add_filter("cf_#{f.id}", '>=', ['8'])
254 issues = find_issues_with_query(query)
282 issues = find_issues_with_query(query)
255 assert_equal 1, issues.size
283 assert_equal 1, issues.size
256 assert_equal 2, issues.first.id
284 assert_equal 2, issues.first.id
257 end
285 end
258
286
259 def test_operator_lesser_than
287 def test_operator_lesser_than
260 query = Query.new(:project => Project.find(1), :name => '_')
288 query = Query.new(:project => Project.find(1), :name => '_')
261 query.add_filter('done_ratio', '<=', ['30'])
289 query.add_filter('done_ratio', '<=', ['30'])
262 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
290 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
263 find_issues_with_query(query)
291 find_issues_with_query(query)
264 end
292 end
265
293
266 def test_operator_lesser_than_on_custom_field
294 def test_operator_lesser_than_on_custom_field
267 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
295 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
268 query = Query.new(:project => Project.find(1), :name => '_')
296 query = Query.new(:project => Project.find(1), :name => '_')
269 query.add_filter("cf_#{f.id}", '<=', ['30'])
297 query.add_filter("cf_#{f.id}", '<=', ['30'])
270 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) <= 30.0")
298 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) <= 30.0")
271 find_issues_with_query(query)
299 find_issues_with_query(query)
272 end
300 end
273
301
274 def test_operator_between
302 def test_operator_between
275 query = Query.new(:project => Project.find(1), :name => '_')
303 query = Query.new(:project => Project.find(1), :name => '_')
276 query.add_filter('done_ratio', '><', ['30', '40'])
304 query.add_filter('done_ratio', '><', ['30', '40'])
277 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
305 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
278 find_issues_with_query(query)
306 find_issues_with_query(query)
279 end
307 end
280
308
281 def test_operator_between_on_custom_field
309 def test_operator_between_on_custom_field
282 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
310 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
283 query = Query.new(:project => Project.find(1), :name => '_')
311 query = Query.new(:project => Project.find(1), :name => '_')
284 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
312 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
285 assert_include "CAST(custom_values.value AS decimal(60,3)) BETWEEN 30.0 AND 40.0", query.statement
313 assert_include "CAST(custom_values.value AS decimal(60,3)) BETWEEN 30.0 AND 40.0", query.statement
286 find_issues_with_query(query)
314 find_issues_with_query(query)
287 end
315 end
288
316
289 def test_date_filter_should_not_accept_non_date_values
317 def test_date_filter_should_not_accept_non_date_values
290 query = Query.new(:name => '_')
318 query = Query.new(:name => '_')
291 query.add_filter('created_on', '=', ['a'])
319 query.add_filter('created_on', '=', ['a'])
292
320
293 assert query.has_filter?('created_on')
321 assert query.has_filter?('created_on')
294 assert !query.valid?
322 assert !query.valid?
295 end
323 end
296
324
297 def test_date_filter_should_not_accept_invalid_date_values
325 def test_date_filter_should_not_accept_invalid_date_values
298 query = Query.new(:name => '_')
326 query = Query.new(:name => '_')
299 query.add_filter('created_on', '=', ['2011-01-34'])
327 query.add_filter('created_on', '=', ['2011-01-34'])
300
328
301 assert query.has_filter?('created_on')
329 assert query.has_filter?('created_on')
302 assert !query.valid?
330 assert !query.valid?
303 end
331 end
304
332
305 def test_relative_date_filter_should_not_accept_non_integer_values
333 def test_relative_date_filter_should_not_accept_non_integer_values
306 query = Query.new(:name => '_')
334 query = Query.new(:name => '_')
307 query.add_filter('created_on', '>t-', ['a'])
335 query.add_filter('created_on', '>t-', ['a'])
308
336
309 assert query.has_filter?('created_on')
337 assert query.has_filter?('created_on')
310 assert !query.valid?
338 assert !query.valid?
311 end
339 end
312
340
313 def test_operator_date_equals
341 def test_operator_date_equals
314 query = Query.new(:name => '_')
342 query = Query.new(:name => '_')
315 query.add_filter('due_date', '=', ['2011-07-10'])
343 query.add_filter('due_date', '=', ['2011-07-10'])
316 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
344 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
317 find_issues_with_query(query)
345 find_issues_with_query(query)
318 end
346 end
319
347
320 def test_operator_date_lesser_than
348 def test_operator_date_lesser_than
321 query = Query.new(:name => '_')
349 query = Query.new(:name => '_')
322 query.add_filter('due_date', '<=', ['2011-07-10'])
350 query.add_filter('due_date', '<=', ['2011-07-10'])
323 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
351 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
324 find_issues_with_query(query)
352 find_issues_with_query(query)
325 end
353 end
326
354
327 def test_operator_date_greater_than
355 def test_operator_date_greater_than
328 query = Query.new(:name => '_')
356 query = Query.new(:name => '_')
329 query.add_filter('due_date', '>=', ['2011-07-10'])
357 query.add_filter('due_date', '>=', ['2011-07-10'])
330 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
358 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
331 find_issues_with_query(query)
359 find_issues_with_query(query)
332 end
360 end
333
361
334 def test_operator_date_between
362 def test_operator_date_between
335 query = Query.new(:name => '_')
363 query = Query.new(:name => '_')
336 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
364 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
337 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
365 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
338 find_issues_with_query(query)
366 find_issues_with_query(query)
339 end
367 end
340
368
341 def test_operator_in_more_than
369 def test_operator_in_more_than
342 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
370 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
343 query = Query.new(:project => Project.find(1), :name => '_')
371 query = Query.new(:project => Project.find(1), :name => '_')
344 query.add_filter('due_date', '>t+', ['15'])
372 query.add_filter('due_date', '>t+', ['15'])
345 issues = find_issues_with_query(query)
373 issues = find_issues_with_query(query)
346 assert !issues.empty?
374 assert !issues.empty?
347 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
375 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
348 end
376 end
349
377
350 def test_operator_in_less_than
378 def test_operator_in_less_than
351 query = Query.new(:project => Project.find(1), :name => '_')
379 query = Query.new(:project => Project.find(1), :name => '_')
352 query.add_filter('due_date', '<t+', ['15'])
380 query.add_filter('due_date', '<t+', ['15'])
353 issues = find_issues_with_query(query)
381 issues = find_issues_with_query(query)
354 assert !issues.empty?
382 assert !issues.empty?
355 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
383 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
356 end
384 end
357
385
358 def test_operator_less_than_ago
386 def test_operator_less_than_ago
359 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
387 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
360 query = Query.new(:project => Project.find(1), :name => '_')
388 query = Query.new(:project => Project.find(1), :name => '_')
361 query.add_filter('due_date', '>t-', ['3'])
389 query.add_filter('due_date', '>t-', ['3'])
362 issues = find_issues_with_query(query)
390 issues = find_issues_with_query(query)
363 assert !issues.empty?
391 assert !issues.empty?
364 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
392 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
365 end
393 end
366
394
367 def test_operator_more_than_ago
395 def test_operator_more_than_ago
368 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
396 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
369 query = Query.new(:project => Project.find(1), :name => '_')
397 query = Query.new(:project => Project.find(1), :name => '_')
370 query.add_filter('due_date', '<t-', ['10'])
398 query.add_filter('due_date', '<t-', ['10'])
371 assert query.statement.include?("#{Issue.table_name}.due_date <=")
399 assert query.statement.include?("#{Issue.table_name}.due_date <=")
372 issues = find_issues_with_query(query)
400 issues = find_issues_with_query(query)
373 assert !issues.empty?
401 assert !issues.empty?
374 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
402 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
375 end
403 end
376
404
377 def test_operator_in
405 def test_operator_in
378 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
406 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
379 query = Query.new(:project => Project.find(1), :name => '_')
407 query = Query.new(:project => Project.find(1), :name => '_')
380 query.add_filter('due_date', 't+', ['2'])
408 query.add_filter('due_date', 't+', ['2'])
381 issues = find_issues_with_query(query)
409 issues = find_issues_with_query(query)
382 assert !issues.empty?
410 assert !issues.empty?
383 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
411 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
384 end
412 end
385
413
386 def test_operator_ago
414 def test_operator_ago
387 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
415 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
388 query = Query.new(:project => Project.find(1), :name => '_')
416 query = Query.new(:project => Project.find(1), :name => '_')
389 query.add_filter('due_date', 't-', ['3'])
417 query.add_filter('due_date', 't-', ['3'])
390 issues = find_issues_with_query(query)
418 issues = find_issues_with_query(query)
391 assert !issues.empty?
419 assert !issues.empty?
392 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
420 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
393 end
421 end
394
422
395 def test_operator_today
423 def test_operator_today
396 query = Query.new(:project => Project.find(1), :name => '_')
424 query = Query.new(:project => Project.find(1), :name => '_')
397 query.add_filter('due_date', 't', [''])
425 query.add_filter('due_date', 't', [''])
398 issues = find_issues_with_query(query)
426 issues = find_issues_with_query(query)
399 assert !issues.empty?
427 assert !issues.empty?
400 issues.each {|issue| assert_equal Date.today, issue.due_date}
428 issues.each {|issue| assert_equal Date.today, issue.due_date}
401 end
429 end
402
430
403 def test_operator_this_week_on_date
431 def test_operator_this_week_on_date
404 query = Query.new(:project => Project.find(1), :name => '_')
432 query = Query.new(:project => Project.find(1), :name => '_')
405 query.add_filter('due_date', 'w', [''])
433 query.add_filter('due_date', 'w', [''])
406 find_issues_with_query(query)
434 find_issues_with_query(query)
407 end
435 end
408
436
409 def test_operator_this_week_on_datetime
437 def test_operator_this_week_on_datetime
410 query = Query.new(:project => Project.find(1), :name => '_')
438 query = Query.new(:project => Project.find(1), :name => '_')
411 query.add_filter('created_on', 'w', [''])
439 query.add_filter('created_on', 'w', [''])
412 find_issues_with_query(query)
440 find_issues_with_query(query)
413 end
441 end
414
442
415 def test_operator_contains
443 def test_operator_contains
416 query = Query.new(:project => Project.find(1), :name => '_')
444 query = Query.new(:project => Project.find(1), :name => '_')
417 query.add_filter('subject', '~', ['uNable'])
445 query.add_filter('subject', '~', ['uNable'])
418 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
446 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
419 result = find_issues_with_query(query)
447 result = find_issues_with_query(query)
420 assert result.empty?
448 assert result.empty?
421 result.each {|issue| assert issue.subject.downcase.include?('unable') }
449 result.each {|issue| assert issue.subject.downcase.include?('unable') }
422 end
450 end
423
451
424 def test_range_for_this_week_with_week_starting_on_monday
452 def test_range_for_this_week_with_week_starting_on_monday
425 I18n.locale = :fr
453 I18n.locale = :fr
426 assert_equal '1', I18n.t(:general_first_day_of_week)
454 assert_equal '1', I18n.t(:general_first_day_of_week)
427
455
428 Date.stubs(:today).returns(Date.parse('2011-04-29'))
456 Date.stubs(:today).returns(Date.parse('2011-04-29'))
429
457
430 query = Query.new(:project => Project.find(1), :name => '_')
458 query = Query.new(:project => Project.find(1), :name => '_')
431 query.add_filter('due_date', 'w', [''])
459 query.add_filter('due_date', 'w', [''])
432 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}"
460 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}"
433 I18n.locale = :en
461 I18n.locale = :en
434 end
462 end
435
463
436 def test_range_for_this_week_with_week_starting_on_sunday
464 def test_range_for_this_week_with_week_starting_on_sunday
437 I18n.locale = :en
465 I18n.locale = :en
438 assert_equal '7', I18n.t(:general_first_day_of_week)
466 assert_equal '7', I18n.t(:general_first_day_of_week)
439
467
440 Date.stubs(:today).returns(Date.parse('2011-04-29'))
468 Date.stubs(:today).returns(Date.parse('2011-04-29'))
441
469
442 query = Query.new(:project => Project.find(1), :name => '_')
470 query = Query.new(:project => Project.find(1), :name => '_')
443 query.add_filter('due_date', 'w', [''])
471 query.add_filter('due_date', 'w', [''])
444 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}"
472 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}"
445 end
473 end
446
474
447 def test_operator_does_not_contains
475 def test_operator_does_not_contains
448 query = Query.new(:project => Project.find(1), :name => '_')
476 query = Query.new(:project => Project.find(1), :name => '_')
449 query.add_filter('subject', '!~', ['uNable'])
477 query.add_filter('subject', '!~', ['uNable'])
450 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
478 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
451 find_issues_with_query(query)
479 find_issues_with_query(query)
452 end
480 end
453
481
454 def test_filter_assigned_to_me
482 def test_filter_assigned_to_me
455 user = User.find(2)
483 user = User.find(2)
456 group = Group.find(10)
484 group = Group.find(10)
457 User.current = user
485 User.current = user
458 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
486 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
459 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
487 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
460 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
488 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
461 group.users << user
489 group.users << user
462
490
463 query = Query.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
491 query = Query.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
464 result = query.issues
492 result = query.issues
465 assert_equal Issue.visible.all(:conditions => {:assigned_to_id => ([2] + user.reload.group_ids)}).sort_by(&:id), result.sort_by(&:id)
493 assert_equal Issue.visible.all(:conditions => {:assigned_to_id => ([2] + user.reload.group_ids)}).sort_by(&:id), result.sort_by(&:id)
466
494
467 assert result.include?(i1)
495 assert result.include?(i1)
468 assert result.include?(i2)
496 assert result.include?(i2)
469 assert !result.include?(i3)
497 assert !result.include?(i3)
470 end
498 end
471
499
472 def test_user_custom_field_filtered_on_me
500 def test_user_custom_field_filtered_on_me
473 User.current = User.find(2)
501 User.current = User.find(2)
474 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
502 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
475 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
503 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
476 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
504 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
477
505
478 query = Query.new(:name => '_', :project => Project.find(1))
506 query = Query.new(:name => '_', :project => Project.find(1))
479 filter = query.available_filters["cf_#{cf.id}"]
507 filter = query.available_filters["cf_#{cf.id}"]
480 assert_not_nil filter
508 assert_not_nil filter
481 assert_include 'me', filter[:values].map{|v| v[1]}
509 assert_include 'me', filter[:values].map{|v| v[1]}
482
510
483 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
511 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
484 result = query.issues
512 result = query.issues
485 assert_equal 1, result.size
513 assert_equal 1, result.size
486 assert_equal issue1, result.first
514 assert_equal issue1, result.first
487 end
515 end
488
516
489 def test_filter_my_projects
517 def test_filter_my_projects
490 User.current = User.find(2)
518 User.current = User.find(2)
491 query = Query.new(:name => '_')
519 query = Query.new(:name => '_')
492 filter = query.available_filters['project_id']
520 filter = query.available_filters['project_id']
493 assert_not_nil filter
521 assert_not_nil filter
494 assert_include 'mine', filter[:values].map{|v| v[1]}
522 assert_include 'mine', filter[:values].map{|v| v[1]}
495
523
496 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
524 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
497 result = query.issues
525 result = query.issues
498 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
526 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
499 end
527 end
500
528
501 def test_filter_watched_issues
529 def test_filter_watched_issues
502 User.current = User.find(1)
530 User.current = User.find(1)
503 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
531 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
504 result = find_issues_with_query(query)
532 result = find_issues_with_query(query)
505 assert_not_nil result
533 assert_not_nil result
506 assert !result.empty?
534 assert !result.empty?
507 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
535 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
508 User.current = nil
536 User.current = nil
509 end
537 end
510
538
511 def test_filter_unwatched_issues
539 def test_filter_unwatched_issues
512 User.current = User.find(1)
540 User.current = User.find(1)
513 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
541 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
514 result = find_issues_with_query(query)
542 result = find_issues_with_query(query)
515 assert_not_nil result
543 assert_not_nil result
516 assert !result.empty?
544 assert !result.empty?
517 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
545 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
518 User.current = nil
546 User.current = nil
519 end
547 end
520
548
521 def test_statement_should_be_nil_with_no_filters
549 def test_statement_should_be_nil_with_no_filters
522 q = Query.new(:name => '_')
550 q = Query.new(:name => '_')
523 q.filters = {}
551 q.filters = {}
524
552
525 assert q.valid?
553 assert q.valid?
526 assert_nil q.statement
554 assert_nil q.statement
527 end
555 end
528
556
529 def test_default_columns
557 def test_default_columns
530 q = Query.new
558 q = Query.new
531 assert !q.columns.empty?
559 assert !q.columns.empty?
532 end
560 end
533
561
534 def test_set_column_names
562 def test_set_column_names
535 q = Query.new
563 q = Query.new
536 q.column_names = ['tracker', :subject, '', 'unknonw_column']
564 q.column_names = ['tracker', :subject, '', 'unknonw_column']
537 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
565 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
538 c = q.columns.first
566 c = q.columns.first
539 assert q.has_column?(c)
567 assert q.has_column?(c)
540 end
568 end
541
569
542 def test_query_should_preload_spent_hours
570 def test_query_should_preload_spent_hours
543 q = Query.new(:name => '_', :column_names => [:subject, :spent_hours])
571 q = Query.new(:name => '_', :column_names => [:subject, :spent_hours])
544 assert q.has_column?(:spent_hours)
572 assert q.has_column?(:spent_hours)
545 issues = q.issues
573 issues = q.issues
546 assert_not_nil issues.first.instance_variable_get("@spent_hours")
574 assert_not_nil issues.first.instance_variable_get("@spent_hours")
547 end
575 end
548
576
549 def test_groupable_columns_should_include_custom_fields
577 def test_groupable_columns_should_include_custom_fields
550 q = Query.new
578 q = Query.new
551 column = q.groupable_columns.detect {|c| c.name == :cf_1}
579 column = q.groupable_columns.detect {|c| c.name == :cf_1}
552 assert_not_nil column
580 assert_not_nil column
553 assert_kind_of QueryCustomFieldColumn, column
581 assert_kind_of QueryCustomFieldColumn, column
554 end
582 end
555
583
556 def test_groupable_columns_should_not_include_multi_custom_fields
584 def test_groupable_columns_should_not_include_multi_custom_fields
557 field = CustomField.find(1)
585 field = CustomField.find(1)
558 field.update_attribute :multiple, true
586 field.update_attribute :multiple, true
559
587
560 q = Query.new
588 q = Query.new
561 column = q.groupable_columns.detect {|c| c.name == :cf_1}
589 column = q.groupable_columns.detect {|c| c.name == :cf_1}
562 assert_nil column
590 assert_nil column
563 end
591 end
564
592
565 def test_grouped_with_valid_column
593 def test_grouped_with_valid_column
566 q = Query.new(:group_by => 'status')
594 q = Query.new(:group_by => 'status')
567 assert q.grouped?
595 assert q.grouped?
568 assert_not_nil q.group_by_column
596 assert_not_nil q.group_by_column
569 assert_equal :status, q.group_by_column.name
597 assert_equal :status, q.group_by_column.name
570 assert_not_nil q.group_by_statement
598 assert_not_nil q.group_by_statement
571 assert_equal 'status', q.group_by_statement
599 assert_equal 'status', q.group_by_statement
572 end
600 end
573
601
574 def test_grouped_with_invalid_column
602 def test_grouped_with_invalid_column
575 q = Query.new(:group_by => 'foo')
603 q = Query.new(:group_by => 'foo')
576 assert !q.grouped?
604 assert !q.grouped?
577 assert_nil q.group_by_column
605 assert_nil q.group_by_column
578 assert_nil q.group_by_statement
606 assert_nil q.group_by_statement
579 end
607 end
580
608
581 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
609 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
582 with_settings :user_format => 'lastname_coma_firstname' do
610 with_settings :user_format => 'lastname_coma_firstname' do
583 q = Query.new
611 q = Query.new
584 assert q.sortable_columns.has_key?('assigned_to')
612 assert q.sortable_columns.has_key?('assigned_to')
585 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
613 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
586 end
614 end
587 end
615 end
588
616
589 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
617 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
590 with_settings :user_format => 'lastname_coma_firstname' do
618 with_settings :user_format => 'lastname_coma_firstname' do
591 q = Query.new
619 q = Query.new
592 assert q.sortable_columns.has_key?('author')
620 assert q.sortable_columns.has_key?('author')
593 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
621 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
594 end
622 end
595 end
623 end
596
624
597 def test_sortable_columns_should_include_custom_field
625 def test_sortable_columns_should_include_custom_field
598 q = Query.new
626 q = Query.new
599 assert q.sortable_columns['cf_1']
627 assert q.sortable_columns['cf_1']
600 end
628 end
601
629
602 def test_sortable_columns_should_not_include_multi_custom_field
630 def test_sortable_columns_should_not_include_multi_custom_field
603 field = CustomField.find(1)
631 field = CustomField.find(1)
604 field.update_attribute :multiple, true
632 field.update_attribute :multiple, true
605
633
606 q = Query.new
634 q = Query.new
607 assert !q.sortable_columns['cf_1']
635 assert !q.sortable_columns['cf_1']
608 end
636 end
609
637
610 def test_default_sort
638 def test_default_sort
611 q = Query.new
639 q = Query.new
612 assert_equal [], q.sort_criteria
640 assert_equal [], q.sort_criteria
613 end
641 end
614
642
615 def test_set_sort_criteria_with_hash
643 def test_set_sort_criteria_with_hash
616 q = Query.new
644 q = Query.new
617 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
645 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
618 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
646 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
619 end
647 end
620
648
621 def test_set_sort_criteria_with_array
649 def test_set_sort_criteria_with_array
622 q = Query.new
650 q = Query.new
623 q.sort_criteria = [['priority', 'desc'], 'tracker']
651 q.sort_criteria = [['priority', 'desc'], 'tracker']
624 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
652 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
625 end
653 end
626
654
627 def test_create_query_with_sort
655 def test_create_query_with_sort
628 q = Query.new(:name => 'Sorted')
656 q = Query.new(:name => 'Sorted')
629 q.sort_criteria = [['priority', 'desc'], 'tracker']
657 q.sort_criteria = [['priority', 'desc'], 'tracker']
630 assert q.save
658 assert q.save
631 q.reload
659 q.reload
632 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
660 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
633 end
661 end
634
662
635 def test_sort_by_string_custom_field_asc
663 def test_sort_by_string_custom_field_asc
636 q = Query.new
664 q = Query.new
637 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
665 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
638 assert c
666 assert c
639 assert c.sortable
667 assert c.sortable
640 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
668 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
641 q.statement
669 q.statement
642 ).order("#{c.sortable} ASC").all
670 ).order("#{c.sortable} ASC").all
643 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
671 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
644 assert !values.empty?
672 assert !values.empty?
645 assert_equal values.sort, values
673 assert_equal values.sort, values
646 end
674 end
647
675
648 def test_sort_by_string_custom_field_desc
676 def test_sort_by_string_custom_field_desc
649 q = Query.new
677 q = Query.new
650 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
678 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
651 assert c
679 assert c
652 assert c.sortable
680 assert c.sortable
653 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
681 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
654 q.statement
682 q.statement
655 ).order("#{c.sortable} DESC").all
683 ).order("#{c.sortable} DESC").all
656 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
684 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
657 assert !values.empty?
685 assert !values.empty?
658 assert_equal values.sort.reverse, values
686 assert_equal values.sort.reverse, values
659 end
687 end
660
688
661 def test_sort_by_float_custom_field_asc
689 def test_sort_by_float_custom_field_asc
662 q = Query.new
690 q = Query.new
663 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
691 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
664 assert c
692 assert c
665 assert c.sortable
693 assert c.sortable
666 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
694 issues = Issue.includes([:assigned_to, :status, :tracker, :project, :priority]).where(
667 q.statement
695 q.statement
668 ).order("#{c.sortable} ASC").all
696 ).order("#{c.sortable} ASC").all
669 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
697 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
670 assert !values.empty?
698 assert !values.empty?
671 assert_equal values.sort, values
699 assert_equal values.sort, values
672 end
700 end
673
701
674 def test_invalid_query_should_raise_query_statement_invalid_error
702 def test_invalid_query_should_raise_query_statement_invalid_error
675 q = Query.new
703 q = Query.new
676 assert_raise Query::StatementInvalid do
704 assert_raise Query::StatementInvalid do
677 q.issues(:conditions => "foo = 1")
705 q.issues(:conditions => "foo = 1")
678 end
706 end
679 end
707 end
680
708
681 def test_issue_count
709 def test_issue_count
682 q = Query.new(:name => '_')
710 q = Query.new(:name => '_')
683 issue_count = q.issue_count
711 issue_count = q.issue_count
684 assert_equal q.issues.size, issue_count
712 assert_equal q.issues.size, issue_count
685 end
713 end
686
714
687 def test_issue_count_with_archived_issues
715 def test_issue_count_with_archived_issues
688 p = Project.generate! do |project|
716 p = Project.generate! do |project|
689 project.status = Project::STATUS_ARCHIVED
717 project.status = Project::STATUS_ARCHIVED
690 end
718 end
691 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
719 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
692 assert !i.visible?
720 assert !i.visible?
693
721
694 test_issue_count
722 test_issue_count
695 end
723 end
696
724
697 def test_issue_count_by_association_group
725 def test_issue_count_by_association_group
698 q = Query.new(:name => '_', :group_by => 'assigned_to')
726 q = Query.new(:name => '_', :group_by => 'assigned_to')
699 count_by_group = q.issue_count_by_group
727 count_by_group = q.issue_count_by_group
700 assert_kind_of Hash, count_by_group
728 assert_kind_of Hash, count_by_group
701 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
729 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
702 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
730 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
703 assert count_by_group.has_key?(User.find(3))
731 assert count_by_group.has_key?(User.find(3))
704 end
732 end
705
733
706 def test_issue_count_by_list_custom_field_group
734 def test_issue_count_by_list_custom_field_group
707 q = Query.new(:name => '_', :group_by => 'cf_1')
735 q = Query.new(:name => '_', :group_by => 'cf_1')
708 count_by_group = q.issue_count_by_group
736 count_by_group = q.issue_count_by_group
709 assert_kind_of Hash, count_by_group
737 assert_kind_of Hash, count_by_group
710 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
738 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
711 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
739 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
712 assert count_by_group.has_key?('MySQL')
740 assert count_by_group.has_key?('MySQL')
713 end
741 end
714
742
715 def test_issue_count_by_date_custom_field_group
743 def test_issue_count_by_date_custom_field_group
716 q = Query.new(:name => '_', :group_by => 'cf_8')
744 q = Query.new(:name => '_', :group_by => 'cf_8')
717 count_by_group = q.issue_count_by_group
745 count_by_group = q.issue_count_by_group
718 assert_kind_of Hash, count_by_group
746 assert_kind_of Hash, count_by_group
719 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
747 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
720 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
748 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
721 end
749 end
722
750
723 def test_issue_count_with_nil_group_only
751 def test_issue_count_with_nil_group_only
724 Issue.update_all("assigned_to_id = NULL")
752 Issue.update_all("assigned_to_id = NULL")
725
753
726 q = Query.new(:name => '_', :group_by => 'assigned_to')
754 q = Query.new(:name => '_', :group_by => 'assigned_to')
727 count_by_group = q.issue_count_by_group
755 count_by_group = q.issue_count_by_group
728 assert_kind_of Hash, count_by_group
756 assert_kind_of Hash, count_by_group
729 assert_equal 1, count_by_group.keys.size
757 assert_equal 1, count_by_group.keys.size
730 assert_nil count_by_group.keys.first
758 assert_nil count_by_group.keys.first
731 end
759 end
732
760
733 def test_issue_ids
761 def test_issue_ids
734 q = Query.new(:name => '_')
762 q = Query.new(:name => '_')
735 order = "issues.subject, issues.id"
763 order = "issues.subject, issues.id"
736 issues = q.issues(:order => order)
764 issues = q.issues(:order => order)
737 assert_equal issues.map(&:id), q.issue_ids(:order => order)
765 assert_equal issues.map(&:id), q.issue_ids(:order => order)
738 end
766 end
739
767
740 def test_label_for
768 def test_label_for
741 q = Query.new
769 q = Query.new
742 assert_equal 'Assignee', q.label_for('assigned_to_id')
770 assert_equal 'Assignee', q.label_for('assigned_to_id')
743 end
771 end
744
772
745 def test_editable_by
773 def test_editable_by
746 admin = User.find(1)
774 admin = User.find(1)
747 manager = User.find(2)
775 manager = User.find(2)
748 developer = User.find(3)
776 developer = User.find(3)
749
777
750 # Public query on project 1
778 # Public query on project 1
751 q = Query.find(1)
779 q = Query.find(1)
752 assert q.editable_by?(admin)
780 assert q.editable_by?(admin)
753 assert q.editable_by?(manager)
781 assert q.editable_by?(manager)
754 assert !q.editable_by?(developer)
782 assert !q.editable_by?(developer)
755
783
756 # Private query on project 1
784 # Private query on project 1
757 q = Query.find(2)
785 q = Query.find(2)
758 assert q.editable_by?(admin)
786 assert q.editable_by?(admin)
759 assert !q.editable_by?(manager)
787 assert !q.editable_by?(manager)
760 assert q.editable_by?(developer)
788 assert q.editable_by?(developer)
761
789
762 # Private query for all projects
790 # Private query for all projects
763 q = Query.find(3)
791 q = Query.find(3)
764 assert q.editable_by?(admin)
792 assert q.editable_by?(admin)
765 assert !q.editable_by?(manager)
793 assert !q.editable_by?(manager)
766 assert q.editable_by?(developer)
794 assert q.editable_by?(developer)
767
795
768 # Public query for all projects
796 # Public query for all projects
769 q = Query.find(4)
797 q = Query.find(4)
770 assert q.editable_by?(admin)
798 assert q.editable_by?(admin)
771 assert !q.editable_by?(manager)
799 assert !q.editable_by?(manager)
772 assert !q.editable_by?(developer)
800 assert !q.editable_by?(developer)
773 end
801 end
774
802
775 def test_visible_scope
803 def test_visible_scope
776 query_ids = Query.visible(User.anonymous).map(&:id)
804 query_ids = Query.visible(User.anonymous).map(&:id)
777
805
778 assert query_ids.include?(1), 'public query on public project was not visible'
806 assert query_ids.include?(1), 'public query on public project was not visible'
779 assert query_ids.include?(4), 'public query for all projects was not visible'
807 assert query_ids.include?(4), 'public query for all projects was not visible'
780 assert !query_ids.include?(2), 'private query on public project was visible'
808 assert !query_ids.include?(2), 'private query on public project was visible'
781 assert !query_ids.include?(3), 'private query for all projects was visible'
809 assert !query_ids.include?(3), 'private query for all projects was visible'
782 assert !query_ids.include?(7), 'public query on private project was visible'
810 assert !query_ids.include?(7), 'public query on private project was visible'
783 end
811 end
784
812
785 context "#available_filters" do
813 context "#available_filters" do
786 setup do
814 setup do
787 @query = Query.new(:name => "_")
815 @query = Query.new(:name => "_")
788 end
816 end
789
817
790 should "include users of visible projects in cross-project view" do
818 should "include users of visible projects in cross-project view" do
791 users = @query.available_filters["assigned_to_id"]
819 users = @query.available_filters["assigned_to_id"]
792 assert_not_nil users
820 assert_not_nil users
793 assert users[:values].map{|u|u[1]}.include?("3")
821 assert users[:values].map{|u|u[1]}.include?("3")
794 end
822 end
795
823
796 should "include users of subprojects" do
824 should "include users of subprojects" do
797 user1 = User.generate!
825 user1 = User.generate!
798 user2 = User.generate!
826 user2 = User.generate!
799 project = Project.find(1)
827 project = Project.find(1)
800 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
828 Member.create!(:principal => user1, :project => project.children.visible.first, :role_ids => [1])
801 @query.project = project
829 @query.project = project
802
830
803 users = @query.available_filters["assigned_to_id"]
831 users = @query.available_filters["assigned_to_id"]
804 assert_not_nil users
832 assert_not_nil users
805 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
833 assert users[:values].map{|u|u[1]}.include?(user1.id.to_s)
806 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
834 assert !users[:values].map{|u|u[1]}.include?(user2.id.to_s)
807 end
835 end
808
836
809 should "include visible projects in cross-project view" do
837 should "include visible projects in cross-project view" do
810 projects = @query.available_filters["project_id"]
838 projects = @query.available_filters["project_id"]
811 assert_not_nil projects
839 assert_not_nil projects
812 assert projects[:values].map{|u|u[1]}.include?("1")
840 assert projects[:values].map{|u|u[1]}.include?("1")
813 end
841 end
814
842
815 context "'member_of_group' filter" do
843 context "'member_of_group' filter" do
816 should "be present" do
844 should "be present" do
817 assert @query.available_filters.keys.include?("member_of_group")
845 assert @query.available_filters.keys.include?("member_of_group")
818 end
846 end
819
847
820 should "be an optional list" do
848 should "be an optional list" do
821 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
849 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
822 end
850 end
823
851
824 should "have a list of the groups as values" do
852 should "have a list of the groups as values" do
825 Group.destroy_all # No fixtures
853 Group.destroy_all # No fixtures
826 group1 = Group.generate!.reload
854 group1 = Group.generate!.reload
827 group2 = Group.generate!.reload
855 group2 = Group.generate!.reload
828
856
829 expected_group_list = [
857 expected_group_list = [
830 [group1.name, group1.id.to_s],
858 [group1.name, group1.id.to_s],
831 [group2.name, group2.id.to_s]
859 [group2.name, group2.id.to_s]
832 ]
860 ]
833 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
861 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
834 end
862 end
835
863
836 end
864 end
837
865
838 context "'assigned_to_role' filter" do
866 context "'assigned_to_role' filter" do
839 should "be present" do
867 should "be present" do
840 assert @query.available_filters.keys.include?("assigned_to_role")
868 assert @query.available_filters.keys.include?("assigned_to_role")
841 end
869 end
842
870
843 should "be an optional list" do
871 should "be an optional list" do
844 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
872 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
845 end
873 end
846
874
847 should "have a list of the Roles as values" do
875 should "have a list of the Roles as values" do
848 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
876 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
849 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
877 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
850 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
878 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
851 end
879 end
852
880
853 should "not include the built in Roles as values" do
881 should "not include the built in Roles as values" do
854 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
882 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
855 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
883 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
856 end
884 end
857
885
858 end
886 end
859
887
860 end
888 end
861
889
862 context "#statement" do
890 context "#statement" do
863 context "with 'member_of_group' filter" do
891 context "with 'member_of_group' filter" do
864 setup do
892 setup do
865 Group.destroy_all # No fixtures
893 Group.destroy_all # No fixtures
866 @user_in_group = User.generate!
894 @user_in_group = User.generate!
867 @second_user_in_group = User.generate!
895 @second_user_in_group = User.generate!
868 @user_in_group2 = User.generate!
896 @user_in_group2 = User.generate!
869 @user_not_in_group = User.generate!
897 @user_not_in_group = User.generate!
870
898
871 @group = Group.generate!.reload
899 @group = Group.generate!.reload
872 @group.users << @user_in_group
900 @group.users << @user_in_group
873 @group.users << @second_user_in_group
901 @group.users << @second_user_in_group
874
902
875 @group2 = Group.generate!.reload
903 @group2 = Group.generate!.reload
876 @group2.users << @user_in_group2
904 @group2.users << @user_in_group2
877
905
878 end
906 end
879
907
880 should "search assigned to for users in the group" do
908 should "search assigned to for users in the group" do
881 @query = Query.new(:name => '_')
909 @query = Query.new(:name => '_')
882 @query.add_filter('member_of_group', '=', [@group.id.to_s])
910 @query.add_filter('member_of_group', '=', [@group.id.to_s])
883
911
884 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
912 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
885 assert_find_issues_with_query_is_successful @query
913 assert_find_issues_with_query_is_successful @query
886 end
914 end
887
915
888 should "search not assigned to any group member (none)" do
916 should "search not assigned to any group member (none)" do
889 @query = Query.new(:name => '_')
917 @query = Query.new(:name => '_')
890 @query.add_filter('member_of_group', '!*', [''])
918 @query.add_filter('member_of_group', '!*', [''])
891
919
892 # Users not in a group
920 # Users not in a group
893 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}')"
921 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}')"
894 assert_find_issues_with_query_is_successful @query
922 assert_find_issues_with_query_is_successful @query
895 end
923 end
896
924
897 should "search assigned to any group member (all)" do
925 should "search assigned to any group member (all)" do
898 @query = Query.new(:name => '_')
926 @query = Query.new(:name => '_')
899 @query.add_filter('member_of_group', '*', [''])
927 @query.add_filter('member_of_group', '*', [''])
900
928
901 # Only users in a group
929 # Only users in a group
902 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}')"
930 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}')"
903 assert_find_issues_with_query_is_successful @query
931 assert_find_issues_with_query_is_successful @query
904 end
932 end
905
933
906 should "return an empty set with = empty group" do
934 should "return an empty set with = empty group" do
907 @empty_group = Group.generate!
935 @empty_group = Group.generate!
908 @query = Query.new(:name => '_')
936 @query = Query.new(:name => '_')
909 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
937 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
910
938
911 assert_equal [], find_issues_with_query(@query)
939 assert_equal [], find_issues_with_query(@query)
912 end
940 end
913
941
914 should "return issues with ! empty group" do
942 should "return issues with ! empty group" do
915 @empty_group = Group.generate!
943 @empty_group = Group.generate!
916 @query = Query.new(:name => '_')
944 @query = Query.new(:name => '_')
917 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
945 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
918
946
919 assert_find_issues_with_query_is_successful @query
947 assert_find_issues_with_query_is_successful @query
920 end
948 end
921 end
949 end
922
950
923 context "with 'assigned_to_role' filter" do
951 context "with 'assigned_to_role' filter" do
924 setup do
952 setup do
925 @manager_role = Role.find_by_name('Manager')
953 @manager_role = Role.find_by_name('Manager')
926 @developer_role = Role.find_by_name('Developer')
954 @developer_role = Role.find_by_name('Developer')
927
955
928 @project = Project.generate!
956 @project = Project.generate!
929 @manager = User.generate!
957 @manager = User.generate!
930 @developer = User.generate!
958 @developer = User.generate!
931 @boss = User.generate!
959 @boss = User.generate!
932 @guest = User.generate!
960 @guest = User.generate!
933 User.add_to_project(@manager, @project, @manager_role)
961 User.add_to_project(@manager, @project, @manager_role)
934 User.add_to_project(@developer, @project, @developer_role)
962 User.add_to_project(@developer, @project, @developer_role)
935 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
963 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
936
964
937 @issue1 = Issue.generate_for_project!(@project, :assigned_to_id => @manager.id)
965 @issue1 = Issue.generate_for_project!(@project, :assigned_to_id => @manager.id)
938 @issue2 = Issue.generate_for_project!(@project, :assigned_to_id => @developer.id)
966 @issue2 = Issue.generate_for_project!(@project, :assigned_to_id => @developer.id)
939 @issue3 = Issue.generate_for_project!(@project, :assigned_to_id => @boss.id)
967 @issue3 = Issue.generate_for_project!(@project, :assigned_to_id => @boss.id)
940 @issue4 = Issue.generate_for_project!(@project, :assigned_to_id => @guest.id)
968 @issue4 = Issue.generate_for_project!(@project, :assigned_to_id => @guest.id)
941 @issue5 = Issue.generate_for_project!(@project)
969 @issue5 = Issue.generate_for_project!(@project)
942 end
970 end
943
971
944 should "search assigned to for users with the Role" do
972 should "search assigned to for users with the Role" do
945 @query = Query.new(:name => '_', :project => @project)
973 @query = Query.new(:name => '_', :project => @project)
946 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
974 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
947
975
948 assert_query_result [@issue1, @issue3], @query
976 assert_query_result [@issue1, @issue3], @query
949 end
977 end
950
978
951 should "search assigned to for users with the Role on the issue project" do
979 should "search assigned to for users with the Role on the issue project" do
952 other_project = Project.generate!
980 other_project = Project.generate!
953 User.add_to_project(@developer, other_project, @manager_role)
981 User.add_to_project(@developer, other_project, @manager_role)
954
982
955 @query = Query.new(:name => '_', :project => @project)
983 @query = Query.new(:name => '_', :project => @project)
956 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
984 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
957
985
958 assert_query_result [@issue1, @issue3], @query
986 assert_query_result [@issue1, @issue3], @query
959 end
987 end
960
988
961 should "return an empty set with empty role" do
989 should "return an empty set with empty role" do
962 @empty_role = Role.generate!
990 @empty_role = Role.generate!
963 @query = Query.new(:name => '_', :project => @project)
991 @query = Query.new(:name => '_', :project => @project)
964 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
992 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
965
993
966 assert_query_result [], @query
994 assert_query_result [], @query
967 end
995 end
968
996
969 should "search assigned to for users without the Role" do
997 should "search assigned to for users without the Role" do
970 @query = Query.new(:name => '_', :project => @project)
998 @query = Query.new(:name => '_', :project => @project)
971 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
999 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
972
1000
973 assert_query_result [@issue2, @issue4, @issue5], @query
1001 assert_query_result [@issue2, @issue4, @issue5], @query
974 end
1002 end
975
1003
976 should "search assigned to for users not assigned to any Role (none)" do
1004 should "search assigned to for users not assigned to any Role (none)" do
977 @query = Query.new(:name => '_', :project => @project)
1005 @query = Query.new(:name => '_', :project => @project)
978 @query.add_filter('assigned_to_role', '!*', [''])
1006 @query.add_filter('assigned_to_role', '!*', [''])
979
1007
980 assert_query_result [@issue4, @issue5], @query
1008 assert_query_result [@issue4, @issue5], @query
981 end
1009 end
982
1010
983 should "search assigned to for users assigned to any Role (all)" do
1011 should "search assigned to for users assigned to any Role (all)" do
984 @query = Query.new(:name => '_', :project => @project)
1012 @query = Query.new(:name => '_', :project => @project)
985 @query.add_filter('assigned_to_role', '*', [''])
1013 @query.add_filter('assigned_to_role', '*', [''])
986
1014
987 assert_query_result [@issue1, @issue2, @issue3], @query
1015 assert_query_result [@issue1, @issue2, @issue3], @query
988 end
1016 end
989
1017
990 should "return issues with ! empty role" do
1018 should "return issues with ! empty role" do
991 @empty_role = Role.generate!
1019 @empty_role = Role.generate!
992 @query = Query.new(:name => '_', :project => @project)
1020 @query = Query.new(:name => '_', :project => @project)
993 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
1021 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
994
1022
995 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
1023 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
996 end
1024 end
997 end
1025 end
998 end
1026 end
999
1027
1000 end
1028 end
General Comments 0
You need to be logged in to leave comments. Login now