BigW Consortium Gitlab

issue.rb 6.55 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
  belongs_to :project
19 20
  belongs_to :moved_to, class_name: 'Issue'

21 22
  has_many :events, as: :target, dependent: :destroy

23
  has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
24

25 26
  validates :project, presence: true

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

31 32 33 34
  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) }

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

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

40 41
  scope :include_associations, -> { includes(:assignee, :labels, project: :namespace) }

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

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

    event :reopen do
Andrew8xx8 committed
51
      transition closed: :reopened
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
  def self.reference_prefix
    '#'
  end

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

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

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

85 86 87 88
  def self.project_foreign_key
    'project_id'
  end

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

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

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

105
  def referenced_merge_requests(current_user = nil)
Yorick Peterse committed
106 107 108 109
    ext = all_references(current_user)

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

    ext.merge_requests.sort_by(&:iid)
113 114
  end

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

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

    branches_with_iid - branches_with_merge_request
125 126
  end

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

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

Yorick Peterse committed
137 138 139 140 141 142
    ext = all_references(current_user)

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

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

152 153 154 155 156 157 158 159 160
  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

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

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

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

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

185
    user ? readable_by?(user) : publicly_visible?
186 187
  end

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

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

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

201 202 203
      if options.has_key?(:labels)
        json[:labels] = labels.as_json(
          project: project,
204
          only: [:id, :title, :description, :color, :priority],
205 206 207
          methods: [:text_color]
        )
      end
208 209
    end
  end
210 211 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

  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
238
end