##// END OF EJS Templates
Fixed: case sensitivity in issue subject filtering (#3536)....
Jean-Philippe Lang -
r2696:a7bb63a18280
parent child
Show More
@@ -1,466 +1,466
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 self.default_order = options[:default_order]
26 self.default_order = options[:default_order]
27 end
27 end
28
28
29 def caption
29 def caption
30 l("field_#{name}")
30 l("field_#{name}")
31 end
31 end
32
32
33 # Returns true if the column is sortable, otherwise false
33 # Returns true if the column is sortable, otherwise false
34 def sortable?
34 def sortable?
35 !sortable.nil?
35 !sortable.nil?
36 end
36 end
37 end
37 end
38
38
39 class QueryCustomFieldColumn < QueryColumn
39 class QueryCustomFieldColumn < QueryColumn
40
40
41 def initialize(custom_field)
41 def initialize(custom_field)
42 self.name = "cf_#{custom_field.id}".to_sym
42 self.name = "cf_#{custom_field.id}".to_sym
43 self.sortable = custom_field.order_statement || false
43 self.sortable = custom_field.order_statement || false
44 @cf = custom_field
44 @cf = custom_field
45 end
45 end
46
46
47 def caption
47 def caption
48 @cf.name
48 @cf.name
49 end
49 end
50
50
51 def custom_field
51 def custom_field
52 @cf
52 @cf
53 end
53 end
54 end
54 end
55
55
56 class Query < ActiveRecord::Base
56 class Query < ActiveRecord::Base
57 belongs_to :project
57 belongs_to :project
58 belongs_to :user
58 belongs_to :user
59 serialize :filters
59 serialize :filters
60 serialize :column_names
60 serialize :column_names
61 serialize :sort_criteria, Array
61 serialize :sort_criteria, Array
62
62
63 attr_protected :project_id, :user_id
63 attr_protected :project_id, :user_id
64
64
65 validates_presence_of :name, :on => :save
65 validates_presence_of :name, :on => :save
66 validates_length_of :name, :maximum => 255
66 validates_length_of :name, :maximum => 255
67
67
68 @@operators = { "=" => :label_equals,
68 @@operators = { "=" => :label_equals,
69 "!" => :label_not_equals,
69 "!" => :label_not_equals,
70 "o" => :label_open_issues,
70 "o" => :label_open_issues,
71 "c" => :label_closed_issues,
71 "c" => :label_closed_issues,
72 "!*" => :label_none,
72 "!*" => :label_none,
73 "*" => :label_all,
73 "*" => :label_all,
74 ">=" => :label_greater_or_equal,
74 ">=" => :label_greater_or_equal,
75 "<=" => :label_less_or_equal,
75 "<=" => :label_less_or_equal,
76 "<t+" => :label_in_less_than,
76 "<t+" => :label_in_less_than,
77 ">t+" => :label_in_more_than,
77 ">t+" => :label_in_more_than,
78 "t+" => :label_in,
78 "t+" => :label_in,
79 "t" => :label_today,
79 "t" => :label_today,
80 "w" => :label_this_week,
80 "w" => :label_this_week,
81 ">t-" => :label_less_than_ago,
81 ">t-" => :label_less_than_ago,
82 "<t-" => :label_more_than_ago,
82 "<t-" => :label_more_than_ago,
83 "t-" => :label_ago,
83 "t-" => :label_ago,
84 "~" => :label_contains,
84 "~" => :label_contains,
85 "!~" => :label_not_contains }
85 "!~" => :label_not_contains }
86
86
87 cattr_reader :operators
87 cattr_reader :operators
88
88
89 @@operators_by_filter_type = { :list => [ "=", "!" ],
89 @@operators_by_filter_type = { :list => [ "=", "!" ],
90 :list_status => [ "o", "=", "!", "c", "*" ],
90 :list_status => [ "o", "=", "!", "c", "*" ],
91 :list_optional => [ "=", "!", "!*", "*" ],
91 :list_optional => [ "=", "!", "!*", "*" ],
92 :list_subprojects => [ "*", "!*", "=" ],
92 :list_subprojects => [ "*", "!*", "=" ],
93 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
93 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
94 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
94 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
95 :string => [ "=", "~", "!", "!~" ],
95 :string => [ "=", "~", "!", "!~" ],
96 :text => [ "~", "!~" ],
96 :text => [ "~", "!~" ],
97 :integer => [ "=", ">=", "<=", "!*", "*" ] }
97 :integer => [ "=", ">=", "<=", "!*", "*" ] }
98
98
99 cattr_reader :operators_by_filter_type
99 cattr_reader :operators_by_filter_type
100
100
101 @@available_columns = [
101 @@available_columns = [
102 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
102 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
103 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
103 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
104 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
104 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
105 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
105 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
106 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
106 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
107 QueryColumn.new(:author),
107 QueryColumn.new(:author),
108 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
108 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
109 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
109 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
110 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
110 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
111 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
111 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
112 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
112 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
113 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
113 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
114 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
114 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
115 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
115 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
116 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
116 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
117 ]
117 ]
118 cattr_reader :available_columns
118 cattr_reader :available_columns
119
119
120 def initialize(attributes = nil)
120 def initialize(attributes = nil)
121 super attributes
121 super attributes
122 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
122 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
123 end
123 end
124
124
125 def after_initialize
125 def after_initialize
126 # Store the fact that project is nil (used in #editable_by?)
126 # Store the fact that project is nil (used in #editable_by?)
127 @is_for_all = project.nil?
127 @is_for_all = project.nil?
128 end
128 end
129
129
130 def validate
130 def validate
131 filters.each_key do |field|
131 filters.each_key do |field|
132 errors.add label_for(field), :blank unless
132 errors.add label_for(field), :blank unless
133 # filter requires one or more values
133 # filter requires one or more values
134 (values_for(field) and !values_for(field).first.blank?) or
134 (values_for(field) and !values_for(field).first.blank?) or
135 # filter doesn't require any value
135 # filter doesn't require any value
136 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
136 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
137 end if filters
137 end if filters
138 end
138 end
139
139
140 def editable_by?(user)
140 def editable_by?(user)
141 return false unless user
141 return false unless user
142 # Admin can edit them all and regular users can edit their private queries
142 # Admin can edit them all and regular users can edit their private queries
143 return true if user.admin? || (!is_public && self.user_id == user.id)
143 return true if user.admin? || (!is_public && self.user_id == user.id)
144 # Members can not edit public queries that are for all project (only admin is allowed to)
144 # Members can not edit public queries that are for all project (only admin is allowed to)
145 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
145 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
146 end
146 end
147
147
148 def available_filters
148 def available_filters
149 return @available_filters if @available_filters
149 return @available_filters if @available_filters
150
150
151 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
151 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
152
152
153 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
153 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
154 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
154 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
155 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
155 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
156 "subject" => { :type => :text, :order => 8 },
156 "subject" => { :type => :text, :order => 8 },
157 "created_on" => { :type => :date_past, :order => 9 },
157 "created_on" => { :type => :date_past, :order => 9 },
158 "updated_on" => { :type => :date_past, :order => 10 },
158 "updated_on" => { :type => :date_past, :order => 10 },
159 "start_date" => { :type => :date, :order => 11 },
159 "start_date" => { :type => :date, :order => 11 },
160 "due_date" => { :type => :date, :order => 12 },
160 "due_date" => { :type => :date, :order => 12 },
161 "estimated_hours" => { :type => :integer, :order => 13 },
161 "estimated_hours" => { :type => :integer, :order => 13 },
162 "done_ratio" => { :type => :integer, :order => 14 }}
162 "done_ratio" => { :type => :integer, :order => 14 }}
163
163
164 user_values = []
164 user_values = []
165 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
165 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
166 if project
166 if project
167 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
167 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
168 else
168 else
169 # members of the user's projects
169 # members of the user's projects
170 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
170 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
171 end
171 end
172 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
172 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
173 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
173 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
174
174
175 if User.current.logged?
175 if User.current.logged?
176 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
176 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
177 end
177 end
178
178
179 if project
179 if project
180 # project specific filters
180 # project specific filters
181 unless @project.issue_categories.empty?
181 unless @project.issue_categories.empty?
182 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
182 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
183 end
183 end
184 unless @project.versions.empty?
184 unless @project.versions.empty?
185 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
185 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
186 end
186 end
187 unless @project.descendants.active.empty?
187 unless @project.descendants.active.empty?
188 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
188 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
189 end
189 end
190 add_custom_fields_filters(@project.all_issue_custom_fields)
190 add_custom_fields_filters(@project.all_issue_custom_fields)
191 else
191 else
192 # global filters for cross project issue list
192 # global filters for cross project issue list
193 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
193 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
194 end
194 end
195 @available_filters
195 @available_filters
196 end
196 end
197
197
198 def add_filter(field, operator, values)
198 def add_filter(field, operator, values)
199 # values must be an array
199 # values must be an array
200 return unless values and values.is_a? Array # and !values.first.empty?
200 return unless values and values.is_a? Array # and !values.first.empty?
201 # check if field is defined as an available filter
201 # check if field is defined as an available filter
202 if available_filters.has_key? field
202 if available_filters.has_key? field
203 filter_options = available_filters[field]
203 filter_options = available_filters[field]
204 # check if operator is allowed for that filter
204 # check if operator is allowed for that filter
205 #if @@operators_by_filter_type[filter_options[:type]].include? operator
205 #if @@operators_by_filter_type[filter_options[:type]].include? operator
206 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
206 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
207 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
207 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
208 #end
208 #end
209 filters[field] = {:operator => operator, :values => values }
209 filters[field] = {:operator => operator, :values => values }
210 end
210 end
211 end
211 end
212
212
213 def add_short_filter(field, expression)
213 def add_short_filter(field, expression)
214 return unless expression
214 return unless expression
215 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
215 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
216 add_filter field, (parms[0] || "="), [parms[1] || ""]
216 add_filter field, (parms[0] || "="), [parms[1] || ""]
217 end
217 end
218
218
219 def has_filter?(field)
219 def has_filter?(field)
220 filters and filters[field]
220 filters and filters[field]
221 end
221 end
222
222
223 def operator_for(field)
223 def operator_for(field)
224 has_filter?(field) ? filters[field][:operator] : nil
224 has_filter?(field) ? filters[field][:operator] : nil
225 end
225 end
226
226
227 def values_for(field)
227 def values_for(field)
228 has_filter?(field) ? filters[field][:values] : nil
228 has_filter?(field) ? filters[field][:values] : nil
229 end
229 end
230
230
231 def label_for(field)
231 def label_for(field)
232 label = available_filters[field][:name] if available_filters.has_key?(field)
232 label = available_filters[field][:name] if available_filters.has_key?(field)
233 label ||= field.gsub(/\_id$/, "")
233 label ||= field.gsub(/\_id$/, "")
234 end
234 end
235
235
236 def available_columns
236 def available_columns
237 return @available_columns if @available_columns
237 return @available_columns if @available_columns
238 @available_columns = Query.available_columns
238 @available_columns = Query.available_columns
239 @available_columns += (project ?
239 @available_columns += (project ?
240 project.all_issue_custom_fields :
240 project.all_issue_custom_fields :
241 IssueCustomField.find(:all)
241 IssueCustomField.find(:all)
242 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
242 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
243 end
243 end
244
244
245 # Returns an array of columns that can be used to group the results
245 # Returns an array of columns that can be used to group the results
246 def groupable_columns
246 def groupable_columns
247 available_columns.select {|c| c.groupable}
247 available_columns.select {|c| c.groupable}
248 end
248 end
249
249
250 def columns
250 def columns
251 if has_default_columns?
251 if has_default_columns?
252 available_columns.select do |c|
252 available_columns.select do |c|
253 # Adds the project column by default for cross-project lists
253 # Adds the project column by default for cross-project lists
254 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
254 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
255 end
255 end
256 else
256 else
257 # preserve the column_names order
257 # preserve the column_names order
258 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
258 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
259 end
259 end
260 end
260 end
261
261
262 def column_names=(names)
262 def column_names=(names)
263 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
263 names = names.select {|n| n.is_a?(Symbol) || !n.blank? } if names
264 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
264 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } if names
265 write_attribute(:column_names, names)
265 write_attribute(:column_names, names)
266 end
266 end
267
267
268 def has_column?(column)
268 def has_column?(column)
269 column_names && column_names.include?(column.name)
269 column_names && column_names.include?(column.name)
270 end
270 end
271
271
272 def has_default_columns?
272 def has_default_columns?
273 column_names.nil? || column_names.empty?
273 column_names.nil? || column_names.empty?
274 end
274 end
275
275
276 def sort_criteria=(arg)
276 def sort_criteria=(arg)
277 c = []
277 c = []
278 if arg.is_a?(Hash)
278 if arg.is_a?(Hash)
279 arg = arg.keys.sort.collect {|k| arg[k]}
279 arg = arg.keys.sort.collect {|k| arg[k]}
280 end
280 end
281 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
281 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
282 write_attribute(:sort_criteria, c)
282 write_attribute(:sort_criteria, c)
283 end
283 end
284
284
285 def sort_criteria
285 def sort_criteria
286 read_attribute(:sort_criteria) || []
286 read_attribute(:sort_criteria) || []
287 end
287 end
288
288
289 def sort_criteria_key(arg)
289 def sort_criteria_key(arg)
290 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
290 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
291 end
291 end
292
292
293 def sort_criteria_order(arg)
293 def sort_criteria_order(arg)
294 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
294 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
295 end
295 end
296
296
297 # Returns the SQL sort order that should be prepended for grouping
297 # Returns the SQL sort order that should be prepended for grouping
298 def group_by_sort_order
298 def group_by_sort_order
299 if grouped? && (column = group_by_column)
299 if grouped? && (column = group_by_column)
300 column.sortable.is_a?(Array) ?
300 column.sortable.is_a?(Array) ?
301 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
301 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
302 "#{column.sortable} #{column.default_order}"
302 "#{column.sortable} #{column.default_order}"
303 end
303 end
304 end
304 end
305
305
306 # Returns true if the query is a grouped query
306 # Returns true if the query is a grouped query
307 def grouped?
307 def grouped?
308 !group_by.blank?
308 !group_by.blank?
309 end
309 end
310
310
311 def group_by_column
311 def group_by_column
312 groupable_columns.detect {|c| c.name.to_s == group_by}
312 groupable_columns.detect {|c| c.name.to_s == group_by}
313 end
313 end
314
314
315 def project_statement
315 def project_statement
316 project_clauses = []
316 project_clauses = []
317 if project && !@project.descendants.active.empty?
317 if project && !@project.descendants.active.empty?
318 ids = [project.id]
318 ids = [project.id]
319 if has_filter?("subproject_id")
319 if has_filter?("subproject_id")
320 case operator_for("subproject_id")
320 case operator_for("subproject_id")
321 when '='
321 when '='
322 # include the selected subprojects
322 # include the selected subprojects
323 ids += values_for("subproject_id").each(&:to_i)
323 ids += values_for("subproject_id").each(&:to_i)
324 when '!*'
324 when '!*'
325 # main project only
325 # main project only
326 else
326 else
327 # all subprojects
327 # all subprojects
328 ids += project.descendants.collect(&:id)
328 ids += project.descendants.collect(&:id)
329 end
329 end
330 elsif Setting.display_subprojects_issues?
330 elsif Setting.display_subprojects_issues?
331 ids += project.descendants.collect(&:id)
331 ids += project.descendants.collect(&:id)
332 end
332 end
333 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
333 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
334 elsif project
334 elsif project
335 project_clauses << "#{Project.table_name}.id = %d" % project.id
335 project_clauses << "#{Project.table_name}.id = %d" % project.id
336 end
336 end
337 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
337 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
338 project_clauses.join(' AND ')
338 project_clauses.join(' AND ')
339 end
339 end
340
340
341 def statement
341 def statement
342 # filters clauses
342 # filters clauses
343 filters_clauses = []
343 filters_clauses = []
344 filters.each_key do |field|
344 filters.each_key do |field|
345 next if field == "subproject_id"
345 next if field == "subproject_id"
346 v = values_for(field).clone
346 v = values_for(field).clone
347 next unless v and !v.empty?
347 next unless v and !v.empty?
348 operator = operator_for(field)
348 operator = operator_for(field)
349
349
350 # "me" value subsitution
350 # "me" value subsitution
351 if %w(assigned_to_id author_id watcher_id).include?(field)
351 if %w(assigned_to_id author_id watcher_id).include?(field)
352 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
352 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
353 end
353 end
354
354
355 sql = ''
355 sql = ''
356 if field =~ /^cf_(\d+)$/
356 if field =~ /^cf_(\d+)$/
357 # custom field
357 # custom field
358 db_table = CustomValue.table_name
358 db_table = CustomValue.table_name
359 db_field = 'value'
359 db_field = 'value'
360 is_custom_filter = true
360 is_custom_filter = true
361 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 "
361 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 "
362 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
362 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
363 elsif field == 'watcher_id'
363 elsif field == 'watcher_id'
364 db_table = Watcher.table_name
364 db_table = Watcher.table_name
365 db_field = 'user_id'
365 db_field = 'user_id'
366 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
366 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
367 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
367 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
368 else
368 else
369 # regular field
369 # regular field
370 db_table = Issue.table_name
370 db_table = Issue.table_name
371 db_field = field
371 db_field = field
372 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
372 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
373 end
373 end
374 filters_clauses << sql
374 filters_clauses << sql
375
375
376 end if filters and valid?
376 end if filters and valid?
377
377
378 (filters_clauses << project_statement).join(' AND ')
378 (filters_clauses << project_statement).join(' AND ')
379 end
379 end
380
380
381 private
381 private
382
382
383 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
383 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
384 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
384 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
385 sql = ''
385 sql = ''
386 case operator
386 case operator
387 when "="
387 when "="
388 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
388 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
389 when "!"
389 when "!"
390 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
390 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
391 when "!*"
391 when "!*"
392 sql = "#{db_table}.#{db_field} IS NULL"
392 sql = "#{db_table}.#{db_field} IS NULL"
393 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
393 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
394 when "*"
394 when "*"
395 sql = "#{db_table}.#{db_field} IS NOT NULL"
395 sql = "#{db_table}.#{db_field} IS NOT NULL"
396 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
396 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
397 when ">="
397 when ">="
398 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
398 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
399 when "<="
399 when "<="
400 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
400 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
401 when "o"
401 when "o"
402 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
402 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
403 when "c"
403 when "c"
404 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
404 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
405 when ">t-"
405 when ">t-"
406 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
406 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
407 when "<t-"
407 when "<t-"
408 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
408 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
409 when "t-"
409 when "t-"
410 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
410 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
411 when ">t+"
411 when ">t+"
412 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
412 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
413 when "<t+"
413 when "<t+"
414 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
414 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
415 when "t+"
415 when "t+"
416 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
416 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
417 when "t"
417 when "t"
418 sql = date_range_clause(db_table, db_field, 0, 0)
418 sql = date_range_clause(db_table, db_field, 0, 0)
419 when "w"
419 when "w"
420 from = l(:general_first_day_of_week) == '7' ?
420 from = l(:general_first_day_of_week) == '7' ?
421 # week starts on sunday
421 # week starts on sunday
422 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
422 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
423 # week starts on monday (Rails default)
423 # week starts on monday (Rails default)
424 Time.now.at_beginning_of_week
424 Time.now.at_beginning_of_week
425 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
425 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
426 when "~"
426 when "~"
427 sql = "#{db_table}.#{db_field} LIKE '%#{connection.quote_string(value.first)}%'"
427 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
428 when "!~"
428 when "!~"
429 sql = "#{db_table}.#{db_field} NOT LIKE '%#{connection.quote_string(value.first)}%'"
429 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
430 end
430 end
431
431
432 return sql
432 return sql
433 end
433 end
434
434
435 def add_custom_fields_filters(custom_fields)
435 def add_custom_fields_filters(custom_fields)
436 @available_filters ||= {}
436 @available_filters ||= {}
437
437
438 custom_fields.select(&:is_filter?).each do |field|
438 custom_fields.select(&:is_filter?).each do |field|
439 case field.field_format
439 case field.field_format
440 when "text"
440 when "text"
441 options = { :type => :text, :order => 20 }
441 options = { :type => :text, :order => 20 }
442 when "list"
442 when "list"
443 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
443 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
444 when "date"
444 when "date"
445 options = { :type => :date, :order => 20 }
445 options = { :type => :date, :order => 20 }
446 when "bool"
446 when "bool"
447 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
447 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
448 else
448 else
449 options = { :type => :string, :order => 20 }
449 options = { :type => :string, :order => 20 }
450 end
450 end
451 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
451 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
452 end
452 end
453 end
453 end
454
454
455 # Returns a SQL clause for a date or datetime field.
455 # Returns a SQL clause for a date or datetime field.
456 def date_range_clause(table, field, from, to)
456 def date_range_clause(table, field, from, to)
457 s = []
457 s = []
458 if from
458 if from
459 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
459 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
460 end
460 end
461 if to
461 if to
462 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
462 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
463 end
463 end
464 s.join(' AND ')
464 s.join(' AND ')
465 end
465 end
466 end
466 end
@@ -1,299 +1,301
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class QueryTest < Test::Unit::TestCase
20 class QueryTest < Test::Unit::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 find_issues_with_query(query)
29 def find_issues_with_query(query)
30 Issue.find :all,
30 Issue.find :all,
31 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
31 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
32 :conditions => query.statement
32 :conditions => query.statement
33 end
33 end
34
34
35 def test_query_with_multiple_custom_fields
35 def test_query_with_multiple_custom_fields
36 query = Query.find(1)
36 query = Query.find(1)
37 assert query.valid?
37 assert query.valid?
38 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
38 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
39 issues = find_issues_with_query(query)
39 issues = find_issues_with_query(query)
40 assert_equal 1, issues.length
40 assert_equal 1, issues.length
41 assert_equal Issue.find(3), issues.first
41 assert_equal Issue.find(3), issues.first
42 end
42 end
43
43
44 def test_operator_none
44 def test_operator_none
45 query = Query.new(:project => Project.find(1), :name => '_')
45 query = Query.new(:project => Project.find(1), :name => '_')
46 query.add_filter('fixed_version_id', '!*', [''])
46 query.add_filter('fixed_version_id', '!*', [''])
47 query.add_filter('cf_1', '!*', [''])
47 query.add_filter('cf_1', '!*', [''])
48 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
48 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
49 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
49 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
50 find_issues_with_query(query)
50 find_issues_with_query(query)
51 end
51 end
52
52
53 def test_operator_none_for_integer
53 def test_operator_none_for_integer
54 query = Query.new(:project => Project.find(1), :name => '_')
54 query = Query.new(:project => Project.find(1), :name => '_')
55 query.add_filter('estimated_hours', '!*', [''])
55 query.add_filter('estimated_hours', '!*', [''])
56 issues = find_issues_with_query(query)
56 issues = find_issues_with_query(query)
57 assert !issues.empty?
57 assert !issues.empty?
58 assert issues.all? {|i| !i.estimated_hours}
58 assert issues.all? {|i| !i.estimated_hours}
59 end
59 end
60
60
61 def test_operator_all
61 def test_operator_all
62 query = Query.new(:project => Project.find(1), :name => '_')
62 query = Query.new(:project => Project.find(1), :name => '_')
63 query.add_filter('fixed_version_id', '*', [''])
63 query.add_filter('fixed_version_id', '*', [''])
64 query.add_filter('cf_1', '*', [''])
64 query.add_filter('cf_1', '*', [''])
65 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
65 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
66 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
66 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
67 find_issues_with_query(query)
67 find_issues_with_query(query)
68 end
68 end
69
69
70 def test_operator_greater_than
70 def test_operator_greater_than
71 query = Query.new(:project => Project.find(1), :name => '_')
71 query = Query.new(:project => Project.find(1), :name => '_')
72 query.add_filter('done_ratio', '>=', ['40'])
72 query.add_filter('done_ratio', '>=', ['40'])
73 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
73 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
74 find_issues_with_query(query)
74 find_issues_with_query(query)
75 end
75 end
76
76
77 def test_operator_in_more_than
77 def test_operator_in_more_than
78 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
78 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
79 query = Query.new(:project => Project.find(1), :name => '_')
79 query = Query.new(:project => Project.find(1), :name => '_')
80 query.add_filter('due_date', '>t+', ['15'])
80 query.add_filter('due_date', '>t+', ['15'])
81 issues = find_issues_with_query(query)
81 issues = find_issues_with_query(query)
82 assert !issues.empty?
82 assert !issues.empty?
83 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
83 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
84 end
84 end
85
85
86 def test_operator_in_less_than
86 def test_operator_in_less_than
87 query = Query.new(:project => Project.find(1), :name => '_')
87 query = Query.new(:project => Project.find(1), :name => '_')
88 query.add_filter('due_date', '<t+', ['15'])
88 query.add_filter('due_date', '<t+', ['15'])
89 issues = find_issues_with_query(query)
89 issues = find_issues_with_query(query)
90 assert !issues.empty?
90 assert !issues.empty?
91 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
91 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
92 end
92 end
93
93
94 def test_operator_less_than_ago
94 def test_operator_less_than_ago
95 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
95 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
96 query = Query.new(:project => Project.find(1), :name => '_')
96 query = Query.new(:project => Project.find(1), :name => '_')
97 query.add_filter('due_date', '>t-', ['3'])
97 query.add_filter('due_date', '>t-', ['3'])
98 issues = find_issues_with_query(query)
98 issues = find_issues_with_query(query)
99 assert !issues.empty?
99 assert !issues.empty?
100 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
100 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
101 end
101 end
102
102
103 def test_operator_more_than_ago
103 def test_operator_more_than_ago
104 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
104 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
105 query = Query.new(:project => Project.find(1), :name => '_')
105 query = Query.new(:project => Project.find(1), :name => '_')
106 query.add_filter('due_date', '<t-', ['10'])
106 query.add_filter('due_date', '<t-', ['10'])
107 assert query.statement.include?("#{Issue.table_name}.due_date <=")
107 assert query.statement.include?("#{Issue.table_name}.due_date <=")
108 issues = find_issues_with_query(query)
108 issues = find_issues_with_query(query)
109 assert !issues.empty?
109 assert !issues.empty?
110 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
110 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
111 end
111 end
112
112
113 def test_operator_in
113 def test_operator_in
114 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
114 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
115 query = Query.new(:project => Project.find(1), :name => '_')
115 query = Query.new(:project => Project.find(1), :name => '_')
116 query.add_filter('due_date', 't+', ['2'])
116 query.add_filter('due_date', 't+', ['2'])
117 issues = find_issues_with_query(query)
117 issues = find_issues_with_query(query)
118 assert !issues.empty?
118 assert !issues.empty?
119 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
119 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
120 end
120 end
121
121
122 def test_operator_ago
122 def test_operator_ago
123 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
123 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
124 query = Query.new(:project => Project.find(1), :name => '_')
124 query = Query.new(:project => Project.find(1), :name => '_')
125 query.add_filter('due_date', 't-', ['3'])
125 query.add_filter('due_date', 't-', ['3'])
126 issues = find_issues_with_query(query)
126 issues = find_issues_with_query(query)
127 assert !issues.empty?
127 assert !issues.empty?
128 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
128 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
129 end
129 end
130
130
131 def test_operator_today
131 def test_operator_today
132 query = Query.new(:project => Project.find(1), :name => '_')
132 query = Query.new(:project => Project.find(1), :name => '_')
133 query.add_filter('due_date', 't', [''])
133 query.add_filter('due_date', 't', [''])
134 issues = find_issues_with_query(query)
134 issues = find_issues_with_query(query)
135 assert !issues.empty?
135 assert !issues.empty?
136 issues.each {|issue| assert_equal Date.today, issue.due_date}
136 issues.each {|issue| assert_equal Date.today, issue.due_date}
137 end
137 end
138
138
139 def test_operator_this_week_on_date
139 def test_operator_this_week_on_date
140 query = Query.new(:project => Project.find(1), :name => '_')
140 query = Query.new(:project => Project.find(1), :name => '_')
141 query.add_filter('due_date', 'w', [''])
141 query.add_filter('due_date', 'w', [''])
142 find_issues_with_query(query)
142 find_issues_with_query(query)
143 end
143 end
144
144
145 def test_operator_this_week_on_datetime
145 def test_operator_this_week_on_datetime
146 query = Query.new(:project => Project.find(1), :name => '_')
146 query = Query.new(:project => Project.find(1), :name => '_')
147 query.add_filter('created_on', 'w', [''])
147 query.add_filter('created_on', 'w', [''])
148 find_issues_with_query(query)
148 find_issues_with_query(query)
149 end
149 end
150
150
151 def test_operator_contains
151 def test_operator_contains
152 query = Query.new(:project => Project.find(1), :name => '_')
152 query = Query.new(:project => Project.find(1), :name => '_')
153 query.add_filter('subject', '~', ['string'])
153 query.add_filter('subject', '~', ['uNable'])
154 assert query.statement.include?("#{Issue.table_name}.subject LIKE '%string%'")
154 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
155 find_issues_with_query(query)
155 result = find_issues_with_query(query)
156 assert result.empty?
157 result.each {|issue| assert issue.subject.downcase.include?('unable') }
156 end
158 end
157
159
158 def test_operator_does_not_contains
160 def test_operator_does_not_contains
159 query = Query.new(:project => Project.find(1), :name => '_')
161 query = Query.new(:project => Project.find(1), :name => '_')
160 query.add_filter('subject', '!~', ['string'])
162 query.add_filter('subject', '!~', ['uNable'])
161 assert query.statement.include?("#{Issue.table_name}.subject NOT LIKE '%string%'")
163 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
162 find_issues_with_query(query)
164 find_issues_with_query(query)
163 end
165 end
164
166
165 def test_filter_watched_issues
167 def test_filter_watched_issues
166 User.current = User.find(1)
168 User.current = User.find(1)
167 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
169 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
168 result = find_issues_with_query(query)
170 result = find_issues_with_query(query)
169 assert_not_nil result
171 assert_not_nil result
170 assert !result.empty?
172 assert !result.empty?
171 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
173 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
172 User.current = nil
174 User.current = nil
173 end
175 end
174
176
175 def test_filter_unwatched_issues
177 def test_filter_unwatched_issues
176 User.current = User.find(1)
178 User.current = User.find(1)
177 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
179 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
178 result = find_issues_with_query(query)
180 result = find_issues_with_query(query)
179 assert_not_nil result
181 assert_not_nil result
180 assert !result.empty?
182 assert !result.empty?
181 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
183 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
182 User.current = nil
184 User.current = nil
183 end
185 end
184
186
185 def test_default_columns
187 def test_default_columns
186 q = Query.new
188 q = Query.new
187 assert !q.columns.empty?
189 assert !q.columns.empty?
188 end
190 end
189
191
190 def test_set_column_names
192 def test_set_column_names
191 q = Query.new
193 q = Query.new
192 q.column_names = ['tracker', :subject, '', 'unknonw_column']
194 q.column_names = ['tracker', :subject, '', 'unknonw_column']
193 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
195 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
194 c = q.columns.first
196 c = q.columns.first
195 assert q.has_column?(c)
197 assert q.has_column?(c)
196 end
198 end
197
199
198 def test_default_sort
200 def test_default_sort
199 q = Query.new
201 q = Query.new
200 assert_equal [], q.sort_criteria
202 assert_equal [], q.sort_criteria
201 end
203 end
202
204
203 def test_set_sort_criteria_with_hash
205 def test_set_sort_criteria_with_hash
204 q = Query.new
206 q = Query.new
205 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
207 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
206 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
208 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
207 end
209 end
208
210
209 def test_set_sort_criteria_with_array
211 def test_set_sort_criteria_with_array
210 q = Query.new
212 q = Query.new
211 q.sort_criteria = [['priority', 'desc'], 'tracker']
213 q.sort_criteria = [['priority', 'desc'], 'tracker']
212 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
214 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
213 end
215 end
214
216
215 def test_create_query_with_sort
217 def test_create_query_with_sort
216 q = Query.new(:name => 'Sorted')
218 q = Query.new(:name => 'Sorted')
217 q.sort_criteria = [['priority', 'desc'], 'tracker']
219 q.sort_criteria = [['priority', 'desc'], 'tracker']
218 assert q.save
220 assert q.save
219 q.reload
221 q.reload
220 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
222 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
221 end
223 end
222
224
223 def test_sort_by_string_custom_field_asc
225 def test_sort_by_string_custom_field_asc
224 q = Query.new
226 q = Query.new
225 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
227 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
226 assert c
228 assert c
227 assert c.sortable
229 assert c.sortable
228 issues = Issue.find :all,
230 issues = Issue.find :all,
229 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
231 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
230 :conditions => q.statement,
232 :conditions => q.statement,
231 :order => "#{c.sortable} ASC"
233 :order => "#{c.sortable} ASC"
232 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
234 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
233 assert !values.empty?
235 assert !values.empty?
234 assert_equal values.sort, values
236 assert_equal values.sort, values
235 end
237 end
236
238
237 def test_sort_by_string_custom_field_desc
239 def test_sort_by_string_custom_field_desc
238 q = Query.new
240 q = Query.new
239 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
241 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
240 assert c
242 assert c
241 assert c.sortable
243 assert c.sortable
242 issues = Issue.find :all,
244 issues = Issue.find :all,
243 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
245 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
244 :conditions => q.statement,
246 :conditions => q.statement,
245 :order => "#{c.sortable} DESC"
247 :order => "#{c.sortable} DESC"
246 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
248 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
247 assert !values.empty?
249 assert !values.empty?
248 assert_equal values.sort.reverse, values
250 assert_equal values.sort.reverse, values
249 end
251 end
250
252
251 def test_sort_by_float_custom_field_asc
253 def test_sort_by_float_custom_field_asc
252 q = Query.new
254 q = Query.new
253 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
255 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
254 assert c
256 assert c
255 assert c.sortable
257 assert c.sortable
256 issues = Issue.find :all,
258 issues = Issue.find :all,
257 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
259 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
258 :conditions => q.statement,
260 :conditions => q.statement,
259 :order => "#{c.sortable} ASC"
261 :order => "#{c.sortable} ASC"
260 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
262 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
261 assert !values.empty?
263 assert !values.empty?
262 assert_equal values.sort, values
264 assert_equal values.sort, values
263 end
265 end
264
266
265 def test_label_for
267 def test_label_for
266 q = Query.new
268 q = Query.new
267 assert_equal 'assigned_to', q.label_for('assigned_to_id')
269 assert_equal 'assigned_to', q.label_for('assigned_to_id')
268 end
270 end
269
271
270 def test_editable_by
272 def test_editable_by
271 admin = User.find(1)
273 admin = User.find(1)
272 manager = User.find(2)
274 manager = User.find(2)
273 developer = User.find(3)
275 developer = User.find(3)
274
276
275 # Public query on project 1
277 # Public query on project 1
276 q = Query.find(1)
278 q = Query.find(1)
277 assert q.editable_by?(admin)
279 assert q.editable_by?(admin)
278 assert q.editable_by?(manager)
280 assert q.editable_by?(manager)
279 assert !q.editable_by?(developer)
281 assert !q.editable_by?(developer)
280
282
281 # Private query on project 1
283 # Private query on project 1
282 q = Query.find(2)
284 q = Query.find(2)
283 assert q.editable_by?(admin)
285 assert q.editable_by?(admin)
284 assert !q.editable_by?(manager)
286 assert !q.editable_by?(manager)
285 assert q.editable_by?(developer)
287 assert q.editable_by?(developer)
286
288
287 # Private query for all projects
289 # Private query for all projects
288 q = Query.find(3)
290 q = Query.find(3)
289 assert q.editable_by?(admin)
291 assert q.editable_by?(admin)
290 assert !q.editable_by?(manager)
292 assert !q.editable_by?(manager)
291 assert q.editable_by?(developer)
293 assert q.editable_by?(developer)
292
294
293 # Public query for all projects
295 # Public query for all projects
294 q = Query.find(4)
296 q = Query.find(4)
295 assert q.editable_by?(admin)
297 assert q.editable_by?(admin)
296 assert !q.editable_by?(manager)
298 assert !q.editable_by?(manager)
297 assert !q.editable_by?(developer)
299 assert !q.editable_by?(developer)
298 end
300 end
299 end
301 end
General Comments 0
You need to be logged in to leave comments. Login now