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