2018-07-25 05:30:33 -04:00
# frozen_string_literal: true
2014-05-23 04:22:00 -04:00
require 'carrierwave/orm/activerecord'
2019-03-28 09:17:42 -04:00
class Issue < ApplicationRecord
2018-03-06 14:09:01 -05:00
include AtomicInternalId
2018-05-11 03:52:48 -04:00
include IidRoutes
2015-05-02 23:11:21 -04:00
include Issuable
2017-03-09 20:29:11 -05:00
include Noteable
2015-05-02 23:11:21 -04:00
include Referable
2016-07-21 19:11:53 -04:00
include Spammable
2016-08-08 10:18:13 -04:00
include FasterCacheKeys
2017-02-01 13:41:01 -05:00
include RelativePositioning
2017-11-01 13:35:14 -04:00
include TimeTrackable
2017-12-01 08:52:16 -05:00
include ThrottledTouch
2017-12-11 08:25:27 -05:00
include IgnorableColumn
2018-08-01 04:58:49 -04:00
include LabelEventable
2017-12-11 08:25:27 -05:00
2018-01-02 11:21:28 -05:00
ignore_column :assignee_id , :branch_name , :deleted_at
2017-06-09 14:34:58 -04:00
2018-05-31 10:01:04 -04:00
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-03-10 09:26:56 -05:00
2019-01-06 19:00:48 -05:00
SORTING_PREFERENCE_FIELD = :issues_sort
2013-04-25 10:15:33 -04:00
belongs_to :project
2016-03-17 05:31:17 -04:00
belongs_to :moved_to , class_name : 'Issue'
2018-02-09 13:23:55 -05:00
belongs_to :closed_by , class_name : 'User'
2016-03-17 05:31:17 -04:00
2018-03-12 11:51:38 -04:00
has_internal_id :iid , scope : :project , init : - > ( s ) { s & . project & . issues & . maximum ( :iid ) }
2018-03-12 10:38:56 -04:00
2017-06-08 11:16:27 -04:00
has_many :events , as : :target , dependent : :destroy # rubocop:disable Cop/ActiveRecordDependent
2016-07-01 09:34:10 -04:00
2017-06-08 11:16:27 -04:00
has_many :merge_requests_closing_issues ,
class_name : 'MergeRequestsClosingIssues' ,
dependent : :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees
2018-04-18 09:41:42 -04:00
has_many :assignees , class_name : " User " , through : :issue_assignees
Improve performance of the cycle analytics page.
1. These changes bring down page load time for 100 issues from more than
a minute to about 1.5 seconds.
2. This entire commit is composed of these types of performance
enhancements:
- Cache relevant data in `IssueMetrics` wherever possible.
- Cache relevant data in `MergeRequestMetrics` wherever possible.
- Preload metrics
3. Given these improvements, we now only need to make 4 SQL calls:
- Load all issues
- Load all merge requests
- Load all metrics for the issues
- Load all metrics for the merge requests
4. A list of all the data points that are now being pre-calculated:
a. The first time an issue is mentioned in a commit
- In `GitPushService`, find all issues mentioned by the given commit
using `ReferenceExtractor`. Set the `first_mentioned_in_commit_at`
flag for each of them.
- There seems to be a (pre-existing) bug here - files (and
therefore commits) created using the Web CI don't have
cross-references created, and issues are not closed even when
the commit title is "Fixes #xx".
b. The first time a merge request is deployed to production
When a `Deployment` is created, find all merge requests that
were merged in before the deployment, and set the
`first_deployed_to_production_at` flag for each of them.
c. The start / end time for a merge request pipeline
Hook into the `Pipeline` state machine. When the `status` moves to
`running`, find the merge requests whose tip commit matches the
pipeline, and record the `latest_build_started_at` time for each
of them. When the `status` moves to `success`, record the
`latest_build_finished_at` time.
d. The merge requests that close an issue
- This was a big cause of the performance problems we were having
with Cycle Analytics. We need to use `ReferenceExtractor` to make
this calculation, which is slow when we have to run it on a large
number of merge requests.
- When a merge request is created, updated, or refreshed, find the
issues it closes, and create an instance of
`MergeRequestsClosingIssues`, which acts as a join model between
merge requests and issues.
- If a `MergeRequestsClosingIssues` instance links a merge request
and an issue, that issue closes that merge request.
5. The `Queries` module was changed into a class, so we can cache the
results of `issues` and `merge_requests_closing_issues` across
various cycle analytics stages.
6. The code added in this commit is untested. Tests will be added in the
next commit.
2016-09-15 04:59:36 -04:00
2013-04-25 10:15:33 -04:00
validates :project , presence : true
2018-01-05 04:15:03 -05:00
alias_attribute :parent_ids , :project_id
2018-01-04 07:29:48 -05:00
2016-01-22 04:24:38 -05:00
scope :in_projects , - > ( project_ids ) { where ( project_id : project_ids ) }
2013-02-19 04:01:19 -05:00
2017-05-04 08:11:15 -04:00
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-06-28 08:48:10 -04:00
scope :with_due_date , - > { where . not ( due_date : nil ) }
2016-04-20 08:41:50 -04:00
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-03-30 09:05:20 -04:00
scope :due_tomorrow , - > { where ( due_date : Date . tomorrow ) }
2016-04-20 08:41:50 -04:00
2016-04-19 07:10:25 -04:00
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-06-28 08:48:10 -04:00
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-04-19 07:10:25 -04:00
2017-06-07 17:14:23 -04:00
scope :preload_associations , - > { preload ( :labels , project : :namespace ) }
2019-03-02 12:31:36 -05:00
scope :with_api_entity_associations , - > { preload ( :timelogs , :assignees , :author , :notes , :labels , project : [ :route , { namespace : :route } ] ) }
2016-12-19 16:26:15 -05:00
2017-08-17 11:21:25 -04:00
scope :public_only , - > { where ( confidential : false ) }
2019-02-25 06:00:24 -05:00
scope :confidential_only , - > { where ( confidential : true ) }
2017-08-17 11:21:25 -04:00
2017-04-05 21:13:06 -04:00
after_save :expire_etag_cache
2018-05-30 11:14:56 -04:00
after_save :ensure_metrics , unless : :imported?
2017-04-05 21:13:06 -04:00
2016-08-05 18:10:08 -04:00
attr_spammable :title , spam_title : true
attr_spammable :description , spam_description : true
2016-07-28 20:02:56 -04:00
2017-05-04 08:11:15 -04:00
participant :assignees
2013-02-18 08:22:18 -05:00
state_machine :state , initial : :opened do
2013-02-18 04:10:58 -05:00
event :close do
2017-07-19 10:03:50 -04:00
transition [ :opened ] = > :closed
2013-02-18 04:10:58 -05:00
end
event :reopen do
2017-07-19 10:03:50 -04:00
transition closed : :opened
2013-02-18 04:10:58 -05:00
end
state :opened
state :closed
2017-03-15 16:58:09 -04:00
before_transition any = > :closed do | issue |
issue . closed_at = Time . zone . now
end
2018-03-05 09:27:53 -05:00
before_transition closed : :opened do | issue |
issue . closed_at = nil
issue . closed_by = nil
end
2013-02-18 04:10:58 -05:00
end
2013-04-09 08:04:31 -04:00
2018-01-04 07:29:48 -05:00
class << self
alias_method :in_parents , :in_projects
end
2018-11-23 08:06:04 -05:00
def self . parent_column
:project_id
end
2015-05-02 23:11:21 -04:00
def self . reference_prefix
'#'
end
2015-05-14 16:59:39 -04:00
# Pattern used to extract `#123` issue references from text
#
# This pattern supports cross-project references.
def self . reference_pattern
2016-03-24 11:41:48 -04:00
@reference_pattern || = %r{
2015-12-01 09:51:27 -05:00
( #{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<issue>\d+)
2015-05-14 16:59:39 -04:00
} x
2014-09-15 03:10:35 -04:00
end
2015-11-30 15:14:46 -05:00
def self . link_reference_pattern
2016-03-24 11:41:48 -04:00
@link_reference_pattern || = super ( " issues " , / (?<issue> \ d+) / )
2015-11-30 15:14:46 -05:00
end
2016-06-18 13:55:45 -04:00
def self . reference_valid? ( reference )
reference . to_i > 0 && reference . to_i < = Gitlab :: Database :: MAX_INT_VALUE
end
2016-10-19 09:49:07 -04:00
def self . project_foreign_key
'project_id'
end
2018-04-04 05:19:47 -04:00
def self . sort_by_attribute ( method , excluded_labels : [ ] )
2016-04-19 07:10:25 -04:00
case method . to_s
2018-05-31 10:01:04 -04:00
when 'closest_future_date' then order_closest_future_date
2017-09-22 20:46:53 -04:00
when 'due_date' then order_due_date_asc
when 'due_date_asc' then order_due_date_asc
2016-05-13 11:26:18 -04:00
when 'due_date_desc' then order_due_date_desc
2016-04-19 07:10:25 -04:00
else
super
end
end
2017-03-07 11:04:44 -05:00
def self . order_by_position_and_priority
2017-06-21 09:48:12 -04:00
order_labels_priority
. reorder ( Gitlab :: Database . nulls_last_order ( 'relative_position' , 'ASC' ) ,
2017-03-07 11:04:44 -05:00
Gitlab :: Database . nulls_last_order ( 'highest_priority' , 'ASC' ) ,
" id DESC " )
end
2017-09-19 13:23:15 -04:00
def hook_attrs
2017-10-05 13:02:50 -04:00
Gitlab :: HookData :: IssueBuilder . new ( self ) . build
2017-09-19 13:23:15 -04:00
end
2017-05-04 08:11:15 -04:00
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
'Author' = > author . try ( :name ) ,
'Assignee' = > assignee_list
}
end
def assignee_or_author? ( user )
author_id == user . id || assignees . exists? ( user . id )
end
def assignee_list
assignees . map ( & :name ) . to_sentence
end
2017-01-11 07:44:12 -05:00
# `from` argument can be a Namespace or Project.
2017-01-10 18:52:25 -05:00
def to_reference ( from = nil , full : false )
2015-05-02 23:11:21 -04:00
reference = " #{ self . class . reference_prefix } #{ iid } "
2017-01-10 18:52:25 -05:00
" #{ project . to_reference ( from , full : full ) } #{ reference } "
2015-05-02 23:11:21 -04:00
end
2018-04-05 11:17:02 -04:00
def suggested_branch_name
return to_branch_name unless project . repository . branch_exists? ( to_branch_name )
2018-04-19 02:59:37 -04:00
start_counting_from = 2
2018-04-19 05:31:01 -04:00
Uniquify . new ( start_counting_from ) . string ( - > ( counter ) { " #{ to_branch_name } - #{ counter } " } ) do | suggested_branch_name |
2018-04-19 02:59:37 -04:00
project . repository . branch_exists? ( suggested_branch_name )
end
2018-04-05 11:17:02 -04:00
end
2017-05-04 04:09:21 -04:00
# Returns boolean if a related branch exists for the current issue
# ignores merge requests branchs
2017-05-11 11:47:44 -04:00
def has_related_branch?
2017-05-04 04:09:21 -04:00
project . repository . branch_names . any? do | branch |
/ \ A #{ iid } -(?! \ d+-stable) /i =~ branch
end
end
2014-09-20 10:06:35 -04:00
# To allow polymorphism with MergeRequest.
def source_project
project
end
2015-10-12 06:04:20 -04:00
2016-03-17 06:11:22 -04:00
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
2016-03-23 04:39:37 -04:00
! moved? && persisted? &&
user . can? ( :admin_issue , self . project )
2016-03-17 06:11:22 -04:00
end
2016-03-19 13:50:15 -04:00
2016-02-12 13:42:25 -05:00
def to_branch_name
2016-04-12 00:29:01 -04:00
if self . confidential?
2016-04-18 23:52:55 -04:00
" #{ iid } -confidential-issue "
2016-04-12 00:29:01 -04:00
else
2016-04-15 00:20:53 -04:00
" #{ iid } - #{ title . parameterize } "
2016-04-12 00:29:01 -04:00
end
2016-02-12 13:42:25 -05:00
end
2018-04-05 11:17:02 -04:00
def can_be_worked_on?
! self . closed? && ! self . project . forked?
2016-02-12 13:42:25 -05:00
end
2016-03-18 13:59:57 -04:00
2016-07-20 14:13:02 -04:00
# 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-04-21 02:45:09 -04:00
return false unless project && project . feature_available? ( :issues , user )
2016-07-20 14:13:02 -04:00
2019-04-05 09:02:56 -04:00
user ? readable_by? ( user ) : publicly_visible?
2016-07-20 14:13:02 -04:00
end
2016-07-30 00:18:32 -04:00
def check_for_spam?
2019-01-17 06:39:28 -05:00
publicly_visible? &&
( title_changed? || description_changed? || confidential_changed? )
2016-07-30 00:18:32 -04:00
end
2016-10-07 04:24:57 -04:00
def as_json ( options = { } )
super ( options ) . tap do | json |
2017-06-02 13:11:26 -04:00
if options . key? ( :labels )
2016-10-14 19:51:41 -04:00
json [ :labels ] = labels . as_json (
project : project ,
2016-10-19 17:33:34 -04:00
only : [ :id , :title , :description , :color , :priority ] ,
2016-10-14 19:51:41 -04:00
methods : [ :text_color ]
)
end
2016-10-07 04:24:57 -04:00
end
end
2016-11-01 16:18:51 -04:00
2018-06-21 08:22:40 -04:00
def etag_caching_enabled?
true
end
2017-08-17 13:26:45 -04:00
def discussions_rendered_on_frontend?
true
end
2018-08-27 11:31:01 -04:00
# rubocop: disable CodeReuse/ServiceClass
2017-08-17 11:21:25 -04:00
def update_project_counter_caches
Projects :: OpenIssuesCountService . new ( project ) . refresh_cache
end
2018-08-27 11:31:01 -04:00
# rubocop: enable CodeReuse/ServiceClass
2017-08-17 11:21:25 -04:00
2019-02-27 05:28:53 -05:00
def merge_requests_count
merge_requests_closing_issues . count
end
2016-11-01 16:18:51 -04:00
private
2017-12-07 12:41:30 -05:00
def ensure_metrics
super
metrics . record!
end
2016-11-01 16:18:51 -04:00
# Returns `true` if the given User can read the current Issue.
#
# 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
def readable_by? ( user )
if user . admin?
true
elsif project . owner == user
true
elsif confidential?
author == user ||
2017-05-04 08:11:15 -04:00
assignees . include? ( user ) ||
2016-11-01 16:18:51 -04:00
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?
2019-04-05 09:02:56 -04:00
project . public? && ! confidential?
2016-11-01 16:18:51 -04:00
end
2017-04-05 21:13:06 -04:00
def expire_etag_cache
2017-06-29 13:06:35 -04:00
key = Gitlab :: Routing . url_helpers . realtime_changes_project_issue_path ( project , self )
2017-04-05 21:13:06 -04:00
Gitlab :: EtagCaching :: Store . new . touch ( key )
end
2011-10-08 17:36:38 -04:00
end