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