File: app/controllers/issues_controller.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  class: IssuesController#18
includes
  AttachmentsHelper   
  CustomFieldsHelper   
  IssueRelationsHelper   
  IssuesHelper   
  ProjectsHelper   
  QueriesHelper   
  RepositoriesHelper   
  SortHelper   
  WatchersHelper   
  PDF ( Redmine::Export )
inherits from
  ApplicationController   
has properties
method: index #56
method: show #102
method: new #128
method: create #145
method: edit #168
method: update #177
method: bulk_edit #212
method: bulk_update #246
method: destroy #281
method: find_issue #316
method: find_project #329
method: retrieve_previous_and_next_issue_ids #336
method: update_issue_from_params #357
method: build_new_issue_from_params #386
method: check_for_default_issue_status #420
method: parse_params_for_bulk_issue_attributes / 1 #427

Class Hierarchy

Code

   1  # Redmine - project management software
   2  # Copyright (C) 2006-2011  Jean-Philippe Lang
   3  #
   4  # This program is free software; you can redistribute it and/or
   5  # modify it under the terms of the GNU General Public License
   6  # as published by the Free Software Foundation; either version 2
   7  # of the License, or (at your option) any later version.
   8  #
   9  # This program is distributed in the hope that it will be useful,
  10  # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  # GNU General Public License for more details.
  13  #
  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
  16  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  17 
  18  class IssuesController < ApplicationController
  19    menu_item :new_issue, :only => [:new, :create]
  20    default_search_scope :issues
  21 
  22    before_filter :find_issue, :only => [:show, :edit, :update]
  23    before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
  24    before_filter :find_project, :only => [:new, :create]
  25    before_filter :authorize, :except => [:index]
  26    before_filter :find_optional_project, :only => [:index]
  27    before_filter :check_for_default_issue_status, :only => [:new, :create]
  28    before_filter :build_new_issue_from_params, :only => [:new, :create]
  29    accept_rss_auth :index, :show
  30    accept_api_auth :index, :show, :create, :update, :destroy
  31 
  32    rescue_from Query::StatementInvalid, :with => :query_statement_invalid
  33 
  34    helper :journals
  35    helper :projects
  36    include ProjectsHelper
  37    helper :custom_fields
  38    include CustomFieldsHelper
  39    helper :issue_relations
  40    include IssueRelationsHelper
  41    helper :watchers
  42    include WatchersHelper
  43    helper :attachments
  44    include AttachmentsHelper
  45    helper :queries
  46    include QueriesHelper
  47    helper :repositories
  48    include RepositoriesHelper
  49    helper :sort
  50    include SortHelper
  51    include IssuesHelper
  52    helper :timelog
  53    helper :gantt
  54    include Redmine::Export::PDF
  55 
  56    def index
  57      retrieve_query
  58      sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
  59      sort_update(@query.sortable_columns)
  60 
  61      if @query.valid?
  62        case params[:format]
  63        when 'csv', 'pdf'
  64          @limit = Setting.issues_export_limit.to_i
  65        when 'atom'
  66          @limit = Setting.feeds_limit.to_i
  67        when 'xml', 'json'
  68          @offset, @limit = api_offset_and_limit
  69        else
  70          @limit = per_page_option
  71        end
  72 
  73        @issue_count = @query.issue_count
  74        @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
  75        @offset ||= @issue_pages.current.offset
  76        @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
  77                                :order => sort_clause,
  78                                :offset => @offset,
  79                                :limit => @limit)
  80        @issue_count_by_group = @query.issue_count_by_group
  81 
  82        respond_to do |format|
  83          format.html { render :template => 'issues/index', :layout => !request.xhr? }
  84          format.api  {
  85            Issue.load_relations(@issues) if include_in_api_response?('relations')
  86          }
  87          format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
  88          format.csv  { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') }
  89          format.pdf  { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
  90        end
  91      else
  92        respond_to do |format|
  93          format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
  94          format.any(:atom, :csv, :pdf) { render(:nothing => true) }
  95          format.api { render_validation_errors(@query) }
  96        end
  97      end
  98    rescue ActiveRecord::RecordNotFound
  99      render_404
 100    end
 101 
 102    def show
 103      @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
 104      @journals.each_with_index {|j,i| j.indice = i+1}
 105      @journals.reverse! if User.current.wants_comments_in_reverse_order?
 106 
 107      @changesets = @issue.changesets.visible.all
 108      @changesets.reverse! if User.current.wants_comments_in_reverse_order?
 109 
 110      @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
 111      @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
 112      @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
 113      @priorities = IssuePriority.active
 114      @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
 115      respond_to do |format|
 116        format.html {
 117          retrieve_previous_and_next_issue_ids
 118          render :template => 'issues/show'
 119        }
 120        format.api
 121        format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
 122        format.pdf  { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
 123      end
 124    end
 125 
 126    # Add a new issue
 127    # The new issue will be created from an existing one if copy_from parameter is given
 128    def new
 129      respond_to do |format|
 130        format.html { render :action => 'new', :layout => !request.xhr? }
 131        format.js {
 132          render(:update) { |page|
 133            if params[:project_change]
 134              page.replace_html 'all_attributes', :partial => 'form'
 135            else
 136              page.replace_html 'attributes', :partial => 'attributes'
 137            end
 138            m = User.current.allowed_to?(:log_time, @issue.project) ? 'show' : 'hide'
 139            page << "if ($('log_time')) {Element.#{m}('log_time');}"
 140          }
 141        }
 142      end
 143    end
 144 
 145    def create
 146      call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
 147      @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
 148      if @issue.save
 149        call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
 150        respond_to do |format|
 151          format.html {
 152            render_attachment_warning_if_needed(@issue)
 153            flash[:notice] = l(:notice_issue_successful_create, :id => "<a href='#{issue_path(@issue)}'>##{@issue.id}</a>")
 154            redirect_to(params[:continue] ?  { :action => 'new', :project_id => @issue.project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
 155                        { :action => 'show', :id => @issue })
 156          }
 157          format.api  { render :action => 'show', :status => :created, :location => issue_url(@issue) }
 158        end
 159        return
 160      else
 161        respond_to do |format|
 162          format.html { render :action => 'new' }
 163          format.api  { render_validation_errors(@issue) }
 164        end
 165      end
 166    end
 167 
 168    def edit
 169      return unless update_issue_from_params
 170 
 171      respond_to do |format|
 172        format.html { }
 173        format.xml  { }
 174      end
 175    end
 176 
 177    def update
 178      return unless update_issue_from_params
 179      @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
 180      saved = false
 181      begin
 182        saved = @issue.save_issue_with_child_records(params, @time_entry)
 183      rescue ActiveRecord::StaleObjectError
 184        @conflict = true
 185        if params[:last_journal_id]
 186          if params[:last_journal_id].present?
 187            last_journal_id = params[:last_journal_id].to_i
 188            @conflict_journals = @issue.journals.all(:conditions => ["#{Journal.table_name}.id > ?", last_journal_id])
 189          else
 190            @conflict_journals = @issue.journals.all
 191          end
 192        end
 193      end
 194 
 195      if saved
 196        render_attachment_warning_if_needed(@issue)
 197        flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
 198 
 199        respond_to do |format|
 200          format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
 201          format.api  { head :ok }
 202        end
 203      else
 204        respond_to do |format|
 205          format.html { render :action => 'edit' }
 206          format.api  { render_validation_errors(@issue) }
 207        end
 208      end
 209    end
 210 
 211    # Bulk edit/copy a set of issues
 212    def bulk_edit
 213      @issues.sort!
 214      @copy = params[:copy].present?
 215      @notes = params[:notes]
 216 
 217      if User.current.allowed_to?(:move_issues, @projects)
 218        @allowed_projects = Issue.allowed_target_projects_on_move
 219        if params[:issue]
 220          @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
 221          if @target_project
 222            target_projects = [@target_project]
 223          end
 224        end
 225      end
 226      target_projects ||= @projects
 227 
 228      if @copy
 229        @available_statuses = [IssueStatus.default]
 230      else
 231        @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
 232      end
 233      @custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&)
 234      @assignables = target_projects.map(&:assignable_users).reduce(:&)
 235      @trackers = target_projects.map(&:trackers).reduce(:&)
 236      @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
 237      @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
 238      if @copy
 239        @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
 240      end
 241 
 242      @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
 243      render :layout => false if request.xhr?
 244    end
 245 
 246    def bulk_update
 247      @issues.sort!
 248      @copy = params[:copy].present?
 249      attributes = parse_params_for_bulk_issue_attributes(params)
 250 
 251      unsaved_issue_ids = []
 252      moved_issues = []
 253      @issues.each do |issue|
 254        issue.reload
 255        if @copy
 256          issue = issue.copy({}, :attachments => params[:copy_attachments].present?)
 257        end
 258        journal = issue.init_journal(User.current, params[:notes])
 259        issue.safe_attributes = attributes
 260        call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
 261        if issue.save
 262          moved_issues << issue
 263        else
 264          # Keep unsaved issue ids to display them in flash error
 265          unsaved_issue_ids << issue.id
 266        end
 267      end
 268      set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
 269 
 270      if params[:follow]
 271        if @issues.size == 1 && moved_issues.size == 1
 272          redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
 273        elsif moved_issues.map(&:project).uniq.size == 1
 274          redirect_to :controller => 'issues', :action => 'index', :project_id => moved_issues.map(&:project).first
 275        end
 276      else
 277        redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
 278      end
 279    end
 280 
 281    def destroy
 282      @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
 283      if @hours > 0
 284        case params[:todo]
 285        when 'destroy'
 286          # nothing to do
 287        when 'nullify'
 288          TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
 289        when 'reassign'
 290          reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
 291          if reassign_to.nil?
 292            flash.now[:error] = l(:error_issue_not_found_in_project)
 293            return
 294          else
 295            TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
 296          end
 297        else
 298          # display the destroy form if it's a user request
 299          return unless api_request?
 300        end
 301      end
 302      @issues.each do |issue|
 303        begin
 304          issue.reload.destroy
 305        rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
 306          # nothing to do, issue was already deleted (eg. by a parent)
 307        end
 308      end
 309      respond_to do |format|
 310        format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
 311        format.api  { head :ok }
 312      end
 313    end
 314 
 315  private
 316    def find_issue
 317      # Issue.visible.find(...) can not be used to redirect user to the login form
 318      # if the issue actually exists but requires authentication
 319      @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
 320      unless @issue.visible?
 321        deny_access
 322        return
 323      end
 324      @project = @issue.project
 325    rescue ActiveRecord::RecordNotFound
 326      render_404
 327    end
 328 
 329    def find_project
 330      project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
 331      @project = Project.find(project_id)
 332    rescue ActiveRecord::RecordNotFound
 333      render_404
 334    end
 335 
 336    def retrieve_previous_and_next_issue_ids
 337      retrieve_query_from_session
 338      if @query
 339        sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
 340        sort_update(@query.sortable_columns, 'issues_index_sort')
 341        limit = 500
 342        issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
 343        if (idx = issue_ids.index(@issue.id)) && idx < limit
 344          if issue_ids.size < 500
 345            @issue_position = idx + 1
 346            @issue_count = issue_ids.size
 347          end
 348          @prev_issue_id = issue_ids[idx - 1] if idx > 0
 349          @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
 350        end
 351      end
 352    end
 353 
 354    # Used by #edit and #update to set some common instance variables
 355    # from the params
 356    # TODO: Refactor, not everything in here is needed by #edit
 357    def update_issue_from_params
 358      @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
 359      @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
 360      @time_entry.attributes = params[:time_entry]
 361 
 362      @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
 363      @issue.init_journal(User.current, @notes)
 364 
 365      issue_attributes = params[:issue]
 366      if issue_attributes && params[:conflict_resolution]
 367        case params[:conflict_resolution]
 368        when 'overwrite'
 369          issue_attributes = issue_attributes.dup
 370          issue_attributes.delete(:lock_version)
 371        when 'add_notes'
 372          issue_attributes = {}
 373        when 'cancel'
 374          redirect_to issue_path(@issue)
 375          return false
 376        end
 377      end
 378      @issue.safe_attributes = issue_attributes
 379      @priorities = IssuePriority.active
 380      @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
 381      true
 382    end
 383 
 384    # TODO: Refactor, lots of extra code in here
 385    # TODO: Changing tracker on an existing issue should not trigger this
 386    def build_new_issue_from_params
 387      if params[:id].blank?
 388        @issue = Issue.new
 389        if params[:copy_from]
 390          begin
 391            @copy_from = Issue.visible.find(params[:copy_from])
 392            @copy_attachments = params[:copy_attachments].present? || request.get?
 393            @issue.copy_from(@copy_from, :attachments => @copy_attachments)
 394          rescue ActiveRecord::RecordNotFound
 395            render_404
 396            return
 397          end
 398        end
 399        @issue.project = @project
 400      else
 401        @issue = @project.issues.visible.find(params[:id])
 402      end
 403 
 404      @issue.project = @project
 405      @issue.author = User.current
 406      # Tracker must be set before custom field values
 407      @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
 408      if @issue.tracker.nil?
 409        render_error l(:error_no_tracker_in_project)
 410        return false
 411      end
 412      @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
 413      @issue.safe_attributes = params[:issue]
 414 
 415      @priorities = IssuePriority.active
 416      @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
 417      @available_watchers = (@issue.project.users.sort + @issue.watcher_users).uniq
 418    end
 419 
 420    def check_for_default_issue_status
 421      if IssueStatus.default.nil?
 422        render_error l(:error_no_default_issue_status)
 423        return false
 424      end
 425    end
 426 
 427    def parse_params_for_bulk_issue_attributes(params)
 428      attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
 429      attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
 430      if custom = attributes[:custom_field_values]
 431        custom.reject! {|k,v| v.blank?}
 432        custom.keys.each do |k|
 433          if custom[k].is_a?(Array)
 434            custom[k] << '' if custom[k].delete('__none__')
 435          else
 436            custom[k] = '' if custom[k] == '__none__'
 437          end
 438        end
 439      end
 440      attributes
 441    end
 442  end