BigW Consortium Gitlab

issue.rb 5.35 KB
Newer Older
1 2
require 'carrierwave/orm/activerecord'

gitlabhq committed
3
class Issue < ActiveRecord::Base
4
  include InternalId
5 6
  include Issuable
  include Referable
7
  include Sortable
8
  include Taskable
9

Rémy Coutable committed
10 11 12 13 14 15
  DueDateStruct = Struct.new(:title, :name).freeze
  NoDueDate     = DueDateStruct.new('No Due Date', '0').freeze
  AnyDueDate    = DueDateStruct.new('Any Due Date', '').freeze
  Overdue       = DueDateStruct.new('Overdue', 'overdue').freeze
  DueThisWeek   = DueDateStruct.new('Due This Week', 'week').freeze
  DueThisMonth  = DueDateStruct.new('Due This Month', 'month').freeze
16

17 18
  ActsAsTaggableOn.strict_case_match = true

19
  belongs_to :project
20 21
  belongs_to :moved_to, class_name: 'Issue'

22 23
  has_many :events, as: :target, dependent: :destroy

24 25
  validates :project, presence: true

Andrey Kumanyaev committed
26
  scope :cared, ->(user) { where(assignee_id: user) }
Dmitriy Zaporozhets committed
27
  scope :open_for, ->(user) { opened.assigned_to(user) }
28
  scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
29

30 31 32 33
  scope :without_due_date, -> { where(due_date: nil) }
  scope :due_before, ->(date) { where('issues.due_date < ?', date) }
  scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) }

34 35 36
  scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
  scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }

Andrew8xx8 committed
37
  state_machine :state, initial: :opened do
Andrew8xx8 committed
38 39 40 41 42
    event :close do
      transition [:reopened, :opened] => :closed
    end

    event :reopen do
Andrew8xx8 committed
43
      transition closed: :reopened
Andrew8xx8 committed
44 45 46 47 48 49
    end

    state :opened
    state :reopened
    state :closed
  end
50

51 52 53 54
  def hook_attrs
    attributes
  end

55
  def self.visible_to_user(user)
56
    return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
57 58
    return all if user.admin?

59 60 61 62 63 64 65 66 67
    where('
      issues.confidential IS NULL
      OR issues.confidential IS FALSE
      OR (issues.confidential = TRUE
        AND (issues.author_id = :user_id
          OR issues.assignee_id = :user_id
          OR issues.project_id IN(:project_ids)))',
      user_id: user.id,
      project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
68 69
  end

70 71 72 73
  def self.reference_prefix
    '#'
  end

74 75 76 77
  # Pattern used to extract `#123` issue references from text
  #
  # This pattern supports cross-project references.
  def self.reference_pattern
78
    @reference_pattern ||= %r{
79 80
      (#{Project.reference_pattern})?
      #{Regexp.escape(reference_prefix)}(?<issue>\d+)
81
    }x
Kirill Zaitsev committed
82 83
  end

84
  def self.link_reference_pattern
85
    @link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
86 87
  end

88 89 90 91
  def self.reference_valid?(reference)
    reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
  end

92
  def self.sort(method, excluded_labels: [])
93 94
    case method.to_s
    when 'due_date_asc' then order_due_date_asc
95
    when 'due_date_desc' then order_due_date_desc
96 97 98 99 100
    else
      super
    end
  end

101 102 103 104 105 106 107 108 109 110
  def to_reference(from_project = nil)
    reference = "#{self.class.reference_prefix}#{iid}"

    if cross_project_reference?(from_project)
      reference = project.to_reference + reference
    end

    reference
  end

111
  def referenced_merge_requests(current_user = nil)
Yorick Peterse committed
112 113 114 115
    ext = all_references(current_user)

    notes_with_associations.each do |object|
      object.all_references(current_user, extractor: ext)
116
    end
Yorick Peterse committed
117 118

    ext.merge_requests.sort_by(&:iid)
119 120
  end

121
  # All branches containing the current issue's ID, except for
122
  # those with a merge request open referencing the current issue.
123 124
  def related_branches(current_user)
    branches_with_iid = project.repository.branch_names.select do |branch|
125
      branch =~ /\A#{iid}-(?!\d+-stable)/i
126
    end
127 128 129 130

    branches_with_merge_request = self.referenced_merge_requests(current_user).map(&:source_branch)

    branches_with_iid - branches_with_merge_request
131 132
  end

133 134 135 136 137 138 139 140 141
  # Reset issue events cache
  #
  # Since we do cache @event we need to reset cache in special cases:
  # * when an issue is updated
  # Events cache stored like  events/23-20130109142513.
  # The cache key includes updated_at timestamp.
  # Thus it will automatically generate a new fragment
  # when the event is updated because the key changes.
  def reset_events_cache
142
    Event.reset_event_cache_for(self)
143
  end
144 145 146 147 148

  # To allow polymorphism with MergeRequest.
  def source_project
    project
  end
149 150 151

  # From all notes on this issue, we'll select the system notes about linked
  # merge requests. Of those, the MRs closing `self` are returned.
152 153 154
  def closed_by_merge_requests(current_user = nil)
    return [] unless open?

Yorick Peterse committed
155 156 157 158 159 160 161
    ext = all_references(current_user)

    notes.system.each do |note|
      note.all_references(current_user, extractor: ext)
    end

    ext.merge_requests.select { |mr| mr.open? && mr.closes_issue?(self) }
162
  end
163

164 165 166 167 168 169 170 171 172
  def moved?
    !moved_to.nil?
  end

  def can_move?(user, to_project = nil)
    if to_project
      return false unless user.can?(:admin_issue, to_project)
    end

173 174
    !moved? && persisted? &&
      user.can?(:admin_issue, self.project)
175
  end
176

177
  def to_branch_name
178
    if self.confidential?
179
      "#{iid}-confidential-issue"
180
    else
181
      "#{iid}-#{title.parameterize}"
182
    end
183 184
  end

185
  def can_be_worked_on?(current_user)
186
    !self.closed? &&
187
      !self.project.forked? &&
188
      self.related_branches(current_user).empty? &&
189
      self.closed_by_merge_requests(current_user).empty?
190
  end
191 192

  def overdue?
Rémy Coutable committed
193
    due_date.try(:past?) || false
194
  end
gitlabhq committed
195
end