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

Feature: Autogenerate nestable elements #1513

Merged
merged 10 commits into from
Nov 11, 2018
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