Skip to content

Commit

Permalink
Merge pull request #1513 from tvdeyen/nestable-elements-autogenerate
Browse files Browse the repository at this point in the history
Feature: Autogenerate nestable elements
  • Loading branch information
tvdeyen authored Nov 11, 2018
2 parents 48a65b8 + a2be9ae commit da86e77
Show file tree
Hide file tree
Showing 29 changed files with 253 additions and 254 deletions.
3 changes: 1 addition & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ gem 'sassc-rails'

group :development, :test do
gem 'simplecov', require: false
gem 'bootsnap', require: false
if ENV['TRAVIS']
gem 'codeclimate-test-reporter', '~> 1.0', require: false
end
Expand All @@ -24,8 +25,6 @@ group :development, :test do
gem 'yard'
gem 'redcarpet'
gem 'pry-byebug'
gem 'spring'
gem 'spring-commands-rspec'
gem 'rubocop', require: false
gem 'listen'
gem 'localeapp', '~> 3.0', require: false
Expand Down
5 changes: 2 additions & 3 deletions app/controllers/alchemy/admin/contents_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@ class ContentsController < Alchemy::Admin::BaseController
authorize_resource class: Alchemy::Content

def create
@element = Element.find(params[:content][:element_id])
@content = Content.create_from_scratch(@element, content_params)
@content = Content.create(content_params)
@html_options = params[:html_options] || {}
end

private

def content_params
params.require(:content).permit(:element_id, :name, :ingredient, :essence_type)
params.require(:content).permit(:element_id, :name, :ingredient)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/alchemy/admin/elements_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def create
@element = paste_element_from_clipboard
@cell = @element.cell
else
@element = Element.new_from_scratch(create_element_params)
@element = Element.new(create_element_params)
if @page.can_have_cells?
@cell = find_or_create_cell
@element.cell = @cell
Expand Down
90 changes: 33 additions & 57 deletions app/models/alchemy/content/factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,21 @@ module ClassMethods

# Builds a new content as descriped in the elements.yml file.
#
# @param [Alchemy::Element]
# The element the content is for
# @param [Hash]
# The content definition used for finding the content in +elements.yml+ file
#
def build(element, essence_hash)
definition = content_definition(element, essence_hash)
def new(attributes = {})
element = attributes[:element] || Element.find_by(id: attributes[:element_id])
return super if attributes.empty? || element.nil?

definition = element.content_definition_for(attributes[:name])
if definition.blank?
raise ContentDefinitionError, "No definition found in elements.yml for #{essence_hash.inspect} and #{element.inspect}"
else
new(name: definition['name'], element_id: element.id)
raise ContentDefinitionError, "No definition found in elements.yml for #{attributes.inspect} and #{element.inspect}"
end
super(name: definition['name'], element_id: element.id)
end
alias_method :build, :new
deprecate build: :new, deprecator: Alchemy::Deprecation

# Creates a new content from elements definition in the +elements.yml+ file.
#
Expand All @@ -32,12 +34,20 @@ def build(element, essence_hash)
#
# @return [Alchemy::Content]
#
def create_from_scratch(element, essence_hash)
if content = build(element, essence_hash)
content.create_essence!(essence_hash[:essence_type])
def create(*args)
attributes = args.last || {}
if args.length > 1
Alchemy::Deprecation.warn 'Passing an element as first argument to Alchemy::Content.create is deprecated! Pass an attribute hash with element inside instead.'
element = args.first
else
element = attributes[:element]
end
new(attributes.merge(element: element)).tap do |content|
content.create_essence!(attributes[:essence_type])
end
content
end
alias_method :create_from_scratch, :create
deprecate create_from_scratch: :create, deprecator: Alchemy::Deprecation

# Creates a copy of source and also copies the associated essence.
#
Expand All @@ -50,60 +60,26 @@ def create_from_scratch(element, essence_hash)
#
def copy(source, differences = {})
new_content = Content.new(
source.attributes.except(*SKIPPED_ATTRIBUTES_ON_COPY).merge(differences)
source.attributes.
except(*SKIPPED_ATTRIBUTES_ON_COPY).
merge(differences.with_indifferent_access)
)

new_essence = new_content.essence.class.create!(
new_content.essence.attributes.except(*SKIPPED_ATTRIBUTES_ON_COPY)
new_essence = source.essence.class.create!(
source.essence.attributes.
except(*SKIPPED_ATTRIBUTES_ON_COPY)
)

new_content.update!(essence_id: new_essence.id)
new_content
end

# Returns the content definition for building a content.
#
# 1. It looks in the element's contents definition
# 2. It builds a definition hash from essence type, if the the name key is not present
#
def content_definition(element, essence_hash)
# No name given. We build the content from essence type.
if essence_hash[:name].blank? && essence_hash[:essence_type].present?
content_definition_from_essence_type(element, essence_hash[:essence_type])
else
element.content_definition_for(essence_hash[:name])
new_content.tap do |content|
content.essence = new_essence
content.save
end
end

# Returns a hash for building a content from essence type.
#
# @param [Alchemy::Element]
# The element the content is for.
# @param [String]
# The essence type the content is from
#
def content_definition_from_essence_type(element, essence_type)
{
'type' => essence_type,
'name' => content_name_from_element_and_essence_type(element, essence_type)
}
end

# A name for content from its essence type and amount of same essences in element.
#
# Example:
#
# essence_picture_1
#
def content_name_from_element_and_essence_type(element, essence_type)
essences_of_same_type = element.contents.where(essence_type: normalize_essence_type(essence_type))
"#{essence_type.classify.demodulize.underscore}_#{essences_of_same_type.count + 1}"
end

# Returns all content definitions from elements.yml
#
def definitions
Element.definitions.collect { |e| e['contents'] }.flatten.compact
definitions = Element.definitions.flat_map { |e| e['contents'] }
definitions.compact!
definitions
end

# Returns a normalized Essence type
Expand Down
59 changes: 30 additions & 29 deletions app/models/alchemy/element.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ class Element < BaseRecord

FORBIDDEN_DEFINITION_ATTRIBUTES = [
"amount",
"autogenerate",
"nestable_elements",
"contents",
"hint",
"picture_gallery",
"taggable",
"compact"
].freeze
Expand Down Expand Up @@ -86,9 +86,11 @@ class Element < BaseRecord
validates_presence_of :name, on: :create
validates_format_of :name, on: :create, with: /\A[a-z0-9_-]+\z/

attr_accessor :create_contents_after_create
attr_accessor :autogenerate_contents
attr_accessor :autogenerate_nested_elements
after_create :create_contents, unless: -> { autogenerate_contents == false }
after_create :generate_nested_elements, unless: -> { autogenerate_nested_elements == false }

after_create :create_contents, unless: proc { |e| e.create_contents_after_create == false }
after_update :touch_touchable_pages

scope :trashed, -> { where(position: nil).order('updated_at DESC') }
Expand Down Expand Up @@ -122,23 +124,21 @@ class << self
# - Raises Alchemy::ElementDefinitionError if no definition for given attributes[:name]
# could be found
#
def new_from_scratch(attributes = {})
return new if attributes[:name].blank?
new_element_from_definition_by(attributes) || raise(ElementDefinitionError, attributes)
end
def new(attributes = {})
return super if attributes[:name].blank?
element_attributes = attributes.to_h.merge(name: attributes[:name].split('#').first)
element_definition = Element.definition_by_name(element_attributes[:name])
if element_definition.nil?
raise(ElementDefinitionError, attributes)
end

# Creates a new element as described in +/config/alchemy/elements.yml+
#
# - Returns a new Alchemy::Element object if no name is given in attributes,
# because the definition can not be found w/o name
# - Raises Alchemy::ElementDefinitionError if no definition for given attributes[:name]
# could be found
#
def create_from_scratch(attributes)
element = new_from_scratch(attributes)
element.save if element
element
super(element_definition.merge(element_attributes).except(*FORBIDDEN_DEFINITION_ATTRIBUTES))
end
alias_method :new_from_scratch, :new
deprecate new_from_scratch: :new, deprecator: Alchemy::Deprecation

alias_method :create_from_scratch, :create
deprecate create_from_scratch: :create, deprecator: Alchemy::Deprecation

# This methods does a copy of source and all depending contents and all of their depending essences.
#
Expand All @@ -156,7 +156,8 @@ def copy(source_element, differences = {})
.except(*SKIPPED_ATTRIBUTES_ON_COPY)
.merge(differences)
.merge({
create_contents_after_create: false,
autogenerate_contents: false,
autogenerate_nested_elements: false,
tag_list: source_element.tag_list
})

Expand Down Expand Up @@ -186,16 +187,6 @@ def all_from_clipboard_for_page(clipboard, page)
page.available_element_names.include?(ce.name)
}
end

private

def new_element_from_definition_by(attributes)
element_attributes = attributes.to_h.merge(name: attributes[:name].split('#').first)
element_definition = Element.definition_by_name(element_attributes[:name])
return if element_definition.nil?

new(element_definition.merge(element_attributes).except(*FORBIDDEN_DEFINITION_ATTRIBUTES))
end
end

# Returns next public element from same page.
Expand Down Expand Up @@ -312,6 +303,16 @@ def copy_nested_elements_to(target_element)

private

def generate_nested_elements
definition.fetch('autogenerate', []).each do |nestable_element|
if nestable_elements.include?(nestable_element)
Element.create(page: page, parent_element_id: id, name: nestable_element)
else
log_warning("Element '#{nestable_element}' not a nestable element for '#{name}'. Skipping!")
end
end
end

def select_element(elements, name, order)
elements = elements.named(name) if name.present?
elements.reorder(position: order).limit(1).first
Expand Down
4 changes: 2 additions & 2 deletions app/models/alchemy/element/element_contents.rb
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ def content_for_rss_meta(type)

# creates the contents for this element as described in the elements.yml
def create_contents
definition.fetch("contents", []).each do |content_hash|
Content.create_from_scratch(self, content_hash)
definition.fetch('contents', []).each do |attributes|
Content.create(attributes.merge(element: self))
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions app/models/alchemy/page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class Page < BaseRecord
include Alchemy::Taggable

DEFAULT_ATTRIBUTES_FOR_COPY = {
do_not_autogenerate: true,
autogenerate_elements: false,
visible: false,
public_on: nil,
public_until: nil,
Expand Down Expand Up @@ -206,7 +206,7 @@ def find_or_create_layout_root_for(language_id)
name: "Layoutroot for #{language.name}",
layoutpage: true,
language: language,
do_not_autogenerate: true,
autogenerate_elements: false,
parent_id: Page.root.id
)
end
Expand Down
11 changes: 6 additions & 5 deletions app/models/alchemy/page/page_elements.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module Page::PageElements
extend ActiveSupport::Concern

included do
attr_accessor :do_not_autogenerate
attr_accessor :autogenerate_elements

has_many :elements, -> { where(parent_element_id: nil).not_trashed.order(:position) }
has_many :trashed_elements,
Expand All @@ -23,12 +23,13 @@ module Page::PageElements
class_name: 'Alchemy::Element',
join_table: ElementToPage.table_name

after_create :autogenerate_elements, unless: -> { systempage? || do_not_autogenerate }
after_create :generate_elements,
unless: -> { systempage? || autogenerate_elements == false }

after_update :trash_not_allowed_elements!,
if: :has_page_layout_changed?

after_update :autogenerate_elements,
after_update :generate_elements,
if: :has_page_layout_changed?
end

Expand Down Expand Up @@ -249,13 +250,13 @@ def element_names_from_cell_definitions
#
# If the page has cells, it looks if there are elements to generate.
#
def autogenerate_elements
def generate_elements
elements_already_on_page = elements.available.pluck(:name)
elements = definition["autogenerate"]
if elements.present?
elements.each do |element|
next if elements_already_on_page.include?(element)
Element.create_from_scratch(attributes_for_element_name(element))
Element.create(attributes_for_element_name(element))
end
end
end
Expand Down
6 changes: 3 additions & 3 deletions app/views/alchemy/admin/contents/create.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ var editor_html = '<%= j(render "alchemy/essences/#{@content.essence_partial_nam
content: @content, options: options_from_params, html_options: @html_options
}) %>';

$("[data-element-<%= @element.id %>-missing-content=\"<%= @content.name %>\"]").replaceWith(editor_html);
$("[data-element-<%= @content.element_id %>-missing-content=\"<%= @content.name %>\"]").replaceWith(editor_html);

<% if @content.essence_type == "Alchemy::EssencePicture" && @content.ingredient %>

$('#picture_to_assign_<%= @content.ingredient.id %> a').attr('href', '#').off('click');

<% elsif @content.essence_type == "Alchemy::EssenceDate" %>

Alchemy.Datepicker('#element_<%= @element.id %>');
Alchemy.Datepicker('#element_<%= @content.element_id %>');

<% elsif @content.essence_type == "Alchemy::EssenceRichtext" %>

Expand All @@ -20,4 +20,4 @@ Alchemy.Tinymce.initEditor(<%= @content.id %>);

Alchemy.reloadPreview();
Alchemy.closeCurrentDialog();
Alchemy.SelectBox("#element_<%= @element.id %>");
Alchemy.SelectBox("#element_<%= @content.element_id %>");
5 changes: 0 additions & 5 deletions bin/rspec
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
#!/usr/bin/env ruby
begin
load File.expand_path('spring', __dir__)
rescue LoadError => e
raise unless e.message.include?('spring')
end
require 'bundler/setup'
load Gem.bin_path('rspec-core', 'rspec')
16 changes: 0 additions & 16 deletions bin/spring

This file was deleted.

Loading

0 comments on commit da86e77

Please sign in to comment.