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