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