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

gitlabhq's avatar
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's avatar
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's avatar
Andrey Kumanyaev committed
26
  scope :cared, ->(user) { where(assignee_id: user) }
Dmitriy Zaporozhets's avatar
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's avatar
Andrew8xx8 committed
37
  state_machine :state, initial: :opened do
Andrew8xx8's avatar
Andrew8xx8 committed
38 39 40 41 42
    event :close do
      transition [:reopened, :opened] => :closed
    end

    event :reopen do
Andrew8xx8's avatar
Andrew8xx8 committed
43
      transition closed: :reopened
Andrew8xx8's avatar
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
  class << self
    private

    # Returns the project that the current scope belongs to if any, nil otherwise.
    #
    # Examples:
    # - my_project.issues.without_due_date.owner_project => my_project
    # - Issue.all.owner_project => nil
    def owner_project
      # No owner if we're not being called from an association
      return unless all.respond_to?(:proxy_association)

      owner = all.proxy_association.owner

      # Check if the association is or belongs to a project
      if owner.is_a?(Project)
        owner
      else
        begin
          owner.association(:project).target
        rescue ActiveRecord::AssociationNotFoundError
          nil
        end
      end
    end
  end

82
  def self.visible_to_user(user)
83
    return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
84 85
    return all if user.admin?

86 87 88 89 90 91 92 93 94 95 96 97 98
    # Check if we are scoped to a specific project's issues
    if owner_project
      if owner_project.authorized_for_user?(user, Gitlab::Access::REPORTER)
        # If the project is authorized for the user, they can see all issues in the project
        return all
      else
        # else only non confidential and authored/assigned to them
        return where('issues.confidential IS NULL OR issues.confidential IS FALSE
          OR issues.author_id = :user_id OR issues.assignee_id = :user_id',
          user_id: user.id)
      end
    end

99 100 101 102 103 104 105 106 107
    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))
108 109
  end

110 111 112 113
  def self.reference_prefix
    '#'
  end

114 115 116 117
  # Pattern used to extract `#123` issue references from text
  #
  # This pattern supports cross-project references.
  def self.reference_pattern
118
    @reference_pattern ||= %r{
119 120
      (#{Project.reference_pattern})?
      #{Regexp.escape(reference_prefix)}(?<issue>\d+)
121
    }x
Kirill Zaitsev's avatar
Kirill Zaitsev committed
122 123
  end

124
  def self.link_reference_pattern
125
    @link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
126 127
  end

128 129 130 131
  def self.reference_valid?(reference)
    reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
  end

132
  def self.sort(method, excluded_labels: [])
133 134
    case method.to_s
    when 'due_date_asc' then order_due_date_asc
135
    when 'due_date_desc' then order_due_date_desc
136 137 138 139 140
    else
      super
    end
  end

141 142 143 144 145 146 147 148 149 150
  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

151
  def referenced_merge_requests(current_user = nil)
Yorick Peterse's avatar
Yorick Peterse committed
152 153 154 155
    ext = all_references(current_user)

    notes_with_associations.each do |object|
      object.all_references(current_user, extractor: ext)
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
156
    end
Yorick Peterse's avatar
Yorick Peterse committed
157 158

    ext.merge_requests.sort_by(&:iid)
159 160
  end

161
  # All branches containing the current issue's ID, except for
162
  # those with a merge request open referencing the current issue.
163 164
  def related_branches(current_user)
    branches_with_iid = project.repository.branch_names.select do |branch|
165
      branch =~ /\A#{iid}-(?!\d+-stable)/i
166
    end
167 168 169 170

    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
171 172
  end

Drew Blessing's avatar
Drew Blessing committed
173 174 175 176 177 178 179 180 181
  # 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
182
    Event.reset_event_cache_for(self)
Drew Blessing's avatar
Drew Blessing committed
183
  end
184 185 186 187 188

  # To allow polymorphism with MergeRequest.
  def source_project
    project
  end
189 190 191

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

Yorick Peterse's avatar
Yorick Peterse committed
195 196 197 198 199 200 201
    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) }
202
  end
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
203

204 205 206 207 208 209 210 211 212
  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

213 214
    !moved? && persisted? &&
      user.can?(:admin_issue, self.project)
215
  end
216

Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
217
  def to_branch_name
218
    if self.confidential?
219
      "#{iid}-confidential-issue"
220
    else
221
      "#{iid}-#{title.parameterize}"
222
    end
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
223 224
  end

225
  def can_be_worked_on?(current_user)
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
226
    !self.closed? &&
227
      !self.project.forked? &&
228
      self.related_branches(current_user).empty? &&
229
      self.closed_by_merge_requests(current_user).empty?
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
230
  end
231 232

  def overdue?
Rémy Coutable's avatar
Rémy Coutable committed
233
    due_date.try(:past?) || false
234
  end
gitlabhq's avatar
gitlabhq committed
235
end