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

343 lines
11 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'
2019-07-07 11:18:12 +05:30
class Issue < ApplicationRecord
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
2018-11-18 11:00:15 +05:30
include LabelEventable
2020-01-01 13:55:28 +05:30
include IgnorableColumns
2017-09-10 17:25:29 +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
2019-03-02 22:35:43 +05:30
SORTING_PREFERENCE_FIELD = :issues_sort
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'
2019-12-04 20:38:33 +05:30
belongs_to :duplicated_to, class_name: 'Issue'
2018-05-09 12:01:36 +05:30
belongs_to :closed_by, class_name: 'User'
2020-03-13 15:44:24 +05:30
has_internal_id :iid, scope: :project, track_if: -> { !importing? }, init: ->(s) { s&.project&.issues&.maximum(:iid) }
2014-09-02 18:07:02 +05:30
2019-12-04 20:38:33 +05:30
has_many :events, as: :target, dependent: :delete_all # 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
2019-12-26 22:10:19 +05:30
has_many :zoom_meetings
2020-03-13 15:44:24 +05:30
has_many :user_mentions, class_name: "IssueUserMention", dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
2020-01-01 13:55:28 +05:30
has_one :sentry_issue
accepts_nested_attributes_for :sentry_issue
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
2019-10-12 21:52:04 +05:30
alias_method :issuing_parent, :project
2018-03-17 18:26:18 +05:30
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
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
2019-12-26 22:10:19 +05:30
scope :order_due_date_asc, -> { reorder(::Gitlab::Database.nulls_last_order('due_date', 'ASC')) }
scope :order_due_date_desc, -> { reorder(::Gitlab::Database.nulls_last_order('due_date', 'DESC')) }
scope :order_closest_future_date, -> { reorder(Arel.sql('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC')) }
2019-09-04 21:01:54 +05:30
scope :order_relative_position_asc, -> { reorder(::Gitlab::Database.nulls_last_order('relative_position', 'ASC')) }
2016-06-02 11:05:42 +05:30
2020-01-01 13:55:28 +05:30
scope :preload_associated_models, -> { preload(:labels, project: :namespace) }
2019-07-07 11:18:12 +05:30
scope :with_api_entity_associations, -> { preload(:timelogs, :assignees, :author, :notes, :labels, project: [:route, { namespace: :route }] ) }
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
scope :public_only, -> { where(confidential: false) }
2019-07-07 11:18:12 +05:30
scope :confidential_only, -> { where(confidential: true) }
2018-03-17 18:26:18 +05:30
2020-01-01 13:55:28 +05:30
scope :counts_by_state, -> { reorder(nil).group(:state_id).count }
ignore_column :state, remove_with: '12.7', remove_after: '2019-12-22'
2019-12-26 22:10:19 +05:30
2020-03-13 15:44:24 +05:30
after_commit :expire_etag_cache, unless: :importing?
after_save :ensure_metrics, unless: :importing?
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
2020-01-01 13:55:28 +05:30
state_machine :state_id, initial: :opened, initialize: false do
2014-09-02 18:07:02 +05:30
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
2019-12-21 20:55:43 +05:30
state :opened, value: Issue.available_states[:opened]
state :closed, value: Issue.available_states[:closed]
2017-08-17 22:00:37 +05:30
before_transition any => :closed do |issue|
2019-07-31 22:56:46 +05:30
issue.closed_at = issue.system_note_timestamp
2017-08-17 22:00:37 +05:30
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
2019-12-21 20:55:43 +05:30
# Alias to state machine .with_state_id method
# This needs to be defined after the state machine block to avoid errors
class << self
alias_method :with_state, :with_state_id
alias_method :with_states, :with_state_ids
end
2019-10-12 21:52:04 +05:30
def self.relative_positioning_query_base(issue)
in_projects(issue.parent_ids)
2015-04-26 12:48:37 +05:30
end
2019-10-12 21:52:04 +05:30
def self.relative_positioning_parent_column
2019-02-15 15:39:39 +05:30
: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
2020-03-13 15:44:24 +05:30
def self.simple_sorts
super.merge(
{
'closest_future_date' => -> { order_closest_future_date },
'closest_future_date_asc' => -> { order_closest_future_date },
'due_date' => -> { order_due_date_asc.with_order_id_desc },
'due_date_asc' => -> { order_due_date_asc.with_order_id_desc },
'due_date_desc' => -> { order_due_date_desc.with_order_id_desc },
'relative_position' => -> { order_relative_position_asc.with_order_id_desc },
'relative_position_asc' => -> { order_relative_position_asc.with_order_id_desc }
}
)
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
2019-12-04 20:38:33 +05:30
when 'closest_future_date', 'closest_future_date_asc' then order_closest_future_date
2019-12-26 22:10:19 +05:30
when 'due_date', 'due_date_asc' then order_due_date_asc.with_order_id_desc
when 'due_date_desc' then order_due_date_desc.with_order_id_desc
2019-12-04 20:38:33 +05:30
when 'relative_position', 'relative_position_asc' then order_relative_position_asc.with_order_id_desc
2016-06-02 11:05:42 +05:30
else
super
end
2015-12-23 02:04:40 +05:30
end
2020-03-13 15:44:24 +05:30
# `with_cte` argument allows sorting when using CTE queries and prevents
# errors in postgres when using CTE search optimisation
def self.order_by_position_and_priority(with_cte: false)
order_labels_priority(with_cte: with_cte)
2017-09-10 17:25:29 +05:30
.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
# `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
2020-03-13 15:44:24 +05:30
"#{project.to_reference_base(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?
2019-12-04 20:38:33 +05:30
!moved_to_id.nil?
end
def duplicated?
!duplicated_to_id.nil?
2016-06-02 11:05:42 +05:30
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
2019-12-26 22:10:19 +05:30
branch_name = "#{iid}-#{title.parameterize}"
if branch_name.length > 100
truncated_string = branch_name[0, 100]
# Delete everything dangling after the last hyphen so as not to risk
# existence of unintended words in the branch name due to mid-word split.
branch_name = truncated_string[0, truncated_string.rindex("-")]
end
branch_name
2016-06-02 11:05:42 +05:30
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
2019-07-07 11:18:12 +05:30
return publicly_visible? unless user
return false unless readable_by?(user)
2020-01-01 13:55:28 +05:30
user.can_read_all_resources? ||
2019-07-07 11:18:12 +05:30
::Gitlab::ExternalAuthorization.access_allowed?(
user, project.external_authorization_classification_label)
2016-09-13 17:45:13 +05:30
end
2016-11-24 13:41:30 +05:30
def check_for_spam?
2019-03-02 22:35:43 +05:30
publicly_visible? &&
(title_changed? || description_changed? || confidential_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
2019-07-07 11:18:12 +05:30
def merge_requests_count(user = nil)
::MergeRequestsClosingIssues.count_for_issue(self.id, user)
end
2019-09-04 21:01:54 +05:30
def labels_hook_attrs
labels.map(&:hook_attrs)
end
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
2020-03-28 13:19:24 +05:30
elsif confidential? && !assignee_or_author?(user)
project.team.member?(user, Gitlab::Access::REPORTER)
2016-09-13 17:45:13 +05:30
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?
2019-07-07 11:18:12 +05:30
project.public? && !confidential? && !::Gitlab::ExternalAuthorization.enabled?
2016-09-13 17:45:13 +05:30
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
2019-12-04 20:38:33 +05:30
Issue.prepend_if_ee('EE::Issue')