Skip to content

Commit a0ca3f9

Browse files
authored
Merge pull request #3554 from AlchemyCMS/move-page-meta-data-to-page-version
feat: Move page metadata to PageVersion
2 parents 7ecc5d3 + 7e01082 commit a0ca3f9

File tree

23 files changed

+583
-81
lines changed

23 files changed

+583
-81
lines changed

app/models/alchemy/page.rb

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
# id :integer not null, primary key
88
# name :string
99
# urlname :string
10-
# title :string
10+
# title :string (deprecated - use draft_version.title)
1111
# language_code :string
1212
# language_root :boolean
1313
# page_layout :string
14-
# meta_keywords :text
15-
# meta_description :text
14+
# meta_keywords :text (deprecated - use draft_version.meta_keywords)
15+
# meta_description :text (deprecated - use draft_version.meta_description)
1616
# lft :integer
1717
# rgt :integer
1818
# parent_id :integer
@@ -66,11 +66,12 @@ class Page < BaseRecord
6666
depth
6767
urlname
6868
cached_tag_list
69+
title
70+
meta_description
71+
meta_keywords
6972
]
7073

7174
PERMITTED_ATTRIBUTES = [
72-
:meta_description,
73-
:meta_keywords,
7475
:name,
7576
:page_layout,
7677
:public_on,
@@ -81,10 +82,12 @@ class Page < BaseRecord
8182
:searchable,
8283
:sitemap,
8384
:tag_list,
84-
:title,
8585
:urlname,
8686
:layoutpage,
87-
:menu_id
87+
:menu_id,
88+
{
89+
draft_version_attributes: [:id] + PageVersion::METADATA_ATTRIBUTES.map(&:to_sym)
90+
}
8891
]
8992

9093
acts_as_nested_set(dependent: :destroy, scope: [:layoutpage, :language_id])
@@ -120,6 +123,8 @@ class Page < BaseRecord
120123
has_one :draft_version, -> { drafts }, class_name: "Alchemy::PageVersion"
121124
has_one :public_version, -> { published }, class_name: "Alchemy::PageVersion", autosave: -> { persisted? }
122125

126+
accepts_nested_attributes_for :draft_version
127+
123128
has_many :page_ingredients, class_name: "Alchemy::Ingredients::Page", foreign_key: :related_object_id, dependent: :nullify
124129

125130
before_validation :set_language,
@@ -129,8 +134,7 @@ class Page < BaseRecord
129134
validates_format_of :page_layout, with: /\A[a-z0-9_-]+\z/, unless: -> { page_layout.blank? }
130135
validates_presence_of :parent, unless: -> { layoutpage? || language_root? }
131136

132-
before_create -> { versions.build },
133-
if: -> { versions.none? }
137+
after_initialize :ensure_draft_version
134138

135139
before_save :set_language_code,
136140
if: -> { language.present? }
@@ -179,7 +183,7 @@ def url_path_class=(klass)
179183
end
180184

181185
def searchable_alchemy_resource_attributes
182-
%w[name urlname title]
186+
%w[name urlname]
183187
end
184188

185189
# @return the language root page for given language id.
@@ -213,8 +217,7 @@ def copy_and_paste(source, new_parent, new_name)
213217
.call(changed_attributes: {
214218
parent: new_parent,
215219
language: new_parent&.language,
216-
name: new_name,
217-
title: new_name
220+
name: new_name
218221
})
219222
if source.children.any?
220223
source.copy_children_to(page)
@@ -458,6 +461,54 @@ def public_until
458461
attribute_fixed?(:public_until) ? fixed_attributes[:public_until] : public_version&.public_until
459462
end
460463

464+
# Returns the title from the public version, falling back to draft version
465+
#
466+
# If it's a fixed attribute then the fixed value is returned instead
467+
#
468+
def title
469+
return fixed_attributes[:title] if attribute_fixed?(:title)
470+
471+
public_version&.title || draft_version&.title
472+
end
473+
474+
# Returns the meta_description from the public version, falling back to draft version
475+
#
476+
# If it's a fixed attribute then the fixed value is returned instead
477+
#
478+
def meta_description
479+
return fixed_attributes[:meta_description] if attribute_fixed?(:meta_description)
480+
481+
public_version&.meta_description || draft_version&.meta_description
482+
end
483+
484+
# Returns the meta_keywords from the public version, falling back to draft version
485+
#
486+
# If it's a fixed attribute then the fixed value is returned instead
487+
#
488+
def meta_keywords
489+
return fixed_attributes[:meta_keywords] if attribute_fixed?(:meta_keywords)
490+
491+
public_version&.meta_keywords || draft_version&.meta_keywords
492+
end
493+
494+
# @deprecated Use draft_version.title= instead
495+
def title=(value)
496+
draft_version&.title = value
497+
end
498+
deprecate "title=": :"page.draft_version.title=", deprecator: Alchemy::Deprecation
499+
500+
# @deprecated Use draft_version.meta_description= instead
501+
def meta_description=(value)
502+
draft_version&.meta_description = value
503+
end
504+
deprecate "meta_description=": :"page.draft_version.meta_description=", deprecator: Alchemy::Deprecation
505+
506+
# @deprecated Use draft_version.meta_keywords= instead
507+
def meta_keywords=(value)
508+
draft_version&.meta_keywords = value
509+
end
510+
deprecate "meta_keywords=": :"page.draft_version.meta_keywords=", deprecator: Alchemy::Deprecation
511+
461512
# Returns the name of the creator of this page.
462513
#
463514
# If no creator could be found or associated user model
@@ -499,9 +550,18 @@ def menus
499550

500551
private
501552

553+
def ensure_draft_version
554+
self.draft_version ||= versions.build
555+
end
556+
502557
def set_fixed_attributes
503558
fixed_attributes.all.each do |attribute, value|
504-
send(:"#{attribute}=", value)
559+
attribute_name = attribute.to_s
560+
if PageVersion::METADATA_ATTRIBUTES.include?(attribute_name)
561+
draft_version&.send(:"#{attribute}=", value)
562+
else
563+
send(:"#{attribute}=", value)
564+
end
505565
end
506566
end
507567

app/models/alchemy/page/page_naming.rb

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ module PageNaming
1919
uniqueness: {scope: [:language_id, :layoutpage], if: -> { urlname.present? }, case_sensitive: false},
2020
exclusion: {in: RESERVED_URLNAMES}
2121

22-
before_save :set_title,
23-
if: -> { title.blank? }
24-
2522
after_update :update_descendants_urlnames,
2623
if: :saved_change_to_urlname?
2724

@@ -69,10 +66,6 @@ def set_urlname
6966
self[:urlname] = nested_url_name
7067
end
7168

72-
def set_title
73-
self[:title] = name
74-
end
75-
7669
# Returns the full nested urlname.
7770
#
7871
def nested_url_name

app/models/alchemy/page/publisher.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ def publish!(public_on:)
2424
version = public_version(public_on)
2525
DeleteElements.new(version.elements).call
2626

27+
copy_metadata(public_version: version)
28+
2729
repository = page.draft_version.element_repository
2830
ActiveRecord::Base.no_touching do
2931
Element.acts_as_list_no_update do
@@ -51,6 +53,16 @@ def publish!(public_on:)
5153
def public_version(public_on)
5254
page.public_version || page.versions.create!(public_on: public_on)
5355
end
56+
57+
# Copy metadata from draft_version to public_version.
58+
def copy_metadata(public_version:)
59+
draft = page.draft_version
60+
return unless draft
61+
62+
PageVersion::METADATA_ATTRIBUTES.each do |attr|
63+
public_version.send(:"#{attr}=", draft.send(attr))
64+
end
65+
end
5466
end
5567
end
5668
end

app/models/alchemy/page_version.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
module Alchemy
44
class PageVersion < BaseRecord
5+
# Metadata attributes that are versioned (moved from Page)
6+
METADATA_ATTRIBUTES = %w[
7+
title
8+
meta_description
9+
meta_keywords
10+
].freeze
11+
512
belongs_to :page, class_name: "Alchemy::Page", inverse_of: :versions, touch: true
613

714
has_many :elements, -> { order(:position) },
@@ -11,6 +18,8 @@ class PageVersion < BaseRecord
1118
scope :drafts, -> { where(public_on: nil).order(updated_at: :desc) }
1219
scope :published, -> { where.not(public_on: nil).order(public_on: :desc) }
1320

21+
before_create :set_title_from_page
22+
1423
def self.public_on(time = Time.current)
1524
where("#{table_name}.public_on <= :time AND " \
1625
"(#{table_name}.public_until IS NULL " \
@@ -54,5 +63,11 @@ def element_repository
5463
def delete_elements
5564
DeleteElements.new(elements).call
5665
end
66+
67+
def set_title_from_page
68+
return if title.present?
69+
70+
self.title = page&.name
71+
end
5772
end
5873
end

app/services/alchemy/copy_page.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,14 @@ class CopyPage
2828
depth
2929
urlname
3030
cached_tag_list
31+
title
32+
meta_description
33+
meta_keywords
3134
]
3235

36+
# Metadata to copy via nested attributes (title is derived from page.name)
37+
METADATA_ATTRIBUTES_TO_COPY = (Alchemy::PageVersion::METADATA_ATTRIBUTES - %w[title]).freeze
38+
3339
attr_reader :page
3440

3541
# @param page [Alchemy::Page]
@@ -70,6 +76,7 @@ def attributes_from_source_for_copy(differences = {})
7076
.merge(DEFAULT_ATTRIBUTES_FOR_COPY)
7177
.merge(differences)
7278
desired_attributes["name"] = best_name_for_copy(source_attributes, desired_attributes)
79+
desired_attributes["draft_version_attributes"] = draft_version_attributes_for_copy
7380
desired_attributes.except(*SKIPPED_ATTRIBUTES_ON_COPY)
7481
end
7582

@@ -94,5 +101,15 @@ def best_name_for_copy(source_attributes, desired_attributes)
94101
desired_name
95102
end
96103
end
104+
105+
# Builds nested attributes for draft_version metadata (except title).
106+
# Title is handled by PageVersion#set_title_from_page callback based on page.name.
107+
def draft_version_attributes_for_copy
108+
return {} unless page.draft_version
109+
110+
METADATA_ATTRIBUTES_TO_COPY.each_with_object({}) do |attr, hash|
111+
hash[attr] = page.draft_version.send(attr)
112+
end
113+
end
97114
end
98115
end

app/views/alchemy/admin/pages/_form.html.erb

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,14 @@
1919

2020
<%= f.input :name, autofocus: true %>
2121
<%= f.input :urlname, as: 'string', input_html: {value: @page.slug}, label: Alchemy::Page.human_attribute_name(:slug) %>
22-
<alchemy-char-counter max-chars="60">
23-
<%= f.input :title %>
24-
</alchemy-char-counter>
22+
23+
<%= f.fields_for :draft_version, @page.draft_version do |v| %>
24+
<alchemy-char-counter max-chars="60">
25+
<%= v.input :title, input_html: {
26+
disabled: @page.attribute_fixed?(:title)
27+
} %>
28+
</alchemy-char-counter>
29+
<% end %>
2530

2631
<% if Alchemy.config.show_page_searchable_checkbox %>
2732
<div class="input check_boxes">
@@ -40,13 +45,20 @@
4045
</div>
4146
</div>
4247

43-
<alchemy-char-counter max-chars="160">
44-
<%= f.input :meta_description, as: 'text' %>
45-
</alchemy-char-counter>
48+
<%= f.fields_for :draft_version, @page.draft_version do |v| %>
49+
<alchemy-char-counter max-chars="160">
50+
<%= v.input :meta_description, as: 'text', input_html: {
51+
disabled: @page.attribute_fixed?(:meta_description)
52+
} %>
53+
</alchemy-char-counter>
4654

47-
<%= f.input :meta_keywords,
48-
as: 'text',
49-
hint: Alchemy.t('pages.update.comma_seperated') %>
55+
<%= v.input :meta_keywords,
56+
as: 'text',
57+
hint: Alchemy.t('pages.update.comma_seperated'),
58+
input_html: {
59+
disabled: @page.attribute_fixed?(:meta_keywords)
60+
} %>
61+
<% end %>
5062

5163
<%= render Alchemy::Admin::TagsAutocomplete.new do %>
5264
<%= f.input :tag_list, input_html: { value: f.object.tag_list.join(",") } %>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
class AddMetadataToPageVersions < ActiveRecord::Migration[7.1]
4+
def change
5+
add_column :alchemy_page_versions, :title, :string
6+
add_column :alchemy_page_versions, :meta_description, :text
7+
add_column :alchemy_page_versions, :meta_keywords, :text
8+
end
9+
end

lib/alchemy/tasks/usage.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ def elements_count_by_name
1818
end
1919

2020
def pages_count_by_type
21-
res = Alchemy::Page.all
22-
.select("page_layout, COUNT(*) AS count")
21+
res = Alchemy::Page
2322
.group(:page_layout)
24-
.order("count DESC, page_layout ASC")
25-
.map { |p| {"page_layout" => p.page_layout, "count" => p.count} }
23+
.order("count_all DESC, page_layout ASC")
24+
.count
25+
.map { |layout, count| {"page_layout" => layout, "count" => count} }
2626
Alchemy::PageDefinition.all.reject { |page_layout| res.map { |p| p["page_layout"] }.include?(page_layout.name) }.sort_by(&:name).each do |page_layout|
2727
res << {"page_layout" => page_layout.name, "count" => 0}
2828
end

lib/alchemy/test_support/factories/page_version_factory.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
FactoryBot.define do
44
factory :alchemy_page_version, class: "Alchemy::PageVersion" do
55
association :page, factory: :alchemy_page
6+
title { nil }
7+
meta_description { nil }
8+
meta_keywords { nil }
69

710
trait :published do
811
public_on { Time.current }

lib/alchemy/upgrader.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,19 @@ class Upgrader
1212
Dir["#{File.dirname(__FILE__)}/upgrader/*.rb"].sort.each { require(_1) }
1313

1414
VERSION_MODULE_MAP = {
15-
"8.0" => "Alchemy::Upgrader::EightZero"
15+
"8.0" => "Alchemy::Upgrader::EightZero",
16+
"8.1" => "Alchemy::Upgrader::EightOne"
1617
}
1718

1819
source_root Alchemy::Engine.root.join("lib/generators/alchemy/install")
1920

21+
# Returns a memoized upgrader instance for the given version.
22+
# This ensures todos are accumulated across rake tasks.
23+
def self.[](version)
24+
@instances ||= {}
25+
@instances[version.to_s] ||= new(version)
26+
end
27+
2028
def initialize(version)
2129
super()
2230
self.class.include VERSION_MODULE_MAP[version.to_s].constantize

0 commit comments

Comments
 (0)