##// END OF EJS Templates
Code cleanup (#23054)....
Jean-Philippe Lang -
r15152:f694839c8242
parent child
Show More
@@ -1,662 +1,678
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'uri'
18 require 'uri'
19 require 'cgi'
19 require 'cgi'
20
20
21 class Unauthorized < Exception; end
21 class Unauthorized < Exception; end
22
22
23 class ApplicationController < ActionController::Base
23 class ApplicationController < ActionController::Base
24 include Redmine::I18n
24 include Redmine::I18n
25 include Redmine::Pagination
25 include Redmine::Pagination
26 include Redmine::Hook::Helper
26 include Redmine::Hook::Helper
27 include RoutesHelper
27 include RoutesHelper
28 helper :routes
28 helper :routes
29
29
30 class_attribute :accept_api_auth_actions
30 class_attribute :accept_api_auth_actions
31 class_attribute :accept_rss_auth_actions
31 class_attribute :accept_rss_auth_actions
32 class_attribute :model_object
32 class_attribute :model_object
33
33
34 layout 'base'
34 layout 'base'
35
35
36 protect_from_forgery
36 protect_from_forgery
37
37
38 def verify_authenticity_token
38 def verify_authenticity_token
39 unless api_request?
39 unless api_request?
40 super
40 super
41 end
41 end
42 end
42 end
43
43
44 def handle_unverified_request
44 def handle_unverified_request
45 unless api_request?
45 unless api_request?
46 super
46 super
47 cookies.delete(autologin_cookie_name)
47 cookies.delete(autologin_cookie_name)
48 self.logged_user = nil
48 self.logged_user = nil
49 set_localization
49 set_localization
50 render_error :status => 422, :message => "Invalid form authenticity token."
50 render_error :status => 422, :message => "Invalid form authenticity token."
51 end
51 end
52 end
52 end
53
53
54 before_filter :session_expiration, :user_setup, :check_if_login_required, :check_password_change, :set_localization
54 before_filter :session_expiration, :user_setup, :check_if_login_required, :check_password_change, :set_localization
55
55
56 rescue_from ::Unauthorized, :with => :deny_access
56 rescue_from ::Unauthorized, :with => :deny_access
57 rescue_from ::ActionView::MissingTemplate, :with => :missing_template
57 rescue_from ::ActionView::MissingTemplate, :with => :missing_template
58
58
59 include Redmine::Search::Controller
59 include Redmine::Search::Controller
60 include Redmine::MenuManager::MenuController
60 include Redmine::MenuManager::MenuController
61 helper Redmine::MenuManager::MenuHelper
61 helper Redmine::MenuManager::MenuHelper
62
62
63 include Redmine::SudoMode::Controller
63 include Redmine::SudoMode::Controller
64
64
65 def session_expiration
65 def session_expiration
66 if session[:user_id] && Rails.application.config.redmine_verify_sessions != false
66 if session[:user_id] && Rails.application.config.redmine_verify_sessions != false
67 if session_expired? && !try_to_autologin
67 if session_expired? && !try_to_autologin
68 set_localization(User.active.find_by_id(session[:user_id]))
68 set_localization(User.active.find_by_id(session[:user_id]))
69 self.logged_user = nil
69 self.logged_user = nil
70 flash[:error] = l(:error_session_expired)
70 flash[:error] = l(:error_session_expired)
71 require_login
71 require_login
72 end
72 end
73 end
73 end
74 end
74 end
75
75
76 def session_expired?
76 def session_expired?
77 ! User.verify_session_token(session[:user_id], session[:tk])
77 ! User.verify_session_token(session[:user_id], session[:tk])
78 end
78 end
79
79
80 def start_user_session(user)
80 def start_user_session(user)
81 session[:user_id] = user.id
81 session[:user_id] = user.id
82 session[:tk] = user.generate_session_token
82 session[:tk] = user.generate_session_token
83 if user.must_change_password?
83 if user.must_change_password?
84 session[:pwd] = '1'
84 session[:pwd] = '1'
85 end
85 end
86 end
86 end
87
87
88 def user_setup
88 def user_setup
89 # Check the settings cache for each request
89 # Check the settings cache for each request
90 Setting.check_cache
90 Setting.check_cache
91 # Find the current user
91 # Find the current user
92 User.current = find_current_user
92 User.current = find_current_user
93 logger.info(" Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger
93 logger.info(" Current user: " + (User.current.logged? ? "#{User.current.login} (id=#{User.current.id})" : "anonymous")) if logger
94 end
94 end
95
95
96 # Returns the current user or nil if no user is logged in
96 # Returns the current user or nil if no user is logged in
97 # and starts a session if needed
97 # and starts a session if needed
98 def find_current_user
98 def find_current_user
99 user = nil
99 user = nil
100 unless api_request?
100 unless api_request?
101 if session[:user_id]
101 if session[:user_id]
102 # existing session
102 # existing session
103 user = (User.active.find(session[:user_id]) rescue nil)
103 user = (User.active.find(session[:user_id]) rescue nil)
104 elsif autologin_user = try_to_autologin
104 elsif autologin_user = try_to_autologin
105 user = autologin_user
105 user = autologin_user
106 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
106 elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
107 # RSS key authentication does not start a session
107 # RSS key authentication does not start a session
108 user = User.find_by_rss_key(params[:key])
108 user = User.find_by_rss_key(params[:key])
109 end
109 end
110 end
110 end
111 if user.nil? && Setting.rest_api_enabled? && accept_api_auth?
111 if user.nil? && Setting.rest_api_enabled? && accept_api_auth?
112 if (key = api_key_from_request)
112 if (key = api_key_from_request)
113 # Use API key
113 # Use API key
114 user = User.find_by_api_key(key)
114 user = User.find_by_api_key(key)
115 elsif request.authorization.to_s =~ /\ABasic /i
115 elsif request.authorization.to_s =~ /\ABasic /i
116 # HTTP Basic, either username/password or API key/random
116 # HTTP Basic, either username/password or API key/random
117 authenticate_with_http_basic do |username, password|
117 authenticate_with_http_basic do |username, password|
118 user = User.try_to_login(username, password) || User.find_by_api_key(username)
118 user = User.try_to_login(username, password) || User.find_by_api_key(username)
119 end
119 end
120 if user && user.must_change_password?
120 if user && user.must_change_password?
121 render_error :message => 'You must change your password', :status => 403
121 render_error :message => 'You must change your password', :status => 403
122 return
122 return
123 end
123 end
124 end
124 end
125 # Switch user if requested by an admin user
125 # Switch user if requested by an admin user
126 if user && user.admin? && (username = api_switch_user_from_request)
126 if user && user.admin? && (username = api_switch_user_from_request)
127 su = User.find_by_login(username)
127 su = User.find_by_login(username)
128 if su && su.active?
128 if su && su.active?
129 logger.info(" User switched by: #{user.login} (id=#{user.id})") if logger
129 logger.info(" User switched by: #{user.login} (id=#{user.id})") if logger
130 user = su
130 user = su
131 else
131 else
132 render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412
132 render_error :message => 'Invalid X-Redmine-Switch-User header', :status => 412
133 end
133 end
134 end
134 end
135 end
135 end
136 # store current ip address in user object ephemerally
136 # store current ip address in user object ephemerally
137 user.remote_ip = request.remote_ip if user
137 user.remote_ip = request.remote_ip if user
138 user
138 user
139 end
139 end
140
140
141 def autologin_cookie_name
141 def autologin_cookie_name
142 Redmine::Configuration['autologin_cookie_name'].presence || 'autologin'
142 Redmine::Configuration['autologin_cookie_name'].presence || 'autologin'
143 end
143 end
144
144
145 def try_to_autologin
145 def try_to_autologin
146 if cookies[autologin_cookie_name] && Setting.autologin?
146 if cookies[autologin_cookie_name] && Setting.autologin?
147 # auto-login feature starts a new session
147 # auto-login feature starts a new session
148 user = User.try_to_autologin(cookies[autologin_cookie_name])
148 user = User.try_to_autologin(cookies[autologin_cookie_name])
149 if user
149 if user
150 reset_session
150 reset_session
151 start_user_session(user)
151 start_user_session(user)
152 end
152 end
153 user
153 user
154 end
154 end
155 end
155 end
156
156
157 # Sets the logged in user
157 # Sets the logged in user
158 def logged_user=(user)
158 def logged_user=(user)
159 reset_session
159 reset_session
160 if user && user.is_a?(User)
160 if user && user.is_a?(User)
161 User.current = user
161 User.current = user
162 start_user_session(user)
162 start_user_session(user)
163 else
163 else
164 User.current = User.anonymous
164 User.current = User.anonymous
165 end
165 end
166 end
166 end
167
167
168 # Logs out current user
168 # Logs out current user
169 def logout_user
169 def logout_user
170 if User.current.logged?
170 if User.current.logged?
171 cookies.delete(autologin_cookie_name)
171 cookies.delete(autologin_cookie_name)
172 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
172 Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
173 Token.delete_all(["user_id = ? AND action = ? AND value = ?", User.current.id, 'session', session[:tk]])
173 Token.delete_all(["user_id = ? AND action = ? AND value = ?", User.current.id, 'session', session[:tk]])
174 self.logged_user = nil
174 self.logged_user = nil
175 end
175 end
176 end
176 end
177
177
178 # check if login is globally required to access the application
178 # check if login is globally required to access the application
179 def check_if_login_required
179 def check_if_login_required
180 # no check needed if user is already logged in
180 # no check needed if user is already logged in
181 return true if User.current.logged?
181 return true if User.current.logged?
182 require_login if Setting.login_required?
182 require_login if Setting.login_required?
183 end
183 end
184
184
185 def check_password_change
185 def check_password_change
186 if session[:pwd]
186 if session[:pwd]
187 if User.current.must_change_password?
187 if User.current.must_change_password?
188 flash[:error] = l(:error_password_expired)
188 flash[:error] = l(:error_password_expired)
189 redirect_to my_password_path
189 redirect_to my_password_path
190 else
190 else
191 session.delete(:pwd)
191 session.delete(:pwd)
192 end
192 end
193 end
193 end
194 end
194 end
195
195
196 def set_localization(user=User.current)
196 def set_localization(user=User.current)
197 lang = nil
197 lang = nil
198 if user && user.logged?
198 if user && user.logged?
199 lang = find_language(user.language)
199 lang = find_language(user.language)
200 end
200 end
201 if lang.nil? && !Setting.force_default_language_for_anonymous? && request.env['HTTP_ACCEPT_LANGUAGE']
201 if lang.nil? && !Setting.force_default_language_for_anonymous? && request.env['HTTP_ACCEPT_LANGUAGE']
202 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
202 accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
203 if !accept_lang.blank?
203 if !accept_lang.blank?
204 accept_lang = accept_lang.downcase
204 accept_lang = accept_lang.downcase
205 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
205 lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
206 end
206 end
207 end
207 end
208 lang ||= Setting.default_language
208 lang ||= Setting.default_language
209 set_language_if_valid(lang)
209 set_language_if_valid(lang)
210 end
210 end
211
211
212 def require_login
212 def require_login
213 if !User.current.logged?
213 if !User.current.logged?
214 # Extract only the basic url parameters on non-GET requests
214 # Extract only the basic url parameters on non-GET requests
215 if request.get?
215 if request.get?
216 url = url_for(params)
216 url = url_for(params)
217 else
217 else
218 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
218 url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
219 end
219 end
220 respond_to do |format|
220 respond_to do |format|
221 format.html {
221 format.html {
222 if request.xhr?
222 if request.xhr?
223 head :unauthorized
223 head :unauthorized
224 else
224 else
225 redirect_to signin_path(:back_url => url)
225 redirect_to signin_path(:back_url => url)
226 end
226 end
227 }
227 }
228 format.any(:atom, :pdf, :csv) {
228 format.any(:atom, :pdf, :csv) {
229 redirect_to signin_path(:back_url => url)
229 redirect_to signin_path(:back_url => url)
230 }
230 }
231 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
231 format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
232 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
232 format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
233 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
233 format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
234 format.any { head :unauthorized }
234 format.any { head :unauthorized }
235 end
235 end
236 return false
236 return false
237 end
237 end
238 true
238 true
239 end
239 end
240
240
241 def require_admin
241 def require_admin
242 return unless require_login
242 return unless require_login
243 if !User.current.admin?
243 if !User.current.admin?
244 render_403
244 render_403
245 return false
245 return false
246 end
246 end
247 true
247 true
248 end
248 end
249
249
250 def deny_access
250 def deny_access
251 User.current.logged? ? render_403 : require_login
251 User.current.logged? ? render_403 : require_login
252 end
252 end
253
253
254 # Authorize the user for the requested action
254 # Authorize the user for the requested action
255 def authorize(ctrl = params[:controller], action = params[:action], global = false)
255 def authorize(ctrl = params[:controller], action = params[:action], global = false)
256 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
256 allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
257 if allowed
257 if allowed
258 true
258 true
259 else
259 else
260 if @project && @project.archived?
260 if @project && @project.archived?
261 render_403 :message => :notice_not_authorized_archived_project
261 render_403 :message => :notice_not_authorized_archived_project
262 else
262 else
263 deny_access
263 deny_access
264 end
264 end
265 end
265 end
266 end
266 end
267
267
268 # Authorize the user for the requested action outside a project
268 # Authorize the user for the requested action outside a project
269 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
269 def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
270 authorize(ctrl, action, global)
270 authorize(ctrl, action, global)
271 end
271 end
272
272
273 # Find project of id params[:id]
273 # Find project of id params[:id]
274 def find_project
274 def find_project
275 @project = Project.find(params[:id])
275 @project = Project.find(params[:id])
276 rescue ActiveRecord::RecordNotFound
276 rescue ActiveRecord::RecordNotFound
277 render_404
277 render_404
278 end
278 end
279
279
280 # Find project of id params[:project_id]
280 # Find project of id params[:project_id]
281 def find_project_by_project_id
281 def find_project_by_project_id
282 @project = Project.find(params[:project_id])
282 @project = Project.find(params[:project_id])
283 rescue ActiveRecord::RecordNotFound
283 rescue ActiveRecord::RecordNotFound
284 render_404
284 render_404
285 end
285 end
286
286
287 # Find a project based on params[:project_id]
287 # Find a project based on params[:project_id]
288 # TODO: some subclasses override this, see about merging their logic
288 # TODO: some subclasses override this, see about merging their logic
289 def find_optional_project
289 def find_optional_project
290 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
290 @project = Project.find(params[:project_id]) unless params[:project_id].blank?
291 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
291 allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
292 allowed ? true : deny_access
292 allowed ? true : deny_access
293 rescue ActiveRecord::RecordNotFound
293 rescue ActiveRecord::RecordNotFound
294 render_404
294 render_404
295 end
295 end
296
296
297 # Finds and sets @project based on @object.project
297 # Finds and sets @project based on @object.project
298 def find_project_from_association
298 def find_project_from_association
299 render_404 unless @object.present?
299 render_404 unless @object.present?
300
300
301 @project = @object.project
301 @project = @object.project
302 end
302 end
303
303
304 def find_model_object
304 def find_model_object
305 model = self.class.model_object
305 model = self.class.model_object
306 if model
306 if model
307 @object = model.find(params[:id])
307 @object = model.find(params[:id])
308 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
308 self.instance_variable_set('@' + controller_name.singularize, @object) if @object
309 end
309 end
310 rescue ActiveRecord::RecordNotFound
310 rescue ActiveRecord::RecordNotFound
311 render_404
311 render_404
312 end
312 end
313
313
314 def self.model_object(model)
314 def self.model_object(model)
315 self.model_object = model
315 self.model_object = model
316 end
316 end
317
317
318 # Find the issue whose id is the :id parameter
318 # Find the issue whose id is the :id parameter
319 # Raises a Unauthorized exception if the issue is not visible
319 # Raises a Unauthorized exception if the issue is not visible
320 def find_issue
320 def find_issue
321 # Issue.visible.find(...) can not be used to redirect user to the login form
321 # Issue.visible.find(...) can not be used to redirect user to the login form
322 # if the issue actually exists but requires authentication
322 # if the issue actually exists but requires authentication
323 @issue = Issue.find(params[:id])
323 @issue = Issue.find(params[:id])
324 raise Unauthorized unless @issue.visible?
324 raise Unauthorized unless @issue.visible?
325 @project = @issue.project
325 @project = @issue.project
326 rescue ActiveRecord::RecordNotFound
326 rescue ActiveRecord::RecordNotFound
327 render_404
327 render_404
328 end
328 end
329
329
330 # Find issues with a single :id param or :ids array param
330 # Find issues with a single :id param or :ids array param
331 # Raises a Unauthorized exception if one of the issues is not visible
331 # Raises a Unauthorized exception if one of the issues is not visible
332 def find_issues
332 def find_issues
333 @issues = Issue.
333 @issues = Issue.
334 where(:id => (params[:id] || params[:ids])).
334 where(:id => (params[:id] || params[:ids])).
335 preload(:project, :status, :tracker, :priority, :author, :assigned_to, :relations_to, {:custom_values => :custom_field}).
335 preload(:project, :status, :tracker, :priority, :author, :assigned_to, :relations_to, {:custom_values => :custom_field}).
336 to_a
336 to_a
337 raise ActiveRecord::RecordNotFound if @issues.empty?
337 raise ActiveRecord::RecordNotFound if @issues.empty?
338 raise Unauthorized unless @issues.all?(&:visible?)
338 raise Unauthorized unless @issues.all?(&:visible?)
339 @projects = @issues.collect(&:project).compact.uniq
339 @projects = @issues.collect(&:project).compact.uniq
340 @project = @projects.first if @projects.size == 1
340 @project = @projects.first if @projects.size == 1
341 rescue ActiveRecord::RecordNotFound
341 rescue ActiveRecord::RecordNotFound
342 render_404
342 render_404
343 end
343 end
344
344
345 def find_attachments
345 def find_attachments
346 if (attachments = params[:attachments]).present?
346 if (attachments = params[:attachments]).present?
347 att = attachments.values.collect do |attachment|
347 att = attachments.values.collect do |attachment|
348 Attachment.find_by_token( attachment[:token] ) if attachment[:token].present?
348 Attachment.find_by_token( attachment[:token] ) if attachment[:token].present?
349 end
349 end
350 att.compact!
350 att.compact!
351 end
351 end
352 @attachments = att || []
352 @attachments = att || []
353 end
353 end
354
354
355 def parse_params_for_bulk_update(params)
356 attributes = (params || {}).reject {|k,v| v.blank?}
357 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
358 if custom = attributes[:custom_field_values]
359 custom.reject! {|k,v| v.blank?}
360 custom.keys.each do |k|
361 if custom[k].is_a?(Array)
362 custom[k] << '' if custom[k].delete('__none__')
363 else
364 custom[k] = '' if custom[k] == '__none__'
365 end
366 end
367 end
368 attributes
369 end
370
355 # make sure that the user is a member of the project (or admin) if project is private
371 # make sure that the user is a member of the project (or admin) if project is private
356 # used as a before_filter for actions that do not require any particular permission on the project
372 # used as a before_filter for actions that do not require any particular permission on the project
357 def check_project_privacy
373 def check_project_privacy
358 if @project && !@project.archived?
374 if @project && !@project.archived?
359 if @project.visible?
375 if @project.visible?
360 true
376 true
361 else
377 else
362 deny_access
378 deny_access
363 end
379 end
364 else
380 else
365 @project = nil
381 @project = nil
366 render_404
382 render_404
367 false
383 false
368 end
384 end
369 end
385 end
370
386
371 def back_url
387 def back_url
372 url = params[:back_url]
388 url = params[:back_url]
373 if url.nil? && referer = request.env['HTTP_REFERER']
389 if url.nil? && referer = request.env['HTTP_REFERER']
374 url = CGI.unescape(referer.to_s)
390 url = CGI.unescape(referer.to_s)
375 end
391 end
376 url
392 url
377 end
393 end
378
394
379 def redirect_back_or_default(default, options={})
395 def redirect_back_or_default(default, options={})
380 back_url = params[:back_url].to_s
396 back_url = params[:back_url].to_s
381 if back_url.present? && valid_url = validate_back_url(back_url)
397 if back_url.present? && valid_url = validate_back_url(back_url)
382 redirect_to(valid_url)
398 redirect_to(valid_url)
383 return
399 return
384 elsif options[:referer]
400 elsif options[:referer]
385 redirect_to_referer_or default
401 redirect_to_referer_or default
386 return
402 return
387 end
403 end
388 redirect_to default
404 redirect_to default
389 false
405 false
390 end
406 end
391
407
392 # Returns a validated URL string if back_url is a valid url for redirection,
408 # Returns a validated URL string if back_url is a valid url for redirection,
393 # otherwise false
409 # otherwise false
394 def validate_back_url(back_url)
410 def validate_back_url(back_url)
395 if CGI.unescape(back_url).include?('..')
411 if CGI.unescape(back_url).include?('..')
396 return false
412 return false
397 end
413 end
398
414
399 begin
415 begin
400 uri = URI.parse(back_url)
416 uri = URI.parse(back_url)
401 rescue URI::InvalidURIError
417 rescue URI::InvalidURIError
402 return false
418 return false
403 end
419 end
404
420
405 [:scheme, :host, :port].each do |component|
421 [:scheme, :host, :port].each do |component|
406 if uri.send(component).present? && uri.send(component) != request.send(component)
422 if uri.send(component).present? && uri.send(component) != request.send(component)
407 return false
423 return false
408 end
424 end
409 uri.send(:"#{component}=", nil)
425 uri.send(:"#{component}=", nil)
410 end
426 end
411 # Always ignore basic user:password in the URL
427 # Always ignore basic user:password in the URL
412 uri.userinfo = nil
428 uri.userinfo = nil
413
429
414 path = uri.to_s
430 path = uri.to_s
415 # Ensure that the remaining URL starts with a slash, followed by a
431 # Ensure that the remaining URL starts with a slash, followed by a
416 # non-slash character or the end
432 # non-slash character or the end
417 if path !~ %r{\A/([^/]|\z)}
433 if path !~ %r{\A/([^/]|\z)}
418 return false
434 return false
419 end
435 end
420
436
421 if path.match(%r{/(login|account/register)})
437 if path.match(%r{/(login|account/register)})
422 return false
438 return false
423 end
439 end
424
440
425 if relative_url_root.present? && !path.starts_with?(relative_url_root)
441 if relative_url_root.present? && !path.starts_with?(relative_url_root)
426 return false
442 return false
427 end
443 end
428
444
429 return path
445 return path
430 end
446 end
431 private :validate_back_url
447 private :validate_back_url
432
448
433 def valid_back_url?(back_url)
449 def valid_back_url?(back_url)
434 !!validate_back_url(back_url)
450 !!validate_back_url(back_url)
435 end
451 end
436 private :valid_back_url?
452 private :valid_back_url?
437
453
438 # Redirects to the request referer if present, redirects to args or call block otherwise.
454 # Redirects to the request referer if present, redirects to args or call block otherwise.
439 def redirect_to_referer_or(*args, &block)
455 def redirect_to_referer_or(*args, &block)
440 redirect_to :back
456 redirect_to :back
441 rescue ::ActionController::RedirectBackError
457 rescue ::ActionController::RedirectBackError
442 if args.any?
458 if args.any?
443 redirect_to *args
459 redirect_to *args
444 elsif block_given?
460 elsif block_given?
445 block.call
461 block.call
446 else
462 else
447 raise "#redirect_to_referer_or takes arguments or a block"
463 raise "#redirect_to_referer_or takes arguments or a block"
448 end
464 end
449 end
465 end
450
466
451 def render_403(options={})
467 def render_403(options={})
452 @project = nil
468 @project = nil
453 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
469 render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
454 return false
470 return false
455 end
471 end
456
472
457 def render_404(options={})
473 def render_404(options={})
458 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
474 render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
459 return false
475 return false
460 end
476 end
461
477
462 # Renders an error response
478 # Renders an error response
463 def render_error(arg)
479 def render_error(arg)
464 arg = {:message => arg} unless arg.is_a?(Hash)
480 arg = {:message => arg} unless arg.is_a?(Hash)
465
481
466 @message = arg[:message]
482 @message = arg[:message]
467 @message = l(@message) if @message.is_a?(Symbol)
483 @message = l(@message) if @message.is_a?(Symbol)
468 @status = arg[:status] || 500
484 @status = arg[:status] || 500
469
485
470 respond_to do |format|
486 respond_to do |format|
471 format.html {
487 format.html {
472 render :template => 'common/error', :layout => use_layout, :status => @status
488 render :template => 'common/error', :layout => use_layout, :status => @status
473 }
489 }
474 format.any { head @status }
490 format.any { head @status }
475 end
491 end
476 end
492 end
477
493
478 # Handler for ActionView::MissingTemplate exception
494 # Handler for ActionView::MissingTemplate exception
479 def missing_template
495 def missing_template
480 logger.warn "Missing template, responding with 404"
496 logger.warn "Missing template, responding with 404"
481 @project = nil
497 @project = nil
482 render_404
498 render_404
483 end
499 end
484
500
485 # Filter for actions that provide an API response
501 # Filter for actions that provide an API response
486 # but have no HTML representation for non admin users
502 # but have no HTML representation for non admin users
487 def require_admin_or_api_request
503 def require_admin_or_api_request
488 return true if api_request?
504 return true if api_request?
489 if User.current.admin?
505 if User.current.admin?
490 true
506 true
491 elsif User.current.logged?
507 elsif User.current.logged?
492 render_error(:status => 406)
508 render_error(:status => 406)
493 else
509 else
494 deny_access
510 deny_access
495 end
511 end
496 end
512 end
497
513
498 # Picks which layout to use based on the request
514 # Picks which layout to use based on the request
499 #
515 #
500 # @return [boolean, string] name of the layout to use or false for no layout
516 # @return [boolean, string] name of the layout to use or false for no layout
501 def use_layout
517 def use_layout
502 request.xhr? ? false : 'base'
518 request.xhr? ? false : 'base'
503 end
519 end
504
520
505 def render_feed(items, options={})
521 def render_feed(items, options={})
506 @items = (items || []).to_a
522 @items = (items || []).to_a
507 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
523 @items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
508 @items = @items.slice(0, Setting.feeds_limit.to_i)
524 @items = @items.slice(0, Setting.feeds_limit.to_i)
509 @title = options[:title] || Setting.app_title
525 @title = options[:title] || Setting.app_title
510 render :template => "common/feed", :formats => [:atom], :layout => false,
526 render :template => "common/feed", :formats => [:atom], :layout => false,
511 :content_type => 'application/atom+xml'
527 :content_type => 'application/atom+xml'
512 end
528 end
513
529
514 def self.accept_rss_auth(*actions)
530 def self.accept_rss_auth(*actions)
515 if actions.any?
531 if actions.any?
516 self.accept_rss_auth_actions = actions
532 self.accept_rss_auth_actions = actions
517 else
533 else
518 self.accept_rss_auth_actions || []
534 self.accept_rss_auth_actions || []
519 end
535 end
520 end
536 end
521
537
522 def accept_rss_auth?(action=action_name)
538 def accept_rss_auth?(action=action_name)
523 self.class.accept_rss_auth.include?(action.to_sym)
539 self.class.accept_rss_auth.include?(action.to_sym)
524 end
540 end
525
541
526 def self.accept_api_auth(*actions)
542 def self.accept_api_auth(*actions)
527 if actions.any?
543 if actions.any?
528 self.accept_api_auth_actions = actions
544 self.accept_api_auth_actions = actions
529 else
545 else
530 self.accept_api_auth_actions || []
546 self.accept_api_auth_actions || []
531 end
547 end
532 end
548 end
533
549
534 def accept_api_auth?(action=action_name)
550 def accept_api_auth?(action=action_name)
535 self.class.accept_api_auth.include?(action.to_sym)
551 self.class.accept_api_auth.include?(action.to_sym)
536 end
552 end
537
553
538 # Returns the number of objects that should be displayed
554 # Returns the number of objects that should be displayed
539 # on the paginated list
555 # on the paginated list
540 def per_page_option
556 def per_page_option
541 per_page = nil
557 per_page = nil
542 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
558 if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
543 per_page = params[:per_page].to_s.to_i
559 per_page = params[:per_page].to_s.to_i
544 session[:per_page] = per_page
560 session[:per_page] = per_page
545 elsif session[:per_page]
561 elsif session[:per_page]
546 per_page = session[:per_page]
562 per_page = session[:per_page]
547 else
563 else
548 per_page = Setting.per_page_options_array.first || 25
564 per_page = Setting.per_page_options_array.first || 25
549 end
565 end
550 per_page
566 per_page
551 end
567 end
552
568
553 # Returns offset and limit used to retrieve objects
569 # Returns offset and limit used to retrieve objects
554 # for an API response based on offset, limit and page parameters
570 # for an API response based on offset, limit and page parameters
555 def api_offset_and_limit(options=params)
571 def api_offset_and_limit(options=params)
556 if options[:offset].present?
572 if options[:offset].present?
557 offset = options[:offset].to_i
573 offset = options[:offset].to_i
558 if offset < 0
574 if offset < 0
559 offset = 0
575 offset = 0
560 end
576 end
561 end
577 end
562 limit = options[:limit].to_i
578 limit = options[:limit].to_i
563 if limit < 1
579 if limit < 1
564 limit = 25
580 limit = 25
565 elsif limit > 100
581 elsif limit > 100
566 limit = 100
582 limit = 100
567 end
583 end
568 if offset.nil? && options[:page].present?
584 if offset.nil? && options[:page].present?
569 offset = (options[:page].to_i - 1) * limit
585 offset = (options[:page].to_i - 1) * limit
570 offset = 0 if offset < 0
586 offset = 0 if offset < 0
571 end
587 end
572 offset ||= 0
588 offset ||= 0
573
589
574 [offset, limit]
590 [offset, limit]
575 end
591 end
576
592
577 # qvalues http header parser
593 # qvalues http header parser
578 # code taken from webrick
594 # code taken from webrick
579 def parse_qvalues(value)
595 def parse_qvalues(value)
580 tmp = []
596 tmp = []
581 if value
597 if value
582 parts = value.split(/,\s*/)
598 parts = value.split(/,\s*/)
583 parts.each {|part|
599 parts.each {|part|
584 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
600 if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
585 val = m[1]
601 val = m[1]
586 q = (m[2] or 1).to_f
602 q = (m[2] or 1).to_f
587 tmp.push([val, q])
603 tmp.push([val, q])
588 end
604 end
589 }
605 }
590 tmp = tmp.sort_by{|val, q| -q}
606 tmp = tmp.sort_by{|val, q| -q}
591 tmp.collect!{|val, q| val}
607 tmp.collect!{|val, q| val}
592 end
608 end
593 return tmp
609 return tmp
594 rescue
610 rescue
595 nil
611 nil
596 end
612 end
597
613
598 # Returns a string that can be used as filename value in Content-Disposition header
614 # Returns a string that can be used as filename value in Content-Disposition header
599 def filename_for_content_disposition(name)
615 def filename_for_content_disposition(name)
600 request.env['HTTP_USER_AGENT'] =~ %r{(MSIE|Trident|Edge)} ? ERB::Util.url_encode(name) : name
616 request.env['HTTP_USER_AGENT'] =~ %r{(MSIE|Trident|Edge)} ? ERB::Util.url_encode(name) : name
601 end
617 end
602
618
603 def api_request?
619 def api_request?
604 %w(xml json).include? params[:format]
620 %w(xml json).include? params[:format]
605 end
621 end
606
622
607 # Returns the API key present in the request
623 # Returns the API key present in the request
608 def api_key_from_request
624 def api_key_from_request
609 if params[:key].present?
625 if params[:key].present?
610 params[:key].to_s
626 params[:key].to_s
611 elsif request.headers["X-Redmine-API-Key"].present?
627 elsif request.headers["X-Redmine-API-Key"].present?
612 request.headers["X-Redmine-API-Key"].to_s
628 request.headers["X-Redmine-API-Key"].to_s
613 end
629 end
614 end
630 end
615
631
616 # Returns the API 'switch user' value if present
632 # Returns the API 'switch user' value if present
617 def api_switch_user_from_request
633 def api_switch_user_from_request
618 request.headers["X-Redmine-Switch-User"].to_s.presence
634 request.headers["X-Redmine-Switch-User"].to_s.presence
619 end
635 end
620
636
621 # Renders a warning flash if obj has unsaved attachments
637 # Renders a warning flash if obj has unsaved attachments
622 def render_attachment_warning_if_needed(obj)
638 def render_attachment_warning_if_needed(obj)
623 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
639 flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
624 end
640 end
625
641
626 # Rescues an invalid query statement. Just in case...
642 # Rescues an invalid query statement. Just in case...
627 def query_statement_invalid(exception)
643 def query_statement_invalid(exception)
628 logger.error "Query::StatementInvalid: #{exception.message}" if logger
644 logger.error "Query::StatementInvalid: #{exception.message}" if logger
629 session.delete(:query)
645 session.delete(:query)
630 sort_clear if respond_to?(:sort_clear)
646 sort_clear if respond_to?(:sort_clear)
631 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
647 render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
632 end
648 end
633
649
634 # Renders a 200 response for successfull updates or deletions via the API
650 # Renders a 200 response for successfull updates or deletions via the API
635 def render_api_ok
651 def render_api_ok
636 render_api_head :ok
652 render_api_head :ok
637 end
653 end
638
654
639 # Renders a head API response
655 # Renders a head API response
640 def render_api_head(status)
656 def render_api_head(status)
641 # #head would return a response body with one space
657 # #head would return a response body with one space
642 render :text => '', :status => status, :layout => nil
658 render :text => '', :status => status, :layout => nil
643 end
659 end
644
660
645 # Renders API response on validation failure
661 # Renders API response on validation failure
646 # for an object or an array of objects
662 # for an object or an array of objects
647 def render_validation_errors(objects)
663 def render_validation_errors(objects)
648 messages = Array.wrap(objects).map {|object| object.errors.full_messages}.flatten
664 messages = Array.wrap(objects).map {|object| object.errors.full_messages}.flatten
649 render_api_errors(messages)
665 render_api_errors(messages)
650 end
666 end
651
667
652 def render_api_errors(*messages)
668 def render_api_errors(*messages)
653 @error_messages = messages.flatten
669 @error_messages = messages.flatten
654 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
670 render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
655 end
671 end
656
672
657 # Overrides #_include_layout? so that #render with no arguments
673 # Overrides #_include_layout? so that #render with no arguments
658 # doesn't use the layout for api requests
674 # doesn't use the layout for api requests
659 def _include_layout?(*args)
675 def _include_layout?(*args)
660 api_request? ? false : super
676 api_request? ? false : super
661 end
677 end
662 end
678 end
@@ -1,563 +1,547
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class IssuesController < ApplicationController
18 class IssuesController < ApplicationController
19 default_search_scope :issues
19 default_search_scope :issues
20
20
21 before_filter :find_issue, :only => [:show, :edit, :update]
21 before_filter :find_issue, :only => [:show, :edit, :update]
22 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
22 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
23 before_filter :authorize, :except => [:index, :new, :create]
23 before_filter :authorize, :except => [:index, :new, :create]
24 before_filter :find_optional_project, :only => [:index, :new, :create]
24 before_filter :find_optional_project, :only => [:index, :new, :create]
25 before_filter :build_new_issue_from_params, :only => [:new, :create]
25 before_filter :build_new_issue_from_params, :only => [:new, :create]
26 accept_rss_auth :index, :show
26 accept_rss_auth :index, :show
27 accept_api_auth :index, :show, :create, :update, :destroy
27 accept_api_auth :index, :show, :create, :update, :destroy
28
28
29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
29 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
30
30
31 helper :journals
31 helper :journals
32 helper :projects
32 helper :projects
33 helper :custom_fields
33 helper :custom_fields
34 helper :issue_relations
34 helper :issue_relations
35 helper :watchers
35 helper :watchers
36 helper :attachments
36 helper :attachments
37 helper :queries
37 helper :queries
38 include QueriesHelper
38 include QueriesHelper
39 helper :repositories
39 helper :repositories
40 helper :sort
40 helper :sort
41 include SortHelper
41 include SortHelper
42 helper :timelog
42 helper :timelog
43
43
44 def index
44 def index
45 retrieve_query
45 retrieve_query
46 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
46 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
47 sort_update(@query.sortable_columns)
47 sort_update(@query.sortable_columns)
48 @query.sort_criteria = sort_criteria.to_a
48 @query.sort_criteria = sort_criteria.to_a
49
49
50 if @query.valid?
50 if @query.valid?
51 case params[:format]
51 case params[:format]
52 when 'csv', 'pdf'
52 when 'csv', 'pdf'
53 @limit = Setting.issues_export_limit.to_i
53 @limit = Setting.issues_export_limit.to_i
54 if params[:columns] == 'all'
54 if params[:columns] == 'all'
55 @query.column_names = @query.available_inline_columns.map(&:name)
55 @query.column_names = @query.available_inline_columns.map(&:name)
56 end
56 end
57 when 'atom'
57 when 'atom'
58 @limit = Setting.feeds_limit.to_i
58 @limit = Setting.feeds_limit.to_i
59 when 'xml', 'json'
59 when 'xml', 'json'
60 @offset, @limit = api_offset_and_limit
60 @offset, @limit = api_offset_and_limit
61 @query.column_names = %w(author)
61 @query.column_names = %w(author)
62 else
62 else
63 @limit = per_page_option
63 @limit = per_page_option
64 end
64 end
65
65
66 @issue_count = @query.issue_count
66 @issue_count = @query.issue_count
67 @issue_pages = Paginator.new @issue_count, @limit, params['page']
67 @issue_pages = Paginator.new @issue_count, @limit, params['page']
68 @offset ||= @issue_pages.offset
68 @offset ||= @issue_pages.offset
69 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
69 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
70 :order => sort_clause,
70 :order => sort_clause,
71 :offset => @offset,
71 :offset => @offset,
72 :limit => @limit)
72 :limit => @limit)
73 @issue_count_by_group = @query.issue_count_by_group
73 @issue_count_by_group = @query.issue_count_by_group
74
74
75 respond_to do |format|
75 respond_to do |format|
76 format.html { render :template => 'issues/index', :layout => !request.xhr? }
76 format.html { render :template => 'issues/index', :layout => !request.xhr? }
77 format.api {
77 format.api {
78 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
78 Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
79 }
79 }
80 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
80 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
81 format.csv { send_data(query_to_csv(@issues, @query, params[:csv]), :type => 'text/csv; header=present', :filename => 'issues.csv') }
81 format.csv { send_data(query_to_csv(@issues, @query, params[:csv]), :type => 'text/csv; header=present', :filename => 'issues.csv') }
82 format.pdf { send_file_headers! :type => 'application/pdf', :filename => 'issues.pdf' }
82 format.pdf { send_file_headers! :type => 'application/pdf', :filename => 'issues.pdf' }
83 end
83 end
84 else
84 else
85 respond_to do |format|
85 respond_to do |format|
86 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
86 format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
87 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
87 format.any(:atom, :csv, :pdf) { render(:nothing => true) }
88 format.api { render_validation_errors(@query) }
88 format.api { render_validation_errors(@query) }
89 end
89 end
90 end
90 end
91 rescue ActiveRecord::RecordNotFound
91 rescue ActiveRecord::RecordNotFound
92 render_404
92 render_404
93 end
93 end
94
94
95 def show
95 def show
96 @journals = @issue.journals.includes(:user, :details).
96 @journals = @issue.journals.includes(:user, :details).
97 references(:user, :details).
97 references(:user, :details).
98 reorder(:created_on, :id).to_a
98 reorder(:created_on, :id).to_a
99 @journals.each_with_index {|j,i| j.indice = i+1}
99 @journals.each_with_index {|j,i| j.indice = i+1}
100 @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
100 @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
101 Journal.preload_journals_details_custom_fields(@journals)
101 Journal.preload_journals_details_custom_fields(@journals)
102 @journals.select! {|journal| journal.notes? || journal.visible_details.any?}
102 @journals.select! {|journal| journal.notes? || journal.visible_details.any?}
103 @journals.reverse! if User.current.wants_comments_in_reverse_order?
103 @journals.reverse! if User.current.wants_comments_in_reverse_order?
104
104
105 @changesets = @issue.changesets.visible.preload(:repository, :user).to_a
105 @changesets = @issue.changesets.visible.preload(:repository, :user).to_a
106 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
106 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
107
107
108 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
108 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
109 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
109 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
110 @priorities = IssuePriority.active
110 @priorities = IssuePriority.active
111 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
111 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
112 @relation = IssueRelation.new
112 @relation = IssueRelation.new
113
113
114 respond_to do |format|
114 respond_to do |format|
115 format.html {
115 format.html {
116 retrieve_previous_and_next_issue_ids
116 retrieve_previous_and_next_issue_ids
117 render :template => 'issues/show'
117 render :template => 'issues/show'
118 }
118 }
119 format.api
119 format.api
120 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
120 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
121 format.pdf {
121 format.pdf {
122 send_file_headers! :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf"
122 send_file_headers! :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf"
123 }
123 }
124 end
124 end
125 end
125 end
126
126
127 def new
127 def new
128 respond_to do |format|
128 respond_to do |format|
129 format.html { render :action => 'new', :layout => !request.xhr? }
129 format.html { render :action => 'new', :layout => !request.xhr? }
130 format.js
130 format.js
131 end
131 end
132 end
132 end
133
133
134 def create
134 def create
135 unless User.current.allowed_to?(:add_issues, @issue.project, :global => true)
135 unless User.current.allowed_to?(:add_issues, @issue.project, :global => true)
136 raise ::Unauthorized
136 raise ::Unauthorized
137 end
137 end
138 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
138 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
139 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
139 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
140 if @issue.save
140 if @issue.save
141 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
141 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
142 respond_to do |format|
142 respond_to do |format|
143 format.html {
143 format.html {
144 render_attachment_warning_if_needed(@issue)
144 render_attachment_warning_if_needed(@issue)
145 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
145 flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
146 redirect_after_create
146 redirect_after_create
147 }
147 }
148 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
148 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
149 end
149 end
150 return
150 return
151 else
151 else
152 respond_to do |format|
152 respond_to do |format|
153 format.html {
153 format.html {
154 if @issue.project.nil?
154 if @issue.project.nil?
155 render_error :status => 422
155 render_error :status => 422
156 else
156 else
157 render :action => 'new'
157 render :action => 'new'
158 end
158 end
159 }
159 }
160 format.api { render_validation_errors(@issue) }
160 format.api { render_validation_errors(@issue) }
161 end
161 end
162 end
162 end
163 end
163 end
164
164
165 def edit
165 def edit
166 return unless update_issue_from_params
166 return unless update_issue_from_params
167
167
168 respond_to do |format|
168 respond_to do |format|
169 format.html { }
169 format.html { }
170 format.js
170 format.js
171 end
171 end
172 end
172 end
173
173
174 def update
174 def update
175 return unless update_issue_from_params
175 return unless update_issue_from_params
176 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
176 @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
177 saved = false
177 saved = false
178 begin
178 begin
179 saved = save_issue_with_child_records
179 saved = save_issue_with_child_records
180 rescue ActiveRecord::StaleObjectError
180 rescue ActiveRecord::StaleObjectError
181 @conflict = true
181 @conflict = true
182 if params[:last_journal_id]
182 if params[:last_journal_id]
183 @conflict_journals = @issue.journals_after(params[:last_journal_id]).to_a
183 @conflict_journals = @issue.journals_after(params[:last_journal_id]).to_a
184 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
184 @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
185 end
185 end
186 end
186 end
187
187
188 if saved
188 if saved
189 render_attachment_warning_if_needed(@issue)
189 render_attachment_warning_if_needed(@issue)
190 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
190 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
191
191
192 respond_to do |format|
192 respond_to do |format|
193 format.html { redirect_back_or_default issue_path(@issue, previous_and_next_issue_ids_params) }
193 format.html { redirect_back_or_default issue_path(@issue, previous_and_next_issue_ids_params) }
194 format.api { render_api_ok }
194 format.api { render_api_ok }
195 end
195 end
196 else
196 else
197 respond_to do |format|
197 respond_to do |format|
198 format.html { render :action => 'edit' }
198 format.html { render :action => 'edit' }
199 format.api { render_validation_errors(@issue) }
199 format.api { render_validation_errors(@issue) }
200 end
200 end
201 end
201 end
202 end
202 end
203
203
204 # Bulk edit/copy a set of issues
204 # Bulk edit/copy a set of issues
205 def bulk_edit
205 def bulk_edit
206 @issues.sort!
206 @issues.sort!
207 @copy = params[:copy].present?
207 @copy = params[:copy].present?
208 @notes = params[:notes]
208 @notes = params[:notes]
209
209
210 if @copy
210 if @copy
211 unless User.current.allowed_to?(:copy_issues, @projects)
211 unless User.current.allowed_to?(:copy_issues, @projects)
212 raise ::Unauthorized
212 raise ::Unauthorized
213 end
213 end
214 else
214 else
215 unless @issues.all?(&:attributes_editable?)
215 unless @issues.all?(&:attributes_editable?)
216 raise ::Unauthorized
216 raise ::Unauthorized
217 end
217 end
218 end
218 end
219
219
220 @allowed_projects = Issue.allowed_target_projects
220 @allowed_projects = Issue.allowed_target_projects
221 if params[:issue]
221 if params[:issue]
222 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
222 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
223 if @target_project
223 if @target_project
224 target_projects = [@target_project]
224 target_projects = [@target_project]
225 end
225 end
226 end
226 end
227 target_projects ||= @projects
227 target_projects ||= @projects
228
228
229 if @copy
229 if @copy
230 # Copied issues will get their default statuses
230 # Copied issues will get their default statuses
231 @available_statuses = []
231 @available_statuses = []
232 else
232 else
233 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
233 @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
234 end
234 end
235 @custom_fields = @issues.map{|i|i.editable_custom_fields}.reduce(:&)
235 @custom_fields = @issues.map{|i|i.editable_custom_fields}.reduce(:&)
236 @assignables = target_projects.map(&:assignable_users).reduce(:&)
236 @assignables = target_projects.map(&:assignable_users).reduce(:&)
237 @trackers = target_projects.map {|p| Issue.allowed_target_trackers(p) }.reduce(:&)
237 @trackers = target_projects.map {|p| Issue.allowed_target_trackers(p) }.reduce(:&)
238 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
238 @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
239 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
239 @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
240 if @copy
240 if @copy
241 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
241 @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
242 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
242 @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
243 end
243 end
244
244
245 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
245 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
246
246
247 @issue_params = params[:issue] || {}
247 @issue_params = params[:issue] || {}
248 @issue_params[:custom_field_values] ||= {}
248 @issue_params[:custom_field_values] ||= {}
249 end
249 end
250
250
251 def bulk_update
251 def bulk_update
252 @issues.sort!
252 @issues.sort!
253 @copy = params[:copy].present?
253 @copy = params[:copy].present?
254
254
255 attributes = parse_params_for_bulk_issue_attributes(params)
255 attributes = parse_params_for_bulk_update(params[:issue])
256 copy_subtasks = (params[:copy_subtasks] == '1')
256 copy_subtasks = (params[:copy_subtasks] == '1')
257 copy_attachments = (params[:copy_attachments] == '1')
257 copy_attachments = (params[:copy_attachments] == '1')
258
258
259 if @copy
259 if @copy
260 unless User.current.allowed_to?(:copy_issues, @projects)
260 unless User.current.allowed_to?(:copy_issues, @projects)
261 raise ::Unauthorized
261 raise ::Unauthorized
262 end
262 end
263 target_projects = @projects
263 target_projects = @projects
264 if attributes['project_id'].present?
264 if attributes['project_id'].present?
265 target_projects = Project.where(:id => attributes['project_id']).to_a
265 target_projects = Project.where(:id => attributes['project_id']).to_a
266 end
266 end
267 unless User.current.allowed_to?(:add_issues, target_projects)
267 unless User.current.allowed_to?(:add_issues, target_projects)
268 raise ::Unauthorized
268 raise ::Unauthorized
269 end
269 end
270 else
270 else
271 unless @issues.all?(&:attributes_editable?)
271 unless @issues.all?(&:attributes_editable?)
272 raise ::Unauthorized
272 raise ::Unauthorized
273 end
273 end
274 end
274 end
275
275
276 unsaved_issues = []
276 unsaved_issues = []
277 saved_issues = []
277 saved_issues = []
278
278
279 if @copy && copy_subtasks
279 if @copy && copy_subtasks
280 # Descendant issues will be copied with the parent task
280 # Descendant issues will be copied with the parent task
281 # Don't copy them twice
281 # Don't copy them twice
282 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
282 @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
283 end
283 end
284
284
285 @issues.each do |orig_issue|
285 @issues.each do |orig_issue|
286 orig_issue.reload
286 orig_issue.reload
287 if @copy
287 if @copy
288 issue = orig_issue.copy({},
288 issue = orig_issue.copy({},
289 :attachments => copy_attachments,
289 :attachments => copy_attachments,
290 :subtasks => copy_subtasks,
290 :subtasks => copy_subtasks,
291 :link => link_copy?(params[:link_copy])
291 :link => link_copy?(params[:link_copy])
292 )
292 )
293 else
293 else
294 issue = orig_issue
294 issue = orig_issue
295 end
295 end
296 journal = issue.init_journal(User.current, params[:notes])
296 journal = issue.init_journal(User.current, params[:notes])
297 issue.safe_attributes = attributes
297 issue.safe_attributes = attributes
298 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
298 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
299 if issue.save
299 if issue.save
300 saved_issues << issue
300 saved_issues << issue
301 else
301 else
302 unsaved_issues << orig_issue
302 unsaved_issues << orig_issue
303 end
303 end
304 end
304 end
305
305
306 if unsaved_issues.empty?
306 if unsaved_issues.empty?
307 flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
307 flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
308 if params[:follow]
308 if params[:follow]
309 if @issues.size == 1 && saved_issues.size == 1
309 if @issues.size == 1 && saved_issues.size == 1
310 redirect_to issue_path(saved_issues.first)
310 redirect_to issue_path(saved_issues.first)
311 elsif saved_issues.map(&:project).uniq.size == 1
311 elsif saved_issues.map(&:project).uniq.size == 1
312 redirect_to project_issues_path(saved_issues.map(&:project).first)
312 redirect_to project_issues_path(saved_issues.map(&:project).first)
313 end
313 end
314 else
314 else
315 redirect_back_or_default _project_issues_path(@project)
315 redirect_back_or_default _project_issues_path(@project)
316 end
316 end
317 else
317 else
318 @saved_issues = @issues
318 @saved_issues = @issues
319 @unsaved_issues = unsaved_issues
319 @unsaved_issues = unsaved_issues
320 @issues = Issue.visible.where(:id => @unsaved_issues.map(&:id)).to_a
320 @issues = Issue.visible.where(:id => @unsaved_issues.map(&:id)).to_a
321 bulk_edit
321 bulk_edit
322 render :action => 'bulk_edit'
322 render :action => 'bulk_edit'
323 end
323 end
324 end
324 end
325
325
326 def destroy
326 def destroy
327 raise Unauthorized unless @issues.all?(&:deletable?)
327 raise Unauthorized unless @issues.all?(&:deletable?)
328 @hours = TimeEntry.where(:issue_id => @issues.map(&:id)).sum(:hours).to_f
328 @hours = TimeEntry.where(:issue_id => @issues.map(&:id)).sum(:hours).to_f
329 if @hours > 0
329 if @hours > 0
330 case params[:todo]
330 case params[:todo]
331 when 'destroy'
331 when 'destroy'
332 # nothing to do
332 # nothing to do
333 when 'nullify'
333 when 'nullify'
334 TimeEntry.where(['issue_id IN (?)', @issues]).update_all('issue_id = NULL')
334 TimeEntry.where(['issue_id IN (?)', @issues]).update_all('issue_id = NULL')
335 when 'reassign'
335 when 'reassign'
336 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
336 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
337 if reassign_to.nil?
337 if reassign_to.nil?
338 flash.now[:error] = l(:error_issue_not_found_in_project)
338 flash.now[:error] = l(:error_issue_not_found_in_project)
339 return
339 return
340 else
340 else
341 TimeEntry.where(['issue_id IN (?)', @issues]).
341 TimeEntry.where(['issue_id IN (?)', @issues]).
342 update_all("issue_id = #{reassign_to.id}")
342 update_all("issue_id = #{reassign_to.id}")
343 end
343 end
344 else
344 else
345 # display the destroy form if it's a user request
345 # display the destroy form if it's a user request
346 return unless api_request?
346 return unless api_request?
347 end
347 end
348 end
348 end
349 @issues.each do |issue|
349 @issues.each do |issue|
350 begin
350 begin
351 issue.reload.destroy
351 issue.reload.destroy
352 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
352 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
353 # nothing to do, issue was already deleted (eg. by a parent)
353 # nothing to do, issue was already deleted (eg. by a parent)
354 end
354 end
355 end
355 end
356 respond_to do |format|
356 respond_to do |format|
357 format.html { redirect_back_or_default _project_issues_path(@project) }
357 format.html { redirect_back_or_default _project_issues_path(@project) }
358 format.api { render_api_ok }
358 format.api { render_api_ok }
359 end
359 end
360 end
360 end
361
361
362 # Overrides Redmine::MenuManager::MenuController::ClassMethods for
362 # Overrides Redmine::MenuManager::MenuController::ClassMethods for
363 # when the "New issue" tab is enabled
363 # when the "New issue" tab is enabled
364 def current_menu_item
364 def current_menu_item
365 if Setting.new_item_menu_tab == '1' && [:new, :create].include?(action_name.to_sym)
365 if Setting.new_item_menu_tab == '1' && [:new, :create].include?(action_name.to_sym)
366 :new_issue
366 :new_issue
367 else
367 else
368 super
368 super
369 end
369 end
370 end
370 end
371
371
372 private
372 private
373
373
374 def retrieve_previous_and_next_issue_ids
374 def retrieve_previous_and_next_issue_ids
375 if params[:prev_issue_id].present? || params[:next_issue_id].present?
375 if params[:prev_issue_id].present? || params[:next_issue_id].present?
376 @prev_issue_id = params[:prev_issue_id].presence.try(:to_i)
376 @prev_issue_id = params[:prev_issue_id].presence.try(:to_i)
377 @next_issue_id = params[:next_issue_id].presence.try(:to_i)
377 @next_issue_id = params[:next_issue_id].presence.try(:to_i)
378 @issue_position = params[:issue_position].presence.try(:to_i)
378 @issue_position = params[:issue_position].presence.try(:to_i)
379 @issue_count = params[:issue_count].presence.try(:to_i)
379 @issue_count = params[:issue_count].presence.try(:to_i)
380 else
380 else
381 retrieve_query_from_session
381 retrieve_query_from_session
382 if @query
382 if @query
383 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
383 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
384 sort_update(@query.sortable_columns, 'issues_index_sort')
384 sort_update(@query.sortable_columns, 'issues_index_sort')
385 limit = 500
385 limit = 500
386 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
386 issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
387 if (idx = issue_ids.index(@issue.id)) && idx < limit
387 if (idx = issue_ids.index(@issue.id)) && idx < limit
388 if issue_ids.size < 500
388 if issue_ids.size < 500
389 @issue_position = idx + 1
389 @issue_position = idx + 1
390 @issue_count = issue_ids.size
390 @issue_count = issue_ids.size
391 end
391 end
392 @prev_issue_id = issue_ids[idx - 1] if idx > 0
392 @prev_issue_id = issue_ids[idx - 1] if idx > 0
393 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
393 @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
394 end
394 end
395 end
395 end
396 end
396 end
397 end
397 end
398
398
399 def previous_and_next_issue_ids_params
399 def previous_and_next_issue_ids_params
400 {
400 {
401 :prev_issue_id => params[:prev_issue_id],
401 :prev_issue_id => params[:prev_issue_id],
402 :next_issue_id => params[:next_issue_id],
402 :next_issue_id => params[:next_issue_id],
403 :issue_position => params[:issue_position],
403 :issue_position => params[:issue_position],
404 :issue_count => params[:issue_count]
404 :issue_count => params[:issue_count]
405 }.reject {|k,v| k.blank?}
405 }.reject {|k,v| k.blank?}
406 end
406 end
407
407
408 # Used by #edit and #update to set some common instance variables
408 # Used by #edit and #update to set some common instance variables
409 # from the params
409 # from the params
410 def update_issue_from_params
410 def update_issue_from_params
411 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
411 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
412 if params[:time_entry]
412 if params[:time_entry]
413 @time_entry.safe_attributes = params[:time_entry]
413 @time_entry.safe_attributes = params[:time_entry]
414 end
414 end
415
415
416 @issue.init_journal(User.current)
416 @issue.init_journal(User.current)
417
417
418 issue_attributes = params[:issue]
418 issue_attributes = params[:issue]
419 if issue_attributes && params[:conflict_resolution]
419 if issue_attributes && params[:conflict_resolution]
420 case params[:conflict_resolution]
420 case params[:conflict_resolution]
421 when 'overwrite'
421 when 'overwrite'
422 issue_attributes = issue_attributes.dup
422 issue_attributes = issue_attributes.dup
423 issue_attributes.delete(:lock_version)
423 issue_attributes.delete(:lock_version)
424 when 'add_notes'
424 when 'add_notes'
425 issue_attributes = issue_attributes.slice(:notes, :private_notes)
425 issue_attributes = issue_attributes.slice(:notes, :private_notes)
426 when 'cancel'
426 when 'cancel'
427 redirect_to issue_path(@issue)
427 redirect_to issue_path(@issue)
428 return false
428 return false
429 end
429 end
430 end
430 end
431 @issue.safe_attributes = issue_attributes
431 @issue.safe_attributes = issue_attributes
432 @priorities = IssuePriority.active
432 @priorities = IssuePriority.active
433 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
433 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
434 true
434 true
435 end
435 end
436
436
437 # Used by #new and #create to build a new issue from the params
437 # Used by #new and #create to build a new issue from the params
438 # The new issue will be copied from an existing one if copy_from parameter is given
438 # The new issue will be copied from an existing one if copy_from parameter is given
439 def build_new_issue_from_params
439 def build_new_issue_from_params
440 @issue = Issue.new
440 @issue = Issue.new
441 if params[:copy_from]
441 if params[:copy_from]
442 begin
442 begin
443 @issue.init_journal(User.current)
443 @issue.init_journal(User.current)
444 @copy_from = Issue.visible.find(params[:copy_from])
444 @copy_from = Issue.visible.find(params[:copy_from])
445 unless User.current.allowed_to?(:copy_issues, @copy_from.project)
445 unless User.current.allowed_to?(:copy_issues, @copy_from.project)
446 raise ::Unauthorized
446 raise ::Unauthorized
447 end
447 end
448 @link_copy = link_copy?(params[:link_copy]) || request.get?
448 @link_copy = link_copy?(params[:link_copy]) || request.get?
449 @copy_attachments = params[:copy_attachments].present? || request.get?
449 @copy_attachments = params[:copy_attachments].present? || request.get?
450 @copy_subtasks = params[:copy_subtasks].present? || request.get?
450 @copy_subtasks = params[:copy_subtasks].present? || request.get?
451 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks, :link => @link_copy)
451 @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks, :link => @link_copy)
452 @issue.parent_issue_id = @copy_from.parent_id
452 @issue.parent_issue_id = @copy_from.parent_id
453 rescue ActiveRecord::RecordNotFound
453 rescue ActiveRecord::RecordNotFound
454 render_404
454 render_404
455 return
455 return
456 end
456 end
457 end
457 end
458 @issue.project = @project
458 @issue.project = @project
459 if request.get?
459 if request.get?
460 @issue.project ||= @issue.allowed_target_projects.first
460 @issue.project ||= @issue.allowed_target_projects.first
461 end
461 end
462 @issue.author ||= User.current
462 @issue.author ||= User.current
463 @issue.start_date ||= User.current.today if Setting.default_issue_start_date_to_creation_date?
463 @issue.start_date ||= User.current.today if Setting.default_issue_start_date_to_creation_date?
464
464
465 attrs = (params[:issue] || {}).deep_dup
465 attrs = (params[:issue] || {}).deep_dup
466 if action_name == 'new' && params[:was_default_status] == attrs[:status_id]
466 if action_name == 'new' && params[:was_default_status] == attrs[:status_id]
467 attrs.delete(:status_id)
467 attrs.delete(:status_id)
468 end
468 end
469 if action_name == 'new' && params[:form_update_triggered_by] == 'issue_project_id'
469 if action_name == 'new' && params[:form_update_triggered_by] == 'issue_project_id'
470 # Discard submitted version when changing the project on the issue form
470 # Discard submitted version when changing the project on the issue form
471 # so we can use the default version for the new project
471 # so we can use the default version for the new project
472 attrs.delete(:fixed_version_id)
472 attrs.delete(:fixed_version_id)
473 end
473 end
474 @issue.safe_attributes = attrs
474 @issue.safe_attributes = attrs
475
475
476 if @issue.project
476 if @issue.project
477 @issue.tracker ||= @issue.allowed_target_trackers.first
477 @issue.tracker ||= @issue.allowed_target_trackers.first
478 if @issue.tracker.nil?
478 if @issue.tracker.nil?
479 if @issue.project.trackers.any?
479 if @issue.project.trackers.any?
480 # None of the project trackers is allowed to the user
480 # None of the project trackers is allowed to the user
481 render_error :message => l(:error_no_tracker_allowed_for_new_issue_in_project), :status => 403
481 render_error :message => l(:error_no_tracker_allowed_for_new_issue_in_project), :status => 403
482 else
482 else
483 # Project has no trackers
483 # Project has no trackers
484 render_error l(:error_no_tracker_in_project)
484 render_error l(:error_no_tracker_in_project)
485 end
485 end
486 return false
486 return false
487 end
487 end
488 if @issue.status.nil?
488 if @issue.status.nil?
489 render_error l(:error_no_default_issue_status)
489 render_error l(:error_no_default_issue_status)
490 return false
490 return false
491 end
491 end
492 end
492 end
493
493
494 @priorities = IssuePriority.active
494 @priorities = IssuePriority.active
495 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
495 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
496 end
496 end
497
497
498 def parse_params_for_bulk_issue_attributes(params)
499 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
500 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
501 if custom = attributes[:custom_field_values]
502 custom.reject! {|k,v| v.blank?}
503 custom.keys.each do |k|
504 if custom[k].is_a?(Array)
505 custom[k] << '' if custom[k].delete('__none__')
506 else
507 custom[k] = '' if custom[k] == '__none__'
508 end
509 end
510 end
511 attributes
512 end
513
514 # Saves @issue and a time_entry from the parameters
498 # Saves @issue and a time_entry from the parameters
515 def save_issue_with_child_records
499 def save_issue_with_child_records
516 Issue.transaction do
500 Issue.transaction do
517 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, @issue.project)
501 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, @issue.project)
518 time_entry = @time_entry || TimeEntry.new
502 time_entry = @time_entry || TimeEntry.new
519 time_entry.project = @issue.project
503 time_entry.project = @issue.project
520 time_entry.issue = @issue
504 time_entry.issue = @issue
521 time_entry.user = User.current
505 time_entry.user = User.current
522 time_entry.spent_on = User.current.today
506 time_entry.spent_on = User.current.today
523 time_entry.attributes = params[:time_entry]
507 time_entry.attributes = params[:time_entry]
524 @issue.time_entries << time_entry
508 @issue.time_entries << time_entry
525 end
509 end
526
510
527 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
511 call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
528 if @issue.save
512 if @issue.save
529 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
513 call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
530 else
514 else
531 raise ActiveRecord::Rollback
515 raise ActiveRecord::Rollback
532 end
516 end
533 end
517 end
534 end
518 end
535
519
536 # Returns true if the issue copy should be linked
520 # Returns true if the issue copy should be linked
537 # to the original issue
521 # to the original issue
538 def link_copy?(param)
522 def link_copy?(param)
539 case Setting.link_copied_issue
523 case Setting.link_copied_issue
540 when 'yes'
524 when 'yes'
541 true
525 true
542 when 'no'
526 when 'no'
543 false
527 false
544 when 'ask'
528 when 'ask'
545 param == '1'
529 param == '1'
546 end
530 end
547 end
531 end
548
532
549 # Redirects user after a successful issue creation
533 # Redirects user after a successful issue creation
550 def redirect_after_create
534 def redirect_after_create
551 if params[:continue]
535 if params[:continue]
552 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
536 attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
553 if params[:project_id]
537 if params[:project_id]
554 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
538 redirect_to new_project_issue_path(@issue.project, :issue => attrs)
555 else
539 else
556 attrs.merge! :project_id => @issue.project_id
540 attrs.merge! :project_id => @issue.project_id
557 redirect_to new_issue_path(:issue => attrs)
541 redirect_to new_issue_path(:issue => attrs)
558 end
542 end
559 else
543 else
560 redirect_to issue_path(@issue)
544 redirect_to issue_path(@issue)
561 end
545 end
562 end
546 end
563 end
547 end
@@ -1,290 +1,274
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class TimelogController < ApplicationController
18 class TimelogController < ApplicationController
19 menu_item :issues
19 menu_item :issues
20
20
21 before_filter :find_time_entry, :only => [:show, :edit, :update]
21 before_filter :find_time_entry, :only => [:show, :edit, :update]
22 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
22 before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
23 before_filter :authorize, :only => [:show, :edit, :update, :bulk_edit, :bulk_update, :destroy]
23 before_filter :authorize, :only => [:show, :edit, :update, :bulk_edit, :bulk_update, :destroy]
24
24
25 before_filter :find_optional_project, :only => [:new, :create, :index, :report]
25 before_filter :find_optional_project, :only => [:new, :create, :index, :report]
26 before_filter :authorize_global, :only => [:new, :create, :index, :report]
26 before_filter :authorize_global, :only => [:new, :create, :index, :report]
27
27
28 accept_rss_auth :index
28 accept_rss_auth :index
29 accept_api_auth :index, :show, :create, :update, :destroy
29 accept_api_auth :index, :show, :create, :update, :destroy
30
30
31 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
31 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
32
32
33 helper :sort
33 helper :sort
34 include SortHelper
34 include SortHelper
35 helper :issues
35 helper :issues
36 include TimelogHelper
36 include TimelogHelper
37 helper :custom_fields
37 helper :custom_fields
38 include CustomFieldsHelper
38 include CustomFieldsHelper
39 helper :queries
39 helper :queries
40 include QueriesHelper
40 include QueriesHelper
41
41
42 def index
42 def index
43 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
43 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
44
44
45 sort_init(@query.sort_criteria.empty? ? [['spent_on', 'desc']] : @query.sort_criteria)
45 sort_init(@query.sort_criteria.empty? ? [['spent_on', 'desc']] : @query.sort_criteria)
46 sort_update(@query.sortable_columns)
46 sort_update(@query.sortable_columns)
47 scope = time_entry_scope(:order => sort_clause).
47 scope = time_entry_scope(:order => sort_clause).
48 includes(:project, :user, :issue).
48 includes(:project, :user, :issue).
49 preload(:issue => [:project, :tracker, :status, :assigned_to, :priority])
49 preload(:issue => [:project, :tracker, :status, :assigned_to, :priority])
50
50
51 respond_to do |format|
51 respond_to do |format|
52 format.html {
52 format.html {
53 @entry_count = scope.count
53 @entry_count = scope.count
54 @entry_pages = Paginator.new @entry_count, per_page_option, params['page']
54 @entry_pages = Paginator.new @entry_count, per_page_option, params['page']
55 @entries = scope.offset(@entry_pages.offset).limit(@entry_pages.per_page).to_a
55 @entries = scope.offset(@entry_pages.offset).limit(@entry_pages.per_page).to_a
56 @total_hours = scope.sum(:hours).to_f
56 @total_hours = scope.sum(:hours).to_f
57
57
58 render :layout => !request.xhr?
58 render :layout => !request.xhr?
59 }
59 }
60 format.api {
60 format.api {
61 @entry_count = scope.count
61 @entry_count = scope.count
62 @offset, @limit = api_offset_and_limit
62 @offset, @limit = api_offset_and_limit
63 @entries = scope.offset(@offset).limit(@limit).preload(:custom_values => :custom_field).to_a
63 @entries = scope.offset(@offset).limit(@limit).preload(:custom_values => :custom_field).to_a
64 }
64 }
65 format.atom {
65 format.atom {
66 entries = scope.limit(Setting.feeds_limit.to_i).reorder("#{TimeEntry.table_name}.created_on DESC").to_a
66 entries = scope.limit(Setting.feeds_limit.to_i).reorder("#{TimeEntry.table_name}.created_on DESC").to_a
67 render_feed(entries, :title => l(:label_spent_time))
67 render_feed(entries, :title => l(:label_spent_time))
68 }
68 }
69 format.csv {
69 format.csv {
70 # Export all entries
70 # Export all entries
71 @entries = scope.to_a
71 @entries = scope.to_a
72 send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'timelog.csv')
72 send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'timelog.csv')
73 }
73 }
74 end
74 end
75 end
75 end
76
76
77 def report
77 def report
78 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
78 @query = TimeEntryQuery.build_from_params(params, :project => @project, :name => '_')
79 scope = time_entry_scope
79 scope = time_entry_scope
80
80
81 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], scope)
81 @report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], scope)
82
82
83 respond_to do |format|
83 respond_to do |format|
84 format.html { render :layout => !request.xhr? }
84 format.html { render :layout => !request.xhr? }
85 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
85 format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
86 end
86 end
87 end
87 end
88
88
89 def show
89 def show
90 respond_to do |format|
90 respond_to do |format|
91 # TODO: Implement html response
91 # TODO: Implement html response
92 format.html { render :nothing => true, :status => 406 }
92 format.html { render :nothing => true, :status => 406 }
93 format.api
93 format.api
94 end
94 end
95 end
95 end
96
96
97 def new
97 def new
98 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
98 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
99 @time_entry.safe_attributes = params[:time_entry]
99 @time_entry.safe_attributes = params[:time_entry]
100 end
100 end
101
101
102 def create
102 def create
103 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
103 @time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
104 @time_entry.safe_attributes = params[:time_entry]
104 @time_entry.safe_attributes = params[:time_entry]
105 if @time_entry.project && !User.current.allowed_to?(:log_time, @time_entry.project)
105 if @time_entry.project && !User.current.allowed_to?(:log_time, @time_entry.project)
106 render_403
106 render_403
107 return
107 return
108 end
108 end
109
109
110 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
110 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
111
111
112 if @time_entry.save
112 if @time_entry.save
113 respond_to do |format|
113 respond_to do |format|
114 format.html {
114 format.html {
115 flash[:notice] = l(:notice_successful_create)
115 flash[:notice] = l(:notice_successful_create)
116 if params[:continue]
116 if params[:continue]
117 options = {
117 options = {
118 :time_entry => {
118 :time_entry => {
119 :project_id => params[:time_entry][:project_id],
119 :project_id => params[:time_entry][:project_id],
120 :issue_id => @time_entry.issue_id,
120 :issue_id => @time_entry.issue_id,
121 :activity_id => @time_entry.activity_id
121 :activity_id => @time_entry.activity_id
122 },
122 },
123 :back_url => params[:back_url]
123 :back_url => params[:back_url]
124 }
124 }
125 if params[:project_id] && @time_entry.project
125 if params[:project_id] && @time_entry.project
126 redirect_to new_project_time_entry_path(@time_entry.project, options)
126 redirect_to new_project_time_entry_path(@time_entry.project, options)
127 elsif params[:issue_id] && @time_entry.issue
127 elsif params[:issue_id] && @time_entry.issue
128 redirect_to new_issue_time_entry_path(@time_entry.issue, options)
128 redirect_to new_issue_time_entry_path(@time_entry.issue, options)
129 else
129 else
130 redirect_to new_time_entry_path(options)
130 redirect_to new_time_entry_path(options)
131 end
131 end
132 else
132 else
133 redirect_back_or_default project_time_entries_path(@time_entry.project)
133 redirect_back_or_default project_time_entries_path(@time_entry.project)
134 end
134 end
135 }
135 }
136 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
136 format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
137 end
137 end
138 else
138 else
139 respond_to do |format|
139 respond_to do |format|
140 format.html { render :action => 'new' }
140 format.html { render :action => 'new' }
141 format.api { render_validation_errors(@time_entry) }
141 format.api { render_validation_errors(@time_entry) }
142 end
142 end
143 end
143 end
144 end
144 end
145
145
146 def edit
146 def edit
147 @time_entry.safe_attributes = params[:time_entry]
147 @time_entry.safe_attributes = params[:time_entry]
148 end
148 end
149
149
150 def update
150 def update
151 @time_entry.safe_attributes = params[:time_entry]
151 @time_entry.safe_attributes = params[:time_entry]
152
152
153 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
153 call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
154
154
155 if @time_entry.save
155 if @time_entry.save
156 respond_to do |format|
156 respond_to do |format|
157 format.html {
157 format.html {
158 flash[:notice] = l(:notice_successful_update)
158 flash[:notice] = l(:notice_successful_update)
159 redirect_back_or_default project_time_entries_path(@time_entry.project)
159 redirect_back_or_default project_time_entries_path(@time_entry.project)
160 }
160 }
161 format.api { render_api_ok }
161 format.api { render_api_ok }
162 end
162 end
163 else
163 else
164 respond_to do |format|
164 respond_to do |format|
165 format.html { render :action => 'edit' }
165 format.html { render :action => 'edit' }
166 format.api { render_validation_errors(@time_entry) }
166 format.api { render_validation_errors(@time_entry) }
167 end
167 end
168 end
168 end
169 end
169 end
170
170
171 def bulk_edit
171 def bulk_edit
172 @available_activities = TimeEntryActivity.shared.active
172 @available_activities = TimeEntryActivity.shared.active
173 @custom_fields = TimeEntry.first.available_custom_fields
173 @custom_fields = TimeEntry.first.available_custom_fields
174 end
174 end
175
175
176 def bulk_update
176 def bulk_update
177 attributes = parse_params_for_bulk_time_entry_attributes(params)
177 attributes = parse_params_for_bulk_update(params[:time_entry])
178
178
179 unsaved_time_entry_ids = []
179 unsaved_time_entry_ids = []
180 @time_entries.each do |time_entry|
180 @time_entries.each do |time_entry|
181 time_entry.reload
181 time_entry.reload
182 time_entry.safe_attributes = attributes
182 time_entry.safe_attributes = attributes
183 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
183 call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
184 unless time_entry.save
184 unless time_entry.save
185 logger.info "time entry could not be updated: #{time_entry.errors.full_messages}" if logger && logger.info?
185 logger.info "time entry could not be updated: #{time_entry.errors.full_messages}" if logger && logger.info?
186 # Keep unsaved time_entry ids to display them in flash error
186 # Keep unsaved time_entry ids to display them in flash error
187 unsaved_time_entry_ids << time_entry.id
187 unsaved_time_entry_ids << time_entry.id
188 end
188 end
189 end
189 end
190 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
190 set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
191 redirect_back_or_default project_time_entries_path(@projects.first)
191 redirect_back_or_default project_time_entries_path(@projects.first)
192 end
192 end
193
193
194 def destroy
194 def destroy
195 destroyed = TimeEntry.transaction do
195 destroyed = TimeEntry.transaction do
196 @time_entries.each do |t|
196 @time_entries.each do |t|
197 unless t.destroy && t.destroyed?
197 unless t.destroy && t.destroyed?
198 raise ActiveRecord::Rollback
198 raise ActiveRecord::Rollback
199 end
199 end
200 end
200 end
201 end
201 end
202
202
203 respond_to do |format|
203 respond_to do |format|
204 format.html {
204 format.html {
205 if destroyed
205 if destroyed
206 flash[:notice] = l(:notice_successful_delete)
206 flash[:notice] = l(:notice_successful_delete)
207 else
207 else
208 flash[:error] = l(:notice_unable_delete_time_entry)
208 flash[:error] = l(:notice_unable_delete_time_entry)
209 end
209 end
210 redirect_back_or_default project_time_entries_path(@projects.first)
210 redirect_back_or_default project_time_entries_path(@projects.first)
211 }
211 }
212 format.api {
212 format.api {
213 if destroyed
213 if destroyed
214 render_api_ok
214 render_api_ok
215 else
215 else
216 render_validation_errors(@time_entries)
216 render_validation_errors(@time_entries)
217 end
217 end
218 }
218 }
219 end
219 end
220 end
220 end
221
221
222 private
222 private
223 def find_time_entry
223 def find_time_entry
224 @time_entry = TimeEntry.find(params[:id])
224 @time_entry = TimeEntry.find(params[:id])
225 unless @time_entry.editable_by?(User.current)
225 unless @time_entry.editable_by?(User.current)
226 render_403
226 render_403
227 return false
227 return false
228 end
228 end
229 @project = @time_entry.project
229 @project = @time_entry.project
230 rescue ActiveRecord::RecordNotFound
230 rescue ActiveRecord::RecordNotFound
231 render_404
231 render_404
232 end
232 end
233
233
234 def find_time_entries
234 def find_time_entries
235 @time_entries = TimeEntry.where(:id => params[:id] || params[:ids]).to_a
235 @time_entries = TimeEntry.where(:id => params[:id] || params[:ids]).to_a
236 raise ActiveRecord::RecordNotFound if @time_entries.empty?
236 raise ActiveRecord::RecordNotFound if @time_entries.empty?
237 raise Unauthorized unless @time_entries.all? {|t| t.editable_by?(User.current)}
237 raise Unauthorized unless @time_entries.all? {|t| t.editable_by?(User.current)}
238 @projects = @time_entries.collect(&:project).compact.uniq
238 @projects = @time_entries.collect(&:project).compact.uniq
239 @project = @projects.first if @projects.size == 1
239 @project = @projects.first if @projects.size == 1
240 rescue ActiveRecord::RecordNotFound
240 rescue ActiveRecord::RecordNotFound
241 render_404
241 render_404
242 end
242 end
243
243
244 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
244 def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
245 if unsaved_time_entry_ids.empty?
245 if unsaved_time_entry_ids.empty?
246 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
246 flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
247 else
247 else
248 flash[:error] = l(:notice_failed_to_save_time_entries,
248 flash[:error] = l(:notice_failed_to_save_time_entries,
249 :count => unsaved_time_entry_ids.size,
249 :count => unsaved_time_entry_ids.size,
250 :total => time_entries.size,
250 :total => time_entries.size,
251 :ids => '#' + unsaved_time_entry_ids.join(', #'))
251 :ids => '#' + unsaved_time_entry_ids.join(', #'))
252 end
252 end
253 end
253 end
254
254
255 def find_optional_project
255 def find_optional_project
256 if params[:issue_id].present?
256 if params[:issue_id].present?
257 @issue = Issue.find(params[:issue_id])
257 @issue = Issue.find(params[:issue_id])
258 @project = @issue.project
258 @project = @issue.project
259 elsif params[:project_id].present?
259 elsif params[:project_id].present?
260 @project = Project.find(params[:project_id])
260 @project = Project.find(params[:project_id])
261 end
261 end
262 rescue ActiveRecord::RecordNotFound
262 rescue ActiveRecord::RecordNotFound
263 render_404
263 render_404
264 end
264 end
265
265
266 # Returns the TimeEntry scope for index and report actions
266 # Returns the TimeEntry scope for index and report actions
267 def time_entry_scope(options={})
267 def time_entry_scope(options={})
268 scope = @query.results_scope(options)
268 scope = @query.results_scope(options)
269 if @issue
269 if @issue
270 scope = scope.on_issue(@issue)
270 scope = scope.on_issue(@issue)
271 end
271 end
272 scope
272 scope
273 end
273 end
274
275 def parse_params_for_bulk_time_entry_attributes(params)
276 attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
277 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
278 if custom = attributes[:custom_field_values]
279 custom.reject! {|k,v| v.blank?}
280 custom.keys.each do |k|
281 if custom[k].is_a?(Array)
282 custom[k] << '' if custom[k].delete('__none__')
283 else
284 custom[k] = '' if custom[k] == '__none__'
285 end
286 end
287 end
288 attributes
289 end
290 end
274 end
General Comments 0
You need to be logged in to leave comments. Login now