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