##// END OF EJS Templates
Fixed: "None" category issue count is empty while grouping by category (#4308)....
Jean-Philippe Lang -
r2998:346c569f98f5
parent child
Show More
@@ -1,70 +1,64
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 module QueriesHelper
19 19
20 20 def operators_for_select(filter_type)
21 21 Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]}
22 22 end
23 23
24 24 def column_header(column)
25 25 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
26 26 :default_order => column.default_order) :
27 27 content_tag('th', column.caption)
28 28 end
29 29
30 def column_value(column, issue)
31 if column.is_a?(QueryCustomFieldColumn)
32 cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
33 show_value(cv)
34 else
35 value = issue.send(column.name)
36 end
37 end
38
39 30 def column_content(column, issue)
40 if column.is_a?(QueryCustomFieldColumn)
41 cv = issue.custom_values.detect {|v| v.custom_field_id == column.custom_field.id}
42 show_value(cv)
43 else
44 value = issue.send(column.name)
45 if value.is_a?(Date)
46 format_date(value)
47 elsif value.is_a?(Time)
48 format_time(value)
31 value = column.value(issue)
32
33 case value.class.name
34 when 'String'
35 if column.name == :subject
36 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
37 else
38 h(value)
39 end
40 when 'Time'
41 format_time(value)
42 when 'Date'
43 format_date(value)
44 when 'Fixnum', 'Float'
45 if column.name == :done_ratio
46 progress_bar(value, :width => '80px')
49 47 else
50 case column.name
51 when :subject
52 h((!@project.nil? && @project != issue.project) ? "#{issue.project.name} - " : '') +
53 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
54 when :project
55 link_to(h(value), :controller => 'projects', :action => 'show', :id => value)
56 when :assigned_to
57 link_to_user value
58 when :author
59 link_to_user value
60 when :done_ratio
61 progress_bar(value, :width => '80px')
62 when :fixed_version
63 link_to(h(value), { :controller => 'versions', :action => 'show', :id => issue.fixed_version_id })
64 else
65 h(value)
66 end
48 value.to_s
67 49 end
50 when 'User'
51 link_to_user value
52 when 'Project'
53 link_to(h(value), :controller => 'projects', :action => 'show', :id => value)
54 when 'Version'
55 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
56 when 'TrueClass'
57 l(:general_text_Yes)
58 when 'FalseClass'
59 l(:general_text_No)
60 else
61 h(value)
68 62 end
69 63 end
70 64 end
@@ -1,111 +1,130
1 1 # redMine - project management software
2 2 # Copyright (C) 2006 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class CustomField < ActiveRecord::Base
19 19 has_many :custom_values, :dependent => :delete_all
20 20 acts_as_list :scope => 'type = \'#{self.class}\''
21 21 serialize :possible_values
22 22
23 23 FIELD_FORMATS = { "string" => { :name => :label_string, :order => 1 },
24 24 "text" => { :name => :label_text, :order => 2 },
25 25 "int" => { :name => :label_integer, :order => 3 },
26 26 "float" => { :name => :label_float, :order => 4 },
27 27 "list" => { :name => :label_list, :order => 5 },
28 28 "date" => { :name => :label_date, :order => 6 },
29 29 "bool" => { :name => :label_boolean, :order => 7 }
30 30 }.freeze
31 31
32 32 validates_presence_of :name, :field_format
33 33 validates_uniqueness_of :name, :scope => :type
34 34 validates_length_of :name, :maximum => 30
35 35 validates_format_of :name, :with => /^[\w\s\.\'\-]*$/i
36 36 validates_inclusion_of :field_format, :in => FIELD_FORMATS.keys
37 37
38 38 def initialize(attributes = nil)
39 39 super
40 40 self.possible_values ||= []
41 41 end
42 42
43 43 def before_validation
44 44 # make sure these fields are not searchable
45 45 self.searchable = false if %w(int float date bool).include?(field_format)
46 46 true
47 47 end
48 48
49 49 def validate
50 50 if self.field_format == "list"
51 51 errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
52 52 errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
53 53 end
54 54
55 55 # validate default value
56 56 v = CustomValue.new(:custom_field => self.clone, :value => default_value, :customized => nil)
57 57 v.custom_field.is_required = false
58 58 errors.add(:default_value, :invalid) unless v.valid?
59 59 end
60 60
61 61 # Makes possible_values accept a multiline string
62 62 def possible_values=(arg)
63 63 if arg.is_a?(Array)
64 64 write_attribute(:possible_values, arg.compact.collect(&:strip).select {|v| !v.blank?})
65 65 else
66 66 self.possible_values = arg.to_s.split(/[\n\r]+/)
67 67 end
68 68 end
69 69
70 def cast_value(value)
71 casted = nil
72 unless value.blank?
73 case field_format
74 when 'string', 'text', 'list'
75 casted = value
76 when 'date'
77 casted = begin; value.to_date; rescue; nil end
78 when 'bool'
79 casted = (value == '1' ? true : false)
80 when 'int'
81 casted = value.to_i
82 when 'float'
83 casted = value.to_f
84 end
85 end
86 casted
87 end
88
70 89 # Returns a ORDER BY clause that can used to sort customized
71 90 # objects by their value of the custom field.
72 91 # Returns false, if the custom field can not be used for sorting.
73 92 def order_statement
74 93 case field_format
75 94 when 'string', 'text', 'list', 'date', 'bool'
76 95 # COALESCE is here to make sure that blank and NULL values are sorted equally
77 96 "COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
78 97 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
79 98 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
80 99 " AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
81 100 when 'int', 'float'
82 101 # Make the database cast values into numeric
83 102 # Postgresql will raise an error if a value can not be casted!
84 103 # CustomValue validations should ensure that it doesn't occur
85 104 "(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
86 105 " WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
87 106 " AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
88 107 " AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
89 108 else
90 109 nil
91 110 end
92 111 end
93 112
94 113 def <=>(field)
95 114 position <=> field.position
96 115 end
97 116
98 117 def self.customized_class
99 118 self.name =~ /^(.+)CustomField$/
100 119 begin; $1.constantize; rescue nil; end
101 120 end
102 121
103 122 # to move in project_custom_field
104 123 def self.for_all
105 124 find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
106 125 end
107 126
108 127 def type_name
109 128 nil
110 129 end
111 130 end
@@ -1,545 +1,558
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class QueryColumn
19 19 attr_accessor :name, :sortable, :groupable, :default_order
20 20 include Redmine::I18n
21 21
22 22 def initialize(name, options={})
23 23 self.name = name
24 24 self.sortable = options[:sortable]
25 25 self.groupable = options[:groupable] || false
26 26 if groupable == true
27 27 self.groupable = name.to_s
28 28 end
29 29 self.default_order = options[:default_order]
30 30 end
31 31
32 32 def caption
33 33 l("field_#{name}")
34 34 end
35 35
36 36 # Returns true if the column is sortable, otherwise false
37 37 def sortable?
38 38 !sortable.nil?
39 39 end
40
41 def value(issue)
42 issue.send name
43 end
40 44 end
41 45
42 46 class QueryCustomFieldColumn < QueryColumn
43 47
44 48 def initialize(custom_field)
45 49 self.name = "cf_#{custom_field.id}".to_sym
46 50 self.sortable = custom_field.order_statement || false
47 51 if %w(list date bool int).include?(custom_field.field_format)
48 52 self.groupable = custom_field.order_statement
49 53 end
50 54 self.groupable ||= false
51 55 @cf = custom_field
52 56 end
53 57
54 58 def caption
55 59 @cf.name
56 60 end
57 61
58 62 def custom_field
59 63 @cf
60 64 end
65
66 def value(issue)
67 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
68 cv && @cf.cast_value(cv.value)
69 end
61 70 end
62 71
63 72 class Query < ActiveRecord::Base
64 73 class StatementInvalid < ::ActiveRecord::StatementInvalid
65 74 end
66 75
67 76 belongs_to :project
68 77 belongs_to :user
69 78 serialize :filters
70 79 serialize :column_names
71 80 serialize :sort_criteria, Array
72 81
73 82 attr_protected :project_id, :user_id
74 83
75 84 validates_presence_of :name, :on => :save
76 85 validates_length_of :name, :maximum => 255
77 86
78 87 @@operators = { "=" => :label_equals,
79 88 "!" => :label_not_equals,
80 89 "o" => :label_open_issues,
81 90 "c" => :label_closed_issues,
82 91 "!*" => :label_none,
83 92 "*" => :label_all,
84 93 ">=" => :label_greater_or_equal,
85 94 "<=" => :label_less_or_equal,
86 95 "<t+" => :label_in_less_than,
87 96 ">t+" => :label_in_more_than,
88 97 "t+" => :label_in,
89 98 "t" => :label_today,
90 99 "w" => :label_this_week,
91 100 ">t-" => :label_less_than_ago,
92 101 "<t-" => :label_more_than_ago,
93 102 "t-" => :label_ago,
94 103 "~" => :label_contains,
95 104 "!~" => :label_not_contains }
96 105
97 106 cattr_reader :operators
98 107
99 108 @@operators_by_filter_type = { :list => [ "=", "!" ],
100 109 :list_status => [ "o", "=", "!", "c", "*" ],
101 110 :list_optional => [ "=", "!", "!*", "*" ],
102 111 :list_subprojects => [ "*", "!*", "=" ],
103 112 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
104 113 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
105 114 :string => [ "=", "~", "!", "!~" ],
106 115 :text => [ "~", "!~" ],
107 116 :integer => [ "=", ">=", "<=", "!*", "*" ] }
108 117
109 118 cattr_reader :operators_by_filter_type
110 119
111 120 @@available_columns = [
112 121 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
113 122 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
114 123 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
115 124 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
116 125 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
117 126 QueryColumn.new(:author),
118 127 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
119 128 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
120 129 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
121 130 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
122 131 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
123 132 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
124 133 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
125 134 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
126 135 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
127 136 ]
128 137 cattr_reader :available_columns
129 138
130 139 def initialize(attributes = nil)
131 140 super attributes
132 141 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
133 142 end
134 143
135 144 def after_initialize
136 145 # Store the fact that project is nil (used in #editable_by?)
137 146 @is_for_all = project.nil?
138 147 end
139 148
140 149 def validate
141 150 filters.each_key do |field|
142 151 errors.add label_for(field), :blank unless
143 152 # filter requires one or more values
144 153 (values_for(field) and !values_for(field).first.blank?) or
145 154 # filter doesn't require any value
146 155 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
147 156 end if filters
148 157 end
149 158
150 159 def editable_by?(user)
151 160 return false unless user
152 161 # Admin can edit them all and regular users can edit their private queries
153 162 return true if user.admin? || (!is_public && self.user_id == user.id)
154 163 # Members can not edit public queries that are for all project (only admin is allowed to)
155 164 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
156 165 end
157 166
158 167 def available_filters
159 168 return @available_filters if @available_filters
160 169
161 170 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
162 171
163 172 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
164 173 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
165 174 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
166 175 "subject" => { :type => :text, :order => 8 },
167 176 "created_on" => { :type => :date_past, :order => 9 },
168 177 "updated_on" => { :type => :date_past, :order => 10 },
169 178 "start_date" => { :type => :date, :order => 11 },
170 179 "due_date" => { :type => :date, :order => 12 },
171 180 "estimated_hours" => { :type => :integer, :order => 13 },
172 181 "done_ratio" => { :type => :integer, :order => 14 }}
173 182
174 183 user_values = []
175 184 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
176 185 if project
177 186 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
178 187 else
179 188 # members of the user's projects
180 189 user_values += User.current.projects.collect(&:users).flatten.uniq.sort.collect{|s| [s.name, s.id.to_s] }
181 190 end
182 191 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
183 192 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
184 193
185 194 if User.current.logged?
186 195 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
187 196 end
188 197
189 198 if project
190 199 # project specific filters
191 200 unless @project.issue_categories.empty?
192 201 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => @project.issue_categories.collect{|s| [s.name, s.id.to_s] } }
193 202 end
194 203 unless @project.versions.empty?
195 204 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
196 205 end
197 206 unless @project.descendants.active.empty?
198 207 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
199 208 end
200 209 add_custom_fields_filters(@project.all_issue_custom_fields)
201 210 else
202 211 # global filters for cross project issue list
203 212 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
204 213 end
205 214 @available_filters
206 215 end
207 216
208 217 def add_filter(field, operator, values)
209 218 # values must be an array
210 219 return unless values and values.is_a? Array # and !values.first.empty?
211 220 # check if field is defined as an available filter
212 221 if available_filters.has_key? field
213 222 filter_options = available_filters[field]
214 223 # check if operator is allowed for that filter
215 224 #if @@operators_by_filter_type[filter_options[:type]].include? operator
216 225 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
217 226 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
218 227 #end
219 228 filters[field] = {:operator => operator, :values => values }
220 229 end
221 230 end
222 231
223 232 def add_short_filter(field, expression)
224 233 return unless expression
225 234 parms = expression.scan(/^(o|c|\!|\*)?(.*)$/).first
226 235 add_filter field, (parms[0] || "="), [parms[1] || ""]
227 236 end
228 237
229 238 def has_filter?(field)
230 239 filters and filters[field]
231 240 end
232 241
233 242 def operator_for(field)
234 243 has_filter?(field) ? filters[field][:operator] : nil
235 244 end
236 245
237 246 def values_for(field)
238 247 has_filter?(field) ? filters[field][:values] : nil
239 248 end
240 249
241 250 def label_for(field)
242 251 label = available_filters[field][:name] if available_filters.has_key?(field)
243 252 label ||= field.gsub(/\_id$/, "")
244 253 end
245 254
246 255 def available_columns
247 256 return @available_columns if @available_columns
248 257 @available_columns = Query.available_columns
249 258 @available_columns += (project ?
250 259 project.all_issue_custom_fields :
251 260 IssueCustomField.find(:all)
252 261 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
253 262 end
254 263
255 264 # Returns an array of columns that can be used to group the results
256 265 def groupable_columns
257 266 available_columns.select {|c| c.groupable}
258 267 end
259 268
260 269 def columns
261 270 if has_default_columns?
262 271 available_columns.select do |c|
263 272 # Adds the project column by default for cross-project lists
264 273 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
265 274 end
266 275 else
267 276 # preserve the column_names order
268 277 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
269 278 end
270 279 end
271 280
272 281 def column_names=(names)
273 282 if names
274 283 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
275 284 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
276 285 # Set column_names to nil if default columns
277 286 if names.map(&:to_s) == Setting.issue_list_default_columns
278 287 names = nil
279 288 end
280 289 end
281 290 write_attribute(:column_names, names)
282 291 end
283 292
284 293 def has_column?(column)
285 294 column_names && column_names.include?(column.name)
286 295 end
287 296
288 297 def has_default_columns?
289 298 column_names.nil? || column_names.empty?
290 299 end
291 300
292 301 def sort_criteria=(arg)
293 302 c = []
294 303 if arg.is_a?(Hash)
295 304 arg = arg.keys.sort.collect {|k| arg[k]}
296 305 end
297 306 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
298 307 write_attribute(:sort_criteria, c)
299 308 end
300 309
301 310 def sort_criteria
302 311 read_attribute(:sort_criteria) || []
303 312 end
304 313
305 314 def sort_criteria_key(arg)
306 315 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
307 316 end
308 317
309 318 def sort_criteria_order(arg)
310 319 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
311 320 end
312 321
313 322 # Returns the SQL sort order that should be prepended for grouping
314 323 def group_by_sort_order
315 324 if grouped? && (column = group_by_column)
316 325 column.sortable.is_a?(Array) ?
317 326 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
318 327 "#{column.sortable} #{column.default_order}"
319 328 end
320 329 end
321 330
322 331 # Returns true if the query is a grouped query
323 332 def grouped?
324 333 !group_by.blank?
325 334 end
326 335
327 336 def group_by_column
328 337 groupable_columns.detect {|c| c.name.to_s == group_by}
329 338 end
330 339
331 340 def group_by_statement
332 341 group_by_column.groupable
333 342 end
334 343
335 344 def project_statement
336 345 project_clauses = []
337 346 if project && !@project.descendants.active.empty?
338 347 ids = [project.id]
339 348 if has_filter?("subproject_id")
340 349 case operator_for("subproject_id")
341 350 when '='
342 351 # include the selected subprojects
343 352 ids += values_for("subproject_id").each(&:to_i)
344 353 when '!*'
345 354 # main project only
346 355 else
347 356 # all subprojects
348 357 ids += project.descendants.collect(&:id)
349 358 end
350 359 elsif Setting.display_subprojects_issues?
351 360 ids += project.descendants.collect(&:id)
352 361 end
353 362 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
354 363 elsif project
355 364 project_clauses << "#{Project.table_name}.id = %d" % project.id
356 365 end
357 366 project_clauses << Project.allowed_to_condition(User.current, :view_issues)
358 367 project_clauses.join(' AND ')
359 368 end
360 369
361 370 def statement
362 371 # filters clauses
363 372 filters_clauses = []
364 373 filters.each_key do |field|
365 374 next if field == "subproject_id"
366 375 v = values_for(field).clone
367 376 next unless v and !v.empty?
368 377 operator = operator_for(field)
369 378
370 379 # "me" value subsitution
371 380 if %w(assigned_to_id author_id watcher_id).include?(field)
372 381 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
373 382 end
374 383
375 384 sql = ''
376 385 if field =~ /^cf_(\d+)$/
377 386 # custom field
378 387 db_table = CustomValue.table_name
379 388 db_field = 'value'
380 389 is_custom_filter = true
381 390 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 "
382 391 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
383 392 elsif field == 'watcher_id'
384 393 db_table = Watcher.table_name
385 394 db_field = 'user_id'
386 395 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
387 396 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
388 397 else
389 398 # regular field
390 399 db_table = Issue.table_name
391 400 db_field = field
392 401 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
393 402 end
394 403 filters_clauses << sql
395 404
396 405 end if filters and valid?
397 406
398 407 (filters_clauses << project_statement).join(' AND ')
399 408 end
400 409
401 410 # Returns the issue count
402 411 def issue_count
403 412 Issue.count(:include => [:status, :project], :conditions => statement)
404 413 rescue ::ActiveRecord::StatementInvalid => e
405 414 raise StatementInvalid.new(e.message)
406 415 end
407 416
408 417 # Returns the issue count by group or nil if query is not grouped
409 418 def issue_count_by_group
419 r = nil
410 420 if grouped?
411 421 begin
412 422 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
413 Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
423 r = Issue.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
414 424 rescue ActiveRecord::RecordNotFound
415 {nil => issue_count}
425 r = {nil => issue_count}
426 end
427 c = group_by_column
428 if c.is_a?(QueryCustomFieldColumn)
429 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
416 430 end
417 else
418 nil
419 431 end
432 r
420 433 rescue ::ActiveRecord::StatementInvalid => e
421 434 raise StatementInvalid.new(e.message)
422 435 end
423 436
424 437 # Returns the issues
425 438 # Valid options are :order, :offset, :limit, :include, :conditions
426 439 def issues(options={})
427 440 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
428 441 order_option = nil if order_option.blank?
429 442
430 443 Issue.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
431 444 :conditions => Query.merge_conditions(statement, options[:conditions]),
432 445 :order => order_option,
433 446 :limit => options[:limit],
434 447 :offset => options[:offset]
435 448 rescue ::ActiveRecord::StatementInvalid => e
436 449 raise StatementInvalid.new(e.message)
437 450 end
438 451
439 452 # Returns the journals
440 453 # Valid options are :order, :offset, :limit
441 454 def journals(options={})
442 455 Journal.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
443 456 :conditions => statement,
444 457 :order => options[:order],
445 458 :limit => options[:limit],
446 459 :offset => options[:offset]
447 460 rescue ::ActiveRecord::StatementInvalid => e
448 461 raise StatementInvalid.new(e.message)
449 462 end
450 463
451 464 # Returns the versions
452 465 # Valid options are :conditions
453 466 def versions(options={})
454 467 Version.find :all, :include => :project,
455 468 :conditions => Query.merge_conditions(project_statement, options[:conditions])
456 469 rescue ::ActiveRecord::StatementInvalid => e
457 470 raise StatementInvalid.new(e.message)
458 471 end
459 472
460 473 private
461 474
462 475 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
463 476 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
464 477 sql = ''
465 478 case operator
466 479 when "="
467 480 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
468 481 when "!"
469 482 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
470 483 when "!*"
471 484 sql = "#{db_table}.#{db_field} IS NULL"
472 485 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
473 486 when "*"
474 487 sql = "#{db_table}.#{db_field} IS NOT NULL"
475 488 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
476 489 when ">="
477 490 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
478 491 when "<="
479 492 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
480 493 when "o"
481 494 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
482 495 when "c"
483 496 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
484 497 when ">t-"
485 498 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
486 499 when "<t-"
487 500 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
488 501 when "t-"
489 502 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
490 503 when ">t+"
491 504 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
492 505 when "<t+"
493 506 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
494 507 when "t+"
495 508 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
496 509 when "t"
497 510 sql = date_range_clause(db_table, db_field, 0, 0)
498 511 when "w"
499 512 from = l(:general_first_day_of_week) == '7' ?
500 513 # week starts on sunday
501 514 ((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
502 515 # week starts on monday (Rails default)
503 516 Time.now.at_beginning_of_week
504 517 sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
505 518 when "~"
506 519 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
507 520 when "!~"
508 521 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
509 522 end
510 523
511 524 return sql
512 525 end
513 526
514 527 def add_custom_fields_filters(custom_fields)
515 528 @available_filters ||= {}
516 529
517 530 custom_fields.select(&:is_filter?).each do |field|
518 531 case field.field_format
519 532 when "text"
520 533 options = { :type => :text, :order => 20 }
521 534 when "list"
522 535 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
523 536 when "date"
524 537 options = { :type => :date, :order => 20 }
525 538 when "bool"
526 539 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
527 540 else
528 541 options = { :type => :string, :order => 20 }
529 542 end
530 543 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
531 544 end
532 545 end
533 546
534 547 # Returns a SQL clause for a date or datetime field.
535 548 def date_range_clause(table, field, from, to)
536 549 s = []
537 550 if from
538 551 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
539 552 end
540 553 if to
541 554 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
542 555 end
543 556 s.join(' AND ')
544 557 end
545 558 end
@@ -1,34 +1,34
1 1 <% form_tag({}) do -%>
2 2 <%= hidden_field_tag 'back_url', url_for(params) %>
3 3 <table class="list issues">
4 4 <thead><tr>
5 5 <th><%= link_to image_tag('toggle_check.png'), {}, :onclick => 'toggleIssuesSelection(Element.up(this, "form")); return false;',
6 6 :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
7 7 </th>
8 8 <%= sort_header_tag('id', :caption => '#', :default_order => 'desc') %>
9 9 <% query.columns.each do |column| %>
10 10 <%= column_header(column) %>
11 11 <% end %>
12 12 </tr></thead>
13 13 <% previous_group = false %>
14 14 <tbody>
15 15 <% issues.each do |issue| -%>
16 <% if @query.grouped? && (group = column_value(@query.group_by_column, issue) || '') != previous_group %>
16 <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
17 17 <% reset_cycle %>
18 18 <tr class="group open">
19 19 <td colspan="<%= query.columns.size + 2 %>">
20 20 <span class="expander" onclick="toggleRowGroup(this); return false;">&nbsp;</span>
21 <%= group.blank? ? 'None' : group %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
21 <%= group.blank? ? 'None' : column_content(@query.group_by_column, issue) %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
22 22 </td>
23 23 </tr>
24 24 <% previous_group = group %>
25 25 <% end %>
26 26 <tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
27 27 <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
28 28 <td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
29 29 <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>
30 30 </tr>
31 31 <% end -%>
32 32 </tbody>
33 33 </table>
34 34 <% end -%>
@@ -1,103 +1,117
1 1 ---
2 2 custom_fields_001:
3 3 name: Database
4 4 min_length: 0
5 5 regexp: ""
6 6 is_for_all: true
7 7 is_filter: true
8 8 type: IssueCustomField
9 9 max_length: 0
10 10 possible_values:
11 11 - MySQL
12 12 - PostgreSQL
13 13 - Oracle
14 14 id: 1
15 15 is_required: false
16 16 field_format: list
17 17 default_value: ""
18 18 editable: true
19 19 custom_fields_002:
20 20 name: Searchable field
21 21 min_length: 1
22 22 regexp: ""
23 23 is_for_all: true
24 24 type: IssueCustomField
25 25 max_length: 100
26 26 possible_values: ""
27 27 id: 2
28 28 is_required: false
29 29 field_format: string
30 30 searchable: true
31 31 default_value: "Default string"
32 32 editable: true
33 33 custom_fields_003:
34 34 name: Development status
35 35 min_length: 0
36 36 regexp: ""
37 37 is_for_all: false
38 38 is_filter: true
39 39 type: ProjectCustomField
40 40 max_length: 0
41 41 possible_values:
42 42 - Stable
43 43 - Beta
44 44 - Alpha
45 45 - Planning
46 46 id: 3
47 47 is_required: true
48 48 field_format: list
49 49 default_value: ""
50 50 editable: true
51 51 custom_fields_004:
52 52 name: Phone number
53 53 min_length: 0
54 54 regexp: ""
55 55 is_for_all: false
56 56 type: UserCustomField
57 57 max_length: 0
58 58 possible_values: ""
59 59 id: 4
60 60 is_required: false
61 61 field_format: string
62 62 default_value: ""
63 63 editable: true
64 64 custom_fields_005:
65 65 name: Money
66 66 min_length: 0
67 67 regexp: ""
68 68 is_for_all: false
69 69 type: UserCustomField
70 70 max_length: 0
71 71 possible_values: ""
72 72 id: 5
73 73 is_required: false
74 74 field_format: float
75 75 default_value: ""
76 76 editable: true
77 77 custom_fields_006:
78 78 name: Float field
79 79 min_length: 0
80 80 regexp: ""
81 81 is_for_all: true
82 82 type: IssueCustomField
83 83 max_length: 0
84 84 possible_values: ""
85 85 id: 6
86 86 is_required: false
87 87 field_format: float
88 88 default_value: ""
89 89 editable: true
90 90 custom_fields_007:
91 91 name: Billable
92 92 min_length: 0
93 93 regexp: ""
94 94 is_for_all: false
95 95 is_filter: true
96 96 type: TimeEntryActivityCustomField
97 97 max_length: 0
98 98 possible_values: ""
99 99 id: 7
100 100 is_required: false
101 101 field_format: bool
102 102 default_value: ""
103 103 editable: true
104 custom_fields_008:
105 name: Custom date
106 min_length: 0
107 regexp: ""
108 is_for_all: true
109 is_filter: false
110 type: IssueCustomField
111 max_length: 0
112 possible_values: ""
113 id: 8
114 is_required: false
115 field_format: date
116 default_value: ""
117 editable: true
@@ -1,97 +1,103
1 1 ---
2 2 custom_values_006:
3 3 customized_type: Issue
4 4 custom_field_id: 2
5 5 customized_id: 3
6 6 id: 6
7 7 value: "125"
8 8 custom_values_007:
9 9 customized_type: Project
10 10 custom_field_id: 3
11 11 customized_id: 1
12 12 id: 7
13 13 value: Stable
14 14 custom_values_001:
15 15 customized_type: Principal
16 16 custom_field_id: 4
17 17 customized_id: 3
18 18 id: 1
19 19 value: ""
20 20 custom_values_002:
21 21 customized_type: Principal
22 22 custom_field_id: 4
23 23 customized_id: 4
24 24 id: 2
25 25 value: 01 23 45 67 89
26 26 custom_values_003:
27 27 customized_type: Principal
28 28 custom_field_id: 4
29 29 customized_id: 2
30 30 id: 3
31 31 value: ""
32 32 custom_values_004:
33 33 customized_type: Issue
34 34 custom_field_id: 2
35 35 customized_id: 1
36 36 id: 4
37 37 value: "125"
38 38 custom_values_005:
39 39 customized_type: Issue
40 40 custom_field_id: 2
41 41 customized_id: 2
42 42 id: 5
43 43 value: ""
44 44 custom_values_008:
45 45 customized_type: Issue
46 46 custom_field_id: 1
47 47 customized_id: 3
48 48 id: 8
49 49 value: "MySQL"
50 50 custom_values_009:
51 51 customized_type: Issue
52 52 custom_field_id: 2
53 53 customized_id: 3
54 54 id: 9
55 55 value: "this is a stringforcustomfield search"
56 56 custom_values_010:
57 57 customized_type: Issue
58 58 custom_field_id: 6
59 59 customized_id: 1
60 60 id: 10
61 61 value: "2.1"
62 62 custom_values_011:
63 63 customized_type: Issue
64 64 custom_field_id: 6
65 65 customized_id: 2
66 66 id: 11
67 67 value: "2.05"
68 68 custom_values_012:
69 69 customized_type: Issue
70 70 custom_field_id: 6
71 71 customized_id: 3
72 72 id: 12
73 73 value: "11.65"
74 74 custom_values_013:
75 75 customized_type: Issue
76 76 custom_field_id: 6
77 77 customized_id: 7
78 78 id: 13
79 79 value: ""
80 80 custom_values_014:
81 81 customized_type: Issue
82 82 custom_field_id: 6
83 83 customized_id: 5
84 84 id: 14
85 85 value: "-7.6"
86 86 custom_values_015:
87 87 customized_type: Enumeration
88 88 custom_field_id: 7
89 89 customized_id: 10
90 90 id: 15
91 91 value: true
92 92 custom_values_016:
93 93 customized_type: Enumeration
94 94 custom_field_id: 7
95 95 customized_id: 11
96 96 id: 16
97 97 value: '1'
98 custom_values_017:
99 customized_type: Issue
100 custom_field_id: 8
101 customized_id: 1
102 id: 17
103 value: '2009-12-01'
@@ -1,1200 +1,1194
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'issues_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class IssuesController; def rescue_action(e) raise e end; end
23 23
24 24 class IssuesControllerTest < ActionController::TestCase
25 25 fixtures :projects,
26 26 :users,
27 27 :roles,
28 28 :members,
29 29 :member_roles,
30 30 :issues,
31 31 :issue_statuses,
32 32 :versions,
33 33 :trackers,
34 34 :projects_trackers,
35 35 :issue_categories,
36 36 :enabled_modules,
37 37 :enumerations,
38 38 :attachments,
39 39 :workflows,
40 40 :custom_fields,
41 41 :custom_values,
42 42 :custom_fields_projects,
43 43 :custom_fields_trackers,
44 44 :time_entries,
45 45 :journals,
46 46 :journal_details,
47 47 :queries
48 48
49 49 def setup
50 50 @controller = IssuesController.new
51 51 @request = ActionController::TestRequest.new
52 52 @response = ActionController::TestResponse.new
53 53 User.current = nil
54 54 end
55 55
56 56 def test_index_routing
57 57 assert_routing(
58 58 {:method => :get, :path => '/issues'},
59 59 :controller => 'issues', :action => 'index'
60 60 )
61 61 end
62 62
63 63 def test_index
64 64 Setting.default_language = 'en'
65 65
66 66 get :index
67 67 assert_response :success
68 68 assert_template 'index.rhtml'
69 69 assert_not_nil assigns(:issues)
70 70 assert_nil assigns(:project)
71 71 assert_tag :tag => 'a', :content => /Can't print recipes/
72 72 assert_tag :tag => 'a', :content => /Subproject issue/
73 73 # private projects hidden
74 74 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
75 75 assert_no_tag :tag => 'a', :content => /Issue on project 2/
76 76 # project column
77 77 assert_tag :tag => 'th', :content => /Project/
78 78 end
79 79
80 80 def test_index_should_not_list_issues_when_module_disabled
81 81 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
82 82 get :index
83 83 assert_response :success
84 84 assert_template 'index.rhtml'
85 85 assert_not_nil assigns(:issues)
86 86 assert_nil assigns(:project)
87 87 assert_no_tag :tag => 'a', :content => /Can't print recipes/
88 88 assert_tag :tag => 'a', :content => /Subproject issue/
89 89 end
90 90
91 91 def test_index_with_project_routing
92 92 assert_routing(
93 93 {:method => :get, :path => '/projects/23/issues'},
94 94 :controller => 'issues', :action => 'index', :project_id => '23'
95 95 )
96 96 end
97 97
98 98 def test_index_should_not_list_issues_when_module_disabled
99 99 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
100 100 get :index
101 101 assert_response :success
102 102 assert_template 'index.rhtml'
103 103 assert_not_nil assigns(:issues)
104 104 assert_nil assigns(:project)
105 105 assert_no_tag :tag => 'a', :content => /Can't print recipes/
106 106 assert_tag :tag => 'a', :content => /Subproject issue/
107 107 end
108 108
109 109 def test_index_with_project_routing
110 110 assert_routing(
111 111 {:method => :get, :path => 'projects/23/issues'},
112 112 :controller => 'issues', :action => 'index', :project_id => '23'
113 113 )
114 114 end
115 115
116 116 def test_index_with_project
117 117 Setting.display_subprojects_issues = 0
118 118 get :index, :project_id => 1
119 119 assert_response :success
120 120 assert_template 'index.rhtml'
121 121 assert_not_nil assigns(:issues)
122 122 assert_tag :tag => 'a', :content => /Can't print recipes/
123 123 assert_no_tag :tag => 'a', :content => /Subproject issue/
124 124 end
125 125
126 126 def test_index_with_project_and_subprojects
127 127 Setting.display_subprojects_issues = 1
128 128 get :index, :project_id => 1
129 129 assert_response :success
130 130 assert_template 'index.rhtml'
131 131 assert_not_nil assigns(:issues)
132 132 assert_tag :tag => 'a', :content => /Can't print recipes/
133 133 assert_tag :tag => 'a', :content => /Subproject issue/
134 134 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
135 135 end
136 136
137 137 def test_index_with_project_and_subprojects_should_show_private_subprojects
138 138 @request.session[:user_id] = 2
139 139 Setting.display_subprojects_issues = 1
140 140 get :index, :project_id => 1
141 141 assert_response :success
142 142 assert_template 'index.rhtml'
143 143 assert_not_nil assigns(:issues)
144 144 assert_tag :tag => 'a', :content => /Can't print recipes/
145 145 assert_tag :tag => 'a', :content => /Subproject issue/
146 146 assert_tag :tag => 'a', :content => /Issue of a private subproject/
147 147 end
148 148
149 149 def test_index_with_project_routing_formatted
150 150 assert_routing(
151 151 {:method => :get, :path => 'projects/23/issues.pdf'},
152 152 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'pdf'
153 153 )
154 154 assert_routing(
155 155 {:method => :get, :path => 'projects/23/issues.atom'},
156 156 :controller => 'issues', :action => 'index', :project_id => '23', :format => 'atom'
157 157 )
158 158 end
159 159
160 160 def test_index_with_project_and_filter
161 161 get :index, :project_id => 1, :set_filter => 1
162 162 assert_response :success
163 163 assert_template 'index.rhtml'
164 164 assert_not_nil assigns(:issues)
165 165 end
166 166
167 167 def test_index_with_query
168 168 get :index, :project_id => 1, :query_id => 5
169 169 assert_response :success
170 170 assert_template 'index.rhtml'
171 171 assert_not_nil assigns(:issues)
172 172 assert_nil assigns(:issue_count_by_group)
173 173 end
174 174
175 175 def test_index_with_query_grouped_by_tracker
176 176 get :index, :project_id => 1, :query_id => 6
177 177 assert_response :success
178 178 assert_template 'index.rhtml'
179 179 assert_not_nil assigns(:issues)
180 count_by_group = assigns(:issue_count_by_group)
181 assert_kind_of Hash, count_by_group
182 assert_kind_of Tracker, count_by_group.keys.first
183 assert_not_nil count_by_group[Tracker.find(1)]
180 assert_not_nil assigns(:issue_count_by_group)
184 181 end
185 182
186 183 def test_index_with_query_grouped_by_list_custom_field
187 184 get :index, :project_id => 1, :query_id => 9
188 185 assert_response :success
189 186 assert_template 'index.rhtml'
190 187 assert_not_nil assigns(:issues)
191 count_by_group = assigns(:issue_count_by_group)
192 assert_kind_of Hash, count_by_group
193 assert_kind_of String, count_by_group.keys.first
194 assert_not_nil count_by_group['MySQL']
188 assert_not_nil assigns(:issue_count_by_group)
195 189 end
196 190
197 191 def test_index_sort_by_field_not_included_in_columns
198 192 Setting.issue_list_default_columns = %w(subject author)
199 193 get :index, :sort => 'tracker'
200 194 end
201 195
202 196 def test_index_csv_with_project
203 197 Setting.default_language = 'en'
204 198
205 199 get :index, :format => 'csv'
206 200 assert_response :success
207 201 assert_not_nil assigns(:issues)
208 202 assert_equal 'text/csv', @response.content_type
209 203 assert @response.body.starts_with?("#,")
210 204
211 205 get :index, :project_id => 1, :format => 'csv'
212 206 assert_response :success
213 207 assert_not_nil assigns(:issues)
214 208 assert_equal 'text/csv', @response.content_type
215 209 end
216 210
217 211 def test_index_formatted
218 212 assert_routing(
219 213 {:method => :get, :path => 'issues.pdf'},
220 214 :controller => 'issues', :action => 'index', :format => 'pdf'
221 215 )
222 216 assert_routing(
223 217 {:method => :get, :path => 'issues.atom'},
224 218 :controller => 'issues', :action => 'index', :format => 'atom'
225 219 )
226 220 end
227 221
228 222 def test_index_pdf
229 223 get :index, :format => 'pdf'
230 224 assert_response :success
231 225 assert_not_nil assigns(:issues)
232 226 assert_equal 'application/pdf', @response.content_type
233 227
234 228 get :index, :project_id => 1, :format => 'pdf'
235 229 assert_response :success
236 230 assert_not_nil assigns(:issues)
237 231 assert_equal 'application/pdf', @response.content_type
238 232
239 233 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
240 234 assert_response :success
241 235 assert_not_nil assigns(:issues)
242 236 assert_equal 'application/pdf', @response.content_type
243 237 end
244 238
245 239 def test_index_sort
246 240 get :index, :sort => 'tracker,id:desc'
247 241 assert_response :success
248 242
249 243 sort_params = @request.session['issues_index_sort']
250 244 assert sort_params.is_a?(String)
251 245 assert_equal 'tracker,id:desc', sort_params
252 246
253 247 issues = assigns(:issues)
254 248 assert_not_nil issues
255 249 assert !issues.empty?
256 250 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
257 251 end
258 252
259 253 def test_index_with_columns
260 254 columns = ['tracker', 'subject', 'assigned_to']
261 255 get :index, :set_filter => 1, :query => { 'column_names' => columns}
262 256 assert_response :success
263 257
264 258 # query should use specified columns
265 259 query = assigns(:query)
266 260 assert_kind_of Query, query
267 261 assert_equal columns, query.column_names.map(&:to_s)
268 262
269 263 # columns should be stored in session
270 264 assert_kind_of Hash, session[:query]
271 265 assert_kind_of Array, session[:query][:column_names]
272 266 assert_equal columns, session[:query][:column_names].map(&:to_s)
273 267 end
274 268
275 269 def test_gantt
276 270 get :gantt, :project_id => 1
277 271 assert_response :success
278 272 assert_template 'gantt.rhtml'
279 273 assert_not_nil assigns(:gantt)
280 274 events = assigns(:gantt).events
281 275 assert_not_nil events
282 276 # Issue with start and due dates
283 277 i = Issue.find(1)
284 278 assert_not_nil i.due_date
285 279 assert events.include?(Issue.find(1))
286 280 # Issue with without due date but targeted to a version with date
287 281 i = Issue.find(2)
288 282 assert_nil i.due_date
289 283 assert events.include?(i)
290 284 end
291 285
292 286 def test_cross_project_gantt
293 287 get :gantt
294 288 assert_response :success
295 289 assert_template 'gantt.rhtml'
296 290 assert_not_nil assigns(:gantt)
297 291 events = assigns(:gantt).events
298 292 assert_not_nil events
299 293 end
300 294
301 295 def test_gantt_export_to_pdf
302 296 get :gantt, :project_id => 1, :format => 'pdf'
303 297 assert_response :success
304 298 assert_equal 'application/pdf', @response.content_type
305 299 assert @response.body.starts_with?('%PDF')
306 300 assert_not_nil assigns(:gantt)
307 301 end
308 302
309 303 def test_cross_project_gantt_export_to_pdf
310 304 get :gantt, :format => 'pdf'
311 305 assert_response :success
312 306 assert_equal 'application/pdf', @response.content_type
313 307 assert @response.body.starts_with?('%PDF')
314 308 assert_not_nil assigns(:gantt)
315 309 end
316 310
317 311 if Object.const_defined?(:Magick)
318 312 def test_gantt_image
319 313 get :gantt, :project_id => 1, :format => 'png'
320 314 assert_response :success
321 315 assert_equal 'image/png', @response.content_type
322 316 end
323 317 else
324 318 puts "RMagick not installed. Skipping tests !!!"
325 319 end
326 320
327 321 def test_calendar
328 322 get :calendar, :project_id => 1
329 323 assert_response :success
330 324 assert_template 'calendar'
331 325 assert_not_nil assigns(:calendar)
332 326 end
333 327
334 328 def test_cross_project_calendar
335 329 get :calendar
336 330 assert_response :success
337 331 assert_template 'calendar'
338 332 assert_not_nil assigns(:calendar)
339 333 end
340 334
341 335 def test_changes
342 336 get :changes, :project_id => 1
343 337 assert_response :success
344 338 assert_not_nil assigns(:journals)
345 339 assert_equal 'application/atom+xml', @response.content_type
346 340 end
347 341
348 342 def test_show_routing
349 343 assert_routing(
350 344 {:method => :get, :path => '/issues/64'},
351 345 :controller => 'issues', :action => 'show', :id => '64'
352 346 )
353 347 end
354 348
355 349 def test_show_routing_formatted
356 350 assert_routing(
357 351 {:method => :get, :path => '/issues/2332.pdf'},
358 352 :controller => 'issues', :action => 'show', :id => '2332', :format => 'pdf'
359 353 )
360 354 assert_routing(
361 355 {:method => :get, :path => '/issues/23123.atom'},
362 356 :controller => 'issues', :action => 'show', :id => '23123', :format => 'atom'
363 357 )
364 358 end
365 359
366 360 def test_show_by_anonymous
367 361 get :show, :id => 1
368 362 assert_response :success
369 363 assert_template 'show.rhtml'
370 364 assert_not_nil assigns(:issue)
371 365 assert_equal Issue.find(1), assigns(:issue)
372 366
373 367 # anonymous role is allowed to add a note
374 368 assert_tag :tag => 'form',
375 369 :descendant => { :tag => 'fieldset',
376 370 :child => { :tag => 'legend',
377 371 :content => /Notes/ } }
378 372 end
379 373
380 374 def test_show_by_manager
381 375 @request.session[:user_id] = 2
382 376 get :show, :id => 1
383 377 assert_response :success
384 378
385 379 assert_tag :tag => 'form',
386 380 :descendant => { :tag => 'fieldset',
387 381 :child => { :tag => 'legend',
388 382 :content => /Change properties/ } },
389 383 :descendant => { :tag => 'fieldset',
390 384 :child => { :tag => 'legend',
391 385 :content => /Log time/ } },
392 386 :descendant => { :tag => 'fieldset',
393 387 :child => { :tag => 'legend',
394 388 :content => /Notes/ } }
395 389 end
396 390
397 391 def test_show_should_deny_anonymous_access_without_permission
398 392 Role.anonymous.remove_permission!(:view_issues)
399 393 get :show, :id => 1
400 394 assert_response :redirect
401 395 end
402 396
403 397 def test_show_should_deny_non_member_access_without_permission
404 398 Role.non_member.remove_permission!(:view_issues)
405 399 @request.session[:user_id] = 9
406 400 get :show, :id => 1
407 401 assert_response 403
408 402 end
409 403
410 404 def test_show_should_deny_member_access_without_permission
411 405 Role.find(1).remove_permission!(:view_issues)
412 406 @request.session[:user_id] = 2
413 407 get :show, :id => 1
414 408 assert_response 403
415 409 end
416 410
417 411 def test_show_should_not_disclose_relations_to_invisible_issues
418 412 Setting.cross_project_issue_relations = '1'
419 413 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
420 414 # Relation to a private project issue
421 415 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
422 416
423 417 get :show, :id => 1
424 418 assert_response :success
425 419
426 420 assert_tag :div, :attributes => { :id => 'relations' },
427 421 :descendant => { :tag => 'a', :content => /#2$/ }
428 422 assert_no_tag :div, :attributes => { :id => 'relations' },
429 423 :descendant => { :tag => 'a', :content => /#4$/ }
430 424 end
431 425
432 426 def test_show_atom
433 427 get :show, :id => 2, :format => 'atom'
434 428 assert_response :success
435 429 assert_template 'changes.rxml'
436 430 # Inline image
437 431 assert @response.body.include?("&lt;img src=\"http://test.host/attachments/download/10\" alt=\"\" /&gt;"), "Body did not match. Body: #{@response.body}"
438 432 end
439 433
440 434 def test_new_routing
441 435 assert_routing(
442 436 {:method => :get, :path => '/projects/1/issues/new'},
443 437 :controller => 'issues', :action => 'new', :project_id => '1'
444 438 )
445 439 assert_recognizes(
446 440 {:controller => 'issues', :action => 'new', :project_id => '1'},
447 441 {:method => :post, :path => '/projects/1/issues'}
448 442 )
449 443 end
450 444
451 445 def test_show_export_to_pdf
452 446 get :show, :id => 3, :format => 'pdf'
453 447 assert_response :success
454 448 assert_equal 'application/pdf', @response.content_type
455 449 assert @response.body.starts_with?('%PDF')
456 450 assert_not_nil assigns(:issue)
457 451 end
458 452
459 453 def test_get_new
460 454 @request.session[:user_id] = 2
461 455 get :new, :project_id => 1, :tracker_id => 1
462 456 assert_response :success
463 457 assert_template 'new'
464 458
465 459 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
466 460 :value => 'Default string' }
467 461 end
468 462
469 463 def test_get_new_without_tracker_id
470 464 @request.session[:user_id] = 2
471 465 get :new, :project_id => 1
472 466 assert_response :success
473 467 assert_template 'new'
474 468
475 469 issue = assigns(:issue)
476 470 assert_not_nil issue
477 471 assert_equal Project.find(1).trackers.first, issue.tracker
478 472 end
479 473
480 474 def test_get_new_with_no_default_status_should_display_an_error
481 475 @request.session[:user_id] = 2
482 476 IssueStatus.delete_all
483 477
484 478 get :new, :project_id => 1
485 479 assert_response 500
486 480 assert_not_nil flash[:error]
487 481 assert_tag :tag => 'div', :attributes => { :class => /error/ },
488 482 :content => /No default issue/
489 483 end
490 484
491 485 def test_get_new_with_no_tracker_should_display_an_error
492 486 @request.session[:user_id] = 2
493 487 Tracker.delete_all
494 488
495 489 get :new, :project_id => 1
496 490 assert_response 500
497 491 assert_not_nil flash[:error]
498 492 assert_tag :tag => 'div', :attributes => { :class => /error/ },
499 493 :content => /No tracker/
500 494 end
501 495
502 496 def test_update_new_form
503 497 @request.session[:user_id] = 2
504 498 xhr :post, :update_form, :project_id => 1,
505 499 :issue => {:tracker_id => 2,
506 500 :subject => 'This is the test_new issue',
507 501 :description => 'This is the description',
508 502 :priority_id => 5}
509 503 assert_response :success
510 504 assert_template 'attributes'
511 505
512 506 issue = assigns(:issue)
513 507 assert_kind_of Issue, issue
514 508 assert_equal 1, issue.project_id
515 509 assert_equal 2, issue.tracker_id
516 510 assert_equal 'This is the test_new issue', issue.subject
517 511 end
518 512
519 513 def test_post_new
520 514 @request.session[:user_id] = 2
521 515 assert_difference 'Issue.count' do
522 516 post :new, :project_id => 1,
523 517 :issue => {:tracker_id => 3,
524 518 :subject => 'This is the test_new issue',
525 519 :description => 'This is the description',
526 520 :priority_id => 5,
527 521 :estimated_hours => '',
528 522 :custom_field_values => {'2' => 'Value for field 2'}}
529 523 end
530 524 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
531 525
532 526 issue = Issue.find_by_subject('This is the test_new issue')
533 527 assert_not_nil issue
534 528 assert_equal 2, issue.author_id
535 529 assert_equal 3, issue.tracker_id
536 530 assert_nil issue.estimated_hours
537 531 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
538 532 assert_not_nil v
539 533 assert_equal 'Value for field 2', v.value
540 534 end
541 535
542 536 def test_post_new_and_continue
543 537 @request.session[:user_id] = 2
544 538 post :new, :project_id => 1,
545 539 :issue => {:tracker_id => 3,
546 540 :subject => 'This is first issue',
547 541 :priority_id => 5},
548 542 :continue => ''
549 543 assert_redirected_to :controller => 'issues', :action => 'new', :tracker_id => 3
550 544 end
551 545
552 546 def test_post_new_without_custom_fields_param
553 547 @request.session[:user_id] = 2
554 548 assert_difference 'Issue.count' do
555 549 post :new, :project_id => 1,
556 550 :issue => {:tracker_id => 1,
557 551 :subject => 'This is the test_new issue',
558 552 :description => 'This is the description',
559 553 :priority_id => 5}
560 554 end
561 555 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
562 556 end
563 557
564 558 def test_post_new_with_required_custom_field_and_without_custom_fields_param
565 559 field = IssueCustomField.find_by_name('Database')
566 560 field.update_attribute(:is_required, true)
567 561
568 562 @request.session[:user_id] = 2
569 563 post :new, :project_id => 1,
570 564 :issue => {:tracker_id => 1,
571 565 :subject => 'This is the test_new issue',
572 566 :description => 'This is the description',
573 567 :priority_id => 5}
574 568 assert_response :success
575 569 assert_template 'new'
576 570 issue = assigns(:issue)
577 571 assert_not_nil issue
578 572 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
579 573 end
580 574
581 575 def test_post_new_with_watchers
582 576 @request.session[:user_id] = 2
583 577 ActionMailer::Base.deliveries.clear
584 578
585 579 assert_difference 'Watcher.count', 2 do
586 580 post :new, :project_id => 1,
587 581 :issue => {:tracker_id => 1,
588 582 :subject => 'This is a new issue with watchers',
589 583 :description => 'This is the description',
590 584 :priority_id => 5,
591 585 :watcher_user_ids => ['2', '3']}
592 586 end
593 587 issue = Issue.find_by_subject('This is a new issue with watchers')
594 588 assert_not_nil issue
595 589 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
596 590
597 591 # Watchers added
598 592 assert_equal [2, 3], issue.watcher_user_ids.sort
599 593 assert issue.watched_by?(User.find(3))
600 594 # Watchers notified
601 595 mail = ActionMailer::Base.deliveries.last
602 596 assert_kind_of TMail::Mail, mail
603 597 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
604 598 end
605 599
606 600 def test_post_new_should_send_a_notification
607 601 ActionMailer::Base.deliveries.clear
608 602 @request.session[:user_id] = 2
609 603 assert_difference 'Issue.count' do
610 604 post :new, :project_id => 1,
611 605 :issue => {:tracker_id => 3,
612 606 :subject => 'This is the test_new issue',
613 607 :description => 'This is the description',
614 608 :priority_id => 5,
615 609 :estimated_hours => '',
616 610 :custom_field_values => {'2' => 'Value for field 2'}}
617 611 end
618 612 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
619 613
620 614 assert_equal 1, ActionMailer::Base.deliveries.size
621 615 end
622 616
623 617 def test_post_should_preserve_fields_values_on_validation_failure
624 618 @request.session[:user_id] = 2
625 619 post :new, :project_id => 1,
626 620 :issue => {:tracker_id => 1,
627 621 # empty subject
628 622 :subject => '',
629 623 :description => 'This is a description',
630 624 :priority_id => 6,
631 625 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
632 626 assert_response :success
633 627 assert_template 'new'
634 628
635 629 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
636 630 :content => 'This is a description'
637 631 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
638 632 :child => { :tag => 'option', :attributes => { :selected => 'selected',
639 633 :value => '6' },
640 634 :content => 'High' }
641 635 # Custom fields
642 636 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
643 637 :child => { :tag => 'option', :attributes => { :selected => 'selected',
644 638 :value => 'Oracle' },
645 639 :content => 'Oracle' }
646 640 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
647 641 :value => 'Value for field 2'}
648 642 end
649 643
650 644 def test_copy_routing
651 645 assert_routing(
652 646 {:method => :get, :path => '/projects/world_domination/issues/567/copy'},
653 647 :controller => 'issues', :action => 'new', :project_id => 'world_domination', :copy_from => '567'
654 648 )
655 649 end
656 650
657 651 def test_copy_issue
658 652 @request.session[:user_id] = 2
659 653 get :new, :project_id => 1, :copy_from => 1
660 654 assert_template 'new'
661 655 assert_not_nil assigns(:issue)
662 656 orig = Issue.find(1)
663 657 assert_equal orig.subject, assigns(:issue).subject
664 658 end
665 659
666 660 def test_edit_routing
667 661 assert_routing(
668 662 {:method => :get, :path => '/issues/1/edit'},
669 663 :controller => 'issues', :action => 'edit', :id => '1'
670 664 )
671 665 assert_recognizes( #TODO: use a PUT on the issue URI isntead, need to adjust form
672 666 {:controller => 'issues', :action => 'edit', :id => '1'},
673 667 {:method => :post, :path => '/issues/1/edit'}
674 668 )
675 669 end
676 670
677 671 def test_get_edit
678 672 @request.session[:user_id] = 2
679 673 get :edit, :id => 1
680 674 assert_response :success
681 675 assert_template 'edit'
682 676 assert_not_nil assigns(:issue)
683 677 assert_equal Issue.find(1), assigns(:issue)
684 678 end
685 679
686 680 def test_get_edit_with_params
687 681 @request.session[:user_id] = 2
688 682 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
689 683 assert_response :success
690 684 assert_template 'edit'
691 685
692 686 issue = assigns(:issue)
693 687 assert_not_nil issue
694 688
695 689 assert_equal 5, issue.status_id
696 690 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
697 691 :child => { :tag => 'option',
698 692 :content => 'Closed',
699 693 :attributes => { :selected => 'selected' } }
700 694
701 695 assert_equal 7, issue.priority_id
702 696 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
703 697 :child => { :tag => 'option',
704 698 :content => 'Urgent',
705 699 :attributes => { :selected => 'selected' } }
706 700 end
707 701
708 702 def test_update_edit_form
709 703 @request.session[:user_id] = 2
710 704 xhr :post, :update_form, :project_id => 1,
711 705 :id => 1,
712 706 :issue => {:tracker_id => 2,
713 707 :subject => 'This is the test_new issue',
714 708 :description => 'This is the description',
715 709 :priority_id => 5}
716 710 assert_response :success
717 711 assert_template 'attributes'
718 712
719 713 issue = assigns(:issue)
720 714 assert_kind_of Issue, issue
721 715 assert_equal 1, issue.id
722 716 assert_equal 1, issue.project_id
723 717 assert_equal 2, issue.tracker_id
724 718 assert_equal 'This is the test_new issue', issue.subject
725 719 end
726 720
727 721 def test_reply_routing
728 722 assert_routing(
729 723 {:method => :post, :path => '/issues/1/quoted'},
730 724 :controller => 'issues', :action => 'reply', :id => '1'
731 725 )
732 726 end
733 727
734 728 def test_reply_to_issue
735 729 @request.session[:user_id] = 2
736 730 get :reply, :id => 1
737 731 assert_response :success
738 732 assert_select_rjs :show, "update"
739 733 end
740 734
741 735 def test_reply_to_note
742 736 @request.session[:user_id] = 2
743 737 get :reply, :id => 1, :journal_id => 2
744 738 assert_response :success
745 739 assert_select_rjs :show, "update"
746 740 end
747 741
748 742 def test_post_edit_without_custom_fields_param
749 743 @request.session[:user_id] = 2
750 744 ActionMailer::Base.deliveries.clear
751 745
752 746 issue = Issue.find(1)
753 747 assert_equal '125', issue.custom_value_for(2).value
754 748 old_subject = issue.subject
755 749 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
756 750
757 751 assert_difference('Journal.count') do
758 752 assert_difference('JournalDetail.count', 2) do
759 753 post :edit, :id => 1, :issue => {:subject => new_subject,
760 754 :priority_id => '6',
761 755 :category_id => '1' # no change
762 756 }
763 757 end
764 758 end
765 759 assert_redirected_to :action => 'show', :id => '1'
766 760 issue.reload
767 761 assert_equal new_subject, issue.subject
768 762 # Make sure custom fields were not cleared
769 763 assert_equal '125', issue.custom_value_for(2).value
770 764
771 765 mail = ActionMailer::Base.deliveries.last
772 766 assert_kind_of TMail::Mail, mail
773 767 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
774 768 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
775 769 end
776 770
777 771 def test_post_edit_with_custom_field_change
778 772 @request.session[:user_id] = 2
779 773 issue = Issue.find(1)
780 774 assert_equal '125', issue.custom_value_for(2).value
781 775
782 776 assert_difference('Journal.count') do
783 777 assert_difference('JournalDetail.count', 3) do
784 778 post :edit, :id => 1, :issue => {:subject => 'Custom field change',
785 779 :priority_id => '6',
786 780 :category_id => '1', # no change
787 781 :custom_field_values => { '2' => 'New custom value' }
788 782 }
789 783 end
790 784 end
791 785 assert_redirected_to :action => 'show', :id => '1'
792 786 issue.reload
793 787 assert_equal 'New custom value', issue.custom_value_for(2).value
794 788
795 789 mail = ActionMailer::Base.deliveries.last
796 790 assert_kind_of TMail::Mail, mail
797 791 assert mail.body.include?("Searchable field changed from 125 to New custom value")
798 792 end
799 793
800 794 def test_post_edit_with_status_and_assignee_change
801 795 issue = Issue.find(1)
802 796 assert_equal 1, issue.status_id
803 797 @request.session[:user_id] = 2
804 798 assert_difference('TimeEntry.count', 0) do
805 799 post :edit,
806 800 :id => 1,
807 801 :issue => { :status_id => 2, :assigned_to_id => 3 },
808 802 :notes => 'Assigned to dlopper',
809 803 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
810 804 end
811 805 assert_redirected_to :action => 'show', :id => '1'
812 806 issue.reload
813 807 assert_equal 2, issue.status_id
814 808 j = issue.journals.find(:first, :order => 'id DESC')
815 809 assert_equal 'Assigned to dlopper', j.notes
816 810 assert_equal 2, j.details.size
817 811
818 812 mail = ActionMailer::Base.deliveries.last
819 813 assert mail.body.include?("Status changed from New to Assigned")
820 814 # subject should contain the new status
821 815 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
822 816 end
823 817
824 818 def test_post_edit_with_note_only
825 819 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
826 820 # anonymous user
827 821 post :edit,
828 822 :id => 1,
829 823 :notes => notes
830 824 assert_redirected_to :action => 'show', :id => '1'
831 825 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
832 826 assert_equal notes, j.notes
833 827 assert_equal 0, j.details.size
834 828 assert_equal User.anonymous, j.user
835 829
836 830 mail = ActionMailer::Base.deliveries.last
837 831 assert mail.body.include?(notes)
838 832 end
839 833
840 834 def test_post_edit_with_note_and_spent_time
841 835 @request.session[:user_id] = 2
842 836 spent_hours_before = Issue.find(1).spent_hours
843 837 assert_difference('TimeEntry.count') do
844 838 post :edit,
845 839 :id => 1,
846 840 :notes => '2.5 hours added',
847 841 :time_entry => { :hours => '2.5', :comments => '', :activity_id => TimeEntryActivity.first }
848 842 end
849 843 assert_redirected_to :action => 'show', :id => '1'
850 844
851 845 issue = Issue.find(1)
852 846
853 847 j = issue.journals.find(:first, :order => 'id DESC')
854 848 assert_equal '2.5 hours added', j.notes
855 849 assert_equal 0, j.details.size
856 850
857 851 t = issue.time_entries.find(:first, :order => 'id DESC')
858 852 assert_not_nil t
859 853 assert_equal 2.5, t.hours
860 854 assert_equal spent_hours_before + 2.5, issue.spent_hours
861 855 end
862 856
863 857 def test_post_edit_with_attachment_only
864 858 set_tmp_attachments_directory
865 859
866 860 # Delete all fixtured journals, a race condition can occur causing the wrong
867 861 # journal to get fetched in the next find.
868 862 Journal.delete_all
869 863
870 864 # anonymous user
871 865 post :edit,
872 866 :id => 1,
873 867 :notes => '',
874 868 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
875 869 assert_redirected_to :action => 'show', :id => '1'
876 870 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
877 871 assert j.notes.blank?
878 872 assert_equal 1, j.details.size
879 873 assert_equal 'testfile.txt', j.details.first.value
880 874 assert_equal User.anonymous, j.user
881 875
882 876 mail = ActionMailer::Base.deliveries.last
883 877 assert mail.body.include?('testfile.txt')
884 878 end
885 879
886 880 def test_post_edit_with_no_change
887 881 issue = Issue.find(1)
888 882 issue.journals.clear
889 883 ActionMailer::Base.deliveries.clear
890 884
891 885 post :edit,
892 886 :id => 1,
893 887 :notes => ''
894 888 assert_redirected_to :action => 'show', :id => '1'
895 889
896 890 issue.reload
897 891 assert issue.journals.empty?
898 892 # No email should be sent
899 893 assert ActionMailer::Base.deliveries.empty?
900 894 end
901 895
902 896 def test_post_edit_should_send_a_notification
903 897 @request.session[:user_id] = 2
904 898 ActionMailer::Base.deliveries.clear
905 899 issue = Issue.find(1)
906 900 old_subject = issue.subject
907 901 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
908 902
909 903 post :edit, :id => 1, :issue => {:subject => new_subject,
910 904 :priority_id => '6',
911 905 :category_id => '1' # no change
912 906 }
913 907 assert_equal 1, ActionMailer::Base.deliveries.size
914 908 end
915 909
916 910 def test_post_edit_with_invalid_spent_time
917 911 @request.session[:user_id] = 2
918 912 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
919 913
920 914 assert_no_difference('Journal.count') do
921 915 post :edit,
922 916 :id => 1,
923 917 :notes => notes,
924 918 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
925 919 end
926 920 assert_response :success
927 921 assert_template 'edit'
928 922
929 923 assert_tag :textarea, :attributes => { :name => 'notes' },
930 924 :content => notes
931 925 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
932 926 end
933 927
934 928 def test_get_bulk_edit
935 929 @request.session[:user_id] = 2
936 930 get :bulk_edit, :ids => [1, 2]
937 931 assert_response :success
938 932 assert_template 'bulk_edit'
939 933 end
940 934
941 935 def test_bulk_edit
942 936 @request.session[:user_id] = 2
943 937 # update issues priority
944 938 post :bulk_edit, :ids => [1, 2], :priority_id => 7,
945 939 :assigned_to_id => '',
946 940 :custom_field_values => {'2' => ''},
947 941 :notes => 'Bulk editing'
948 942 assert_response 302
949 943 # check that the issues were updated
950 944 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
951 945
952 946 issue = Issue.find(1)
953 947 journal = issue.journals.find(:first, :order => 'created_on DESC')
954 948 assert_equal '125', issue.custom_value_for(2).value
955 949 assert_equal 'Bulk editing', journal.notes
956 950 assert_equal 1, journal.details.size
957 951 end
958 952
959 953 def test_bullk_edit_should_send_a_notification
960 954 @request.session[:user_id] = 2
961 955 ActionMailer::Base.deliveries.clear
962 956 post(:bulk_edit,
963 957 {
964 958 :ids => [1, 2],
965 959 :priority_id => 7,
966 960 :assigned_to_id => '',
967 961 :custom_field_values => {'2' => ''},
968 962 :notes => 'Bulk editing'
969 963 })
970 964
971 965 assert_response 302
972 966 assert_equal 2, ActionMailer::Base.deliveries.size
973 967 end
974 968
975 969 def test_bulk_edit_status
976 970 @request.session[:user_id] = 2
977 971 # update issues priority
978 972 post :bulk_edit, :ids => [1, 2], :priority_id => '',
979 973 :assigned_to_id => '',
980 974 :status_id => '5',
981 975 :notes => 'Bulk editing status'
982 976 assert_response 302
983 977 issue = Issue.find(1)
984 978 assert issue.closed?
985 979 end
986 980
987 981 def test_bulk_edit_custom_field
988 982 @request.session[:user_id] = 2
989 983 # update issues priority
990 984 post :bulk_edit, :ids => [1, 2], :priority_id => '',
991 985 :assigned_to_id => '',
992 986 :custom_field_values => {'2' => '777'},
993 987 :notes => 'Bulk editing custom field'
994 988 assert_response 302
995 989
996 990 issue = Issue.find(1)
997 991 journal = issue.journals.find(:first, :order => 'created_on DESC')
998 992 assert_equal '777', issue.custom_value_for(2).value
999 993 assert_equal 1, journal.details.size
1000 994 assert_equal '125', journal.details.first.old_value
1001 995 assert_equal '777', journal.details.first.value
1002 996 end
1003 997
1004 998 def test_bulk_unassign
1005 999 assert_not_nil Issue.find(2).assigned_to
1006 1000 @request.session[:user_id] = 2
1007 1001 # unassign issues
1008 1002 post :bulk_edit, :ids => [1, 2], :notes => 'Bulk unassigning', :assigned_to_id => 'none'
1009 1003 assert_response 302
1010 1004 # check that the issues were updated
1011 1005 assert_nil Issue.find(2).assigned_to
1012 1006 end
1013 1007
1014 1008 def test_move_routing
1015 1009 assert_routing(
1016 1010 {:method => :get, :path => '/issues/1/move'},
1017 1011 :controller => 'issues', :action => 'move', :id => '1'
1018 1012 )
1019 1013 assert_recognizes(
1020 1014 {:controller => 'issues', :action => 'move', :id => '1'},
1021 1015 {:method => :post, :path => '/issues/1/move'}
1022 1016 )
1023 1017 end
1024 1018
1025 1019 def test_move_one_issue_to_another_project
1026 1020 @request.session[:user_id] = 2
1027 1021 post :move, :id => 1, :new_project_id => 2
1028 1022 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1029 1023 assert_equal 2, Issue.find(1).project_id
1030 1024 end
1031 1025
1032 1026 def test_move_one_issue_to_another_project_should_follow_when_needed
1033 1027 @request.session[:user_id] = 2
1034 1028 post :move, :id => 1, :new_project_id => 2, :follow => '1'
1035 1029 assert_redirected_to '/issues/1'
1036 1030 end
1037 1031
1038 1032 def test_bulk_move_to_another_project
1039 1033 @request.session[:user_id] = 2
1040 1034 post :move, :ids => [1, 2], :new_project_id => 2
1041 1035 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1042 1036 # Issues moved to project 2
1043 1037 assert_equal 2, Issue.find(1).project_id
1044 1038 assert_equal 2, Issue.find(2).project_id
1045 1039 # No tracker change
1046 1040 assert_equal 1, Issue.find(1).tracker_id
1047 1041 assert_equal 2, Issue.find(2).tracker_id
1048 1042 end
1049 1043
1050 1044 def test_bulk_move_to_another_tracker
1051 1045 @request.session[:user_id] = 2
1052 1046 post :move, :ids => [1, 2], :new_tracker_id => 2
1053 1047 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1054 1048 assert_equal 2, Issue.find(1).tracker_id
1055 1049 assert_equal 2, Issue.find(2).tracker_id
1056 1050 end
1057 1051
1058 1052 def test_bulk_copy_to_another_project
1059 1053 @request.session[:user_id] = 2
1060 1054 assert_difference 'Issue.count', 2 do
1061 1055 assert_no_difference 'Project.find(1).issues.count' do
1062 1056 post :move, :ids => [1, 2], :new_project_id => 2, :copy_options => {:copy => '1'}
1063 1057 end
1064 1058 end
1065 1059 assert_redirected_to 'projects/ecookbook/issues'
1066 1060 end
1067 1061
1068 1062 def test_copy_to_another_project_should_follow_when_needed
1069 1063 @request.session[:user_id] = 2
1070 1064 post :move, :ids => [1], :new_project_id => 2, :copy_options => {:copy => '1'}, :follow => '1'
1071 1065 issue = Issue.first(:order => 'id DESC')
1072 1066 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
1073 1067 end
1074 1068
1075 1069 def test_context_menu_one_issue
1076 1070 @request.session[:user_id] = 2
1077 1071 get :context_menu, :ids => [1]
1078 1072 assert_response :success
1079 1073 assert_template 'context_menu'
1080 1074 assert_tag :tag => 'a', :content => 'Edit',
1081 1075 :attributes => { :href => '/issues/1/edit',
1082 1076 :class => 'icon-edit' }
1083 1077 assert_tag :tag => 'a', :content => 'Closed',
1084 1078 :attributes => { :href => '/issues/1/edit?issue%5Bstatus_id%5D=5',
1085 1079 :class => '' }
1086 1080 assert_tag :tag => 'a', :content => 'Immediate',
1087 1081 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;priority_id=8',
1088 1082 :class => '' }
1089 1083 assert_tag :tag => 'a', :content => 'Dave Lopper',
1090 1084 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1',
1091 1085 :class => '' }
1092 1086 assert_tag :tag => 'a', :content => 'Copy',
1093 1087 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
1094 1088 :class => 'icon-copy' }
1095 1089 assert_tag :tag => 'a', :content => 'Move',
1096 1090 :attributes => { :href => '/issues/move?ids%5B%5D=1',
1097 1091 :class => 'icon-move' }
1098 1092 assert_tag :tag => 'a', :content => 'Delete',
1099 1093 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
1100 1094 :class => 'icon-del' }
1101 1095 end
1102 1096
1103 1097 def test_context_menu_one_issue_by_anonymous
1104 1098 get :context_menu, :ids => [1]
1105 1099 assert_response :success
1106 1100 assert_template 'context_menu'
1107 1101 assert_tag :tag => 'a', :content => 'Delete',
1108 1102 :attributes => { :href => '#',
1109 1103 :class => 'icon-del disabled' }
1110 1104 end
1111 1105
1112 1106 def test_context_menu_multiple_issues_of_same_project
1113 1107 @request.session[:user_id] = 2
1114 1108 get :context_menu, :ids => [1, 2]
1115 1109 assert_response :success
1116 1110 assert_template 'context_menu'
1117 1111 assert_tag :tag => 'a', :content => 'Edit',
1118 1112 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
1119 1113 :class => 'icon-edit' }
1120 1114 assert_tag :tag => 'a', :content => 'Immediate',
1121 1115 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;priority_id=8',
1122 1116 :class => '' }
1123 1117 assert_tag :tag => 'a', :content => 'Dave Lopper',
1124 1118 :attributes => { :href => '/issues/bulk_edit?assigned_to_id=3&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
1125 1119 :class => '' }
1126 1120 assert_tag :tag => 'a', :content => 'Move',
1127 1121 :attributes => { :href => '/issues/move?ids%5B%5D=1&amp;ids%5B%5D=2',
1128 1122 :class => 'icon-move' }
1129 1123 assert_tag :tag => 'a', :content => 'Delete',
1130 1124 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
1131 1125 :class => 'icon-del' }
1132 1126 end
1133 1127
1134 1128 def test_context_menu_multiple_issues_of_different_project
1135 1129 @request.session[:user_id] = 2
1136 1130 get :context_menu, :ids => [1, 2, 4]
1137 1131 assert_response :success
1138 1132 assert_template 'context_menu'
1139 1133 assert_tag :tag => 'a', :content => 'Delete',
1140 1134 :attributes => { :href => '#',
1141 1135 :class => 'icon-del disabled' }
1142 1136 end
1143 1137
1144 1138 def test_destroy_routing
1145 1139 assert_recognizes( #TODO: use DELETE on issue URI (need to change forms)
1146 1140 {:controller => 'issues', :action => 'destroy', :id => '1'},
1147 1141 {:method => :post, :path => '/issues/1/destroy'}
1148 1142 )
1149 1143 end
1150 1144
1151 1145 def test_destroy_issue_with_no_time_entries
1152 1146 assert_nil TimeEntry.find_by_issue_id(2)
1153 1147 @request.session[:user_id] = 2
1154 1148 post :destroy, :id => 2
1155 1149 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1156 1150 assert_nil Issue.find_by_id(2)
1157 1151 end
1158 1152
1159 1153 def test_destroy_issues_with_time_entries
1160 1154 @request.session[:user_id] = 2
1161 1155 post :destroy, :ids => [1, 3]
1162 1156 assert_response :success
1163 1157 assert_template 'destroy'
1164 1158 assert_not_nil assigns(:hours)
1165 1159 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1166 1160 end
1167 1161
1168 1162 def test_destroy_issues_and_destroy_time_entries
1169 1163 @request.session[:user_id] = 2
1170 1164 post :destroy, :ids => [1, 3], :todo => 'destroy'
1171 1165 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1172 1166 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1173 1167 assert_nil TimeEntry.find_by_id([1, 2])
1174 1168 end
1175 1169
1176 1170 def test_destroy_issues_and_assign_time_entries_to_project
1177 1171 @request.session[:user_id] = 2
1178 1172 post :destroy, :ids => [1, 3], :todo => 'nullify'
1179 1173 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1180 1174 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1181 1175 assert_nil TimeEntry.find(1).issue_id
1182 1176 assert_nil TimeEntry.find(2).issue_id
1183 1177 end
1184 1178
1185 1179 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1186 1180 @request.session[:user_id] = 2
1187 1181 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1188 1182 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1189 1183 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1190 1184 assert_equal 2, TimeEntry.find(1).issue_id
1191 1185 assert_equal 2, TimeEntry.find(2).issue_id
1192 1186 end
1193 1187
1194 1188 def test_default_search_scope
1195 1189 get :index
1196 1190 assert_tag :div, :attributes => {:id => 'quick-search'},
1197 1191 :child => {:tag => 'form',
1198 1192 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1199 1193 end
1200 1194 end
@@ -1,313 +1,339
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class QueryTest < ActiveSupport::TestCase
21 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 23 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
24 24 query = Query.new(:project => nil, :name => '_')
25 25 assert query.available_filters.has_key?('cf_1')
26 26 assert !query.available_filters.has_key?('cf_3')
27 27 end
28 28
29 29 def find_issues_with_query(query)
30 30 Issue.find :all,
31 31 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
32 32 :conditions => query.statement
33 33 end
34 34
35 35 def test_query_with_multiple_custom_fields
36 36 query = Query.find(1)
37 37 assert query.valid?
38 38 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
39 39 issues = find_issues_with_query(query)
40 40 assert_equal 1, issues.length
41 41 assert_equal Issue.find(3), issues.first
42 42 end
43 43
44 44 def test_operator_none
45 45 query = Query.new(:project => Project.find(1), :name => '_')
46 46 query.add_filter('fixed_version_id', '!*', [''])
47 47 query.add_filter('cf_1', '!*', [''])
48 48 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
49 49 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
50 50 find_issues_with_query(query)
51 51 end
52 52
53 53 def test_operator_none_for_integer
54 54 query = Query.new(:project => Project.find(1), :name => '_')
55 55 query.add_filter('estimated_hours', '!*', [''])
56 56 issues = find_issues_with_query(query)
57 57 assert !issues.empty?
58 58 assert issues.all? {|i| !i.estimated_hours}
59 59 end
60 60
61 61 def test_operator_all
62 62 query = Query.new(:project => Project.find(1), :name => '_')
63 63 query.add_filter('fixed_version_id', '*', [''])
64 64 query.add_filter('cf_1', '*', [''])
65 65 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
66 66 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
67 67 find_issues_with_query(query)
68 68 end
69 69
70 70 def test_operator_greater_than
71 71 query = Query.new(:project => Project.find(1), :name => '_')
72 72 query.add_filter('done_ratio', '>=', ['40'])
73 73 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40")
74 74 find_issues_with_query(query)
75 75 end
76 76
77 77 def test_operator_in_more_than
78 78 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
79 79 query = Query.new(:project => Project.find(1), :name => '_')
80 80 query.add_filter('due_date', '>t+', ['15'])
81 81 issues = find_issues_with_query(query)
82 82 assert !issues.empty?
83 83 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
84 84 end
85 85
86 86 def test_operator_in_less_than
87 87 query = Query.new(:project => Project.find(1), :name => '_')
88 88 query.add_filter('due_date', '<t+', ['15'])
89 89 issues = find_issues_with_query(query)
90 90 assert !issues.empty?
91 91 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
92 92 end
93 93
94 94 def test_operator_less_than_ago
95 95 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
96 96 query = Query.new(:project => Project.find(1), :name => '_')
97 97 query.add_filter('due_date', '>t-', ['3'])
98 98 issues = find_issues_with_query(query)
99 99 assert !issues.empty?
100 100 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
101 101 end
102 102
103 103 def test_operator_more_than_ago
104 104 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
105 105 query = Query.new(:project => Project.find(1), :name => '_')
106 106 query.add_filter('due_date', '<t-', ['10'])
107 107 assert query.statement.include?("#{Issue.table_name}.due_date <=")
108 108 issues = find_issues_with_query(query)
109 109 assert !issues.empty?
110 110 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
111 111 end
112 112
113 113 def test_operator_in
114 114 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
115 115 query = Query.new(:project => Project.find(1), :name => '_')
116 116 query.add_filter('due_date', 't+', ['2'])
117 117 issues = find_issues_with_query(query)
118 118 assert !issues.empty?
119 119 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
120 120 end
121 121
122 122 def test_operator_ago
123 123 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
124 124 query = Query.new(:project => Project.find(1), :name => '_')
125 125 query.add_filter('due_date', 't-', ['3'])
126 126 issues = find_issues_with_query(query)
127 127 assert !issues.empty?
128 128 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
129 129 end
130 130
131 131 def test_operator_today
132 132 query = Query.new(:project => Project.find(1), :name => '_')
133 133 query.add_filter('due_date', 't', [''])
134 134 issues = find_issues_with_query(query)
135 135 assert !issues.empty?
136 136 issues.each {|issue| assert_equal Date.today, issue.due_date}
137 137 end
138 138
139 139 def test_operator_this_week_on_date
140 140 query = Query.new(:project => Project.find(1), :name => '_')
141 141 query.add_filter('due_date', 'w', [''])
142 142 find_issues_with_query(query)
143 143 end
144 144
145 145 def test_operator_this_week_on_datetime
146 146 query = Query.new(:project => Project.find(1), :name => '_')
147 147 query.add_filter('created_on', 'w', [''])
148 148 find_issues_with_query(query)
149 149 end
150 150
151 151 def test_operator_contains
152 152 query = Query.new(:project => Project.find(1), :name => '_')
153 153 query.add_filter('subject', '~', ['uNable'])
154 154 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
155 155 result = find_issues_with_query(query)
156 156 assert result.empty?
157 157 result.each {|issue| assert issue.subject.downcase.include?('unable') }
158 158 end
159 159
160 160 def test_operator_does_not_contains
161 161 query = Query.new(:project => Project.find(1), :name => '_')
162 162 query.add_filter('subject', '!~', ['uNable'])
163 163 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
164 164 find_issues_with_query(query)
165 165 end
166 166
167 167 def test_filter_watched_issues
168 168 User.current = User.find(1)
169 169 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
170 170 result = find_issues_with_query(query)
171 171 assert_not_nil result
172 172 assert !result.empty?
173 173 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
174 174 User.current = nil
175 175 end
176 176
177 177 def test_filter_unwatched_issues
178 178 User.current = User.find(1)
179 179 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
180 180 result = find_issues_with_query(query)
181 181 assert_not_nil result
182 182 assert !result.empty?
183 183 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
184 184 User.current = nil
185 185 end
186 186
187 187 def test_default_columns
188 188 q = Query.new
189 189 assert !q.columns.empty?
190 190 end
191 191
192 192 def test_set_column_names
193 193 q = Query.new
194 194 q.column_names = ['tracker', :subject, '', 'unknonw_column']
195 195 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
196 196 c = q.columns.first
197 197 assert q.has_column?(c)
198 198 end
199 199
200 200 def test_groupable_columns_should_include_custom_fields
201 201 q = Query.new
202 202 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
203 203 end
204 204
205 205 def test_default_sort
206 206 q = Query.new
207 207 assert_equal [], q.sort_criteria
208 208 end
209 209
210 210 def test_set_sort_criteria_with_hash
211 211 q = Query.new
212 212 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
213 213 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
214 214 end
215 215
216 216 def test_set_sort_criteria_with_array
217 217 q = Query.new
218 218 q.sort_criteria = [['priority', 'desc'], 'tracker']
219 219 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
220 220 end
221 221
222 222 def test_create_query_with_sort
223 223 q = Query.new(:name => 'Sorted')
224 224 q.sort_criteria = [['priority', 'desc'], 'tracker']
225 225 assert q.save
226 226 q.reload
227 227 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
228 228 end
229 229
230 230 def test_sort_by_string_custom_field_asc
231 231 q = Query.new
232 232 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
233 233 assert c
234 234 assert c.sortable
235 235 issues = Issue.find :all,
236 236 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
237 237 :conditions => q.statement,
238 238 :order => "#{c.sortable} ASC"
239 239 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
240 240 assert !values.empty?
241 241 assert_equal values.sort, values
242 242 end
243 243
244 244 def test_sort_by_string_custom_field_desc
245 245 q = Query.new
246 246 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
247 247 assert c
248 248 assert c.sortable
249 249 issues = Issue.find :all,
250 250 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
251 251 :conditions => q.statement,
252 252 :order => "#{c.sortable} DESC"
253 253 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
254 254 assert !values.empty?
255 255 assert_equal values.sort.reverse, values
256 256 end
257 257
258 258 def test_sort_by_float_custom_field_asc
259 259 q = Query.new
260 260 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
261 261 assert c
262 262 assert c.sortable
263 263 issues = Issue.find :all,
264 264 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
265 265 :conditions => q.statement,
266 266 :order => "#{c.sortable} ASC"
267 267 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
268 268 assert !values.empty?
269 269 assert_equal values.sort, values
270 270 end
271 271
272 272 def test_invalid_query_should_raise_query_statement_invalid_error
273 273 q = Query.new
274 274 assert_raise Query::StatementInvalid do
275 275 q.issues(:conditions => "foo = 1")
276 276 end
277 277 end
278 278
279 def test_issue_count_by_association_group
280 q = Query.new(:name => '_', :group_by => 'assigned_to')
281 count_by_group = q.issue_count_by_group
282 assert_kind_of Hash, count_by_group
283 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
284 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
285 assert count_by_group.has_key?(User.find(3))
286 end
287
288 def test_issue_count_by_list_custom_field_group
289 q = Query.new(:name => '_', :group_by => 'cf_1')
290 count_by_group = q.issue_count_by_group
291 assert_kind_of Hash, count_by_group
292 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
293 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
294 assert count_by_group.has_key?('MySQL')
295 end
296
297 def test_issue_count_by_date_custom_field_group
298 q = Query.new(:name => '_', :group_by => 'cf_8')
299 count_by_group = q.issue_count_by_group
300 assert_kind_of Hash, count_by_group
301 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
302 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
303 end
304
279 305 def test_label_for
280 306 q = Query.new
281 307 assert_equal 'assigned_to', q.label_for('assigned_to_id')
282 308 end
283 309
284 310 def test_editable_by
285 311 admin = User.find(1)
286 312 manager = User.find(2)
287 313 developer = User.find(3)
288 314
289 315 # Public query on project 1
290 316 q = Query.find(1)
291 317 assert q.editable_by?(admin)
292 318 assert q.editable_by?(manager)
293 319 assert !q.editable_by?(developer)
294 320
295 321 # Private query on project 1
296 322 q = Query.find(2)
297 323 assert q.editable_by?(admin)
298 324 assert !q.editable_by?(manager)
299 325 assert q.editable_by?(developer)
300 326
301 327 # Private query for all projects
302 328 q = Query.find(3)
303 329 assert q.editable_by?(admin)
304 330 assert !q.editable_by?(manager)
305 331 assert q.editable_by?(developer)
306 332
307 333 # Public query for all projects
308 334 q = Query.find(4)
309 335 assert q.editable_by?(admin)
310 336 assert !q.editable_by?(manager)
311 337 assert !q.editable_by?(developer)
312 338 end
313 339 end
General Comments 0
You need to be logged in to leave comments. Login now