Project

General

Profile

Patch #42081 » issue.rb

redmine-5.1.3\app\models - Alfredo Renzetti, 2025-01-07 16:42

 
1
# frozen_string_literal: true
2

    
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

    
20
class Issue < ActiveRecord::Base
21
  include Redmine::SafeAttributes
22
  include Redmine::Utils::DateCalculation
23
  include Redmine::I18n
24
  before_save :set_parent_id
25
  include Redmine::NestedSet::IssueNestedSet
26

    
27
  belongs_to :project
28
  belongs_to :tracker
29
  belongs_to :status, :class_name => 'IssueStatus'
30
  belongs_to :author, :class_name => 'User'
31
  belongs_to :assigned_to, :class_name => 'Principal'
32
  belongs_to :fixed_version, :class_name => 'Version'
33
  belongs_to :priority, :class_name => 'IssuePriority'
34
  belongs_to :category, :class_name => 'IssueCategory'
35

    
36
  has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized
37
  has_many :time_entries, :dependent => :destroy
38
  has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
39

    
40
  has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
41
  has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
42

    
43
  acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
44
  acts_as_customizable
45
  acts_as_watchable
46
  acts_as_searchable :columns => ['subject', "#{table_name}.description"],
47
                     :preload => [:project, :status, :tracker],
48
                     :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
49

    
50
  acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
51
                :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
52
                :type => Proc.new {|o| 'issue' + (o.closed? ? '-closed' : '')}
53

    
54
  acts_as_activity_provider :scope => proc {preload(:project, :author, :tracker, :status)},
55
                            :author_key => :author_id
56

    
57
  acts_as_mentionable :attributes => ['description']
58

    
59
  DONE_RATIO_OPTIONS = %w(issue_field issue_status)
60

    
61
  attr_reader :transition_warning
62
  attr_writer :deleted_attachment_ids
63
  delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
64

    
65
  validates_presence_of :subject, :project, :tracker
66
  validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
67
  validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
68
  validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
69

    
70
  validates_length_of :subject, :maximum => 255
71
  validates_inclusion_of :done_ratio, :in => 0..100
72
  validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
73
  validates :start_date, :date => true
74
  validates :due_date, :date => true
75
  validate :validate_issue, :validate_required_fields, :validate_permissions
76

    
77
  scope :visible, (lambda do |*args|
78
    joins(:project).
79
    where(Issue.visible_condition(args.shift || User.current, *args))
80
  end)
81

    
82
  scope :open, (lambda do |*args|
83
    is_closed = !args.empty? ? !args.first : false
84
    joins(:status).
85
    where(:issue_statuses => {:is_closed => is_closed})
86
  end)
87

    
88
  scope :recently_updated, lambda {order(:updated_on => :desc)}
89
  scope :on_active_project, (lambda do
90
    joins(:project).
91
    where(:projects => {:status => Project::STATUS_ACTIVE})
92
  end)
93
  scope :fixed_version, (lambda do |versions|
94
    ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
95
    ids.any? ? where(:fixed_version_id => ids) : none
96
  end)
97
  scope :assigned_to, (lambda do |arg|
98
    arg = Array(arg).uniq
99
    ids = arg.map {|p| p.is_a?(Principal) ? p.id : p}
100
    ids += arg.select {|p| p.is_a?(User)}.map(&:group_ids).flatten.uniq
101
    ids.compact!
102
    ids.any? ? where(:assigned_to_id => ids) : none
103
  end)
104
  scope :like, (lambda do |q|
105
    if q.present?
106
      where(*::Query.tokenized_like_conditions("#{table_name}.subject", q))
107
    end
108
  end)
109

    
110
  before_validation :default_assign, on: :create
111
  before_validation :clear_disabled_fields
112
  before_save :close_duplicates, :update_done_ratio_from_issue_status,
113
              :force_updated_on_change, :update_closed_on
114
  after_save do |issue|
115
    if !issue.saved_change_to_id? && issue.saved_change_to_project_id?
116
      issue.send :after_project_change
117
    end
118
  end
119
  after_save :reschedule_following_issues, :update_nested_set_attributes,
120
             :update_parent_attributes, :delete_selected_attachments, :create_journal
121
  # Should be after_create but would be called before previous after_save callbacks
122
  after_save :after_create_from_copy
123
  after_destroy :update_parent_attributes
124
  # add_auto_watcher needs to run before sending notifications, thus it needs
125
  # to be added after send_notification (after_ callbacks are run in inverse order)
126
  # https://api.rubyonrails.org/v5.2.3/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-set_callback
127
  after_create_commit :send_notification
128
  after_create_commit :add_auto_watcher
129
  after_commit :create_parent_issue_journal
130

    
131
  # Returns a SQL conditions string used to find all issues visible by the specified user
132
  def self.visible_condition(user, options={})
133
    Project.allowed_to_condition(user, :view_issues, options) do |role, user|
134
      sql =
135
        if user.id && user.logged?
136
          case role.issues_visibility
137
          when 'all'
138
            '1=1'
139
          when 'default'
140
            user_ids = [user.id] + user.groups.pluck(:id).compact
141
            "(#{table_name}.is_private = #{connection.quoted_false} " \
142
              "OR #{table_name}.author_id = #{user.id} " \
143
              "OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
144
          when 'own'
145
            user_ids = [user.id] + user.groups.pluck(:id).compact
146
            "(#{table_name}.author_id = #{user.id} OR " \
147
              "#{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
148
          else
149
            '1=0'
150
          end
151
        else
152
          "(#{table_name}.is_private = #{connection.quoted_false})"
153
        end
154
      unless role.permissions_all_trackers?(:view_issues)
155
        tracker_ids = role.permissions_tracker_ids(:view_issues)
156
        if tracker_ids.any?
157
          sql = "(#{sql} AND #{table_name}.tracker_id IN (#{tracker_ids.join(',')}))"
158
        else
159
          sql = '1=0'
160
        end
161
      end
162
      sql
163
    end
164
  end
165

    
166
  # Returns true if usr or current user is allowed to view the issue
167
  def visible?(usr=nil)
168
    (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
169
      visible =
170
        if user.logged?
171
          case role.issues_visibility
172
          when 'all'
173
            true
174
          when 'default'
175
            !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
176
          when 'own'
177
            self.author == user || user.is_or_belongs_to?(assigned_to)
178
          else
179
            false
180
          end
181
        else
182
          !self.is_private?
183
        end
184
      unless role.permissions_all_trackers?(:view_issues)
185
        visible &&= role.permissions_tracker_ids?(:view_issues, tracker_id)
186
      end
187
      visible
188
    end
189
  end
190

    
191
  # Returns true if user or current user is allowed to edit or add notes to the issue
192
  def editable?(user=User.current)
193
    attributes_editable?(user) || notes_addable?(user)
194
  end
195

    
196
  # Returns true if user or current user is allowed to edit the issue
197
  def attributes_editable?(user=User.current)
198
    user_tracker_permission?(user, :edit_issues) || (
199
      user_tracker_permission?(user, :edit_own_issues) && author == user
200
    )
201
  end
202

    
203
  def attachments_addable?(user=User.current)
204
    attributes_editable?(user) || notes_addable?(user)
205
  end
206

    
207
  # Overrides Redmine::Acts::Attachable::InstanceMethods#attachments_editable?
208
  def attachments_editable?(user=User.current)
209
    attributes_editable?(user)
210
  end
211

    
212
  # Returns true if user or current user is allowed to add notes to the issue
213
  def notes_addable?(user=User.current)
214
    user_tracker_permission?(user, :add_issue_notes)
215
  end
216

    
217
  # Returns true if user or current user is allowed to delete the issue
218
  def deletable?(user=User.current)
219
    user_tracker_permission?(user, :delete_issues)
220
  end
221

    
222
  # Overrides Redmine::Acts::Attachable::InstanceMethods#attachments_deletable?
223
  def attachments_deletable?(user=User.current)
224
    attributes_editable?(user)
225
  end
226

    
227
  def initialize(attributes=nil, *args)
228
    super
229
    if new_record?
230
      # set default values for new records only
231
      self.priority ||= IssuePriority.default
232
      self.watcher_user_ids = []
233
    end
234
  end
235

    
236
  def create_or_update(*args)
237
    super()
238
  ensure
239
    @status_was = nil
240
  end
241
  private :create_or_update
242

    
243
  # AR#Persistence#destroy would raise and RecordNotFound exception
244
  # if the issue was already deleted or updated (non matching lock_version).
245
  # This is a problem when bulk deleting issues or deleting a project
246
  # (because an issue may already be deleted if its parent was deleted
247
  # first).
248
  # The issue is reloaded by the nested_set before being deleted so
249
  # the lock_version condition should not be an issue but we handle it.
250
  def destroy
251
    super
252
  rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound
253
    # Stale or already deleted
254
    begin
255
      reload
256
    rescue ActiveRecord::RecordNotFound
257
      # The issue was actually already deleted
258
      @destroyed = true
259
      return freeze
260
    end
261
    # The issue was stale, retry to destroy
262
    super
263
  end
264

    
265
  alias :base_reload :reload
266
  def reload(*args)
267
    @workflow_rule_by_attribute = nil
268
    @assignable_versions = nil
269
    @relations = nil
270
    @spent_hours = nil
271
    @total_spent_hours = nil
272
    @total_estimated_hours = nil
273
    @last_updated_by = nil
274
    @last_notes = nil
275
    base_reload(*args)
276
  end
277

    
278
  # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
279
  def available_custom_fields
280
    (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
281
  end
282

    
283
  def visible_custom_field_values(user=nil)
284
    user_real = user || User.current
285
    custom_field_values.select do |value|
286
      value.custom_field.visible_by?(project, user_real)
287
    end
288
  end
289

    
290
  # Overrides Redmine::Acts::Customizable::InstanceMethods#set_custom_field_default?
291
  def set_custom_field_default?(custom_value)
292
    new_record? || project_id_changed?|| tracker_id_changed?
293
  end
294

    
295
  # Copies attributes from another issue, arg can be an id or an Issue
296
  def copy_from(arg, options={})
297
    issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
298
    self.attributes =
299
      issue.attributes.dup.except(
300
        "id", "root_id", "parent_id", "lft", "rgt",
301
        "created_on", "updated_on", "status_id", "closed_on"
302
      )
303
    self.custom_field_values =
304
      issue.custom_field_values.inject({}) do |h, v|
305
        h[v.custom_field_id] = v.value
306
        h
307
      end
308
    if options[:keep_status]
309
      self.status = issue.status
310
    end
311
    self.author = User.current
312
    unless options[:attachments] == false
313
      self.attachments = issue.attachments.map do |attachement|
314
        attachement.copy(:container => self)
315
      end
316
    end
317
    unless options[:watchers] == false
318
      self.watcher_user_ids =
319
        issue.watcher_users.select{|u| u.status == User::STATUS_ACTIVE}.map(&:id)
320
    end
321
    @copied_from = issue
322
    @copy_options = options
323
    self
324
  end
325

    
326
  # Returns an unsaved copy of the issue
327
  def copy(attributes=nil, copy_options={})
328
    copy = self.class.new.copy_from(self, copy_options)
329
    copy.attributes = attributes if attributes
330
    copy
331
  end
332

    
333
  # Returns true if the issue is a copy
334
  def copy?
335
    @copied_from.present?
336
  end
337

    
338
  def status_id=(status_id)
339
    if status_id.to_s != self.status_id.to_s
340
      self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
341
    end
342
    self.status_id
343
  end
344

    
345
  # Sets the status.
346
  def status=(status)
347
    if status != self.status
348
      @workflow_rule_by_attribute = nil
349
    end
350
    association(:status).writer(status)
351
  end
352

    
353
  def priority_id=(pid)
354
    self.priority = nil
355
    write_attribute(:priority_id, pid)
356
  end
357

    
358
  def category_id=(cid)
359
    self.category = nil
360
    write_attribute(:category_id, cid)
361
  end
362

    
363
  def fixed_version_id=(vid)
364
    self.fixed_version = nil
365
    write_attribute(:fixed_version_id, vid)
366
  end
367

    
368
  def tracker_id=(tracker_id)
369
    if tracker_id.to_s != self.tracker_id.to_s
370
      self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
371
    end
372
    self.tracker_id
373
  end
374

    
375
  # Sets the tracker.
376
  # This will set the status to the default status of the new tracker if:
377
  # * the status was the default for the previous tracker
378
  # * or if the status was not part of the new tracker statuses
379
  # * or the status was nil
380
  def tracker=(tracker)
381
    tracker_was = self.tracker
382
    association(:tracker).writer(tracker)
383
    if tracker != tracker_was
384
      if status == tracker_was.try(:default_status)
385
        self.status = nil
386
      elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
387
        self.status = nil
388
      end
389
      reassign_custom_field_values
390
      @workflow_rule_by_attribute = nil
391
    end
392
    self.status ||= default_status
393
    self.tracker
394
  end
395

    
396
  def project_id=(project_id)
397
    if project_id.to_s != self.project_id.to_s
398
      self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
399
    end
400
    self.project_id
401
  end
402

    
403
  # Sets the project.
404
  # Unless keep_tracker argument is set to true, this will change the tracker
405
  # to the first tracker of the new project if the previous tracker is not part
406
  # of the new project trackers.
407
  # This will:
408
  # * clear the fixed_version is it's no longer valid for the new project.
409
  # * clear the parent issue if it's no longer valid for the new project.
410
  # * set the category to the category with the same name in the new
411
  #   project if it exists, or clear it if it doesn't.
412
  # * for new issue, set the fixed_version to the project default version
413
  #   if it's a valid fixed_version.
414
  def project=(project, keep_tracker=false)
415
    project_was = self.project
416
    association(:project).writer(project)
417
    if project != project_was
418
      @safe_attribute_names = nil
419
    end
420
    if project_was && project && project_was != project
421
      @assignable_versions = nil
422

    
423
      unless keep_tracker || project.trackers.include?(tracker)
424
        self.tracker = project.trackers.first
425
      end
426
      # Reassign to the category with same name if any
427
      if category
428
        self.category = project.issue_categories.find_by_name(category.name)
429
      end
430
      # Clear the assignee if not available in the new project for new issues (eg. copy)
431
      # For existing issue, the previous assignee is still valid, so we keep it
432
      if new_record? && assigned_to && !assignable_users.include?(assigned_to)
433
        self.assigned_to_id = nil
434
      end
435
      # Keep the fixed_version if it's still valid in the new_project
436
      if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
437
        self.fixed_version = nil
438
      end
439
      # Clear the parent task if it's no longer valid
440
      unless valid_parent_project?
441
        self.parent_issue_id = nil
442
      end
443
      reassign_custom_field_values
444
      @workflow_rule_by_attribute = nil
445
    end
446
    # Set fixed_version to the project default version if it's valid
447
    if new_record? && fixed_version.nil? && project && project.default_version_id?
448
      if project.shared_versions.open.exists?(project.default_version_id)
449
        self.fixed_version_id = project.default_version_id
450
      end
451
    end
452
    self.project
453
  end
454

    
455
  def description=(arg)
456
    if arg.is_a?(String)
457
      arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
458
    end
459
    write_attribute(:description, arg)
460
  end
461

    
462
  def deleted_attachment_ids
463
    Array(@deleted_attachment_ids).map(&:to_i)
464
  end
465

    
466
  # Overrides assign_attributes so that project and tracker get assigned first
467
  def assign_attributes(new_attributes, *args)
468
    return if new_attributes.nil?
469

    
470
    attrs = new_attributes.dup
471
    attrs.stringify_keys!
472

    
473
    %w(project project_id tracker tracker_id).each do |attr|
474
      if attrs.has_key?(attr)
475
        send "#{attr}=", attrs.delete(attr)
476
      end
477
    end
478
    super attrs, *args
479
  end
480

    
481
  def attributes=(new_attributes)
482
    assign_attributes new_attributes
483
  end
484

    
485
  def estimated_hours=(h)
486
    write_attribute :estimated_hours, (h.is_a?(String) ? (h.to_hours || h) : h)
487
  end
488

    
489
  safe_attributes(
490
    'project_id',
491
    'tracker_id',
492
    'status_id',
493
    'category_id',
494
    'assigned_to_id',
495
    'priority_id',
496
    'fixed_version_id',
497
    'subject',
498
    'description',
499
    'start_date',
500
    'due_date',
501
    'done_ratio',
502
    'estimated_hours',
503
    'custom_field_values',
504
    'custom_fields',
505
    'lock_version',
506
    :if => lambda {|issue, user| issue.new_record? || issue.attributes_editable?(user)})
507
  safe_attributes(
508
    'notes',
509
    :if => lambda {|issue, user| issue.notes_addable?(user)})
510
  safe_attributes(
511
    'private_notes',
512
    :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)})
513
  safe_attributes(
514
    'watcher_user_ids',
515
    :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)})
516
  safe_attributes(
517
    'is_private',
518
    :if => lambda do |issue, user|
519
      user.allowed_to?(:set_issues_private, issue.project) ||
520
        (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
521
    end)
522
  safe_attributes(
523
    'parent_issue_id',
524
    :if => lambda do |issue, user|
525
      (issue.new_record? || issue.attributes_editable?(user)) &&
526
        user.allowed_to?(:manage_subtasks, issue.project)
527
    end)
528
  safe_attributes(
529
    'deleted_attachment_ids',
530
    :if => lambda {|issue, user| issue.attachments_deletable?(user)})
531

    
532
  def safe_attribute_names(user=nil)
533
    names = super
534
    names -= disabled_core_fields
535
    names -= read_only_attribute_names(user)
536
    if new_record?
537
      # Make sure that project_id can always be set for new issues
538
      names |= %w(project_id)
539
    end
540
    if dates_derived?
541
      names -= %w(start_date due_date)
542
    end
543
    if priority_derived?
544
      names -= %w(priority_id)
545
    end
546
    if done_ratio_derived?
547
      names -= %w(done_ratio)
548
    end
549
    names
550
  end
551

    
552
  # Safely sets attributes
553
  # Should be called from controllers instead of #attributes=
554
  # attr_accessible is too rough because we still want things like
555
  # Issue.new(:project => foo) to work
556
  def safe_attributes=(attrs, user=User.current)
557
    if attrs.respond_to?(:to_unsafe_hash)
558
      attrs = attrs.to_unsafe_hash
559
    end
560

    
561
    @attributes_set_by = user
562
    return unless attrs.is_a?(Hash)
563

    
564
    attrs = attrs.deep_dup
565

    
566
    # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
567
    if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
568
      if p.is_a?(String) && !/^\d*$/.match?(p)
569
        p_id = Project.find_by_identifier(p).try(:id)
570
      else
571
        p_id = p.to_i
572
      end
573
      if allowed_target_projects(user).where(:id => p_id).exists?
574
        self.project_id = p_id
575
      end
576

    
577
      if project_id_changed? && attrs['category_id'].present? && attrs['category_id'].to_s == category_id_was.to_s
578
        # Discard submitted category on previous project
579
        attrs.delete('category_id')
580
      end
581
    end
582

    
583
    if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
584
      if allowed_target_trackers(user).where(:id => t.to_i).exists?
585
        self.tracker_id = t
586
      end
587
    end
588
    if project && tracker.nil?
589
      # Set a default tracker to accept custom field values
590
      # even if tracker is not specified
591
      allowed_trackers = allowed_target_trackers(user)
592

    
593
      if attrs['parent_issue_id'].present?
594
        # If parent_issue_id is present, the first tracker for which this field
595
        # is not disabled is chosen as default
596
        self.tracker = allowed_trackers.detect {|t| t.core_fields.include?('parent_issue_id')}
597
      end
598
      self.tracker ||= allowed_trackers.first
599
    end
600

    
601
    statuses_allowed = new_statuses_allowed_to(user)
602
    if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
603
      if statuses_allowed.collect(&:id).include?(s.to_i)
604
        self.status_id = s
605
      end
606
    end
607
    if new_record? && !statuses_allowed.include?(status)
608
      self.status = statuses_allowed.first || default_status
609
    end
610
    if (u = attrs.delete('assigned_to_id')) && safe_attribute?('assigned_to_id')
611
      self.assigned_to_id = u
612
    end
613
    attrs = delete_unsafe_attributes(attrs, user)
614
    return if attrs.empty?
615

    
616
    if attrs['parent_issue_id'].present?
617
      s = attrs['parent_issue_id'].to_s
618
      unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
619
        @invalid_parent_issue_id = attrs.delete('parent_issue_id')
620
      end
621
    end
622

    
623
    if attrs['custom_field_values'].present?
624
      editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
625
      attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
626
    end
627

    
628
    if attrs['custom_fields'].present?
629
      editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
630
      attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
631
    end
632

    
633
    assign_attributes attrs
634
  end
635

    
636
  def disabled_core_fields
637
    tracker ? tracker.disabled_core_fields : []
638
  end
639

    
640
  # Returns the custom_field_values that can be edited by the given user
641
  def editable_custom_field_values(user=nil)
642
    read_only = read_only_attribute_names(user)
643
    visible_custom_field_values(user).reject do |value|
644
      read_only.include?(value.custom_field_id.to_s)
645
    end
646
  end
647

    
648
  # Returns the custom fields that can be edited by the given user
649
  def editable_custom_fields(user=nil)
650
    editable_custom_field_values(user).map(&:custom_field).uniq
651
  end
652

    
653
  # Returns the names of attributes that are read-only for user or the current user
654
  # For users with multiple roles, the read-only fields are the intersection of
655
  # read-only fields of each role
656
  # The result is an array of strings where sustom fields are represented with their ids
657
  #
658
  # Examples:
659
  #   issue.read_only_attribute_names # => ['due_date', '2']
660
  #   issue.read_only_attribute_names(user) # => []
661
  def read_only_attribute_names(user=nil)
662
    workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
663
  end
664

    
665
  # Returns the names of required attributes for user or the current user
666
  # For users with multiple roles, the required fields are the intersection of
667
  # required fields of each role
668
  # The result is an array of strings where sustom fields are represented with their ids
669
  #
670
  # Examples:
671
  #   issue.required_attribute_names # => ['due_date', '2']
672
  #   issue.required_attribute_names(user) # => []
673
  def required_attribute_names(user=nil)
674
    workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
675
  end
676

    
677
  # Returns true if the attribute is required for user
678
  def required_attribute?(name, user=nil)
679
    required_attribute_names(user).include?(name.to_s)
680
  end
681

    
682
  # Returns a hash of the workflow rule by attribute for the given user
683
  #
684
  # Examples:
685
  #   issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
686
  def workflow_rule_by_attribute(user=nil)
687
    return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
688

    
689
    roles = roles_for_workflow(user || User.current)
690
    return {} if roles.empty?
691

    
692
    result = {}
693
    workflow_permissions =
694
      WorkflowPermission.where(
695
        :tracker_id => tracker_id, :old_status_id => status_id,
696
        :role_id => roles.map(&:id)
697
      ).to_a
698
    if workflow_permissions.any?
699
      workflow_rules = workflow_permissions.inject({}) do |h, wp|
700
        h[wp.field_name] ||= {}
701
        h[wp.field_name][wp.role_id] = wp.rule
702
        h
703
      end
704
      fields_with_roles = {}
705
      IssueCustomField.where(:visible => false).
706
        joins(:roles).pluck(:id, "role_id").
707
          each do |field_id, role_id|
708
        fields_with_roles[field_id] ||= []
709
        fields_with_roles[field_id] << role_id
710
      end
711
      roles.each do |role|
712
        fields_with_roles.each do |field_id, role_ids|
713
          unless role_ids.include?(role.id)
714
            field_name = field_id.to_s
715
            workflow_rules[field_name] ||= {}
716
            workflow_rules[field_name][role.id] = 'readonly'
717
          end
718
        end
719
      end
720
      workflow_rules.each do |attr, rules|
721
        next if rules.size < roles.size
722

    
723
        uniq_rules = rules.values.uniq
724
        if uniq_rules.size == 1
725
          result[attr] = uniq_rules.first
726
        else
727
          result[attr] = 'required'
728
        end
729
      end
730
    end
731
    @workflow_rule_by_attribute = result if user.nil?
732
    result
733
  end
734
  private :workflow_rule_by_attribute
735

    
736
  def done_ratio
737
    if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
738
      status.default_done_ratio
739
    else
740
      read_attribute(:done_ratio)
741
    end
742
  end
743

    
744
  def self.use_status_for_done_ratio?
745
    Setting.issue_done_ratio == 'issue_status'
746
  end
747

    
748
  def self.use_field_for_done_ratio?
749
    Setting.issue_done_ratio == 'issue_field'
750
  end
751

    
752
  def validate_issue
753
    if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
754
      errors.add :due_date, :greater_than_start_date
755
    end
756

    
757
    if start_date && start_date_changed? && soonest_start && start_date < soonest_start
758
      errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
759
    end
760

    
761
    if project && fixed_version_id
762
      if fixed_version.nil? || assignable_versions.exclude?(fixed_version)
763
        errors.add :fixed_version_id, :inclusion
764
      elsif reopening? && fixed_version.closed?
765
        errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
766
      end
767
    end
768

    
769
    if project && category_id
770
      unless project.issue_category_ids.include?(category_id)
771
        errors.add :category_id, :inclusion
772
      end
773
    end
774

    
775
    # Checks that the issue can not be added/moved to a disabled tracker
776
    if project && (tracker_id_changed? || project_id_changed?)
777
      if tracker && !project.trackers.include?(tracker)
778
        errors.add :tracker_id, :inclusion
779
      end
780
    end
781

    
782
    if project && assigned_to_id_changed? && assigned_to_id.present?
783
      unless assignable_users.include?(assigned_to)
784
        errors.add :assigned_to_id, :invalid
785
      end
786
    end
787

    
788
    # Checks parent issue assignment
789
    if @invalid_parent_issue_id.present?
790
      errors.add :parent_issue_id, :invalid
791
    elsif @parent_issue
792
      if !valid_parent_project?(@parent_issue)
793
        errors.add :parent_issue_id, :invalid
794
      elsif (@parent_issue != parent) && (
795
          self.would_reschedule?(@parent_issue) ||
796
          @parent_issue.self_and_ancestors.any? do |a|
797
            a.relations_from.any? do |r|
798
              r.relation_type == IssueRelation::TYPE_PRECEDES &&
799
                 r.issue_to.would_reschedule?(self)
800
            end
801
          end
802
        )
803
        errors.add :parent_issue_id, :invalid
804
      elsif !closed? && @parent_issue.closed?
805
        # cannot attach an open issue to a closed parent
806
        errors.add :base, :open_issue_with_closed_parent
807
      elsif !new_record?
808
        # moving an existing issue
809
        if move_possible?(@parent_issue)
810
          # move accepted
811
        else
812
          errors.add :parent_issue_id, :invalid
813
        end
814
      end
815
    end
816
  end
817

    
818
  # Validates the issue against additional workflow requirements
819
  def validate_required_fields
820
    user = new_record? ? author : current_journal.try(:user)
821

    
822
    required_attribute_names(user).each do |attribute|
823
      if /^\d+$/.match?(attribute)
824
        attribute = attribute.to_i
825
        v = custom_field_values.detect {|v| v.custom_field_id == attribute}
826
        if v && Array(v.value).detect(&:present?).nil?
827
          errors.add(v.custom_field.name, l('activerecord.errors.messages.blank'))
828
        end
829
      else
830
        if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
831
          next if attribute == 'category_id' && project.try(:issue_categories).blank?
832
          next if attribute == 'fixed_version_id' && assignable_versions.blank?
833

    
834
          errors.add attribute, :blank
835
        end
836
      end
837
    end
838
  end
839

    
840
  def validate_permissions
841
    if @attributes_set_by && new_record? && copy?
842
      unless allowed_target_trackers(@attributes_set_by).include?(tracker)
843
        errors.add :tracker, :invalid
844
      end
845
    end
846
  end
847

    
848
  # Overrides Redmine::Acts::Customizable::InstanceMethods#validate_custom_field_values
849
  # so that custom values that are not editable are not validated (eg. a custom field that
850
  # is marked as required should not trigger a validation error if the user is not allowed
851
  # to edit this field).
852
  def validate_custom_field_values
853
    user = new_record? ? author : current_journal.try(:user)
854
    if new_record? || custom_field_values_changed?
855
      editable_custom_field_values(user).each(&:validate_value)
856
    end
857
  end
858

    
859
  # Set the done_ratio using the status if that setting is set.  This will keep the done_ratios
860
  # even if the user turns off the setting later
861
  def update_done_ratio_from_issue_status
862
    if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
863
      self.done_ratio = status.default_done_ratio
864
    end
865
  end
866

    
867
  def init_journal(user, notes = "")
868
    @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
869
  end
870

    
871
  # Returns the current journal or nil if it's not initialized
872
  def current_journal
873
    @current_journal
874
  end
875

    
876
  # Clears the current journal
877
  def clear_journal
878
    @current_journal = nil
879
  end
880

    
881
  # Returns the names of attributes that are journalized when updating the issue
882
  def journalized_attribute_names
883
    names = Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
884
    if tracker
885
      names -= tracker.disabled_core_fields
886
    end
887
    names
888
  end
889

    
890
  # Returns the id of the last journal or nil
891
  def last_journal_id
892
    if new_record?
893
      nil
894
    else
895
      journals.maximum(:id)
896
    end
897
  end
898

    
899
  # Returns a scope for journals that have an id greater than journal_id
900
  def journals_after(journal_id)
901
    scope = journals.reorder("#{Journal.table_name}.id ASC")
902
    if journal_id.present?
903
      scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
904
    end
905
    scope
906
  end
907

    
908
  # Returns the journals that are visible to user with their index
909
  # Used to display the issue history
910
  def visible_journals_with_index(user=User.current)
911
    result = journals.
912
      preload(:details).
913
      preload(:user => :email_address).
914
      reorder(:created_on, :id).to_a
915

    
916
    result.each_with_index {|j, i| j.indice = i + 1}
917

    
918
    unless user.allowed_to?(:view_private_notes, project)
919
      result.select! do |journal|
920
        !journal.private_notes? || journal.user == user
921
      end
922
    end
923
    Journal.preload_journals_details_custom_fields(result)
924
    result.select! {|journal| journal.notes? || journal.visible_details.any?}
925
    result
926
  end
927

    
928
  # Returns the initial status of the issue
929
  # Returns nil for a new issue
930
  def status_was
931
    if status_id_changed?
932
      if status_id_was.to_i > 0
933
        @status_was ||= IssueStatus.find_by_id(status_id_was)
934
      end
935
    else
936
      @status_was ||= status
937
    end
938
  end
939

    
940
  # Return true if the issue is closed, otherwise false
941
  def closed?
942
    status.present? && status.is_closed?
943
  end
944

    
945
  # Returns true if the issue was closed when loaded
946
  def was_closed?
947
    status_was.present? && status_was.is_closed?
948
  end
949

    
950
  # Return true if the issue is being reopened
951
  def reopening?
952
    if new_record?
953
      false
954
    else
955
      status_id_changed? && !closed? && was_closed?
956
    end
957
  end
958
  alias :reopened? :reopening?
959

    
960
  # Return true if the issue is being closed
961
  def closing?
962
    if new_record?
963
      closed?
964
    else
965
      status_id_changed? && closed? && !was_closed?
966
    end
967
  end
968

    
969
  # Returns true if the issue is overdue
970
  def overdue?
971
    due_date.present? && (due_date < User.current.today) && !closed?
972
  end
973

    
974
  # Is the amount of work done less than it should for the due date
975
  def behind_schedule?
976
    return false if start_date.nil? || due_date.nil?
977

    
978
    done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
979
    return done_date <= User.current.today
980
  end
981

    
982
  # Does this issue have children?
983
  def children?
984
    !leaf?
985
  end
986

    
987
  # Users the issue can be assigned to
988
  def assignable_users
989
    return [] if project.nil?
990

    
991
    users = project.assignable_users(tracker).to_a
992
    users << author if author && author.active?
993
    if assigned_to_id_was.present? && assignee = Principal.find_by_id(assigned_to_id_was)
994
      users << assignee
995
    end
996
    users.uniq.sort
997
  end
998

    
999
  # Versions that the issue can be assigned to
1000
  def assignable_versions
1001
    return @assignable_versions if @assignable_versions
1002
    return [] if project.nil?
1003

    
1004
    versions = project.shared_versions.open.to_a
1005
    if fixed_version
1006
      if fixed_version_id_changed?
1007
        # nothing to do
1008
      elsif project_id_changed?
1009
        if project.shared_versions.include?(fixed_version)
1010
          versions << fixed_version
1011
        end
1012
      else
1013
        versions << fixed_version
1014
      end
1015
    end
1016
    @assignable_versions = versions.uniq.sort
1017
  end
1018

    
1019
  # Returns true if this issue is blocked by another issue that is still open
1020
  def blocked?
1021
    !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
1022
  end
1023

    
1024
  # Returns true if this issue can be closed and if not, returns false and populates the reason
1025
  def closable?
1026
    if descendants.open.any?
1027
      @transition_warning = l(:notice_issue_not_closable_by_open_tasks)
1028
      return false
1029
    end
1030
    if blocked?
1031
      @transition_warning = l(:notice_issue_not_closable_by_blocking_issue)
1032
      return false
1033
    end
1034
    return true
1035
  end
1036

    
1037
  # Returns true if this issue can be reopen and if not, returns false and populates the reason
1038
  def reopenable?
1039
    if ancestors.open(false).any?
1040
      @transition_warning = l(:notice_issue_not_reopenable_by_closed_parent_issue)
1041
      return false
1042
    end
1043
    return true
1044
  end
1045

    
1046
  # Returns the default status of the issue based on its tracker
1047
  # Returns nil if tracker is nil
1048
  def default_status
1049
    tracker.try(:default_status)
1050
  end
1051

    
1052
  # Returns an array of statuses that user is able to apply
1053
  def new_statuses_allowed_to(user=User.current, include_default=false)
1054
    initial_status = nil
1055
    if new_record?
1056
      # nop
1057
    elsif tracker_id_changed?
1058
      if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
1059
        initial_status = default_status
1060
      elsif tracker.issue_status_ids.include?(status_id_was)
1061
        initial_status = IssueStatus.find_by_id(status_id_was)
1062
      else
1063
        initial_status = default_status
1064
      end
1065
    else
1066
      initial_status = status_was
1067
    end
1068

    
1069
    initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
1070
    assignee_transitions_allowed = initial_assigned_to_id.present? &&
1071
      (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
1072

    
1073
    statuses = []
1074
    statuses += IssueStatus.new_statuses_allowed(
1075
      initial_status,
1076
      roles_for_workflow(user),
1077
      tracker,
1078
      author == user,
1079
      assignee_transitions_allowed
1080
    )
1081
    statuses << initial_status unless statuses.empty?
1082
    statuses << default_status if include_default || (new_record? && statuses.empty?)
1083

    
1084
    statuses = statuses.compact.uniq.sort
1085
    unless closable?
1086
      # cannot close a blocked issue or a parent with open subtasks
1087
      statuses.reject!(&:is_closed?)
1088
    end
1089
    unless reopenable?
1090
      # cannot reopen a subtask of a closed parent
1091
      statuses.select!(&:is_closed?)
1092
    end
1093
    statuses
1094
  end
1095

    
1096
  # Returns the original tracker
1097
  def tracker_was
1098
    Tracker.find_by_id(tracker_id_in_database)
1099
  end
1100

    
1101
  # Returns the previous assignee whenever we're before the save
1102
  # or in after_* callbacks
1103
  def previous_assignee
1104
    previous_assigned_to_id =
1105
      if assigned_to_id_change_to_be_saved.nil?
1106
        assigned_to_id_before_last_save
1107
      else
1108
        assigned_to_id_in_database
1109
      end
1110
    if previous_assigned_to_id
1111
      Principal.find_by_id(previous_assigned_to_id)
1112
    end
1113
  end
1114

    
1115
  # Returns the users that should be notified
1116
  def notified_users
1117
    # Author and assignee are always notified unless they have been
1118
    # locked or don't want to be notified
1119
    notified = [author, assigned_to, previous_assignee].compact.uniq
1120
    notified = notified.map {|n| n.is_a?(Group) ? n.users : n}.flatten
1121
    notified.uniq!
1122
    notified = notified.select {|u| u.active? && u.notify_about?(self)}
1123

    
1124
    notified += project.notified_users
1125
    notified += project.users.preload(:preference).select(&:notify_about_high_priority_issues?) if priority.high?
1126
    notified.uniq!
1127
    # Remove users that can not view the issue
1128
    notified.reject! {|user| !visible?(user)}
1129
    notified
1130
  end
1131

    
1132
  # Returns the email addresses that should be notified
1133
  def recipients
1134
    notified_users.collect(&:mail)
1135
  end
1136

    
1137
  def notify?
1138
    @notify != false
1139
  end
1140

    
1141
  def notify=(arg)
1142
    @notify = arg
1143
  end
1144

    
1145
  # Returns the number of hours spent on this issue
1146
  def spent_hours
1147
    @spent_hours ||= time_entries.sum(:hours) || 0.0
1148
  end
1149

    
1150
  # Returns the total number of hours spent on this issue and its descendants
1151
  def total_spent_hours
1152
    @total_spent_hours ||=
1153
      if leaf?
1154
        spent_hours
1155
      else
1156
        self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
1157
      end
1158
  end
1159

    
1160
  def total_estimated_hours
1161
    if leaf?
1162
      estimated_hours
1163
    else
1164
      @total_estimated_hours ||= self_and_descendants.visible.sum(:estimated_hours)
1165
    end
1166
  end
1167

    
1168
  def relations
1169
    @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
1170
  end
1171

    
1172
  def last_updated_by
1173
    if @last_updated_by
1174
      @last_updated_by.presence
1175
    else
1176
      journals.reorder(:id => :desc).first.try(:user)
1177
    end
1178
  end
1179

    
1180
  def last_notes
1181
    if @last_notes
1182
      @last_notes
1183
    else
1184
      journals.visible.where.not(notes: '').reorder(:id => :desc).first.try(:notes)
1185
    end
1186
  end
1187

    
1188
  # Preloads relations for a collection of issues
1189
  def self.load_relations(issues)
1190
    if issues.any?
1191
      relations =
1192
        IssueRelation.where(
1193
          "issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)
1194
        ).all
1195
      issues.each do |issue|
1196
        issue.instance_variable_set(
1197
          :@relations,
1198
          relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
1199
        )
1200
      end
1201
    end
1202
  end
1203

    
1204
  # Preloads visible spent time for a collection of issues
1205
  def self.load_visible_spent_hours(issues, user=User.current)
1206
    if issues.any?
1207
      hours_by_issue_id = TimeEntry.visible(user).where(:issue_id => issues.map(&:id)).group(:issue_id).sum(:hours)
1208
      issues.each do |issue|
1209
        issue.instance_variable_set :@spent_hours, (hours_by_issue_id[issue.id] || 0.0)
1210
      end
1211
    end
1212
  end
1213

    
1214
  # Preloads visible total spent time for a collection of issues
1215
  def self.load_visible_total_spent_hours(issues, user=User.current)
1216
    if issues.any?
1217
      hours_by_issue_id = TimeEntry.visible(user).joins(:issue).
1218
        joins("JOIN #{Issue.table_name} parent ON parent.root_id = #{Issue.table_name}.root_id" +
1219
          " AND parent.lft <= #{Issue.table_name}.lft AND parent.rgt >= #{Issue.table_name}.rgt").
1220
        where("parent.id IN (?)", issues.map(&:id)).group("parent.id").sum(:hours)
1221
      issues.each do |issue|
1222
        issue.instance_variable_set :@total_spent_hours, (hours_by_issue_id[issue.id] || 0.0)
1223
      end
1224
    end
1225
  end
1226

    
1227
  # Preloads visible relations for a collection of issues
1228
  def self.load_visible_relations(issues, user=User.current)
1229
    if issues.any?
1230
      issue_ids = issues.map(&:id)
1231
      # Relations with issue_from in given issues and visible issue_to
1232
      relations_from = IssueRelation.joins(:issue_to => :project).
1233
                         where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
1234
      # Relations with issue_to in given issues and visible issue_from
1235
      relations_to = IssueRelation.joins(:issue_from => :project).
1236
                         where(visible_condition(user)).
1237
                         where(:issue_to_id => issue_ids).to_a
1238
      issues.each do |issue|
1239
        relations =
1240
          relations_from.select {|relation| relation.issue_from_id == issue.id} +
1241
          relations_to.select {|relation| relation.issue_to_id == issue.id}
1242

    
1243
        issue.instance_variable_set :@relations, IssueRelation::Relations.new(issue, relations.sort)
1244
      end
1245
    end
1246
  end
1247

    
1248
  # Returns a scope of the given issues and their descendants
1249
  def self.self_and_descendants(issues)
1250
    Issue.joins(
1251
      "JOIN #{Issue.table_name} ancestors" +
1252
      " ON ancestors.root_id = #{Issue.table_name}.root_id" +
1253
      " AND ancestors.lft <= #{Issue.table_name}.lft AND ancestors.rgt >= #{Issue.table_name}.rgt"
1254
    ).
1255
      where(:ancestors => {:id => issues.map(&:id)})
1256
  end
1257

    
1258
  # Preloads users who updated last a collection of issues
1259
  def self.load_visible_last_updated_by(issues, user=User.current)
1260
    if issues.any?
1261
      issue_ids = issues.map(&:id)
1262
      journal_ids = Journal.joins(issue: :project).
1263
        where(:journalized_type => 'Issue', :journalized_id => issue_ids).
1264
        where(Journal.visible_notes_condition(user, :skip_pre_condition => true)).
1265
        group(:journalized_id).
1266
        maximum(:id).
1267
        values
1268
      journals = Journal.where(:id => journal_ids).preload(:user).to_a
1269

    
1270
      issues.each do |issue|
1271
        journal = journals.detect {|j| j.journalized_id == issue.id}
1272
        issue.instance_variable_set(:@last_updated_by, journal.try(:user) || '')
1273
      end
1274
    end
1275
  end
1276

    
1277
  # Preloads visible last notes for a collection of issues
1278
  def self.load_visible_last_notes(issues, user=User.current)
1279
    if issues.any?
1280
      issue_ids = issues.map(&:id)
1281
      journal_ids = Journal.joins(issue: :project).
1282
        where(:journalized_type => 'Issue', :journalized_id => issue_ids).
1283
        where(Journal.visible_notes_condition(user, :skip_pre_condition => true)).
1284
        where.not(notes: '').
1285
        group(:journalized_id).
1286
        maximum(:id).
1287
        values
1288
      journals = Journal.where(:id => journal_ids).to_a
1289

    
1290
      issues.each do |issue|
1291
        journal = journals.detect {|j| j.journalized_id == issue.id}
1292
        issue.instance_variable_set(:@last_notes, journal.try(:notes) || '')
1293
      end
1294
    end
1295
  end
1296

    
1297
  # Finds an issue relation given its id.
1298
  def find_relation(relation_id)
1299
    IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
1300
  end
1301

    
1302
  # Returns true if this issue blocks the other issue, otherwise returns false
1303
  def blocks?(other)
1304
    all = [self]
1305
    last = [self]
1306
    while last.any?
1307
      current =
1308
        last.map do |i|
1309
          i.relations_from.where(:relation_type => IssueRelation::TYPE_BLOCKS).map(&:issue_to)
1310
        end.flatten.uniq
1311
      current -= last
1312
      current -= all
1313
      return true if current.include?(other)
1314

    
1315
      last = current
1316
      all += last
1317
    end
1318
    false
1319
  end
1320

    
1321
  # Returns true if the other issue might be rescheduled if the start/due dates of this issue change
1322
  def would_reschedule?(other)
1323
    all = [self]
1324
    last = [self]
1325
    while last.any?
1326
      current = last.map do |i|
1327
        i.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to) +
1328
        i.leaves.to_a +
1329
        i.ancestors.map {|a| a.relations_from.where(:relation_type => IssueRelation::TYPE_PRECEDES).map(&:issue_to)}
1330
      end.flatten.uniq
1331
      current -= last
1332
      current -= all
1333
      return true if current.include?(other)
1334

    
1335
      last = current
1336
      all += last
1337
    end
1338
    false
1339
  end
1340

    
1341
  # Returns an array of issues that duplicate this one
1342
  def duplicates
1343
    relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1344
  end
1345

    
1346
  # Returns the due date or the target due date if any
1347
  # Used on gantt chart
1348
  def due_before
1349
    due_date || (fixed_version ? fixed_version.effective_date : nil)
1350
  end
1351

    
1352
  # Returns the time scheduled for this issue.
1353
  #
1354
  # Example:
1355
  #   Start Date: 2/26/09, End Date: 3/04/09
1356
  #   duration => 6
1357
  def duration
1358
    (start_date && due_date) ? due_date - start_date : 0
1359
  end
1360

    
1361
  # Returns the duration in working days
1362
  def working_duration
1363
    (start_date && due_date) ? working_days(start_date, due_date) : 0
1364
  end
1365

    
1366
  def soonest_start(reload=false)
1367
    if @soonest_start.nil? || reload
1368
      relations_to.reload if reload
1369
      dates = relations_to.collect{|relation| relation.successor_soonest_start}
1370
      p = @parent_issue || parent
1371
      if p && Setting.parent_issue_dates == 'derived'
1372
        dates << p.soonest_start
1373
      end
1374
      @soonest_start = dates.compact.max
1375
    end
1376
    @soonest_start
1377
  end
1378

    
1379
  # Sets start_date on the given date or the next working day
1380
  # and changes due_date to keep the same working duration.
1381
  def reschedule_on(date)
1382
    wd = working_duration
1383
    date = next_working_date(date)
1384
    self.start_date = date
1385
    self.due_date = add_working_days(date, wd)
1386
  end
1387

    
1388
  # Reschedules the issue on the given date or the next working day and saves the record.
1389
  # If the issue is a parent task, this is done by rescheduling its subtasks.
1390
  def reschedule_on!(date, journal=nil)
1391
    return if date.nil?
1392

    
1393
    if leaf? || !dates_derived?
1394
      if start_date.nil? || start_date != date
1395
        if start_date && start_date > date
1396
          # Issue can not be moved earlier than its soonest start date
1397
          date = [soonest_start(true), date].compact.max
1398
        end
1399
        if journal
1400
          init_journal(journal.user)
1401
        end
1402
        reschedule_on(date)
1403
        begin
1404
          save
1405
        rescue ActiveRecord::StaleObjectError
1406
          reload
1407
          reschedule_on(date)
1408
          save
1409
        end
1410
      end
1411
    else
1412
      leaves.each do |leaf|
1413
        if leaf.start_date
1414
          # Only move subtask if it starts at the same date as the parent
1415
          # or if it starts before the given date
1416
          if start_date == leaf.start_date || date > leaf.start_date
1417
            leaf.reschedule_on!(date)
1418
          end
1419
        else
1420
          leaf.reschedule_on!(date)
1421
        end
1422
      end
1423
    end
1424
  end
1425

    
1426
  def dates_derived?
1427
    !leaf? && Setting.parent_issue_dates == 'derived'
1428
  end
1429

    
1430
  def priority_derived?
1431
    !leaf? && Setting.parent_issue_priority == 'derived'
1432
  end
1433

    
1434
  def done_ratio_derived?
1435
    !leaf? && Setting.parent_issue_done_ratio == 'derived'
1436
  end
1437

    
1438
  def <=>(issue)
1439
    return nil unless issue.is_a?(Issue)
1440

    
1441
    if root_id != issue.root_id
1442
      (root_id || 0) <=> (issue.root_id || 0)
1443
    else
1444
      (lft || 0) <=> (issue.lft || 0)
1445
    end
1446
  end
1447

    
1448
  def to_s
1449
    "#{tracker} ##{id}: #{subject}"
1450
  end
1451

    
1452
  # Returns a string of css classes that apply to the issue
1453
  def css_classes(user=User.current)
1454
    s = +"issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1455
    s << ' closed' if closed?
1456
    s << ' overdue' if overdue?
1457
    s << ' child' if child?
1458
    s << ' parent' unless leaf?
1459
    s << ' private' if is_private?
1460
    s << ' behind-schedule' if behind_schedule?
1461
    if user.logged?
1462
      s << ' created-by-me' if author_id == user.id
1463
      s << ' assigned-to-me' if assigned_to_id == user.id
1464
      s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1465
    end
1466
    s
1467
  end
1468

    
1469
  # Unassigns issues from +version+ if it's no longer shared with issue's project
1470
  def self.update_versions_from_sharing_change(version)
1471
    # Update issues assigned to the version
1472
    update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1473
  end
1474

    
1475
  # Unassigns issues from versions that are no longer shared
1476
  # after +project+ was moved
1477
  def self.update_versions_from_hierarchy_change(project)
1478
    moved_project_ids = project.self_and_descendants.reload.pluck(:id)
1479
    # Update issues of the moved projects and issues assigned to a version of a moved project
1480
    Issue.
1481
      update_versions(
1482
        ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1483
         moved_project_ids, moved_project_ids]
1484
      )
1485
  end
1486

    
1487
  def parent_issue_id=(arg)
1488
    s = arg.to_s.strip.presence
1489
    if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1490
      @invalid_parent_issue_id = nil
1491
    elsif s.blank?
1492
      @parent_issue = nil
1493
      @invalid_parent_issue_id = nil
1494
    else
1495
      @parent_issue = nil
1496
      @invalid_parent_issue_id = arg
1497
    end
1498
  end
1499

    
1500
  def parent_issue_id
1501
    if @invalid_parent_issue_id
1502
      @invalid_parent_issue_id
1503
    elsif instance_variable_defined? :@parent_issue
1504
      @parent_issue.nil? ? nil : @parent_issue.id
1505
    else
1506
      parent_id
1507
    end
1508
  end
1509

    
1510
  alias :parent_issue :parent
1511

    
1512
  def set_parent_id
1513
    self.parent_id = parent_issue_id
1514
  end
1515

    
1516
  # Returns true if issue's project is a valid
1517
  # parent issue project
1518
  def valid_parent_project?(issue=parent)
1519
    return true if issue.nil? || issue.project_id == project_id
1520

    
1521
    case Setting.cross_project_subtasks
1522
    when 'system'
1523
      true
1524
    when 'tree'
1525
      issue.project.root == project.root
1526
    when 'hierarchy'
1527
      issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1528
    when 'descendants'
1529
      issue.project.is_or_is_ancestor_of?(project)
1530
    else
1531
      false
1532
    end
1533
  end
1534

    
1535
  # Returns an issue scope based on project and scope
1536
  def self.cross_project_scope(project, scope=nil)
1537
    if project.nil?
1538
      return Issue
1539
    end
1540

    
1541
    case scope
1542
    when 'all', 'system'
1543
      Issue
1544
    when 'tree'
1545
      Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1546
                                  :lft => project.root.lft, :rgt => project.root.rgt)
1547
    when 'hierarchy'
1548
      Issue.joins(:project).
1549
        where(
1550
          "(#{Project.table_name}.lft >= :lft AND " \
1551
            "#{Project.table_name}.rgt <= :rgt) OR " \
1552
            "(#{Project.table_name}.lft < :lft AND #{Project.table_name}.rgt > :rgt)",
1553
          :lft => project.lft, :rgt => project.rgt
1554
        )
1555
    when 'descendants'
1556
      Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1557
                                  :lft => project.lft, :rgt => project.rgt)
1558
    else
1559
      Issue.where(:project_id => project.id)
1560
    end
1561
  end
1562

    
1563
  def self.by_tracker(project, with_subprojects=false)
1564
    count_and_group_by(:project => project, :association => :tracker, :with_subprojects => with_subprojects)
1565
  end
1566

    
1567
  def self.by_version(project, with_subprojects=false)
1568
    count_and_group_by(:project => project, :association => :fixed_version, :with_subprojects => with_subprojects)
1569
  end
1570

    
1571
  def self.by_priority(project, with_subprojects=false)
1572
    count_and_group_by(:project => project, :association => :priority, :with_subprojects => with_subprojects)
1573
  end
1574

    
1575
  def self.by_category(project, with_subprojects=false)
1576
    count_and_group_by(:project => project, :association => :category, :with_subprojects => with_subprojects)
1577
  end
1578

    
1579
  def self.by_assigned_to(project, with_subprojects=false)
1580
    count_and_group_by(:project => project, :association => :assigned_to, :with_subprojects => with_subprojects)
1581
  end
1582

    
1583
  def self.by_author(project, with_subprojects=false)
1584
    count_and_group_by(:project => project, :association => :author, :with_subprojects => with_subprojects)
1585
  end
1586

    
1587
  def self.by_subproject(project)
1588
    r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1589
    r.reject {|r| r["project_id"] == project.id.to_s}
1590
  end
1591

    
1592
  # Query generator for selecting groups of issue counts for a project
1593
  # based on specific criteria
1594
  #
1595
  # Options
1596
  # * project - Project to search in.
1597
  # * with_subprojects - Includes subprojects issues if set to true.
1598
  # * association - Symbol. Association for grouping.
1599
  def self.count_and_group_by(options)
1600
    assoc = reflect_on_association(options[:association])
1601
    select_field = assoc.foreign_key
1602

    
1603
    Issue.
1604
      visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1605
      joins(:status).
1606
      group(:status_id, :is_closed, select_field).
1607
      count.
1608
      map do |columns, total|
1609
        status_id, is_closed, field_value = columns
1610
        is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1611
        {
1612
          "status_id" => status_id.to_s,
1613
          "closed" => is_closed,
1614
          select_field => field_value.to_s,
1615
          "total" => total.to_s
1616
        }
1617
      end
1618
  end
1619

    
1620
  # Returns a scope of projects that user can assign the subtask
1621
  def allowed_target_projects_for_subtask(user=User.current)
1622
    if parent_issue_id.present?
1623
      scope = filter_projects_scope(Setting.cross_project_subtasks)
1624
    end
1625

    
1626
    self.class.allowed_target_projects(user, project, scope)
1627
  end
1628

    
1629
  # Returns a scope of projects that user can assign the issue to
1630
  def allowed_target_projects(user=User.current, scope=nil)
1631
    current_project = new_record? ? nil : project
1632
    if scope
1633
      scope = filter_projects_scope(scope)
1634
    end
1635

    
1636
    self.class.allowed_target_projects(user, current_project, scope)
1637
  end
1638

    
1639
  # Returns a scope of projects that user can assign issues to
1640
  # If current_project is given, it will be included in the scope
1641
  def self.allowed_target_projects(user=User.current, current_project=nil, scope=nil)
1642
    condition = Project.allowed_to_condition(user, :add_issues)
1643
    if current_project
1644
      condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1645
    end
1646

    
1647
    if scope.nil?
1648
      scope = Project
1649
    end
1650

    
1651
    scope.where(condition).having_trackers
1652
  end
1653

    
1654
  # Returns a scope of trackers that user can assign the issue to
1655
  def allowed_target_trackers(user=User.current)
1656
    self.class.allowed_target_trackers(project, user, tracker_id_was)
1657
  end
1658

    
1659
  # Returns a scope of trackers that user can assign project issues to
1660
  def self.allowed_target_trackers(project, user=User.current, current_tracker=nil)
1661
    if project
1662
      scope = project.trackers.sorted
1663
      unless user.admin?
1664
        roles = user.roles_for_project(project).select {|r| r.has_permission?(:add_issues)}
1665
        unless roles.any? {|r| r.permissions_all_trackers?(:add_issues)}
1666
          tracker_ids = roles.map {|r| r.permissions_tracker_ids(:add_issues)}.flatten.uniq
1667
          if current_tracker
1668
            tracker_ids << current_tracker
1669
          end
1670
          scope = scope.where(:id => tracker_ids)
1671
        end
1672
      end
1673
      scope
1674
    else
1675
      Tracker.none
1676
    end
1677
  end
1678

    
1679
  # ALF
1680
  # Returns true if the element follows other (directly or indirectly)
1681
  def follows?(other)
1682
    other.relations_from.where(relation_type: 'precedes').each do |relation|
1683
	  if self.id == relation.issue_to.id
1684
	    return true
1685
      else
1686
		return self.follows?(relation.issue_to)
1687
	  end
1688
	end
1689
	false
1690
  end
1691

    
1692
  # Copies following tasks from the copied issue recursively (used by "after_create_from_copy" function)
1693
  # Since it has been copy/pasted from "after_create_from_copy" original definition
1694
  # Pieces of "after_create_from_copy" original code have been maintained and commented
1695
  def recursive_copy_followings
1696
    #return unless copy? && !@after_create_from_copy_handled_follows
1697
  	unless !copy? #&& @after_create_from_copy_handled_follows
1698

    
1699
      unless @copied_from.relations_from.where(relation_type: 'precedes').any? == false || @copy_options[:follows] == false
1700
        #copy_options = (@copy_options || {}).merge(:follows => false)
1701
        copied_issue_ids_flws = {@copied_from.id => self.id}
1702
  	    Issue.merge_static_copied_issue_ids(copied_issue_ids_flws)
1703
  	    #logger.info("ALF 1 after_create_from_copy_FOLLOWS copied_issue_ids_flws = #{copied_issue_ids_flws}")
1704
        @copied_from.relations_from.where(relation_type: 'precedes').each do |relation|
1705
          # Do not copy self when copying an issue as a descendant of the copied issue
1706
          #next if child == self
1707
          # Do not copy subtasks of issues that were not copied
1708
          #next unless copied_issue_ids[child.parent_id]
1709

    
1710
          # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1711
          #unless child.visible?
1712
          #  if logger
1713
          #    logger.error(
1714
          #      "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy " \
1715
          #        "because it is not visible to the current user"
1716
          #    )
1717
          #  end
1718
          #  next
1719
          #end
1720
  	  	if Issue.get_static_copied_issue_ids.has_key?(relation.issue_to.id)
1721
  	  	  copy = Issue.find(Issue.get_static_copied_issue_ids[relation.issue_to.id])
1722
  	  	  #logger.info("ALF FND after_create_from_copy_FOLLOWS Issue.get_static_copied_issue_ids = #{Issue.get_static_copied_issue_ids}")
1723
  	  	else
1724
          copy = Issue.new.copy_from(relation.issue_to, @copy_options)
1725
          #if @current_journal
1726
          #  copy.init_journal(@current_journal.user)
1727
          #end
1728
          #copy.author = author
1729
          #copy.project = project
1730
          #copy.parent_issue_id = copied_issue_ids[child.parent_id]
1731
          #unless child.fixed_version.present? && child.fixed_version.status == 'open'
1732
          #  copy.fixed_version_id = nil
1733
          #end
1734
          #unless child.assigned_to_id.present? &&
1735
          #         child.assigned_to.status == User::STATUS_ACTIVE
1736
          #  copy.assigned_to = nil
1737
          #end
1738
          unless copy.save
1739
            if logger
1740
              logger.error(
1741
                "Could not copy following task ##{relation.issue_to.id} " \
1742
                "while copying ##{@copied_from.id} to ##{id} due to validation errors: " \
1743
                "#{copy.errors.full_messages.join(', ')}"
1744
              )
1745
            end
1746
            next
1747
          end
1748
          copied_issue_ids_flws[relation.issue_to.id] = copy.id
1749
  	  	  Issue.merge_static_copied_issue_ids(copied_issue_ids_flws)
1750
  	  	  #logger.info("ALF 2 after_create_from_copy_FOLLOWS copied_issue_ids_flws = #{copied_issue_ids_flws}")
1751
        end
1752
  	    relation_follows =
1753
           IssueRelation.new(:issue_from => self, :issue_to => copy,
1754
                          :relation_type => relation.relation_type)
1755
  	    unless relation_follows.save
1756
          if logger
1757
            logger.error(
1758
              "Could not create relation while copying ##{self.id} to ##{id} " \
1759
                "due to validation errors: #{relation_follows.errors.full_messages.join(', ')}"
1760
            )
1761
          end
1762
        end
1763
		copy.recursive_copy_followings
1764
  	    end
1765
      end
1766
      #@after_create_from_copy_handled_follows = true
1767
	end
1768
  end
1769

    
1770
  # Copies following tasks from the copied issue recursively
1771
  # Since it has been copy/pasted from "after_create_from_copy" original definition
1772
  # Pieces of "after_create_from_copy" original code have been maintained and commented
1773
  def recursive_copy_followings_2(cpd_to, opt={})
1774
    #return unless copy? && !@after_create_from_copy_handled_follows
1775
  	#unless !copy? #&& @after_create_from_copy_handled_follows
1776

    
1777
      unless self.relations_from.where(relation_type: 'precedes').any? == false || opt[:follows] == false
1778
        #copy_options = (@copy_options || {}).merge(:follows => false)
1779
        copied_issue_ids_flws = {self.id => cpd_to.id}
1780
  	    #Issue.merge_static_copied_issue_ids(copied_issue_ids_flws)
1781
  	    #logger.info("ALF 1 after_create_from_copy_FOLLOWS copied_issue_ids_flws = #{copied_issue_ids_flws}")
1782
        self.relations_from.where(relation_type: 'precedes').each do |relation|
1783
          # Do not copy self when copying an issue as a descendant of the copied issue
1784
          #next if child == self
1785
          # Do not copy subtasks of issues that were not copied
1786
          #next unless copied_issue_ids[child.parent_id]
1787

    
1788
          # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1789
          #unless child.visible?
1790
          #  if logger
1791
          #    logger.error(
1792
          #      "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy " \
1793
          #        "because it is not visible to the current user"
1794
          #    )
1795
          #  end
1796
          #  next
1797
          #end
1798
  	  	if Issue.get_static_copied_issue_ids.has_key?(relation.issue_to.id)
1799
  	  	  copy = Issue.find(Issue.get_static_copied_issue_ids[relation.issue_to.id])
1800
  	  	  #logger.info("ALF FND after_create_from_copy_FOLLOWS Issue.get_static_copied_issue_ids = #{Issue.get_static_copied_issue_ids}")
1801
  	  	else
1802
          copy = Issue.new.copy_from(relation.issue_to, opt)
1803
          #if @current_journal
1804
          #  copy.init_journal(@current_journal.user)
1805
          #end
1806
          #copy.author = author
1807
          #copy.project = project
1808
          #copy.parent_issue_id = copied_issue_ids[child.parent_id]
1809
          #unless child.fixed_version.present? && child.fixed_version.status == 'open'
1810
          #  copy.fixed_version_id = nil
1811
          #end
1812
          #unless child.assigned_to_id.present? &&
1813
          #         child.assigned_to.status == User::STATUS_ACTIVE
1814
          #  copy.assigned_to = nil
1815
          #end
1816
          unless copy.save
1817
            if logger
1818
              logger.error(
1819
                "Could not copy following task ##{relation.issue_to.id} " \
1820
                "while copying ##{cpd_frm.id} to ##{id} due to validation errors: " \
1821
                "#{copy.errors.full_messages.join(', ')}"
1822
              )
1823
            end
1824
            next
1825
          end
1826
          copied_issue_ids_flws[relation.issue_to.id] = copy.id
1827
  	  	  Issue.merge_static_copied_issue_ids(copied_issue_ids_flws)
1828
  	  	  #logger.info("ALF 2 after_create_from_copy_FOLLOWS copied_issue_ids_flws = #{copied_issue_ids_flws}")
1829
        end
1830
  	    relation_follows =
1831
           IssueRelation.new(:issue_from => cpd_to, :issue_to => copy,
1832
                          :relation_type => relation.relation_type)
1833
  	    unless relation_follows.save
1834
          if logger
1835
            logger.error(
1836
              "Could not create relation while copying ##{cpd_to.id} to ##{id} " \
1837
                "due to validation errors: #{relation_follows.errors.full_messages.join(', ')}"
1838
            )
1839
          end
1840
        end
1841
	  	relation.issue_to.recursive_copy_followings_2(copy, opt)
1842
  	    end
1843
      end
1844
      #@after_create_from_copy_handled_follows = true
1845
	#end
1846
  end
1847
  # /ALF
1848

    
1849
  private
1850

    
1851
  def user_tracker_permission?(user, permission)
1852
    if project && !project.active?
1853
      perm = Redmine::AccessControl.permission(permission)
1854
      return false unless perm && perm.read?
1855
    end
1856

    
1857
    if user.admin?
1858
      true
1859
    else
1860
      roles = user.roles_for_project(project).select {|r| r.has_permission?(permission)}
1861
      roles.any? do |r|
1862
        r.permissions_all_trackers?(permission) ||
1863
          r.permissions_tracker_ids?(permission, tracker_id)
1864
      end
1865
    end
1866
  end
1867

    
1868
  def after_project_change
1869
    # Update project_id on related time entries
1870
    TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1871

    
1872
    # Delete issue relations
1873
    unless Setting.cross_project_issue_relations?
1874
      relations_from.clear
1875
      relations_to.clear
1876
    end
1877

    
1878
    # Move subtasks that were in the same project
1879
    children.each do |child|
1880
      next unless child.project_id == project_id_before_last_save
1881

    
1882
      # Change project and keep project
1883
      child.send :project=, project, true
1884
      unless child.save
1885
        errors.add(
1886
          :base,
1887
          l(:error_move_of_child_not_possible,
1888
            :child => "##{child.id}",
1889
            :errors => child.errors.full_messages.join(", "))
1890
        )
1891
        raise ActiveRecord::Rollback
1892
      end
1893
    end
1894
  end
1895

    
1896
  # ALF: Commented "after_create_from_copy" original definition
1897

    
1898
  # Callback for after the creation of an issue by copy
1899
  # * adds a "copied to" relation with the copied issue
1900
  # * copies subtasks from the copied issue
1901
  #def after_create_from_copy
1902
  #  return unless copy? && !@after_create_from_copy_handled
1903
  #
1904
  #  if (@copied_from.project_id == project_id ||
1905
  #        Setting.cross_project_issue_relations?) &&
1906
  #      @copy_options[:link] != false
1907
  #    if @current_journal
1908
  #      @copied_from.init_journal(@current_journal.user)
1909
  #    end
1910
  #    relation =
1911
  #      IssueRelation.new(:issue_from => @copied_from, :issue_to => self,
1912
  #                        :relation_type => IssueRelation::TYPE_COPIED_TO)
1913
  #    unless relation.save
1914
  #      if logger
1915
  #        logger.error(
1916
  #          "Could not create relation while copying ##{@copied_from.id} to ##{id} " \
1917
  #            "due to validation errors: #{relation.errors.full_messages.join(', ')}"
1918
  #        )
1919
  #      end
1920
  #    end
1921
  #  end
1922
  #
1923
  #  unless @copied_from.leaf? || @copy_options[:subtasks] == false
1924
  #    copy_options = (@copy_options || {}).merge(:subtasks => false)
1925
  #    copied_issue_ids = {@copied_from.id => self.id}
1926
  #    @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1927
  #      # Do not copy self when copying an issue as a descendant of the copied issue
1928
  #      next if child == self
1929
  #      # Do not copy subtasks of issues that were not copied
1930
  #      next unless copied_issue_ids[child.parent_id]
1931
  #
1932
  #      # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1933
  #      unless child.visible?
1934
  #        if logger
1935
  #          logger.error(
1936
  #            "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy " \
1937
  #              "because it is not visible to the current user"
1938
  #          )
1939
  #        end
1940
  #        next
1941
  #      end
1942
  #      copy = Issue.new.copy_from(child, copy_options)
1943
  #      if @current_journal
1944
  #        copy.init_journal(@current_journal.user)
1945
  #      end
1946
  #      copy.author = author
1947
  #      copy.project = project
1948
  #      copy.parent_issue_id = copied_issue_ids[child.parent_id]
1949
  #      unless child.fixed_version.present? && child.fixed_version.status == 'open'
1950
  #        copy.fixed_version_id = nil
1951
  #      end
1952
  #      unless child.assigned_to_id.present? &&
1953
  #               child.assigned_to.status == User::STATUS_ACTIVE
1954
  #        copy.assigned_to = nil
1955
  #      end
1956
  #      unless copy.save
1957
  #        if logger
1958
  #          logger.error(
1959
  #            "Could not copy subtask ##{child.id} " \
1960
  #              "while copying ##{@copied_from.id} to ##{id} due to validation errors: " \
1961
  #              "#{copy.errors.full_messages.join(', ')}"
1962
  #          )
1963
  #        end
1964
  #        next
1965
  #      end
1966
  #      copied_issue_ids[child.id] = copy.id
1967
  #    end
1968
  #  end
1969
  #  @after_create_from_copy_handled = true
1970
  #end
1971

    
1972
  # Modified callback for after the creation of an issue by copy
1973
  # * adds a "copied to" relation with the copied issue
1974
  # * copies subtasks from the copied issue
1975
  # Modification: if also following tasks shall be copied then
1976
  # checks if the child has already been copied, if so it picks it
1977
  # from the db instead of copying it again
1978
  def after_create_from_copy
1979
    return unless copy? && !@after_create_from_copy_handled
1980

    
1981
    if (@copied_from.project_id == project_id ||
1982
          Setting.cross_project_issue_relations?) &&
1983
        @copy_options[:link] != false
1984
      if @current_journal
1985
        @copied_from.init_journal(@current_journal.user)
1986
      end
1987
      relation =
1988
        IssueRelation.new(:issue_from => @copied_from, :issue_to => self,
1989
                          :relation_type => IssueRelation::TYPE_COPIED_TO)
1990
      unless relation.save
1991
        if logger
1992
          logger.error(
1993
            "Could not create relation while copying ##{@copied_from.id} to ##{id} " \
1994
              "due to validation errors: #{relation.errors.full_messages.join(', ')}"
1995
          )
1996
        end
1997
      end
1998
    end
1999

    
2000
    unless @copied_from.leaf? || @copy_options[:subtasks] == false
2001
      copy_options = (@copy_options || {}).merge(:subtasks => false)
2002
      copied_issue_ids = {@copied_from.id => self.id}
2003
	  # ALF
2004
	  if @copy_options[:follows] == true
2005
	    Issue.merge_static_copied_issue_ids(copied_issue_ids)
2006
		#logger.info("ALF 1 after_create_from_copy copied_issue_ids = #{copied_issue_ids}")
2007
	  end
2008
	  # /ALF
2009
      @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
2010
        # Do not copy self when copying an issue as a descendant of the copied issue
2011
        next if child == self
2012
        # Do not copy subtasks of issues that were not copied
2013
        next unless copied_issue_ids[child.parent_id]
2014

    
2015
        # Do not copy subtasks that are not visible to avoid potential disclosure of private data
2016
        unless child.visible?
2017
          if logger
2018
            logger.error(
2019
              "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy " \
2020
                "because it is not visible to the current user"
2021
            )
2022
          end
2023
          next
2024
        end
2025

    
2026
        copy = Issue.new.copy_from(child, copy_options)
2027

    
2028
        if @current_journal
2029
          copy.init_journal(@current_journal.user)
2030
        end
2031
        copy.author = author
2032
        copy.project = project
2033
        copy.parent_issue_id = copied_issue_ids[child.parent_id]
2034
        unless child.fixed_version.present? && child.fixed_version.status == 'open'
2035
          copy.fixed_version_id = nil
2036
        end
2037
        unless child.assigned_to_id.present? &&
2038
                 child.assigned_to.status == User::STATUS_ACTIVE
2039
          copy.assigned_to = nil
2040
        end
2041
        unless copy.save
2042
          if logger
2043
            logger.error(
2044
              "Could not copy subtask ##{child.id} " \
2045
                "while copying ##{@copied_from.id} to ##{id} due to validation errors: " \
2046
                "#{copy.errors.full_messages.join(', ')}"
2047
            )
2048
          end
2049
          next
2050
        end
2051
        copied_issue_ids[child.id] = copy.id
2052
	    # ALF
2053
		Issue.merge_static_copied_issue_ids(copied_issue_ids)
2054
	    if @copy_options[:follows] == true && !Issue.get_from_bulk_update
2055
		  #logger.info("ALF 2 after_create_from_copy copied_issue_ids = #{copied_issue_ids}")
2056
	      copy.recursive_copy_followings
2057
	    end
2058
	    # /ALF
2059
      end
2060
    end
2061
    @after_create_from_copy_handled = true
2062
  end
2063

    
2064
  def self.merge_static_copied_issue_ids(hash)
2065
    @@static_copied_issue_ids = @@static_copied_issue_ids.merge(hash)
2066
	#logger.info("ALF VALUE @@static_copied_issue_ids = #{@@static_copied_issue_ids}")
2067
  end
2068

    
2069
  def self.get_static_copied_issue_ids
2070
    @@static_copied_issue_ids || 0
2071
  end
2072

    
2073
  def self.init_static_copied_issue_ids
2074
    @@static_copied_issue_ids = {}
2075
	@@from_bulk_update = false
2076
	#logger.info("ALF INIT @@static_copied_issue_ids")
2077
  end
2078

    
2079
  def self.set_from_bulk_update(value)
2080
	@@from_bulk_update = value
2081
	#logger.info("ALF INIT @@static_copied_issue_ids")
2082
  end
2083

    
2084
  def self.get_from_bulk_update
2085
    @@from_bulk_update
2086
  end
2087
  # /ALF
2088

    
2089
  def update_nested_set_attributes
2090
    if saved_change_to_parent_id?
2091
      update_nested_set_attributes_on_parent_change
2092
    end
2093
    remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
2094
  end
2095

    
2096
  # Updates the nested set for when an existing issue is moved
2097
  def update_nested_set_attributes_on_parent_change
2098
    former_parent_id = parent_id_before_last_save
2099
    # delete invalid relations of all descendants
2100
    self_and_descendants.each do |issue|
2101
      issue.relations.each do |relation|
2102
        relation.destroy unless relation.valid?
2103
      end
2104
    end
2105
    # update former parent
2106
    recalculate_attributes_for(former_parent_id) if former_parent_id
2107
  end
2108

    
2109
  def update_parent_attributes
2110
    if parent_id
2111
      recalculate_attributes_for(parent_id)
2112
      association(:parent).reset
2113
    end
2114
  end
2115

    
2116
  def recalculate_attributes_for(issue_id)
2117
    if issue_id && p = Issue.find_by_id(issue_id)
2118
      if p.priority_derived?
2119
        # priority = highest priority of open children
2120
        # priority is left unchanged if all children are closed and there's no default priority defined
2121
        if priority_position =
2122
             p.children.open.joins(:priority).maximum("#{IssuePriority.table_name}.position")
2123
          p.priority = IssuePriority.find_by_position(priority_position)
2124
        elsif default_priority = IssuePriority.default
2125
          p.priority = default_priority
2126
        end
2127
      end
2128

    
2129
      if p.dates_derived?
2130
        # start/due dates = lowest/highest dates of children
2131
        p.start_date = p.children.minimum(:start_date)
2132
        p.due_date = p.children.maximum(:due_date)
2133
        if p.start_date && p.due_date && p.due_date < p.start_date
2134
          p.start_date, p.due_date = p.due_date, p.start_date
2135
        end
2136
      end
2137

    
2138
      if p.done_ratio_derived?
2139
        # done ratio = average ratio of children weighted with their total estimated hours
2140
        unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
2141
          children = p.children.to_a
2142
          if children.any?
2143
            child_with_total_estimated_hours = children.select {|c| c.total_estimated_hours.to_f > 0.0}
2144
            if child_with_total_estimated_hours.any?
2145
              average = Rational(
2146
                child_with_total_estimated_hours.sum(&:total_estimated_hours).to_s,
2147
                child_with_total_estimated_hours.count
2148
              )
2149
            else
2150
              average = Rational(1)
2151
            end
2152
            done = children.sum do |c|
2153
              estimated = Rational(c.total_estimated_hours.to_f.to_s)
2154
              estimated = average unless estimated > 0.0
2155
              ratio = c.closed? ? 100 : (c.done_ratio || 0)
2156
              estimated * ratio
2157
            end
2158
            progress = Rational(done, average * children.count)
2159
            p.done_ratio = progress.floor
2160
          end
2161
        end
2162
      end
2163

    
2164
      # ancestors will be recursively updated
2165
      p.save(:validate => false)
2166
    end
2167
  end
2168

    
2169
  # Singleton class method is public
2170
  class << self
2171
    # Update issues so their versions are not pointing to a
2172
    # fixed_version that is not shared with the issue's project
2173
    def update_versions(conditions=nil)
2174
      # Only need to update issues with a fixed_version from
2175
      # a different project and that is not systemwide shared
2176
      Issue.joins(:project, :fixed_version).
2177
        where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
2178
          " AND #{Issue.table_name}.project_id <> #{::Version.table_name}.project_id" +
2179
          " AND #{::Version.table_name}.sharing <> 'system'").
2180
        where(conditions).each do |issue|
2181
        next if issue.project.nil? || issue.fixed_version.nil?
2182

    
2183
        unless issue.project.shared_versions.include?(issue.fixed_version)
2184
          retried = false
2185
          begin
2186
            issue.init_journal(User.current)
2187
            issue.fixed_version = nil
2188
            issue.save
2189
          rescue ActiveRecord::StaleObjectError
2190
            raise if retried
2191

    
2192
            retried = true
2193
            issue.reload
2194
            retry
2195
          end
2196
        end
2197
      end
2198
    end
2199
  end
2200

    
2201
  def delete_selected_attachments
2202
    if deleted_attachment_ids.present?
2203
      objects = attachments.where(:id => deleted_attachment_ids.map(&:to_i))
2204
      attachments.delete(objects)
2205
    end
2206
  end
2207

    
2208
  # Callback on file attachment
2209
  def attachment_added(attachment)
2210
    if current_journal && !attachment.new_record? && !copy?
2211
      current_journal.journalize_attachment(attachment, :added)
2212
    end
2213
  end
2214

    
2215
  # Callback on attachment deletion
2216
  def attachment_removed(attachment)
2217
    if current_journal && !attachment.new_record?
2218
      current_journal.journalize_attachment(attachment, :removed)
2219
      current_journal.save
2220
    end
2221
  end
2222

    
2223
  # Called after a relation is added
2224
  def relation_added(relation)
2225
    if current_journal
2226
      current_journal.journalize_relation(relation, :added)
2227
      current_journal.save
2228
    end
2229
  end
2230

    
2231
  # Called after a relation is removed
2232
  def relation_removed(relation)
2233
    if current_journal
2234
      current_journal.journalize_relation(relation, :removed)
2235
      current_journal.save
2236
    end
2237
  end
2238

    
2239
  # Default assignment based on project or category
2240
  def default_assign
2241
    if assigned_to.nil?
2242
      if category && category.assigned_to
2243
        self.assigned_to = category.assigned_to
2244
      elsif project && project.default_assigned_to
2245
        self.assigned_to = project.default_assigned_to
2246
      end
2247
    end
2248
  end
2249

    
2250
  # Updates start/due dates of following issues
2251
  def reschedule_following_issues
2252
    if saved_change_to_start_date? || saved_change_to_due_date?
2253
      relations_from.each do |relation|
2254
        relation.set_issue_to_dates(@current_journal)
2255
      end
2256
    end
2257
  end
2258

    
2259
  # Closes duplicates if the issue is being closed
2260
  def close_duplicates
2261
    if Setting.close_duplicate_issues? && closing?
2262
      duplicates.each do |duplicate|
2263
        # Reload is needed in case the duplicate was updated by a previous duplicate
2264
        duplicate.reload
2265
        # Don't re-close it if it's already closed
2266
        next if duplicate.closed?
2267

    
2268
        # Same user and notes
2269
        if @current_journal
2270
          duplicate.init_journal(@current_journal.user, @current_journal.notes)
2271
          duplicate.private_notes = @current_journal.private_notes
2272
        end
2273
        duplicate.update_attribute :status, self.status
2274
      end
2275
    end
2276
  end
2277

    
2278
  # Make sure updated_on is updated when adding a note and set updated_on now
2279
  # so we can set closed_on with the same value on closing
2280
  def force_updated_on_change
2281
    if @current_journal || changed?
2282
      self.updated_on = current_time_from_proper_timezone
2283
      if new_record?
2284
        self.created_on = updated_on
2285
      end
2286
    end
2287
  end
2288

    
2289
  # Callback for setting closed_on when the issue is closed.
2290
  # The closed_on attribute stores the time of the last closing
2291
  # and is preserved when the issue is reopened.
2292
  def update_closed_on
2293
    if closing?
2294
      self.closed_on = updated_on
2295
    end
2296
  end
2297

    
2298
  # Saves the changes in a Journal
2299
  # Called after_save
2300
  def create_journal
2301
    if current_journal
2302
      current_journal.save
2303
    end
2304
  end
2305

    
2306
  def create_parent_issue_journal
2307
    return if persisted? && !saved_change_to_parent_id?
2308
    return if destroyed? && @without_nested_set_update
2309

    
2310
    child_id = self.id
2311
    old_parent_id, new_parent_id =
2312
      if persisted?
2313
        [parent_id_before_last_save, parent_id]
2314
      elsif destroyed?
2315
        [parent_id, nil]
2316
      else
2317
        [nil, parent_id]
2318
      end
2319

    
2320
    if old_parent_id.present?
2321
      Issue.transaction do
2322
        if old_parent_issue = Issue.visible.lock.find_by_id(old_parent_id)
2323
          old_parent_issue.init_journal(User.current)
2324
          old_parent_issue.current_journal.__send__(:add_attribute_detail, 'child_id', child_id, nil)
2325
          old_parent_issue.save
2326
        end
2327
      end
2328
    end
2329

    
2330
    if new_parent_id.present?
2331
      Issue.transaction do
2332
        if new_parent_issue = Issue.visible.lock.find_by_id(new_parent_id)
2333
          new_parent_issue.init_journal(User.current)
2334
          new_parent_issue.current_journal.__send__(:add_attribute_detail, 'child_id', nil, child_id)
2335
          new_parent_issue.save
2336
        end
2337
      end
2338
    end
2339
  end
2340

    
2341
  def add_auto_watcher
2342
    if author&.active? &&
2343
        author&.allowed_to?(:add_issue_watchers, project) &&
2344
        author.pref.auto_watch_on?('issue_created') &&
2345
        self.watcher_user_ids.exclude?(author.id)
2346
      self.set_watcher(author, true)
2347
    end
2348
  end
2349

    
2350
  def send_notification
2351
    if notify? && Setting.notified_events.include?('issue_added')
2352
      Mailer.deliver_issue_add(self)
2353
    end
2354
  end
2355

    
2356
  def clear_disabled_fields
2357
    if tracker
2358
      tracker.disabled_core_fields.each do |attribute|
2359
        send "#{attribute}=", nil
2360
      end
2361
      self.priority_id ||= IssuePriority.default&.id || IssuePriority.active.first.id
2362
      self.done_ratio ||= 0
2363
    end
2364
  end
2365

    
2366
  def filter_projects_scope(scope=nil)
2367
    case scope
2368
    when 'system'
2369
      Project
2370
    when 'tree'
2371
      project.root.self_and_descendants
2372
    when 'hierarchy'
2373
      project.hierarchy
2374
    when 'descendants'
2375
      project.self_and_descendants
2376
    when ''
2377
      Project.where(:id => project.id)
2378
    else
2379
      Project
2380
    end
2381
  end
2382

    
2383
  def roles_for_workflow(user)
2384
    roles = user.admin ? Role.all.to_a : user.roles_for_project(project)
2385
    roles.select(&:consider_workflow?)
2386
  end
2387
end
(5-5/5)