2018-08-03 13:22:24 -04:00
# frozen_string_literal: true
2013-01-03 02:06:07 -05:00
# == Issuable concern
2012-12-30 09:19:31 -05:00
#
2012-10-09 15:09:46 -04:00
# Contains common functionality shared between Issues and MergeRequests
2012-12-30 09:19:31 -05:00
#
2019-10-07 02:06:10 -04:00
# Used by Issue, MergeRequest, Epic
2012-12-30 09:19:31 -05:00
#
2013-01-03 02:06:07 -05:00
module Issuable
2012-08-08 21:40:57 -04:00
extend ActiveSupport :: Concern
2017-08-23 06:54:14 -04:00
include Gitlab :: SQL :: Pattern
2018-09-20 10:14:46 -04:00
include Redactable
2016-10-06 17:17:11 -04:00
include CacheMarkdownField
2015-04-21 09:23:20 -04:00
include Participable
2015-10-14 10:20:11 -04:00
include Mentionable
2016-02-12 09:58:39 -05:00
include Subscribable
2015-11-26 10:16:50 -05:00
include StripAttribute
2016-04-16 15:09:08 -04:00
include Awardable
2016-09-26 17:36:11 -04:00
include Taskable
2017-03-27 10:43:35 -04:00
include Importable
2017-05-18 08:24:34 -04:00
include Editable
2017-08-01 08:38:45 -04:00
include AfterCommitQueue
2017-11-07 08:34:12 -05:00
include Sortable
include CreatedAtFilterable
2018-02-28 06:16:29 -05:00
include UpdatedAtFilterable
2019-01-17 12:49:07 -05:00
include ClosedAtFilterable
2019-10-18 07:11:44 -04:00
include VersionedDescription
2012-08-08 21:40:57 -04:00
2019-10-07 02:06:10 -04:00
TITLE_LENGTH_MAX = 255
TITLE_HTML_LENGTH_MAX = 800
2019-10-15 20:06:16 -04:00
DESCRIPTION_LENGTH_MAX = 1 . megabyte
DESCRIPTION_HTML_LENGTH_MAX = 5 . megabytes
2019-10-07 02:06:10 -04:00
2019-10-18 07:11:44 -04:00
STATE_ID_MAP = {
opened : 1 ,
closed : 2 ,
merged : 3 ,
locked : 4
} . with_indifferent_access . freeze
2017-01-23 15:40:25 -05:00
# This object is used to gather issuable meta data for displaying
2017-02-16 15:03:42 -05:00
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests
2017-01-23 15:40:25 -05:00
# lists avoiding n+1 queries and improving performance.
2019-06-12 12:28:25 -04:00
IssuableMeta = Struct . new ( :upvotes , :downvotes , :user_notes_count , :mrs_count ) do
def merge_requests_count ( user = nil )
mrs_count
end
end
2017-01-23 15:40:25 -05:00
2012-08-08 21:40:57 -04:00
included do
2016-10-06 17:17:11 -04:00
cache_markdown_field :title , pipeline : :single_line
2017-04-19 10:38:46 -04:00
cache_markdown_field :description , issuable_state_filter_enabled : true
2016-10-06 17:17:11 -04:00
2018-09-20 10:14:46 -04:00
redact_field :description
2019-02-27 05:28:53 -05:00
belongs_to :author , class_name : 'User'
belongs_to :updated_by , class_name : 'User'
2017-05-03 01:32:21 -04:00
belongs_to :last_edited_by , class_name : 'User'
2012-10-30 03:22:24 -04:00
belongs_to :milestone
2017-07-07 11:08:49 -04:00
2017-06-08 11:16:27 -04:00
has_many :notes , as : :noteable , inverse_of : :noteable , dependent : :destroy do # rubocop:disable Cop/ActiveRecordDependent
2016-06-06 16:19:39 -04:00
def authors_loaded?
2016-06-21 01:26:43 -04:00
# We check first if we're loaded to not load unnecessarily.
2016-06-06 16:19:39 -04:00
loaded? && to_a . all? { | note | note . association ( :author ) . loaded? }
end
2016-06-21 01:26:43 -04:00
def award_emojis_loaded?
# We check first if we're loaded to not load unnecessarily.
loaded? && to_a . all? { | note | note . association ( :award_emoji ) . loaded? }
end
2016-06-06 16:19:39 -04:00
end
2016-09-14 10:10:31 -04:00
2018-09-06 13:02:51 -04:00
has_many :label_links , as : :target , dependent : :destroy , inverse_of : :target # rubocop:disable Cop/ActiveRecordDependent
2018-04-18 09:41:42 -04:00
has_many :labels , through : :label_links
2017-06-08 11:16:27 -04:00
has_many :todos , as : :target , dependent : :destroy # rubocop:disable Cop/ActiveRecordDependent
2012-08-08 21:40:57 -04:00
2016-09-20 08:43:11 -04:00
has_one :metrics
2017-02-22 17:35:08 -05:00
delegate :name ,
:email ,
2017-02-16 18:41:34 -05:00
:public_email ,
2017-02-22 17:35:08 -05:00
to : :author ,
2017-03-17 14:48:01 -04:00
allow_nil : true ,
2017-02-22 17:35:08 -05:00
prefix : true
2012-10-08 20:10:04 -04:00
validates :author , presence : true
2019-10-07 02:06:10 -04:00
validates :title , presence : true , length : { maximum : TITLE_LENGTH_MAX }
# we validate the description against DESCRIPTION_LENGTH_MAX only for Issuables being created
# to avoid breaking the existing Issuables which may have their descriptions longer
validates :description , length : { maximum : DESCRIPTION_LENGTH_MAX } , allow_blank : true , on : :create
validate :description_max_length_for_new_records_is_valid , on : :update
2019-01-14 05:46:39 -05:00
validate :milestone_is_valid
2012-08-08 21:40:57 -04:00
2019-10-07 02:06:10 -04:00
before_validation :truncate_description_on_import!
2013-08-10 13:25:53 -04:00
scope :authored , - > ( user ) { where ( author_id : user ) }
2015-11-11 09:17:12 -05:00
scope :recent , - > { reorder ( id : :desc ) }
2013-08-08 10:29:31 -04:00
scope :of_projects , - > ( ids ) { where ( project_id : ids ) }
2016-02-22 19:08:00 -05:00
scope :of_milestones , - > ( ids ) { where ( milestone_id : ids ) }
2018-10-03 07:00:03 -04:00
scope :any_milestone , - > { where ( 'milestone_id IS NOT NULL' ) }
2016-05-19 18:30:38 -04:00
scope :with_milestone , - > ( title ) { left_joins_milestones . where ( milestones : { title : title } ) }
2019-11-22 13:06:00 -05:00
scope :any_release , - > { joins_milestone_releases }
scope :with_release , - > ( tag , project_id ) { joins_milestone_releases . where ( milestones : { releases : { tag : tag , project_id : project_id } } ) }
2017-07-19 10:03:50 -04:00
scope :opened , - > { with_state ( :opened ) }
2014-06-04 11:10:53 -04:00
scope :only_opened , - > { with_state ( :opened ) }
2014-02-28 15:43:16 -05:00
scope :closed , - > { with_state ( :closed ) }
2013-08-19 15:10:56 -04:00
2019-04-07 14:35:16 -04:00
# rubocop:disable GitlabSecurity/SqlInjection
# The `to_ability_name` method is not an user input.
scope :assigned , - > do
where ( " EXISTS (SELECT TRUE FROM #{ to_ability_name } _assignees WHERE #{ to_ability_name } _id = #{ to_ability_name } s.id) " )
end
scope :unassigned , - > do
where ( " NOT EXISTS (SELECT TRUE FROM #{ to_ability_name } _assignees WHERE #{ to_ability_name } _id = #{ to_ability_name } s.id) " )
end
scope :assigned_to , - > ( u ) do
where ( " EXISTS (SELECT TRUE FROM #{ to_ability_name } _assignees WHERE user_id = ? AND #{ to_ability_name } _id = #{ to_ability_name } s.id) " , u . id )
end
# rubocop:enable GitlabSecurity/SqlInjection
2016-04-27 11:35:30 -04:00
scope :left_joins_milestones , - > { joins ( " LEFT OUTER JOIN milestones ON #{ table_name } .milestone_id = milestones.id " ) }
2019-11-12 10:06:26 -05:00
scope :order_milestone_due_desc , - > { left_joins_milestones . reorder ( Arel . sql ( 'milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC' ) ) }
scope :order_milestone_due_asc , - > { left_joins_milestones . reorder ( Arel . sql ( 'milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC' ) ) }
2013-08-19 15:10:56 -04:00
2019-11-22 13:06:00 -05:00
scope :without_release , - > do
joins ( " LEFT OUTER JOIN milestone_releases ON #{ table_name } .milestone_id = milestone_releases.milestone_id " )
. where ( 'milestone_releases.release_id IS NULL' )
end
scope :joins_milestone_releases , - > do
2019-12-06 04:06:39 -05:00
joins ( " JOIN milestone_releases ON #{ table_name } .milestone_id = milestone_releases.milestone_id
2019-11-22 13:06:00 -05:00
JOIN releases ON milestone_releases . release_id = releases . id " ).distinct
end
2016-04-27 11:35:30 -04:00
scope :without_label , - > { joins ( " LEFT OUTER JOIN label_links ON label_links.target_type = ' #{ name } ' AND label_links.target_id = #{ table_name } .id " ) . where ( label_links : { id : nil } ) }
2018-10-29 05:50:18 -04:00
scope :any_label , - > { joins ( :label_links ) . group ( :id ) }
2015-11-11 06:50:36 -05:00
scope :join_project , - > { joins ( :project ) }
2017-02-21 18:33:53 -05:00
scope :inc_notes_with_associations , - > { includes ( notes : [ :project , :author , :award_emoji ] ) }
2015-11-11 06:50:36 -05:00
scope :references_project , - > { references ( :project ) }
2016-04-02 08:36:41 -04:00
scope :non_archived , - > { join_project . where ( projects : { archived : false } ) }
2015-11-11 06:50:36 -05:00
2015-10-22 09:40:02 -04:00
attr_mentionable :title , pipeline : :single_line
2016-05-26 07:38:28 -04:00
attr_mentionable :description
participant :author
participant :notes_with_associations
2019-04-07 14:35:16 -04:00
participant :assignees
2016-05-26 07:38:28 -04:00
2015-11-26 10:16:50 -05:00
strip_attributes :title
2016-03-14 16:46:44 -04:00
2016-08-01 11:34:17 -04:00
# We want to use optimistic lock for cases when only title or description are involved
# http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
def locking_enabled?
2019-04-23 05:30:18 -04:00
will_save_change_to_title? || will_save_change_to_description?
2016-08-01 11:34:17 -04:00
end
2017-06-14 16:08:24 -04:00
def allows_multiple_assignees?
false
end
def has_multiple_assignees?
2017-06-20 15:32:49 -04:00
assignees . count > 1
2017-06-14 16:08:24 -04:00
end
2019-01-14 05:46:39 -05:00
private
def milestone_is_valid
2019-11-22 10:06:39 -05:00
errors . add ( :milestone_id , message : " is invalid " ) if respond_to? ( :milestone_id ) && milestone_id . present? && ! milestone_available?
2019-01-14 05:46:39 -05:00
end
2019-10-07 02:06:10 -04:00
def description_max_length_for_new_records_is_valid
if new_record? && description . length > Issuable :: DESCRIPTION_LENGTH_MAX
errors . add ( :description , :too_long , count : Issuable :: DESCRIPTION_LENGTH_MAX )
end
end
def truncate_description_on_import!
self . description = description & . slice ( 0 , Issuable :: DESCRIPTION_LENGTH_MAX ) if importing?
end
2012-08-08 21:40:57 -04:00
end
2018-08-27 08:35:31 -04:00
class_methods do
2016-03-01 10:59:36 -05:00
# Searches for records with a matching title.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
#
# Returns an ActiveRecord::Relation.
2012-08-09 13:45:12 -04:00
def search ( query )
2017-11-24 06:24:24 -05:00
fuzzy_search ( query , [ :title ] )
2012-08-09 13:45:12 -04:00
end
2014-01-14 13:49:32 -05:00
2019-10-18 07:11:44 -04:00
def available_states
@available_states || = STATE_ID_MAP . slice ( * available_state_names )
end
# Available state names used to persist state_id column using state machine
2019-02-13 14:31:14 -05:00
#
# Override this on subclasses if different states are needed
#
2019-10-18 07:11:44 -04:00
# Check MergeRequest.available_states_names for example
def available_state_names
[ :opened , :closed ]
2019-02-13 14:31:14 -05:00
end
2016-03-01 10:59:36 -05:00
# Searches for records with a matching title or description.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
#
# query - The search query as a String
2019-01-13 11:24:31 -05:00
# matched_columns - Modify the scope of the query. 'title', 'description' or joining them with a comma.
2016-03-01 10:59:36 -05:00
#
# Returns an ActiveRecord::Relation.
2019-06-12 20:08:44 -04:00
def full_search ( query , matched_columns : 'title,description' , use_minimum_char_limit : true )
2019-01-13 11:24:31 -05:00
allowed_columns = [ :title , :description ]
matched_columns = matched_columns . to_s . split ( ',' ) . map ( & :to_sym )
matched_columns & = allowed_columns
# Matching title or description if the matched_columns did not contain any allowed columns.
matched_columns = [ :title , :description ] if matched_columns . empty?
2019-06-12 20:08:44 -04:00
fuzzy_search ( query , matched_columns , use_minimum_char_limit : use_minimum_char_limit )
2014-08-27 05:47:30 -04:00
end
Extend CTE search optimisation to projects
When we use the `search` param on an `IssuableFinder`, we can run into
issues. We have trigram indexes to support these searches. On
GitLab.com, we often see Postgres's optimiser prioritise the (global)
trigram indexes over the index on `project_id`. For group and project
searches, we know that it will be quicker to filter by `project_id`
first, as it returns fewer rows in most cases.
For group issues search, we ran into this issue previously, and went
through the following iterations:
1. Use a CTE on the project IDs as an optimisation fence. This prevents
the planner from disregarding the index on `project_id`.
Unfortunately it breaks some types of sorting, like priority and
popularity, as they sort on a joined table.
2. Use a subquery for listing issues, and a CTE for counts. The subquery
- in the case of group lists - didn't help as much as the CTE, but
was faster than not including it. We can safely use a CTE for counts
as they don't have sorting.
Now, however, we're seeing the same issue in a project context. The
subquery doesn't help at all there (it would only return one row, after
all). In an attempt to keep total code complexity under control, this
commit removes the subquery optimisation and applies the CTE
optimisation only for sorts we know that are safe.
This means that for more complicated sorts (like priority and
popularity), the search will continue to be very slow. If this is a
high-priority issue, we can consider introducing further optimisations,
but this finder is already very complicated and additional complexity
has a cost.
The group CTE optimisation is controlled by the same feature flag as
before, `attempt_group_search_optimizations`, which is enabled by
default. The new project CTE optimisation is controlled by a new feature
flag, `attempt_project_search_optimizations`, which is disabled by
default.
2019-04-03 05:46:13 -04:00
def simple_sorts
super . except ( 'name_asc' , 'name_desc' )
end
2018-04-04 05:19:47 -04:00
def sort_by_attribute ( method , excluded_labels : [ ] )
2017-09-22 20:46:53 -04:00
sorted =
case method . to_s
2019-08-15 06:56:33 -04:00
when 'downvotes_desc' then order_downvotes_desc
when 'label_priority' , 'label_priority_asc' then order_labels_priority ( excluded_labels : excluded_labels )
when 'label_priority_desc' then order_labels_priority ( 'DESC' , excluded_labels : excluded_labels )
when 'milestone' , 'milestone_due_asc' then order_milestone_due_asc
when 'milestone_due_desc' then order_milestone_due_desc
when 'popularity_asc' then order_upvotes_asc
when 'popularity' , 'popularity_desc' , 'upvotes_desc' then order_upvotes_desc
when 'priority' , 'priority_asc' then order_due_date_and_labels_priority ( excluded_labels : excluded_labels )
when 'priority_desc' then order_due_date_and_labels_priority ( 'DESC' , excluded_labels : excluded_labels )
2017-09-22 20:46:53 -04:00
else order_by ( method )
end
2016-06-23 09:15:46 -04:00
# Break ties with the ID column for pagination
2018-08-06 15:38:37 -04:00
sorted . with_order_id_desc
2014-01-14 13:49:32 -05:00
end
2016-05-12 03:23:21 -04:00
2018-12-06 11:57:19 -05:00
def order_due_date_and_labels_priority ( direction = 'ASC' , excluded_labels : [ ] )
2017-03-10 06:10:48 -05:00
# The order_ methods also modify the query in other ways:
#
# - For milestones, we add a JOIN.
# - For label priority, we change the SELECT, and add a GROUP BY.#
#
# After doing those, we need to reorder to the order we want. The existing
# ORDER BYs won't work because:
#
# 1. We need milestone due date first.
# 2. We can't ORDER BY a column that isn't in the GROUP BY and doesn't
# have an aggregate function applied, so we do a useless MIN() instead.
#
milestones_due_date = 'MIN(milestones.due_date)'
2017-06-21 09:48:12 -04:00
order_milestone_due_asc
. order_labels_priority ( excluded_labels : excluded_labels , extra_select_columns : [ milestones_due_date ] )
2018-12-06 11:57:19 -05:00
. reorder ( Gitlab :: Database . nulls_last_order ( milestones_due_date , direction ) ,
Gitlab :: Database . nulls_last_order ( 'highest_priority' , direction ) )
2017-03-10 06:10:48 -05:00
end
2018-12-06 11:57:19 -05:00
def order_labels_priority ( direction = 'ASC' , excluded_labels : [ ] , extra_select_columns : [ ] )
2016-10-17 16:03:06 -04:00
params = {
target_type : name ,
target_column : " #{ table_name } .id " ,
2016-10-19 09:49:07 -04:00
project_column : " #{ table_name } . #{ project_foreign_key } " ,
2016-10-17 16:03:06 -04:00
excluded_labels : excluded_labels
}
highest_priority = highest_label_priority ( params ) . to_sql
2016-07-26 17:21:20 -04:00
2017-03-10 06:10:48 -05:00
select_columns = [
" #{ table_name } .* " ,
" ( #{ highest_priority } ) AS highest_priority "
] + extra_select_columns
2017-06-21 09:48:12 -04:00
select ( select_columns . join ( ', ' ) )
. group ( arel_table [ :id ] )
2018-12-06 11:57:19 -05:00
. reorder ( Gitlab :: Database . nulls_last_order ( 'highest_priority' , direction ) )
2016-05-13 11:26:18 -04:00
end
2016-05-27 15:53:20 -04:00
def with_label ( title , sort = nil )
2016-05-12 03:23:21 -04:00
if title . is_a? ( Array ) && title . size > 1
2016-05-31 18:33:46 -04:00
joins ( :labels ) . where ( labels : { title : title } ) . group ( * grouping_columns ( sort ) ) . having ( " COUNT(DISTINCT labels.title) = #{ title . size } " )
2016-05-12 03:23:21 -04:00
else
joins ( :labels ) . where ( labels : { title : title } )
end
end
2016-05-27 15:53:20 -04:00
# Includes table keys in group by clause when sorting
# preventing errors in postgres
#
# Returns an array of arel columns
2016-05-31 18:33:46 -04:00
def grouping_columns ( sort )
grouping_columns = [ arel_table [ :id ] ]
2016-05-27 15:53:20 -04:00
2017-09-22 20:46:53 -04:00
if %w( milestone_due_desc milestone_due_asc milestone ) . include? ( sort )
2016-05-27 15:53:20 -04:00
milestone_table = Milestone . arel_table
2016-05-31 18:33:46 -04:00
grouping_columns << milestone_table [ :id ]
grouping_columns << milestone_table [ :due_date ]
2016-05-27 15:53:20 -04:00
end
2016-05-31 18:33:46 -04:00
grouping_columns
2016-05-27 15:53:20 -04:00
end
2016-10-26 13:34:06 -04:00
def to_ability_name
model_name . singular
end
2018-02-28 02:48:23 -05:00
def parent_class
:: Project
end
2012-08-08 21:40:57 -04:00
end
2019-10-18 07:11:44 -04:00
def state
self . class . available_states . key ( state_id )
end
def state = ( value )
self . state_id = self . class . available_states [ value ]
end
2019-09-03 17:29:55 -04:00
def resource_parent
project
end
2019-04-18 04:33:05 -04:00
def milestone_available?
project_id == milestone & . project_id || project . ancestors_upto . compact . include? ( milestone & . group )
end
2019-04-07 14:35:16 -04:00
def assignee_or_author? ( user )
author_id == user . id || assignees . exists? ( user . id )
end
2012-08-08 21:40:57 -04:00
def today?
Date . today == created_at . to_date
end
def new?
today? && created_at == updated_at
end
2012-10-09 18:25:29 -04:00
2015-10-13 03:41:46 -04:00
def open?
2017-07-19 10:03:50 -04:00
opened?
2015-10-13 03:41:46 -04:00
end
2018-07-16 09:35:19 -04:00
def overdue?
return false unless respond_to? ( :due_date )
due_date . try ( :past? ) || false
end
2016-06-01 05:23:09 -04:00
def user_notes_count
2016-06-06 16:19:39 -04:00
if notes . loaded?
# Use the in-memory association to select and count to avoid hitting the db
notes . to_a . count { | note | ! note . system? }
else
# do the count query
notes . user . count
end
2016-06-01 05:23:09 -04:00
end
2016-10-31 15:19:14 -04:00
def subscribed_without_subscriptions? ( user , project )
2016-03-01 11:33:13 -05:00
participants ( user ) . include? ( user )
end
2017-11-21 12:13:07 -05:00
def to_hook_data ( user , old_associations : { } )
2017-09-15 13:08:27 -04:00
changes = previous_changes
2017-09-19 13:23:15 -04:00
2019-01-25 10:41:21 -05:00
if old_associations
old_labels = old_associations . fetch ( :labels , [ ] )
old_assignees = old_associations . fetch ( :assignees , [ ] )
2017-09-15 13:08:27 -04:00
2019-01-25 10:41:21 -05:00
if old_labels != labels
changes [ :labels ] = [ old_labels . map ( & :hook_attrs ) , labels . map ( & :hook_attrs ) ]
2017-09-19 13:23:15 -04:00
end
2015-10-17 18:11:36 -04:00
2019-01-25 10:41:21 -05:00
if old_assignees != assignees
2019-04-07 14:35:16 -04:00
changes [ :assignees ] = [ old_assignees . map ( & :hook_attrs ) , assignees . map ( & :hook_attrs ) ]
2019-01-25 10:41:21 -05:00
end
if self . respond_to? ( :total_time_spent )
old_total_time_spent = old_associations . fetch ( :total_time_spent , nil )
2017-11-21 12:13:07 -05:00
2019-01-25 10:41:21 -05:00
if old_total_time_spent != total_time_spent
changes [ :total_time_spent ] = [ old_total_time_spent , total_time_spent ]
end
2017-11-21 12:13:07 -05:00
end
2017-11-14 12:55:00 -05:00
end
2017-09-19 13:23:15 -04:00
Gitlab :: HookData :: IssuableBuilder . new ( self ) . build ( user : user , changes : changes )
2013-12-03 04:31:56 -05:00
end
2014-07-30 10:17:29 -04:00
2016-06-02 07:17:54 -04:00
def labels_array
labels . to_a
end
2014-07-30 10:17:29 -04:00
def label_names
labels . order ( 'title ASC' ) . pluck ( :title )
end
2015-07-24 15:31:15 -04:00
# Convert this Issuable class name to a format usable by Ability definitions
#
# Examples:
#
# issuable.class # => MergeRequest
# issuable.to_ability_name # => "merge_request"
def to_ability_name
2016-10-26 13:34:06 -04:00
self . class . to_ability_name
2015-07-24 15:31:15 -04:00
end
2015-12-24 17:03:54 -05:00
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
'Author' = > author . try ( :name ) ,
2019-04-07 14:35:16 -04:00
'Assignee' = > assignee_list
2015-12-24 17:03:54 -05:00
}
end
2019-04-07 14:35:16 -04:00
def assignee_list
assignees . map ( & :name ) . to_sentence
end
def assignee_username_list
assignees . map ( & :username ) . to_sentence
end
2015-10-13 05:49:01 -04:00
def notes_with_associations
2016-06-06 16:19:39 -04:00
# If A has_many Bs, and B has_many Cs, and you do
# `A.includes(b: :c).each { |a| a.b.includes(:c) }`, sadly ActiveRecord
# will do the inclusion again. So, we check if all notes in the relation
# already have their authors loaded (possibly because the scope
# `inc_notes_with_associations` was used) and skip the inclusion if that's
# the case.
2016-06-21 01:26:43 -04:00
includes = [ ]
includes << :author unless notes . authors_loaded?
includes << :award_emoji unless notes . award_emojis_loaded?
2018-01-11 11:34:01 -05:00
2016-06-21 01:26:43 -04:00
if includes . any?
notes . includes ( includes )
else
notes
end
2015-10-13 05:49:01 -04:00
end
2015-10-22 11:18:59 -04:00
def updated_tasks
Taskable . get_updated_tasks ( old_content : previous_changes [ 'description' ] . first ,
new_content : description )
2014-11-05 15:45:18 -05:00
end
2016-03-18 09:48:55 -04:00
##
# Method that checks if issuable can be moved to another project.
#
# Should be overridden if issuable can be moved.
#
def can_move? ( * )
false
end
2016-09-20 08:43:11 -04:00
2017-08-15 09:21:27 -04:00
##
# Override in issuable specialization
#
2017-08-29 09:46:40 -04:00
def first_contribution?
2017-08-15 09:21:27 -04:00
false
2017-07-29 11:04:42 -04:00
end
2017-11-21 14:25:37 -05:00
2017-12-07 12:41:30 -05:00
def ensure_metrics
self . metrics || create_metrics
end
2017-11-21 14:25:37 -05:00
##
2018-10-30 06:53:01 -04:00
# Overridden in MergeRequest
2017-11-21 14:25:37 -05:00
#
def wipless_title_changed ( old_title )
old_title != title
end
2019-07-18 14:52:10 -04:00
##
# Overridden on EE module
#
def supports_milestone?
respond_to? ( :milestone_id )
end
2012-08-08 21:26:56 -04:00
end
2019-09-13 09:26:31 -04:00
Issuable . prepend_if_ee ( 'EE::Issuable' ) # rubocop: disable Cop/InjectEnterpriseEditionModule
Issuable :: ClassMethods . prepend_if_ee ( 'EE::Issuable::ClassMethods' )