gitlab-org--gitlab-foss/app/models/concerns/has_wiki_page_meta_attribut...

165 lines
5.0 KiB
Ruby

# frozen_string_literal: true
module HasWikiPageMetaAttributes
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
CanonicalSlugConflictError = Class.new(ActiveRecord::RecordInvalid)
WikiPageInvalid = Class.new(ArgumentError)
included do
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
validates :title, length: { maximum: 255 }, allow_nil: false
validate :no_two_metarecords_in_same_container_can_have_same_canonical_slug
scope :with_canonical_slug, ->(slug) do
slug_table_name = klass.reflect_on_association(:slugs).table_name
joins(:slugs).where(slug_table_name => { canonical: true, slug: slug })
end
end
class_methods do
# Return the (updated) WikiPage::Meta record for a given wiki page
#
# If none is found, then a new record is created, and its fields are set
# to reflect the wiki_page passed.
#
# @param [String] last_known_slug
# @param [WikiPage] wiki_page
#
# This method raises errors on validation issues.
def find_or_create(last_known_slug, wiki_page)
raise WikiPageInvalid unless wiki_page.valid?
container = wiki_page.wiki.container
known_slugs = [last_known_slug, wiki_page.slug].compact.uniq
raise 'No slugs found! This should not be possible.' if known_slugs.empty?
transaction do
updates = wiki_page_updates(wiki_page)
found = find_by_canonical_slug(known_slugs, container)
meta = found || create!(updates.merge(container_attrs(container)))
meta.update_state(found.nil?, known_slugs, wiki_page, updates)
# We don't need to run validations here, since find_by_canonical_slug
# guarantees that there is no conflict in canonical_slug, and DB
# constraints on title and project_id/group_id enforce our other invariants
# This saves us a query.
meta
end
end
def find_by_canonical_slug(canonical_slug, container)
meta, conflict = with_canonical_slug(canonical_slug)
.where(container_attrs(container))
.limit(2)
if conflict.present?
meta.errors.add(:canonical_slug, 'Duplicate value found')
raise CanonicalSlugConflictError, meta
end
meta
end
private
def wiki_page_updates(wiki_page)
last_commit_date = wiki_page.version_commit_timestamp || Time.now.utc
{
title: wiki_page.title,
created_at: last_commit_date,
updated_at: last_commit_date
}
end
def container_key
raise NotImplementedError
end
def container_attrs(container)
{ container_key => container.id }
end
end
def canonical_slug
strong_memoize(:canonical_slug) { slugs.canonical.take&.slug }
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def canonical_slug=(slug)
return if @canonical_slug == slug
if persisted?
transaction do
slugs.canonical.update_all(canonical: false)
page_slug = slugs.create_with(canonical: true).find_or_create_by(slug: slug)
page_slug.update_columns(canonical: true) unless page_slug.canonical?
end
else
slugs.new(slug: slug, canonical: true)
end
@canonical_slug = slug
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
def update_state(created, known_slugs, wiki_page, updates)
update_wiki_page_attributes(updates)
insert_slugs(known_slugs, created, wiki_page.slug)
self.canonical_slug = wiki_page.slug
end
private
def update_wiki_page_attributes(updates)
# Remove all unnecessary updates:
updates.delete(:updated_at) if updated_at == updates[:updated_at]
updates.delete(:created_at) if created_at <= updates[:created_at]
updates.delete(:title) if title == updates[:title]
update_columns(updates) unless updates.empty?
end
def insert_slugs(strings, is_new, canonical_slug)
creation = Time.current.utc
slug_attrs = strings.map do |slug|
slug_attributes(slug, canonical_slug, is_new, creation)
end
slugs.insert_all(slug_attrs) unless !is_new && slug_attrs.size == 1
@canonical_slug = canonical_slug if is_new || strings.size == 1 # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def slug_attributes(slug, canonical_slug, is_new, creation)
{
slug: slug,
canonical: (is_new && slug == canonical_slug),
created_at: creation,
updated_at: creation
}.merge(slug_meta_attributes)
end
def slug_meta_attributes
{ self.association(:slugs).reflection.foreign_key => id }
end
def no_two_metarecords_in_same_container_can_have_same_canonical_slug
container_id = attributes[self.class.container_key.to_s]
return unless container_id.present? && canonical_slug.present?
offending = self.class.with_canonical_slug(canonical_slug).where(self.class.container_key => container_id)
offending = offending.where.not(id: id) if persisted?
if offending.exists?
errors.add(:canonical_slug, 'each page in a wiki must have a distinct canonical slug')
end
end
end