summaryrefslogtreecommitdiff
path: root/app/models/milestone.rb
diff options
context:
space:
mode:
Diffstat (limited to 'app/models/milestone.rb')
-rw-r--r--app/models/milestone.rb91
1 files changed, 87 insertions, 4 deletions
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index ff4fadb0f13..da07d8dd9fc 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -1,11 +1,13 @@
# frozen_string_literal: true
class Milestone < ApplicationRecord
+ include AtomicInternalId
include Sortable
include Timebox
include Milestoneish
include FromUnion
include Importable
+ include IidRoutes
prepend_mod_with('Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule
@@ -13,6 +15,9 @@ class Milestone < ApplicationRecord
ALL = [::Timebox::None, ::Timebox::Any, ::Timebox::Started, ::Timebox::Upcoming].freeze
end
+ belongs_to :project
+ belongs_to :group
+
has_many :milestone_releases
has_many :releases, through: :milestone_releases
@@ -30,13 +35,28 @@ class Milestone < ApplicationRecord
.order(:project_id, :group_id, :due_date)
end
+ scope :of_projects, ->(ids) { where(project_id: ids) }
+ scope :for_projects, -> { where(group: nil).includes(:project) }
+ scope :for_projects_and_groups, -> (projects, groups) do
+ projects = projects.compact if projects.is_a? Array
+ projects = [] if projects.nil?
+
+ groups = groups.compact if groups.is_a? Array
+ groups = [] if groups.nil?
+
+ from_union([where(project_id: projects), where(group_id: groups)], remove_duplicates: false)
+ end
+
scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) }
scope :reorder_by_due_date_asc, -> { reorder(arel_table[:due_date].asc.nulls_last) }
scope :with_api_entity_associations, -> { preload(project: [:project_feature, :route, namespace: :route]) }
scope :order_by_dates_and_title, -> { order(due_date: :asc, start_date: :asc, title: :asc) }
+ validates :group, presence: true, unless: :project
+ validates :project, presence: true, unless: :group
validates :title, presence: true
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
+ validate :parent_type_check
validate :uniqueness_of_title, if: :title_changed?
state_machine :state, initial: :active do
@@ -176,10 +196,18 @@ class Milestone < ApplicationRecord
# TODO: remove after all code paths use `timebox_id`
# https://gitlab.com/gitlab-org/gitlab/-/issues/215688
alias_method :milestoneish_id, :timebox_id
- # TODO: remove after all code paths use (group|project)_timebox?
- # https://gitlab.com/gitlab-org/gitlab/-/issues/215690
- alias_method :group_milestone?, :group_timebox?
- alias_method :project_milestone?, :project_timebox?
+
+ def group_milestone?
+ group_id.present?
+ end
+
+ def project_milestone?
+ project_id.present?
+ end
+
+ def resource_parent
+ group || project
+ end
def parent
if group_milestone?
@@ -193,8 +221,63 @@ class Milestone < ApplicationRecord
group_milestone? && parent.subgroup?
end
+ def merge_requests_enabled?
+ if group_milestone?
+ # Assume that groups have at least one project with merge requests enabled.
+ # Otherwise, we would need to load all of the projects from the database.
+ true
+ elsif project_milestone?
+ project&.merge_requests_enabled?
+ end
+ end
+
+ ##
+ # Returns the String necessary to reference a milestone in Markdown. Group
+ # milestones only support name references, and do not support cross-project
+ # references.
+ #
+ # format - Symbol format to use (default: :iid, optional: :name)
+ #
+ # Examples:
+ #
+ # Milestone.first.to_reference # => "%1"
+ # Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-foss%1"
+ #
+ def to_reference(from = nil, format: :name, full: false)
+ format_reference = timebox_format_reference(format)
+ reference = "#{self.class.reference_prefix}#{format_reference}"
+
+ if project
+ "#{project.to_reference_base(from, full: full)}#{reference}"
+ else
+ reference
+ end
+ end
+
private
+ def timebox_format_reference(format = :iid)
+ raise ArgumentError, _('Unknown format') unless [:iid, :name].include?(format)
+
+ if group_milestone? && format == :iid
+ raise ArgumentError, _('Cannot refer to a group milestone by an internal id!')
+ end
+
+ if format == :name && !name.include?('"')
+ %("#{name}")
+ else
+ iid
+ end
+ end
+
+ # Milestone should be either a project milestone or a group milestone
+ def parent_type_check
+ return unless group_id && project_id
+
+ field = project_id_changed? ? :project_id : :group_id
+ errors.add(field, _("milestone should belong either to a project or a group.") % { timebox_name: timebox_name })
+ end
+
def issues_finder_params
{ project_id: project_id, group_id: group_id, include_subgroups: group_id.present? }.compact
end