Read about our upcoming Code of Conduct on this issue

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

Rémy Coutable's avatar
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's avatar
Andrey Kumanyaev committed
29
  scope :cared, ->(user) { where(assignee_id: user) }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
30
  scope :open_for, ->(user) { opened.assigned_to(user) }
Valery Sizov's avatar
Valery Sizov committed
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
  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
    # Check if we are scoped to a specific project's issues
    if owner_project
96
      if owner_project.team.member?(user, Gitlab::Access::REPORTER)
97
98
99
100
101
102
103
104
105
106
        # 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
141
142
143
  def self.project_foreign_key
    'project_id'
  end

144
  def self.sort(method, excluded_labels: [])
145
146
    case method.to_s
    when 'due_date_asc' then order_due_date_asc
147
    when 'due_date_desc' then order_due_date_desc
148
149
150
151
152
    else
      super
    end
  end

153
154
155
  def to_reference(from_project = nil)
    reference = "#{self.class.reference_prefix}#{iid}"

156
    "#{project.to_reference(from_project)}#{reference}"
157
158
  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

181
182
183
184
  # To allow polymorphism with MergeRequest.
  def source_project
    project
  end
185
186
187

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

Yorick Peterse's avatar
Yorick Peterse committed
191
192
193
194
195
196
    ext = all_references(current_user)

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

197
198
199
200
201
202
203
    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
204
  end
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
205

206
207
208
209
210
211
212
213
214
  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

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

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

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

234
235
236
  # 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)
237
    return false unless project.feature_available?(:issues, user)
238

239
    user ? readable_by?(user) : publicly_visible?
240
241
  end

242
  def overdue?
Rémy Coutable's avatar
Rémy Coutable committed
243
    due_date.try(:past?) || false
244
  end
245

246
  # Only issues on public projects should be checked for spam
247
  def check_for_spam?
248
    project.public?
249
  end
Phil Hughes's avatar
Phil Hughes committed
250
251
252

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

255
256
257
      if options.has_key?(:labels)
        json[:labels] = labels.as_json(
          project: project,
258
          only: [:id, :title, :description, :color, :priority],
259
260
261
          methods: [:text_color]
        )
      end
Phil Hughes's avatar
Phil Hughes committed
262
263
    end
  end
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291

  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's avatar
gitlabhq committed
292
end