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