annotate app/controllers/.svn/text-base/issues_controller.rb.svn-base @ 922:ad295b270cd4 live

FIx #446: "non-utf8 paths in repositories blow up repo viewer and reposman" by ensuring the iconv conversion always happens even if source and dest are intended to be the same encoding
author Chris Cannam
date Tue, 13 Mar 2012 16:33:49 +0000
parents 0c939c159af4
children
rev   line source
Chris@0 1 # Redmine - project management software
Chris@441 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
Chris@0 3 #
Chris@0 4 # This program is free software; you can redistribute it and/or
Chris@0 5 # modify it under the terms of the GNU General Public License
Chris@0 6 # as published by the Free Software Foundation; either version 2
Chris@0 7 # of the License, or (at your option) any later version.
Chris@441 8 #
Chris@0 9 # This program is distributed in the hope that it will be useful,
Chris@0 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
Chris@0 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Chris@0 12 # GNU General Public License for more details.
Chris@441 13 #
Chris@0 14 # You should have received a copy of the GNU General Public License
Chris@0 15 # along with this program; if not, write to the Free Software
Chris@0 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Chris@0 17
Chris@0 18 class IssuesController < ApplicationController
Chris@0 19 menu_item :new_issue, :only => [:new, :create]
Chris@0 20 default_search_scope :issues
Chris@441 21
Chris@14 22 before_filter :find_issue, :only => [:show, :edit, :update]
Chris@14 23 before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :move, :perform_move, :destroy]
chris@37 24 before_filter :check_project_uniqueness, :only => [:move, :perform_move]
Chris@14 25 before_filter :find_project, :only => [:new, :create]
Chris@14 26 before_filter :authorize, :except => [:index]
Chris@14 27 before_filter :find_optional_project, :only => [:index]
Chris@0 28 before_filter :check_for_default_issue_status, :only => [:new, :create]
Chris@0 29 before_filter :build_new_issue_from_params, :only => [:new, :create]
Chris@507 30 accept_rss_auth :index, :show
Chris@507 31 accept_api_auth :index, :show, :create, :update, :destroy
Chris@0 32
Chris@0 33 rescue_from Query::StatementInvalid, :with => :query_statement_invalid
Chris@441 34
Chris@0 35 helper :journals
Chris@0 36 helper :projects
Chris@441 37 include ProjectsHelper
Chris@0 38 helper :custom_fields
Chris@0 39 include CustomFieldsHelper
Chris@0 40 helper :issue_relations
Chris@0 41 include IssueRelationsHelper
Chris@0 42 helper :watchers
Chris@0 43 include WatchersHelper
Chris@0 44 helper :attachments
Chris@0 45 include AttachmentsHelper
Chris@0 46 helper :queries
Chris@0 47 include QueriesHelper
Chris@119 48 helper :repositories
Chris@119 49 include RepositoriesHelper
Chris@0 50 helper :sort
Chris@0 51 include SortHelper
Chris@0 52 include IssuesHelper
Chris@0 53 helper :timelog
chris@22 54 helper :gantt
Chris@0 55 include Redmine::Export::PDF
Chris@0 56
Chris@0 57 verify :method => [:post, :delete],
Chris@0 58 :only => :destroy,
Chris@0 59 :render => { :nothing => true, :status => :method_not_allowed }
Chris@0 60
Chris@0 61 verify :method => :post, :only => :create, :render => {:nothing => true, :status => :method_not_allowed }
Chris@14 62 verify :method => :post, :only => :bulk_update, :render => {:nothing => true, :status => :method_not_allowed }
Chris@0 63 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
Chris@441 64
Chris@0 65 def index
Chris@0 66 retrieve_query
Chris@0 67 sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
Chris@0 68 sort_update(@query.sortable_columns)
Chris@441 69
Chris@0 70 if @query.valid?
Chris@119 71 case params[:format]
Chris@0 72 when 'csv', 'pdf'
Chris@119 73 @limit = Setting.issues_export_limit.to_i
Chris@0 74 when 'atom'
Chris@119 75 @limit = Setting.feeds_limit.to_i
Chris@119 76 when 'xml', 'json'
Chris@119 77 @offset, @limit = api_offset_and_limit
Chris@0 78 else
Chris@119 79 @limit = per_page_option
Chris@0 80 end
Chris@441 81
Chris@0 82 @issue_count = @query.issue_count
Chris@119 83 @issue_pages = Paginator.new self, @issue_count, @limit, params['page']
Chris@119 84 @offset ||= @issue_pages.current.offset
Chris@0 85 @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
Chris@441 86 :order => sort_clause,
Chris@441 87 :offset => @offset,
Chris@119 88 :limit => @limit)
Chris@0 89 @issue_count_by_group = @query.issue_count_by_group
Chris@441 90
Chris@0 91 respond_to do |format|
Chris@0 92 format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
Chris@119 93 format.api
Chris@0 94 format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
Chris@0 95 format.csv { send_data(issues_to_csv(@issues, @project), :type => 'text/csv; header=present', :filename => 'export.csv') }
Chris@0 96 format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
Chris@0 97 end
Chris@0 98 else
Chris@0 99 # Send html if the query is not valid
Chris@0 100 render(:template => 'issues/index.rhtml', :layout => !request.xhr?)
Chris@0 101 end
Chris@0 102 rescue ActiveRecord::RecordNotFound
Chris@0 103 render_404
Chris@0 104 end
Chris@441 105
Chris@0 106 def show
Chris@0 107 @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
Chris@0 108 @journals.each_with_index {|j,i| j.indice = i+1}
Chris@0 109 @journals.reverse! if User.current.wants_comments_in_reverse_order?
Chris@441 110
Chris@441 111 if User.current.allowed_to?(:view_changesets, @project)
Chris@441 112 @changesets = @issue.changesets.visible.all
Chris@441 113 @changesets.reverse! if User.current.wants_comments_in_reverse_order?
Chris@441 114 end
Chris@441 115
Chris@210 116 @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
Chris@0 117 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
Chris@0 118 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
Chris@0 119 @priorities = IssuePriority.all
Chris@441 120 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
Chris@0 121 respond_to do |format|
Chris@0 122 format.html { render :template => 'issues/show.rhtml' }
Chris@119 123 format.api
Chris@14 124 format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
Chris@0 125 format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
Chris@0 126 end
Chris@0 127 end
Chris@0 128
Chris@0 129 # Add a new issue
Chris@0 130 # The new issue will be created from an existing one if copy_from parameter is given
Chris@0 131 def new
Chris@14 132 respond_to do |format|
Chris@14 133 format.html { render :action => 'new', :layout => !request.xhr? }
Chris@14 134 format.js { render :partial => 'attributes' }
Chris@14 135 end
Chris@0 136 end
Chris@0 137
Chris@0 138 def create
Chris@0 139 call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
Chris@0 140 if @issue.save
Chris@0 141 attachments = Attachment.attach_files(@issue, params[:attachments])
Chris@0 142 render_attachment_warning_if_needed(@issue)
Chris@0 143 flash[:notice] = l(:notice_successful_create)
Chris@0 144 call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
Chris@0 145 respond_to do |format|
Chris@0 146 format.html {
chris@22 147 redirect_to(params[:continue] ? { :action => 'new', :project_id => @project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
Chris@0 148 { :action => 'show', :id => @issue })
Chris@0 149 }
Chris@119 150 format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
Chris@0 151 end
Chris@0 152 return
Chris@0 153 else
Chris@0 154 respond_to do |format|
Chris@0 155 format.html { render :action => 'new' }
Chris@119 156 format.api { render_validation_errors(@issue) }
Chris@0 157 end
Chris@0 158 end
Chris@0 159 end
Chris@441 160
Chris@0 161 def edit
Chris@0 162 update_issue_from_params
Chris@0 163
Chris@0 164 @journal = @issue.current_journal
Chris@0 165
Chris@0 166 respond_to do |format|
Chris@0 167 format.html { }
Chris@0 168 format.xml { }
Chris@0 169 end
Chris@0 170 end
Chris@0 171
Chris@0 172 def update
Chris@0 173 update_issue_from_params
Chris@0 174
Chris@0 175 if @issue.save_issue_with_child_records(params, @time_entry)
Chris@0 176 render_attachment_warning_if_needed(@issue)
Chris@0 177 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
Chris@0 178
Chris@0 179 respond_to do |format|
Chris@0 180 format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
Chris@119 181 format.api { head :ok }
Chris@0 182 end
Chris@0 183 else
Chris@0 184 render_attachment_warning_if_needed(@issue)
Chris@0 185 flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
Chris@0 186 @journal = @issue.current_journal
Chris@0 187
Chris@0 188 respond_to do |format|
Chris@0 189 format.html { render :action => 'edit' }
Chris@119 190 format.api { render_validation_errors(@issue) }
Chris@0 191 end
Chris@0 192 end
Chris@0 193 end
Chris@0 194
Chris@0 195 # Bulk edit a set of issues
Chris@0 196 def bulk_edit
Chris@0 197 @issues.sort!
chris@37 198 @available_statuses = @projects.map{|p|Workflow.available_statuses(p)}.inject{|memo,w|memo & w}
chris@37 199 @custom_fields = @projects.map{|p|p.all_issue_custom_fields}.inject{|memo,c|memo & c}
chris@37 200 @assignables = @projects.map(&:assignable_users).inject{|memo,a| memo & a}
chris@37 201 @trackers = @projects.map(&:trackers).inject{|memo,t| memo & t}
Chris@0 202 end
Chris@0 203
Chris@14 204 def bulk_update
Chris@0 205 @issues.sort!
Chris@14 206 attributes = parse_params_for_bulk_issue_attributes(params)
Chris@14 207
Chris@14 208 unsaved_issue_ids = []
Chris@14 209 @issues.each do |issue|
Chris@14 210 issue.reload
Chris@14 211 journal = issue.init_journal(User.current, params[:notes])
Chris@14 212 issue.safe_attributes = attributes
Chris@14 213 call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
Chris@14 214 unless issue.save
Chris@14 215 # Keep unsaved issue ids to display them in flash error
Chris@14 216 unsaved_issue_ids << issue.id
Chris@0 217 end
Chris@0 218 end
Chris@14 219 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
Chris@14 220 redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
Chris@0 221 end
Chris@441 222
Chris@0 223 def destroy
Chris@0 224 @hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
Chris@0 225 if @hours > 0
Chris@0 226 case params[:todo]
Chris@0 227 when 'destroy'
Chris@0 228 # nothing to do
Chris@0 229 when 'nullify'
Chris@0 230 TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
Chris@0 231 when 'reassign'
Chris@0 232 reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
Chris@0 233 if reassign_to.nil?
Chris@0 234 flash.now[:error] = l(:error_issue_not_found_in_project)
Chris@0 235 return
Chris@0 236 else
Chris@0 237 TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
Chris@0 238 end
Chris@0 239 else
Chris@119 240 # display the destroy form if it's a user request
Chris@119 241 return unless api_request?
Chris@0 242 end
Chris@0 243 end
Chris@441 244 @issues.each do |issue|
Chris@441 245 begin
Chris@441 246 issue.reload.destroy
Chris@441 247 rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
Chris@441 248 # nothing to do, issue was already deleted (eg. by a parent)
Chris@441 249 end
Chris@441 250 end
Chris@0 251 respond_to do |format|
chris@37 252 format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
Chris@119 253 format.api { head :ok }
Chris@0 254 end
Chris@0 255 end
Chris@0 256
Chris@0 257 private
Chris@0 258 def find_issue
Chris@441 259 # Issue.visible.find(...) can not be used to redirect user to the login form
Chris@441 260 # if the issue actually exists but requires authentication
Chris@0 261 @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
Chris@441 262 unless @issue.visible?
Chris@441 263 deny_access
Chris@441 264 return
Chris@441 265 end
Chris@0 266 @project = @issue.project
Chris@0 267 rescue ActiveRecord::RecordNotFound
Chris@0 268 render_404
Chris@0 269 end
Chris@441 270
Chris@0 271 def find_project
Chris@0 272 project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
Chris@0 273 @project = Project.find(project_id)
Chris@0 274 rescue ActiveRecord::RecordNotFound
Chris@0 275 render_404
Chris@0 276 end
Chris@441 277
Chris@0 278 # Used by #edit and #update to set some common instance variables
Chris@0 279 # from the params
Chris@0 280 # TODO: Refactor, not everything in here is needed by #edit
Chris@0 281 def update_issue_from_params
Chris@0 282 @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
Chris@0 283 @priorities = IssuePriority.all
Chris@0 284 @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
Chris@441 285 @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
chris@37 286 @time_entry.attributes = params[:time_entry]
Chris@441 287
chris@22 288 @notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
Chris@0 289 @issue.init_journal(User.current, @notes)
chris@37 290 @issue.safe_attributes = params[:issue]
Chris@0 291 end
Chris@0 292
Chris@0 293 # TODO: Refactor, lots of extra code in here
chris@37 294 # TODO: Changing tracker on an existing issue should not trigger this
Chris@0 295 def build_new_issue_from_params
Chris@14 296 if params[:id].blank?
Chris@14 297 @issue = Issue.new
Chris@14 298 @issue.copy_from(params[:copy_from]) if params[:copy_from]
Chris@14 299 @issue.project = @project
Chris@14 300 else
Chris@14 301 @issue = @project.issues.visible.find(params[:id])
Chris@14 302 end
Chris@441 303
Chris@0 304 @issue.project = @project
Chris@507 305 @issue.author = User.current
Chris@0 306 # Tracker must be set before custom field values
Chris@0 307 @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
Chris@0 308 if @issue.tracker.nil?
Chris@0 309 render_error l(:error_no_tracker_in_project)
Chris@0 310 return false
Chris@0 311 end
chris@37 312 @issue.start_date ||= Date.today
Chris@0 313 if params[:issue].is_a?(Hash)
Chris@0 314 @issue.safe_attributes = params[:issue]
chris@37 315 if User.current.allowed_to?(:add_issue_watchers, @project) && @issue.new_record?
chris@37 316 @issue.watcher_user_ids = params[:issue]['watcher_user_ids']
chris@37 317 end
Chris@0 318 end
Chris@0 319 @priorities = IssuePriority.all
Chris@0 320 @allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
Chris@0 321 end
Chris@0 322
Chris@0 323 def check_for_default_issue_status
Chris@0 324 if IssueStatus.default.nil?
Chris@0 325 render_error l(:error_no_default_issue_status)
Chris@0 326 return false
Chris@0 327 end
Chris@0 328 end
Chris@14 329
Chris@14 330 def parse_params_for_bulk_issue_attributes(params)
Chris@14 331 attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
Chris@14 332 attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
Chris@14 333 attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
Chris@14 334 attributes
Chris@14 335 end
Chris@0 336 end