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

344 lines
9.6 KiB
Ruby
Raw Normal View History

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
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-03-17 18:26:18 +05:30
ignore_column :assignee_id, :branch_name, :deleted_at
2014-09-02 18:07:02 +05:30
2016-06-02 11:05:42 +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
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)}
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) }
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') }
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
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
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-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
def referenced_merge_requests(current_user = nil)
ext = all_references(current_user)
notes_with_associations.each do |object|
object.all_references(current_user, extractor: ext)
2016-06-02 11:05:42 +05:30
end
2018-03-27 19:54:05 +05:30
merge_requests = ext.merge_requests.sort_by(&:iid)
cross_project_filter = -> (merge_requests) do
merge_requests.select { |mr| mr.target_project == project }
end
Ability.merge_requests_readable_by_user(
merge_requests, current_user,
filters: {
read_cross_project: cross_project_filter
}
)
2016-06-02 11:05:42 +05:30
end
# All branches containing the current issue's ID, except for
# those with a merge request open referencing the current issue.
def related_branches(current_user)
branches_with_iid = project.repository.branch_names.select do |branch|
branch =~ /\A#{iid}-(?!\d+-stable)/i
end
branches_with_merge_request = self.referenced_merge_requests(current_user).map(&:source_branch)
branches_with_iid - branches_with_merge_request
2015-12-23 02:04:40 +05:30
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
# From all notes on this issue, we'll select the system notes about linked
# merge requests. Of those, the MRs closing `self` are returned.
def closed_by_merge_requests(current_user = nil)
return [] unless open?
ext = all_references(current_user)
notes.system.each do |note|
note.all_references(current_user, extractor: ext)
end
2017-08-17 22:00:37 +05:30
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
2015-10-24 18:46:33 +05:30
end
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
def can_be_worked_on?(current_user)
!self.closed? &&
!self.project.forked? &&
self.related_branches(current_user).empty? &&
self.closed_by_merge_requests(current_user).empty?
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 overdue?
due_date.try(:past?) || false
end
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|
2018-05-09 12:01:36 +05:30
if options.key?(:issue_endpoints) && project
2018-03-17 18:26:18 +05:30
url_helper = Gitlab::Routing.url_helpers
2018-05-09 12:01:36 +05:30
issue_reference = options[:include_full_project_path] ? to_reference(full: true) : to_reference
json.merge!(
reference_path: issue_reference,
real_path: url_helper.project_issue_path(project, self),
issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'),
toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self)
)
2018-03-17 18:26:18 +05:30
end
2017-08-17 22:00:37 +05:30
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-03-17 18:26:18 +05:30
def discussions_rendered_on_frontend?
true
end
def update_project_counter_caches
Projects::OpenIssuesCountService.new(project).refresh_cache
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
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