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