##// END OF EJS Templates
Added ability to delete issues from different projects through contextual menu (#5332)...
Jean-Baptiste Barth -
r4122:b255b7760ac6
parent child
Show More
@@ -1,415 +1,415
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'uri'
19 19 require 'cgi'
20 20
21 21 class ApplicationController < ActionController::Base
22 22 include Redmine::I18n
23 23
24 24 layout 'base'
25 25 exempt_from_layout 'builder'
26 26
27 27 # Remove broken cookie after upgrade from 0.8.x (#4292)
28 28 # See https://rails.lighthouseapp.com/projects/8994/tickets/3360
29 29 # TODO: remove it when Rails is fixed
30 30 before_filter :delete_broken_cookies
31 31 def delete_broken_cookies
32 32 if cookies['_redmine_session'] && cookies['_redmine_session'] !~ /--/
33 33 cookies.delete '_redmine_session'
34 34 redirect_to home_path
35 35 return false
36 36 end
37 37 end
38 38
39 39 before_filter :user_setup, :check_if_login_required, :set_localization
40 40 filter_parameter_logging :password
41 41 protect_from_forgery
42 42
43 43 rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
44 44
45 45 include Redmine::Search::Controller
46 46 include Redmine::MenuManager::MenuController
47 47 helper Redmine::MenuManager::MenuHelper
48 48
49 49 Redmine::Scm::Base.all.each do |scm|
50 50 require_dependency "repository/#{scm.underscore}"
51 51 end
52 52
53 53 def user_setup
54 54 # Check the settings cache for each request
55 55 Setting.check_cache
56 56 # Find the current user
57 57 User.current = find_current_user
58 58 end
59 59
60 60 # Returns the current user or nil if no user is logged in
61 61 # and starts a session if needed
62 62 def find_current_user
63 63 if session[:user_id]
64 64 # existing session
65 65 (User.active.find(session[:user_id]) rescue nil)
66 66 elsif cookies[:autologin] && Setting.autologin?
67 67 # auto-login feature starts a new session
68 68 user = User.try_to_autologin(cookies[:autologin])
69 69 session[:user_id] = user.id if user
70 70 user
71 71 elsif params[:format] == 'atom' && params[:key] && accept_key_auth_actions.include?(params[:action])
72 72 # RSS key authentication does not start a session
73 73 User.find_by_rss_key(params[:key])
74 74 elsif Setting.rest_api_enabled? && ['xml', 'json'].include?(params[:format])
75 75 if params[:key].present? && accept_key_auth_actions.include?(params[:action])
76 76 # Use API key
77 77 User.find_by_api_key(params[:key])
78 78 else
79 79 # HTTP Basic, either username/password or API key/random
80 80 authenticate_with_http_basic do |username, password|
81 81 User.try_to_login(username, password) || User.find_by_api_key(username)
82 82 end
83 83 end
84 84 end
85 85 end
86 86
87 87 # Sets the logged in user
88 88 def logged_user=(user)
89 89 reset_session
90 90 if user && user.is_a?(User)
91 91 User.current = user
92 92 session[:user_id] = user.id
93 93 else
94 94 User.current = User.anonymous
95 95 end
96 96 end
97 97
98 98 # check if login is globally required to access the application
99 99 def check_if_login_required
100 100 # no check needed if user is already logged in
101 101 return true if User.current.logged?
102 102 require_login if Setting.login_required?
103 103 end
104 104
105 105 def set_localization
106 106 lang = nil
107 107 if User.current.logged?
108 108 lang = find_language(User.current.language)
109 109 end
110 110 if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
111 111 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
112 112 if !accept_lang.blank?
113 113 accept_lang = accept_lang.downcase
114 114 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
115 115 end
116 116 end
117 117 lang ||= Setting.default_language
118 118 set_language_if_valid(lang)
119 119 end
120 120
121 121 def require_login
122 122 if !User.current.logged?
123 123 # Extract only the basic url parameters on non-GET requests
124 124 if request.get?
125 125 url = url_for(params)
126 126 else
127 127 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
128 128 end
129 129 respond_to do |format|
130 130 format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
131 131 format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
132 132 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
133 133 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
134 134 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
135 135 end
136 136 return false
137 137 end
138 138 true
139 139 end
140 140
141 141 def require_admin
142 142 return unless require_login
143 143 if !User.current.admin?
144 144 render_403
145 145 return false
146 146 end
147 147 true
148 148 end
149 149
150 150 def deny_access
151 151 User.current.logged? ? render_403 : require_login
152 152 end
153 153
154 154 # Authorize the user for the requested action
155 155 def authorize(ctrl = params[:controller], action = params[:action], global = false)
156 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project, :global => global)
156 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
157 157 allowed ? true : deny_access
158 158 end
159 159
160 160 # Authorize the user for the requested action outside a project
161 161 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
162 162 authorize(ctrl, action, global)
163 163 end
164 164
165 165 # Find project of id params[:id]
166 166 def find_project
167 167 @project = Project.find(params[:id])
168 168 rescue ActiveRecord::RecordNotFound
169 169 render_404
170 170 end
171 171
172 172 # Find project of id params[:project_id]
173 173 def find_project_by_project_id
174 174 @project = Project.find(params[:project_id])
175 175 rescue ActiveRecord::RecordNotFound
176 176 render_404
177 177 end
178 178
179 179 # Find a project based on params[:project_id]
180 180 # TODO: some subclasses override this, see about merging their logic
181 181 def find_optional_project
182 182 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
183 183 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
184 184 allowed ? true : deny_access
185 185 rescue ActiveRecord::RecordNotFound
186 186 render_404
187 187 end
188 188
189 189 # Finds and sets @project based on @object.project
190 190 def find_project_from_association
191 191 render_404 unless @object.present?
192 192
193 193 @project = @object.project
194 194 rescue ActiveRecord::RecordNotFound
195 195 render_404
196 196 end
197 197
198 198 def find_model_object
199 199 model = self.class.read_inheritable_attribute('model_object')
200 200 if model
201 201 @object = model.find(params[:id])
202 202 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
203 203 end
204 204 rescue ActiveRecord::RecordNotFound
205 205 render_404
206 206 end
207 207
208 208 def self.model_object(model)
209 209 write_inheritable_attribute('model_object', model)
210 210 end
211 211
212 212 # Filter for bulk issue operations
213 213 def find_issues
214 214 @issues = Issue.find_all_by_id(params[:id] || params[:ids])
215 215 raise ActiveRecord::RecordNotFound if @issues.empty?
216 216 @projects = @issues.collect(&:project).compact.uniq
217 217 @project = @projects.first if @projects.size == 1
218 218 rescue ActiveRecord::RecordNotFound
219 219 render_404
220 220 end
221 221
222 222 # Check if project is unique before bulk operations
223 223 def check_project_uniqueness
224 224 unless @project
225 225 # TODO: let users bulk edit/move/destroy issues from different projects
226 226 render_error 'Can not bulk edit/move/destroy issues from different projects'
227 227 return false
228 228 end
229 229 end
230 230
231 231 # make sure that the user is a member of the project (or admin) if project is private
232 232 # used as a before_filter for actions that do not require any particular permission on the project
233 233 def check_project_privacy
234 234 if @project && @project.active?
235 235 if @project.is_public? || User.current.member_of?(@project) || User.current.admin?
236 236 true
237 237 else
238 238 User.current.logged? ? render_403 : require_login
239 239 end
240 240 else
241 241 @project = nil
242 242 render_404
243 243 false
244 244 end
245 245 end
246 246
247 247 def back_url
248 248 params[:back_url] || request.env['HTTP_REFERER']
249 249 end
250 250
251 251 def redirect_back_or_default(default)
252 252 back_url = CGI.unescape(params[:back_url].to_s)
253 253 if !back_url.blank?
254 254 begin
255 255 uri = URI.parse(back_url)
256 256 # do not redirect user to another host or to the login or register page
257 257 if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
258 258 redirect_to(back_url)
259 259 return
260 260 end
261 261 rescue URI::InvalidURIError
262 262 # redirect to default
263 263 end
264 264 end
265 265 redirect_to default
266 266 end
267 267
268 268 def render_403
269 269 @project = nil
270 270 respond_to do |format|
271 271 format.html { render :template => "common/403", :layout => use_layout, :status => 403 }
272 272 format.atom { head 403 }
273 273 format.xml { head 403 }
274 274 format.js { head 403 }
275 275 format.json { head 403 }
276 276 end
277 277 return false
278 278 end
279 279
280 280 def render_404
281 281 respond_to do |format|
282 282 format.html { render :template => "common/404", :layout => use_layout, :status => 404 }
283 283 format.atom { head 404 }
284 284 format.xml { head 404 }
285 285 format.js { head 404 }
286 286 format.json { head 404 }
287 287 end
288 288 return false
289 289 end
290 290
291 291 def render_error(msg)
292 292 respond_to do |format|
293 293 format.html {
294 294 flash.now[:error] = msg
295 295 render :text => '', :layout => use_layout, :status => 500
296 296 }
297 297 format.atom { head 500 }
298 298 format.xml { head 500 }
299 299 format.js { head 500 }
300 300 format.json { head 500 }
301 301 end
302 302 end
303 303
304 304 # Picks which layout to use based on the request
305 305 #
306 306 # @return [boolean, string] name of the layout to use or false for no layout
307 307 def use_layout
308 308 request.xhr? ? false : 'base'
309 309 end
310 310
311 311 def invalid_authenticity_token
312 312 if api_request?
313 313 logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
314 314 end
315 315 render_error "Invalid form authenticity token."
316 316 end
317 317
318 318 def render_feed(items, options={})
319 319 @items = items || []
320 320 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
321 321 @items = @items.slice(0, Setting.feeds_limit.to_i)
322 322 @title = options[:title] || Setting.app_title
323 323 render :template => "common/feed.atom.rxml", :layout => false, :content_type => 'application/atom+xml'
324 324 end
325 325
326 326 def self.accept_key_auth(*actions)
327 327 actions = actions.flatten.map(&:to_s)
328 328 write_inheritable_attribute('accept_key_auth_actions', actions)
329 329 end
330 330
331 331 def accept_key_auth_actions
332 332 self.class.read_inheritable_attribute('accept_key_auth_actions') || []
333 333 end
334 334
335 335 # Returns the number of objects that should be displayed
336 336 # on the paginated list
337 337 def per_page_option
338 338 per_page = nil
339 339 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
340 340 per_page = params[:per_page].to_s.to_i
341 341 session[:per_page] = per_page
342 342 elsif session[:per_page]
343 343 per_page = session[:per_page]
344 344 else
345 345 per_page = Setting.per_page_options_array.first || 25
346 346 end
347 347 per_page
348 348 end
349 349
350 350 # qvalues http header parser
351 351 # code taken from webrick
352 352 def parse_qvalues(value)
353 353 tmp = []
354 354 if value
355 355 parts = value.split(/,\s*/)
356 356 parts.each {|part|
357 357 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
358 358 val = m[1]
359 359 q = (m[2] or 1).to_f
360 360 tmp.push([val, q])
361 361 end
362 362 }
363 363 tmp = tmp.sort_by{|val, q| -q}
364 364 tmp.collect!{|val, q| val}
365 365 end
366 366 return tmp
367 367 rescue
368 368 nil
369 369 end
370 370
371 371 # Returns a string that can be used as filename value in Content-Disposition header
372 372 def filename_for_content_disposition(name)
373 373 request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
374 374 end
375 375
376 376 def api_request?
377 377 %w(xml json).include? params[:format]
378 378 end
379 379
380 380 # Renders a warning flash if obj has unsaved attachments
381 381 def render_attachment_warning_if_needed(obj)
382 382 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
383 383 end
384 384
385 385 # Sets the `flash` notice or error based the number of issues that did not save
386 386 #
387 387 # @param [Array, Issue] issues all of the saved and unsaved Issues
388 388 # @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
389 389 def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
390 390 if unsaved_issue_ids.empty?
391 391 flash[:notice] = l(:notice_successful_update) unless issues.empty?
392 392 else
393 393 flash[:error] = l(:notice_failed_to_save_issues,
394 394 :count => unsaved_issue_ids.size,
395 395 :total => issues.size,
396 396 :ids => '#' + unsaved_issue_ids.join(', #'))
397 397 end
398 398 end
399 399
400 400 # Rescues an invalid query statement. Just in case...
401 401 def query_statement_invalid(exception)
402 402 logger.error "Query::StatementInvalid: #{exception.message}" if logger
403 403 session.delete(:query)
404 404 sort_clear if respond_to?(:sort_clear)
405 405 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
406 406 end
407 407
408 408 # Converts the errors on an ActiveRecord object into a common JSON format
409 409 def object_errors_to_json(object)
410 410 object.errors.collect do |attribute, error|
411 411 { attribute => error }
412 412 end.to_json
413 413 end
414 414
415 415 end
@@ -1,39 +1,39
1 1 class ContextMenusController < ApplicationController
2 2 helper :watchers
3 3
4 4 def issues
5 5 @issues = Issue.find_all_by_id(params[:ids], :include => :project)
6 6 if (@issues.size == 1)
7 7 @issue = @issues.first
8 8 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
9 9 else
10 10 @allowed_statuses = @issues.map do |i|
11 11 i.new_statuses_allowed_to(User.current)
12 12 end.inject do |memo,s|
13 13 memo & s
14 14 end
15 15 end
16 16 @projects = @issues.collect(&:project).compact.uniq
17 17 @project = @projects.first if @projects.size == 1
18 18
19 19 @can = {:edit => (@project && User.current.allowed_to?(:edit_issues, @project)),
20 20 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
21 21 :update => (@project && (User.current.allowed_to?(:edit_issues, @project) || (User.current.allowed_to?(:change_status, @project) && @allowed_statuses && !@allowed_statuses.empty?))),
22 22 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
23 23 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
24 :delete => (@project && User.current.allowed_to?(:delete_issues, @project))
24 :delete => User.current.allowed_to?(:delete_issues, @projects)
25 25 }
26 26 if @project
27 27 @assignables = @project.assignable_users
28 28 @assignables << @issue.assigned_to if @issue && @issue.assigned_to && !@assignables.include?(@issue.assigned_to)
29 29 @trackers = @project.trackers
30 30 end
31 31
32 32 @priorities = IssuePriority.all.reverse
33 33 @statuses = IssueStatus.find(:all, :order => 'position')
34 34 @back = back_url
35 35
36 36 render :layout => false
37 37 end
38 38
39 39 end
@@ -1,330 +1,330
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class IssuesController < ApplicationController
19 19 menu_item :new_issue, :only => [:new, :create]
20 20 default_search_scope :issues
21 21
22 22 before_filter :find_issue, :only => [:show, :edit, :update]
23 23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
24 before_filter :check_project_uniqueness, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
24 before_filter :check_project_uniqueness, :only => [:bulk_edit, :bulk_update, :move, :perform_move]
25 25 before_filter :find_project, :only => [:new, :create]
26 26 before_filter :authorize, :except => [:index]
27 27 before_filter :find_optional_project, :only => [:index]
28 28 before_filter :check_for_default_issue_status, :only => [:new, :create]
29 29 before_filter :build_new_issue_from_params, :only => [:new, :create]
30 30 accept_key_auth :index, :show
31 31
32 32 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
33 33
34 34 helper :journals
35 35 helper :projects
36 36 include ProjectsHelper
37 37 helper :custom_fields
38 38 include CustomFieldsHelper
39 39 helper :issue_relations
40 40 include IssueRelationsHelper
41 41 helper :watchers
42 42 include WatchersHelper
43 43 helper :attachments
44 44 include AttachmentsHelper
45 45 helper :queries
46 46 include QueriesHelper
47 47 helper :sort
48 48 include SortHelper
49 49 include IssuesHelper
50 50 helper :timelog
51 51 helper :gantt
52 52 include Redmine::Export::PDF
53 53
54 54 verify :method => [:post, :delete],
55 55 :only => :destroy,
56 56 :render => { :nothing => true, :status => :method_not_allowed }
57 57
58 58 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
59 59 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
60 60 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
61 61
62 62 def index
63 63 retrieve_query
64 64 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
65 65 sort_update(@query.sortable_columns)
66 66
67 67 if @query.valid?
68 68 limit = case params[:format]
69 69 when 'csv', 'pdf'
70 70 Setting.issues_export_limit.to_i
71 71 when 'atom'
72 72 Setting.feeds_limit.to_i
73 73 else
74 74 per_page_option
75 75 end
76 76
77 77 @issue_count = @query.issue_count
78 78 @issue_pages = Paginator.new self, @issue_count, limit, params['page']
79 79 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
80 80 :order => sort_clause,
81 81 :offset => @issue_pages.current.offset,
82 82 :limit => limit)
83 83 @issue_count_by_group = @query.issue_count_by_group
84 84
85 85 respond_to do |format|
86 86 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
87 87 format.xml { render :layout => false }
88 88 format.json { render :text => @issues.to_json, :layout => false }
89 89 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
90 90 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
91 91 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
92 92 end
93 93 else
94 94 # Send html if the query is not valid
95 95 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
96 96 end
97 97 rescue ActiveRecord::RecordNotFound
98 98 render_404
99 99 end
100 100
101 101 def show
102 102 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
103 103 @journals.each_with_index {|j,i| j.indice = i+1}
104 104 @journals.reverse! if User.current.wants_comments_in_reverse_order?
105 105 @changesets = @issue.changesets.visible.all
106 106 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
107 107 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
108 108 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
109 109 @priorities = IssuePriority.all
110 110 @time_entry = TimeEntry.new
111 111 respond_to do |format|
112 112 format.html { render :template => 'issues/show.rhtml' }
113 113 format.xml { render :layout => false }
114 114 format.json { render :text => @issue.to_json, :layout => false }
115 115 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
116 116 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
117 117 end
118 118 end
119 119
120 120 # Add a new issue
121 121 # The new issue will be created from an existing one if copy_from parameter is given
122 122 def new
123 123 respond_to do |format|
124 124 format.html { render :action => 'new', :layout => !request.xhr? }
125 125 format.js { render :partial => 'attributes' }
126 126 end
127 127 end
128 128
129 129 def create
130 130 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
131 131 if @issue.save
132 132 attachments = Attachment.attach_files(@issue, params[:attachments])
133 133 render_attachment_warning_if_needed(@issue)
134 134 flash[:notice] = l(:notice_successful_create)
135 135 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
136 136 respond_to do |format|
137 137 format.html {
138 138 redirect_to(params[:continue] ? { :action => 'new', :project_id => @project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
139 139 { :action => 'show', :id => @issue })
140 140 }
141 141 format.xml { render :action => 'show', :status => :created, :location => url_for(:controller => 'issues', :action => 'show', :id => @issue) }
142 142 format.json { render :text => @issue.to_json, :status => :created, :location => url_for(:controller => 'issues', :action => 'show'), :layout => false }
143 143 end
144 144 return
145 145 else
146 146 respond_to do |format|
147 147 format.html { render :action => 'new' }
148 148 format.xml { render(:xml => @issue.errors, :status => :unprocessable_entity); return }
149 149 format.json { render :text => object_errors_to_json(@issue), :status => :unprocessable_entity, :layout => false }
150 150 end
151 151 end
152 152 end
153 153
154 154 # Attributes that can be updated on workflow transition (without :edit permission)
155 155 # TODO: make it configurable (at least per role)
156 156 UPDATABLE_ATTRS_ON_TRANSITION = %w(status_id assigned_to_id fixed_version_id done_ratio) unless const_defined?(:UPDATABLE_ATTRS_ON_TRANSITION)
157 157
158 158 def edit
159 159 update_issue_from_params
160 160
161 161 @journal = @issue.current_journal
162 162
163 163 respond_to do |format|
164 164 format.html { }
165 165 format.xml { }
166 166 end
167 167 end
168 168
169 169 def update
170 170 update_issue_from_params
171 171
172 172 if @issue.save_issue_with_child_records(params, @time_entry)
173 173 render_attachment_warning_if_needed(@issue)
174 174 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
175 175
176 176 respond_to do |format|
177 177 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
178 178 format.xml { head :ok }
179 179 format.json { head :ok }
180 180 end
181 181 else
182 182 render_attachment_warning_if_needed(@issue)
183 183 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
184 184 @journal = @issue.current_journal
185 185
186 186 respond_to do |format|
187 187 format.html { render :action => 'edit' }
188 188 format.xml { render :xml => @issue.errors, :status => :unprocessable_entity }
189 189 format.json { render :text => object_errors_to_json(@issue), :status => :unprocessable_entity, :layout => false }
190 190 end
191 191 end
192 192 end
193 193
194 194 # Bulk edit a set of issues
195 195 def bulk_edit
196 196 @issues.sort!
197 197 @available_statuses = Workflow.available_statuses(@project)
198 198 @custom_fields = @project.all_issue_custom_fields
199 199 end
200 200
201 201 def bulk_update
202 202 @issues.sort!
203 203 attributes = parse_params_for_bulk_issue_attributes(params)
204 204
205 205 unsaved_issue_ids = []
206 206 @issues.each do |issue|
207 207 issue.reload
208 208 journal = issue.init_journal(User.current, params[:notes])
209 209 issue.safe_attributes = attributes
210 210 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
211 211 unless issue.save
212 212 # Keep unsaved issue ids to display them in flash error
213 213 unsaved_issue_ids << issue.id
214 214 end
215 215 end
216 216 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
217 217 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
218 218 end
219 219
220 220 def destroy
221 221 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
222 222 if @hours > 0
223 223 case params[:todo]
224 224 when 'destroy'
225 225 # nothing to do
226 226 when 'nullify'
227 227 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
228 228 when 'reassign'
229 229 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
230 230 if reassign_to.nil?
231 231 flash.now[:error] = l(:error_issue_not_found_in_project)
232 232 return
233 233 else
234 234 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
235 235 end
236 236 else
237 237 unless params[:format] == 'xml' || params[:format] == 'json'
238 238 # display the destroy form if it's a user request
239 239 return
240 240 end
241 241 end
242 242 end
243 243 @issues.each(&:destroy)
244 244 respond_to do |format|
245 format.html { redirect_to :action => 'index', :project_id => @project }
245 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
246 246 format.xml { head :ok }
247 247 format.json { head :ok }
248 248 end
249 249 end
250 250
251 251 private
252 252 def find_issue
253 253 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
254 254 @project = @issue.project
255 255 rescue ActiveRecord::RecordNotFound
256 256 render_404
257 257 end
258 258
259 259 def find_project
260 260 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
261 261 @project = Project.find(project_id)
262 262 rescue ActiveRecord::RecordNotFound
263 263 render_404
264 264 end
265 265
266 266 # Used by #edit and #update to set some common instance variables
267 267 # from the params
268 268 # TODO: Refactor, not everything in here is needed by #edit
269 269 def update_issue_from_params
270 270 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
271 271 @priorities = IssuePriority.all
272 272 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
273 273 @time_entry = TimeEntry.new
274 274
275 275 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
276 276 @issue.init_journal(User.current, @notes)
277 277 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
278 278 if (@edit_allowed || !@allowed_statuses.empty?) && params[:issue]
279 279 attrs = params[:issue].dup
280 280 attrs.delete_if {|k,v| !UPDATABLE_ATTRS_ON_TRANSITION.include?(k) } unless @edit_allowed
281 281 attrs.delete(:status_id) unless @allowed_statuses.detect {|s| s.id.to_s == attrs[:status_id].to_s}
282 282 @issue.safe_attributes = attrs
283 283 end
284 284
285 285 end
286 286
287 287 # TODO: Refactor, lots of extra code in here
288 288 # TODO: Changing tracker on an existing issue should not trigger this
289 289 def build_new_issue_from_params
290 290 if params[:id].blank?
291 291 @issue = Issue.new
292 292 @issue.copy_from(params[:copy_from]) if params[:copy_from]
293 293 @issue.project = @project
294 294 else
295 295 @issue = @project.issues.visible.find(params[:id])
296 296 end
297 297
298 298 @issue.project = @project
299 299 # Tracker must be set before custom field values
300 300 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
301 301 if @issue.tracker.nil?
302 302 render_error l(:error_no_tracker_in_project)
303 303 return false
304 304 end
305 305 if params[:issue].is_a?(Hash)
306 306 @issue.safe_attributes = params[:issue]
307 307 if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record?
308 308 @issue.watcher_user_ids = params[:issue]['watcher_user_ids']
309 309 end
310 310 end
311 311 @issue.author = User.current
312 312 @issue.start_date ||= Date.today
313 313 @priorities = IssuePriority.all
314 314 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
315 315 end
316 316
317 317 def check_for_default_issue_status
318 318 if IssueStatus.default.nil?
319 319 render_error l(:error_no_default_issue_status)
320 320 return false
321 321 end
322 322 end
323 323
324 324 def parse_params_for_bulk_issue_attributes(params)
325 325 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
326 326 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
327 327 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
328 328 attributes
329 329 end
330 330 end
@@ -1,122 +1,122
1 1 <ul>
2 2 <%= call_hook(:view_issues_context_menu_start, {:issues => @issues, :can => @can, :back => @back }) %>
3 3
4 4 <% if !@issue.nil? -%>
5 5 <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'edit', :id => @issue},
6 6 :class => 'icon-edit', :disabled => !@can[:edit] %></li>
7 7 <% else %>
8 8 <li><%= context_menu_link l(:button_edit), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id)},
9 9 :class => 'icon-edit', :disabled => !@can[:edit] %></li>
10 10 <% end %>
11 11
12 12 <% unless @allowed_statuses.empty? %>
13 13 <li class="folder">
14 14 <a href="#" class="submenu" onclick="return false;"><%= l(:field_status) %></a>
15 15 <ul>
16 16 <% @statuses.each do |s| -%>
17 17 <li><%= context_menu_link s.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {:status_id => s}, :back_url => @back}, :method => :post,
18 18 :selected => (@issue && s == @issue.status), :disabled => !(@can[:update] && @allowed_statuses.include?(s)) %></li>
19 19 <% end -%>
20 20 </ul>
21 21 </li>
22 22 <% end %>
23 23
24 24 <% unless @trackers.nil? %>
25 25 <li class="folder">
26 26 <a href="#" class="submenu"><%= l(:field_tracker) %></a>
27 27 <ul>
28 28 <% @trackers.each do |t| -%>
29 29 <li><%= context_menu_link t.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'tracker_id' => t}, :back_url => @back}, :method => :post,
30 30 :selected => (@issue && t == @issue.tracker), :disabled => !@can[:edit] %></li>
31 31 <% end -%>
32 32 </ul>
33 33 </li>
34 34 <% end %>
35 35
36 36 <% if @projects.size == 1 %>
37 37 <li class="folder">
38 38 <a href="#" class="submenu"><%= l(:field_priority) %></a>
39 39 <ul>
40 40 <% @priorities.each do |p| -%>
41 41 <li><%= context_menu_link p.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'priority_id' => p}, :back_url => @back}, :method => :post,
42 42 :selected => (@issue && p == @issue.priority), :disabled => (!@can[:edit] || @issues.detect {|i| !i.leaf?}) %></li>
43 43 <% end -%>
44 44 </ul>
45 45 </li>
46 46 <% end %>
47 47
48 48 <% unless @project.nil? || @project.shared_versions.open.empty? -%>
49 49 <li class="folder">
50 50 <a href="#" class="submenu"><%= l(:field_fixed_version) %></a>
51 51 <ul>
52 52 <% @project.shared_versions.open.sort.each do |v| -%>
53 53 <li><%= context_menu_link format_version_name(v), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'fixed_version_id' => v}, :back_url => @back}, :method => :post,
54 54 :selected => (@issue && v == @issue.fixed_version), :disabled => !@can[:update] %></li>
55 55 <% end -%>
56 56 <li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'fixed_version_id' => 'none'}, :back_url => @back}, :method => :post,
57 57 :selected => (@issue && @issue.fixed_version.nil?), :disabled => !@can[:update] %></li>
58 58 </ul>
59 59 </li>
60 60 <% end %>
61 61 <% unless @assignables.nil? || @assignables.empty? -%>
62 62 <li class="folder">
63 63 <a href="#" class="submenu"><%= l(:field_assigned_to) %></a>
64 64 <ul>
65 65 <% @assignables.each do |u| -%>
66 66 <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'assigned_to_id' => u}, :back_url => @back}, :method => :post,
67 67 :selected => (@issue && u == @issue.assigned_to), :disabled => !@can[:update] %></li>
68 68 <% end -%>
69 69 <li><%= context_menu_link l(:label_nobody), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'assigned_to_id' => 'none'}, :back_url => @back}, :method => :post,
70 70 :selected => (@issue && @issue.assigned_to.nil?), :disabled => !@can[:update] %></li>
71 71 </ul>
72 72 </li>
73 73 <% end %>
74 74 <% unless @project.nil? || @project.issue_categories.empty? -%>
75 75 <li class="folder">
76 76 <a href="#" class="submenu"><%= l(:field_category) %></a>
77 77 <ul>
78 78 <% @project.issue_categories.each do |u| -%>
79 79 <li><%= context_menu_link u.name, {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'category_id' => u}, :back_url => @back}, :method => :post,
80 80 :selected => (@issue && u == @issue.category), :disabled => !@can[:update] %></li>
81 81 <% end -%>
82 82 <li><%= context_menu_link l(:label_none), {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'category_id' => 'none'}, :back_url => @back}, :method => :post,
83 83 :selected => (@issue && @issue.category.nil?), :disabled => !@can[:update] %></li>
84 84 </ul>
85 85 </li>
86 86 <% end -%>
87 87
88 88 <% if Issue.use_field_for_done_ratio? && @projects.size == 1 %>
89 89 <li class="folder">
90 90 <a href="#" class="submenu"><%= l(:field_done_ratio) %></a>
91 91 <ul>
92 92 <% (0..10).map{|x|x*10}.each do |p| -%>
93 93 <li><%= context_menu_link "#{p}%", {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), :issue => {'done_ratio' => p}, :back_url => @back}, :method => :post,
94 94 :selected => (@issue && p == @issue.done_ratio), :disabled => (!@can[:edit] || @issues.detect {|i| !i.leaf?}) %></li>
95 95 <% end -%>
96 96 </ul>
97 97 </li>
98 98 <% end %>
99 99
100 100 <% if !@issue.nil? %>
101 101 <% if @can[:log_time] -%>
102 102 <li><%= context_menu_link l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue},
103 103 :class => 'icon-time-add' %></li>
104 104 <% end %>
105 105 <% if User.current.logged? %>
106 106 <li><%= watcher_link(@issue, User.current) %></li>
107 107 <% end %>
108 108 <% end %>
109 109
110 110 <% if @issue.present? %>
111 111 <li><%= context_menu_link l(:button_duplicate), {:controller => 'issues', :action => 'new', :project_id => @project, :copy_from => @issue},
112 112 :class => 'icon-duplicate', :disabled => !@can[:copy] %></li>
113 113 <% end %>
114 114 <li><%= context_menu_link l(:button_copy), new_issue_move_path(:ids => @issues.collect(&:id), :copy_options => {:copy => 't'}),
115 115 :class => 'icon-copy', :disabled => !@can[:move] %></li>
116 116 <li><%= context_menu_link l(:button_move), new_issue_move_path(:ids => @issues.collect(&:id)),
117 117 :class => 'icon-move', :disabled => !@can[:move] %></li>
118 <li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :ids => @issues.collect(&:id)},
118 <li><%= context_menu_link l(:button_delete), {:controller => 'issues', :action => 'destroy', :ids => @issues.collect(&:id), :back_url => @back},
119 119 :method => :post, :confirm => l(:text_issues_destroy_confirmation), :class => 'icon-del', :disabled => !@can[:delete] %></li>
120 120
121 121 <%= call_hook(:view_issues_context_menu_end, {:issues => @issues, :can => @can, :back => @back }) %>
122 122 </ul>
@@ -1,92 +1,93
1 1 require File.dirname(__FILE__) + '/../test_helper'
2 2
3 3 class ContextMenusControllerTest < ActionController::TestCase
4 4 fixtures :all
5 5
6 6 def test_context_menu_one_issue
7 7 @request.session[:user_id] = 2
8 8 get :issues, :ids => [1]
9 9 assert_response :success
10 10 assert_template 'context_menu'
11 11 assert_tag :tag => 'a', :content => 'Edit',
12 12 :attributes => { :href => '/issues/1/edit',
13 13 :class => 'icon-edit' }
14 14 assert_tag :tag => 'a', :content => 'Closed',
15 15 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;issue%5Bstatus_id%5D=5',
16 16 :class => '' }
17 17 assert_tag :tag => 'a', :content => 'Immediate',
18 18 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;issue%5Bpriority_id%5D=8',
19 19 :class => '' }
20 20 # Versions
21 21 assert_tag :tag => 'a', :content => '2.0',
22 22 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;issue%5Bfixed_version_id%5D=3',
23 23 :class => '' }
24 24 assert_tag :tag => 'a', :content => 'eCookbook Subproject 1 - 2.0',
25 25 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;issue%5Bfixed_version_id%5D=4',
26 26 :class => '' }
27 27
28 28 assert_tag :tag => 'a', :content => 'Dave Lopper',
29 29 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;issue%5Bassigned_to_id%5D=3',
30 30 :class => '' }
31 31 assert_tag :tag => 'a', :content => 'Duplicate',
32 32 :attributes => { :href => '/projects/ecookbook/issues/1/copy',
33 33 :class => 'icon-duplicate' }
34 34 assert_tag :tag => 'a', :content => 'Copy',
35 35 :attributes => { :href => '/issues/move/new?copy_options%5Bcopy%5D=t&amp;ids%5B%5D=1',
36 36 :class => 'icon-copy' }
37 37 assert_tag :tag => 'a', :content => 'Move',
38 38 :attributes => { :href => '/issues/move/new?ids%5B%5D=1',
39 39 :class => 'icon-move' }
40 40 assert_tag :tag => 'a', :content => 'Delete',
41 41 :attributes => { :href => '/issues/destroy?ids%5B%5D=1',
42 42 :class => 'icon-del' }
43 43 end
44 44
45 45 def test_context_menu_one_issue_by_anonymous
46 46 get :issues, :ids => [1]
47 47 assert_response :success
48 48 assert_template 'context_menu'
49 49 assert_tag :tag => 'a', :content => 'Delete',
50 50 :attributes => { :href => '#',
51 51 :class => 'icon-del disabled' }
52 52 end
53 53
54 54 def test_context_menu_multiple_issues_of_same_project
55 55 @request.session[:user_id] = 2
56 56 get :issues, :ids => [1, 2]
57 57 assert_response :success
58 58 assert_template 'context_menu'
59 59 assert_tag :tag => 'a', :content => 'Edit',
60 60 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2',
61 61 :class => 'icon-edit' }
62 62 assert_tag :tag => 'a', :content => 'Closed',
63 63 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;issue%5Bstatus_id%5D=5',
64 64 :class => '' }
65 65 assert_tag :tag => 'a', :content => 'Immediate',
66 66 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;issue%5Bpriority_id%5D=8',
67 67 :class => '' }
68 68 assert_tag :tag => 'a', :content => 'Dave Lopper',
69 69 :attributes => { :href => '/issues/bulk_edit?ids%5B%5D=1&amp;ids%5B%5D=2&amp;issue%5Bassigned_to_id%5D=3',
70 70 :class => '' }
71 71 assert_tag :tag => 'a', :content => 'Copy',
72 72 :attributes => { :href => '/issues/move/new?copy_options%5Bcopy%5D=t&amp;ids%5B%5D=1&amp;ids%5B%5D=2',
73 73 :class => 'icon-copy' }
74 74 assert_tag :tag => 'a', :content => 'Move',
75 75 :attributes => { :href => '/issues/move/new?ids%5B%5D=1&amp;ids%5B%5D=2',
76 76 :class => 'icon-move' }
77 77 assert_tag :tag => 'a', :content => 'Delete',
78 78 :attributes => { :href => '/issues/destroy?ids%5B%5D=1&amp;ids%5B%5D=2',
79 79 :class => 'icon-del' }
80 80 end
81 81
82 def test_context_menu_multiple_issues_of_different_project
82 def test_context_menu_multiple_issues_of_different_projects
83 83 @request.session[:user_id] = 2
84 get :issues, :ids => [1, 2, 4]
84 get :issues, :ids => [1, 2, 6]
85 85 assert_response :success
86 86 assert_template 'context_menu'
87 ids = "ids%5B%5D=1&amp;ids%5B%5D=2&amp;ids%5B%5D=6"
87 88 assert_tag :tag => 'a', :content => 'Delete',
88 :attributes => { :href => '#',
89 :class => 'icon-del disabled' }
89 :attributes => { :href => "/issues/destroy?#{ids}",
90 :class => 'icon-del' }
90 91 end
91 92
92 93 end
@@ -1,1070 +1,1077
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'issues_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class IssuesController; def rescue_action(e) raise e end; end
23 23
24 24 class IssuesControllerTest < ActionController::TestCase
25 25 fixtures :projects,
26 26 :users,
27 27 :roles,
28 28 :members,
29 29 :member_roles,
30 30 :issues,
31 31 :issue_statuses,
32 32 :versions,
33 33 :trackers,
34 34 :projects_trackers,
35 35 :issue_categories,
36 36 :enabled_modules,
37 37 :enumerations,
38 38 :attachments,
39 39 :workflows,
40 40 :custom_fields,
41 41 :custom_values,
42 42 :custom_fields_projects,
43 43 :custom_fields_trackers,
44 44 :time_entries,
45 45 :journals,
46 46 :journal_details,
47 47 :queries
48 48
49 49 def setup
50 50 @controller = IssuesController.new
51 51 @request = ActionController::TestRequest.new
52 52 @response = ActionController::TestResponse.new
53 53 User.current = nil
54 54 end
55 55
56 56 def test_index
57 57 Setting.default_language = 'en'
58 58
59 59 get :index
60 60 assert_response :success
61 61 assert_template 'index.rhtml'
62 62 assert_not_nil assigns(:issues)
63 63 assert_nil assigns(:project)
64 64 assert_tag :tag => 'a', :content => /Can't print recipes/
65 65 assert_tag :tag => 'a', :content => /Subproject issue/
66 66 # private projects hidden
67 67 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
68 68 assert_no_tag :tag => 'a', :content => /Issue on project 2/
69 69 # project column
70 70 assert_tag :tag => 'th', :content => /Project/
71 71 end
72 72
73 73 def test_index_should_not_list_issues_when_module_disabled
74 74 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
75 75 get :index
76 76 assert_response :success
77 77 assert_template 'index.rhtml'
78 78 assert_not_nil assigns(:issues)
79 79 assert_nil assigns(:project)
80 80 assert_no_tag :tag => 'a', :content => /Can't print recipes/
81 81 assert_tag :tag => 'a', :content => /Subproject issue/
82 82 end
83 83
84 84 def test_index_should_not_list_issues_when_module_disabled
85 85 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
86 86 get :index
87 87 assert_response :success
88 88 assert_template 'index.rhtml'
89 89 assert_not_nil assigns(:issues)
90 90 assert_nil assigns(:project)
91 91 assert_no_tag :tag => 'a', :content => /Can't print recipes/
92 92 assert_tag :tag => 'a', :content => /Subproject issue/
93 93 end
94 94
95 95 def test_index_with_project
96 96 Setting.display_subprojects_issues = 0
97 97 get :index, :project_id => 1
98 98 assert_response :success
99 99 assert_template 'index.rhtml'
100 100 assert_not_nil assigns(:issues)
101 101 assert_tag :tag => 'a', :content => /Can't print recipes/
102 102 assert_no_tag :tag => 'a', :content => /Subproject issue/
103 103 end
104 104
105 105 def test_index_with_project_and_subprojects
106 106 Setting.display_subprojects_issues = 1
107 107 get :index, :project_id => 1
108 108 assert_response :success
109 109 assert_template 'index.rhtml'
110 110 assert_not_nil assigns(:issues)
111 111 assert_tag :tag => 'a', :content => /Can't print recipes/
112 112 assert_tag :tag => 'a', :content => /Subproject issue/
113 113 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
114 114 end
115 115
116 116 def test_index_with_project_and_subprojects_should_show_private_subprojects
117 117 @request.session[:user_id] = 2
118 118 Setting.display_subprojects_issues = 1
119 119 get :index, :project_id => 1
120 120 assert_response :success
121 121 assert_template 'index.rhtml'
122 122 assert_not_nil assigns(:issues)
123 123 assert_tag :tag => 'a', :content => /Can't print recipes/
124 124 assert_tag :tag => 'a', :content => /Subproject issue/
125 125 assert_tag :tag => 'a', :content => /Issue of a private subproject/
126 126 end
127 127
128 128 def test_index_with_project_and_filter
129 129 get :index, :project_id => 1, :set_filter => 1
130 130 assert_response :success
131 131 assert_template 'index.rhtml'
132 132 assert_not_nil assigns(:issues)
133 133 end
134 134
135 135 def test_index_with_query
136 136 get :index, :project_id => 1, :query_id => 5
137 137 assert_response :success
138 138 assert_template 'index.rhtml'
139 139 assert_not_nil assigns(:issues)
140 140 assert_nil assigns(:issue_count_by_group)
141 141 end
142 142
143 143 def test_index_with_query_grouped_by_tracker
144 144 get :index, :project_id => 1, :query_id => 6
145 145 assert_response :success
146 146 assert_template 'index.rhtml'
147 147 assert_not_nil assigns(:issues)
148 148 assert_not_nil assigns(:issue_count_by_group)
149 149 end
150 150
151 151 def test_index_with_query_grouped_by_list_custom_field
152 152 get :index, :project_id => 1, :query_id => 9
153 153 assert_response :success
154 154 assert_template 'index.rhtml'
155 155 assert_not_nil assigns(:issues)
156 156 assert_not_nil assigns(:issue_count_by_group)
157 157 end
158 158
159 159 def test_index_sort_by_field_not_included_in_columns
160 160 Setting.issue_list_default_columns = %w(subject author)
161 161 get :index, :sort => 'tracker'
162 162 end
163 163
164 164 def test_index_csv_with_project
165 165 Setting.default_language = 'en'
166 166
167 167 get :index, :format => 'csv'
168 168 assert_response :success
169 169 assert_not_nil assigns(:issues)
170 170 assert_equal 'text/csv', @response.content_type
171 171 assert @response.body.starts_with?("#,")
172 172
173 173 get :index, :project_id => 1, :format => 'csv'
174 174 assert_response :success
175 175 assert_not_nil assigns(:issues)
176 176 assert_equal 'text/csv', @response.content_type
177 177 end
178 178
179 179 def test_index_pdf
180 180 get :index, :format => 'pdf'
181 181 assert_response :success
182 182 assert_not_nil assigns(:issues)
183 183 assert_equal 'application/pdf', @response.content_type
184 184
185 185 get :index, :project_id => 1, :format => 'pdf'
186 186 assert_response :success
187 187 assert_not_nil assigns(:issues)
188 188 assert_equal 'application/pdf', @response.content_type
189 189
190 190 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
191 191 assert_response :success
192 192 assert_not_nil assigns(:issues)
193 193 assert_equal 'application/pdf', @response.content_type
194 194 end
195 195
196 196 def test_index_pdf_with_query_grouped_by_list_custom_field
197 197 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
198 198 assert_response :success
199 199 assert_not_nil assigns(:issues)
200 200 assert_not_nil assigns(:issue_count_by_group)
201 201 assert_equal 'application/pdf', @response.content_type
202 202 end
203 203
204 204 def test_index_sort
205 205 get :index, :sort => 'tracker,id:desc'
206 206 assert_response :success
207 207
208 208 sort_params = @request.session['issues_index_sort']
209 209 assert sort_params.is_a?(String)
210 210 assert_equal 'tracker,id:desc', sort_params
211 211
212 212 issues = assigns(:issues)
213 213 assert_not_nil issues
214 214 assert !issues.empty?
215 215 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
216 216 end
217 217
218 218 def test_index_with_columns
219 219 columns = ['tracker', 'subject', 'assigned_to']
220 220 get :index, :set_filter => 1, :query => { 'column_names' => columns}
221 221 assert_response :success
222 222
223 223 # query should use specified columns
224 224 query = assigns(:query)
225 225 assert_kind_of Query, query
226 226 assert_equal columns, query.column_names.map(&:to_s)
227 227
228 228 # columns should be stored in session
229 229 assert_kind_of Hash, session[:query]
230 230 assert_kind_of Array, session[:query][:column_names]
231 231 assert_equal columns, session[:query][:column_names].map(&:to_s)
232 232 end
233 233
234 234 def test_show_by_anonymous
235 235 get :show, :id => 1
236 236 assert_response :success
237 237 assert_template 'show.rhtml'
238 238 assert_not_nil assigns(:issue)
239 239 assert_equal Issue.find(1), assigns(:issue)
240 240
241 241 # anonymous role is allowed to add a note
242 242 assert_tag :tag => 'form',
243 243 :descendant => { :tag => 'fieldset',
244 244 :child => { :tag => 'legend',
245 245 :content => /Notes/ } }
246 246 end
247 247
248 248 def test_show_by_manager
249 249 @request.session[:user_id] = 2
250 250 get :show, :id => 1
251 251 assert_response :success
252 252
253 253 assert_tag :tag => 'form',
254 254 :descendant => { :tag => 'fieldset',
255 255 :child => { :tag => 'legend',
256 256 :content => /Change properties/ } },
257 257 :descendant => { :tag => 'fieldset',
258 258 :child => { :tag => 'legend',
259 259 :content => /Log time/ } },
260 260 :descendant => { :tag => 'fieldset',
261 261 :child => { :tag => 'legend',
262 262 :content => /Notes/ } }
263 263 end
264 264
265 265 def test_show_should_deny_anonymous_access_without_permission
266 266 Role.anonymous.remove_permission!(:view_issues)
267 267 get :show, :id => 1
268 268 assert_response :redirect
269 269 end
270 270
271 271 def test_show_should_deny_non_member_access_without_permission
272 272 Role.non_member.remove_permission!(:view_issues)
273 273 @request.session[:user_id] = 9
274 274 get :show, :id => 1
275 275 assert_response 403
276 276 end
277 277
278 278 def test_show_should_deny_member_access_without_permission
279 279 Role.find(1).remove_permission!(:view_issues)
280 280 @request.session[:user_id] = 2
281 281 get :show, :id => 1
282 282 assert_response 403
283 283 end
284 284
285 285 def test_show_should_not_disclose_relations_to_invisible_issues
286 286 Setting.cross_project_issue_relations = '1'
287 287 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
288 288 # Relation to a private project issue
289 289 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
290 290
291 291 get :show, :id => 1
292 292 assert_response :success
293 293
294 294 assert_tag :div, :attributes => { :id => 'relations' },
295 295 :descendant => { :tag => 'a', :content => /#2$/ }
296 296 assert_no_tag :div, :attributes => { :id => 'relations' },
297 297 :descendant => { :tag => 'a', :content => /#4$/ }
298 298 end
299 299
300 300 def test_show_atom
301 301 get :show, :id => 2, :format => 'atom'
302 302 assert_response :success
303 303 assert_template 'journals/index.rxml'
304 304 # Inline image
305 305 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
306 306 end
307 307
308 308 def test_show_export_to_pdf
309 309 get :show, :id => 3, :format => 'pdf'
310 310 assert_response :success
311 311 assert_equal 'application/pdf', @response.content_type
312 312 assert @response.body.starts_with?('%PDF')
313 313 assert_not_nil assigns(:issue)
314 314 end
315 315
316 316 def test_get_new
317 317 @request.session[:user_id] = 2
318 318 get :new, :project_id => 1, :tracker_id => 1
319 319 assert_response :success
320 320 assert_template 'new'
321 321
322 322 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
323 323 :value => 'Default string' }
324 324 end
325 325
326 326 def test_get_new_without_tracker_id
327 327 @request.session[:user_id] = 2
328 328 get :new, :project_id => 1
329 329 assert_response :success
330 330 assert_template 'new'
331 331
332 332 issue = assigns(:issue)
333 333 assert_not_nil issue
334 334 assert_equal Project.find(1).trackers.first, issue.tracker
335 335 end
336 336
337 337 def test_get_new_with_no_default_status_should_display_an_error
338 338 @request.session[:user_id] = 2
339 339 IssueStatus.delete_all
340 340
341 341 get :new, :project_id => 1
342 342 assert_response 500
343 343 assert_not_nil flash[:error]
344 344 assert_tag :tag => 'div', :attributes => { :class => /error/ },
345 345 :content => /No default issue/
346 346 end
347 347
348 348 def test_get_new_with_no_tracker_should_display_an_error
349 349 @request.session[:user_id] = 2
350 350 Tracker.delete_all
351 351
352 352 get :new, :project_id => 1
353 353 assert_response 500
354 354 assert_not_nil flash[:error]
355 355 assert_tag :tag => 'div', :attributes => { :class => /error/ },
356 356 :content => /No tracker/
357 357 end
358 358
359 359 def test_update_new_form
360 360 @request.session[:user_id] = 2
361 361 xhr :post, :new, :project_id => 1,
362 362 :issue => {:tracker_id => 2,
363 363 :subject => 'This is the test_new issue',
364 364 :description => 'This is the description',
365 365 :priority_id => 5}
366 366 assert_response :success
367 367 assert_template 'attributes'
368 368
369 369 issue = assigns(:issue)
370 370 assert_kind_of Issue, issue
371 371 assert_equal 1, issue.project_id
372 372 assert_equal 2, issue.tracker_id
373 373 assert_equal 'This is the test_new issue', issue.subject
374 374 end
375 375
376 376 def test_post_create
377 377 @request.session[:user_id] = 2
378 378 assert_difference 'Issue.count' do
379 379 post :create, :project_id => 1,
380 380 :issue => {:tracker_id => 3,
381 381 :status_id => 2,
382 382 :subject => 'This is the test_new issue',
383 383 :description => 'This is the description',
384 384 :priority_id => 5,
385 385 :estimated_hours => '',
386 386 :custom_field_values => {'2' => 'Value for field 2'}}
387 387 end
388 388 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
389 389
390 390 issue = Issue.find_by_subject('This is the test_new issue')
391 391 assert_not_nil issue
392 392 assert_equal 2, issue.author_id
393 393 assert_equal 3, issue.tracker_id
394 394 assert_equal 2, issue.status_id
395 395 assert_nil issue.estimated_hours
396 396 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
397 397 assert_not_nil v
398 398 assert_equal 'Value for field 2', v.value
399 399 end
400 400
401 401 def test_post_create_and_continue
402 402 @request.session[:user_id] = 2
403 403 post :create, :project_id => 1,
404 404 :issue => {:tracker_id => 3,
405 405 :subject => 'This is first issue',
406 406 :priority_id => 5},
407 407 :continue => ''
408 408 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook',
409 409 :issue => {:tracker_id => 3}
410 410 end
411 411
412 412 def test_post_create_without_custom_fields_param
413 413 @request.session[:user_id] = 2
414 414 assert_difference 'Issue.count' do
415 415 post :create, :project_id => 1,
416 416 :issue => {:tracker_id => 1,
417 417 :subject => 'This is the test_new issue',
418 418 :description => 'This is the description',
419 419 :priority_id => 5}
420 420 end
421 421 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
422 422 end
423 423
424 424 def test_post_create_with_required_custom_field_and_without_custom_fields_param
425 425 field = IssueCustomField.find_by_name('Database')
426 426 field.update_attribute(:is_required, true)
427 427
428 428 @request.session[:user_id] = 2
429 429 post :create, :project_id => 1,
430 430 :issue => {:tracker_id => 1,
431 431 :subject => 'This is the test_new issue',
432 432 :description => 'This is the description',
433 433 :priority_id => 5}
434 434 assert_response :success
435 435 assert_template 'new'
436 436 issue = assigns(:issue)
437 437 assert_not_nil issue
438 438 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
439 439 end
440 440
441 441 def test_post_create_with_watchers
442 442 @request.session[:user_id] = 2
443 443 ActionMailer::Base.deliveries.clear
444 444
445 445 assert_difference 'Watcher.count', 2 do
446 446 post :create, :project_id => 1,
447 447 :issue => {:tracker_id => 1,
448 448 :subject => 'This is a new issue with watchers',
449 449 :description => 'This is the description',
450 450 :priority_id => 5,
451 451 :watcher_user_ids => ['2', '3']}
452 452 end
453 453 issue = Issue.find_by_subject('This is a new issue with watchers')
454 454 assert_not_nil issue
455 455 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
456 456
457 457 # Watchers added
458 458 assert_equal [2, 3], issue.watcher_user_ids.sort
459 459 assert issue.watched_by?(User.find(3))
460 460 # Watchers notified
461 461 mail = ActionMailer::Base.deliveries.last
462 462 assert_kind_of TMail::Mail, mail
463 463 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
464 464 end
465 465
466 466 def test_post_create_subissue
467 467 @request.session[:user_id] = 2
468 468
469 469 assert_difference 'Issue.count' do
470 470 post :create, :project_id => 1,
471 471 :issue => {:tracker_id => 1,
472 472 :subject => 'This is a child issue',
473 473 :parent_issue_id => 2}
474 474 end
475 475 issue = Issue.find_by_subject('This is a child issue')
476 476 assert_not_nil issue
477 477 assert_equal Issue.find(2), issue.parent
478 478 end
479 479
480 480 def test_post_create_should_send_a_notification
481 481 ActionMailer::Base.deliveries.clear
482 482 @request.session[:user_id] = 2
483 483 assert_difference 'Issue.count' do
484 484 post :create, :project_id => 1,
485 485 :issue => {:tracker_id => 3,
486 486 :subject => 'This is the test_new issue',
487 487 :description => 'This is the description',
488 488 :priority_id => 5,
489 489 :estimated_hours => '',
490 490 :custom_field_values => {'2' => 'Value for field 2'}}
491 491 end
492 492 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
493 493
494 494 assert_equal 1, ActionMailer::Base.deliveries.size
495 495 end
496 496
497 497 def test_post_create_should_preserve_fields_values_on_validation_failure
498 498 @request.session[:user_id] = 2
499 499 post :create, :project_id => 1,
500 500 :issue => {:tracker_id => 1,
501 501 # empty subject
502 502 :subject => '',
503 503 :description => 'This is a description',
504 504 :priority_id => 6,
505 505 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
506 506 assert_response :success
507 507 assert_template 'new'
508 508
509 509 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
510 510 :content => 'This is a description'
511 511 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
512 512 :child => { :tag => 'option', :attributes => { :selected => 'selected',
513 513 :value => '6' },
514 514 :content => 'High' }
515 515 # Custom fields
516 516 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
517 517 :child => { :tag => 'option', :attributes => { :selected => 'selected',
518 518 :value => 'Oracle' },
519 519 :content => 'Oracle' }
520 520 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
521 521 :value => 'Value for field 2'}
522 522 end
523 523
524 524 def test_post_create_should_ignore_non_safe_attributes
525 525 @request.session[:user_id] = 2
526 526 assert_nothing_raised do
527 527 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
528 528 end
529 529 end
530 530
531 531 context "without workflow privilege" do
532 532 setup do
533 533 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
534 534 Role.anonymous.add_permission! :add_issues
535 535 end
536 536
537 537 context "#new" do
538 538 should "propose default status only" do
539 539 get :new, :project_id => 1
540 540 assert_response :success
541 541 assert_template 'new'
542 542 assert_tag :tag => 'select',
543 543 :attributes => {:name => 'issue[status_id]'},
544 544 :children => {:count => 1},
545 545 :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}}
546 546 end
547 547
548 548 should "accept default status" do
549 549 assert_difference 'Issue.count' do
550 550 post :create, :project_id => 1,
551 551 :issue => {:tracker_id => 1,
552 552 :subject => 'This is an issue',
553 553 :status_id => 1}
554 554 end
555 555 issue = Issue.last(:order => 'id')
556 556 assert_equal IssueStatus.default, issue.status
557 557 end
558 558
559 559 should "ignore unauthorized status" do
560 560 assert_difference 'Issue.count' do
561 561 post :create, :project_id => 1,
562 562 :issue => {:tracker_id => 1,
563 563 :subject => 'This is an issue',
564 564 :status_id => 3}
565 565 end
566 566 issue = Issue.last(:order => 'id')
567 567 assert_equal IssueStatus.default, issue.status
568 568 end
569 569 end
570 570 end
571 571
572 572 def test_copy_issue
573 573 @request.session[:user_id] = 2
574 574 get :new, :project_id => 1, :copy_from => 1
575 575 assert_template 'new'
576 576 assert_not_nil assigns(:issue)
577 577 orig = Issue.find(1)
578 578 assert_equal orig.subject, assigns(:issue).subject
579 579 end
580 580
581 581 def test_get_edit
582 582 @request.session[:user_id] = 2
583 583 get :edit, :id => 1
584 584 assert_response :success
585 585 assert_template 'edit'
586 586 assert_not_nil assigns(:issue)
587 587 assert_equal Issue.find(1), assigns(:issue)
588 588 end
589 589
590 590 def test_get_edit_with_params
591 591 @request.session[:user_id] = 2
592 592 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 }
593 593 assert_response :success
594 594 assert_template 'edit'
595 595
596 596 issue = assigns(:issue)
597 597 assert_not_nil issue
598 598
599 599 assert_equal 5, issue.status_id
600 600 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
601 601 :child => { :tag => 'option',
602 602 :content => 'Closed',
603 603 :attributes => { :selected => 'selected' } }
604 604
605 605 assert_equal 7, issue.priority_id
606 606 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
607 607 :child => { :tag => 'option',
608 608 :content => 'Urgent',
609 609 :attributes => { :selected => 'selected' } }
610 610 end
611 611
612 612 def test_update_edit_form
613 613 @request.session[:user_id] = 2
614 614 xhr :post, :new, :project_id => 1,
615 615 :id => 1,
616 616 :issue => {:tracker_id => 2,
617 617 :subject => 'This is the test_new issue',
618 618 :description => 'This is the description',
619 619 :priority_id => 5}
620 620 assert_response :success
621 621 assert_template 'attributes'
622 622
623 623 issue = assigns(:issue)
624 624 assert_kind_of Issue, issue
625 625 assert_equal 1, issue.id
626 626 assert_equal 1, issue.project_id
627 627 assert_equal 2, issue.tracker_id
628 628 assert_equal 'This is the test_new issue', issue.subject
629 629 end
630 630
631 631 def test_update_using_invalid_http_verbs
632 632 @request.session[:user_id] = 2
633 633 subject = 'Updated by an invalid http verb'
634 634
635 635 get :update, :id => 1, :issue => {:subject => subject}
636 636 assert_not_equal subject, Issue.find(1).subject
637 637
638 638 post :update, :id => 1, :issue => {:subject => subject}
639 639 assert_not_equal subject, Issue.find(1).subject
640 640
641 641 delete :update, :id => 1, :issue => {:subject => subject}
642 642 assert_not_equal subject, Issue.find(1).subject
643 643 end
644 644
645 645 def test_put_update_without_custom_fields_param
646 646 @request.session[:user_id] = 2
647 647 ActionMailer::Base.deliveries.clear
648 648
649 649 issue = Issue.find(1)
650 650 assert_equal '125', issue.custom_value_for(2).value
651 651 old_subject = issue.subject
652 652 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
653 653
654 654 assert_difference('Journal.count') do
655 655 assert_difference('JournalDetail.count', 2) do
656 656 put :update, :id => 1, :issue => {:subject => new_subject,
657 657 :priority_id => '6',
658 658 :category_id => '1' # no change
659 659 }
660 660 end
661 661 end
662 662 assert_redirected_to :action => 'show', :id => '1'
663 663 issue.reload
664 664 assert_equal new_subject, issue.subject
665 665 # Make sure custom fields were not cleared
666 666 assert_equal '125', issue.custom_value_for(2).value
667 667
668 668 mail = ActionMailer::Base.deliveries.last
669 669 assert_kind_of TMail::Mail, mail
670 670 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
671 671 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
672 672 end
673 673
674 674 def test_put_update_with_custom_field_change
675 675 @request.session[:user_id] = 2
676 676 issue = Issue.find(1)
677 677 assert_equal '125', issue.custom_value_for(2).value
678 678
679 679 assert_difference('Journal.count') do
680 680 assert_difference('JournalDetail.count', 3) do
681 681 put :update, :id => 1, :issue => {:subject => 'Custom field change',
682 682 :priority_id => '6',
683 683 :category_id => '1', # no change
684 684 :custom_field_values => { '2' => 'New custom value' }
685 685 }
686 686 end
687 687 end
688 688 assert_redirected_to :action => 'show', :id => '1'
689 689 issue.reload
690 690 assert_equal 'New custom value', issue.custom_value_for(2).value
691 691
692 692 mail = ActionMailer::Base.deliveries.last
693 693 assert_kind_of TMail::Mail, mail
694 694 assert mail.body.include?("Searchable field changed from 125 to New custom value")
695 695 end
696 696
697 697 def test_put_update_with_status_and_assignee_change
698 698 issue = Issue.find(1)
699 699 assert_equal 1, issue.status_id
700 700 @request.session[:user_id] = 2
701 701 assert_difference('TimeEntry.count', 0) do
702 702 put :update,
703 703 :id => 1,
704 704 :issue => { :status_id => 2, :assigned_to_id => 3 },
705 705 :notes => 'Assigned to dlopper',
706 706 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
707 707 end
708 708 assert_redirected_to :action => 'show', :id => '1'
709 709 issue.reload
710 710 assert_equal 2, issue.status_id
711 711 j = Journal.find(:first, :order => 'id DESC')
712 712 assert_equal 'Assigned to dlopper', j.notes
713 713 assert_equal 2, j.details.size
714 714
715 715 mail = ActionMailer::Base.deliveries.last
716 716 assert mail.body.include?("Status changed from New to Assigned")
717 717 # subject should contain the new status
718 718 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
719 719 end
720 720
721 721 def test_put_update_with_note_only
722 722 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
723 723 # anonymous user
724 724 put :update,
725 725 :id => 1,
726 726 :notes => notes
727 727 assert_redirected_to :action => 'show', :id => '1'
728 728 j = Journal.find(:first, :order => 'id DESC')
729 729 assert_equal notes, j.notes
730 730 assert_equal 0, j.details.size
731 731 assert_equal User.anonymous, j.user
732 732
733 733 mail = ActionMailer::Base.deliveries.last
734 734 assert mail.body.include?(notes)
735 735 end
736 736
737 737 def test_put_update_with_note_and_spent_time
738 738 @request.session[:user_id] = 2
739 739 spent_hours_before = Issue.find(1).spent_hours
740 740 assert_difference('TimeEntry.count') do
741 741 put :update,
742 742 :id => 1,
743 743 :notes => '2.5 hours added',
744 744 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
745 745 end
746 746 assert_redirected_to :action => 'show', :id => '1'
747 747
748 748 issue = Issue.find(1)
749 749
750 750 j = Journal.find(:first, :order => 'id DESC')
751 751 assert_equal '2.5 hours added', j.notes
752 752 assert_equal 0, j.details.size
753 753
754 754 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
755 755 assert_not_nil t
756 756 assert_equal 2.5, t.hours
757 757 assert_equal spent_hours_before + 2.5, issue.spent_hours
758 758 end
759 759
760 760 def test_put_update_with_attachment_only
761 761 set_tmp_attachments_directory
762 762
763 763 # Delete all fixtured journals, a race condition can occur causing the wrong
764 764 # journal to get fetched in the next find.
765 765 Journal.delete_all
766 766
767 767 # anonymous user
768 768 put :update,
769 769 :id => 1,
770 770 :notes => '',
771 771 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
772 772 assert_redirected_to :action => 'show', :id => '1'
773 773 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
774 774 assert j.notes.blank?
775 775 assert_equal 1, j.details.size
776 776 assert_equal 'testfile.txt', j.details.first.value
777 777 assert_equal User.anonymous, j.user
778 778
779 779 mail = ActionMailer::Base.deliveries.last
780 780 assert mail.body.include?('testfile.txt')
781 781 end
782 782
783 783 def test_put_update_with_attachment_that_fails_to_save
784 784 set_tmp_attachments_directory
785 785
786 786 # Delete all fixtured journals, a race condition can occur causing the wrong
787 787 # journal to get fetched in the next find.
788 788 Journal.delete_all
789 789
790 790 # Mock out the unsaved attachment
791 791 Attachment.any_instance.stubs(:create).returns(Attachment.new)
792 792
793 793 # anonymous user
794 794 put :update,
795 795 :id => 1,
796 796 :notes => '',
797 797 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
798 798 assert_redirected_to :action => 'show', :id => '1'
799 799 assert_equal '1 file(s) could not be saved.', flash[:warning]
800 800
801 801 end if Object.const_defined?(:Mocha)
802 802
803 803 def test_put_update_with_no_change
804 804 issue = Issue.find(1)
805 805 issue.journals.clear
806 806 ActionMailer::Base.deliveries.clear
807 807
808 808 put :update,
809 809 :id => 1,
810 810 :notes => ''
811 811 assert_redirected_to :action => 'show', :id => '1'
812 812
813 813 issue.reload
814 814 assert issue.journals.empty?
815 815 # No email should be sent
816 816 assert ActionMailer::Base.deliveries.empty?
817 817 end
818 818
819 819 def test_put_update_should_send_a_notification
820 820 @request.session[:user_id] = 2
821 821 ActionMailer::Base.deliveries.clear
822 822 issue = Issue.find(1)
823 823 old_subject = issue.subject
824 824 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
825 825
826 826 put :update, :id => 1, :issue => {:subject => new_subject,
827 827 :priority_id => '6',
828 828 :category_id => '1' # no change
829 829 }
830 830 assert_equal 1, ActionMailer::Base.deliveries.size
831 831 end
832 832
833 833 def test_put_update_with_invalid_spent_time
834 834 @request.session[:user_id] = 2
835 835 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
836 836
837 837 assert_no_difference('Journal.count') do
838 838 put :update,
839 839 :id => 1,
840 840 :notes => notes,
841 841 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
842 842 end
843 843 assert_response :success
844 844 assert_template 'edit'
845 845
846 846 assert_tag :textarea, :attributes => { :name => 'notes' },
847 847 :content => notes
848 848 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
849 849 end
850 850
851 851 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
852 852 issue = Issue.find(2)
853 853 @request.session[:user_id] = 2
854 854
855 855 put :update,
856 856 :id => issue.id,
857 857 :issue => {
858 858 :fixed_version_id => 4
859 859 }
860 860
861 861 assert_response :redirect
862 862 issue.reload
863 863 assert_equal 4, issue.fixed_version_id
864 864 assert_not_equal issue.project_id, issue.fixed_version.project_id
865 865 end
866 866
867 867 def test_put_update_should_redirect_back_using_the_back_url_parameter
868 868 issue = Issue.find(2)
869 869 @request.session[:user_id] = 2
870 870
871 871 put :update,
872 872 :id => issue.id,
873 873 :issue => {
874 874 :fixed_version_id => 4
875 875 },
876 876 :back_url => '/issues'
877 877
878 878 assert_response :redirect
879 879 assert_redirected_to '/issues'
880 880 end
881 881
882 882 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
883 883 issue = Issue.find(2)
884 884 @request.session[:user_id] = 2
885 885
886 886 put :update,
887 887 :id => issue.id,
888 888 :issue => {
889 889 :fixed_version_id => 4
890 890 },
891 891 :back_url => 'http://google.com'
892 892
893 893 assert_response :redirect
894 894 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
895 895 end
896 896
897 897 def test_get_bulk_edit
898 898 @request.session[:user_id] = 2
899 899 get :bulk_edit, :ids => [1, 2]
900 900 assert_response :success
901 901 assert_template 'bulk_edit'
902 902
903 903 # Project specific custom field, date type
904 904 field = CustomField.find(9)
905 905 assert !field.is_for_all?
906 906 assert_equal 'date', field.field_format
907 907 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
908 908
909 909 # System wide custom field
910 910 assert CustomField.find(1).is_for_all?
911 911 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
912 912 end
913 913
914 914 def test_bulk_update
915 915 @request.session[:user_id] = 2
916 916 # update issues priority
917 917 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
918 918 :issue => {:priority_id => 7,
919 919 :assigned_to_id => '',
920 920 :custom_field_values => {'2' => ''}}
921 921
922 922 assert_response 302
923 923 # check that the issues were updated
924 924 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
925 925
926 926 issue = Issue.find(1)
927 927 journal = issue.journals.find(:first, :order => 'created_on DESC')
928 928 assert_equal '125', issue.custom_value_for(2).value
929 929 assert_equal 'Bulk editing', journal.notes
930 930 assert_equal 1, journal.details.size
931 931 end
932 932
933 933 def test_bullk_update_should_send_a_notification
934 934 @request.session[:user_id] = 2
935 935 ActionMailer::Base.deliveries.clear
936 936 post(:bulk_update,
937 937 {
938 938 :ids => [1, 2],
939 939 :notes => 'Bulk editing',
940 940 :issue => {
941 941 :priority_id => 7,
942 942 :assigned_to_id => '',
943 943 :custom_field_values => {'2' => ''}
944 944 }
945 945 })
946 946
947 947 assert_response 302
948 948 assert_equal 2, ActionMailer::Base.deliveries.size
949 949 end
950 950
951 951 def test_bulk_update_status
952 952 @request.session[:user_id] = 2
953 953 # update issues priority
954 954 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
955 955 :issue => {:priority_id => '',
956 956 :assigned_to_id => '',
957 957 :status_id => '5'}
958 958
959 959 assert_response 302
960 960 issue = Issue.find(1)
961 961 assert issue.closed?
962 962 end
963 963
964 964 def test_bulk_update_custom_field
965 965 @request.session[:user_id] = 2
966 966 # update issues priority
967 967 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
968 968 :issue => {:priority_id => '',
969 969 :assigned_to_id => '',
970 970 :custom_field_values => {'2' => '777'}}
971 971
972 972 assert_response 302
973 973
974 974 issue = Issue.find(1)
975 975 journal = issue.journals.find(:first, :order => 'created_on DESC')
976 976 assert_equal '777', issue.custom_value_for(2).value
977 977 assert_equal 1, journal.details.size
978 978 assert_equal '125', journal.details.first.old_value
979 979 assert_equal '777', journal.details.first.value
980 980 end
981 981
982 982 def test_bulk_update_unassign
983 983 assert_not_nil Issue.find(2).assigned_to
984 984 @request.session[:user_id] = 2
985 985 # unassign issues
986 986 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
987 987 assert_response 302
988 988 # check that the issues were updated
989 989 assert_nil Issue.find(2).assigned_to
990 990 end
991 991
992 992 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
993 993 @request.session[:user_id] = 2
994 994
995 995 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
996 996
997 997 assert_response :redirect
998 998 issues = Issue.find([1,2])
999 999 issues.each do |issue|
1000 1000 assert_equal 4, issue.fixed_version_id
1001 1001 assert_not_equal issue.project_id, issue.fixed_version.project_id
1002 1002 end
1003 1003 end
1004 1004
1005 1005 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
1006 1006 @request.session[:user_id] = 2
1007 1007 post :bulk_update, :ids => [1,2], :back_url => '/issues'
1008 1008
1009 1009 assert_response :redirect
1010 1010 assert_redirected_to '/issues'
1011 1011 end
1012 1012
1013 1013 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1014 1014 @request.session[:user_id] = 2
1015 1015 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
1016 1016
1017 1017 assert_response :redirect
1018 1018 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
1019 1019 end
1020 1020
1021 1021 def test_destroy_issue_with_no_time_entries
1022 1022 assert_nil TimeEntry.find_by_issue_id(2)
1023 1023 @request.session[:user_id] = 2
1024 1024 post :destroy, :id => 2
1025 1025 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1026 1026 assert_nil Issue.find_by_id(2)
1027 1027 end
1028 1028
1029 1029 def test_destroy_issues_with_time_entries
1030 1030 @request.session[:user_id] = 2
1031 1031 post :destroy, :ids => [1, 3]
1032 1032 assert_response :success
1033 1033 assert_template 'destroy'
1034 1034 assert_not_nil assigns(:hours)
1035 1035 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1036 1036 end
1037 1037
1038 1038 def test_destroy_issues_and_destroy_time_entries
1039 1039 @request.session[:user_id] = 2
1040 1040 post :destroy, :ids => [1, 3], :todo => 'destroy'
1041 1041 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1042 1042 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1043 1043 assert_nil TimeEntry.find_by_id([1, 2])
1044 1044 end
1045 1045
1046 1046 def test_destroy_issues_and_assign_time_entries_to_project
1047 1047 @request.session[:user_id] = 2
1048 1048 post :destroy, :ids => [1, 3], :todo => 'nullify'
1049 1049 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1050 1050 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1051 1051 assert_nil TimeEntry.find(1).issue_id
1052 1052 assert_nil TimeEntry.find(2).issue_id
1053 1053 end
1054 1054
1055 1055 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1056 1056 @request.session[:user_id] = 2
1057 1057 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1058 1058 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1059 1059 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1060 1060 assert_equal 2, TimeEntry.find(1).issue_id
1061 1061 assert_equal 2, TimeEntry.find(2).issue_id
1062 1062 end
1063 1063
1064 def test_destroy_issues_from_different_projects
1065 @request.session[:user_id] = 2
1066 post :destroy, :ids => [1, 2, 6], :todo => 'destroy'
1067 assert_redirected_to :controller => 'issues', :action => 'index'
1068 assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6))
1069 end
1070
1064 1071 def test_default_search_scope
1065 1072 get :index
1066 1073 assert_tag :div, :attributes => {:id => 'quick-search'},
1067 1074 :child => {:tag => 'form',
1068 1075 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1069 1076 end
1070 1077 end
General Comments 0
You need to be logged in to leave comments. Login now