##// END OF EJS Templates
Adds "me" to user custom fields filters (#9923)....
Jean-Philippe Lang -
r8518:980b6db5c022
parent child
Show More
@@ -1,846 +1,856
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 sortable
42 def sortable
43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
44 end
44 end
45
45
46 def value(issue)
46 def value(issue)
47 issue.send name
47 issue.send name
48 end
48 end
49
49
50 def css_classes
50 def css_classes
51 name
51 name
52 end
52 end
53 end
53 end
54
54
55 class QueryCustomFieldColumn < QueryColumn
55 class QueryCustomFieldColumn < QueryColumn
56
56
57 def initialize(custom_field)
57 def initialize(custom_field)
58 self.name = "cf_#{custom_field.id}".to_sym
58 self.name = "cf_#{custom_field.id}".to_sym
59 self.sortable = custom_field.order_statement || false
59 self.sortable = custom_field.order_statement || false
60 if %w(list date bool int).include?(custom_field.field_format)
60 if %w(list date bool int).include?(custom_field.field_format)
61 self.groupable = custom_field.order_statement
61 self.groupable = custom_field.order_statement
62 end
62 end
63 self.groupable ||= false
63 self.groupable ||= false
64 @cf = custom_field
64 @cf = custom_field
65 end
65 end
66
66
67 def caption
67 def caption
68 @cf.name
68 @cf.name
69 end
69 end
70
70
71 def custom_field
71 def custom_field
72 @cf
72 @cf
73 end
73 end
74
74
75 def value(issue)
75 def value(issue)
76 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
76 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
77 cv && @cf.cast_value(cv.value)
77 cv && @cf.cast_value(cv.value)
78 end
78 end
79
79
80 def css_classes
80 def css_classes
81 @css_classes ||= "#{name} #{@cf.field_format}"
81 @css_classes ||= "#{name} #{@cf.field_format}"
82 end
82 end
83 end
83 end
84
84
85 class Query < ActiveRecord::Base
85 class Query < ActiveRecord::Base
86 class StatementInvalid < ::ActiveRecord::StatementInvalid
86 class StatementInvalid < ::ActiveRecord::StatementInvalid
87 end
87 end
88
88
89 belongs_to :project
89 belongs_to :project
90 belongs_to :user
90 belongs_to :user
91 serialize :filters
91 serialize :filters
92 serialize :column_names
92 serialize :column_names
93 serialize :sort_criteria, Array
93 serialize :sort_criteria, Array
94
94
95 attr_protected :project_id, :user_id
95 attr_protected :project_id, :user_id
96
96
97 validates_presence_of :name, :on => :save
97 validates_presence_of :name, :on => :save
98 validates_length_of :name, :maximum => 255
98 validates_length_of :name, :maximum => 255
99 validate :validate_query_filters
99 validate :validate_query_filters
100
100
101 @@operators = { "=" => :label_equals,
101 @@operators = { "=" => :label_equals,
102 "!" => :label_not_equals,
102 "!" => :label_not_equals,
103 "o" => :label_open_issues,
103 "o" => :label_open_issues,
104 "c" => :label_closed_issues,
104 "c" => :label_closed_issues,
105 "!*" => :label_none,
105 "!*" => :label_none,
106 "*" => :label_all,
106 "*" => :label_all,
107 ">=" => :label_greater_or_equal,
107 ">=" => :label_greater_or_equal,
108 "<=" => :label_less_or_equal,
108 "<=" => :label_less_or_equal,
109 "><" => :label_between,
109 "><" => :label_between,
110 "<t+" => :label_in_less_than,
110 "<t+" => :label_in_less_than,
111 ">t+" => :label_in_more_than,
111 ">t+" => :label_in_more_than,
112 "t+" => :label_in,
112 "t+" => :label_in,
113 "t" => :label_today,
113 "t" => :label_today,
114 "w" => :label_this_week,
114 "w" => :label_this_week,
115 ">t-" => :label_less_than_ago,
115 ">t-" => :label_less_than_ago,
116 "<t-" => :label_more_than_ago,
116 "<t-" => :label_more_than_ago,
117 "t-" => :label_ago,
117 "t-" => :label_ago,
118 "~" => :label_contains,
118 "~" => :label_contains,
119 "!~" => :label_not_contains }
119 "!~" => :label_not_contains }
120
120
121 cattr_reader :operators
121 cattr_reader :operators
122
122
123 @@operators_by_filter_type = { :list => [ "=", "!" ],
123 @@operators_by_filter_type = { :list => [ "=", "!" ],
124 :list_status => [ "o", "=", "!", "c", "*" ],
124 :list_status => [ "o", "=", "!", "c", "*" ],
125 :list_optional => [ "=", "!", "!*", "*" ],
125 :list_optional => [ "=", "!", "!*", "*" ],
126 :list_subprojects => [ "*", "!*", "=" ],
126 :list_subprojects => [ "*", "!*", "=" ],
127 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ],
127 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ],
128 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ],
128 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ],
129 :string => [ "=", "~", "!", "!~" ],
129 :string => [ "=", "~", "!", "!~" ],
130 :text => [ "~", "!~" ],
130 :text => [ "~", "!~" ],
131 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
131 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
132 :float => [ "=", ">=", "<=", "><", "!*", "*" ] }
132 :float => [ "=", ">=", "<=", "><", "!*", "*" ] }
133
133
134 cattr_reader :operators_by_filter_type
134 cattr_reader :operators_by_filter_type
135
135
136 @@available_columns = [
136 @@available_columns = [
137 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
137 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
138 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
138 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
139 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
139 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
140 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
140 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
141 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
141 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
142 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
142 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
143 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
143 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
144 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
144 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
145 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
145 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
146 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
146 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
147 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
147 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
148 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
148 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
149 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
149 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
150 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
150 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
151 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
151 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
152 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
152 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
153 ]
153 ]
154 cattr_reader :available_columns
154 cattr_reader :available_columns
155
155
156 named_scope :visible, lambda {|*args|
156 named_scope :visible, lambda {|*args|
157 user = args.shift || User.current
157 user = args.shift || User.current
158 base = Project.allowed_to_condition(user, :view_issues, *args)
158 base = Project.allowed_to_condition(user, :view_issues, *args)
159 user_id = user.logged? ? user.id : 0
159 user_id = user.logged? ? user.id : 0
160 {
160 {
161 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
161 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
162 :include => :project
162 :include => :project
163 }
163 }
164 }
164 }
165
165
166 def initialize(attributes=nil, *args)
166 def initialize(attributes=nil, *args)
167 super attributes
167 super attributes
168 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
168 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
169 @is_for_all = project.nil?
169 @is_for_all = project.nil?
170 end
170 end
171
171
172 def validate_query_filters
172 def validate_query_filters
173 filters.each_key do |field|
173 filters.each_key do |field|
174 if values_for(field)
174 if values_for(field)
175 case type_for(field)
175 case type_for(field)
176 when :integer
176 when :integer
177 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
177 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
178 when :float
178 when :float
179 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+(\.\d*)?$/) }
179 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+(\.\d*)?$/) }
180 when :date, :date_past
180 when :date, :date_past
181 case operator_for(field)
181 case operator_for(field)
182 when "=", ">=", "<=", "><"
182 when "=", ">=", "<=", "><"
183 add_filter_error(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 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
184 when ">t-", "<t-", "t-"
184 when ">t-", "<t-", "t-"
185 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
185 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
186 end
186 end
187 end
187 end
188 end
188 end
189
189
190 add_filter_error(field, :blank) unless
190 add_filter_error(field, :blank) unless
191 # filter requires one or more values
191 # filter requires one or more values
192 (values_for(field) and !values_for(field).first.blank?) or
192 (values_for(field) and !values_for(field).first.blank?) or
193 # filter doesn't require any value
193 # filter doesn't require any value
194 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
194 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
195 end if filters
195 end if filters
196 end
196 end
197
197
198 def add_filter_error(field, message)
198 def add_filter_error(field, message)
199 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
199 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
200 errors.add(:base, m)
200 errors.add(:base, m)
201 end
201 end
202
202
203 # Returns true if the query is visible to +user+ or the current user.
203 # Returns true if the query is visible to +user+ or the current user.
204 def visible?(user=User.current)
204 def visible?(user=User.current)
205 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
205 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
206 end
206 end
207
207
208 def editable_by?(user)
208 def editable_by?(user)
209 return false unless user
209 return false unless user
210 # Admin can edit them all and regular users can edit their private queries
210 # Admin can edit them all and regular users can edit their private queries
211 return true if user.admin? || (!is_public && self.user_id == user.id)
211 return true if user.admin? || (!is_public && self.user_id == user.id)
212 # Members can not edit public queries that are for all project (only admin is allowed to)
212 # Members can not edit public queries that are for all project (only admin is allowed to)
213 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
213 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
214 end
214 end
215
215
216 def available_filters
216 def available_filters
217 return @available_filters if @available_filters
217 return @available_filters if @available_filters
218
218
219 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
219 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
220
220
221 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
221 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
222 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
222 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
223 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
223 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
224 "subject" => { :type => :text, :order => 8 },
224 "subject" => { :type => :text, :order => 8 },
225 "created_on" => { :type => :date_past, :order => 9 },
225 "created_on" => { :type => :date_past, :order => 9 },
226 "updated_on" => { :type => :date_past, :order => 10 },
226 "updated_on" => { :type => :date_past, :order => 10 },
227 "start_date" => { :type => :date, :order => 11 },
227 "start_date" => { :type => :date, :order => 11 },
228 "due_date" => { :type => :date, :order => 12 },
228 "due_date" => { :type => :date, :order => 12 },
229 "estimated_hours" => { :type => :float, :order => 13 },
229 "estimated_hours" => { :type => :float, :order => 13 },
230 "done_ratio" => { :type => :integer, :order => 14 }}
230 "done_ratio" => { :type => :integer, :order => 14 }}
231
231
232 principals = []
232 principals = []
233 if project
233 if project
234 principals += project.principals.sort
234 principals += project.principals.sort
235 else
235 else
236 all_projects = Project.visible.all
236 all_projects = Project.visible.all
237 if all_projects.any?
237 if all_projects.any?
238 # members of visible projects
238 # members of visible projects
239 principals += Principal.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort
239 principals += Principal.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort
240
240
241 # project filter
241 # project filter
242 project_values = []
242 project_values = []
243 if User.current.logged? && User.current.memberships.any?
243 if User.current.logged? && User.current.memberships.any?
244 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
244 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
245 end
245 end
246 Project.project_tree(all_projects) do |p, level|
246 Project.project_tree(all_projects) do |p, level|
247 prefix = (level > 0 ? ('--' * level + ' ') : '')
247 prefix = (level > 0 ? ('--' * level + ' ') : '')
248 project_values << ["#{prefix}#{p.name}", p.id.to_s]
248 project_values << ["#{prefix}#{p.name}", p.id.to_s]
249 end
249 end
250 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
250 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
251 end
251 end
252 end
252 end
253 users = principals.select {|p| p.is_a?(User)}
253 users = principals.select {|p| p.is_a?(User)}
254
254
255 assigned_to_values = []
255 assigned_to_values = []
256 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
256 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
257 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
257 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
258 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty?
258 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty?
259
259
260 author_values = []
260 author_values = []
261 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
261 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
262 author_values += users.collect{|s| [s.name, s.id.to_s] }
262 author_values += users.collect{|s| [s.name, s.id.to_s] }
263 @available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty?
263 @available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty?
264
264
265 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
265 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
266 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
266 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
267
267
268 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
268 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
269 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
269 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
270
270
271 if User.current.logged?
271 if User.current.logged?
272 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
272 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
273 end
273 end
274
274
275 if project
275 if project
276 # project specific filters
276 # project specific filters
277 categories = project.issue_categories.all
277 categories = project.issue_categories.all
278 unless categories.empty?
278 unless categories.empty?
279 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
279 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
280 end
280 end
281 versions = project.shared_versions.all
281 versions = project.shared_versions.all
282 unless versions.empty?
282 unless versions.empty?
283 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
283 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
284 end
284 end
285 unless project.leaf?
285 unless project.leaf?
286 subprojects = project.descendants.visible.all
286 subprojects = project.descendants.visible.all
287 unless subprojects.empty?
287 unless subprojects.empty?
288 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
288 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
289 end
289 end
290 end
290 end
291 add_custom_fields_filters(project.all_issue_custom_fields)
291 add_custom_fields_filters(project.all_issue_custom_fields)
292 else
292 else
293 # global filters for cross project issue list
293 # global filters for cross project issue list
294 system_shared_versions = Version.visible.find_all_by_sharing('system')
294 system_shared_versions = Version.visible.find_all_by_sharing('system')
295 unless system_shared_versions.empty?
295 unless system_shared_versions.empty?
296 @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] } }
296 @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] } }
297 end
297 end
298 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
298 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
299 end
299 end
300 @available_filters
300 @available_filters
301 end
301 end
302
302
303 def add_filter(field, operator, values)
303 def add_filter(field, operator, values)
304 # values must be an array
304 # values must be an array
305 return unless values.nil? || values.is_a?(Array)
305 return unless values.nil? || values.is_a?(Array)
306 # check if field is defined as an available filter
306 # check if field is defined as an available filter
307 if available_filters.has_key? field
307 if available_filters.has_key? field
308 filter_options = available_filters[field]
308 filter_options = available_filters[field]
309 # check if operator is allowed for that filter
309 # check if operator is allowed for that filter
310 #if @@operators_by_filter_type[filter_options[:type]].include? operator
310 #if @@operators_by_filter_type[filter_options[:type]].include? operator
311 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
311 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
312 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
312 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
313 #end
313 #end
314 filters[field] = {:operator => operator, :values => (values || [''])}
314 filters[field] = {:operator => operator, :values => (values || [''])}
315 end
315 end
316 end
316 end
317
317
318 def add_short_filter(field, expression)
318 def add_short_filter(field, expression)
319 return unless expression && available_filters.has_key?(field)
319 return unless expression && available_filters.has_key?(field)
320 field_type = available_filters[field][:type]
320 field_type = available_filters[field][:type]
321 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
321 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
322 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
322 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
323 add_filter field, operator, $1.present? ? $1.split('|') : ['']
323 add_filter field, operator, $1.present? ? $1.split('|') : ['']
324 end || add_filter(field, '=', expression.split('|'))
324 end || add_filter(field, '=', expression.split('|'))
325 end
325 end
326
326
327 # Add multiple filters using +add_filter+
327 # Add multiple filters using +add_filter+
328 def add_filters(fields, operators, values)
328 def add_filters(fields, operators, values)
329 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
329 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
330 fields.each do |field|
330 fields.each do |field|
331 add_filter(field, operators[field], values && values[field])
331 add_filter(field, operators[field], values && values[field])
332 end
332 end
333 end
333 end
334 end
334 end
335
335
336 def has_filter?(field)
336 def has_filter?(field)
337 filters and filters[field]
337 filters and filters[field]
338 end
338 end
339
339
340 def type_for(field)
340 def type_for(field)
341 available_filters[field][:type] if available_filters.has_key?(field)
341 available_filters[field][:type] if available_filters.has_key?(field)
342 end
342 end
343
343
344 def operator_for(field)
344 def operator_for(field)
345 has_filter?(field) ? filters[field][:operator] : nil
345 has_filter?(field) ? filters[field][:operator] : nil
346 end
346 end
347
347
348 def values_for(field)
348 def values_for(field)
349 has_filter?(field) ? filters[field][:values] : nil
349 has_filter?(field) ? filters[field][:values] : nil
350 end
350 end
351
351
352 def value_for(field, index=0)
352 def value_for(field, index=0)
353 (values_for(field) || [])[index]
353 (values_for(field) || [])[index]
354 end
354 end
355
355
356 def label_for(field)
356 def label_for(field)
357 label = available_filters[field][:name] if available_filters.has_key?(field)
357 label = available_filters[field][:name] if available_filters.has_key?(field)
358 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
358 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
359 end
359 end
360
360
361 def available_columns
361 def available_columns
362 return @available_columns if @available_columns
362 return @available_columns if @available_columns
363 @available_columns = ::Query.available_columns.dup
363 @available_columns = ::Query.available_columns.dup
364 @available_columns += (project ?
364 @available_columns += (project ?
365 project.all_issue_custom_fields :
365 project.all_issue_custom_fields :
366 IssueCustomField.find(:all)
366 IssueCustomField.find(:all)
367 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
367 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
368
368
369 if User.current.allowed_to?(:view_time_entries, project, :global => true)
369 if User.current.allowed_to?(:view_time_entries, project, :global => true)
370 index = nil
370 index = nil
371 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
371 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
372 index = (index ? index + 1 : -1)
372 index = (index ? index + 1 : -1)
373 # insert the column after estimated_hours or at the end
373 # insert the column after estimated_hours or at the end
374 @available_columns.insert index, QueryColumn.new(:spent_hours,
374 @available_columns.insert index, QueryColumn.new(:spent_hours,
375 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
375 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
376 :default_order => 'desc',
376 :default_order => 'desc',
377 :caption => :label_spent_time
377 :caption => :label_spent_time
378 )
378 )
379 end
379 end
380 @available_columns
380 @available_columns
381 end
381 end
382
382
383 def self.available_columns=(v)
383 def self.available_columns=(v)
384 self.available_columns = (v)
384 self.available_columns = (v)
385 end
385 end
386
386
387 def self.add_available_column(column)
387 def self.add_available_column(column)
388 self.available_columns << (column) if column.is_a?(QueryColumn)
388 self.available_columns << (column) if column.is_a?(QueryColumn)
389 end
389 end
390
390
391 # Returns an array of columns that can be used to group the results
391 # Returns an array of columns that can be used to group the results
392 def groupable_columns
392 def groupable_columns
393 available_columns.select {|c| c.groupable}
393 available_columns.select {|c| c.groupable}
394 end
394 end
395
395
396 # Returns a Hash of columns and the key for sorting
396 # Returns a Hash of columns and the key for sorting
397 def sortable_columns
397 def sortable_columns
398 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
398 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
399 h[column.name.to_s] = column.sortable
399 h[column.name.to_s] = column.sortable
400 h
400 h
401 })
401 })
402 end
402 end
403
403
404 def columns
404 def columns
405 # preserve the column_names order
405 # preserve the column_names order
406 (has_default_columns? ? default_columns_names : column_names).collect do |name|
406 (has_default_columns? ? default_columns_names : column_names).collect do |name|
407 available_columns.find { |col| col.name == name }
407 available_columns.find { |col| col.name == name }
408 end.compact
408 end.compact
409 end
409 end
410
410
411 def default_columns_names
411 def default_columns_names
412 @default_columns_names ||= begin
412 @default_columns_names ||= begin
413 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
413 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
414
414
415 project.present? ? default_columns : [:project] | default_columns
415 project.present? ? default_columns : [:project] | default_columns
416 end
416 end
417 end
417 end
418
418
419 def column_names=(names)
419 def column_names=(names)
420 if names
420 if names
421 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
421 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
422 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
422 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
423 # Set column_names to nil if default columns
423 # Set column_names to nil if default columns
424 if names == default_columns_names
424 if names == default_columns_names
425 names = nil
425 names = nil
426 end
426 end
427 end
427 end
428 write_attribute(:column_names, names)
428 write_attribute(:column_names, names)
429 end
429 end
430
430
431 def has_column?(column)
431 def has_column?(column)
432 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
432 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
433 end
433 end
434
434
435 def has_default_columns?
435 def has_default_columns?
436 column_names.nil? || column_names.empty?
436 column_names.nil? || column_names.empty?
437 end
437 end
438
438
439 def sort_criteria=(arg)
439 def sort_criteria=(arg)
440 c = []
440 c = []
441 if arg.is_a?(Hash)
441 if arg.is_a?(Hash)
442 arg = arg.keys.sort.collect {|k| arg[k]}
442 arg = arg.keys.sort.collect {|k| arg[k]}
443 end
443 end
444 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
444 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
445 write_attribute(:sort_criteria, c)
445 write_attribute(:sort_criteria, c)
446 end
446 end
447
447
448 def sort_criteria
448 def sort_criteria
449 read_attribute(:sort_criteria) || []
449 read_attribute(:sort_criteria) || []
450 end
450 end
451
451
452 def sort_criteria_key(arg)
452 def sort_criteria_key(arg)
453 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
453 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
454 end
454 end
455
455
456 def sort_criteria_order(arg)
456 def sort_criteria_order(arg)
457 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
457 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
458 end
458 end
459
459
460 # Returns the SQL sort order that should be prepended for grouping
460 # Returns the SQL sort order that should be prepended for grouping
461 def group_by_sort_order
461 def group_by_sort_order
462 if grouped? && (column = group_by_column)
462 if grouped? && (column = group_by_column)
463 column.sortable.is_a?(Array) ?
463 column.sortable.is_a?(Array) ?
464 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
464 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
465 "#{column.sortable} #{column.default_order}"
465 "#{column.sortable} #{column.default_order}"
466 end
466 end
467 end
467 end
468
468
469 # Returns true if the query is a grouped query
469 # Returns true if the query is a grouped query
470 def grouped?
470 def grouped?
471 !group_by_column.nil?
471 !group_by_column.nil?
472 end
472 end
473
473
474 def group_by_column
474 def group_by_column
475 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
475 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
476 end
476 end
477
477
478 def group_by_statement
478 def group_by_statement
479 group_by_column.try(:groupable)
479 group_by_column.try(:groupable)
480 end
480 end
481
481
482 def project_statement
482 def project_statement
483 project_clauses = []
483 project_clauses = []
484 if project && !project.descendants.active.empty?
484 if project && !project.descendants.active.empty?
485 ids = [project.id]
485 ids = [project.id]
486 if has_filter?("subproject_id")
486 if has_filter?("subproject_id")
487 case operator_for("subproject_id")
487 case operator_for("subproject_id")
488 when '='
488 when '='
489 # include the selected subprojects
489 # include the selected subprojects
490 ids += values_for("subproject_id").each(&:to_i)
490 ids += values_for("subproject_id").each(&:to_i)
491 when '!*'
491 when '!*'
492 # main project only
492 # main project only
493 else
493 else
494 # all subprojects
494 # all subprojects
495 ids += project.descendants.collect(&:id)
495 ids += project.descendants.collect(&:id)
496 end
496 end
497 elsif Setting.display_subprojects_issues?
497 elsif Setting.display_subprojects_issues?
498 ids += project.descendants.collect(&:id)
498 ids += project.descendants.collect(&:id)
499 end
499 end
500 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
500 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
501 elsif project
501 elsif project
502 project_clauses << "#{Project.table_name}.id = %d" % project.id
502 project_clauses << "#{Project.table_name}.id = %d" % project.id
503 end
503 end
504 project_clauses.any? ? project_clauses.join(' AND ') : nil
504 project_clauses.any? ? project_clauses.join(' AND ') : nil
505 end
505 end
506
506
507 def statement
507 def statement
508 # filters clauses
508 # filters clauses
509 filters_clauses = []
509 filters_clauses = []
510 filters.each_key do |field|
510 filters.each_key do |field|
511 next if field == "subproject_id"
511 next if field == "subproject_id"
512 v = values_for(field).clone
512 v = values_for(field).clone
513 next unless v and !v.empty?
513 next unless v and !v.empty?
514 operator = operator_for(field)
514 operator = operator_for(field)
515
515
516 # "me" value subsitution
516 # "me" value subsitution
517 if %w(assigned_to_id author_id watcher_id).include?(field)
517 if %w(assigned_to_id author_id watcher_id).include?(field)
518 if v.delete("me")
518 if v.delete("me")
519 if User.current.logged?
519 if User.current.logged?
520 v.push(User.current.id.to_s)
520 v.push(User.current.id.to_s)
521 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
521 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
522 else
522 else
523 v.push("0")
523 v.push("0")
524 end
524 end
525 end
525 end
526 end
526 end
527
527
528 if field == 'project_id'
528 if field == 'project_id'
529 if v.delete('mine')
529 if v.delete('mine')
530 v += User.current.memberships.map(&:project_id).map(&:to_s)
530 v += User.current.memberships.map(&:project_id).map(&:to_s)
531 end
531 end
532 end
532 end
533
533
534 if field =~ /^cf_(\d+)$/
534 if field =~ /^cf_(\d+)$/
535 # custom field
535 # custom field
536 filters_clauses << sql_for_custom_field(field, operator, v, $1)
536 filters_clauses << sql_for_custom_field(field, operator, v, $1)
537 elsif respond_to?("sql_for_#{field}_field")
537 elsif respond_to?("sql_for_#{field}_field")
538 # specific statement
538 # specific statement
539 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
539 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
540 else
540 else
541 # regular field
541 # regular field
542 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
542 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
543 end
543 end
544 end if filters and valid?
544 end if filters and valid?
545
545
546 filters_clauses << project_statement
546 filters_clauses << project_statement
547 filters_clauses.reject!(&:blank?)
547 filters_clauses.reject!(&:blank?)
548
548
549 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
549 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
550 end
550 end
551
551
552 # Returns the issue count
552 # Returns the issue count
553 def issue_count
553 def issue_count
554 Issue.visible.count(:include => [:status, :project], :conditions => statement)
554 Issue.visible.count(:include => [:status, :project], :conditions => statement)
555 rescue ::ActiveRecord::StatementInvalid => e
555 rescue ::ActiveRecord::StatementInvalid => e
556 raise StatementInvalid.new(e.message)
556 raise StatementInvalid.new(e.message)
557 end
557 end
558
558
559 # Returns the issue count by group or nil if query is not grouped
559 # Returns the issue count by group or nil if query is not grouped
560 def issue_count_by_group
560 def issue_count_by_group
561 r = nil
561 r = nil
562 if grouped?
562 if grouped?
563 begin
563 begin
564 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
564 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
565 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
565 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
566 rescue ActiveRecord::RecordNotFound
566 rescue ActiveRecord::RecordNotFound
567 r = {nil => issue_count}
567 r = {nil => issue_count}
568 end
568 end
569 c = group_by_column
569 c = group_by_column
570 if c.is_a?(QueryCustomFieldColumn)
570 if c.is_a?(QueryCustomFieldColumn)
571 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
571 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
572 end
572 end
573 end
573 end
574 r
574 r
575 rescue ::ActiveRecord::StatementInvalid => e
575 rescue ::ActiveRecord::StatementInvalid => e
576 raise StatementInvalid.new(e.message)
576 raise StatementInvalid.new(e.message)
577 end
577 end
578
578
579 # Returns the issues
579 # Returns the issues
580 # Valid options are :order, :offset, :limit, :include, :conditions
580 # Valid options are :order, :offset, :limit, :include, :conditions
581 def issues(options={})
581 def issues(options={})
582 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
582 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
583 order_option = nil if order_option.blank?
583 order_option = nil if order_option.blank?
584
584
585 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
585 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
586
586
587 issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
587 issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
588 :conditions => statement,
588 :conditions => statement,
589 :order => order_option,
589 :order => order_option,
590 :joins => joins,
590 :joins => joins,
591 :limit => options[:limit],
591 :limit => options[:limit],
592 :offset => options[:offset]
592 :offset => options[:offset]
593
593
594 if has_column?(:spent_hours)
594 if has_column?(:spent_hours)
595 Issue.load_visible_spent_hours(issues)
595 Issue.load_visible_spent_hours(issues)
596 end
596 end
597 issues
597 issues
598 rescue ::ActiveRecord::StatementInvalid => e
598 rescue ::ActiveRecord::StatementInvalid => e
599 raise StatementInvalid.new(e.message)
599 raise StatementInvalid.new(e.message)
600 end
600 end
601
601
602 # Returns the issues ids
602 # Returns the issues ids
603 def issue_ids(options={})
603 def issue_ids(options={})
604 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
604 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
605 order_option = nil if order_option.blank?
605 order_option = nil if order_option.blank?
606
606
607 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
607 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
608
608
609 Issue.visible.scoped(:conditions => options[:conditions]).find_ids :include => ([:status, :project] + (options[:include] || [])).uniq,
609 Issue.visible.scoped(:conditions => options[:conditions]).find_ids :include => ([:status, :project] + (options[:include] || [])).uniq,
610 :conditions => statement,
610 :conditions => statement,
611 :order => order_option,
611 :order => order_option,
612 :joins => joins,
612 :joins => joins,
613 :limit => options[:limit],
613 :limit => options[:limit],
614 :offset => options[:offset]
614 :offset => options[:offset]
615 rescue ::ActiveRecord::StatementInvalid => e
615 rescue ::ActiveRecord::StatementInvalid => e
616 raise StatementInvalid.new(e.message)
616 raise StatementInvalid.new(e.message)
617 end
617 end
618
618
619 # Returns the journals
619 # Returns the journals
620 # Valid options are :order, :offset, :limit
620 # Valid options are :order, :offset, :limit
621 def journals(options={})
621 def journals(options={})
622 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
622 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
623 :conditions => statement,
623 :conditions => statement,
624 :order => options[:order],
624 :order => options[:order],
625 :limit => options[:limit],
625 :limit => options[:limit],
626 :offset => options[:offset]
626 :offset => options[:offset]
627 rescue ::ActiveRecord::StatementInvalid => e
627 rescue ::ActiveRecord::StatementInvalid => e
628 raise StatementInvalid.new(e.message)
628 raise StatementInvalid.new(e.message)
629 end
629 end
630
630
631 # Returns the versions
631 # Returns the versions
632 # Valid options are :conditions
632 # Valid options are :conditions
633 def versions(options={})
633 def versions(options={})
634 Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
634 Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
635 rescue ::ActiveRecord::StatementInvalid => e
635 rescue ::ActiveRecord::StatementInvalid => e
636 raise StatementInvalid.new(e.message)
636 raise StatementInvalid.new(e.message)
637 end
637 end
638
638
639 def sql_for_watcher_id_field(field, operator, value)
639 def sql_for_watcher_id_field(field, operator, value)
640 db_table = Watcher.table_name
640 db_table = Watcher.table_name
641 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
641 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
642 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
642 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
643 end
643 end
644
644
645 def sql_for_member_of_group_field(field, operator, value)
645 def sql_for_member_of_group_field(field, operator, value)
646 if operator == '*' # Any group
646 if operator == '*' # Any group
647 groups = Group.all
647 groups = Group.all
648 operator = '=' # Override the operator since we want to find by assigned_to
648 operator = '=' # Override the operator since we want to find by assigned_to
649 elsif operator == "!*"
649 elsif operator == "!*"
650 groups = Group.all
650 groups = Group.all
651 operator = '!' # Override the operator since we want to find by assigned_to
651 operator = '!' # Override the operator since we want to find by assigned_to
652 else
652 else
653 groups = Group.find_all_by_id(value)
653 groups = Group.find_all_by_id(value)
654 end
654 end
655 groups ||= []
655 groups ||= []
656
656
657 members_of_groups = groups.inject([]) {|user_ids, group|
657 members_of_groups = groups.inject([]) {|user_ids, group|
658 if group && group.user_ids.present?
658 if group && group.user_ids.present?
659 user_ids << group.user_ids
659 user_ids << group.user_ids
660 end
660 end
661 user_ids.flatten.uniq.compact
661 user_ids.flatten.uniq.compact
662 }.sort.collect(&:to_s)
662 }.sort.collect(&:to_s)
663
663
664 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
664 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
665 end
665 end
666
666
667 def sql_for_assigned_to_role_field(field, operator, value)
667 def sql_for_assigned_to_role_field(field, operator, value)
668 case operator
668 case operator
669 when "*", "!*" # Member / Not member
669 when "*", "!*" # Member / Not member
670 sw = operator == "!*" ? 'NOT' : ''
670 sw = operator == "!*" ? 'NOT' : ''
671 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
671 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
672 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
672 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
673 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
673 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
674 when "=", "!"
674 when "=", "!"
675 role_cond = value.any? ?
675 role_cond = value.any? ?
676 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
676 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
677 "1=0"
677 "1=0"
678
678
679 sw = operator == "!" ? 'NOT' : ''
679 sw = operator == "!" ? 'NOT' : ''
680 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
680 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
681 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
681 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
682 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
682 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
683 end
683 end
684 end
684 end
685
685
686 private
686 private
687
687
688 def sql_for_custom_field(field, operator, value, custom_field_id)
688 def sql_for_custom_field(field, operator, value, custom_field_id)
689 db_table = CustomValue.table_name
689 db_table = CustomValue.table_name
690 db_field = 'value'
690 db_field = 'value'
691 filter = @available_filters[field]
692 if filter && filter[:format] == 'user'
693 if value.delete('me')
694 value.push User.current.id.to_s
695 end
696 end
691 "#{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=#{custom_field_id} WHERE " +
697 "#{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=#{custom_field_id} WHERE " +
692 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
698 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
693 end
699 end
694
700
695 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
701 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
696 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
702 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
697 sql = ''
703 sql = ''
698 case operator
704 case operator
699 when "="
705 when "="
700 if value.any?
706 if value.any?
701 case type_for(field)
707 case type_for(field)
702 when :date, :date_past
708 when :date, :date_past
703 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
709 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
704 when :integer
710 when :integer
705 if is_custom_filter
711 if is_custom_filter
706 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
712 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
707 else
713 else
708 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
714 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
709 end
715 end
710 when :float
716 when :float
711 if is_custom_filter
717 if is_custom_filter
712 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
718 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
713 else
719 else
714 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
720 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
715 end
721 end
716 else
722 else
717 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
723 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
718 end
724 end
719 else
725 else
720 # IN an empty set
726 # IN an empty set
721 sql = "1=0"
727 sql = "1=0"
722 end
728 end
723 when "!"
729 when "!"
724 if value.any?
730 if value.any?
725 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
731 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
726 else
732 else
727 # NOT IN an empty set
733 # NOT IN an empty set
728 sql = "1=1"
734 sql = "1=1"
729 end
735 end
730 when "!*"
736 when "!*"
731 sql = "#{db_table}.#{db_field} IS NULL"
737 sql = "#{db_table}.#{db_field} IS NULL"
732 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
738 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
733 when "*"
739 when "*"
734 sql = "#{db_table}.#{db_field} IS NOT NULL"
740 sql = "#{db_table}.#{db_field} IS NOT NULL"
735 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
741 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
736 when ">="
742 when ">="
737 if [:date, :date_past].include?(type_for(field))
743 if [:date, :date_past].include?(type_for(field))
738 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
744 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
739 else
745 else
740 if is_custom_filter
746 if is_custom_filter
741 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
747 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
742 else
748 else
743 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
749 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
744 end
750 end
745 end
751 end
746 when "<="
752 when "<="
747 if [:date, :date_past].include?(type_for(field))
753 if [:date, :date_past].include?(type_for(field))
748 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
754 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
749 else
755 else
750 if is_custom_filter
756 if is_custom_filter
751 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
757 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
752 else
758 else
753 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
759 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
754 end
760 end
755 end
761 end
756 when "><"
762 when "><"
757 if [:date, :date_past].include?(type_for(field))
763 if [:date, :date_past].include?(type_for(field))
758 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
764 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
759 else
765 else
760 if is_custom_filter
766 if is_custom_filter
761 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
767 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
762 else
768 else
763 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
769 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
764 end
770 end
765 end
771 end
766 when "o"
772 when "o"
767 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
773 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
768 when "c"
774 when "c"
769 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
775 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
770 when ">t-"
776 when ">t-"
771 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
777 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
772 when "<t-"
778 when "<t-"
773 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
779 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
774 when "t-"
780 when "t-"
775 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
781 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
776 when ">t+"
782 when ">t+"
777 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
783 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
778 when "<t+"
784 when "<t+"
779 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
785 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
780 when "t+"
786 when "t+"
781 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
787 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
782 when "t"
788 when "t"
783 sql = relative_date_clause(db_table, db_field, 0, 0)
789 sql = relative_date_clause(db_table, db_field, 0, 0)
784 when "w"
790 when "w"
785 first_day_of_week = l(:general_first_day_of_week).to_i
791 first_day_of_week = l(:general_first_day_of_week).to_i
786 day_of_week = Date.today.cwday
792 day_of_week = Date.today.cwday
787 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
793 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
788 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
794 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
789 when "~"
795 when "~"
790 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
796 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
791 when "!~"
797 when "!~"
792 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
798 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
793 else
799 else
794 raise "Unknown query operator #{operator}"
800 raise "Unknown query operator #{operator}"
795 end
801 end
796
802
797 return sql
803 return sql
798 end
804 end
799
805
800 def add_custom_fields_filters(custom_fields)
806 def add_custom_fields_filters(custom_fields)
801 @available_filters ||= {}
807 @available_filters ||= {}
802
808
803 custom_fields.select(&:is_filter?).each do |field|
809 custom_fields.select(&:is_filter?).each do |field|
804 case field.field_format
810 case field.field_format
805 when "text"
811 when "text"
806 options = { :type => :text, :order => 20 }
812 options = { :type => :text, :order => 20 }
807 when "list"
813 when "list"
808 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
814 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
809 when "date"
815 when "date"
810 options = { :type => :date, :order => 20 }
816 options = { :type => :date, :order => 20 }
811 when "bool"
817 when "bool"
812 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
818 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
813 when "int"
819 when "int"
814 options = { :type => :integer, :order => 20 }
820 options = { :type => :integer, :order => 20 }
815 when "float"
821 when "float"
816 options = { :type => :float, :order => 20 }
822 options = { :type => :float, :order => 20 }
817 when "user", "version"
823 when "user", "version"
818 next unless project
824 next unless project
819 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20}
825 values = field.possible_values_options(project)
826 if User.current.logged? && field.field_format == 'user'
827 values.unshift ["<< #{l(:label_me)} >>", "me"]
828 end
829 options = { :type => :list_optional, :values => values, :order => 20}
820 else
830 else
821 options = { :type => :string, :order => 20 }
831 options = { :type => :string, :order => 20 }
822 end
832 end
823 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
833 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name, :format => field.field_format })
824 end
834 end
825 end
835 end
826
836
827 # Returns a SQL clause for a date or datetime field.
837 # Returns a SQL clause for a date or datetime field.
828 def date_clause(table, field, from, to)
838 def date_clause(table, field, from, to)
829 s = []
839 s = []
830 if from
840 if from
831 from_yesterday = from - 1
841 from_yesterday = from - 1
832 from_yesterday_utc = Time.gm(from_yesterday.year, from_yesterday.month, from_yesterday.day)
842 from_yesterday_utc = Time.gm(from_yesterday.year, from_yesterday.month, from_yesterday.day)
833 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_utc.end_of_day)])
843 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_utc.end_of_day)])
834 end
844 end
835 if to
845 if to
836 to_utc = Time.gm(to.year, to.month, to.day)
846 to_utc = Time.gm(to.year, to.month, to.day)
837 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_utc.end_of_day)])
847 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_utc.end_of_day)])
838 end
848 end
839 s.join(' AND ')
849 s.join(' AND ')
840 end
850 end
841
851
842 # Returns a SQL clause for a date or datetime field using relative dates.
852 # Returns a SQL clause for a date or datetime field using relative dates.
843 def relative_date_clause(table, field, days_from, days_to)
853 def relative_date_clause(table, field, days_from, days_to)
844 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
854 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
845 end
855 end
846 end
856 end
@@ -1,882 +1,899
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,
21 fixtures :projects, :enabled_modules, :users, :members,
22 :member_roles, :roles, :trackers, :issue_statuses,
22 :member_roles, :roles, :trackers, :issue_statuses,
23 :issue_categories, :enumerations, :issues,
23 :issue_categories, :enumerations, :issues,
24 :watchers, :custom_fields, :custom_values, :versions,
24 :watchers, :custom_fields, :custom_values, :versions,
25 :queries,
25 :queries,
26 :projects_trackers
26 :projects_trackers
27
27
28 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
28 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
29 query = Query.new(:project => nil, :name => '_')
29 query = Query.new(:project => nil, :name => '_')
30 assert query.available_filters.has_key?('cf_1')
30 assert query.available_filters.has_key?('cf_1')
31 assert !query.available_filters.has_key?('cf_3')
31 assert !query.available_filters.has_key?('cf_3')
32 end
32 end
33
33
34 def test_system_shared_versions_should_be_available_in_global_queries
34 def test_system_shared_versions_should_be_available_in_global_queries
35 Version.find(2).update_attribute :sharing, 'system'
35 Version.find(2).update_attribute :sharing, 'system'
36 query = Query.new(:project => nil, :name => '_')
36 query = Query.new(:project => nil, :name => '_')
37 assert query.available_filters.has_key?('fixed_version_id')
37 assert query.available_filters.has_key?('fixed_version_id')
38 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
38 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
39 end
39 end
40
40
41 def test_project_filter_in_global_queries
41 def test_project_filter_in_global_queries
42 query = Query.new(:project => nil, :name => '_')
42 query = Query.new(:project => nil, :name => '_')
43 project_filter = query.available_filters["project_id"]
43 project_filter = query.available_filters["project_id"]
44 assert_not_nil project_filter
44 assert_not_nil project_filter
45 project_ids = project_filter[:values].map{|p| p[1]}
45 project_ids = project_filter[:values].map{|p| p[1]}
46 assert project_ids.include?("1") #public project
46 assert project_ids.include?("1") #public project
47 assert !project_ids.include?("2") #private project user cannot see
47 assert !project_ids.include?("2") #private project user cannot see
48 end
48 end
49
49
50 def find_issues_with_query(query)
50 def find_issues_with_query(query)
51 Issue.find :all,
51 Issue.find :all,
52 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
52 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
53 :conditions => query.statement
53 :conditions => query.statement
54 end
54 end
55
55
56 def assert_find_issues_with_query_is_successful(query)
56 def assert_find_issues_with_query_is_successful(query)
57 assert_nothing_raised do
57 assert_nothing_raised do
58 find_issues_with_query(query)
58 find_issues_with_query(query)
59 end
59 end
60 end
60 end
61
61
62 def assert_query_statement_includes(query, condition)
62 def assert_query_statement_includes(query, condition)
63 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
63 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
64 end
64 end
65
65
66 def assert_query_result(expected, query)
66 def assert_query_result(expected, query)
67 assert_nothing_raised do
67 assert_nothing_raised do
68 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
68 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
69 assert_equal expected.size, query.issue_count
69 assert_equal expected.size, query.issue_count
70 end
70 end
71 end
71 end
72
72
73 def test_query_should_allow_shared_versions_for_a_project_query
73 def test_query_should_allow_shared_versions_for_a_project_query
74 subproject_version = Version.find(4)
74 subproject_version = Version.find(4)
75 query = Query.new(:project => Project.find(1), :name => '_')
75 query = Query.new(:project => Project.find(1), :name => '_')
76 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
76 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
77
77
78 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
78 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
79 end
79 end
80
80
81 def test_query_with_multiple_custom_fields
81 def test_query_with_multiple_custom_fields
82 query = Query.find(1)
82 query = Query.find(1)
83 assert query.valid?
83 assert query.valid?
84 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
84 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
85 issues = find_issues_with_query(query)
85 issues = find_issues_with_query(query)
86 assert_equal 1, issues.length
86 assert_equal 1, issues.length
87 assert_equal Issue.find(3), issues.first
87 assert_equal Issue.find(3), issues.first
88 end
88 end
89
89
90 def test_operator_none
90 def test_operator_none
91 query = Query.new(:project => Project.find(1), :name => '_')
91 query = Query.new(:project => Project.find(1), :name => '_')
92 query.add_filter('fixed_version_id', '!*', [''])
92 query.add_filter('fixed_version_id', '!*', [''])
93 query.add_filter('cf_1', '!*', [''])
93 query.add_filter('cf_1', '!*', [''])
94 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
94 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
95 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
95 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
96 find_issues_with_query(query)
96 find_issues_with_query(query)
97 end
97 end
98
98
99 def test_operator_none_for_integer
99 def test_operator_none_for_integer
100 query = Query.new(:project => Project.find(1), :name => '_')
100 query = Query.new(:project => Project.find(1), :name => '_')
101 query.add_filter('estimated_hours', '!*', [''])
101 query.add_filter('estimated_hours', '!*', [''])
102 issues = find_issues_with_query(query)
102 issues = find_issues_with_query(query)
103 assert !issues.empty?
103 assert !issues.empty?
104 assert issues.all? {|i| !i.estimated_hours}
104 assert issues.all? {|i| !i.estimated_hours}
105 end
105 end
106
106
107 def test_operator_none_for_date
107 def test_operator_none_for_date
108 query = Query.new(:project => Project.find(1), :name => '_')
108 query = Query.new(:project => Project.find(1), :name => '_')
109 query.add_filter('start_date', '!*', [''])
109 query.add_filter('start_date', '!*', [''])
110 issues = find_issues_with_query(query)
110 issues = find_issues_with_query(query)
111 assert !issues.empty?
111 assert !issues.empty?
112 assert issues.all? {|i| i.start_date.nil?}
112 assert issues.all? {|i| i.start_date.nil?}
113 end
113 end
114
114
115 def test_operator_all
115 def test_operator_all
116 query = Query.new(:project => Project.find(1), :name => '_')
116 query = Query.new(:project => Project.find(1), :name => '_')
117 query.add_filter('fixed_version_id', '*', [''])
117 query.add_filter('fixed_version_id', '*', [''])
118 query.add_filter('cf_1', '*', [''])
118 query.add_filter('cf_1', '*', [''])
119 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
119 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
120 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
120 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
121 find_issues_with_query(query)
121 find_issues_with_query(query)
122 end
122 end
123
123
124 def test_operator_all_for_date
124 def test_operator_all_for_date
125 query = Query.new(:project => Project.find(1), :name => '_')
125 query = Query.new(:project => Project.find(1), :name => '_')
126 query.add_filter('start_date', '*', [''])
126 query.add_filter('start_date', '*', [''])
127 issues = find_issues_with_query(query)
127 issues = find_issues_with_query(query)
128 assert !issues.empty?
128 assert !issues.empty?
129 assert issues.all? {|i| i.start_date.present?}
129 assert issues.all? {|i| i.start_date.present?}
130 end
130 end
131
131
132 def test_numeric_filter_should_not_accept_non_numeric_values
132 def test_numeric_filter_should_not_accept_non_numeric_values
133 query = Query.new(:name => '_')
133 query = Query.new(:name => '_')
134 query.add_filter('estimated_hours', '=', ['a'])
134 query.add_filter('estimated_hours', '=', ['a'])
135
135
136 assert query.has_filter?('estimated_hours')
136 assert query.has_filter?('estimated_hours')
137 assert !query.valid?
137 assert !query.valid?
138 end
138 end
139
139
140 def test_operator_is_on_float
140 def test_operator_is_on_float
141 Issue.update_all("estimated_hours = 171.2", "id=2")
141 Issue.update_all("estimated_hours = 171.2", "id=2")
142
142
143 query = Query.new(:name => '_')
143 query = Query.new(:name => '_')
144 query.add_filter('estimated_hours', '=', ['171.20'])
144 query.add_filter('estimated_hours', '=', ['171.20'])
145 issues = find_issues_with_query(query)
145 issues = find_issues_with_query(query)
146 assert_equal 1, issues.size
146 assert_equal 1, issues.size
147 assert_equal 2, issues.first.id
147 assert_equal 2, issues.first.id
148 end
148 end
149
149
150 def test_operator_is_on_integer_custom_field
150 def test_operator_is_on_integer_custom_field
151 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
151 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_for_all => true, :is_filter => true)
152 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
152 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
153 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
153 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
154 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
154 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
155
155
156 query = Query.new(:name => '_')
156 query = Query.new(:name => '_')
157 query.add_filter("cf_#{f.id}", '=', ['12'])
157 query.add_filter("cf_#{f.id}", '=', ['12'])
158 issues = find_issues_with_query(query)
158 issues = find_issues_with_query(query)
159 assert_equal 1, issues.size
159 assert_equal 1, issues.size
160 assert_equal 2, issues.first.id
160 assert_equal 2, issues.first.id
161 end
161 end
162
162
163 def test_operator_is_on_float_custom_field
163 def test_operator_is_on_float_custom_field
164 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
164 f = IssueCustomField.create!(:name => 'filter', :field_format => 'float', :is_filter => true, :is_for_all => true)
165 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
165 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7.3')
166 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
166 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12.7')
167 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
167 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
168
168
169 query = Query.new(:name => '_')
169 query = Query.new(:name => '_')
170 query.add_filter("cf_#{f.id}", '=', ['12.7'])
170 query.add_filter("cf_#{f.id}", '=', ['12.7'])
171 issues = find_issues_with_query(query)
171 issues = find_issues_with_query(query)
172 assert_equal 1, issues.size
172 assert_equal 1, issues.size
173 assert_equal 2, issues.first.id
173 assert_equal 2, issues.first.id
174 end
174 end
175
175
176 def test_operator_greater_than
176 def test_operator_greater_than
177 query = Query.new(:project => Project.find(1), :name => '_')
177 query = Query.new(:project => Project.find(1), :name => '_')
178 query.add_filter('done_ratio', '>=', ['40'])
178 query.add_filter('done_ratio', '>=', ['40'])
179 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
179 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
180 find_issues_with_query(query)
180 find_issues_with_query(query)
181 end
181 end
182
182
183 def test_operator_greater_than_a_float
183 def test_operator_greater_than_a_float
184 query = Query.new(:project => Project.find(1), :name => '_')
184 query = Query.new(:project => Project.find(1), :name => '_')
185 query.add_filter('estimated_hours', '>=', ['40.5'])
185 query.add_filter('estimated_hours', '>=', ['40.5'])
186 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
186 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
187 find_issues_with_query(query)
187 find_issues_with_query(query)
188 end
188 end
189
189
190 def test_operator_greater_than_on_int_custom_field
190 def test_operator_greater_than_on_int_custom_field
191 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
191 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
192 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
192 CustomValue.create!(:custom_field => f, :customized => Issue.find(1), :value => '7')
193 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
193 CustomValue.create!(:custom_field => f, :customized => Issue.find(2), :value => '12')
194 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
194 CustomValue.create!(:custom_field => f, :customized => Issue.find(3), :value => '')
195
195
196 query = Query.new(:project => Project.find(1), :name => '_')
196 query = Query.new(:project => Project.find(1), :name => '_')
197 query.add_filter("cf_#{f.id}", '>=', ['8'])
197 query.add_filter("cf_#{f.id}", '>=', ['8'])
198 issues = find_issues_with_query(query)
198 issues = find_issues_with_query(query)
199 assert_equal 1, issues.size
199 assert_equal 1, issues.size
200 assert_equal 2, issues.first.id
200 assert_equal 2, issues.first.id
201 end
201 end
202
202
203 def test_operator_lesser_than
203 def test_operator_lesser_than
204 query = Query.new(:project => Project.find(1), :name => '_')
204 query = Query.new(:project => Project.find(1), :name => '_')
205 query.add_filter('done_ratio', '<=', ['30'])
205 query.add_filter('done_ratio', '<=', ['30'])
206 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
206 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
207 find_issues_with_query(query)
207 find_issues_with_query(query)
208 end
208 end
209
209
210 def test_operator_lesser_than_on_custom_field
210 def test_operator_lesser_than_on_custom_field
211 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
211 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
212 query = Query.new(:project => Project.find(1), :name => '_')
212 query = Query.new(:project => Project.find(1), :name => '_')
213 query.add_filter("cf_#{f.id}", '<=', ['30'])
213 query.add_filter("cf_#{f.id}", '<=', ['30'])
214 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) <= 30.0")
214 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) <= 30.0")
215 find_issues_with_query(query)
215 find_issues_with_query(query)
216 end
216 end
217
217
218 def test_operator_between
218 def test_operator_between
219 query = Query.new(:project => Project.find(1), :name => '_')
219 query = Query.new(:project => Project.find(1), :name => '_')
220 query.add_filter('done_ratio', '><', ['30', '40'])
220 query.add_filter('done_ratio', '><', ['30', '40'])
221 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
221 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
222 find_issues_with_query(query)
222 find_issues_with_query(query)
223 end
223 end
224
224
225 def test_operator_between_on_custom_field
225 def test_operator_between_on_custom_field
226 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
226 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
227 query = Query.new(:project => Project.find(1), :name => '_')
227 query = Query.new(:project => Project.find(1), :name => '_')
228 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
228 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
229 assert_include "CAST(custom_values.value AS decimal(60,3)) BETWEEN 30.0 AND 40.0", query.statement
229 assert_include "CAST(custom_values.value AS decimal(60,3)) BETWEEN 30.0 AND 40.0", query.statement
230 find_issues_with_query(query)
230 find_issues_with_query(query)
231 end
231 end
232
232
233 def test_date_filter_should_not_accept_non_date_values
233 def test_date_filter_should_not_accept_non_date_values
234 query = Query.new(:name => '_')
234 query = Query.new(:name => '_')
235 query.add_filter('created_on', '=', ['a'])
235 query.add_filter('created_on', '=', ['a'])
236
236
237 assert query.has_filter?('created_on')
237 assert query.has_filter?('created_on')
238 assert !query.valid?
238 assert !query.valid?
239 end
239 end
240
240
241 def test_date_filter_should_not_accept_invalid_date_values
241 def test_date_filter_should_not_accept_invalid_date_values
242 query = Query.new(:name => '_')
242 query = Query.new(:name => '_')
243 query.add_filter('created_on', '=', ['2011-01-34'])
243 query.add_filter('created_on', '=', ['2011-01-34'])
244
244
245 assert query.has_filter?('created_on')
245 assert query.has_filter?('created_on')
246 assert !query.valid?
246 assert !query.valid?
247 end
247 end
248
248
249 def test_relative_date_filter_should_not_accept_non_integer_values
249 def test_relative_date_filter_should_not_accept_non_integer_values
250 query = Query.new(:name => '_')
250 query = Query.new(:name => '_')
251 query.add_filter('created_on', '>t-', ['a'])
251 query.add_filter('created_on', '>t-', ['a'])
252
252
253 assert query.has_filter?('created_on')
253 assert query.has_filter?('created_on')
254 assert !query.valid?
254 assert !query.valid?
255 end
255 end
256
256
257 def test_operator_date_equals
257 def test_operator_date_equals
258 query = Query.new(:name => '_')
258 query = Query.new(:name => '_')
259 query.add_filter('due_date', '=', ['2011-07-10'])
259 query.add_filter('due_date', '=', ['2011-07-10'])
260 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
260 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
261 find_issues_with_query(query)
261 find_issues_with_query(query)
262 end
262 end
263
263
264 def test_operator_date_lesser_than
264 def test_operator_date_lesser_than
265 query = Query.new(:name => '_')
265 query = Query.new(:name => '_')
266 query.add_filter('due_date', '<=', ['2011-07-10'])
266 query.add_filter('due_date', '<=', ['2011-07-10'])
267 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
267 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
268 find_issues_with_query(query)
268 find_issues_with_query(query)
269 end
269 end
270
270
271 def test_operator_date_greater_than
271 def test_operator_date_greater_than
272 query = Query.new(:name => '_')
272 query = Query.new(:name => '_')
273 query.add_filter('due_date', '>=', ['2011-07-10'])
273 query.add_filter('due_date', '>=', ['2011-07-10'])
274 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
274 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
275 find_issues_with_query(query)
275 find_issues_with_query(query)
276 end
276 end
277
277
278 def test_operator_date_between
278 def test_operator_date_between
279 query = Query.new(:name => '_')
279 query = Query.new(:name => '_')
280 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
280 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
281 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
281 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
282 find_issues_with_query(query)
282 find_issues_with_query(query)
283 end
283 end
284
284
285 def test_operator_in_more_than
285 def test_operator_in_more_than
286 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
286 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
287 query = Query.new(:project => Project.find(1), :name => '_')
287 query = Query.new(:project => Project.find(1), :name => '_')
288 query.add_filter('due_date', '>t+', ['15'])
288 query.add_filter('due_date', '>t+', ['15'])
289 issues = find_issues_with_query(query)
289 issues = find_issues_with_query(query)
290 assert !issues.empty?
290 assert !issues.empty?
291 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
291 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
292 end
292 end
293
293
294 def test_operator_in_less_than
294 def test_operator_in_less_than
295 query = Query.new(:project => Project.find(1), :name => '_')
295 query = Query.new(:project => Project.find(1), :name => '_')
296 query.add_filter('due_date', '<t+', ['15'])
296 query.add_filter('due_date', '<t+', ['15'])
297 issues = find_issues_with_query(query)
297 issues = find_issues_with_query(query)
298 assert !issues.empty?
298 assert !issues.empty?
299 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
299 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
300 end
300 end
301
301
302 def test_operator_less_than_ago
302 def test_operator_less_than_ago
303 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
303 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
304 query = Query.new(:project => Project.find(1), :name => '_')
304 query = Query.new(:project => Project.find(1), :name => '_')
305 query.add_filter('due_date', '>t-', ['3'])
305 query.add_filter('due_date', '>t-', ['3'])
306 issues = find_issues_with_query(query)
306 issues = find_issues_with_query(query)
307 assert !issues.empty?
307 assert !issues.empty?
308 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
308 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
309 end
309 end
310
310
311 def test_operator_more_than_ago
311 def test_operator_more_than_ago
312 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
312 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
313 query = Query.new(:project => Project.find(1), :name => '_')
313 query = Query.new(:project => Project.find(1), :name => '_')
314 query.add_filter('due_date', '<t-', ['10'])
314 query.add_filter('due_date', '<t-', ['10'])
315 assert query.statement.include?("#{Issue.table_name}.due_date <=")
315 assert query.statement.include?("#{Issue.table_name}.due_date <=")
316 issues = find_issues_with_query(query)
316 issues = find_issues_with_query(query)
317 assert !issues.empty?
317 assert !issues.empty?
318 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
318 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
319 end
319 end
320
320
321 def test_operator_in
321 def test_operator_in
322 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
322 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
323 query = Query.new(:project => Project.find(1), :name => '_')
323 query = Query.new(:project => Project.find(1), :name => '_')
324 query.add_filter('due_date', 't+', ['2'])
324 query.add_filter('due_date', 't+', ['2'])
325 issues = find_issues_with_query(query)
325 issues = find_issues_with_query(query)
326 assert !issues.empty?
326 assert !issues.empty?
327 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
327 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
328 end
328 end
329
329
330 def test_operator_ago
330 def test_operator_ago
331 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
331 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
332 query = Query.new(:project => Project.find(1), :name => '_')
332 query = Query.new(:project => Project.find(1), :name => '_')
333 query.add_filter('due_date', 't-', ['3'])
333 query.add_filter('due_date', 't-', ['3'])
334 issues = find_issues_with_query(query)
334 issues = find_issues_with_query(query)
335 assert !issues.empty?
335 assert !issues.empty?
336 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
336 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
337 end
337 end
338
338
339 def test_operator_today
339 def test_operator_today
340 query = Query.new(:project => Project.find(1), :name => '_')
340 query = Query.new(:project => Project.find(1), :name => '_')
341 query.add_filter('due_date', 't', [''])
341 query.add_filter('due_date', 't', [''])
342 issues = find_issues_with_query(query)
342 issues = find_issues_with_query(query)
343 assert !issues.empty?
343 assert !issues.empty?
344 issues.each {|issue| assert_equal Date.today, issue.due_date}
344 issues.each {|issue| assert_equal Date.today, issue.due_date}
345 end
345 end
346
346
347 def test_operator_this_week_on_date
347 def test_operator_this_week_on_date
348 query = Query.new(:project => Project.find(1), :name => '_')
348 query = Query.new(:project => Project.find(1), :name => '_')
349 query.add_filter('due_date', 'w', [''])
349 query.add_filter('due_date', 'w', [''])
350 find_issues_with_query(query)
350 find_issues_with_query(query)
351 end
351 end
352
352
353 def test_operator_this_week_on_datetime
353 def test_operator_this_week_on_datetime
354 query = Query.new(:project => Project.find(1), :name => '_')
354 query = Query.new(:project => Project.find(1), :name => '_')
355 query.add_filter('created_on', 'w', [''])
355 query.add_filter('created_on', 'w', [''])
356 find_issues_with_query(query)
356 find_issues_with_query(query)
357 end
357 end
358
358
359 def test_operator_contains
359 def test_operator_contains
360 query = Query.new(:project => Project.find(1), :name => '_')
360 query = Query.new(:project => Project.find(1), :name => '_')
361 query.add_filter('subject', '~', ['uNable'])
361 query.add_filter('subject', '~', ['uNable'])
362 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
362 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
363 result = find_issues_with_query(query)
363 result = find_issues_with_query(query)
364 assert result.empty?
364 assert result.empty?
365 result.each {|issue| assert issue.subject.downcase.include?('unable') }
365 result.each {|issue| assert issue.subject.downcase.include?('unable') }
366 end
366 end
367
367
368 def test_range_for_this_week_with_week_starting_on_monday
368 def test_range_for_this_week_with_week_starting_on_monday
369 I18n.locale = :fr
369 I18n.locale = :fr
370 assert_equal '1', I18n.t(:general_first_day_of_week)
370 assert_equal '1', I18n.t(:general_first_day_of_week)
371
371
372 Date.stubs(:today).returns(Date.parse('2011-04-29'))
372 Date.stubs(:today).returns(Date.parse('2011-04-29'))
373
373
374 query = Query.new(:project => Project.find(1), :name => '_')
374 query = Query.new(:project => Project.find(1), :name => '_')
375 query.add_filter('due_date', 'w', [''])
375 query.add_filter('due_date', 'w', [''])
376 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}"
376 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}"
377 I18n.locale = :en
377 I18n.locale = :en
378 end
378 end
379
379
380 def test_range_for_this_week_with_week_starting_on_sunday
380 def test_range_for_this_week_with_week_starting_on_sunday
381 I18n.locale = :en
381 I18n.locale = :en
382 assert_equal '7', I18n.t(:general_first_day_of_week)
382 assert_equal '7', I18n.t(:general_first_day_of_week)
383
383
384 Date.stubs(:today).returns(Date.parse('2011-04-29'))
384 Date.stubs(:today).returns(Date.parse('2011-04-29'))
385
385
386 query = Query.new(:project => Project.find(1), :name => '_')
386 query = Query.new(:project => Project.find(1), :name => '_')
387 query.add_filter('due_date', 'w', [''])
387 query.add_filter('due_date', 'w', [''])
388 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}"
388 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}"
389 end
389 end
390
390
391 def test_operator_does_not_contains
391 def test_operator_does_not_contains
392 query = Query.new(:project => Project.find(1), :name => '_')
392 query = Query.new(:project => Project.find(1), :name => '_')
393 query.add_filter('subject', '!~', ['uNable'])
393 query.add_filter('subject', '!~', ['uNable'])
394 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
394 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
395 find_issues_with_query(query)
395 find_issues_with_query(query)
396 end
396 end
397
397
398 def test_filter_assigned_to_me
398 def test_filter_assigned_to_me
399 user = User.find(2)
399 user = User.find(2)
400 group = Group.find(10)
400 group = Group.find(10)
401 User.current = user
401 User.current = user
402 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
402 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
403 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
403 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
404 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
404 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
405 group.users << user
405 group.users << user
406
406
407 query = Query.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
407 query = Query.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
408 result = query.issues
408 result = query.issues
409 assert_equal Issue.visible.all(:conditions => {:assigned_to_id => ([2] + user.reload.group_ids)}).sort_by(&:id), result.sort_by(&:id)
409 assert_equal Issue.visible.all(:conditions => {:assigned_to_id => ([2] + user.reload.group_ids)}).sort_by(&:id), result.sort_by(&:id)
410
410
411 assert result.include?(i1)
411 assert result.include?(i1)
412 assert result.include?(i2)
412 assert result.include?(i2)
413 assert !result.include?(i3)
413 assert !result.include?(i3)
414 end
414 end
415
415
416 def test_user_custom_field_filtered_on_me
417 User.current = User.find(2)
418 cf = IssueCustomField.create!(:field_format => 'user', :is_for_all => true, :is_filter => true, :name => 'User custom field', :tracker_ids => [1])
419 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '2'}, :subject => 'Test', :author_id => 1)
420 issue2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {cf.id.to_s => '3'})
421
422 query = Query.new(:name => '_', :project => Project.find(1))
423 filter = query.available_filters["cf_#{cf.id}"]
424 assert_not_nil filter
425 assert_include 'me', filter[:values].map{|v| v[1]}
426
427 query.filters = { "cf_#{cf.id}" => {:operator => '=', :values => ['me']}}
428 result = query.issues
429 assert_equal 1, result.size
430 assert issue1, result.first
431 end
432
416 def test_filter_my_projects
433 def test_filter_my_projects
417 User.current = User.find(2)
434 User.current = User.find(2)
418 query = Query.new(:name => '_')
435 query = Query.new(:name => '_')
419 filter = query.available_filters['project_id']
436 filter = query.available_filters['project_id']
420 assert_not_nil filter
437 assert_not_nil filter
421 assert_include 'mine', filter[:values].map{|v| v[1]}
438 assert_include 'mine', filter[:values].map{|v| v[1]}
422
439
423 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
440 query.filters = { 'project_id' => {:operator => '=', :values => ['mine']}}
424 result = query.issues
441 result = query.issues
425 assert_include "issues.project_id IN ('1','2','5')", query.statement
442 assert_include "issues.project_id IN ('1','2','5')", query.statement
426 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
443 assert_nil result.detect {|issue| !User.current.member_of?(issue.project)}
427 end
444 end
428
445
429 def test_filter_watched_issues
446 def test_filter_watched_issues
430 User.current = User.find(1)
447 User.current = User.find(1)
431 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
448 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
432 result = find_issues_with_query(query)
449 result = find_issues_with_query(query)
433 assert_not_nil result
450 assert_not_nil result
434 assert !result.empty?
451 assert !result.empty?
435 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
452 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
436 User.current = nil
453 User.current = nil
437 end
454 end
438
455
439 def test_filter_unwatched_issues
456 def test_filter_unwatched_issues
440 User.current = User.find(1)
457 User.current = User.find(1)
441 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
458 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
442 result = find_issues_with_query(query)
459 result = find_issues_with_query(query)
443 assert_not_nil result
460 assert_not_nil result
444 assert !result.empty?
461 assert !result.empty?
445 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
462 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
446 User.current = nil
463 User.current = nil
447 end
464 end
448
465
449 def test_statement_should_be_nil_with_no_filters
466 def test_statement_should_be_nil_with_no_filters
450 q = Query.new(:name => '_')
467 q = Query.new(:name => '_')
451 q.filters = {}
468 q.filters = {}
452
469
453 assert q.valid?
470 assert q.valid?
454 assert_nil q.statement
471 assert_nil q.statement
455 end
472 end
456
473
457 def test_default_columns
474 def test_default_columns
458 q = Query.new
475 q = Query.new
459 assert !q.columns.empty?
476 assert !q.columns.empty?
460 end
477 end
461
478
462 def test_set_column_names
479 def test_set_column_names
463 q = Query.new
480 q = Query.new
464 q.column_names = ['tracker', :subject, '', 'unknonw_column']
481 q.column_names = ['tracker', :subject, '', 'unknonw_column']
465 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
482 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
466 c = q.columns.first
483 c = q.columns.first
467 assert q.has_column?(c)
484 assert q.has_column?(c)
468 end
485 end
469
486
470 def test_query_should_preload_spent_hours
487 def test_query_should_preload_spent_hours
471 q = Query.new(:name => '_', :column_names => [:subject, :spent_hours])
488 q = Query.new(:name => '_', :column_names => [:subject, :spent_hours])
472 assert q.has_column?(:spent_hours)
489 assert q.has_column?(:spent_hours)
473 issues = q.issues
490 issues = q.issues
474 assert_not_nil issues.first.instance_variable_get("@spent_hours")
491 assert_not_nil issues.first.instance_variable_get("@spent_hours")
475 end
492 end
476
493
477 def test_groupable_columns_should_include_custom_fields
494 def test_groupable_columns_should_include_custom_fields
478 q = Query.new
495 q = Query.new
479 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
496 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
480 end
497 end
481
498
482 def test_grouped_with_valid_column
499 def test_grouped_with_valid_column
483 q = Query.new(:group_by => 'status')
500 q = Query.new(:group_by => 'status')
484 assert q.grouped?
501 assert q.grouped?
485 assert_not_nil q.group_by_column
502 assert_not_nil q.group_by_column
486 assert_equal :status, q.group_by_column.name
503 assert_equal :status, q.group_by_column.name
487 assert_not_nil q.group_by_statement
504 assert_not_nil q.group_by_statement
488 assert_equal 'status', q.group_by_statement
505 assert_equal 'status', q.group_by_statement
489 end
506 end
490
507
491 def test_grouped_with_invalid_column
508 def test_grouped_with_invalid_column
492 q = Query.new(:group_by => 'foo')
509 q = Query.new(:group_by => 'foo')
493 assert !q.grouped?
510 assert !q.grouped?
494 assert_nil q.group_by_column
511 assert_nil q.group_by_column
495 assert_nil q.group_by_statement
512 assert_nil q.group_by_statement
496 end
513 end
497
514
498 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
515 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
499 with_settings :user_format => 'lastname_coma_firstname' do
516 with_settings :user_format => 'lastname_coma_firstname' do
500 q = Query.new
517 q = Query.new
501 assert q.sortable_columns.has_key?('assigned_to')
518 assert q.sortable_columns.has_key?('assigned_to')
502 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
519 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
503 end
520 end
504 end
521 end
505
522
506 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
523 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
507 with_settings :user_format => 'lastname_coma_firstname' do
524 with_settings :user_format => 'lastname_coma_firstname' do
508 q = Query.new
525 q = Query.new
509 assert q.sortable_columns.has_key?('author')
526 assert q.sortable_columns.has_key?('author')
510 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
527 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
511 end
528 end
512 end
529 end
513
530
514 def test_default_sort
531 def test_default_sort
515 q = Query.new
532 q = Query.new
516 assert_equal [], q.sort_criteria
533 assert_equal [], q.sort_criteria
517 end
534 end
518
535
519 def test_set_sort_criteria_with_hash
536 def test_set_sort_criteria_with_hash
520 q = Query.new
537 q = Query.new
521 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
538 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
522 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
539 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
523 end
540 end
524
541
525 def test_set_sort_criteria_with_array
542 def test_set_sort_criteria_with_array
526 q = Query.new
543 q = Query.new
527 q.sort_criteria = [['priority', 'desc'], 'tracker']
544 q.sort_criteria = [['priority', 'desc'], 'tracker']
528 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
545 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
529 end
546 end
530
547
531 def test_create_query_with_sort
548 def test_create_query_with_sort
532 q = Query.new(:name => 'Sorted')
549 q = Query.new(:name => 'Sorted')
533 q.sort_criteria = [['priority', 'desc'], 'tracker']
550 q.sort_criteria = [['priority', 'desc'], 'tracker']
534 assert q.save
551 assert q.save
535 q.reload
552 q.reload
536 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
553 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
537 end
554 end
538
555
539 def test_sort_by_string_custom_field_asc
556 def test_sort_by_string_custom_field_asc
540 q = Query.new
557 q = Query.new
541 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
558 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
542 assert c
559 assert c
543 assert c.sortable
560 assert c.sortable
544 issues = Issue.find :all,
561 issues = Issue.find :all,
545 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
562 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
546 :conditions => q.statement,
563 :conditions => q.statement,
547 :order => "#{c.sortable} ASC"
564 :order => "#{c.sortable} ASC"
548 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
565 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
549 assert !values.empty?
566 assert !values.empty?
550 assert_equal values.sort, values
567 assert_equal values.sort, values
551 end
568 end
552
569
553 def test_sort_by_string_custom_field_desc
570 def test_sort_by_string_custom_field_desc
554 q = Query.new
571 q = Query.new
555 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
572 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
556 assert c
573 assert c
557 assert c.sortable
574 assert c.sortable
558 issues = Issue.find :all,
575 issues = Issue.find :all,
559 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
576 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
560 :conditions => q.statement,
577 :conditions => q.statement,
561 :order => "#{c.sortable} DESC"
578 :order => "#{c.sortable} DESC"
562 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
579 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
563 assert !values.empty?
580 assert !values.empty?
564 assert_equal values.sort.reverse, values
581 assert_equal values.sort.reverse, values
565 end
582 end
566
583
567 def test_sort_by_float_custom_field_asc
584 def test_sort_by_float_custom_field_asc
568 q = Query.new
585 q = Query.new
569 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
586 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
570 assert c
587 assert c
571 assert c.sortable
588 assert c.sortable
572 issues = Issue.find :all,
589 issues = Issue.find :all,
573 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
590 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
574 :conditions => q.statement,
591 :conditions => q.statement,
575 :order => "#{c.sortable} ASC"
592 :order => "#{c.sortable} ASC"
576 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
593 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
577 assert !values.empty?
594 assert !values.empty?
578 assert_equal values.sort, values
595 assert_equal values.sort, values
579 end
596 end
580
597
581 def test_invalid_query_should_raise_query_statement_invalid_error
598 def test_invalid_query_should_raise_query_statement_invalid_error
582 q = Query.new
599 q = Query.new
583 assert_raise Query::StatementInvalid do
600 assert_raise Query::StatementInvalid do
584 q.issues(:conditions => "foo = 1")
601 q.issues(:conditions => "foo = 1")
585 end
602 end
586 end
603 end
587
604
588 def test_issue_count
605 def test_issue_count
589 q = Query.new(:name => '_')
606 q = Query.new(:name => '_')
590 issue_count = q.issue_count
607 issue_count = q.issue_count
591 assert_equal q.issues.size, issue_count
608 assert_equal q.issues.size, issue_count
592 end
609 end
593
610
594 def test_issue_count_with_archived_issues
611 def test_issue_count_with_archived_issues
595 p = Project.generate!( :status => Project::STATUS_ARCHIVED )
612 p = Project.generate!( :status => Project::STATUS_ARCHIVED )
596 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
613 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
597 assert !i.visible?
614 assert !i.visible?
598
615
599 test_issue_count
616 test_issue_count
600 end
617 end
601
618
602 def test_issue_count_by_association_group
619 def test_issue_count_by_association_group
603 q = Query.new(:name => '_', :group_by => 'assigned_to')
620 q = Query.new(:name => '_', :group_by => 'assigned_to')
604 count_by_group = q.issue_count_by_group
621 count_by_group = q.issue_count_by_group
605 assert_kind_of Hash, count_by_group
622 assert_kind_of Hash, count_by_group
606 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
623 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
607 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
624 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
608 assert count_by_group.has_key?(User.find(3))
625 assert count_by_group.has_key?(User.find(3))
609 end
626 end
610
627
611 def test_issue_count_by_list_custom_field_group
628 def test_issue_count_by_list_custom_field_group
612 q = Query.new(:name => '_', :group_by => 'cf_1')
629 q = Query.new(:name => '_', :group_by => 'cf_1')
613 count_by_group = q.issue_count_by_group
630 count_by_group = q.issue_count_by_group
614 assert_kind_of Hash, count_by_group
631 assert_kind_of Hash, count_by_group
615 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
632 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
616 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
633 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
617 assert count_by_group.has_key?('MySQL')
634 assert count_by_group.has_key?('MySQL')
618 end
635 end
619
636
620 def test_issue_count_by_date_custom_field_group
637 def test_issue_count_by_date_custom_field_group
621 q = Query.new(:name => '_', :group_by => 'cf_8')
638 q = Query.new(:name => '_', :group_by => 'cf_8')
622 count_by_group = q.issue_count_by_group
639 count_by_group = q.issue_count_by_group
623 assert_kind_of Hash, count_by_group
640 assert_kind_of Hash, count_by_group
624 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
641 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
625 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
642 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
626 end
643 end
627
644
628 def test_issue_ids
645 def test_issue_ids
629 q = Query.new(:name => '_')
646 q = Query.new(:name => '_')
630 order = "issues.subject, issues.id"
647 order = "issues.subject, issues.id"
631 issues = q.issues(:order => order)
648 issues = q.issues(:order => order)
632 assert_equal issues.map(&:id), q.issue_ids(:order => order)
649 assert_equal issues.map(&:id), q.issue_ids(:order => order)
633 end
650 end
634
651
635 def test_label_for
652 def test_label_for
636 q = Query.new
653 q = Query.new
637 assert_equal 'Assignee', q.label_for('assigned_to_id')
654 assert_equal 'Assignee', q.label_for('assigned_to_id')
638 end
655 end
639
656
640 def test_editable_by
657 def test_editable_by
641 admin = User.find(1)
658 admin = User.find(1)
642 manager = User.find(2)
659 manager = User.find(2)
643 developer = User.find(3)
660 developer = User.find(3)
644
661
645 # Public query on project 1
662 # Public query on project 1
646 q = Query.find(1)
663 q = Query.find(1)
647 assert q.editable_by?(admin)
664 assert q.editable_by?(admin)
648 assert q.editable_by?(manager)
665 assert q.editable_by?(manager)
649 assert !q.editable_by?(developer)
666 assert !q.editable_by?(developer)
650
667
651 # Private query on project 1
668 # Private query on project 1
652 q = Query.find(2)
669 q = Query.find(2)
653 assert q.editable_by?(admin)
670 assert q.editable_by?(admin)
654 assert !q.editable_by?(manager)
671 assert !q.editable_by?(manager)
655 assert q.editable_by?(developer)
672 assert q.editable_by?(developer)
656
673
657 # Private query for all projects
674 # Private query for all projects
658 q = Query.find(3)
675 q = Query.find(3)
659 assert q.editable_by?(admin)
676 assert q.editable_by?(admin)
660 assert !q.editable_by?(manager)
677 assert !q.editable_by?(manager)
661 assert q.editable_by?(developer)
678 assert q.editable_by?(developer)
662
679
663 # Public query for all projects
680 # Public query for all projects
664 q = Query.find(4)
681 q = Query.find(4)
665 assert q.editable_by?(admin)
682 assert q.editable_by?(admin)
666 assert !q.editable_by?(manager)
683 assert !q.editable_by?(manager)
667 assert !q.editable_by?(developer)
684 assert !q.editable_by?(developer)
668 end
685 end
669
686
670 def test_visible_scope
687 def test_visible_scope
671 query_ids = Query.visible(User.anonymous).map(&:id)
688 query_ids = Query.visible(User.anonymous).map(&:id)
672
689
673 assert query_ids.include?(1), 'public query on public project was not visible'
690 assert query_ids.include?(1), 'public query on public project was not visible'
674 assert query_ids.include?(4), 'public query for all projects was not visible'
691 assert query_ids.include?(4), 'public query for all projects was not visible'
675 assert !query_ids.include?(2), 'private query on public project was visible'
692 assert !query_ids.include?(2), 'private query on public project was visible'
676 assert !query_ids.include?(3), 'private query for all projects was visible'
693 assert !query_ids.include?(3), 'private query for all projects was visible'
677 assert !query_ids.include?(7), 'public query on private project was visible'
694 assert !query_ids.include?(7), 'public query on private project was visible'
678 end
695 end
679
696
680 context "#available_filters" do
697 context "#available_filters" do
681 setup do
698 setup do
682 @query = Query.new(:name => "_")
699 @query = Query.new(:name => "_")
683 end
700 end
684
701
685 should "include users of visible projects in cross-project view" do
702 should "include users of visible projects in cross-project view" do
686 users = @query.available_filters["assigned_to_id"]
703 users = @query.available_filters["assigned_to_id"]
687 assert_not_nil users
704 assert_not_nil users
688 assert users[:values].map{|u|u[1]}.include?("3")
705 assert users[:values].map{|u|u[1]}.include?("3")
689 end
706 end
690
707
691 should "include visible projects in cross-project view" do
708 should "include visible projects in cross-project view" do
692 projects = @query.available_filters["project_id"]
709 projects = @query.available_filters["project_id"]
693 assert_not_nil projects
710 assert_not_nil projects
694 assert projects[:values].map{|u|u[1]}.include?("1")
711 assert projects[:values].map{|u|u[1]}.include?("1")
695 end
712 end
696
713
697 context "'member_of_group' filter" do
714 context "'member_of_group' filter" do
698 should "be present" do
715 should "be present" do
699 assert @query.available_filters.keys.include?("member_of_group")
716 assert @query.available_filters.keys.include?("member_of_group")
700 end
717 end
701
718
702 should "be an optional list" do
719 should "be an optional list" do
703 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
720 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
704 end
721 end
705
722
706 should "have a list of the groups as values" do
723 should "have a list of the groups as values" do
707 Group.destroy_all # No fixtures
724 Group.destroy_all # No fixtures
708 group1 = Group.generate!.reload
725 group1 = Group.generate!.reload
709 group2 = Group.generate!.reload
726 group2 = Group.generate!.reload
710
727
711 expected_group_list = [
728 expected_group_list = [
712 [group1.name, group1.id.to_s],
729 [group1.name, group1.id.to_s],
713 [group2.name, group2.id.to_s]
730 [group2.name, group2.id.to_s]
714 ]
731 ]
715 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
732 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
716 end
733 end
717
734
718 end
735 end
719
736
720 context "'assigned_to_role' filter" do
737 context "'assigned_to_role' filter" do
721 should "be present" do
738 should "be present" do
722 assert @query.available_filters.keys.include?("assigned_to_role")
739 assert @query.available_filters.keys.include?("assigned_to_role")
723 end
740 end
724
741
725 should "be an optional list" do
742 should "be an optional list" do
726 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
743 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
727 end
744 end
728
745
729 should "have a list of the Roles as values" do
746 should "have a list of the Roles as values" do
730 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
747 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
731 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
748 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
732 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
749 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
733 end
750 end
734
751
735 should "not include the built in Roles as values" do
752 should "not include the built in Roles as values" do
736 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
753 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
737 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
754 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
738 end
755 end
739
756
740 end
757 end
741
758
742 end
759 end
743
760
744 context "#statement" do
761 context "#statement" do
745 context "with 'member_of_group' filter" do
762 context "with 'member_of_group' filter" do
746 setup do
763 setup do
747 Group.destroy_all # No fixtures
764 Group.destroy_all # No fixtures
748 @user_in_group = User.generate!
765 @user_in_group = User.generate!
749 @second_user_in_group = User.generate!
766 @second_user_in_group = User.generate!
750 @user_in_group2 = User.generate!
767 @user_in_group2 = User.generate!
751 @user_not_in_group = User.generate!
768 @user_not_in_group = User.generate!
752
769
753 @group = Group.generate!.reload
770 @group = Group.generate!.reload
754 @group.users << @user_in_group
771 @group.users << @user_in_group
755 @group.users << @second_user_in_group
772 @group.users << @second_user_in_group
756
773
757 @group2 = Group.generate!.reload
774 @group2 = Group.generate!.reload
758 @group2.users << @user_in_group2
775 @group2.users << @user_in_group2
759
776
760 end
777 end
761
778
762 should "search assigned to for users in the group" do
779 should "search assigned to for users in the group" do
763 @query = Query.new(:name => '_')
780 @query = Query.new(:name => '_')
764 @query.add_filter('member_of_group', '=', [@group.id.to_s])
781 @query.add_filter('member_of_group', '=', [@group.id.to_s])
765
782
766 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
783 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
767 assert_find_issues_with_query_is_successful @query
784 assert_find_issues_with_query_is_successful @query
768 end
785 end
769
786
770 should "search not assigned to any group member (none)" do
787 should "search not assigned to any group member (none)" do
771 @query = Query.new(:name => '_')
788 @query = Query.new(:name => '_')
772 @query.add_filter('member_of_group', '!*', [''])
789 @query.add_filter('member_of_group', '!*', [''])
773
790
774 # Users not in a group
791 # Users not in a group
775 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}')"
792 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}')"
776 assert_find_issues_with_query_is_successful @query
793 assert_find_issues_with_query_is_successful @query
777 end
794 end
778
795
779 should "search assigned to any group member (all)" do
796 should "search assigned to any group member (all)" do
780 @query = Query.new(:name => '_')
797 @query = Query.new(:name => '_')
781 @query.add_filter('member_of_group', '*', [''])
798 @query.add_filter('member_of_group', '*', [''])
782
799
783 # Only users in a group
800 # Only users in a group
784 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}')"
801 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}')"
785 assert_find_issues_with_query_is_successful @query
802 assert_find_issues_with_query_is_successful @query
786 end
803 end
787
804
788 should "return an empty set with = empty group" do
805 should "return an empty set with = empty group" do
789 @empty_group = Group.generate!
806 @empty_group = Group.generate!
790 @query = Query.new(:name => '_')
807 @query = Query.new(:name => '_')
791 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
808 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
792
809
793 assert_equal [], find_issues_with_query(@query)
810 assert_equal [], find_issues_with_query(@query)
794 end
811 end
795
812
796 should "return issues with ! empty group" do
813 should "return issues with ! empty group" do
797 @empty_group = Group.generate!
814 @empty_group = Group.generate!
798 @query = Query.new(:name => '_')
815 @query = Query.new(:name => '_')
799 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
816 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
800
817
801 assert_find_issues_with_query_is_successful @query
818 assert_find_issues_with_query_is_successful @query
802 end
819 end
803 end
820 end
804
821
805 context "with 'assigned_to_role' filter" do
822 context "with 'assigned_to_role' filter" do
806 setup do
823 setup do
807 @manager_role = Role.find_by_name('Manager')
824 @manager_role = Role.find_by_name('Manager')
808 @developer_role = Role.find_by_name('Developer')
825 @developer_role = Role.find_by_name('Developer')
809
826
810 @project = Project.generate!
827 @project = Project.generate!
811 @manager = User.generate!
828 @manager = User.generate!
812 @developer = User.generate!
829 @developer = User.generate!
813 @boss = User.generate!
830 @boss = User.generate!
814 @guest = User.generate!
831 @guest = User.generate!
815 User.add_to_project(@manager, @project, @manager_role)
832 User.add_to_project(@manager, @project, @manager_role)
816 User.add_to_project(@developer, @project, @developer_role)
833 User.add_to_project(@developer, @project, @developer_role)
817 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
834 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
818
835
819 @issue1 = Issue.generate_for_project!(@project, :assigned_to_id => @manager.id)
836 @issue1 = Issue.generate_for_project!(@project, :assigned_to_id => @manager.id)
820 @issue2 = Issue.generate_for_project!(@project, :assigned_to_id => @developer.id)
837 @issue2 = Issue.generate_for_project!(@project, :assigned_to_id => @developer.id)
821 @issue3 = Issue.generate_for_project!(@project, :assigned_to_id => @boss.id)
838 @issue3 = Issue.generate_for_project!(@project, :assigned_to_id => @boss.id)
822 @issue4 = Issue.generate_for_project!(@project, :assigned_to_id => @guest.id)
839 @issue4 = Issue.generate_for_project!(@project, :assigned_to_id => @guest.id)
823 @issue5 = Issue.generate_for_project!(@project)
840 @issue5 = Issue.generate_for_project!(@project)
824 end
841 end
825
842
826 should "search assigned to for users with the Role" do
843 should "search assigned to for users with the Role" do
827 @query = Query.new(:name => '_', :project => @project)
844 @query = Query.new(:name => '_', :project => @project)
828 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
845 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
829
846
830 assert_query_result [@issue1, @issue3], @query
847 assert_query_result [@issue1, @issue3], @query
831 end
848 end
832
849
833 should "search assigned to for users with the Role on the issue project" do
850 should "search assigned to for users with the Role on the issue project" do
834 other_project = Project.generate!
851 other_project = Project.generate!
835 User.add_to_project(@developer, other_project, @manager_role)
852 User.add_to_project(@developer, other_project, @manager_role)
836
853
837 @query = Query.new(:name => '_', :project => @project)
854 @query = Query.new(:name => '_', :project => @project)
838 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
855 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
839
856
840 assert_query_result [@issue1, @issue3], @query
857 assert_query_result [@issue1, @issue3], @query
841 end
858 end
842
859
843 should "return an empty set with empty role" do
860 should "return an empty set with empty role" do
844 @empty_role = Role.generate!
861 @empty_role = Role.generate!
845 @query = Query.new(:name => '_', :project => @project)
862 @query = Query.new(:name => '_', :project => @project)
846 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
863 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
847
864
848 assert_query_result [], @query
865 assert_query_result [], @query
849 end
866 end
850
867
851 should "search assigned to for users without the Role" do
868 should "search assigned to for users without the Role" do
852 @query = Query.new(:name => '_', :project => @project)
869 @query = Query.new(:name => '_', :project => @project)
853 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
870 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
854
871
855 assert_query_result [@issue2, @issue4, @issue5], @query
872 assert_query_result [@issue2, @issue4, @issue5], @query
856 end
873 end
857
874
858 should "search assigned to for users not assigned to any Role (none)" do
875 should "search assigned to for users not assigned to any Role (none)" do
859 @query = Query.new(:name => '_', :project => @project)
876 @query = Query.new(:name => '_', :project => @project)
860 @query.add_filter('assigned_to_role', '!*', [''])
877 @query.add_filter('assigned_to_role', '!*', [''])
861
878
862 assert_query_result [@issue4, @issue5], @query
879 assert_query_result [@issue4, @issue5], @query
863 end
880 end
864
881
865 should "search assigned to for users assigned to any Role (all)" do
882 should "search assigned to for users assigned to any Role (all)" do
866 @query = Query.new(:name => '_', :project => @project)
883 @query = Query.new(:name => '_', :project => @project)
867 @query.add_filter('assigned_to_role', '*', [''])
884 @query.add_filter('assigned_to_role', '*', [''])
868
885
869 assert_query_result [@issue1, @issue2, @issue3], @query
886 assert_query_result [@issue1, @issue2, @issue3], @query
870 end
887 end
871
888
872 should "return issues with ! empty role" do
889 should "return issues with ! empty role" do
873 @empty_role = Role.generate!
890 @empty_role = Role.generate!
874 @query = Query.new(:name => '_', :project => @project)
891 @query = Query.new(:name => '_', :project => @project)
875 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
892 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
876
893
877 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
894 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
878 end
895 end
879 end
896 end
880 end
897 end
881
898
882 end
899 end
General Comments 0
You need to be logged in to leave comments. Login now