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