Commit 3416ee42 authored by Alex Kalderimis's avatar Alex Kalderimis

Add wiki page metadata models

Adds wiki page slug model:

A slug represents a historical set of slugs that identify wiki pages. At
most one can be canonical at any one time, per page.

Adds wiki page meta model:

The wiki page metadata model contains the current title, and has access
to a collection of slugs, at most one of which may be canonical at any
one point (all others are historic).

Rather than polluting the model namespace, and to clarify the meaning of
the class names, these classes are placed under the `WikiPage`
namespace.

Adds adds wiki page meta/spec factories with sequences for use in the
specs, guaranteeing unique titles and slugs
parent 2af8ad0f
......@@ -29,6 +29,12 @@ class WikiPage
alias_method :==, :eql?
def meta
raise 'Metadata only available for valid pages' unless valid?
@meta ||= WikiPage::Meta.find_or_create(slug, self)
end
# Sorts and groups pages by directory.
#
# pages - an array of WikiPage objects.
......@@ -236,6 +242,7 @@ class WikiPage
end
save do
@meta = nil if title_changed?
wiki.update_page(
@page,
content: content,
......
# frozen_string_literal: true
class WikiPage
class Meta < ApplicationRecord
include Gitlab::Utils::StrongMemoize
self.table_name = 'wiki_page_meta'
belongs_to :project
has_many :slugs, class_name: 'WikiPage::Slug', foreign_key: 'wiki_page_meta_id', inverse_of: :wiki_page_meta
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
validates :title, presence: true
validates :project_id, presence: true
alias_method :resource_parent, :project
# 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
#
# As with all `find_or_create` methods, this one raises errors on
# validation issues.
def self.find_or_create(last_known_slug, wiki_page)
project = wiki_page.wiki.project
meta = find_by_canonical_slug(last_known_slug, project) || create(title: wiki_page.title, project_id: project.id)
meta.update_wiki_page_attributes(wiki_page)
meta.insert_slugs([last_known_slug, wiki_page.slug], wiki_page.slug)
meta.canonical_slug = wiki_page.slug
meta
end
def update_wiki_page_attributes(page)
update_column(:title, page.title) unless page.title == title
end
def insert_slugs(strings, canonical)
slug_attrs = strings.uniq.map do |slug|
{ wiki_page_meta_id: id, slug: slug }
end
slugs.insert_all(slug_attrs)
end
def self.find_by_canonical_slug(canonical_slug, project)
meta = joins(:slugs).find_by(project_id: project.id,
wiki_page_slugs: { canonical: true, slug: canonical_slug })
# Prevent queries for canonical_slug
meta.instance_variable_set(:@canonical_slug, canonical_slug) if meta
meta
end
def canonical_slug
strong_memoize(:canonical_slug) { slugs.canonical.first&.slug }
end
def canonical_slug=(slug)
return if @canonical_slug == slug
if persisted?
slugs.update_all(canonical: false)
page_slug = slugs.create_with(canonical: true).find_or_create_by(slug: slug)
page_slug.update_column(:canonical, true) unless page_slug.canonical?
else
slugs.new(slug: slug, canonical: true)
end
@canonical_slug = slug
end
end
end
# frozen_string_literal: true
class WikiPage
class Slug < ApplicationRecord
self.table_name = 'wiki_page_slugs'
belongs_to :wiki_page_meta, class_name: 'WikiPage::Meta', inverse_of: :slugs
validates :slug, presence: true, uniqueness: { scope: :wiki_page_meta_id }
validate :only_one_slug_can_be_canonical_per_meta_record
scope :canonical, -> { where(canonical: true) }
private
def only_one_slug_can_be_canonical_per_meta_record
return unless canonical?
if other_slugs.canonical.exists?
errors.add(:canonical, 'Only one slug can be canonical per wiki metadata record')
end
end
def other_slugs
self.class.unscoped.where(wiki_page_meta_id: wiki_page_meta_id)
end
end
end
This diff is collapsed.
# frozen_string_literal: true
require 'spec_helper'
describe WikiPage::Slug do
let_it_be(:meta) { create(:wiki_page_meta) }
describe 'Associations' do
it { is_expected.to belong_to(:wiki_page_meta) }
it 'refers correctly to the wiki_page_meta' do
created = create(:wiki_page_slug, wiki_page_meta: meta)
expect(created.reload.wiki_page_meta).to eq(meta)
end
end
describe 'scopes' do
describe 'canonical' do
subject { described_class.canonical }
context 'there are no slugs' do
it { is_expected.to be_empty }
end
context 'there are some non-canonical slugs' do
before do
create_list(:wiki_page_slug, 3)
end
it { is_expected.to be_empty }
end
context 'there is at least one canonical slugs' do
before do
create(:wiki_page_slug, canonical: true)
end
it { is_expected.not_to be_empty }
end
end
end
describe 'Validations' do
let(:canonical) { false }
subject do
build(:wiki_page_slug, canonical: canonical, wiki_page_meta: meta)
end
it { is_expected.to validate_presence_of(:slug) }
it { is_expected.to validate_uniqueness_of(:slug).scoped_to(:wiki_page_meta_id) }
describe 'only_one_slug_can_be_canonical_per_meta_record' do
context 'there are no other slugs' do
it { is_expected.to be_valid }
context 'the current slug is canonical' do
let(:canonical) { true }
it { is_expected.to be_valid }
end
end
context 'there are other slugs, but they are not canonical' do
before do
create_list(:wiki_page_slug, 3, wiki_page_meta: meta)
end
it { is_expected.to be_valid }
context 'the current slug is canonical' do
let(:canonical) { true }
it { is_expected.to be_valid }
end
end
context 'there is already a canonical slug' do
before do
create(:wiki_page_slug, canonical: true, wiki_page_meta: meta)
end
it { is_expected.to be_valid }
context 'the current slug is canonical' do
let(:canonical) { true }
it { is_expected.not_to be_valid }
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment