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