issue.rb 5.54 KB
Newer Older
1 2 3 4
# == Schema Information
#
# Table name: issues
#
Stan Hu's avatar
Stan Hu committed
5 6 7 8 9 10 11 12 13 14 15 16 17 18
#  id            :integer          not null, primary key
#  title         :string(255)
#  assignee_id   :integer
#  author_id     :integer
#  project_id    :integer
#  created_at    :datetime
#  updated_at    :datetime
#  position      :integer          default(0)
#  branch_name   :string(255)
#  description   :text
#  milestone_id  :integer
#  state         :string(255)
#  iid           :integer
#  updated_by_id :integer
19
#  moved_to_id   :integer
20 21
#

22 23
require 'carrierwave/orm/activerecord'

gitlabhq's avatar
gitlabhq committed
24
class Issue < ActiveRecord::Base
25
  include InternalId
26 27
  include Issuable
  include Referable
28
  include Sortable
29
  include Taskable
30

Rémy Coutable's avatar
Rémy Coutable committed
31 32 33 34 35 36
  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
37

38 39
  ActsAsTaggableOn.strict_case_match = true

40
  belongs_to :project
41 42
  belongs_to :moved_to, class_name: 'Issue'

43 44
  validates :project, presence: true

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
45
  scope :cared, ->(user) { where(assignee_id: user) }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
46
  scope :open_for, ->(user) { opened.assigned_to(user) }
47
  scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
48

49 50 51 52
  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) }

53 54 55
  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's avatar
Andrew8xx8 committed
56
  state_machine :state, initial: :opened do
Andrew8xx8's avatar
Andrew8xx8 committed
57 58 59 60 61
    event :close do
      transition [:reopened, :opened] => :closed
    end

    event :reopen do
Andrew8xx8's avatar
Andrew8xx8 committed
62
      transition closed: :reopened
Andrew8xx8's avatar
Andrew8xx8 committed
63 64 65 66 67 68
    end

    state :opened
    state :reopened
    state :closed
  end
69

70 71 72 73
  def hook_attrs
    attributes
  end

74 75 76 77 78 79 80
  def self.visible_to_user(user)
    return where(confidential: false) if user.blank?
    return all if user.admin?

    where('issues.confidential = 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.select(:id))
  end

81 82 83 84
  def self.reference_prefix
    '#'
  end

85 86 87 88
  # Pattern used to extract `#123` issue references from text
  #
  # This pattern supports cross-project references.
  def self.reference_pattern
89
    @reference_pattern ||= %r{
90 91
      (#{Project.reference_pattern})?
      #{Regexp.escape(reference_prefix)}(?<issue>\d+)
92
    }x
Kirill Zaitsev's avatar
Kirill Zaitsev committed
93 94
  end

95
  def self.link_reference_pattern
96
    @link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
97 98
  end

99 100 101 102 103 104 105 106 107
  def self.sort(method)
    case method.to_s
    when 'due_date_asc' then order_due_date_asc
    when 'due_date_desc'  then order_due_date_desc
    else
      super
    end
  end

108 109 110 111 112 113 114 115 116 117
  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

118
  def referenced_merge_requests(current_user = nil)
119 120 121 122 123 124 125
    @referenced_merge_requests ||= {}
    @referenced_merge_requests[current_user] ||= begin
      Gitlab::ReferenceExtractor.lazily do
        [self, *notes].flat_map do |note|
          note.all_references(current_user).merge_requests
        end
      end.sort_by(&:iid).uniq
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
126
    end
127 128
  end

129
  # All branches containing the current issue's ID, except for
130
  # those with a merge request open referencing the current issue.
131 132
  def related_branches(current_user)
    branches_with_iid = project.repository.branch_names.select do |branch|
133
      branch =~ /\A#{iid}-(?!\d+-stable)/i
134
    end
135 136 137 138

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

    branches_with_iid - branches_with_merge_request
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
139 140
  end

Drew Blessing's avatar
Drew Blessing committed
141 142 143 144 145 146 147 148 149
  # 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
150
    Event.reset_event_cache_for(self)
Drew Blessing's avatar
Drew Blessing committed
151
  end
152 153 154 155 156

  # To allow polymorphism with MergeRequest.
  def source_project
    project
  end
157 158 159

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

163
    notes.system.flat_map do |note|
164 165
      note.all_references(current_user).merge_requests
    end.uniq.select { |mr| mr.open? && mr.closes_issue?(self) }
166
  end
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
167

168 169 170 171 172 173 174 175 176
  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

177 178
    !moved? && persisted? &&
      user.can?(:admin_issue, self.project)
179
  end
180

Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
181
  def to_branch_name
182
    if self.confidential?
183
      "#{iid}-confidential-issue"
184
    else
185
      "#{iid}-#{title.parameterize}"
186
    end
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
187 188
  end

189
  def can_be_worked_on?(current_user)
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
190
    !self.closed? &&
191
      !self.project.forked? &&
192
      self.related_branches(current_user).empty? &&
193
      self.closed_by_merge_requests(current_user).empty?
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
194
  end
195 196

  def overdue?
Rémy Coutable's avatar
Rémy Coutable committed
197
    due_date.try(:past?) || false
198
  end
gitlabhq's avatar
gitlabhq committed
199
end