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
2016-01-14 18:37:52 +05:30
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
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
2016-06-16 23:09:34 +05:30
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 |
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' ) ,
2018-12-13 13:39:08 +05:30
toggle_subscription_endpoint : url_helper . toggle_subscription_project_issue_path ( project , self ) ,
assignable_labels_endpoint : url_helper . project_labels_path ( project , format : :json , include_ancestor_groups : true )
2018-05-09 12:01:36 +05:30
)
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-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