BigW Consortium Gitlab

issue.rb 7.23 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
  validates :project, presence: true

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) }

Regis Boudinot committed
42 43
  after_save :expire_etag_cache

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
    end

    state :opened
    state :reopened
    state :closed
59 60 61 62

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

65
  def hook_attrs
66 67 68 69 70 71 72
    attrs = {
      total_time_spent: total_time_spent,
      human_total_time_spent: human_total_time_spent,
      human_time_estimate: human_time_estimate
    }

    attributes.merge!(attrs)
73 74
  end

75 76 77 78
  def self.reference_prefix
    '#'
  end

79 80 81 82
  # Pattern used to extract `#123` issue references from text
  #
  # This pattern supports cross-project references.
  def self.reference_pattern
83
    @reference_pattern ||= %r{
84 85
      (#{Project.reference_pattern})?
      #{Regexp.escape(reference_prefix)}(?<issue>\d+)
86
    }x
Kirill Zaitsev committed
87 88
  end

89
  def self.link_reference_pattern
90
    @link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
91 92
  end

93 94 95 96
  def self.reference_valid?(reference)
    reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
  end

97 98 99 100
  def self.project_foreign_key
    'project_id'
  end

101
  def self.sort(method, excluded_labels: [])
102 103
    case method.to_s
    when 'due_date_asc' then order_due_date_asc
104
    when 'due_date_desc' then order_due_date_desc
105 106 107 108 109
    else
      super
    end
  end

110 111 112 113 114 115 116
  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

117
  # `from` argument can be a Namespace or Project.
118
  def to_reference(from = nil, full: false)
119 120
    reference = "#{self.class.reference_prefix}#{iid}"

121
    "#{project.to_reference(from, full: full)}#{reference}"
122 123
  end

124
  def referenced_merge_requests(current_user = nil)
Yorick Peterse committed
125 126 127 128
    ext = all_references(current_user)

    notes_with_associations.each do |object|
      object.all_references(current_user, extractor: ext)
129
    end
Yorick Peterse committed
130 131

    ext.merge_requests.sort_by(&:iid)
132 133
  end

134
  # All branches containing the current issue's ID, except for
135
  # those with a merge request open referencing the current issue.
136 137
  def related_branches(current_user)
    branches_with_iid = project.repository.branch_names.select do |branch|
138
      branch =~ /\A#{iid}-(?!\d+-stable)/i
139
    end
140 141 142 143

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

    branches_with_iid - branches_with_merge_request
144 145
  end

146 147 148 149
  # To allow polymorphism with MergeRequest.
  def source_project
    project
  end
150 151 152

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

Yorick Peterse committed
156 157 158 159 160 161
    ext = all_references(current_user)

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

162 163 164 165 166 167 168
    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
169
  end
170

171 172 173 174 175 176 177 178 179
  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

180 181
    !moved? && persisted? &&
      user.can?(:admin_issue, self.project)
182
  end
183

184
  def to_branch_name
185
    if self.confidential?
186
      "#{iid}-confidential-issue"
187
    else
188
      "#{iid}-#{title.parameterize}"
189
    end
190 191
  end

192
  def can_be_worked_on?(current_user)
193
    !self.closed? &&
194
      !self.project.forked? &&
195
      self.related_branches(current_user).empty? &&
196
      self.closed_by_merge_requests(current_user).empty?
197
  end
198

199 200 201
  # 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)
202
    return false unless project.feature_available?(:issues, user)
203

204
    user ? readable_by?(user) : publicly_visible?
205 206
  end

207
  def overdue?
Rémy Coutable committed
208
    due_date.try(:past?) || false
209
  end
210 211

  def check_for_spam?
212
    project.public? && (title_changed? || description_changed?)
213
  end
214 215 216

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

219 220 221
      if options.has_key?(:labels)
        json[:labels] = labels.as_json(
          project: project,
222
          only: [:id, :title, :description, :color, :priority],
223 224 225
          methods: [:text_color]
        )
      end
226 227
    end
  end
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255

  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
Regis Boudinot committed
256 257 258 259 260 261 262 263 264

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