debian-mirror-gitlab/app/models/issue.rb

300 lines
8.4 KiB
Ruby
Raw Normal View History

2018-11-18 11:00:15 +05:30
# frozen_string_literal: true
2014-09-02 18:07:02 +05:30
require 'carrierwave/orm/activerecord'
class Issue < ActiveRecord::Base
2018-05-09 12:01:36 +05:30
include AtomicInternalId
2018-11-08 19:23:39 +05:30
include IidRoutes
2015-09-11 14:41:01 +05:30
include Issuable
2017-08-17 22:00:37 +05:30
include Noteable
2015-09-11 14:41:01 +05:30
include Referable
2016-09-13 17:45:13 +05:30
include Spammable
include FasterCacheKeys
2017-08-17 22:00:37 +05:30
include RelativePositioning
2018-03-17 18:26:18 +05:30
include TimeTrackable
include ThrottledTouch
2017-09-10 17:25:29 +05:30
include IgnorableColumn
2018-11-18 11:00:15 +05:30
include LabelEventable
2017-09-10 17:25:29 +05:30
2018-03-17 18:26:18 +05:30
ignore_column :assignee_id, :branch_name, :deleted_at
2014-09-02 18:07:02 +05:30
2018-11-08 19:23:39 +05:30
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
DueNextMonthAndPreviousTwoWeeks = DueDateStruct.new('Due Next Month And Previous Two Weeks', 'next_month_and_previous_two_weeks').freeze
2016-06-02 11:05:42 +05:30
2014-09-02 18:07:02 +05:30
belongs_to :project
2016-06-02 11:05:42 +05:30
belongs_to :moved_to, class_name: 'Issue'
2018-05-09 12:01:36 +05:30
belongs_to :closed_by, class_name: 'User'
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) }
2014-09-02 18:07:02 +05:30
2017-09-10 17:25:29 +05:30
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
2016-08-24 12:49:21 +05:30
2017-09-10 17:25:29 +05:30
has_many :merge_requests_closing_issues,
class_name: 'MergeRequestsClosingIssues',
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees
2016-09-29 09:46:39 +05:30
2016-06-02 11:05:42 +05:30
validates :project, presence: true
2018-03-17 18:26:18 +05:30
alias_attribute :parent_ids, :project_id
2016-04-02 18:10:28 +05:30
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
2014-09-02 18:07:02 +05:30
2017-08-17 22:00:37 +05:30
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)}
2018-11-08 19:23:39 +05:30
scope :with_due_date, -> { where.not(due_date: nil) }
2016-06-02 11:05:42 +05:30
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) }
2018-10-15 14:42:47 +05:30
scope :due_tomorrow, -> { where(due_date: Date.tomorrow) }
2016-06-02 11:05:42 +05:30
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') }
2018-11-08 19:23:39 +05:30
scope :order_closest_future_date, -> { reorder('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC') }
2016-06-02 11:05:42 +05:30
2017-09-10 17:25:29 +05:30
scope :preload_associations, -> { preload(:labels, project: :namespace) }
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
scope :public_only, -> { where(confidential: false) }
2017-08-17 22:00:37 +05:30
after_save :expire_etag_cache
2018-11-08 19:23:39 +05:30
after_save :ensure_metrics, unless: :imported?
2017-08-17 22:00:37 +05:30
2016-09-13 17:45:13 +05:30
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
2017-08-17 22:00:37 +05:30
participant :assignees
2014-09-02 18:07:02 +05:30
state_machine :state, initial: :opened do
event :close do
2017-09-10 17:25:29 +05:30
transition [:opened] => :closed
2014-09-02 18:07:02 +05:30
end
event :reopen do
2017-09-10 17:25:29 +05:30
transition closed: :opened
2014-09-02 18:07:02 +05:30
end
state :opened
state :closed
2017-08-17 22:00:37 +05:30
before_transition any => :closed do |issue|
issue.closed_at = Time.zone.now
end
2018-05-09 12:01:36 +05:30
before_transition closed: :opened do |issue|
issue.closed_at = nil
issue.closed_by = nil
end
2014-09-02 18:07:02 +05:30
end
2018-03-17 18:26:18 +05:30
class << self
alias_method :in_parents, :in_projects
2015-04-26 12:48:37 +05:30
end
2018-12-23 12:14:25 +05:30
def self.parent_column
:project_id
end
2015-09-11 14:41:01 +05:30
def self.reference_prefix
'#'
end
# Pattern used to extract `#123` issue references from text
#
# This pattern supports cross-project references.
def self.reference_pattern
2016-06-02 11:05:42 +05:30
@reference_pattern ||= %r{
2015-09-11 14:41:01 +05:30
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<issue>\d+)
}x
end
2015-12-23 02:04:40 +05:30
def self.link_reference_pattern
2016-06-02 11:05:42 +05:30
@link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
end
2016-06-22 15:30:34 +05:30
def self.reference_valid?(reference)
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end
2016-11-03 12:29:30 +05:30
def self.project_foreign_key
'project_id'
end
2018-05-09 12:01:36 +05:30
def self.sort_by_attribute(method, excluded_labels: [])
2016-06-02 11:05:42 +05:30
case method.to_s
2018-11-08 19:23:39 +05:30
when 'closest_future_date' then order_closest_future_date
2018-03-17 18:26:18 +05:30
when 'due_date' then order_due_date_asc
when 'due_date_asc' then order_due_date_asc
when 'due_date_desc' then order_due_date_desc
2016-06-02 11:05:42 +05:30
else
super
end
2015-12-23 02:04:40 +05:30
end
2017-08-17 22:00:37 +05:30
def self.order_by_position_and_priority
2017-09-10 17:25:29 +05:30
order_labels_priority
.reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC'),
2017-08-17 22:00:37 +05:30
Gitlab::Database.nulls_last_order('highest_priority', 'ASC'),
"id DESC")
end
2015-09-11 14:41:01 +05:30
2018-03-17 18:26:18 +05:30
def hook_attrs
Gitlab::HookData::IssueBuilder.new(self).build
end
2017-08-17 22:00:37 +05:30
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
'Author' => author.try(:name),
'Assignee' => assignee_list
}
end
2014-09-02 18:07:02 +05:30
2017-08-17 22:00:37 +05:30
def assignee_or_author?(user)
author_id == user.id || assignees.exists?(user.id)
end
def assignee_list
assignees.map(&:name).to_sentence
end
# `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
"#{project.to_reference(from, full: full)}#{reference}"
2014-09-02 18:07:02 +05:30
end
2018-10-15 14:42:47 +05:30
def suggested_branch_name
return to_branch_name unless project.repository.branch_exists?(to_branch_name)
start_counting_from = 2
Uniquify.new(start_counting_from).string(-> (counter) { "#{to_branch_name}-#{counter}" }) do |suggested_branch_name|
project.repository.branch_exists?(suggested_branch_name)
end
end
2017-08-17 22:00:37 +05:30
# Returns boolean if a related branch exists for the current issue
# ignores merge requests branchs
2017-09-10 17:25:29 +05:30
def has_related_branch?
2017-08-17 22:00:37 +05:30
project.repository.branch_names.any? do |branch|
/\A#{iid}-(?!\d+-stable)/i =~ branch
end
2014-09-02 18:07:02 +05:30
end
2015-04-26 12:48:37 +05:30
# To allow polymorphism with MergeRequest.
def source_project
project
end
2015-10-24 18:46:33 +05:30
2016-06-02 11:05:42 +05:30
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
!moved? && persisted? &&
user.can?(:admin_issue, self.project)
end
def to_branch_name
if self.confidential?
"#{iid}-confidential-issue"
else
"#{iid}-#{title.parameterize}"
end
end
2018-10-15 14:42:47 +05:30
def can_be_worked_on?
!self.closed? && !self.project.forked?
2016-06-02 11:05:42 +05:30
end
2016-09-13 17:45:13 +05:30
# 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)
2017-08-17 22:00:37 +05:30
return false unless project && project.feature_available?(:issues, user)
2016-11-24 13:41:30 +05:30
2016-09-13 17:45:13 +05:30
user ? readable_by?(user) : publicly_visible?
end
2016-11-24 13:41:30 +05:30
def check_for_spam?
2017-08-17 22:00:37 +05:30
project.public? && (title_changed? || description_changed?)
2016-11-24 13:41:30 +05:30
end
def as_json(options = {})
super(options).tap do |json|
2017-09-10 17:25:29 +05:30
if options.key?(:labels)
2016-11-24 13:41:30 +05:30
json[:labels] = labels.as_json(
project: project,
2017-08-17 22:00:37 +05:30
only: [:id, :title, :description, :color, :priority],
2016-11-24 13:41:30 +05:30
methods: [:text_color]
)
end
end
end
2018-11-08 19:23:39 +05:30
def etag_caching_enabled?
true
end
2018-03-17 18:26:18 +05:30
def discussions_rendered_on_frontend?
true
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2018-03-17 18:26:18 +05:30
def update_project_counter_caches
Projects::OpenIssuesCountService.new(project).refresh_cache
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2018-03-17 18:26:18 +05:30
2016-11-24 13:41:30 +05:30
private
2018-03-17 18:26:18 +05:30
def ensure_metrics
super
metrics.record!
end
2016-09-13 17:45:13 +05:30
# Returns `true` if the given User can read the current Issue.
2016-11-24 13:41:30 +05:30
#
# 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
2016-09-13 17:45:13 +05:30
def readable_by?(user)
if user.admin?
true
elsif project.owner == user
true
elsif confidential?
author == user ||
2017-08-17 22:00:37 +05:30
assignees.include?(user) ||
2016-09-13 17:45:13 +05:30
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
2017-08-17 22:00:37 +05:30
def expire_etag_cache
2017-09-10 17:25:29 +05:30
key = Gitlab::Routing.url_helpers.realtime_changes_project_issue_path(project, self)
2017-08-17 22:00:37 +05:30
Gitlab::EtagCaching::Store.new.touch(key)
end
2014-09-02 18:07:02 +05:30
end