diff options
Diffstat (limited to 'app/models/milestone.rb')
-rw-r--r-- | app/models/milestone.rb | 91 |
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 |