issue.rb 7.75 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
  include Spammable
10
  include FasterCacheKeys
11

Rémy Coutable's avatar
Rémy Coutable committed
12 13 14 15 16 17
  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
18

19 20
  ActsAsTaggableOn.strict_case_match = true

21
  belongs_to :project
22 23
  belongs_to :moved_to, class_name: 'Issue'

24 25
  has_many :events, as: :target, dependent: :destroy

26
  has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues'
27 28
  has_many :closed_by_merge_requests, through: :merge_requests_closing_issues, source: :merge_request

29 30
  validates :project, presence: true

Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
31
  scope :cared, ->(user) { where(assignee_id: user) }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
32
  scope :open_for, ->(user) { opened.assigned_to(user) }
33
  scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
34

35 36 37 38
  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) }

39 40 41
  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') }

42 43
  attr_spammable :title, spam_title: true
  attr_spammable :description, spam_description: true
44

Andrew8xx8's avatar
Andrew8xx8 committed
45
  state_machine :state, initial: :opened do
Andrew8xx8's avatar
Andrew8xx8 committed
46 47 48 49 50
    event :close do
      transition [:reopened, :opened] => :closed
    end

    event :reopen do
Andrew8xx8's avatar
Andrew8xx8 committed
51
      transition closed: :reopened
Andrew8xx8's avatar
Andrew8xx8 committed
52 53 54 55 56 57
    end

    state :opened
    state :reopened
    state :closed
  end
58

59 60 61 62
  def hook_attrs
    attributes
  end

63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
  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

90
  def self.visible_to_user(user)
91
    return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
92 93
    return all if user.admin?

94 95 96 97 98 99 100 101 102 103 104 105 106
    # 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

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

118 119 120 121
  def self.reference_prefix
    '#'
  end

122 123 124 125
  # Pattern used to extract `#123` issue references from text
  #
  # This pattern supports cross-project references.
  def self.reference_pattern
126
    @reference_pattern ||= %r{
127 128
      (#{Project.reference_pattern})?
      #{Regexp.escape(reference_prefix)}(?<issue>\d+)
129
    }x
Kirill Zaitsev's avatar
Kirill Zaitsev committed
130 131
  end

132
  def self.link_reference_pattern
133
    @link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
134 135
  end

136 137 138 139
  def self.reference_valid?(reference)
    reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
  end

140
  def self.sort(method, excluded_labels: [])
141 142
    case method.to_s
    when 'due_date_asc' then order_due_date_asc
143
    when 'due_date_desc' then order_due_date_desc
144 145 146 147 148
    else
      super
    end
  end

149 150 151 152 153 154 155 156 157 158
  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

159
  def referenced_merge_requests(current_user = nil)
Yorick Peterse's avatar
Yorick Peterse committed
160 161 162 163
    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
164
    end
Yorick Peterse's avatar
Yorick Peterse committed
165 166

    ext.merge_requests.sort_by(&:iid)
167 168
  end

169
  # All branches containing the current issue's ID, except for
170
  # those with a merge request open referencing the current issue.
171 172
  def related_branches(current_user)
    branches_with_iid = project.repository.branch_names.select do |branch|
173
      branch =~ /\A#{iid}-(?!\d+-stable)/i
174
    end
175 176 177 178

    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
179 180
  end

Drew Blessing's avatar
Drew Blessing committed
181 182 183 184 185 186 187 188 189
  # 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
190
    Event.reset_event_cache_for(self)
Drew Blessing's avatar
Drew Blessing committed
191
  end
192 193 194 195 196

  # To allow polymorphism with MergeRequest.
  def source_project
    project
  end
197 198 199

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

Yorick Peterse's avatar
Yorick Peterse committed
203 204 205 206 207 208
    ext = all_references(current_user)

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

209
    ext.merge_requests.select { |mr| mr.open? && mr.closes_issue?(self) }
210
  end
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
211

212 213 214 215 216 217 218 219 220
  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

221 222
    !moved? && persisted? &&
      user.can?(:admin_issue, self.project)
223
  end
224

Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
225
  def to_branch_name
226
    if self.confidential?
227
      "#{iid}-confidential-issue"
228
    else
229
      "#{iid}-#{title.parameterize}"
230
    end
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
231 232
  end

233
  def can_be_worked_on?(current_user)
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
234
    !self.closed? &&
235
      !self.project.forked? &&
236
      self.related_branches(current_user).empty? &&
237
      self.closed_by_merge_requests(current_user).empty?
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
238
  end
239

240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
  # Returns `true` if the current issue can be viewed by either a logged in User
  # or an anonymous user.
  def visible_to_user?(user = nil)
    user ? readable_by?(user) : publicly_visible?
  end

  # Returns `true` if the given User can read the current Issue.
  def readable_by?(user)
    if user.admin?
      true
    elsif project.owner == user
      true
    elsif confidential?
      author == user ||
        assignee == user ||
        project.team.member?(user, Gitlab::Access::REPORTER)
    else
      project.public? ||
        project.internal? && !user.external? ||
        project.team.member?(user)
    end
  end

  # Returns `true` if this Issue is visible to everybody.
  def publicly_visible?
    project.public? && !confidential?
  end

268
  def overdue?
Rémy Coutable's avatar
Rémy Coutable committed
269
    due_date.try(:past?) || false
270
  end
271

272
  # Only issues on public projects should be checked for spam
273
  def check_for_spam?
274
    project.public?
275
  end
gitlabhq's avatar
gitlabhq committed
276
end