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

G
gitlabhq 已提交
3
class Issue < ActiveRecord::Base
4
  include AtomicInternalId
5
  include Issuable
6
  include Noteable
7
  include Referable
8
  include Spammable
9
  include FasterCacheKeys
10
  include RelativePositioning
11
  include TimeTrackable
12
  include ThrottledTouch
13 14
  include IgnorableColumn

15
  ignore_column :assignee_id, :branch_name, :deleted_at
16

17 18 19 20 21 22 23
  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
  DueNextMonthAndPreviousTwoWeeks = DueDateStruct.new('Due Next Month And Previous Two Weeks', 'next_month_and_previous_two_weeks').freeze
24

I
Izaak Alpert 已提交
25
  belongs_to :project
26
  belongs_to :moved_to, class_name: 'Issue'
H
haseeb 已提交
27
  belongs_to :closed_by, class_name: 'User'
28

29
  has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) }
30

31
  has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
32

33 34 35 36 37
  has_many :merge_requests_closing_issues,
    class_name: 'MergeRequestsClosingIssues',
    dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent

  has_many :issue_assignees
38
  has_many :assignees, class_name: "User", through: :issue_assignees
39

I
Izaak Alpert 已提交
40 41
  validates :project, presence: true

42
  alias_attribute :parent_ids, :project_id
43

V
Valery Sizov 已提交
44
  scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
45

46 47 48 49
  scope :assigned, -> { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') }
  scope :unassigned, -> { where('NOT EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') }
  scope :assigned_to, ->(u) { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = ? AND issue_id = issues.id)', u.id)}

50
  scope :with_due_date, -> { where('due_date IS NOT NULL') }
51 52 53
  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) }
54
  scope :due_tomorrow, -> { where(due_date: Date.tomorrow) }
55

56 57
  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') }
58
  scope :order_closest_future_date, -> { reorder('CASE WHEN due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - due_date) ASC') }
59

F
Felipe Artur 已提交
60
  scope :preload_associations, -> { preload(:labels, project: :namespace) }
61

62 63
  scope :public_only, -> { where(confidential: false) }

R
Regis Boudinot 已提交
64 65
  after_save :expire_etag_cache

66 67
  attr_spammable :title, spam_title: true
  attr_spammable :description, spam_description: true
68

69 70
  participant :assignees

A
Andrew8xx8 已提交
71
  state_machine :state, initial: :opened do
A
Andrew8xx8 已提交
72
    event :close do
73
      transition [:opened] => :closed
A
Andrew8xx8 已提交
74 75 76
    end

    event :reopen do
77
      transition closed: :opened
A
Andrew8xx8 已提交
78 79 80 81
    end

    state :opened
    state :closed
F
Felipe Artur 已提交
82 83 84 85

    before_transition any => :closed do |issue|
      issue.closed_at = Time.zone.now
    end
H
haseeb 已提交
86 87 88 89 90

    before_transition closed: :opened do |issue|
      issue.closed_at = nil
      issue.closed_by = nil
    end
A
Andrew8xx8 已提交
91
  end
92

93 94 95 96
  class << self
    alias_method :in_parents, :in_projects
  end

97 98 99 100
  def self.reference_prefix
    '#'
  end

101 102 103 104
  # Pattern used to extract `#123` issue references from text
  #
  # This pattern supports cross-project references.
  def self.reference_pattern
105
    @reference_pattern ||= %r{
106 107
      (#{Project.reference_pattern})?
      #{Regexp.escape(reference_prefix)}(?<issue>\d+)
108
    }x
K
Kirill Zaitsev 已提交
109 110
  end

111
  def self.link_reference_pattern
112
    @link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
113 114
  end

115 116 117 118
  def self.reference_valid?(reference)
    reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
  end

119 120 121 122
  def self.project_foreign_key
    'project_id'
  end

123
  def self.sort_by_attribute(method, excluded_labels: [])
124
    case method.to_s
125
    when 'closest_future_date' then order_closest_future_date
126 127
    when 'due_date'      then order_due_date_asc
    when 'due_date_asc'  then order_due_date_asc
128
    when 'due_date_desc' then order_due_date_desc
129 130 131 132 133
    else
      super
    end
  end

134
  def self.order_by_position_and_priority
135 136
    order_labels_priority
      .reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC'),
137 138 139 140
              Gitlab::Database.nulls_last_order('highest_priority', 'ASC'),
              "id DESC")
  end

141
  def hook_attrs
142
    Gitlab::HookData::IssueBuilder.new(self).build
143 144
  end

145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
  # Returns a Hash of attributes to be used for Twitter card metadata
  def card_attributes
    {
      'Author'   => author.try(:name),
      'Assignee' => assignee_list
    }
  end

  def assignee_or_author?(user)
    author_id == user.id || assignees.exists?(user.id)
  end

  def assignee_list
    assignees.map(&:name).to_sentence
  end

161
  # `from` argument can be a Namespace or Project.
162
  def to_reference(from = nil, full: false)
163 164
    reference = "#{self.class.reference_prefix}#{iid}"

165
    "#{project.to_reference(from, full: full)}#{reference}"
166 167
  end

168
  def referenced_merge_requests(current_user = nil)
Y
Yorick Peterse 已提交
169 170 171 172
    ext = all_references(current_user)

    notes_with_associations.each do |object|
      object.all_references(current_user, extractor: ext)
Z
Zeger-Jan van de Weg 已提交
173
    end
Y
Yorick Peterse 已提交
174

175 176 177 178 179 180 181 182 183 184 185 186
    merge_requests = ext.merge_requests.sort_by(&:iid)

    cross_project_filter = -> (merge_requests) do
      merge_requests.select { |mr| mr.target_project == project }
    end

    Ability.merge_requests_readable_by_user(
      merge_requests, current_user,
      filters: {
        read_cross_project: cross_project_filter
      }
    )
187 188
  end

189
  # All branches containing the current issue's ID, except for
190
  # those with a merge request open referencing the current issue.
191 192
  def related_branches(current_user)
    branches_with_iid = project.repository.branch_names.select do |branch|
193
      branch =~ /\A#{iid}-(?!\d+-stable)/i
194
    end
195 196 197 198

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

    branches_with_iid - branches_with_merge_request
Z
Zeger-Jan van de Weg 已提交
199 200
  end

201 202 203
  def suggested_branch_name
    return to_branch_name unless project.repository.branch_exists?(to_branch_name)

204
    start_counting_from = 2
205
    Uniquify.new(start_counting_from).string(-> (counter) { "#{to_branch_name}-#{counter}" }) do |suggested_branch_name|
206 207
      project.repository.branch_exists?(suggested_branch_name)
    end
208 209
  end

210 211
  # Returns boolean if a related branch exists for the current issue
  # ignores merge requests branchs
212
  def has_related_branch?
213 214 215 216 217
    project.repository.branch_names.any? do |branch|
      /\A#{iid}-(?!\d+-stable)/i =~ branch
    end
  end

218 219 220 221
  # To allow polymorphism with MergeRequest.
  def source_project
    project
  end
222 223 224

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

Y
Yorick Peterse 已提交
228 229 230 231 232 233
    ext = all_references(current_user)

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

234 235 236 237 238 239 240
    merge_requests = ext.merge_requests.select(&:open?)
    if merge_requests.any?
      ids = MergeRequestsClosingIssues.where(merge_request_id: merge_requests.map(&:id), issue_id: id).pluck(:merge_request_id)
      merge_requests.select { |mr| mr.id.in?(ids) }
    else
      []
    end
241
  end
Z
Zeger-Jan van de Weg 已提交
242

243 244 245 246 247 248 249 250 251
  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

252 253
    !moved? && persisted? &&
      user.can?(:admin_issue, self.project)
254
  end
255

Z
Zeger-Jan van de Weg 已提交
256
  def to_branch_name
257
    if self.confidential?
258
      "#{iid}-confidential-issue"
259
    else
260
      "#{iid}-#{title.parameterize}"
261
    end
Z
Zeger-Jan van de Weg 已提交
262 263
  end

264 265
  def can_be_worked_on?
    !self.closed? && !self.project.forked?
Z
Zeger-Jan van de Weg 已提交
266
  end
267

268 269 270
  # 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)
271
    return false unless project && project.feature_available?(:issues, user)
272

273
    user ? readable_by?(user) : publicly_visible?
274 275
  end

276
  def overdue?
R
Rémy Coutable 已提交
277
    due_date.try(:past?) || false
278
  end
279 280

  def check_for_spam?
281
    project.public? && (title_changed? || description_changed?)
282
  end
P
Phil Hughes 已提交
283 284 285

  def as_json(options = {})
    super(options).tap do |json|
286
      if options.key?(:issue_endpoints) && project
287 288
        url_helper = Gitlab::Routing.url_helpers

289 290 291 292 293 294 295 296
        issue_reference = options[:include_full_project_path] ? to_reference(full: true) : to_reference

        json.merge!(
          reference_path: issue_reference,
          real_path: url_helper.project_issue_path(project, self),
          issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'),
          toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self)
        )
297
      end
298

299
      if options.key?(:labels)
300 301
        json[:labels] = labels.as_json(
          project: project,
302
          only: [:id, :title, :description, :color, :priority],
303 304 305
          methods: [:text_color]
        )
      end
P
Phil Hughes 已提交
306 307
    end
  end
308

D
Douwe Maan 已提交
309 310 311 312
  def discussions_rendered_on_frontend?
    true
  end

313 314 315 316
  def update_project_counter_caches
    Projects::OpenIssuesCountService.new(project).refresh_cache
  end

317 318
  private

319 320 321 322 323
  def ensure_metrics
    super
    metrics.record!
  end

324 325 326 327 328 329 330 331 332 333 334 335
  # Returns `true` if the given User can read the current Issue.
  #
  # This method duplicates the same check of issue_policy.rb
  # for performance reasons, check commit: 002ad215818450d2cbbc5fa065850a953dc7ada8
  # Make sure to sync this method with issue_policy.rb
  def readable_by?(user)
    if user.admin?
      true
    elsif project.owner == user
      true
    elsif confidential?
      author == user ||
336
        assignees.include?(user) ||
337 338 339 340 341 342 343 344 345 346 347 348
        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
R
Regis Boudinot 已提交
349 350

  def expire_etag_cache
351
    key = Gitlab::Routing.url_helpers.realtime_changes_project_issue_path(project, self)
R
Regis Boudinot 已提交
352 353
    Gitlab::EtagCaching::Store.new.touch(key)
  end
G
gitlabhq 已提交
354
end