BigW Consortium Gitlab

issue.rb 8.66 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
  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 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 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 156 157 158 159 160 161 162
  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

163
  def referenced_merge_requests(current_user = nil)
Yorick Peterse committed
164 165 166 167
    ext = all_references(current_user)

    notes_with_associations.each do |object|
      object.all_references(current_user, extractor: ext)
168
    end
Yorick Peterse committed
169 170

    ext.merge_requests.sort_by(&:iid)
171 172
  end

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

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

    branches_with_iid - branches_with_merge_request
183 184
  end

185 186 187 188 189 190 191 192 193
  # 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
194
    Event.reset_event_cache_for(self)
195
  end
196 197 198 199 200

  # To allow polymorphism with MergeRequest.
  def source_project
    project
  end
201 202 203

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

Yorick Peterse committed
207 208 209 210 211 212
    ext = all_references(current_user)

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

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

222 223 224 225 226 227 228 229 230
  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

231 232
    !moved? && persisted? &&
      user.can?(:admin_issue, self.project)
233
  end
234

235
  def to_branch_name
236
    if self.confidential?
237
      "#{iid}-confidential-issue"
238
    else
239
      "#{iid}-#{title.parameterize}"
240
    end
241 242
  end

243
  def can_be_worked_on?(current_user)
244
    !self.closed? &&
245
      !self.project.forked? &&
246
      self.related_branches(current_user).empty? &&
247
      self.closed_by_merge_requests(current_user).empty?
248
  end
249

250 251 252
  # 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)
253
    return false unless project.feature_available?(:issues, user)
254

255
    user ? readable_by?(user) : publicly_visible?
256 257
  end

258
  def overdue?
Rémy Coutable committed
259
    due_date.try(:past?) || false
260
  end
261

262
  # Only issues on public projects should be checked for spam
263
  def check_for_spam?
264
    project.public?
265
  end
266 267 268

  def as_json(options = {})
    super(options).tap do |json|
269
      json[:subscribed] = subscribed?(options[:user]) if options.has_key?(:user) && options[:user]
270

271 272 273
      if options.has_key?(:labels)
        json[:labels] = labels.as_json(
          project: project,
274
          only: [:id, :title, :description, :color, :priority],
275 276 277
          methods: [:text_color]
        )
      end
278 279
    end
  end
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307

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