Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Page Versions #2022

Merged
merged 41 commits into from
Mar 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e370c9a
Add page version model
tvdeyen Dec 7, 2020
19289f9
Create a version for each new page
tvdeyen Dec 7, 2020
33b9481
Add page_version association to elements
tvdeyen Dec 7, 2020
5ba0e70
Add upgrade task to create page versions
tvdeyen Dec 7, 2020
3b4c7b4
Add draft_version association to Page
tvdeyen Dec 7, 2020
c081bae
Add public_version association to page
tvdeyen Dec 7, 2020
b7f4972
Create public version on page.publish!
tvdeyen Dec 7, 2020
74d771d
Copy page_version_id on Element.copy
tvdeyen Dec 7, 2020
2fd525b
Copy public elements on page.publish! to version
tvdeyen Dec 7, 2020
3df9c8e
Add Gemfile to dummy app
tvdeyen Dec 8, 2020
ade4223
Update page version upgrader
tvdeyen Dec 8, 2020
3ab84c0
Autogenerate elements on the draft version
tvdeyen Dec 8, 2020
8446ac9
Load associated page elements from public version
tvdeyen Dec 8, 2020
c3558f4
Load elements in edit mode from draft version
tvdeyen Dec 9, 2020
3b5212f
Remove trashed_elements association from Page elements
tvdeyen Dec 9, 2020
0b0c1fe
Copy page elements from draft version
tvdeyen Dec 11, 2020
b8f1b18
Check element availability on pages draft version
tvdeyen Dec 11, 2020
9c5807e
Create new elements on pages draft version
tvdeyen Dec 11, 2020
b79ac69
Return included elements from public page version
tvdeyen Dec 11, 2020
3a4acaa
Remove the page relation from elements
tvdeyen Dec 14, 2020
76380fd
Use page_version in admin elements controller
tvdeyen Dec 15, 2020
f431c23
Return elements from public versions in API
tvdeyen Dec 15, 2020
b322faf
Add 6.0 upgrade task
tvdeyen Dec 16, 2020
3b4f24d
Do not use find_each during copy element to public version
tvdeyen Dec 17, 2020
51ba5e7
Allow to pass a time to PageVersion.published scope
tvdeyen Dec 17, 2020
c0e9858
Unpublish former published pages on Page#publish!
tvdeyen Dec 17, 2020
fbad994
Use page_version in ElementsFinder
tvdeyen Dec 18, 2020
40c0b57
Use draft version in Page preview
tvdeyen Dec 18, 2020
e0f4fe1
Render elements from public version or assigned page_version
tvdeyen Jan 6, 2021
a17b66b
Have only one public version
tvdeyen Jan 11, 2021
986fadb
Delegate Page#public? to public version
tvdeyen Jan 11, 2021
f89578d
Update Page.not_public scope to use versions
tvdeyen Feb 1, 2021
ae8e592
Do not set published_at after update
tvdeyen Feb 3, 2021
1b25c7b
Delegate public_on and public_until to page version
tvdeyen Feb 3, 2021
43b7328
Remove Page#public_on and Page#public_until timestamps
tvdeyen Feb 12, 2021
91aa12a
Fix page_version loading in render_elements
tvdeyen Feb 8, 2021
6ad4871
Raise deprecated major version to 7.0
tvdeyen Feb 12, 2021
12247aa
Adjust upgrader to page legacy timestamps
tvdeyen Feb 12, 2021
5d7ae6b
Update brakeman ignores
tvdeyen Feb 15, 2021
fdea837
Revert "Fix page_version loading in render_elements"
tvdeyen Feb 22, 2021
7e801d5
render_elements from draft version in preview mode
tvdeyen Feb 22, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions app/controllers/alchemy/admin/elements_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,27 @@ class ElementsController < Alchemy::Admin::BaseController
authorize_resource class: Alchemy::Element

def index
@page = Page.find(params[:page_id])
@elements = @page.all_elements.not_nested.unfixed.includes(*element_includes)
@fixed_elements = @page.all_elements.fixed.includes(*element_includes)
@page_version = PageVersion.find(params[:page_version_id])
@page = @page_version.page
elements = @page_version.elements.order(:position).includes(*element_includes)
@elements = elements.not_nested.unfixed
@fixed_elements = elements.not_nested.fixed
end

def new
@page = Page.find(params[:page_id])
@page_version = PageVersion.find(params[:page_version_id])
@page = @page_version.page
@parent_element = Element.find_by(id: params[:parent_element_id])
@elements = @page.available_elements_within_current_scope(@parent_element)
@element = @page.elements.build
@element = @page_version.elements.build
@clipboard = get_clipboard("elements")
@clipboard_items = Element.all_from_clipboard_for_page(@clipboard, @page)
end

# Creates a element as discribed in config/alchemy/elements.yml on page via AJAX.
def create
@page = Page.find(params[:element][:page_id])
@page_version = PageVersion.find(params[:element][:page_version_id])
@page = @page_version.page
Element.transaction do
if @paste_from_clipboard = params[:paste_from_clipboard].present?
@element = paste_element_from_clipboard
Expand All @@ -38,7 +42,7 @@ def create
if @element.valid?
render :create
else
@element.page = @page
@element.page_version = @page_version
@elements = @page.available_element_definitions
@clipboard = get_clipboard("elements")
@clipboard_items = Element.all_from_clipboard_for_page(@clipboard, @page)
Expand Down Expand Up @@ -131,7 +135,7 @@ def paste_element_from_clipboard
@source_element = Element.find(element_from_clipboard["id"])
element = Element.copy(@source_element, {
parent_element_id: create_element_params[:parent_element_id],
page_id: @page.id,
page_version_id: @page_version.id,
})
if element_from_clipboard["action"] == "cut"
@cut_element_id = @source_element.id
Expand All @@ -154,7 +158,7 @@ def element_params
end

def create_element_params
params.require(:element).permit(:name, :page_id, :parent_element_id)
params.require(:element).permit(:name, :page_version_id, :parent_element_id)
end
end
end
Expand Down
6 changes: 6 additions & 0 deletions app/controllers/alchemy/admin/pages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class PagesController < ResourcesController

before_action :set_view, only: [:index]

before_action :set_page_version, only: [:show, :edit]

def index
@query = @current_language.pages.contentpages.ransack(search_filter_params[:q])

Expand Down Expand Up @@ -402,6 +404,10 @@ def set_root_page
@page_root = @current_language.root_page
end

def set_page_version
@page_version = @page.draft_version
end

def serialized_page_tree
PageTreeSerializer.new(@page, ability: current_ability,
user: current_alchemy_user,
Expand Down
11 changes: 7 additions & 4 deletions app/controllers/alchemy/api/elements_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ class Api::ElementsController < Api::BaseController
# If you want to only load a specific type of element pass ?named=an_element_name
#
def index
@elements = Element.not_nested
if params[:page_id].present?
@page = Page.find(params[:page_id])
@elements = @page.elements.not_nested
else
@elements = Element.not_nested.joins(:page_version).merge(PageVersion.published)
end

# Fix for cancancan not able to merge multiple AR scopes for logged in users
if cannot? :manage, Alchemy::Element
@elements = @elements.accessible_by(current_ability, :index)
end
if params[:page_id].present?
@elements = @elements.where(page_id: params[:page_id])
end
if params[:named].present?
@elements = @elements.named(params[:named])
end
Expand Down
11 changes: 9 additions & 2 deletions app/helpers/alchemy/elements_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,14 @@ def render_elements(options = {})
}.update(options)

finder = options[:finder] || Alchemy::ElementsFinder.new(options)
elements = finder.elements(page: options[:from_page])

page_version = if @preview_mode
options[:from_page]&.draft_version
else
options[:from_page]&.public_version
end

elements = finder.elements(page_version: page_version)

buff = []
elements.each_with_index do |element, i|
Expand Down Expand Up @@ -133,7 +140,7 @@ def render_elements(options = {})
def render_element(element, options = {}, counter = 1)
if element.nil?
warning("Element is nil")
render "alchemy/elements/view_not_found", {name: "nil"}
render "alchemy/elements/view_not_found", { name: "nil" }
return
end

Expand Down
15 changes: 8 additions & 7 deletions app/models/alchemy/element.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# id :integer not null, primary key
# name :string
# position :integer
# page_id :integer not null
# page_version_id :integer not null
# public :boolean default(TRUE)
# fixed :boolean default(FALSE)
# folded :boolean default(FALSE)
Expand Down Expand Up @@ -56,15 +56,15 @@ class Element < BaseRecord
"updater_id",
].freeze

# All Elements that share the same page id and parent element id and are fixed or not are considered a list.
# All Elements that share the same page version and parent element and are fixed or not are considered a list.
#
# If parent element id is nil (typical case for a simple page),
# If parent_element_id is nil (typical case for a simple page),
# then all elements on that page are still in one list,
# because acts_as_list correctly creates this statement:
#
# WHERE page_id = 1 and fixed = FALSE AND parent_element_id = NULL
# WHERE page_version_id = 1 and fixed = FALSE AND parent_element_id = NULL
#
acts_as_list scope: [:page_id, :fixed, :parent_element_id]
acts_as_list scope: [:page_version_id, :fixed, :parent_element_id]

stampable stamper_class_name: Alchemy.user_class_name

Expand All @@ -83,7 +83,8 @@ class Element < BaseRecord
dependent: :destroy,
inverse_of: :parent_element

belongs_to :page, touch: true, inverse_of: :elements
belongs_to :page_version, touch: true, inverse_of: :elements
has_one :page, through: :page_version

# A nested element belongs to a parent element.
belongs_to :parent_element,
Expand Down Expand Up @@ -315,7 +316,7 @@ def copy_nested_elements_to(target_element)
nested_elements.map do |nested_element|
Element.copy(nested_element, {
parent_element_id: target_element.id,
page_id: target_element.page_id,
page_version_id: target_element.page_version_id,
})
end
end
Expand Down
64 changes: 42 additions & 22 deletions app/models/alchemy/page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ class Page < BaseRecord
has_many :folded_pages
has_many :legacy_urls, class_name: "Alchemy::LegacyPageUrl"
has_many :nodes, class_name: "Alchemy::Node", inverse_of: :page
has_many :versions, class_name: "Alchemy::PageVersion", inverse_of: :page, dependent: :destroy
has_one :draft_version, -> { drafts }, class_name: "Alchemy::PageVersion"
has_one :public_version, -> { published }, class_name: "Alchemy::PageVersion"

before_validation :set_language,
if: -> { language.nil? }
Expand All @@ -123,6 +126,8 @@ class Page < BaseRecord
validates_format_of :page_layout, with: /\A[a-z0-9_-]+\z/, unless: -> { page_layout.blank? }
validates_presence_of :parent, unless: -> { layoutpage? || language_root? }

before_create -> { versions.build }

before_save :set_language_code,
if: -> { language.present? }

Expand All @@ -132,9 +137,6 @@ class Page < BaseRecord
before_save :inherit_restricted_status,
if: -> { parent && parent.restricted? }

before_save :set_published_at,
if: -> { public_on.present? && published_at.nil? }

before_save :set_fixed_attributes,
if: -> { fixed_attributes.any? }

Expand All @@ -152,6 +154,14 @@ class Page < BaseRecord
# site_name accessor
delegate :name, to: :site, prefix: true, allow_nil: true

# Old public_on and public_until attributes for historical reasons
#
# These attributes now exist on the page versions
#
attr_readonly :legacy_public_on, :legacy_public_until
deprecate :legacy_public_on, deprecator: Alchemy::Deprecation
deprecate :legacy_public_until, deprecator: Alchemy::Deprecation

# Class methods
#
class << self
Expand Down Expand Up @@ -297,7 +307,9 @@ def new_name_for_copy(custom_name, source_name)
# Instance methods
#

# Returns elements from page.
# Returns elements from pages public version.
#
# You can pass another page_version to load elements from in the options.
#
# @option options [Array<String>|String] :only
# Returns only elements with given names
Expand All @@ -316,11 +328,14 @@ def new_name_for_copy(custom_name, source_name)
# @option options [Class] :finder (Alchemy::ElementsFinder)
# A class that will return elements from page.
# Use this for your custom element loading logic.
# @option options [Alchemy::PageVersion] :page_version
# A page version to load elements from.
# Uses the pages public_version by default.
#
# @return [ActiveRecord::Relation]
def find_elements(options = {})
finder = options[:finder] || Alchemy::ElementsFinder.new(options)
finder.elements(page: self)
finder.elements(page_version: options[:page_version] || public_version)
end

# = The url_path for this page
Expand Down Expand Up @@ -429,22 +444,31 @@ def copy_children_to(new_parent)
end
end

# Publishes the page.
# Creates a public version of the page.
#
# Sets +public_on+ and the +published_at+ value to current time
# and resets +public_until+ to nil
# Sets the +published_at+ value to current time
#
# The +published_at+ attribute is used as +cache_key+.
#
def publish!
current_time = Time.current
update_columns(
published_at: current_time,
public_on: already_public_for?(current_time) ? public_on : current_time,
public_until: still_public_for?(current_time) ? public_until : nil,
)
def publish!(current_time = Time.current)
update_columns(published_at: current_time)
Publisher.new(self).publish!(public_on: current_time)
end

# Sets the public_on date on the published version
#
# Builds a new version if none exists yet.
#
def public_on=(time)
if public_version
public_version.public_on = time
else
versions.build(public_on: time)
end
end

delegate :public_until=, to: :public_version, allow_nil: true

# Updates an Alchemy::Page based on a new ordering to be applied to it
#
# Note: Page's urls should not be updated (and a legacy URL created) if nesting is OFF
Expand Down Expand Up @@ -485,20 +509,20 @@ def editable_by?(user)
(editor_roles & user.alchemy_roles).any?
end

# Returns the value of +public_on+ attribute
# Returns the value of +public_on+ attribute from public version
#
# If it's a fixed attribute then the fixed value is returned instead
#
def public_on
attribute_fixed?(:public_on) ? fixed_attributes[:public_on] : self[:public_on]
attribute_fixed?(:public_on) ? fixed_attributes[:public_on] : public_version&.public_on
end

# Returns the value of +public_until+ attribute
#
# If it's a fixed attribute then the fixed value is returned instead
#
def public_until
attribute_fixed?(:public_until) ? fixed_attributes[:public_until] : self[:public_until]
attribute_fixed?(:public_until) ? fixed_attributes[:public_until] : public_version&.public_until
end

# Returns the name of the creator of this page.
Expand Down Expand Up @@ -562,9 +586,5 @@ def set_language_code
def create_legacy_url
legacy_urls.find_or_create_by(urlname: urlname_before_last_save)
end

def set_published_at
self.published_at = Time.current
end
end
end
35 changes: 15 additions & 20 deletions app/models/alchemy/page/page_elements.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,17 @@ module PageElements
included do
attr_accessor :autogenerate_elements

has_many :all_elements,
-> { order(:position) },
with_options(
class_name: "Alchemy::Element",
inverse_of: :page
has_many :elements,
-> { order(:position).not_nested.unfixed.available },
class_name: "Alchemy::Element",
inverse_of: :page
has_many :fixed_elements,
-> { order(:position).fixed.available },
class_name: "Alchemy::Element",
inverse_of: :page
has_many :dependent_destroyable_elements,
-> { not_nested },
class_name: "Alchemy::Element",
dependent: :destroy
through: :public_version,
inverse_of: :page,
source: :elements,
) do
has_many :all_elements
has_many :elements, -> { not_nested.unfixed.available }
has_many :fixed_elements, -> { fixed.available }
end

has_many :contents, through: :elements
has_and_belongs_to_many :to_be_swept_elements, -> { distinct },
class_name: "Alchemy::Element",
Expand All @@ -41,10 +36,10 @@ module ClassMethods
# @return [Array]
#
def copy_elements(source, target)
source_elements = source.all_elements.not_nested
source_elements = source.draft_version.elements.not_nested
source_elements.order(:position).map do |source_element|
Element.copy(source_element, {
page_id: target.id,
page_version_id: target.draft_version.id,
}).tap(&:move_to_bottom)
end
end
Expand Down Expand Up @@ -80,7 +75,7 @@ def available_element_definitions(only_element_named = nil)

return [] if @_element_definitions.blank?

existing_elements = all_elements.not_nested
existing_elements = draft_version.elements.not_nested
@_existing_element_names = existing_elements.pluck(:name)
delete_unique_element_definitions!
delete_outnumbered_element_definitions!
Expand Down Expand Up @@ -172,7 +167,7 @@ def feed_elements
#
def richtext_contents_ids
Alchemy::Content.joins(:element)
.where(Element.table_name => { page_id: id, folded: false })
.where(Element.table_name => { page_version_id: draft_version.id, folded: false })
.select(&:has_tinymce?)
.collect(&:id)
end
Expand All @@ -185,7 +180,7 @@ def richtext_contents_ids
#
def generate_elements
definition.fetch("autogenerate", []).each do |element_name|
Element.create(page: self, name: element_name)
Element.create(page: self, page_version: draft_version, name: element_name)
end
end

Expand Down
Loading