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