To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / app / controllers / issues_controller.rb @ 1541:2696466256ff

History | View | Annotate | Download (18.1 KB)

1
# Redmine - project management software
2
# Copyright (C) 2006-2014  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, :update_form]
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, :update_form]
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
  include Redmine::Export::PDF
54

    
55
  def index
56
    retrieve_query
57
    sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
58
    sort_update(@query.sortable_columns)
59
    @query.sort_criteria = sort_criteria.to_a
60

    
61
    if @query.valid?
62
      case params[:format]
63
      when 'csv', 'pdf'
64
        @limit = Setting.issues_export_limit.to_i
65
        if params[:columns] == 'all'
66
          @query.column_names = @query.available_inline_columns.map(&:name)
67
        end
68
      when 'atom'
69
        @limit = Setting.feeds_limit.to_i
70
      when 'xml', 'json'
71
        @offset, @limit = api_offset_and_limit
72
        @query.column_names = %w(author)
73
      else
74
        @limit = per_page_option
75
      end
76

    
77
      @issue_count = @query.issue_count
78
      @issue_pages = Paginator.new @issue_count, @limit, params['page']
79
      @offset ||= @issue_pages.offset
80
      @issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
81
                              :order => sort_clause,
82
                              :offset => @offset,
83
                              :limit => @limit)
84
      @issue_count_by_group = @query.issue_count_by_group
85

    
86
      respond_to do |format|
87
        format.html { render :template => 'issues/index', :layout => !request.xhr? }
88
        format.api  {
89
          Issue.load_visible_relations(@issues) if include_in_api_response?('relations')
90
        }
91
        format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
92
        format.csv  { send_data(query_to_csv(@issues, @query, params), :type => 'text/csv; header=present', :filename => 'issues.csv') }
93
        format.pdf  { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'issues.pdf') }
94
      end
95
    else
96
      respond_to do |format|
97
        format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
98
        format.any(:atom, :csv, :pdf) { render(:nothing => true) }
99
        format.api { render_validation_errors(@query) }
100
      end
101
    end
102
  rescue ActiveRecord::RecordNotFound
103
    render_404
104
  end
105

    
106
  def show
107
    @journals = @issue.journals.includes(:user, :details).reorder("#{Journal.table_name}.id ASC").all
108
    @journals.each_with_index {|j,i| j.indice = i+1}
109
    @journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
110
    Journal.preload_journals_details_custom_fields(@journals)
111
    # TODO: use #select! when ruby1.8 support is dropped
112
    @journals.reject! {|journal| !journal.notes? && journal.visible_details.empty?}
113
    @journals.reverse! if User.current.wants_comments_in_reverse_order?
114

    
115
    @changesets = @issue.changesets.visible.all
116
    @changesets.reverse! if User.current.wants_comments_in_reverse_order?
117

    
118
    @relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
119
    @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
120
    @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
121
    @priorities = IssuePriority.active
122
    @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
123
    @relation = IssueRelation.new
124

    
125
    respond_to do |format|
126
      format.html {
127
        retrieve_previous_and_next_issue_ids
128
        render :template => 'issues/show'
129
      }
130
      format.api
131
      format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
132
      format.pdf  {
133
        pdf = issue_to_pdf(@issue, :journals => @journals)
134
        send_data(pdf, :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf")
135
      }
136
    end
137
  end
138

    
139
  # Add a new issue
140
  # The new issue will be created from an existing one if copy_from parameter is given
141
  def new
142
    respond_to do |format|
143
      format.html { render :action => 'new', :layout => !request.xhr? }
144
    end
145
  end
146

    
147
  def create
148
    call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
149
    @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
150
    if @issue.save
151
      
152
      call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
153

    
154
      # Also adds the assignee to the watcher's list
155
      if params[:issue][:assigned_to_id] && !params[:issue][:assigned_to_id].empty?
156
       unless @issue.watcher_ids.include?(params[:issue][:assigned_to_id])
157
         @issue.add_watcher(User.find(params[:issue][:assigned_to_id]))
158
       end
159
      end
160

    
161
      respond_to do |format|
162
        format.html {
163
          render_attachment_warning_if_needed(@issue)
164
          flash[:notice] = l(:notice_issue_successful_create, :id => view_context.link_to("##{@issue.id}", issue_path(@issue), :title => @issue.subject))
165
          if params[:continue]
166
            attrs = {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?}
167
            redirect_to new_project_issue_path(@issue.project, :issue => attrs)
168
          else
169
            redirect_to issue_path(@issue)
170
          end
171
        }
172
        format.api  { render :action => 'show', :status => :created, :location => issue_url(@issue) }
173
      end
174
      return
175
    else
176
      respond_to do |format|
177
        format.html { render :action => 'new' }
178
        format.api  { render_validation_errors(@issue) }
179
      end
180
    end
181
  end
182

    
183
  def edit
184
    return unless update_issue_from_params
185

    
186
    respond_to do |format|
187
      format.html { }
188
      format.xml  { }
189
    end
190
  end
191

    
192
  def update
193
    return unless update_issue_from_params
194
    @issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
195
    saved = false
196
    begin
197
      saved = save_issue_with_child_records
198
    rescue ActiveRecord::StaleObjectError
199
      @conflict = true
200
      if params[:last_journal_id]
201
        @conflict_journals = @issue.journals_after(params[:last_journal_id]).all
202
        @conflict_journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
203
      end
204
    end
205

    
206
    if saved
207
      render_attachment_warning_if_needed(@issue)
208
      flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
209

    
210
      respond_to do |format|
211
        format.html { redirect_back_or_default issue_path(@issue) }
212
        format.api  { render_api_ok }
213
      end
214
    else
215
      respond_to do |format|
216
        format.html { render :action => 'edit' }
217
        format.api  { render_validation_errors(@issue) }
218
      end
219
    end
220
  end
221

    
222
  # Updates the issue form when changing the project, status or tracker
223
  # on issue creation/update
224
  def update_form
225
  end
226

    
227
  # Bulk edit/copy a set of issues
228
  def bulk_edit
229
    @issues.sort!
230
    @copy = params[:copy].present?
231
    @notes = params[:notes]
232

    
233
    if User.current.allowed_to?(:move_issues, @projects)
234
      @allowed_projects = Issue.allowed_target_projects_on_move
235
      if params[:issue]
236
        @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
237
        if @target_project
238
          target_projects = [@target_project]
239
        end
240
      end
241
    end
242
    target_projects ||= @projects
243

    
244
    if @copy
245
      @available_statuses = [IssueStatus.default]
246
    else
247
      @available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
248
    end
249
    @custom_fields = target_projects.map{|p|p.all_issue_custom_fields.visible}.reduce(:&)
250
    @assignables = target_projects.map(&:assignable_users).reduce(:&)
251
    @trackers = target_projects.map(&:trackers).reduce(:&)
252
    @versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
253
    @categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
254
    if @copy
255
      @attachments_present = @issues.detect {|i| i.attachments.any?}.present?
256
      @subtasks_present = @issues.detect {|i| !i.leaf?}.present?
257
    end
258

    
259
    @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
260

    
261
    @issue_params = params[:issue] || {}
262
    @issue_params[:custom_field_values] ||= {}
263
  end
264

    
265
  def bulk_update
266
    @issues.sort!
267
    @copy = params[:copy].present?
268
    attributes = parse_params_for_bulk_issue_attributes(params)
269

    
270
    unsaved_issues = []
271
    saved_issues = []
272

    
273
    if @copy && params[:copy_subtasks].present?
274
      # Descendant issues will be copied with the parent task
275
      # Don't copy them twice
276
      @issues.reject! {|issue| @issues.detect {|other| issue.is_descendant_of?(other)}}
277
    end
278

    
279
    @issues.each do |orig_issue|
280
      orig_issue.reload
281
      if @copy
282
        issue = orig_issue.copy({},
283
          :attachments => params[:copy_attachments].present?,
284
          :subtasks => params[:copy_subtasks].present?
285
        )
286
      else
287
        issue = orig_issue
288
      end
289
      journal = issue.init_journal(User.current, params[:notes])
290
      issue.safe_attributes = attributes
291
      call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
292
      if issue.save
293
        saved_issues << issue
294
      else
295
        unsaved_issues << orig_issue
296
      end
297
    end
298

    
299
    if unsaved_issues.empty?
300
      flash[:notice] = l(:notice_successful_update) unless saved_issues.empty?
301
      if params[:follow]
302
        if @issues.size == 1 && saved_issues.size == 1
303
          redirect_to issue_path(saved_issues.first)
304
        elsif saved_issues.map(&:project).uniq.size == 1
305
          redirect_to project_issues_path(saved_issues.map(&:project).first)
306
        end
307
      else
308
        redirect_back_or_default _project_issues_path(@project)
309
      end
310
    else
311
      @saved_issues = @issues
312
      @unsaved_issues = unsaved_issues
313
      @issues = Issue.visible.where(:id => @unsaved_issues.map(&:id)).all
314
      bulk_edit
315
      render :action => 'bulk_edit'
316
    end
317
  end
318

    
319
  def destroy
320
    @hours = TimeEntry.where(:issue_id => @issues.map(&:id)).sum(:hours).to_f
321
    if @hours > 0
322
      case params[:todo]
323
      when 'destroy'
324
        # nothing to do
325
      when 'nullify'
326
        TimeEntry.where(['issue_id IN (?)', @issues]).update_all('issue_id = NULL')
327
      when 'reassign'
328
        reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
329
        if reassign_to.nil?
330
          flash.now[:error] = l(:error_issue_not_found_in_project)
331
          return
332
        else
333
          TimeEntry.where(['issue_id IN (?)', @issues]).
334
            update_all("issue_id = #{reassign_to.id}")
335
        end
336
      else
337
        # display the destroy form if it's a user request
338
        return unless api_request?
339
      end
340
    end
341
    @issues.each do |issue|
342
      begin
343
        issue.reload.destroy
344
      rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
345
        # nothing to do, issue was already deleted (eg. by a parent)
346
      end
347
    end
348
    respond_to do |format|
349
      format.html { redirect_back_or_default _project_issues_path(@project) }
350
      format.api  { render_api_ok }
351
    end
352
  end
353

    
354
  private
355

    
356
  def find_project
357
    project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
358
    @project = Project.find(project_id)
359
  rescue ActiveRecord::RecordNotFound
360
    render_404
361
  end
362

    
363
  def retrieve_previous_and_next_issue_ids
364
    retrieve_query_from_session
365
    if @query
366
      sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
367
      sort_update(@query.sortable_columns, 'issues_index_sort')
368
      limit = 500
369
      issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
370
      if (idx = issue_ids.index(@issue.id)) && idx < limit
371
        if issue_ids.size < 500
372
          @issue_position = idx + 1
373
          @issue_count = issue_ids.size
374
        end
375
        @prev_issue_id = issue_ids[idx - 1] if idx > 0
376
        @next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
377
      end
378
    end
379
  end
380

    
381
  # Used by #edit and #update to set some common instance variables
382
  # from the params
383
  # TODO: Refactor, not everything in here is needed by #edit
384
  def update_issue_from_params
385
    @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
386
    @time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
387
    @time_entry.attributes = params[:time_entry]
388

    
389
    @issue.init_journal(User.current)
390

    
391
    issue_attributes = params[:issue]
392
    if issue_attributes && params[:conflict_resolution]
393
      case params[:conflict_resolution]
394
      when 'overwrite'
395
        issue_attributes = issue_attributes.dup
396
        issue_attributes.delete(:lock_version)
397
      when 'add_notes'
398
        issue_attributes = issue_attributes.slice(:notes)
399
      when 'cancel'
400
        redirect_to issue_path(@issue)
401
        return false
402
      end
403
    end
404

    
405
    # tests if the the user assigned_to_id
406
    # is in this issues watcher's list
407
    # if not, adds it.
408

    
409
    if params[:issue] && params[:issue][:assigned_to_id] && !params[:issue][:assigned_to_id].empty?
410
     unless @issue.watched_by?(User.find(params[:issue][:assigned_to_id]))
411
       @issue.add_watcher(User.find(params[:issue][:assigned_to_id]))
412
     end
413
    end
414

    
415
    @issue.safe_attributes = issue_attributes
416
    @priorities = IssuePriority.active
417
    @allowed_statuses = @issue.new_statuses_allowed_to(User.current)
418
    true
419

    
420
  end
421

    
422
  # TODO: Refactor, lots of extra code in here
423
  # TODO: Changing tracker on an existing issue should not trigger this
424
  def build_new_issue_from_params
425
    if params[:id].blank?
426
      @issue = Issue.new
427
      if params[:copy_from]
428
        begin
429
          @copy_from = Issue.visible.find(params[:copy_from])
430
          @copy_attachments = params[:copy_attachments].present? || request.get?
431
          @copy_subtasks = params[:copy_subtasks].present? || request.get?
432
          @issue.copy_from(@copy_from, :attachments => @copy_attachments, :subtasks => @copy_subtasks)
433
        rescue ActiveRecord::RecordNotFound
434
          render_404
435
          return
436
        end
437
      end
438
      @issue.project = @project
439
    else
440
      @issue = @project.issues.visible.find(params[:id])
441
    end
442

    
443
    @issue.project = @project
444
    @issue.author ||= User.current
445
    # Tracker must be set before custom field values
446
    @issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
447
    if @issue.tracker.nil?
448
      render_error l(:error_no_tracker_in_project)
449
      return false
450
    end
451
    @issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
452
    @issue.safe_attributes = params[:issue]
453

    
454
    @priorities = IssuePriority.active
455
    @allowed_statuses = @issue.new_statuses_allowed_to(User.current, @issue.new_record?)
456
    @available_watchers = @issue.watcher_users
457
    if @issue.project.users.count <= 20
458
      @available_watchers = (@available_watchers + @issue.project.users.sort).uniq
459
    end
460
  end
461

    
462
  def check_for_default_issue_status
463
    if IssueStatus.default.nil?
464
      render_error l(:error_no_default_issue_status)
465
      return false
466
    end
467
  end
468

    
469
  def parse_params_for_bulk_issue_attributes(params)
470
    attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
471
    attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
472
    if custom = attributes[:custom_field_values]
473
      custom.reject! {|k,v| v.blank?}
474
      custom.keys.each do |k|
475
        if custom[k].is_a?(Array)
476
          custom[k] << '' if custom[k].delete('__none__')
477
        else
478
          custom[k] = '' if custom[k] == '__none__'
479
        end
480
      end
481
    end
482
    attributes
483
  end
484

    
485
  # Saves @issue and a time_entry from the parameters
486
  def save_issue_with_child_records
487
    Issue.transaction do
488
      if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, @issue.project)
489
        time_entry = @time_entry || TimeEntry.new
490
        time_entry.project = @issue.project
491
        time_entry.issue = @issue
492
        time_entry.user = User.current
493
        time_entry.spent_on = User.current.today
494
        time_entry.attributes = params[:time_entry]
495
        @issue.time_entries << time_entry
496
      end
497

    
498
      call_hook(:controller_issues_edit_before_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
499
      if @issue.save
500
        call_hook(:controller_issues_edit_after_save, { :params => params, :issue => @issue, :time_entry => time_entry, :journal => @issue.current_journal})
501
      else
502
        raise ActiveRecord::Rollback
503
      end
504
    end
505
  end
506
end