BigW Consortium Gitlab

issue.rb 6.6 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 Spammable
9
  include FasterCacheKeys
10

Rémy Coutable committed
11 12 13 14 15 16
  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
17

18 19
  ActsAsTaggableOn.strict_case_match = true

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

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

25
  has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
26

27 28
  validates :project, presence: true

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

33 34 35 36
  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) }

37 38 39
  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') }

40 41
  scope :created_after, -> (datetime) { where("created_at >= ?", datetime) }

42 43
  scope :include_associations, -> { includes(:assignee, :labels, project: :namespace) }

44 45
  attr_spammable :title, spam_title: true
  attr_spammable :description, spam_description: true
46

Andrew8xx8 committed
47
  state_machine :state, initial: :opened do
Andrew8xx8 committed
48 49 50 51 52
    event :close do
      transition [:reopened, :opened] => :closed
    end

    event :reopen do
Andrew8xx8 committed
53
      transition closed: :reopened
Andrew8xx8 committed
54 55 56 57 58 59
    end

    state :opened
    state :reopened
    state :closed
  end
60

61 62 63 64
  def hook_attrs
    attributes
  end

65 66 67 68
  def self.reference_prefix
    '#'
  end

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

79
  def self.link_reference_pattern
80
    @link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
81 82
  end

83 84 85 86
  def self.reference_valid?(reference)
    reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
  end

87 88 89 90
  def self.project_foreign_key
    'project_id'
  end

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

100
  # `from` argument can be a Namespace or Project.
101
  def to_reference(from = nil, full: false)
102 103
    reference = "#{self.class.reference_prefix}#{iid}"

104
    "#{project.to_reference(from, full: full)}#{reference}"
105 106
  end

107
  def referenced_merge_requests(current_user = nil)
Yorick Peterse committed
108 109 110 111
    ext = all_references(current_user)

    notes_with_associations.each do |object|
      object.all_references(current_user, extractor: ext)
112
    end
Yorick Peterse committed
113 114

    ext.merge_requests.sort_by(&:iid)
115 116
  end

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

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

    branches_with_iid - branches_with_merge_request
127 128
  end

129 130 131 132
  # To allow polymorphism with MergeRequest.
  def source_project
    project
  end
133 134 135

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

Yorick Peterse committed
139 140 141 142 143 144
    ext = all_references(current_user)

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

145 146 147 148 149 150 151
    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
152
  end
153

154 155 156 157 158 159 160 161 162
  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

163 164
    !moved? && persisted? &&
      user.can?(:admin_issue, self.project)
165
  end
166

167
  def to_branch_name
168
    if self.confidential?
169
      "#{iid}-confidential-issue"
170
    else
171
      "#{iid}-#{title.parameterize}"
172
    end
173 174
  end

175
  def can_be_worked_on?(current_user)
176
    !self.closed? &&
177
      !self.project.forked? &&
178
      self.related_branches(current_user).empty? &&
179
      self.closed_by_merge_requests(current_user).empty?
180
  end
181

182 183 184
  # 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)
185
    return false unless project.feature_available?(:issues, user)
186

187
    user ? readable_by?(user) : publicly_visible?
188 189
  end

190
  def overdue?
Rémy Coutable committed
191
    due_date.try(:past?) || false
192
  end
193

194
  # Only issues on public projects should be checked for spam
195
  def check_for_spam?
196
    project.public?
197
  end
198 199 200

  def as_json(options = {})
    super(options).tap do |json|
201
      json[:subscribed] = subscribed?(options[:user], project) if options.has_key?(:user) && options[:user]
202

203 204 205
      if options.has_key?(:labels)
        json[:labels] = labels.as_json(
          project: project,
206
          only: [:id, :title, :description, :color, :priority],
207 208 209
          methods: [:text_color]
        )
      end
210 211
    end
  end
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239

  private

  # 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 ||
        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
gitlabhq committed
240
end