diff options
Diffstat (limited to 'app/models/milestone.rb')
-rw-r--r-- | app/models/milestone.rb | 195 |
1 files changed, 18 insertions, 177 deletions
diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 4ccfe314526..b5e4f62792e 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -1,88 +1,37 @@ # frozen_string_literal: true class Milestone < ApplicationRecord - # Represents a "No Milestone" state used for filtering Issues and Merge - # Requests that have no milestone assigned. - MilestoneStruct = Struct.new(:title, :name, :id) do - # Ensure these models match the interface required for exporting - def serializable_hash(_opts = {}) - { title: title, name: name, id: id } - end - end - - None = MilestoneStruct.new('No Milestone', 'No Milestone', 0) - Any = MilestoneStruct.new('Any Milestone', '', -1) - Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2) - Started = MilestoneStruct.new('Started', '#started', -3) - - include CacheMarkdownField - include AtomicInternalId - include IidRoutes include Sortable include Referable - include StripAttribute + include Timebox include Milestoneish include FromUnion include Importable - include Gitlab::SQL::Pattern prepend_if_ee('::EE::Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule - cache_markdown_field :title, pipeline: :single_line - cache_markdown_field :description - - belongs_to :project - belongs_to :group - has_many :milestone_releases has_many :releases, through: :milestone_releases has_internal_id :iid, scope: :project, track_if: -> { !importing? }, init: ->(s) { s&.project&.milestones&.maximum(:iid) } has_internal_id :iid, scope: :group, track_if: -> { !importing? }, init: ->(s) { s&.group&.milestones&.maximum(:iid) } - has_many :issues - has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues - has_many :merge_requests has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent - scope :of_projects, ->(ids) { where(project_id: ids) } - scope :of_groups, ->(ids) { where(group_id: ids) } scope :active, -> { with_state(:active) } - scope :closed, -> { with_state(:closed) } - scope :for_projects, -> { where(group: nil).includes(:project) } scope :started, -> { active.where('milestones.start_date <= CURRENT_DATE') } - - 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? - - where(project_id: projects).or(where(group_id: groups)) - end - - scope :within_timeframe, -> (start_date, end_date) do - where('start_date is not NULL or due_date is not NULL') - .where('start_date is NULL or start_date <= ?', end_date) - .where('due_date is NULL or due_date >= ?', start_date) + scope :not_started, -> { active.where('milestones.start_date > CURRENT_DATE') } + scope :not_upcoming, -> do + active + .where('milestones.due_date <= CURRENT_DATE') + .order(:project_id, :group_id, :due_date) end scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) } scope :reorder_by_due_date_asc, -> { reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) } - validates :group, presence: true, unless: :project - validates :project, presence: true, unless: :group - validates :title, presence: true - - validate :uniqueness_of_title, if: :title_changed? - validate :milestone_type_check - validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? } - validate :dates_within_4_digits validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") } - strip_attributes :title - state_machine :state, initial: :active do event :close do transition active: :closed @@ -97,52 +46,6 @@ class Milestone < ApplicationRecord state :active end - alias_attribute :name, :title - - class << self - # Searches for milestones with a matching title or description. - # - # This method uses ILIKE on PostgreSQL and LIKE on MySQL. - # - # query - The search query as a String - # - # Returns an ActiveRecord::Relation. - def search(query) - fuzzy_search(query, [:title, :description]) - end - - # Searches for milestones 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. - def search_title(query) - fuzzy_search(query, [:title]) - end - - def filter_by_state(milestones, state) - case state - when 'closed' then milestones.closed - when 'all' then milestones - else milestones.active - end - end - - def count_by_state - reorder(nil).group(:state).count - end - - def predefined_id?(id) - [Any.id, None.id, Upcoming.id, Started.id].include?(id) - end - - def predefined?(milestone) - predefined_id?(milestone&.id) - end - end - def self.reference_prefix '%' end @@ -220,7 +123,7 @@ class Milestone < ApplicationRecord end ## - # Returns the String necessary to reference this Milestone in Markdown. Group + # Returns the String necessary to reference a Milestone in Markdown. Group # milestones only support name references, and do not support cross-project # references. # @@ -248,10 +151,6 @@ class Milestone < ApplicationRecord self.class.reference_prefix + self.title end - def milestoneish_id - id - end - def for_display self end @@ -264,62 +163,24 @@ class Milestone < ApplicationRecord nil end - def title=(value) - write_attribute(:title, sanitize_title(value)) if value.present? - end + # 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 safe_title - title.to_slug.normalize.to_s - end - - def resource_parent - group || project - end - - def to_ability_name - model_name.singular - end - - def group_milestone? - group_id.present? - end - - def project_milestone? - project_id.present? - end - - def merge_requests_enabled? + def parent 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? + group + else + project end end private - # Milestone titles must be unique across project milestones and group milestones - def uniqueness_of_title - if project - relation = Milestone.for_projects_and_groups([project_id], [project.group&.id]) - elsif group - relation = Milestone.for_projects_and_groups(group.projects.select(:id), [group.id]) - end - - title_exists = relation.find_by_title(title) - errors.add(:title, _("already being used for another group or project milestone.")) if title_exists - end - - # Milestone should be either a project milestone or a group milestone - def milestone_type_check - if 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.")) - end - end - def milestone_format_reference(format = :iid) raise ArgumentError, _('Unknown format') unless [:iid, :name].include?(format) @@ -334,26 +195,6 @@ class Milestone < ApplicationRecord end end - def sanitize_title(value) - CGI.unescape_html(Sanitize.clean(value.to_s)) - end - - def start_date_should_be_less_than_due_date - if due_date <= start_date - errors.add(:due_date, _("must be greater than start date")) - end - end - - def dates_within_4_digits - if start_date && start_date > Date.new(9999, 12, 31) - errors.add(:start_date, _("date must not be after 9999-12-31")) - end - - if due_date && due_date > Date.new(9999, 12, 31) - errors.add(:due_date, _("date must not be after 9999-12-31")) - end - end - def issues_finder_params { project_id: project_id, group_id: group_id, include_subgroups: group_id.present? }.compact end |