fixed editable elements (wip)

This commit is contained in:
Didier Lafforgue 2012-03-19 02:29:59 +01:00
parent 1ef1e3fbf4
commit cfbe68564d
17 changed files with 399 additions and 144 deletions

View File

@ -6,13 +6,12 @@ module Locomotive
## fields ##
field :slug
field :block
field :default_content, :localize => true
field :default_attribute
field :hint
field :priority, :type => Integer, :default => 0
field :fixed, :type => Boolean, :default => false
field :disabled, :type => Boolean, :default => false
field :assignable, :type => Boolean, :default => true
field :from_parent, :type => Boolean, :default => false
# field :locales, :type => Array TODO
## associations ##
embedded_in :page, :class_name => 'Locomotive::Page', :inverse_of => :editable_elements
@ -20,10 +19,60 @@ module Locomotive
## validations ##
validates_presence_of :slug
## callbacks ##
after_save :propagate_content, :if => :fixed?
## scopes ##
scope :by_priority, :order_by => [[:priority, :desc]]
## methods ##
def _run_rearrange_callbacks
# callback from page/tree. not needed in the editable elements
end
def default_content?
# needs to be overridden for each kind of elements
true
end
# Copy attributes extracted from the corresponding Liquid tag
# Each editable element overrides this method.
#
# @param [ Hash ] attributes The up-to-date attributes
#
def copy_attributes(attributes)
self.attributes = attributes
end
# Copy attributes from an existing editable element coming
# from the parent page. Each editable element may or not
# override this method. The source element is a new record.
#
# @param [ EditableElement] el The source element
#
def copy_attributes_from(el)
self.attributes = el.attributes.reject { |attr| !%w(slug block hint priority fixed disabled from_parent).include?(attr) }
self.from_parent = true
end
protected
def _selector
locale = ::Mongoid::Fields::I18n.locale
{
'site_id' => self.page.site_id,
"template_dependencies.#{locale}" => { '$in' => [self.page._id] },
'editable_elements.fixed' => true,
'editable_elements.block' => self.block,
'editable_elements.slug' => self.slug
}
end
def propagate_content
# needs to be overridden for each kind of elements (file, short text, ...etc)
true
end
end
end

View File

@ -5,13 +5,72 @@ module Locomotive
replace_field 'source', ::String, true
field :default_source_url, :localize => true
after_save :propagate_content
## methods ##
# Returns the url or the path to the uploaded file
# if it exists. Otherwise returns the default url.
#
# @note This method is not used for the rendering, only for the back-office
#
# @return [String] The url or path of the file
#
def content
self.source? ? self.source.url : self.default_content
self.source? ? self.source.url : self.default_source_url
end
def default_content?
!self.source? && self.default_source_url.present?
end
def copy_attributes(attributes)
unless self.default_content?
attributes.delete(:default_source_url)
end
super(attributes)
end
def copy_attributes_from(el)
super(el)
if el.source_translations.nil?
self.attributes['default_source_url'] = el.attributes['default_source_url'] || {}
else
el.source_translations.keys.each do |locale|
::Mongoid::Fields::I18n.with_locale(locale) do
self.default_source_url = el.source? ? el.source.url : el.default_source_url
end
end
end
end
def remove_source=(value)
self.source_will_change! # notify the page to run the callbacks for that element
self.default_source_url = nil
super
end
def as_json(options = {})
Locomotive::EditableFilePresenter.new(self).as_json
end
protected
def propagate_content
if self.source_changed?
operations = {
'$set' => {
"editable_elements.$.default_source_url.#{::Mongoid::Fields::I18n.locale}" => self.source.url
}
}
self.page.collection.update self._selector, operations, :multi => true
end
end
end
end

View File

@ -2,20 +2,46 @@ module Locomotive
class EditableShortText < EditableElement
## fields ##
field :content, :localize => true
field :content, :localize => true
field :default_content, :type => Boolean, :localize => true, :default => true
## methods ##
def content_with_localization
value = self.content_without_localization
value.blank? ? self.default_content : value
def content=(value)
self.default_content = false unless self.new_record?
super
end
alias_method_chain :content, :localization
def default_content?
!!self.default_content
end
def copy_attributes_from(el)
super(el)
self.attributes['content'] = el.content_translations || {}
self.attributes['default_content'] = el.default_content_translations
end
def as_json(options = {})
Locomotive::EditableShortTextPresenter.new(self).as_json
end
protected
def propagate_content
if self.content_changed?
operations = {
'$set' => {
"editable_elements.$.content.#{::Mongoid::Fields::I18n.locale}" => self.content,
"editable_elements.$.default_content.#{::Mongoid::Fields::I18n.locale}" => false,
}
}
self.page.collection.update self._selector, operations, :multi => true
end
true
end
end
end

View File

@ -6,15 +6,10 @@ module Locomotive
extend ActiveSupport::Concern
included do
embeds_many :editable_elements, :class_name => 'Locomotive::EditableElement'
embeds_many :editable_elements, :class_name => 'Locomotive::EditableElement', :cascade_callbacks => true
after_save :remove_disabled_editable_elements
# editable file callbacks
after_save :store_file_sources!
before_save :write_file_source_identifiers
after_destroy :remove_file_sources!
accepts_nested_attributes_for :editable_elements
end
@ -31,7 +26,7 @@ module Locomotive
end
def enabled_editable_elements
self.editable_elements.by_priority.reject { |el| el.disabled? }
self.editable_elements.by_priority.reject { |el| el.disabled? || (el.fixed? && el.from_parent?) }
end
def editable_elements_grouped_by_blocks
@ -48,10 +43,12 @@ module Locomotive
end
def add_or_update_editable_element(attributes, type)
# locale = attributes.delete(:locale) # TODO
element = self.find_editable_element(attributes[:block], attributes[:slug])
if element
element.attributes = attributes
element.copy_attributes(attributes)
else
self.editable_elements.build(attributes, type)
end
@ -63,24 +60,13 @@ module Locomotive
def merge_editable_elements_from_page(source)
source.editable_elements.each do |el|
next if el.disabled? or !el.assignable?
next if el.disabled?
existing_el = self.find_editable_element(el.block, el.slug)
if existing_el.nil? # new one from parents
new_attributes = el.attributes.merge(:from_parent => true)
if new_attributes['default_attribute'].present?
new_attributes['default_content'] = self.send(new_attributes['default_attribute']) || el.content
else
if el.respond_to?(:content) # only for text
new_attributes['default_content'] = el.content
end
end
self.editable_elements.build(new_attributes, el.class)
elsif existing_el.default_attribute.nil?
existing_el.attributes = { :disabled => false, :default_content => el.content }
new_el = self.editable_elements.build({}, el.class)
new_el.copy_attributes_from(el)
else
existing_el.attributes = { :disabled => false }
end
@ -90,29 +76,10 @@ module Locomotive
def remove_disabled_editable_elements
return unless self.editable_elements.any? { |el| el.disabled? }
# super fast way to remove useless elements all in once (TODO callbacks)
# super fast way to remove useless elements all in once
self.collection.update(self.atomic_selector, '$pull' => { 'editable_elements' => { 'disabled' => true } })
end
protected
## callbacks for editable files
# equivalent to "after_save :store_source!" in EditableFile
def store_file_sources!
self.find_editable_files.collect(&:store_source!)
end
# equivalent to "before_save :write_source_identifier" in EditableFile
def write_file_source_identifiers
self.find_editable_files.collect(&:write_source_identifier)
end
# equivalent to "after_destroy :remove_source!" in EditableFile
def remove_file_sources!
self.find_editable_files.collect(&:remove_source!)
end
end
end
end

View File

@ -52,6 +52,7 @@ module Locomotive
scope :fullpath, lambda { |fullpath| { :where => { :fullpath => fullpath } } }
scope :handle, lambda { |handle| { :where => { :handle => handle } } }
scope :minimal_attributes, lambda { |attrs = []| { :only => (attrs || []) + %w(title slug fullpath position depth published templatized redirect listed parent_id site_id created_at updated_at) } }
scope :dependent_from, lambda { |id| { :where => { :template_dependencies.in => [id] } } }
## methods ##

View File

@ -6,7 +6,7 @@
.inner
%h2!= t('locomotive.pages.index.latest_entries')
%ul
- current_site.pages.latest_updated.minimal_attributes.each do |page|
- current_site.pages.unscoped.latest_updated.minimal_attributes.each do |page|
%li
= link_to truncate(page.title, :length => 25), edit_page_url(page)
%span= time_ago_in_words(page.updated_at)

View File

@ -102,7 +102,7 @@ en:
slug: "It will be used as the name of the collection in the liquid templates. Ex: <span class='code'>{{ contents.my_projects }}</span>"
raw_item_template: "You can customize the text displayed for each item in the list. Simply use Liquid. Ex: <span class='code'>{{ entry.name }})</span>"
public_submission_enabled: "It is used to let people from outside to create new entries (example: messages in a contact form)"
public_submission_accounts: "If the public submission option is enabled and for each entry created, sends a notification email tothe accounts listed above."
public_submission_accounts: "If the public submission option is enabled and for each entry created, sends a notification email to the accounts listed above."
"custom_fields/field":
name: "Name of the property for liquid templates. Ex: <span class='code'>&#123;&#123; your_object.&lt;name_of_your_field&gt; &#125;&#125;</span>"
hint: "Text displayed in the model form just below the field"

View File

@ -6,7 +6,7 @@ module Locomotive
delegate :seo_title, :meta_keywords, :meta_description, :to => '_source'
def title
self._source.templatized? ? @context['content_entry']._label : self._source.title
self._source.templatized? ? @context['entry']._label : self._source.title
end
def slug

View File

@ -9,7 +9,7 @@ module Locomotive
def initialize(tag_name, markup, tokens, context)
if markup =~ Syntax
@slug = $1.gsub('\'', '')
@options = { :assignable => true }
@options = { :fixed => false }
markup.scan(::Liquid::TagAttributes) { |key, value| @options[key.to_sym] = value.gsub(/^'/, '').gsub(/'$/, '') }
else
raise ::Liquid::SyntaxError.new("Syntax Error in 'editable_xxx' - Valid syntax: editable_xxx <slug>(, <options>)")
@ -22,18 +22,7 @@ module Locomotive
super
if @context[:page].present?
@context[:page].add_or_update_editable_element({
:block => @context[:current_block].try(:name),
:slug => @slug,
:hint => @options[:hint],
:priority => @options[:priority] || 0,
:default_attribute => @options[:default],
:default_content => default_content_option,
:assignable => @options[:assignable],
:disabled => false,
:from_parent => false,
:_type => self.document_type.to_s
}, document_type)
@context[:page].add_or_update_editable_element(default_element_attributes, document_type)
end
end
@ -43,9 +32,6 @@ module Locomotive
element = current_page.find_editable_element(context['block'].try(:name), @slug)
if element.present?
unless element.default_content.present?
element.default_content = render_default_content(context)
end
render_element(context, element)
else
Locomotive.log :error, "[editable element] missing element `#{context['block'].try(:name)}` / #{@slug} on #{current_page.fullpath}"
@ -55,6 +41,19 @@ module Locomotive
protected
def default_element_attributes
{
:block => @options[:block] || @context[:current_block].try(:name),
:slug => @slug,
:hint => @options[:hint],
:priority => @options[:priority] || 0,
:fixed => !!@options[:fixed],
:disabled => false,
:from_parent => false,
:_type => self.document_type.to_s
}
end
def render_element(element)
raise 'FIXME: has to be overidden'
end
@ -63,14 +62,6 @@ module Locomotive
raise 'FIXME: has to be overidden'
end
def default_content_option
result = nil
if @options[:default].present?
result = @context[:page].send(@options[:default])
end
result
end
def render_default_content(context)
render_all(@nodelist, context).join(' ')
end

View File

@ -6,8 +6,26 @@ module Locomotive
protected
protected
def default_element_attributes
if @nodelist.first.is_a?(String)
super.merge(:default_source_url => @nodelist.first.try(:to_s))
else
super
end
end
def render_element(context, element)
element.source? ? element.source.url : element.default_content
if element.source?
element.source.url
else
if element.default_source_url.present?
element.default_source_url
else
render_default_content(context)
end
end
end
def document_type

View File

@ -7,14 +7,16 @@ module Locomotive
protected
def render_element(context, element)
if context.registers[:inline_editor]
content = element.default_content? ? render_default_content(context) : element.content
if editable?(context, element)
%{
<div class='editable-long-text' data-element-id='#{element.id}' data-element-index='#{element._index}'>
#{element.content}
#{content}
</div>
}
else
element.content
content
end
end

View File

@ -7,14 +7,16 @@ module Locomotive
protected
def render_element(context, element)
if context.registers[:inline_editor]
content = element.default_content? ? render_default_content(context) : element.content
if editable?(context, element)
%{
<span class='editable-short-text' data-element-id='#{element.id}' data-element-index='#{element._index}'>
#{element.content}
#{content}
</span>
}
else
element.content
content
end
end
@ -22,6 +24,10 @@ module Locomotive
EditableShortText
end
def editable?(context, element)
context.registers[:inline_editor] && (!element.fixed? || (element.fixed? && !element.from_parent?))
end
end
::Liquid::Template.register_tag('editable_short_text', ShortText)

View File

@ -74,9 +74,9 @@ describe Locomotive::Liquid::Drops::Page do
it 'renders the content instance highlighted field instead for a templatized page' do
templatized = FactoryGirl.build(:page, :title => 'Lorem ipsum template', :templatized => true)
content_entry = Locomotive::Liquid::Drops::ContentEntry.new(mock('content_entry', :_label => 'Locomotive rocks !'))
entry = Locomotive::Liquid::Drops::ContentEntry.new(mock('entry', :_label => 'Locomotive rocks !'))
render_template('{{ page.title }}', 'page' => templatized, 'content_entry' => content_entry).should == 'Locomotive rocks !'
render_template('{{ page.title }}', 'page' => templatized, 'entry' => entry).should == 'Locomotive rocks !'
end
end

View File

@ -1,56 +0,0 @@
require 'spec_helper'
describe Locomotive::EditableElement do
before(:each) do
@site = FactoryGirl.create(:site)
@home = @site.pages.root.first
@home.update_attributes :raw_template => "{% block body %}{% editable_short_text 'body' %}Lorem ipsum{% endeditable_short_text %}{% endblock %}"
@sub_page_1 = FactoryGirl.create(:page, :slug => 'sub_page_1', :parent => @home, :raw_template => "{% extends 'parent' %}")
@sub_page_2 = FactoryGirl.create(:page, :slug => 'sub_page_2', :parent => @home, :raw_template => "{% extends 'parent' %}")
@sub_page_1_1 = FactoryGirl.create(:page, :slug => 'sub_page_1_1', :parent => @sub_page_1, :raw_template => "{% extends 'parent' %}")
end
context 'in sub pages level #1' do
before(:each) do
@sub_page_1.reload
@sub_page_2.reload
end
it 'exists' do
@sub_page_1.editable_elements.size.should == 1
@sub_page_2.editable_elements.size.should == 1
end
it 'has a non-nil slug' do
@sub_page_1.editable_elements.first.slug.should == 'body'
end
end
context 'in sub pages level #2' do
before(:each) do
@sub_page_1_1.reload
end
it 'exists' do
@sub_page_1_1.editable_elements.size.should == 1
end
it 'has a non-nil slug' do
@sub_page_1_1.editable_elements.first.slug.should == 'body'
end
it 'removes editable elements' do
@sub_page_1_1.editable_elements.destroy_all
@sub_page_1_1.reload
@sub_page_1_1.editable_elements.size.should == 0
end
end
end

View File

@ -16,13 +16,25 @@ describe Locomotive::EditableFile do
@home.editable_elements.first.slug.should == 'image'
end
it 'disables the default content flag if the remove_source method is called' do
@home.editable_elements.first.remove_source = true
@home.save; @home.reload
@home.editable_elements.first.default_content?.should be_false
end
it 'disables the default content when a new file is uploaded' do
@home.editable_elements.first.source = FixturedAsset.open('5k.png')
@home.save
@home.editable_elements.first.default_content?.should be_false
end
it 'does not have 2 image fields' do
editable_file = @home.editable_elements.first
fields = editable_file.class.fields.keys
(fields.include?('source') && fields.include?(:source)).should be_false
end
context 'with an attached file' do
describe 'with an attached file' do
before(:each) do
@editable_file = @home.editable_elements.first
@ -41,4 +53,43 @@ describe Locomotive::EditableFile do
end
describe '"sticky" files' do
before(:each) do
@home.update_attributes :raw_template => "{% block body %}{% editable_file 'image', fixed: true %}/foo.png{% endeditable_file %}{% endblock %}"
@sub_page_1 = FactoryGirl.create(:page, :slug => 'sub_page_1', :parent => @home, :raw_template => "{% extends 'index' %}")
@sub_page_2 = FactoryGirl.create(:page, :slug => 'sub_page_2', :parent => @home, :raw_template => "{% extends 'index' %}")
@sub_page_1_el = @sub_page_1.editable_elements.first
@sub_page_2_el = @sub_page_2.editable_elements.first
end
it 'exists in sub pages' do
@sub_page_1.editable_elements.size.should == 1
@sub_page_2.editable_elements.size.should == 1
end
it 'is marked as fixed' do
@sub_page_1_el.fixed?.should be_true
@sub_page_2_el.fixed?.should be_true
end
it 'enables the default content when it just got created' do
@sub_page_1_el.source?.should be_false
@sub_page_1_el.default_source_url.should == '/foo.png'
@sub_page_1_el.default_content?.should be_true
end
it 'gets also updated when updating the very first element' do
@home.editable_elements.first.source = FixturedAsset.open('5k.png')
@home.save; @sub_page_1.reload; @sub_page_2.reload
@sub_page_1.editable_elements.first.default_source_url.should be_true
@sub_page_1.editable_elements.first.default_source_url.should =~ /files\/5k.png$/
@sub_page_2.editable_elements.first.default_source_url.should be_true
@sub_page_2.editable_elements.first.default_source_url.should =~ /files\/5k.png$/
end
end
end

View File

@ -0,0 +1,27 @@
require 'spec_helper'
describe Locomotive::EditableLongText do
before(:each) do
@site = FactoryGirl.create(:site)
@home = @site.pages.root.first
@home.update_attributes :raw_template => "{% block body %}{% editable_long_text 'body' %}Lorem ipsum{% endeditable_long_text %}{% endblock %}"
@sub_page_1 = FactoryGirl.create(:page, :slug => 'sub_page_1', :parent => @home, :raw_template => "{% extends 'parent' %}")
end
it 'exists' do
@sub_page_1.editable_elements.size.should == 1
end
it 'has a non-nil slug' do
@sub_page_1.editable_elements.first.slug.should == 'body'
end
it 'does not have a content at first' do
@sub_page_1.editable_elements.first.content.should be_nil
@sub_page_1.editable_elements.first.default_content.should be_true
end
end

View File

@ -0,0 +1,114 @@
require 'spec_helper'
describe Locomotive::EditableShortText do
describe 'a simple case' do
before(:each) do
@site = FactoryGirl.create(:site)
@home = @site.pages.root.first
@home.update_attributes :raw_template => "{% block body %}{% editable_short_text 'body' %}Lorem ipsum{% endeditable_short_text %}{% endblock %}"
@sub_page_1 = FactoryGirl.create(:page, :slug => 'sub_page_1', :parent => @home, :raw_template => "{% extends 'parent' %}")
@sub_page_2 = FactoryGirl.create(:page, :slug => 'sub_page_2', :parent => @home, :raw_template => "{% extends 'parent' %}")
@sub_page_1_el = @sub_page_1.editable_elements.first
@sub_page_1_1 = FactoryGirl.create(:page, :slug => 'sub_page_1_1', :parent => @sub_page_1, :raw_template => "{% extends 'parent' %}")
end
context 'in sub pages level #1' do
before(:each) do
@sub_page_1.reload
@sub_page_2.reload
end
it 'exists' do
@sub_page_1.editable_elements.size.should == 1
@sub_page_2.editable_elements.size.should == 1
end
it 'has a non-nil slug' do
@sub_page_1.editable_elements.first.slug.should == 'body'
end
it 'does not have a content at first' do
@sub_page_1_el.content.should be_nil
@sub_page_1_el.default_content.should be_true
end
end
context 'in sub pages level #2' do
before(:each) do
@sub_page_1_1.reload
end
it 'exists' do
@sub_page_1_1.editable_elements.size.should == 1
end
it 'has a non-nil slug' do
@sub_page_1_1.editable_elements.first.slug.should == 'body'
end
it 'removes editable elements' do
@sub_page_1_1.editable_elements.destroy_all
@sub_page_1_1.reload
@sub_page_1_1.editable_elements.size.should == 0
end
end
end
describe '"sticky" elements' do
before(:each) do
@site = FactoryGirl.create(:site)
@home = @site.pages.root.first
@home.update_attributes :raw_template => "{% block body %}{% editable_short_text 'body', fixed: true %}Lorem ipsum{% endeditable_short_text %}{% endblock %}"
@home_el = @home.editable_elements.first
@sub_page_1 = FactoryGirl.create(:page, :slug => 'sub_page_1', :parent => @home, :raw_template => "{% extends 'parent' %}")
@sub_page_2 = FactoryGirl.create(:page, :slug => 'sub_page_2', :parent => @home, :raw_template => "{% extends 'parent' %}")
@sub_page_1_el = @sub_page_1.editable_elements.first
@sub_page_2_el = @sub_page_2.editable_elements.first
end
it 'exists in sub pages' do
@sub_page_1.editable_elements.size.should == 1
@sub_page_2.editable_elements.size.should == 1
end
it 'is marked as fixed' do
@sub_page_1_el.fixed?.should be_true
@sub_page_2_el.fixed?.should be_true
end
it 'enables the default content when it just got created' do
@sub_page_1_el.default_content?.should be_true
end
it 'disables the default content if the content changed' do
@sub_page_1_el.content = 'Bla bla'
@sub_page_1_el.default_content?.should be_false
end
it 'gets also updated when updating the very first element' do
@home_el.content = 'Hello world'
@home.save
@sub_page_1.reload; @sub_page_1_el = @sub_page_1.editable_elements.first
@sub_page_2.reload; @sub_page_2_el = @sub_page_2.editable_elements.first
@sub_page_1_el.content.should == 'Hello world'
@sub_page_2_el.content.should == 'Hello world'
end
end
end