BigW Consortium Gitlab

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

gitlabhq committed
3
class Issue < ActiveRecord::Base
4
  include InternalId
5
  include Issuable
6
  include Noteable
7
  include Referable
8
  include Sortable
9
  include Spammable
10
  include FasterCacheKeys
11
  include RelativePositioning
12

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

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 29
  has_many :issue_assignees
  has_many :assignees, class_name: "User", through: :issue_assignees

30 31
  validates :project, presence: true

32
  scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
33

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

38 39 40 41
  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) }

42 43 44
  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') }

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

47
  scope :include_associations, -> { includes(:labels, project: :namespace) }
48

Regis Boudinot committed
49 50
  after_save :expire_etag_cache

51 52
  attr_spammable :title, spam_title: true
  attr_spammable :description, spam_description: true
53

54 55
  participant :assignees

Andrew8xx8 committed
56
  state_machine :state, initial: :opened do
Andrew8xx8 committed
57 58 59 60 61
    event :close do
      transition [:reopened, :opened] => :closed
    end

    event :reopen do
Andrew8xx8 committed
62
      transition closed: :reopened
Andrew8xx8 committed
63 64 65 66 67
    end

    state :opened
    state :reopened
    state :closed
68 69 70 71

    before_transition any => :closed do |issue|
      issue.closed_at = Time.zone.now
    end
Andrew8xx8 committed
72
  end
73

74
  def hook_attrs
75 76
    assignee_ids = self.assignee_ids

77 78 79
    attrs = {
      total_time_spent: total_time_spent,
      human_total_time_spent: human_total_time_spent,
80 81 82
      human_time_estimate: human_time_estimate,
      assignee_ids: assignee_ids,
      assignee_id: assignee_ids.first # This key is deprecated
83 84 85
    }

    attributes.merge!(attrs)
86 87
  end

88 89 90 91
  def self.reference_prefix
    '#'
  end

92 93 94 95
  # Pattern used to extract `#123` issue references from text
  #
  # This pattern supports cross-project references.
  def self.reference_pattern
96
    @reference_pattern ||= %r{
97 98
      (#{Project.reference_pattern})?
      #{Regexp.escape(reference_prefix)}(?<issue>\d+)
99
    }x
Kirill Zaitsev committed
100 101
  end

102
  def self.link_reference_pattern
103
    @link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
104 105
  end

106 107 108 109
  def self.reference_valid?(reference)
    reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
  end

110 111 112 113
  def self.project_foreign_key
    'project_id'
  end

114
  def self.sort(method, excluded_labels: [])
115 116
    case method.to_s
    when 'due_date_asc' then order_due_date_asc
117
    when 'due_date_desc' then order_due_date_desc
118 119 120 121 122
    else
      super
    end
  end

123 124 125 126 127 128 129
  def self.order_by_position_and_priority
    order_labels_priority.
      reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC'),
              Gitlab::Database.nulls_last_order('highest_priority', 'ASC'),
              "id DESC")
  end

130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
  # 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

146
  # `from` argument can be a Namespace or Project.
147
  def to_reference(from = nil, full: false)
148 149
    reference = "#{self.class.reference_prefix}#{iid}"

150
    "#{project.to_reference(from, full: full)}#{reference}"
151 152
  end

153
  def referenced_merge_requests(current_user = nil)
Yorick Peterse committed
154 155 156 157
    ext = all_references(current_user)

    notes_with_associations.each do |object|
      object.all_references(current_user, extractor: ext)
158
    end
Yorick Peterse committed
159 160

    ext.merge_requests.sort_by(&:iid)
161 162
  end

163
  # All branches containing the current issue's ID, except for
164
  # those with a merge request open referencing the current issue.
165 166
  def related_branches(current_user)
    branches_with_iid = project.repository.branch_names.select do |branch|
167
      branch =~ /\A#{iid}-(?!\d+-stable)/i
168
    end
169 170 171 172

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

    branches_with_iid - branches_with_merge_request
173 174
  end

175 176 177 178 179 180 181 182
  # Returns boolean if a related branch exists for the current issue
  # ignores merge requests branchs
  def has_related_branch? 
    project.repository.branch_names.any? do |branch|
      /\A#{iid}-(?!\d+-stable)/i =~ branch
    end
  end

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

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

Yorick Peterse committed
193 194 195 196 197 198
    ext = all_references(current_user)

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

199 200 201 202 203 204 205
    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
206
  end
207

208 209 210 211 212 213 214 215 216
  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

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

221
  def to_branch_name
222
    if self.confidential?
223
      "#{iid}-confidential-issue"
224
    else
225
      "#{iid}-#{title.parameterize}"
226
    end
227 228
  end

229
  def can_be_worked_on?(current_user)
230
    !self.closed? &&
231
      !self.project.forked? &&
232
      self.related_branches(current_user).empty? &&
233
      self.closed_by_merge_requests(current_user).empty?
234
  end
235

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

241
    user ? readable_by?(user) : publicly_visible?
242 243
  end

244
  def overdue?
Rémy Coutable committed
245
    due_date.try(:past?) || false
246
  end
247 248

  def check_for_spam?
249
    project.public? && (title_changed? || description_changed?)
250
  end
251 252 253

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

256 257 258
      if options.has_key?(:labels)
        json[:labels] = labels.as_json(
          project: project,
259
          only: [:id, :title, :description, :color, :priority],
260 261 262
          methods: [:text_color]
        )
      end
263 264
    end
  end
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279

  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 ||
280
        assignees.include?(user) ||
281 282 283 284 285 286 287 288 289 290 291 292
        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
Regis Boudinot committed
293 294 295 296 297 298 299 300 301

  def expire_etag_cache
    key = Gitlab::Routing.url_helpers.rendered_title_namespace_project_issue_path(
      project.namespace,
      project,
      self
    )
    Gitlab::EtagCaching::Store.new.touch(key)
  end
gitlabhq committed
302
end