252 lines
7.3 KiB
Ruby
252 lines
7.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class WikiPage
|
|
class Meta < ApplicationRecord
|
|
include Gitlab::Utils::StrongMemoize
|
|
include Mentionable
|
|
include Noteable
|
|
include Subscribable
|
|
include Todoable
|
|
|
|
self.table_name = 'wiki_page_meta'
|
|
|
|
WikiPageInvalid = Class.new(ArgumentError)
|
|
|
|
belongs_to :project, optional: true
|
|
belongs_to :namespace, optional: true
|
|
|
|
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent -- Technical debt
|
|
has_many :slugs, class_name: 'WikiPage::Slug', foreign_key: 'wiki_page_meta_id', inverse_of: :wiki_page_meta
|
|
has_many :notes, as: :noteable
|
|
has_many :todos, as: :target
|
|
has_many :user_mentions, class_name: 'Wikis::UserMention', foreign_key: 'wiki_page_meta_id',
|
|
inverse_of: :wiki_page_meta
|
|
|
|
validates :title, length: { maximum: 255 }, allow_nil: false
|
|
validate :no_two_metarecords_in_same_container_can_have_same_canonical_slug
|
|
validate :project_or_namespace_present?
|
|
|
|
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
|
|
scope :for_project, ->(project) do
|
|
where(project: project)
|
|
end
|
|
|
|
delegate :wiki, to: :container
|
|
|
|
class << self
|
|
# 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)
|
|
return unless canonical_slug.present? && container.present?
|
|
|
|
meta, conflict = with_canonical_slug(canonical_slug)
|
|
.where(container_attrs(container))
|
|
.limit(2)
|
|
|
|
if conflict.present?
|
|
# Ensure the conflict record will be the orphaned record when doing a page update
|
|
if canonical_slug.size > 1
|
|
old_slug, _new_slug = canonical_slug
|
|
|
|
meta, conflict = conflict, meta if conflict.canonical_slug == old_slug
|
|
end
|
|
|
|
transaction(requires_new: false) do
|
|
conflict.todos.each_batch do |batch|
|
|
batch.update_all(target_id: meta.id)
|
|
end
|
|
|
|
conflict.destroy
|
|
end
|
|
end
|
|
|
|
meta
|
|
end
|
|
|
|
def declarative_policy_class
|
|
'WikiPagePolicy'
|
|
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_attrs(container)
|
|
return { project_id: container.id } if container.is_a?(Project)
|
|
|
|
{ namespace_id: container.id } if container.is_a?(Namespace)
|
|
end
|
|
end
|
|
|
|
def wiki_page
|
|
wiki.find_page(canonical_slug, load_content: true)
|
|
end
|
|
|
|
def container
|
|
project || namespace
|
|
end
|
|
|
|
def container=(value)
|
|
self.project = value if value.is_a?(Project)
|
|
self.namespace = value if value.is_a?(Namespace)
|
|
end
|
|
|
|
def resource_parent
|
|
container
|
|
end
|
|
|
|
def for_group_wiki?
|
|
namespace_id.present?
|
|
end
|
|
|
|
def container_key
|
|
for_group_wiki? ? :namespace_id : :project_id
|
|
end
|
|
|
|
def canonical_slug
|
|
slugs.canonical.take&.slug
|
|
end
|
|
strong_memoize_attr :canonical_slug
|
|
|
|
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
|
|
|
|
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
|
|
|
|
def gfm_reference(from = nil)
|
|
"#{container.class.name.downcase} wiki page #{to_reference(from)}"
|
|
end
|
|
|
|
def to_reference(_from = nil)
|
|
return "[[#{canonical_slug}]]" unless for_group_wiki?
|
|
|
|
canonical_slug
|
|
end
|
|
|
|
def reference_link_text
|
|
canonical_slug
|
|
end
|
|
|
|
# Used by app/policies/todo_policy.rb
|
|
def readable_by?(user)
|
|
Ability.allowed?(user, :read_wiki, self)
|
|
end
|
|
|
|
def to_ability_name
|
|
'wiki_page'
|
|
end
|
|
|
|
private
|
|
|
|
def project_or_namespace_present?
|
|
return unless (project_id.nil? && namespace_id.nil?) || (project_id.present? && namespace_id.present?)
|
|
|
|
errors.add(:base, s_('Wiki|WikiPage::Meta should belong to either project or namespace.'))
|
|
end
|
|
|
|
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
|
|
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
|
|
{ association(:slugs).reflection.foreign_key => id }
|
|
end
|
|
|
|
def no_two_metarecords_in_same_container_can_have_same_canonical_slug
|
|
container_id = attributes[container_key.to_s]
|
|
|
|
return unless container_id.present? && canonical_slug.present?
|
|
|
|
offending = self.class.with_canonical_slug(canonical_slug).where(container_key => container_id)
|
|
offending = offending.where.not(id: id) if persisted?
|
|
|
|
return unless offending.exists?
|
|
|
|
errors.add(:canonical_slug, 'each page in a wiki must have a distinct canonical slug')
|
|
end
|
|
end
|
|
end
|