165 lines
5 KiB
Ruby
165 lines
5 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.new(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
|