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