##// END OF EJS Templates
Fixed: private queries should not be accessible to other users (#8729)....
Jean-Philippe Lang -
r6043:8914d323ee14
parent child
Show More
@@ -1,487 +1,490
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 'uri'
19 19 require 'cgi'
20 20
21 class Unauthorized < Exception; end
22
21 23 class ApplicationController < ActionController::Base
22 24 include Redmine::I18n
23 25
24 26 layout 'base'
25 27 exempt_from_layout 'builder', 'rsb'
26 28
27 29 # Remove broken cookie after upgrade from 0.8.x (#4292)
28 30 # See https://rails.lighthouseapp.com/projects/8994/tickets/3360
29 31 # TODO: remove it when Rails is fixed
30 32 before_filter :delete_broken_cookies
31 33 def delete_broken_cookies
32 34 if cookies['_redmine_session'] && cookies['_redmine_session'] !~ /--/
33 35 cookies.delete '_redmine_session'
34 36 redirect_to home_path
35 37 return false
36 38 end
37 39 end
38 40
39 41 before_filter :user_setup, :check_if_login_required, :set_localization
40 42 filter_parameter_logging :password
41 43 protect_from_forgery
42 44
43 45 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
46 rescue_from ::Unauthorized, :with => :deny_access
44 47
45 48 include Redmine::Search::Controller
46 49 include Redmine::MenuManager::MenuController
47 50 helper Redmine::MenuManager::MenuHelper
48 51
49 52 Redmine::Scm::Base.all.each do |scm|
50 53 require_dependency "repository/#{scm.underscore}"
51 54 end
52 55
53 56 def user_setup
54 57 # Check the settings cache for each request
55 58 Setting.check_cache
56 59 # Find the current user
57 60 User.current = find_current_user
58 61 end
59 62
60 63 # Returns the current user or nil if no user is logged in
61 64 # and starts a session if needed
62 65 def find_current_user
63 66 if session[:user_id]
64 67 # existing session
65 68 (User.active.find(session[:user_id]) rescue nil)
66 69 elsif cookies[:autologin] && Setting.autologin?
67 70 # auto-login feature starts a new session
68 71 user = User.try_to_autologin(cookies[:autologin])
69 72 session[:user_id] = user.id if user
70 73 user
71 74 elsif params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
72 75 # RSS key authentication does not start a session
73 76 User.find_by_rss_key(params[:key])
74 77 elsif Setting.rest_api_enabled? && api_request?
75 78 if (key = api_key_from_request) && accept_key_auth_actions.include?(params[:action])
76 79 # Use API key
77 80 User.find_by_api_key(key)
78 81 else
79 82 # HTTP Basic, either username/password or API key/random
80 83 authenticate_with_http_basic do |username, password|
81 84 User.try_to_login(username, password) || User.find_by_api_key(username)
82 85 end
83 86 end
84 87 end
85 88 end
86 89
87 90 # Sets the logged in user
88 91 def logged_user=(user)
89 92 reset_session
90 93 if user && user.is_a?(User)
91 94 User.current = user
92 95 session[:user_id] = user.id
93 96 else
94 97 User.current = User.anonymous
95 98 end
96 99 end
97 100
98 101 # check if login is globally required to access the application
99 102 def check_if_login_required
100 103 # no check needed if user is already logged in
101 104 return true if User.current.logged?
102 105 require_login if Setting.login_required?
103 106 end
104 107
105 108 def set_localization
106 109 lang = nil
107 110 if User.current.logged?
108 111 lang = find_language(User.current.language)
109 112 end
110 113 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
111 114 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
112 115 if !accept_lang.blank?
113 116 accept_lang = accept_lang.downcase
114 117 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
115 118 end
116 119 end
117 120 lang ||= Setting.default_language
118 121 set_language_if_valid(lang)
119 122 end
120 123
121 124 def require_login
122 125 if !User.current.logged?
123 126 # Extract only the basic url parameters on non-GET requests
124 127 if request.get?
125 128 url = url_for(params)
126 129 else
127 130 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
128 131 end
129 132 respond_to do |format|
130 133 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
131 134 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
132 135 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
133 136 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
134 137 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
135 138 end
136 139 return false
137 140 end
138 141 true
139 142 end
140 143
141 144 def require_admin
142 145 return unless require_login
143 146 if !User.current.admin?
144 147 render_403
145 148 return false
146 149 end
147 150 true
148 151 end
149 152
150 153 def deny_access
151 154 User.current.logged? ? render_403 : require_login
152 155 end
153 156
154 157 # Authorize the user for the requested action
155 158 def authorize(ctrl = params[:controller], action = params[:action], global = false)
156 159 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
157 160 if allowed
158 161 true
159 162 else
160 163 if @project && @project.archived?
161 164 render_403 :message => :notice_not_authorized_archived_project
162 165 else
163 166 deny_access
164 167 end
165 168 end
166 169 end
167 170
168 171 # Authorize the user for the requested action outside a project
169 172 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
170 173 authorize(ctrl, action, global)
171 174 end
172 175
173 176 # Find project of id params[:id]
174 177 def find_project
175 178 @project = Project.find(params[:id])
176 179 rescue ActiveRecord::RecordNotFound
177 180 render_404
178 181 end
179 182
180 183 # Find project of id params[:project_id]
181 184 def find_project_by_project_id
182 185 @project = Project.find(params[:project_id])
183 186 rescue ActiveRecord::RecordNotFound
184 187 render_404
185 188 end
186 189
187 190 # Find a project based on params[:project_id]
188 191 # TODO: some subclasses override this, see about merging their logic
189 192 def find_optional_project
190 193 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
191 194 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
192 195 allowed ? true : deny_access
193 196 rescue ActiveRecord::RecordNotFound
194 197 render_404
195 198 end
196 199
197 200 # Finds and sets @project based on @object.project
198 201 def find_project_from_association
199 202 render_404 unless @object.present?
200 203
201 204 @project = @object.project
202 205 rescue ActiveRecord::RecordNotFound
203 206 render_404
204 207 end
205 208
206 209 def find_model_object
207 210 model = self.class.read_inheritable_attribute('model_object')
208 211 if model
209 212 @object = model.find(params[:id])
210 213 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
211 214 end
212 215 rescue ActiveRecord::RecordNotFound
213 216 render_404
214 217 end
215 218
216 219 def self.model_object(model)
217 220 write_inheritable_attribute('model_object', model)
218 221 end
219 222
220 223 # Filter for bulk issue operations
221 224 def find_issues
222 225 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
223 226 raise ActiveRecord::RecordNotFound if @issues.empty?
224 227 if @issues.detect {|issue| !issue.visible?}
225 228 deny_access
226 229 return
227 230 end
228 231 @projects = @issues.collect(&:project).compact.uniq
229 232 @project = @projects.first if @projects.size == 1
230 233 rescue ActiveRecord::RecordNotFound
231 234 render_404
232 235 end
233 236
234 237 # Check if project is unique before bulk operations
235 238 def check_project_uniqueness
236 239 unless @project
237 240 # TODO: let users bulk edit/move/destroy issues from different projects
238 241 render_error 'Can not bulk edit/move/destroy issues from different projects'
239 242 return false
240 243 end
241 244 end
242 245
243 246 # make sure that the user is a member of the project (or admin) if project is private
244 247 # used as a before_filter for actions that do not require any particular permission on the project
245 248 def check_project_privacy
246 249 if @project && @project.active?
247 250 if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
248 251 true
249 252 else
250 253 User.current.logged? ? render_403 : require_login
251 254 end
252 255 else
253 256 @project = nil
254 257 render_404
255 258 false
256 259 end
257 260 end
258 261
259 262 def back_url
260 263 params[:back_url] || request.env['HTTP_REFERER']
261 264 end
262 265
263 266 def redirect_back_or_default(default)
264 267 back_url = CGI.unescape(params[:back_url].to_s)
265 268 if !back_url.blank?
266 269 begin
267 270 uri = URI.parse(back_url)
268 271 # do not redirect user to another host or to the login or register page
269 272 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
270 273 redirect_to(back_url)
271 274 return
272 275 end
273 276 rescue URI::InvalidURIError
274 277 # redirect to default
275 278 end
276 279 end
277 280 redirect_to default
278 281 false
279 282 end
280 283
281 284 def render_403(options={})
282 285 @project = nil
283 286 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
284 287 return false
285 288 end
286 289
287 290 def render_404(options={})
288 291 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
289 292 return false
290 293 end
291 294
292 295 # Renders an error response
293 296 def render_error(arg)
294 297 arg = {:message => arg} unless arg.is_a?(Hash)
295 298
296 299 @message = arg[:message]
297 300 @message = l(@message) if @message.is_a?(Symbol)
298 301 @status = arg[:status] || 500
299 302
300 303 respond_to do |format|
301 304 format.html {
302 305 render :template => 'common/error', :layout => use_layout, :status => @status
303 306 }
304 307 format.atom { head @status }
305 308 format.xml { head @status }
306 309 format.js { head @status }
307 310 format.json { head @status }
308 311 end
309 312 end
310 313
311 314 # Picks which layout to use based on the request
312 315 #
313 316 # @return [boolean, string] name of the layout to use or false for no layout
314 317 def use_layout
315 318 request.xhr? ? false : 'base'
316 319 end
317 320
318 321 def invalid_authenticity_token
319 322 if api_request?
320 323 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
321 324 end
322 325 render_error "Invalid form authenticity token."
323 326 end
324 327
325 328 def render_feed(items, options={})
326 329 @items = items || []
327 330 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
328 331 @items = @items.slice(0, Setting.feeds_limit.to_i)
329 332 @title = options[:title] || Setting.app_title
330 333 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
331 334 end
332 335
333 336 def self.accept_key_auth(*actions)
334 337 actions = actions.flatten.map(&:to_s)
335 338 write_inheritable_attribute('accept_key_auth_actions', actions)
336 339 end
337 340
338 341 def accept_key_auth_actions
339 342 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
340 343 end
341 344
342 345 # Returns the number of objects that should be displayed
343 346 # on the paginated list
344 347 def per_page_option
345 348 per_page = nil
346 349 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
347 350 per_page = params[:per_page].to_s.to_i
348 351 session[:per_page] = per_page
349 352 elsif session[:per_page]
350 353 per_page = session[:per_page]
351 354 else
352 355 per_page = Setting.per_page_options_array.first || 25
353 356 end
354 357 per_page
355 358 end
356 359
357 360 # Returns offset and limit used to retrieve objects
358 361 # for an API response based on offset, limit and page parameters
359 362 def api_offset_and_limit(options=params)
360 363 if options[:offset].present?
361 364 offset = options[:offset].to_i
362 365 if offset < 0
363 366 offset = 0
364 367 end
365 368 end
366 369 limit = options[:limit].to_i
367 370 if limit < 1
368 371 limit = 25
369 372 elsif limit > 100
370 373 limit = 100
371 374 end
372 375 if offset.nil? && options[:page].present?
373 376 offset = (options[:page].to_i - 1) * limit
374 377 offset = 0 if offset < 0
375 378 end
376 379 offset ||= 0
377 380
378 381 [offset, limit]
379 382 end
380 383
381 384 # qvalues http header parser
382 385 # code taken from webrick
383 386 def parse_qvalues(value)
384 387 tmp = []
385 388 if value
386 389 parts = value.split(/,\s*/)
387 390 parts.each {|part|
388 391 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
389 392 val = m[1]
390 393 q = (m[2] or 1).to_f
391 394 tmp.push([val, q])
392 395 end
393 396 }
394 397 tmp = tmp.sort_by{|val, q| -q}
395 398 tmp.collect!{|val, q| val}
396 399 end
397 400 return tmp
398 401 rescue
399 402 nil
400 403 end
401 404
402 405 # Returns a string that can be used as filename value in Content-Disposition header
403 406 def filename_for_content_disposition(name)
404 407 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
405 408 end
406 409
407 410 def api_request?
408 411 %w(xml json).include? params[:format]
409 412 end
410 413
411 414 # Returns the API key present in the request
412 415 def api_key_from_request
413 416 if params[:key].present?
414 417 params[:key]
415 418 elsif request.headers["X-Redmine-API-Key"].present?
416 419 request.headers["X-Redmine-API-Key"]
417 420 end
418 421 end
419 422
420 423 # Renders a warning flash if obj has unsaved attachments
421 424 def render_attachment_warning_if_needed(obj)
422 425 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
423 426 end
424 427
425 428 # Sets the `flash` notice or error based the number of issues that did not save
426 429 #
427 430 # @param [Array, Issue] issues all of the saved and unsaved Issues
428 431 # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
429 432 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
430 433 if unsaved_issue_ids.empty?
431 434 flash[:notice] = l(:notice_successful_update) unless issues.empty?
432 435 else
433 436 flash[:error] = l(:notice_failed_to_save_issues,
434 437 :count => unsaved_issue_ids.size,
435 438 :total => issues.size,
436 439 :ids => '#' + unsaved_issue_ids.join(', #'))
437 440 end
438 441 end
439 442
440 443 # Rescues an invalid query statement. Just in case...
441 444 def query_statement_invalid(exception)
442 445 logger.error "Query::StatementInvalid: #{exception.message}" if logger
443 446 session.delete(:query)
444 447 sort_clear if respond_to?(:sort_clear)
445 448 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
446 449 end
447 450
448 451 # Converts the errors on an ActiveRecord object into a common JSON format
449 452 def object_errors_to_json(object)
450 453 object.errors.collect do |attribute, error|
451 454 { attribute => error }
452 455 end.to_json
453 456 end
454 457
455 458 # Renders API response on validation failure
456 459 def render_validation_errors(object)
457 460 options = { :status => :unprocessable_entity, :layout => false }
458 461 options.merge!(case params[:format]
459 462 when 'xml'; { :xml => object.errors }
460 463 when 'json'; { :json => {'errors' => object.errors} } # ActiveResource client compliance
461 464 else
462 465 raise "Unknown format #{params[:format]} in #render_validation_errors"
463 466 end
464 467 )
465 468 render options
466 469 end
467 470
468 471 # Overrides #default_template so that the api template
469 472 # is used automatically if it exists
470 473 def default_template(action_name = self.action_name)
471 474 if api_request?
472 475 begin
473 476 return self.view_paths.find_template(default_template_name(action_name), 'api')
474 477 rescue ::ActionView::MissingTemplate
475 478 # the api template was not found
476 479 # fallback to the default behaviour
477 480 end
478 481 end
479 482 super
480 483 end
481 484
482 485 # Overrides #pick_layout so that #render with no arguments
483 486 # doesn't use the layout for api requests
484 487 def pick_layout(*args)
485 488 api_request? ? nil : super
486 489 end
487 490 end
@@ -1,99 +1,100
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 30 def column_content(column, issue)
31 31 value = column.value(issue)
32 32
33 33 case value.class.name
34 34 when 'String'
35 35 if column.name == :subject
36 36 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
37 37 else
38 38 h(value)
39 39 end
40 40 when 'Time'
41 41 format_time(value)
42 42 when 'Date'
43 43 format_date(value)
44 44 when 'Fixnum', 'Float'
45 45 if column.name == :done_ratio
46 46 progress_bar(value, :width => '80px')
47 47 else
48 48 value.to_s
49 49 end
50 50 when 'User'
51 51 link_to_user value
52 52 when 'Project'
53 53 link_to_project value
54 54 when 'Version'
55 55 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
56 56 when 'TrueClass'
57 57 l(:general_text_Yes)
58 58 when 'FalseClass'
59 59 l(:general_text_No)
60 60 when 'Issue'
61 61 link_to_issue(value, :subject => false)
62 62 else
63 63 h(value)
64 64 end
65 65 end
66 66
67 67 # Retrieve query from session or build a new query
68 68 def retrieve_query
69 69 if !params[:query_id].blank?
70 70 cond = "project_id IS NULL"
71 71 cond << " OR project_id = #{@project.id}" if @project
72 72 @query = Query.find(params[:query_id], :conditions => cond)
73 raise ::Unauthorized unless @query.visible?
73 74 @query.project = @project
74 75 session[:query] = {:id => @query.id, :project_id => @query.project_id}
75 76 sort_clear
76 77 else
77 78 if api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
78 79 # Give it a name, required to be valid
79 80 @query = Query.new(:name => "_")
80 81 @query.project = @project
81 82 if params[:fields] || params[:f]
82 83 @query.filters = {}
83 84 @query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
84 85 else
85 86 @query.available_filters.keys.each do |field|
86 87 @query.add_short_filter(field, params[field]) if params[field]
87 88 end
88 89 end
89 90 @query.group_by = params[:group_by]
90 91 @query.column_names = params[:c] || (params[:query] && params[:query][:column_names])
91 92 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
92 93 else
93 94 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
94 95 @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
95 96 @query.project = @project
96 97 end
97 98 end
98 99 end
99 100 end
@@ -1,673 +1,678
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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 @caption_key = options[:caption] || "field_#{name}"
31 31 end
32 32
33 33 def caption
34 34 l(@caption_key)
35 35 end
36 36
37 37 # Returns true if the column is sortable, otherwise false
38 38 def sortable?
39 39 !sortable.nil?
40 40 end
41 41
42 42 def value(issue)
43 43 issue.send name
44 44 end
45 45
46 46 def css_classes
47 47 name
48 48 end
49 49 end
50 50
51 51 class QueryCustomFieldColumn < QueryColumn
52 52
53 53 def initialize(custom_field)
54 54 self.name = "cf_#{custom_field.id}".to_sym
55 55 self.sortable = custom_field.order_statement || false
56 56 if %w(list date bool int).include?(custom_field.field_format)
57 57 self.groupable = custom_field.order_statement
58 58 end
59 59 self.groupable ||= false
60 60 @cf = custom_field
61 61 end
62 62
63 63 def caption
64 64 @cf.name
65 65 end
66 66
67 67 def custom_field
68 68 @cf
69 69 end
70 70
71 71 def value(issue)
72 72 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
73 73 cv && @cf.cast_value(cv.value)
74 74 end
75 75
76 76 def css_classes
77 77 @css_classes ||= "#{name} #{@cf.field_format}"
78 78 end
79 79 end
80 80
81 81 class Query < ActiveRecord::Base
82 82 class StatementInvalid < ::ActiveRecord::StatementInvalid
83 83 end
84 84
85 85 belongs_to :project
86 86 belongs_to :user
87 87 serialize :filters
88 88 serialize :column_names
89 89 serialize :sort_criteria, Array
90 90
91 91 attr_protected :project_id, :user_id
92 92
93 93 validates_presence_of :name, :on => :save
94 94 validates_length_of :name, :maximum => 255
95 95
96 96 @@operators = { "=" => :label_equals,
97 97 "!" => :label_not_equals,
98 98 "o" => :label_open_issues,
99 99 "c" => :label_closed_issues,
100 100 "!*" => :label_none,
101 101 "*" => :label_all,
102 102 ">=" => :label_greater_or_equal,
103 103 "<=" => :label_less_or_equal,
104 104 "<t+" => :label_in_less_than,
105 105 ">t+" => :label_in_more_than,
106 106 "t+" => :label_in,
107 107 "t" => :label_today,
108 108 "w" => :label_this_week,
109 109 ">t-" => :label_less_than_ago,
110 110 "<t-" => :label_more_than_ago,
111 111 "t-" => :label_ago,
112 112 "~" => :label_contains,
113 113 "!~" => :label_not_contains }
114 114
115 115 cattr_reader :operators
116 116
117 117 @@operators_by_filter_type = { :list => [ "=", "!" ],
118 118 :list_status => [ "o", "=", "!", "c", "*" ],
119 119 :list_optional => [ "=", "!", "!*", "*" ],
120 120 :list_subprojects => [ "*", "!*", "=" ],
121 121 :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
122 122 :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
123 123 :string => [ "=", "~", "!", "!~" ],
124 124 :text => [ "~", "!~" ],
125 125 :integer => [ "=", ">=", "<=", "!*", "*" ] }
126 126
127 127 cattr_reader :operators_by_filter_type
128 128
129 129 @@available_columns = [
130 130 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
131 131 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
132 132 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
133 133 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
134 134 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
135 135 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
136 136 QueryColumn.new(:author),
137 137 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
138 138 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
139 139 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
140 140 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
141 141 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
142 142 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
143 143 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
144 144 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
145 145 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
146 146 ]
147 147 cattr_reader :available_columns
148 148
149 149 def initialize(attributes = nil)
150 150 super attributes
151 151 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
152 152 end
153 153
154 154 def after_initialize
155 155 # Store the fact that project is nil (used in #editable_by?)
156 156 @is_for_all = project.nil?
157 157 end
158 158
159 159 def validate
160 160 filters.each_key do |field|
161 161 errors.add label_for(field), :blank unless
162 162 # filter requires one or more values
163 163 (values_for(field) and !values_for(field).first.blank?) or
164 164 # filter doesn't require any value
165 165 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
166 166 end if filters
167 167 end
168
169 # Returns true if the query is visible to +user+ or the current user.
170 def visible?(user=User.current)
171 self.is_public? || self.user_id == user.id
172 end
168 173
169 174 def editable_by?(user)
170 175 return false unless user
171 176 # Admin can edit them all and regular users can edit their private queries
172 177 return true if user.admin? || (!is_public && self.user_id == user.id)
173 178 # Members can not edit public queries that are for all project (only admin is allowed to)
174 179 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
175 180 end
176 181
177 182 def available_filters
178 183 return @available_filters if @available_filters
179 184
180 185 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
181 186
182 187 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
183 188 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
184 189 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
185 190 "subject" => { :type => :text, :order => 8 },
186 191 "created_on" => { :type => :date_past, :order => 9 },
187 192 "updated_on" => { :type => :date_past, :order => 10 },
188 193 "start_date" => { :type => :date, :order => 11 },
189 194 "due_date" => { :type => :date, :order => 12 },
190 195 "estimated_hours" => { :type => :integer, :order => 13 },
191 196 "done_ratio" => { :type => :integer, :order => 14 }}
192 197
193 198 user_values = []
194 199 user_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
195 200 if project
196 201 user_values += project.users.sort.collect{|s| [s.name, s.id.to_s] }
197 202 else
198 203 all_projects = Project.visible.all
199 204 if all_projects.any?
200 205 # members of visible projects
201 206 user_values += User.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort.collect{|s| [s.name, s.id.to_s] }
202 207
203 208 # project filter
204 209 project_values = []
205 210 Project.project_tree(all_projects) do |p, level|
206 211 prefix = (level > 0 ? ('--' * level + ' ') : '')
207 212 project_values << ["#{prefix}#{p.name}", p.id.to_s]
208 213 end
209 214 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
210 215 end
211 216 end
212 217 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => user_values } unless user_values.empty?
213 218 @available_filters["author_id"] = { :type => :list, :order => 5, :values => user_values } unless user_values.empty?
214 219
215 220 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
216 221 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
217 222
218 223 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
219 224 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
220 225
221 226 if User.current.logged?
222 227 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
223 228 end
224 229
225 230 if project
226 231 # project specific filters
227 232 categories = @project.issue_categories.all
228 233 unless categories.empty?
229 234 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
230 235 end
231 236 versions = @project.shared_versions.all
232 237 unless versions.empty?
233 238 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
234 239 end
235 240 unless @project.leaf?
236 241 subprojects = @project.descendants.visible.all
237 242 unless subprojects.empty?
238 243 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
239 244 end
240 245 end
241 246 add_custom_fields_filters(@project.all_issue_custom_fields)
242 247 else
243 248 # global filters for cross project issue list
244 249 system_shared_versions = Version.visible.find_all_by_sharing('system')
245 250 unless system_shared_versions.empty?
246 251 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
247 252 end
248 253 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
249 254 end
250 255 @available_filters
251 256 end
252 257
253 258 def add_filter(field, operator, values)
254 259 # values must be an array
255 260 return unless values and values.is_a? Array # and !values.first.empty?
256 261 # check if field is defined as an available filter
257 262 if available_filters.has_key? field
258 263 filter_options = available_filters[field]
259 264 # check if operator is allowed for that filter
260 265 #if @@operators_by_filter_type[filter_options[:type]].include? operator
261 266 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
262 267 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
263 268 #end
264 269 filters[field] = {:operator => operator, :values => values }
265 270 end
266 271 end
267 272
268 273 def add_short_filter(field, expression)
269 274 return unless expression
270 275 parms = expression.scan(/^(o|c|!\*|!|\*)?(.*)$/).first
271 276 add_filter field, (parms[0] || "="), [parms[1] || ""]
272 277 end
273 278
274 279 # Add multiple filters using +add_filter+
275 280 def add_filters(fields, operators, values)
276 281 if fields.is_a?(Array) && operators.is_a?(Hash) && values.is_a?(Hash)
277 282 fields.each do |field|
278 283 add_filter(field, operators[field], values[field])
279 284 end
280 285 end
281 286 end
282 287
283 288 def has_filter?(field)
284 289 filters and filters[field]
285 290 end
286 291
287 292 def operator_for(field)
288 293 has_filter?(field) ? filters[field][:operator] : nil
289 294 end
290 295
291 296 def values_for(field)
292 297 has_filter?(field) ? filters[field][:values] : nil
293 298 end
294 299
295 300 def label_for(field)
296 301 label = available_filters[field][:name] if available_filters.has_key?(field)
297 302 label ||= field.gsub(/\_id$/, "")
298 303 end
299 304
300 305 def available_columns
301 306 return @available_columns if @available_columns
302 307 @available_columns = Query.available_columns
303 308 @available_columns += (project ?
304 309 project.all_issue_custom_fields :
305 310 IssueCustomField.find(:all)
306 311 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
307 312 end
308 313
309 314 def self.available_columns=(v)
310 315 self.available_columns = (v)
311 316 end
312 317
313 318 def self.add_available_column(column)
314 319 self.available_columns << (column) if column.is_a?(QueryColumn)
315 320 end
316 321
317 322 # Returns an array of columns that can be used to group the results
318 323 def groupable_columns
319 324 available_columns.select {|c| c.groupable}
320 325 end
321 326
322 327 # Returns a Hash of columns and the key for sorting
323 328 def sortable_columns
324 329 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
325 330 h[column.name.to_s] = column.sortable
326 331 h
327 332 })
328 333 end
329 334
330 335 def columns
331 336 if has_default_columns?
332 337 available_columns.select do |c|
333 338 # Adds the project column by default for cross-project lists
334 339 Setting.issue_list_default_columns.include?(c.name.to_s) || (c.name == :project && project.nil?)
335 340 end
336 341 else
337 342 # preserve the column_names order
338 343 column_names.collect {|name| available_columns.find {|col| col.name == name}}.compact
339 344 end
340 345 end
341 346
342 347 def column_names=(names)
343 348 if names
344 349 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
345 350 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
346 351 # Set column_names to nil if default columns
347 352 if names.map(&:to_s) == Setting.issue_list_default_columns
348 353 names = nil
349 354 end
350 355 end
351 356 write_attribute(:column_names, names)
352 357 end
353 358
354 359 def has_column?(column)
355 360 column_names && column_names.include?(column.name)
356 361 end
357 362
358 363 def has_default_columns?
359 364 column_names.nil? || column_names.empty?
360 365 end
361 366
362 367 def sort_criteria=(arg)
363 368 c = []
364 369 if arg.is_a?(Hash)
365 370 arg = arg.keys.sort.collect {|k| arg[k]}
366 371 end
367 372 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
368 373 write_attribute(:sort_criteria, c)
369 374 end
370 375
371 376 def sort_criteria
372 377 read_attribute(:sort_criteria) || []
373 378 end
374 379
375 380 def sort_criteria_key(arg)
376 381 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
377 382 end
378 383
379 384 def sort_criteria_order(arg)
380 385 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
381 386 end
382 387
383 388 # Returns the SQL sort order that should be prepended for grouping
384 389 def group_by_sort_order
385 390 if grouped? && (column = group_by_column)
386 391 column.sortable.is_a?(Array) ?
387 392 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
388 393 "#{column.sortable} #{column.default_order}"
389 394 end
390 395 end
391 396
392 397 # Returns true if the query is a grouped query
393 398 def grouped?
394 399 !group_by_column.nil?
395 400 end
396 401
397 402 def group_by_column
398 403 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
399 404 end
400 405
401 406 def group_by_statement
402 407 group_by_column.try(:groupable)
403 408 end
404 409
405 410 def project_statement
406 411 project_clauses = []
407 412 if project && !@project.descendants.active.empty?
408 413 ids = [project.id]
409 414 if has_filter?("subproject_id")
410 415 case operator_for("subproject_id")
411 416 when '='
412 417 # include the selected subprojects
413 418 ids += values_for("subproject_id").each(&:to_i)
414 419 when '!*'
415 420 # main project only
416 421 else
417 422 # all subprojects
418 423 ids += project.descendants.collect(&:id)
419 424 end
420 425 elsif Setting.display_subprojects_issues?
421 426 ids += project.descendants.collect(&:id)
422 427 end
423 428 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
424 429 elsif project
425 430 project_clauses << "#{Project.table_name}.id = %d" % project.id
426 431 end
427 432 project_clauses.any? ? project_clauses.join(' AND ') : nil
428 433 end
429 434
430 435 def statement
431 436 # filters clauses
432 437 filters_clauses = []
433 438 filters.each_key do |field|
434 439 next if field == "subproject_id"
435 440 v = values_for(field).clone
436 441 next unless v and !v.empty?
437 442 operator = operator_for(field)
438 443
439 444 # "me" value subsitution
440 445 if %w(assigned_to_id author_id watcher_id).include?(field)
441 446 v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
442 447 end
443 448
444 449 sql = ''
445 450 if field =~ /^cf_(\d+)$/
446 451 # custom field
447 452 db_table = CustomValue.table_name
448 453 db_field = 'value'
449 454 is_custom_filter = true
450 455 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 "
451 456 sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
452 457 elsif field == 'watcher_id'
453 458 db_table = Watcher.table_name
454 459 db_field = 'user_id'
455 460 sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
456 461 sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
457 462 elsif field == "member_of_group" # named field
458 463 if operator == '*' # Any group
459 464 groups = Group.all
460 465 operator = '=' # Override the operator since we want to find by assigned_to
461 466 elsif operator == "!*"
462 467 groups = Group.all
463 468 operator = '!' # Override the operator since we want to find by assigned_to
464 469 else
465 470 groups = Group.find_all_by_id(v)
466 471 end
467 472 groups ||= []
468 473
469 474 members_of_groups = groups.inject([]) {|user_ids, group|
470 475 if group && group.user_ids.present?
471 476 user_ids << group.user_ids
472 477 end
473 478 user_ids.flatten.uniq.compact
474 479 }.sort.collect(&:to_s)
475 480
476 481 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
477 482
478 483 elsif field == "assigned_to_role" # named field
479 484 if operator == "*" # Any Role
480 485 roles = Role.givable
481 486 operator = '=' # Override the operator since we want to find by assigned_to
482 487 elsif operator == "!*" # No role
483 488 roles = Role.givable
484 489 operator = '!' # Override the operator since we want to find by assigned_to
485 490 else
486 491 roles = Role.givable.find_all_by_id(v)
487 492 end
488 493 roles ||= []
489 494
490 495 members_of_roles = roles.inject([]) {|user_ids, role|
491 496 if role && role.members
492 497 user_ids << role.members.collect(&:user_id)
493 498 end
494 499 user_ids.flatten.uniq.compact
495 500 }.sort.collect(&:to_s)
496 501
497 502 sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
498 503 else
499 504 # regular field
500 505 db_table = Issue.table_name
501 506 db_field = field
502 507 sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
503 508 end
504 509 filters_clauses << sql
505 510
506 511 end if filters and valid?
507 512
508 513 filters_clauses << project_statement
509 514 filters_clauses.reject!(&:blank?)
510 515
511 516 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
512 517 end
513 518
514 519 # Returns the issue count
515 520 def issue_count
516 521 Issue.count(:include => [:status, :project], :conditions => statement)
517 522 rescue ::ActiveRecord::StatementInvalid => e
518 523 raise StatementInvalid.new(e.message)
519 524 end
520 525
521 526 # Returns the issue count by group or nil if query is not grouped
522 527 def issue_count_by_group
523 528 r = nil
524 529 if grouped?
525 530 begin
526 531 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
527 532 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
528 533 rescue ActiveRecord::RecordNotFound
529 534 r = {nil => issue_count}
530 535 end
531 536 c = group_by_column
532 537 if c.is_a?(QueryCustomFieldColumn)
533 538 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
534 539 end
535 540 end
536 541 r
537 542 rescue ::ActiveRecord::StatementInvalid => e
538 543 raise StatementInvalid.new(e.message)
539 544 end
540 545
541 546 # Returns the issues
542 547 # Valid options are :order, :offset, :limit, :include, :conditions
543 548 def issues(options={})
544 549 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
545 550 order_option = nil if order_option.blank?
546 551
547 552 Issue.visible.find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
548 553 :conditions => Query.merge_conditions(statement, options[:conditions]),
549 554 :order => order_option,
550 555 :limit => options[:limit],
551 556 :offset => options[:offset]
552 557 rescue ::ActiveRecord::StatementInvalid => e
553 558 raise StatementInvalid.new(e.message)
554 559 end
555 560
556 561 # Returns the journals
557 562 # Valid options are :order, :offset, :limit
558 563 def journals(options={})
559 564 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
560 565 :conditions => statement,
561 566 :order => options[:order],
562 567 :limit => options[:limit],
563 568 :offset => options[:offset]
564 569 rescue ::ActiveRecord::StatementInvalid => e
565 570 raise StatementInvalid.new(e.message)
566 571 end
567 572
568 573 # Returns the versions
569 574 # Valid options are :conditions
570 575 def versions(options={})
571 576 Version.visible.find :all, :include => :project,
572 577 :conditions => Query.merge_conditions(project_statement, options[:conditions])
573 578 rescue ::ActiveRecord::StatementInvalid => e
574 579 raise StatementInvalid.new(e.message)
575 580 end
576 581
577 582 private
578 583
579 584 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
580 585 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
581 586 sql = ''
582 587 case operator
583 588 when "="
584 589 if value.any?
585 590 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
586 591 else
587 592 # IN an empty set
588 593 sql = "1=0"
589 594 end
590 595 when "!"
591 596 if value.any?
592 597 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
593 598 else
594 599 # NOT IN an empty set
595 600 sql = "1=1"
596 601 end
597 602 when "!*"
598 603 sql = "#{db_table}.#{db_field} IS NULL"
599 604 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
600 605 when "*"
601 606 sql = "#{db_table}.#{db_field} IS NOT NULL"
602 607 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
603 608 when ">="
604 609 sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
605 610 when "<="
606 611 sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
607 612 when "o"
608 613 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
609 614 when "c"
610 615 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
611 616 when ">t-"
612 617 sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
613 618 when "<t-"
614 619 sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
615 620 when "t-"
616 621 sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
617 622 when ">t+"
618 623 sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
619 624 when "<t+"
620 625 sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
621 626 when "t+"
622 627 sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
623 628 when "t"
624 629 sql = date_range_clause(db_table, db_field, 0, 0)
625 630 when "w"
626 631 first_day_of_week = l(:general_first_day_of_week).to_i
627 632 day_of_week = Date.today.cwday
628 633 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
629 634 sql = date_range_clause(db_table, db_field, - days_ago, - days_ago + 6)
630 635 when "~"
631 636 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
632 637 when "!~"
633 638 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
634 639 end
635 640
636 641 return sql
637 642 end
638 643
639 644 def add_custom_fields_filters(custom_fields)
640 645 @available_filters ||= {}
641 646
642 647 custom_fields.select(&:is_filter?).each do |field|
643 648 case field.field_format
644 649 when "text"
645 650 options = { :type => :text, :order => 20 }
646 651 when "list"
647 652 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
648 653 when "date"
649 654 options = { :type => :date, :order => 20 }
650 655 when "bool"
651 656 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
652 657 when "user", "version"
653 658 next unless project
654 659 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20}
655 660 else
656 661 options = { :type => :string, :order => 20 }
657 662 end
658 663 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
659 664 end
660 665 end
661 666
662 667 # Returns a SQL clause for a date or datetime field.
663 668 def date_range_clause(table, field, from, to)
664 669 s = []
665 670 if from
666 671 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
667 672 end
668 673 if to
669 674 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
670 675 end
671 676 s.join(' AND ')
672 677 end
673 678 end
@@ -1,1490 +1,1511
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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.expand_path('../../test_helper', __FILE__)
19 19 require 'issues_controller'
20 20
21 # Re-raise errors caught by the controller.
22 class IssuesController; def rescue_action(e) raise e end; end
23
24 21 class IssuesControllerTest < ActionController::TestCase
25 22 fixtures :projects,
26 23 :users,
27 24 :roles,
28 25 :members,
29 26 :member_roles,
30 27 :issues,
31 28 :issue_statuses,
32 29 :versions,
33 30 :trackers,
34 31 :projects_trackers,
35 32 :issue_categories,
36 33 :enabled_modules,
37 34 :enumerations,
38 35 :attachments,
39 36 :workflows,
40 37 :custom_fields,
41 38 :custom_values,
42 39 :custom_fields_projects,
43 40 :custom_fields_trackers,
44 41 :time_entries,
45 42 :journals,
46 43 :journal_details,
47 44 :queries
48 45
49 46 def setup
50 47 @controller = IssuesController.new
51 48 @request = ActionController::TestRequest.new
52 49 @response = ActionController::TestResponse.new
53 50 User.current = nil
54 51 end
55 52
56 53 def test_index
57 54 Setting.default_language = 'en'
58 55
59 56 get :index
60 57 assert_response :success
61 58 assert_template 'index.rhtml'
62 59 assert_not_nil assigns(:issues)
63 60 assert_nil assigns(:project)
64 61 assert_tag :tag => 'a', :content => /Can't print recipes/
65 62 assert_tag :tag => 'a', :content => /Subproject issue/
66 63 # private projects hidden
67 64 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
68 65 assert_no_tag :tag => 'a', :content => /Issue on project 2/
69 66 # project column
70 67 assert_tag :tag => 'th', :content => /Project/
71 68 end
72 69
73 70 def test_index_should_not_list_issues_when_module_disabled
74 71 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
75 72 get :index
76 73 assert_response :success
77 74 assert_template 'index.rhtml'
78 75 assert_not_nil assigns(:issues)
79 76 assert_nil assigns(:project)
80 77 assert_no_tag :tag => 'a', :content => /Can't print recipes/
81 78 assert_tag :tag => 'a', :content => /Subproject issue/
82 79 end
83 80
84 81 def test_index_should_not_list_issues_when_module_disabled
85 82 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
86 83 get :index
87 84 assert_response :success
88 85 assert_template 'index.rhtml'
89 86 assert_not_nil assigns(:issues)
90 87 assert_nil assigns(:project)
91 88 assert_no_tag :tag => 'a', :content => /Can't print recipes/
92 89 assert_tag :tag => 'a', :content => /Subproject issue/
93 90 end
94 91
95 92 def test_index_should_list_visible_issues_only
96 93 get :index, :per_page => 100
97 94 assert_response :success
98 95 assert_not_nil assigns(:issues)
99 96 assert_nil assigns(:issues).detect {|issue| !issue.visible?}
100 97 end
101 98
102 99 def test_index_with_project
103 100 Setting.display_subprojects_issues = 0
104 101 get :index, :project_id => 1
105 102 assert_response :success
106 103 assert_template 'index.rhtml'
107 104 assert_not_nil assigns(:issues)
108 105 assert_tag :tag => 'a', :content => /Can't print recipes/
109 106 assert_no_tag :tag => 'a', :content => /Subproject issue/
110 107 end
111 108
112 109 def test_index_with_project_and_subprojects
113 110 Setting.display_subprojects_issues = 1
114 111 get :index, :project_id => 1
115 112 assert_response :success
116 113 assert_template 'index.rhtml'
117 114 assert_not_nil assigns(:issues)
118 115 assert_tag :tag => 'a', :content => /Can't print recipes/
119 116 assert_tag :tag => 'a', :content => /Subproject issue/
120 117 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
121 118 end
122 119
123 120 def test_index_with_project_and_subprojects_should_show_private_subprojects
124 121 @request.session[:user_id] = 2
125 122 Setting.display_subprojects_issues = 1
126 123 get :index, :project_id => 1
127 124 assert_response :success
128 125 assert_template 'index.rhtml'
129 126 assert_not_nil assigns(:issues)
130 127 assert_tag :tag => 'a', :content => /Can't print recipes/
131 128 assert_tag :tag => 'a', :content => /Subproject issue/
132 129 assert_tag :tag => 'a', :content => /Issue of a private subproject/
133 130 end
134 131
135 132 def test_index_with_project_and_default_filter
136 133 get :index, :project_id => 1, :set_filter => 1
137 134 assert_response :success
138 135 assert_template 'index.rhtml'
139 136 assert_not_nil assigns(:issues)
140 137
141 138 query = assigns(:query)
142 139 assert_not_nil query
143 140 # default filter
144 141 assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters)
145 142 end
146 143
147 144 def test_index_with_project_and_filter
148 145 get :index, :project_id => 1, :set_filter => 1,
149 146 :f => ['tracker_id'],
150 147 :op => {'tracker_id' => '='},
151 148 :v => {'tracker_id' => ['1']}
152 149 assert_response :success
153 150 assert_template 'index.rhtml'
154 151 assert_not_nil assigns(:issues)
155 152
156 153 query = assigns(:query)
157 154 assert_not_nil query
158 155 assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters)
159 156 end
160 157
161 158 def test_index_with_project_and_empty_filters
162 159 get :index, :project_id => 1, :set_filter => 1, :fields => ['']
163 160 assert_response :success
164 161 assert_template 'index.rhtml'
165 162 assert_not_nil assigns(:issues)
166 163
167 164 query = assigns(:query)
168 165 assert_not_nil query
169 166 # no filter
170 167 assert_equal({}, query.filters)
171 168 end
172 169
173 170 def test_index_with_query
174 171 get :index, :project_id => 1, :query_id => 5
175 172 assert_response :success
176 173 assert_template 'index.rhtml'
177 174 assert_not_nil assigns(:issues)
178 175 assert_nil assigns(:issue_count_by_group)
179 176 end
180 177
181 178 def test_index_with_query_grouped_by_tracker
182 179 get :index, :project_id => 1, :query_id => 6
183 180 assert_response :success
184 181 assert_template 'index.rhtml'
185 182 assert_not_nil assigns(:issues)
186 183 assert_not_nil assigns(:issue_count_by_group)
187 184 end
188 185
189 186 def test_index_with_query_grouped_by_list_custom_field
190 187 get :index, :project_id => 1, :query_id => 9
191 188 assert_response :success
192 189 assert_template 'index.rhtml'
193 190 assert_not_nil assigns(:issues)
194 191 assert_not_nil assigns(:issue_count_by_group)
195 192 end
193
194 def test_private_query_should_not_be_available_to_other_users
195 q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil)
196 @request.session[:user_id] = 3
197
198 get :index, :query_id => q.id
199 assert_response 403
200 end
201
202 def test_private_query_should_be_available_to_its_user
203 q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil)
204 @request.session[:user_id] = 2
205
206 get :index, :query_id => q.id
207 assert_response :success
208 end
209
210 def test_public_query_should_be_available_to_other_users
211 q = Query.create!(:name => "private", :user => User.find(2), :is_public => true, :project => nil)
212 @request.session[:user_id] = 3
213
214 get :index, :query_id => q.id
215 assert_response :success
216 end
196 217
197 218 def test_index_sort_by_field_not_included_in_columns
198 219 Setting.issue_list_default_columns = %w(subject author)
199 220 get :index, :sort => 'tracker'
200 221 end
201 222
202 223 def test_index_csv_with_project
203 224 Setting.default_language = 'en'
204 225
205 226 get :index, :format => 'csv'
206 227 assert_response :success
207 228 assert_not_nil assigns(:issues)
208 229 assert_equal 'text/csv', @response.content_type
209 230 assert @response.body.starts_with?("#,")
210 231
211 232 get :index, :project_id => 1, :format => 'csv'
212 233 assert_response :success
213 234 assert_not_nil assigns(:issues)
214 235 assert_equal 'text/csv', @response.content_type
215 236 end
216 237
217 238 def test_index_pdf
218 239 get :index, :format => 'pdf'
219 240 assert_response :success
220 241 assert_not_nil assigns(:issues)
221 242 assert_equal 'application/pdf', @response.content_type
222 243
223 244 get :index, :project_id => 1, :format => 'pdf'
224 245 assert_response :success
225 246 assert_not_nil assigns(:issues)
226 247 assert_equal 'application/pdf', @response.content_type
227 248
228 249 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
229 250 assert_response :success
230 251 assert_not_nil assigns(:issues)
231 252 assert_equal 'application/pdf', @response.content_type
232 253 end
233 254
234 255 def test_index_pdf_with_query_grouped_by_list_custom_field
235 256 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
236 257 assert_response :success
237 258 assert_not_nil assigns(:issues)
238 259 assert_not_nil assigns(:issue_count_by_group)
239 260 assert_equal 'application/pdf', @response.content_type
240 261 end
241 262
242 263 def test_index_sort
243 264 get :index, :sort => 'tracker,id:desc'
244 265 assert_response :success
245 266
246 267 sort_params = @request.session['issues_index_sort']
247 268 assert sort_params.is_a?(String)
248 269 assert_equal 'tracker,id:desc', sort_params
249 270
250 271 issues = assigns(:issues)
251 272 assert_not_nil issues
252 273 assert !issues.empty?
253 274 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
254 275 end
255 276
256 277 def test_index_with_columns
257 278 columns = ['tracker', 'subject', 'assigned_to']
258 279 get :index, :set_filter => 1, :c => columns
259 280 assert_response :success
260 281
261 282 # query should use specified columns
262 283 query = assigns(:query)
263 284 assert_kind_of Query, query
264 285 assert_equal columns, query.column_names.map(&:to_s)
265 286
266 287 # columns should be stored in session
267 288 assert_kind_of Hash, session[:query]
268 289 assert_kind_of Array, session[:query][:column_names]
269 290 assert_equal columns, session[:query][:column_names].map(&:to_s)
270 291
271 292 # ensure only these columns are kept in the selected columns list
272 293 assert_tag :tag => 'select', :attributes => { :id => 'selected_columns' },
273 294 :children => { :count => 3 }
274 295 assert_no_tag :tag => 'option', :attributes => { :value => 'project' },
275 296 :parent => { :tag => 'select', :attributes => { :id => "selected_columns" } }
276 297 end
277 298
278 299 def test_index_with_custom_field_column
279 300 columns = %w(tracker subject cf_2)
280 301 get :index, :set_filter => 1, :c => columns
281 302 assert_response :success
282 303
283 304 # query should use specified columns
284 305 query = assigns(:query)
285 306 assert_kind_of Query, query
286 307 assert_equal columns, query.column_names.map(&:to_s)
287 308
288 309 assert_tag :td,
289 310 :attributes => {:class => 'cf_2 string'},
290 311 :ancestor => {:tag => 'table', :attributes => {:class => /issues/}}
291 312 end
292 313
293 314 def test_show_by_anonymous
294 315 get :show, :id => 1
295 316 assert_response :success
296 317 assert_template 'show.rhtml'
297 318 assert_not_nil assigns(:issue)
298 319 assert_equal Issue.find(1), assigns(:issue)
299 320
300 321 # anonymous role is allowed to add a note
301 322 assert_tag :tag => 'form',
302 323 :descendant => { :tag => 'fieldset',
303 324 :child => { :tag => 'legend',
304 325 :content => /Notes/ } }
305 326 end
306 327
307 328 def test_show_by_manager
308 329 @request.session[:user_id] = 2
309 330 get :show, :id => 1
310 331 assert_response :success
311 332
312 333 assert_tag :tag => 'a',
313 334 :content => /Quote/
314 335
315 336 assert_tag :tag => 'form',
316 337 :descendant => { :tag => 'fieldset',
317 338 :child => { :tag => 'legend',
318 339 :content => /Change properties/ } },
319 340 :descendant => { :tag => 'fieldset',
320 341 :child => { :tag => 'legend',
321 342 :content => /Log time/ } },
322 343 :descendant => { :tag => 'fieldset',
323 344 :child => { :tag => 'legend',
324 345 :content => /Notes/ } }
325 346 end
326 347
327 348 def test_update_form_should_not_display_inactive_enumerations
328 349 @request.session[:user_id] = 2
329 350 get :show, :id => 1
330 351 assert_response :success
331 352
332 353 assert ! IssuePriority.find(15).active?
333 354 assert_no_tag :option, :attributes => {:value => '15'},
334 355 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
335 356 end
336 357
337 358 def test_show_should_deny_anonymous_access_without_permission
338 359 Role.anonymous.remove_permission!(:view_issues)
339 360 get :show, :id => 1
340 361 assert_response :redirect
341 362 end
342 363
343 364 def test_show_should_deny_anonymous_access_to_private_issue
344 365 Issue.update_all(["is_private = ?", true], "id = 1")
345 366 get :show, :id => 1
346 367 assert_response :redirect
347 368 end
348 369
349 370 def test_show_should_deny_non_member_access_without_permission
350 371 Role.non_member.remove_permission!(:view_issues)
351 372 @request.session[:user_id] = 9
352 373 get :show, :id => 1
353 374 assert_response 403
354 375 end
355 376
356 377 def test_show_should_deny_non_member_access_to_private_issue
357 378 Issue.update_all(["is_private = ?", true], "id = 1")
358 379 @request.session[:user_id] = 9
359 380 get :show, :id => 1
360 381 assert_response 403
361 382 end
362 383
363 384 def test_show_should_deny_member_access_without_permission
364 385 Role.find(1).remove_permission!(:view_issues)
365 386 @request.session[:user_id] = 2
366 387 get :show, :id => 1
367 388 assert_response 403
368 389 end
369 390
370 391 def test_show_should_deny_member_access_to_private_issue_without_permission
371 392 Issue.update_all(["is_private = ?", true], "id = 1")
372 393 @request.session[:user_id] = 3
373 394 get :show, :id => 1
374 395 assert_response 403
375 396 end
376 397
377 398 def test_show_should_allow_author_access_to_private_issue
378 399 Issue.update_all(["is_private = ?, author_id = 3", true], "id = 1")
379 400 @request.session[:user_id] = 3
380 401 get :show, :id => 1
381 402 assert_response :success
382 403 end
383 404
384 405 def test_show_should_allow_assignee_access_to_private_issue
385 406 Issue.update_all(["is_private = ?, assigned_to_id = 3", true], "id = 1")
386 407 @request.session[:user_id] = 3
387 408 get :show, :id => 1
388 409 assert_response :success
389 410 end
390 411
391 412 def test_show_should_allow_member_access_to_private_issue_with_permission
392 413 Issue.update_all(["is_private = ?", true], "id = 1")
393 414 User.find(3).roles_for_project(Project.find(1)).first.update_attribute :issues_visibility, 'all'
394 415 @request.session[:user_id] = 3
395 416 get :show, :id => 1
396 417 assert_response :success
397 418 end
398 419
399 420 def test_show_should_not_disclose_relations_to_invisible_issues
400 421 Setting.cross_project_issue_relations = '1'
401 422 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
402 423 # Relation to a private project issue
403 424 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
404 425
405 426 get :show, :id => 1
406 427 assert_response :success
407 428
408 429 assert_tag :div, :attributes => { :id => 'relations' },
409 430 :descendant => { :tag => 'a', :content => /#2$/ }
410 431 assert_no_tag :div, :attributes => { :id => 'relations' },
411 432 :descendant => { :tag => 'a', :content => /#4$/ }
412 433 end
413 434
414 435 def test_show_atom
415 436 get :show, :id => 2, :format => 'atom'
416 437 assert_response :success
417 438 assert_template 'journals/index.rxml'
418 439 # Inline image
419 440 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
420 441 end
421 442
422 443 def test_show_export_to_pdf
423 444 get :show, :id => 3, :format => 'pdf'
424 445 assert_response :success
425 446 assert_equal 'application/pdf', @response.content_type
426 447 assert @response.body.starts_with?('%PDF')
427 448 assert_not_nil assigns(:issue)
428 449 end
429 450
430 451 def test_get_new
431 452 @request.session[:user_id] = 2
432 453 get :new, :project_id => 1, :tracker_id => 1
433 454 assert_response :success
434 455 assert_template 'new'
435 456
436 457 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
437 458 :value => 'Default string' }
438 459
439 460 # Be sure we don't display inactive IssuePriorities
440 461 assert ! IssuePriority.find(15).active?
441 462 assert_no_tag :option, :attributes => {:value => '15'},
442 463 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
443 464 end
444 465
445 466 def test_get_new_without_tracker_id
446 467 @request.session[:user_id] = 2
447 468 get :new, :project_id => 1
448 469 assert_response :success
449 470 assert_template 'new'
450 471
451 472 issue = assigns(:issue)
452 473 assert_not_nil issue
453 474 assert_equal Project.find(1).trackers.first, issue.tracker
454 475 end
455 476
456 477 def test_get_new_with_no_default_status_should_display_an_error
457 478 @request.session[:user_id] = 2
458 479 IssueStatus.delete_all
459 480
460 481 get :new, :project_id => 1
461 482 assert_response 500
462 483 assert_error_tag :content => /No default issue/
463 484 end
464 485
465 486 def test_get_new_with_no_tracker_should_display_an_error
466 487 @request.session[:user_id] = 2
467 488 Tracker.delete_all
468 489
469 490 get :new, :project_id => 1
470 491 assert_response 500
471 492 assert_error_tag :content => /No tracker/
472 493 end
473 494
474 495 def test_update_new_form
475 496 @request.session[:user_id] = 2
476 497 xhr :post, :new, :project_id => 1,
477 498 :issue => {:tracker_id => 2,
478 499 :subject => 'This is the test_new issue',
479 500 :description => 'This is the description',
480 501 :priority_id => 5}
481 502 assert_response :success
482 503 assert_template 'attributes'
483 504
484 505 issue = assigns(:issue)
485 506 assert_kind_of Issue, issue
486 507 assert_equal 1, issue.project_id
487 508 assert_equal 2, issue.tracker_id
488 509 assert_equal 'This is the test_new issue', issue.subject
489 510 end
490 511
491 512 def test_post_create
492 513 @request.session[:user_id] = 2
493 514 assert_difference 'Issue.count' do
494 515 post :create, :project_id => 1,
495 516 :issue => {:tracker_id => 3,
496 517 :status_id => 2,
497 518 :subject => 'This is the test_new issue',
498 519 :description => 'This is the description',
499 520 :priority_id => 5,
500 521 :start_date => '2010-11-07',
501 522 :estimated_hours => '',
502 523 :custom_field_values => {'2' => 'Value for field 2'}}
503 524 end
504 525 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
505 526
506 527 issue = Issue.find_by_subject('This is the test_new issue')
507 528 assert_not_nil issue
508 529 assert_equal 2, issue.author_id
509 530 assert_equal 3, issue.tracker_id
510 531 assert_equal 2, issue.status_id
511 532 assert_equal Date.parse('2010-11-07'), issue.start_date
512 533 assert_nil issue.estimated_hours
513 534 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
514 535 assert_not_nil v
515 536 assert_equal 'Value for field 2', v.value
516 537 end
517 538
518 539 def test_post_create_without_start_date
519 540 @request.session[:user_id] = 2
520 541 assert_difference 'Issue.count' do
521 542 post :create, :project_id => 1,
522 543 :issue => {:tracker_id => 3,
523 544 :status_id => 2,
524 545 :subject => 'This is the test_new issue',
525 546 :description => 'This is the description',
526 547 :priority_id => 5,
527 548 :start_date => '',
528 549 :estimated_hours => '',
529 550 :custom_field_values => {'2' => 'Value for field 2'}}
530 551 end
531 552 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
532 553
533 554 issue = Issue.find_by_subject('This is the test_new issue')
534 555 assert_not_nil issue
535 556 assert_nil issue.start_date
536 557 end
537 558
538 559 def test_post_create_and_continue
539 560 @request.session[:user_id] = 2
540 561 post :create, :project_id => 1,
541 562 :issue => {:tracker_id => 3,
542 563 :subject => 'This is first issue',
543 564 :priority_id => 5},
544 565 :continue => ''
545 566 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook',
546 567 :issue => {:tracker_id => 3}
547 568 end
548 569
549 570 def test_post_create_without_custom_fields_param
550 571 @request.session[:user_id] = 2
551 572 assert_difference 'Issue.count' do
552 573 post :create, :project_id => 1,
553 574 :issue => {:tracker_id => 1,
554 575 :subject => 'This is the test_new issue',
555 576 :description => 'This is the description',
556 577 :priority_id => 5}
557 578 end
558 579 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
559 580 end
560 581
561 582 def test_post_create_with_required_custom_field_and_without_custom_fields_param
562 583 field = IssueCustomField.find_by_name('Database')
563 584 field.update_attribute(:is_required, true)
564 585
565 586 @request.session[:user_id] = 2
566 587 post :create, :project_id => 1,
567 588 :issue => {:tracker_id => 1,
568 589 :subject => 'This is the test_new issue',
569 590 :description => 'This is the description',
570 591 :priority_id => 5}
571 592 assert_response :success
572 593 assert_template 'new'
573 594 issue = assigns(:issue)
574 595 assert_not_nil issue
575 596 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
576 597 end
577 598
578 599 def test_post_create_with_watchers
579 600 @request.session[:user_id] = 2
580 601 ActionMailer::Base.deliveries.clear
581 602
582 603 assert_difference 'Watcher.count', 2 do
583 604 post :create, :project_id => 1,
584 605 :issue => {:tracker_id => 1,
585 606 :subject => 'This is a new issue with watchers',
586 607 :description => 'This is the description',
587 608 :priority_id => 5,
588 609 :watcher_user_ids => ['2', '3']}
589 610 end
590 611 issue = Issue.find_by_subject('This is a new issue with watchers')
591 612 assert_not_nil issue
592 613 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
593 614
594 615 # Watchers added
595 616 assert_equal [2, 3], issue.watcher_user_ids.sort
596 617 assert issue.watched_by?(User.find(3))
597 618 # Watchers notified
598 619 mail = ActionMailer::Base.deliveries.last
599 620 assert_kind_of TMail::Mail, mail
600 621 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
601 622 end
602 623
603 624 def test_post_create_subissue
604 625 @request.session[:user_id] = 2
605 626
606 627 assert_difference 'Issue.count' do
607 628 post :create, :project_id => 1,
608 629 :issue => {:tracker_id => 1,
609 630 :subject => 'This is a child issue',
610 631 :parent_issue_id => 2}
611 632 end
612 633 issue = Issue.find_by_subject('This is a child issue')
613 634 assert_not_nil issue
614 635 assert_equal Issue.find(2), issue.parent
615 636 end
616 637
617 638 def test_post_create_subissue_with_non_numeric_parent_id
618 639 @request.session[:user_id] = 2
619 640
620 641 assert_difference 'Issue.count' do
621 642 post :create, :project_id => 1,
622 643 :issue => {:tracker_id => 1,
623 644 :subject => 'This is a child issue',
624 645 :parent_issue_id => 'ABC'}
625 646 end
626 647 issue = Issue.find_by_subject('This is a child issue')
627 648 assert_not_nil issue
628 649 assert_nil issue.parent
629 650 end
630 651
631 652 def test_post_create_private
632 653 @request.session[:user_id] = 2
633 654
634 655 assert_difference 'Issue.count' do
635 656 post :create, :project_id => 1,
636 657 :issue => {:tracker_id => 1,
637 658 :subject => 'This is a private issue',
638 659 :is_private => '1'}
639 660 end
640 661 issue = Issue.first(:order => 'id DESC')
641 662 assert issue.is_private?
642 663 end
643 664
644 665 def test_post_create_private_with_set_own_issues_private_permission
645 666 role = Role.find(1)
646 667 role.remove_permission! :set_issues_private
647 668 role.add_permission! :set_own_issues_private
648 669
649 670 @request.session[:user_id] = 2
650 671
651 672 assert_difference 'Issue.count' do
652 673 post :create, :project_id => 1,
653 674 :issue => {:tracker_id => 1,
654 675 :subject => 'This is a private issue',
655 676 :is_private => '1'}
656 677 end
657 678 issue = Issue.first(:order => 'id DESC')
658 679 assert issue.is_private?
659 680 end
660 681
661 682 def test_post_create_should_send_a_notification
662 683 ActionMailer::Base.deliveries.clear
663 684 @request.session[:user_id] = 2
664 685 assert_difference 'Issue.count' do
665 686 post :create, :project_id => 1,
666 687 :issue => {:tracker_id => 3,
667 688 :subject => 'This is the test_new issue',
668 689 :description => 'This is the description',
669 690 :priority_id => 5,
670 691 :estimated_hours => '',
671 692 :custom_field_values => {'2' => 'Value for field 2'}}
672 693 end
673 694 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
674 695
675 696 assert_equal 1, ActionMailer::Base.deliveries.size
676 697 end
677 698
678 699 def test_post_create_should_preserve_fields_values_on_validation_failure
679 700 @request.session[:user_id] = 2
680 701 post :create, :project_id => 1,
681 702 :issue => {:tracker_id => 1,
682 703 # empty subject
683 704 :subject => '',
684 705 :description => 'This is a description',
685 706 :priority_id => 6,
686 707 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
687 708 assert_response :success
688 709 assert_template 'new'
689 710
690 711 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
691 712 :content => 'This is a description'
692 713 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
693 714 :child => { :tag => 'option', :attributes => { :selected => 'selected',
694 715 :value => '6' },
695 716 :content => 'High' }
696 717 # Custom fields
697 718 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
698 719 :child => { :tag => 'option', :attributes => { :selected => 'selected',
699 720 :value => 'Oracle' },
700 721 :content => 'Oracle' }
701 722 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
702 723 :value => 'Value for field 2'}
703 724 end
704 725
705 726 def test_post_create_should_ignore_non_safe_attributes
706 727 @request.session[:user_id] = 2
707 728 assert_nothing_raised do
708 729 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
709 730 end
710 731 end
711 732
712 733 context "without workflow privilege" do
713 734 setup do
714 735 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
715 736 Role.anonymous.add_permission! :add_issues, :add_issue_notes
716 737 end
717 738
718 739 context "#new" do
719 740 should "propose default status only" do
720 741 get :new, :project_id => 1
721 742 assert_response :success
722 743 assert_template 'new'
723 744 assert_tag :tag => 'select',
724 745 :attributes => {:name => 'issue[status_id]'},
725 746 :children => {:count => 1},
726 747 :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}}
727 748 end
728 749
729 750 should "accept default status" do
730 751 assert_difference 'Issue.count' do
731 752 post :create, :project_id => 1,
732 753 :issue => {:tracker_id => 1,
733 754 :subject => 'This is an issue',
734 755 :status_id => 1}
735 756 end
736 757 issue = Issue.last(:order => 'id')
737 758 assert_equal IssueStatus.default, issue.status
738 759 end
739 760
740 761 should "ignore unauthorized status" do
741 762 assert_difference 'Issue.count' do
742 763 post :create, :project_id => 1,
743 764 :issue => {:tracker_id => 1,
744 765 :subject => 'This is an issue',
745 766 :status_id => 3}
746 767 end
747 768 issue = Issue.last(:order => 'id')
748 769 assert_equal IssueStatus.default, issue.status
749 770 end
750 771 end
751 772
752 773 context "#update" do
753 774 should "ignore status change" do
754 775 assert_difference 'Journal.count' do
755 776 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
756 777 end
757 778 assert_equal 1, Issue.find(1).status_id
758 779 end
759 780
760 781 should "ignore attributes changes" do
761 782 assert_difference 'Journal.count' do
762 783 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
763 784 end
764 785 issue = Issue.find(1)
765 786 assert_equal "Can't print recipes", issue.subject
766 787 assert_nil issue.assigned_to
767 788 end
768 789 end
769 790 end
770 791
771 792 context "with workflow privilege" do
772 793 setup do
773 794 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
774 795 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
775 796 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
776 797 Role.anonymous.add_permission! :add_issues, :add_issue_notes
777 798 end
778 799
779 800 context "#update" do
780 801 should "accept authorized status" do
781 802 assert_difference 'Journal.count' do
782 803 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
783 804 end
784 805 assert_equal 3, Issue.find(1).status_id
785 806 end
786 807
787 808 should "ignore unauthorized status" do
788 809 assert_difference 'Journal.count' do
789 810 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
790 811 end
791 812 assert_equal 1, Issue.find(1).status_id
792 813 end
793 814
794 815 should "accept authorized attributes changes" do
795 816 assert_difference 'Journal.count' do
796 817 put :update, :id => 1, :notes => 'just trying', :issue => {:assigned_to_id => 2}
797 818 end
798 819 issue = Issue.find(1)
799 820 assert_equal 2, issue.assigned_to_id
800 821 end
801 822
802 823 should "ignore unauthorized attributes changes" do
803 824 assert_difference 'Journal.count' do
804 825 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed'}
805 826 end
806 827 issue = Issue.find(1)
807 828 assert_equal "Can't print recipes", issue.subject
808 829 end
809 830 end
810 831
811 832 context "and :edit_issues permission" do
812 833 setup do
813 834 Role.anonymous.add_permission! :add_issues, :edit_issues
814 835 end
815 836
816 837 should "accept authorized status" do
817 838 assert_difference 'Journal.count' do
818 839 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
819 840 end
820 841 assert_equal 3, Issue.find(1).status_id
821 842 end
822 843
823 844 should "ignore unauthorized status" do
824 845 assert_difference 'Journal.count' do
825 846 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
826 847 end
827 848 assert_equal 1, Issue.find(1).status_id
828 849 end
829 850
830 851 should "accept authorized attributes changes" do
831 852 assert_difference 'Journal.count' do
832 853 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
833 854 end
834 855 issue = Issue.find(1)
835 856 assert_equal "changed", issue.subject
836 857 assert_equal 2, issue.assigned_to_id
837 858 end
838 859 end
839 860 end
840 861
841 862 def test_copy_issue
842 863 @request.session[:user_id] = 2
843 864 get :new, :project_id => 1, :copy_from => 1
844 865 assert_template 'new'
845 866 assert_not_nil assigns(:issue)
846 867 orig = Issue.find(1)
847 868 assert_equal orig.subject, assigns(:issue).subject
848 869 end
849 870
850 871 def test_get_edit
851 872 @request.session[:user_id] = 2
852 873 get :edit, :id => 1
853 874 assert_response :success
854 875 assert_template 'edit'
855 876 assert_not_nil assigns(:issue)
856 877 assert_equal Issue.find(1), assigns(:issue)
857 878
858 879 # Be sure we don't display inactive IssuePriorities
859 880 assert ! IssuePriority.find(15).active?
860 881 assert_no_tag :option, :attributes => {:value => '15'},
861 882 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
862 883 end
863 884
864 885 def test_get_edit_with_params
865 886 @request.session[:user_id] = 2
866 887 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 },
867 888 :time_entry => { :hours => '2.5', :comments => 'test_get_edit_with_params', :activity_id => TimeEntryActivity.first.id }
868 889 assert_response :success
869 890 assert_template 'edit'
870 891
871 892 issue = assigns(:issue)
872 893 assert_not_nil issue
873 894
874 895 assert_equal 5, issue.status_id
875 896 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
876 897 :child => { :tag => 'option',
877 898 :content => 'Closed',
878 899 :attributes => { :selected => 'selected' } }
879 900
880 901 assert_equal 7, issue.priority_id
881 902 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
882 903 :child => { :tag => 'option',
883 904 :content => 'Urgent',
884 905 :attributes => { :selected => 'selected' } }
885 906
886 907 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => '2.5' }
887 908 assert_tag :select, :attributes => { :name => 'time_entry[activity_id]' },
888 909 :child => { :tag => 'option',
889 910 :attributes => { :selected => 'selected', :value => TimeEntryActivity.first.id } }
890 911 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => 'test_get_edit_with_params' }
891 912 end
892 913
893 914 def test_update_edit_form
894 915 @request.session[:user_id] = 2
895 916 xhr :post, :new, :project_id => 1,
896 917 :id => 1,
897 918 :issue => {:tracker_id => 2,
898 919 :subject => 'This is the test_new issue',
899 920 :description => 'This is the description',
900 921 :priority_id => 5}
901 922 assert_response :success
902 923 assert_template 'attributes'
903 924
904 925 issue = assigns(:issue)
905 926 assert_kind_of Issue, issue
906 927 assert_equal 1, issue.id
907 928 assert_equal 1, issue.project_id
908 929 assert_equal 2, issue.tracker_id
909 930 assert_equal 'This is the test_new issue', issue.subject
910 931 end
911 932
912 933 def test_update_using_invalid_http_verbs
913 934 @request.session[:user_id] = 2
914 935 subject = 'Updated by an invalid http verb'
915 936
916 937 get :update, :id => 1, :issue => {:subject => subject}
917 938 assert_not_equal subject, Issue.find(1).subject
918 939
919 940 post :update, :id => 1, :issue => {:subject => subject}
920 941 assert_not_equal subject, Issue.find(1).subject
921 942
922 943 delete :update, :id => 1, :issue => {:subject => subject}
923 944 assert_not_equal subject, Issue.find(1).subject
924 945 end
925 946
926 947 def test_put_update_without_custom_fields_param
927 948 @request.session[:user_id] = 2
928 949 ActionMailer::Base.deliveries.clear
929 950
930 951 issue = Issue.find(1)
931 952 assert_equal '125', issue.custom_value_for(2).value
932 953 old_subject = issue.subject
933 954 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
934 955
935 956 assert_difference('Journal.count') do
936 957 assert_difference('JournalDetail.count', 2) do
937 958 put :update, :id => 1, :issue => {:subject => new_subject,
938 959 :priority_id => '6',
939 960 :category_id => '1' # no change
940 961 }
941 962 end
942 963 end
943 964 assert_redirected_to :action => 'show', :id => '1'
944 965 issue.reload
945 966 assert_equal new_subject, issue.subject
946 967 # Make sure custom fields were not cleared
947 968 assert_equal '125', issue.custom_value_for(2).value
948 969
949 970 mail = ActionMailer::Base.deliveries.last
950 971 assert_kind_of TMail::Mail, mail
951 972 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
952 973 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
953 974 end
954 975
955 976 def test_put_update_with_custom_field_change
956 977 @request.session[:user_id] = 2
957 978 issue = Issue.find(1)
958 979 assert_equal '125', issue.custom_value_for(2).value
959 980
960 981 assert_difference('Journal.count') do
961 982 assert_difference('JournalDetail.count', 3) do
962 983 put :update, :id => 1, :issue => {:subject => 'Custom field change',
963 984 :priority_id => '6',
964 985 :category_id => '1', # no change
965 986 :custom_field_values => { '2' => 'New custom value' }
966 987 }
967 988 end
968 989 end
969 990 assert_redirected_to :action => 'show', :id => '1'
970 991 issue.reload
971 992 assert_equal 'New custom value', issue.custom_value_for(2).value
972 993
973 994 mail = ActionMailer::Base.deliveries.last
974 995 assert_kind_of TMail::Mail, mail
975 996 assert mail.body.include?("Searchable field changed from 125 to New custom value")
976 997 end
977 998
978 999 def test_put_update_with_status_and_assignee_change
979 1000 issue = Issue.find(1)
980 1001 assert_equal 1, issue.status_id
981 1002 @request.session[:user_id] = 2
982 1003 assert_difference('TimeEntry.count', 0) do
983 1004 put :update,
984 1005 :id => 1,
985 1006 :issue => { :status_id => 2, :assigned_to_id => 3 },
986 1007 :notes => 'Assigned to dlopper',
987 1008 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
988 1009 end
989 1010 assert_redirected_to :action => 'show', :id => '1'
990 1011 issue.reload
991 1012 assert_equal 2, issue.status_id
992 1013 j = Journal.find(:first, :order => 'id DESC')
993 1014 assert_equal 'Assigned to dlopper', j.notes
994 1015 assert_equal 2, j.details.size
995 1016
996 1017 mail = ActionMailer::Base.deliveries.last
997 1018 assert mail.body.include?("Status changed from New to Assigned")
998 1019 # subject should contain the new status
999 1020 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
1000 1021 end
1001 1022
1002 1023 def test_put_update_with_note_only
1003 1024 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
1004 1025 # anonymous user
1005 1026 put :update,
1006 1027 :id => 1,
1007 1028 :notes => notes
1008 1029 assert_redirected_to :action => 'show', :id => '1'
1009 1030 j = Journal.find(:first, :order => 'id DESC')
1010 1031 assert_equal notes, j.notes
1011 1032 assert_equal 0, j.details.size
1012 1033 assert_equal User.anonymous, j.user
1013 1034
1014 1035 mail = ActionMailer::Base.deliveries.last
1015 1036 assert mail.body.include?(notes)
1016 1037 end
1017 1038
1018 1039 def test_put_update_with_note_and_spent_time
1019 1040 @request.session[:user_id] = 2
1020 1041 spent_hours_before = Issue.find(1).spent_hours
1021 1042 assert_difference('TimeEntry.count') do
1022 1043 put :update,
1023 1044 :id => 1,
1024 1045 :notes => '2.5 hours added',
1025 1046 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
1026 1047 end
1027 1048 assert_redirected_to :action => 'show', :id => '1'
1028 1049
1029 1050 issue = Issue.find(1)
1030 1051
1031 1052 j = Journal.find(:first, :order => 'id DESC')
1032 1053 assert_equal '2.5 hours added', j.notes
1033 1054 assert_equal 0, j.details.size
1034 1055
1035 1056 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
1036 1057 assert_not_nil t
1037 1058 assert_equal 2.5, t.hours
1038 1059 assert_equal spent_hours_before + 2.5, issue.spent_hours
1039 1060 end
1040 1061
1041 1062 def test_put_update_with_attachment_only
1042 1063 set_tmp_attachments_directory
1043 1064
1044 1065 # Delete all fixtured journals, a race condition can occur causing the wrong
1045 1066 # journal to get fetched in the next find.
1046 1067 Journal.delete_all
1047 1068
1048 1069 # anonymous user
1049 1070 put :update,
1050 1071 :id => 1,
1051 1072 :notes => '',
1052 1073 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
1053 1074 assert_redirected_to :action => 'show', :id => '1'
1054 1075 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
1055 1076 assert j.notes.blank?
1056 1077 assert_equal 1, j.details.size
1057 1078 assert_equal 'testfile.txt', j.details.first.value
1058 1079 assert_equal User.anonymous, j.user
1059 1080
1060 1081 mail = ActionMailer::Base.deliveries.last
1061 1082 assert mail.body.include?('testfile.txt')
1062 1083 end
1063 1084
1064 1085 def test_put_update_with_attachment_that_fails_to_save
1065 1086 set_tmp_attachments_directory
1066 1087
1067 1088 # Delete all fixtured journals, a race condition can occur causing the wrong
1068 1089 # journal to get fetched in the next find.
1069 1090 Journal.delete_all
1070 1091
1071 1092 # Mock out the unsaved attachment
1072 1093 Attachment.any_instance.stubs(:create).returns(Attachment.new)
1073 1094
1074 1095 # anonymous user
1075 1096 put :update,
1076 1097 :id => 1,
1077 1098 :notes => '',
1078 1099 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
1079 1100 assert_redirected_to :action => 'show', :id => '1'
1080 1101 assert_equal '1 file(s) could not be saved.', flash[:warning]
1081 1102
1082 1103 end if Object.const_defined?(:Mocha)
1083 1104
1084 1105 def test_put_update_with_no_change
1085 1106 issue = Issue.find(1)
1086 1107 issue.journals.clear
1087 1108 ActionMailer::Base.deliveries.clear
1088 1109
1089 1110 put :update,
1090 1111 :id => 1,
1091 1112 :notes => ''
1092 1113 assert_redirected_to :action => 'show', :id => '1'
1093 1114
1094 1115 issue.reload
1095 1116 assert issue.journals.empty?
1096 1117 # No email should be sent
1097 1118 assert ActionMailer::Base.deliveries.empty?
1098 1119 end
1099 1120
1100 1121 def test_put_update_should_send_a_notification
1101 1122 @request.session[:user_id] = 2
1102 1123 ActionMailer::Base.deliveries.clear
1103 1124 issue = Issue.find(1)
1104 1125 old_subject = issue.subject
1105 1126 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
1106 1127
1107 1128 put :update, :id => 1, :issue => {:subject => new_subject,
1108 1129 :priority_id => '6',
1109 1130 :category_id => '1' # no change
1110 1131 }
1111 1132 assert_equal 1, ActionMailer::Base.deliveries.size
1112 1133 end
1113 1134
1114 1135 def test_put_update_with_invalid_spent_time_hours_only
1115 1136 @request.session[:user_id] = 2
1116 1137 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
1117 1138
1118 1139 assert_no_difference('Journal.count') do
1119 1140 put :update,
1120 1141 :id => 1,
1121 1142 :notes => notes,
1122 1143 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
1123 1144 end
1124 1145 assert_response :success
1125 1146 assert_template 'edit'
1126 1147
1127 1148 assert_error_tag :descendant => {:content => /Activity can't be blank/}
1128 1149 assert_tag :textarea, :attributes => { :name => 'notes' }, :content => notes
1129 1150 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
1130 1151 end
1131 1152
1132 1153 def test_put_update_with_invalid_spent_time_comments_only
1133 1154 @request.session[:user_id] = 2
1134 1155 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
1135 1156
1136 1157 assert_no_difference('Journal.count') do
1137 1158 put :update,
1138 1159 :id => 1,
1139 1160 :notes => notes,
1140 1161 :time_entry => {"comments"=>"this is my comment", "activity_id"=>"", "hours"=>""}
1141 1162 end
1142 1163 assert_response :success
1143 1164 assert_template 'edit'
1144 1165
1145 1166 assert_error_tag :descendant => {:content => /Activity can't be blank/}
1146 1167 assert_error_tag :descendant => {:content => /Hours can't be blank/}
1147 1168 assert_tag :textarea, :attributes => { :name => 'notes' }, :content => notes
1148 1169 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => "this is my comment" }
1149 1170 end
1150 1171
1151 1172 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
1152 1173 issue = Issue.find(2)
1153 1174 @request.session[:user_id] = 2
1154 1175
1155 1176 put :update,
1156 1177 :id => issue.id,
1157 1178 :issue => {
1158 1179 :fixed_version_id => 4
1159 1180 }
1160 1181
1161 1182 assert_response :redirect
1162 1183 issue.reload
1163 1184 assert_equal 4, issue.fixed_version_id
1164 1185 assert_not_equal issue.project_id, issue.fixed_version.project_id
1165 1186 end
1166 1187
1167 1188 def test_put_update_should_redirect_back_using_the_back_url_parameter
1168 1189 issue = Issue.find(2)
1169 1190 @request.session[:user_id] = 2
1170 1191
1171 1192 put :update,
1172 1193 :id => issue.id,
1173 1194 :issue => {
1174 1195 :fixed_version_id => 4
1175 1196 },
1176 1197 :back_url => '/issues'
1177 1198
1178 1199 assert_response :redirect
1179 1200 assert_redirected_to '/issues'
1180 1201 end
1181 1202
1182 1203 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1183 1204 issue = Issue.find(2)
1184 1205 @request.session[:user_id] = 2
1185 1206
1186 1207 put :update,
1187 1208 :id => issue.id,
1188 1209 :issue => {
1189 1210 :fixed_version_id => 4
1190 1211 },
1191 1212 :back_url => 'http://google.com'
1192 1213
1193 1214 assert_response :redirect
1194 1215 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
1195 1216 end
1196 1217
1197 1218 def test_get_bulk_edit
1198 1219 @request.session[:user_id] = 2
1199 1220 get :bulk_edit, :ids => [1, 2]
1200 1221 assert_response :success
1201 1222 assert_template 'bulk_edit'
1202 1223
1203 1224 assert_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
1204 1225
1205 1226 # Project specific custom field, date type
1206 1227 field = CustomField.find(9)
1207 1228 assert !field.is_for_all?
1208 1229 assert_equal 'date', field.field_format
1209 1230 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
1210 1231
1211 1232 # System wide custom field
1212 1233 assert CustomField.find(1).is_for_all?
1213 1234 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
1214 1235
1215 1236 # Be sure we don't display inactive IssuePriorities
1216 1237 assert ! IssuePriority.find(15).active?
1217 1238 assert_no_tag :option, :attributes => {:value => '15'},
1218 1239 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
1219 1240 end
1220 1241
1221 1242 def test_get_bulk_edit_on_different_projects
1222 1243 @request.session[:user_id] = 2
1223 1244 get :bulk_edit, :ids => [1, 2, 6]
1224 1245 assert_response :success
1225 1246 assert_template 'bulk_edit'
1226 1247
1227 1248 # Can not set issues from different projects as children of an issue
1228 1249 assert_no_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
1229 1250
1230 1251 # Project specific custom field, date type
1231 1252 field = CustomField.find(9)
1232 1253 assert !field.is_for_all?
1233 1254 assert !field.project_ids.include?(Issue.find(6).project_id)
1234 1255 assert_no_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
1235 1256 end
1236 1257
1237 1258 def test_get_bulk_edit_with_user_custom_field
1238 1259 field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true)
1239 1260
1240 1261 @request.session[:user_id] = 2
1241 1262 get :bulk_edit, :ids => [1, 2]
1242 1263 assert_response :success
1243 1264 assert_template 'bulk_edit'
1244 1265
1245 1266 assert_tag :select,
1246 1267 :attributes => {:name => "issue[custom_field_values][#{field.id}]"},
1247 1268 :children => {
1248 1269 :only => {:tag => 'option'},
1249 1270 :count => Project.find(1).users.count + 1
1250 1271 }
1251 1272 end
1252 1273
1253 1274 def test_get_bulk_edit_with_version_custom_field
1254 1275 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true)
1255 1276
1256 1277 @request.session[:user_id] = 2
1257 1278 get :bulk_edit, :ids => [1, 2]
1258 1279 assert_response :success
1259 1280 assert_template 'bulk_edit'
1260 1281
1261 1282 assert_tag :select,
1262 1283 :attributes => {:name => "issue[custom_field_values][#{field.id}]"},
1263 1284 :children => {
1264 1285 :only => {:tag => 'option'},
1265 1286 :count => Project.find(1).versions.count + 1
1266 1287 }
1267 1288 end
1268 1289
1269 1290 def test_bulk_update
1270 1291 @request.session[:user_id] = 2
1271 1292 # update issues priority
1272 1293 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
1273 1294 :issue => {:priority_id => 7,
1274 1295 :assigned_to_id => '',
1275 1296 :custom_field_values => {'2' => ''}}
1276 1297
1277 1298 assert_response 302
1278 1299 # check that the issues were updated
1279 1300 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
1280 1301
1281 1302 issue = Issue.find(1)
1282 1303 journal = issue.journals.find(:first, :order => 'created_on DESC')
1283 1304 assert_equal '125', issue.custom_value_for(2).value
1284 1305 assert_equal 'Bulk editing', journal.notes
1285 1306 assert_equal 1, journal.details.size
1286 1307 end
1287 1308
1288 1309 def test_bulk_update_on_different_projects
1289 1310 @request.session[:user_id] = 2
1290 1311 # update issues priority
1291 1312 post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing',
1292 1313 :issue => {:priority_id => 7,
1293 1314 :assigned_to_id => '',
1294 1315 :custom_field_values => {'2' => ''}}
1295 1316
1296 1317 assert_response 302
1297 1318 # check that the issues were updated
1298 1319 assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id)
1299 1320
1300 1321 issue = Issue.find(1)
1301 1322 journal = issue.journals.find(:first, :order => 'created_on DESC')
1302 1323 assert_equal '125', issue.custom_value_for(2).value
1303 1324 assert_equal 'Bulk editing', journal.notes
1304 1325 assert_equal 1, journal.details.size
1305 1326 end
1306 1327
1307 1328 def test_bulk_update_on_different_projects_without_rights
1308 1329 @request.session[:user_id] = 3
1309 1330 user = User.find(3)
1310 1331 action = { :controller => "issues", :action => "bulk_update" }
1311 1332 assert user.allowed_to?(action, Issue.find(1).project)
1312 1333 assert ! user.allowed_to?(action, Issue.find(6).project)
1313 1334 post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail',
1314 1335 :issue => {:priority_id => 7,
1315 1336 :assigned_to_id => '',
1316 1337 :custom_field_values => {'2' => ''}}
1317 1338 assert_response 403
1318 1339 assert_not_equal "Bulk should fail", Journal.last.notes
1319 1340 end
1320 1341
1321 1342 def test_bullk_update_should_send_a_notification
1322 1343 @request.session[:user_id] = 2
1323 1344 ActionMailer::Base.deliveries.clear
1324 1345 post(:bulk_update,
1325 1346 {
1326 1347 :ids => [1, 2],
1327 1348 :notes => 'Bulk editing',
1328 1349 :issue => {
1329 1350 :priority_id => 7,
1330 1351 :assigned_to_id => '',
1331 1352 :custom_field_values => {'2' => ''}
1332 1353 }
1333 1354 })
1334 1355
1335 1356 assert_response 302
1336 1357 assert_equal 2, ActionMailer::Base.deliveries.size
1337 1358 end
1338 1359
1339 1360 def test_bulk_update_status
1340 1361 @request.session[:user_id] = 2
1341 1362 # update issues priority
1342 1363 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
1343 1364 :issue => {:priority_id => '',
1344 1365 :assigned_to_id => '',
1345 1366 :status_id => '5'}
1346 1367
1347 1368 assert_response 302
1348 1369 issue = Issue.find(1)
1349 1370 assert issue.closed?
1350 1371 end
1351 1372
1352 1373 def test_bulk_update_parent_id
1353 1374 @request.session[:user_id] = 2
1354 1375 post :bulk_update, :ids => [1, 3],
1355 1376 :notes => 'Bulk editing parent',
1356 1377 :issue => {:priority_id => '', :assigned_to_id => '', :status_id => '', :parent_issue_id => '2'}
1357 1378
1358 1379 assert_response 302
1359 1380 parent = Issue.find(2)
1360 1381 assert_equal parent.id, Issue.find(1).parent_id
1361 1382 assert_equal parent.id, Issue.find(3).parent_id
1362 1383 assert_equal [1, 3], parent.children.collect(&:id).sort
1363 1384 end
1364 1385
1365 1386 def test_bulk_update_custom_field
1366 1387 @request.session[:user_id] = 2
1367 1388 # update issues priority
1368 1389 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
1369 1390 :issue => {:priority_id => '',
1370 1391 :assigned_to_id => '',
1371 1392 :custom_field_values => {'2' => '777'}}
1372 1393
1373 1394 assert_response 302
1374 1395
1375 1396 issue = Issue.find(1)
1376 1397 journal = issue.journals.find(:first, :order => 'created_on DESC')
1377 1398 assert_equal '777', issue.custom_value_for(2).value
1378 1399 assert_equal 1, journal.details.size
1379 1400 assert_equal '125', journal.details.first.old_value
1380 1401 assert_equal '777', journal.details.first.value
1381 1402 end
1382 1403
1383 1404 def test_bulk_update_unassign
1384 1405 assert_not_nil Issue.find(2).assigned_to
1385 1406 @request.session[:user_id] = 2
1386 1407 # unassign issues
1387 1408 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
1388 1409 assert_response 302
1389 1410 # check that the issues were updated
1390 1411 assert_nil Issue.find(2).assigned_to
1391 1412 end
1392 1413
1393 1414 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
1394 1415 @request.session[:user_id] = 2
1395 1416
1396 1417 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
1397 1418
1398 1419 assert_response :redirect
1399 1420 issues = Issue.find([1,2])
1400 1421 issues.each do |issue|
1401 1422 assert_equal 4, issue.fixed_version_id
1402 1423 assert_not_equal issue.project_id, issue.fixed_version.project_id
1403 1424 end
1404 1425 end
1405 1426
1406 1427 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
1407 1428 @request.session[:user_id] = 2
1408 1429 post :bulk_update, :ids => [1,2], :back_url => '/issues'
1409 1430
1410 1431 assert_response :redirect
1411 1432 assert_redirected_to '/issues'
1412 1433 end
1413 1434
1414 1435 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1415 1436 @request.session[:user_id] = 2
1416 1437 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
1417 1438
1418 1439 assert_response :redirect
1419 1440 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
1420 1441 end
1421 1442
1422 1443 def test_destroy_issue_with_no_time_entries
1423 1444 assert_nil TimeEntry.find_by_issue_id(2)
1424 1445 @request.session[:user_id] = 2
1425 1446 post :destroy, :id => 2
1426 1447 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1427 1448 assert_nil Issue.find_by_id(2)
1428 1449 end
1429 1450
1430 1451 def test_destroy_issues_with_time_entries
1431 1452 @request.session[:user_id] = 2
1432 1453 post :destroy, :ids => [1, 3]
1433 1454 assert_response :success
1434 1455 assert_template 'destroy'
1435 1456 assert_not_nil assigns(:hours)
1436 1457 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1437 1458 end
1438 1459
1439 1460 def test_destroy_issues_and_destroy_time_entries
1440 1461 @request.session[:user_id] = 2
1441 1462 post :destroy, :ids => [1, 3], :todo => 'destroy'
1442 1463 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1443 1464 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1444 1465 assert_nil TimeEntry.find_by_id([1, 2])
1445 1466 end
1446 1467
1447 1468 def test_destroy_issues_and_assign_time_entries_to_project
1448 1469 @request.session[:user_id] = 2
1449 1470 post :destroy, :ids => [1, 3], :todo => 'nullify'
1450 1471 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1451 1472 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1452 1473 assert_nil TimeEntry.find(1).issue_id
1453 1474 assert_nil TimeEntry.find(2).issue_id
1454 1475 end
1455 1476
1456 1477 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1457 1478 @request.session[:user_id] = 2
1458 1479 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1459 1480 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1460 1481 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1461 1482 assert_equal 2, TimeEntry.find(1).issue_id
1462 1483 assert_equal 2, TimeEntry.find(2).issue_id
1463 1484 end
1464 1485
1465 1486 def test_destroy_issues_from_different_projects
1466 1487 @request.session[:user_id] = 2
1467 1488 post :destroy, :ids => [1, 2, 6], :todo => 'destroy'
1468 1489 assert_redirected_to :controller => 'issues', :action => 'index'
1469 1490 assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6))
1470 1491 end
1471 1492
1472 1493 def test_destroy_parent_and_child_issues
1473 1494 parent = Issue.generate!(:project_id => 1, :tracker_id => 1)
1474 1495 child = Issue.generate!(:project_id => 1, :tracker_id => 1, :parent_issue_id => parent.id)
1475 1496 assert child.is_descendant_of?(parent.reload)
1476 1497
1477 1498 @request.session[:user_id] = 2
1478 1499 assert_difference 'Issue.count', -2 do
1479 1500 post :destroy, :ids => [parent.id, child.id], :todo => 'destroy'
1480 1501 end
1481 1502 assert_response 302
1482 1503 end
1483 1504
1484 1505 def test_default_search_scope
1485 1506 get :index
1486 1507 assert_tag :div, :attributes => {:id => 'quick-search'},
1487 1508 :child => {:tag => 'form',
1488 1509 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1489 1510 end
1490 1511 end
General Comments 0
You need to be logged in to leave comments. Login now