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