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