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

303 lines
8.4 KiB
Ruby
Raw Normal View History

2014-09-02 18:07:02 +05:30
require 'carrierwave/orm/activerecord'
class Issue < ActiveRecord::Base
include InternalId
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
2015-04-26 12:48:37 +05:30
include Sortable
2016-09-13 17:45:13 +05:30
include Spammable
include FasterCacheKeys
2017-08-17 22:00:37 +05:30
include RelativePositioning
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'
2014-09-02 18:07:02 +05:30
2016-08-24 12:49:21 +05:30
has_many :events, as: :target, dependent: :destroy
2016-09-29 09:46:39 +05:30
has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
2017-08-17 22:00:37 +05:30
has_many :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees
2016-06-02 11:05:42 +05:30
validates :project, presence: true
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') }
2016-09-29 09:46:39 +05:30
scope :created_after, -> (datetime) { where("created_at >= ?", datetime) }
2017-08-17 22:00:37 +05:30
scope :include_associations, -> { includes(:labels, project: :namespace) }
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
transition [:reopened, :opened] => :closed
end
event :reopen do
transition closed: :reopened
end
state :opened
state :reopened
state :closed
2017-08-17 22:00:37 +05:30
before_transition any => :closed do |issue|
issue.closed_at = Time.zone.now
end
2014-09-02 18:07:02 +05:30
end
2015-04-26 12:48:37 +05:30
def hook_attrs
2017-08-17 22:00:37 +05:30
assignee_ids = self.assignee_ids
attrs = {
total_time_spent: total_time_spent,
human_total_time_spent: human_total_time_spent,
human_time_estimate: human_time_estimate,
assignee_ids: assignee_ids,
assignee_id: assignee_ids.first # This key is deprecated
}
attributes.merge!(attrs)
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
def self.sort(method, excluded_labels: [])
2016-06-02 11:05:42 +05:30
case method.to_s
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
order_labels_priority.
reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC'),
Gitlab::Database.nulls_last_order('highest_priority', 'ASC'),
"id DESC")
end
2015-09-11 14:41:01 +05:30
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
ext.merge_requests.sort_by(&:iid)
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
def has_related_branch?
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|
2017-08-17 22:00:37 +05:30
json[:subscribed] = subscribed?(options[:user], project) if options.has_key?(:user) && options[:user]
2016-11-24 13:41:30 +05:30
if options.has_key?(:labels)
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
private
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
key = Gitlab::Routing.url_helpers.rendered_title_namespace_project_issue_path(
project.namespace,
project,
self
)
Gitlab::EtagCaching::Store.new.touch(key)
end
2014-09-02 18:07:02 +05:30
end