new editable element for a page: the control

This commit is contained in:
Didier Lafforgue 2012-03-22 00:50:34 +01:00
parent ab5a4755b8
commit b3b0a5ac16
14 changed files with 132 additions and 235 deletions

View File

@ -32,6 +32,9 @@ class Locomotive.Views.EditableElements.EditAllView extends Backbone.View
view.model = @collection.get(view.model.get('id'))
view.refresh()
unbind_model: ->
_.each @_editable_elements_views, (view) => Backbone.ModelBinding.unbind(view)
render_elements: ->
index = 0
@ -45,6 +48,7 @@ class Locomotive.Views.EditableElements.EditAllView extends Backbone.View
when 'EditableShortText' then Locomotive.Views.EditableElements.ShortTextView
when 'EditableLongText' then Locomotive.Views.EditableElements.LongTextView
when 'EditableFile' then Locomotive.Views.EditableElements.FileView
when 'EditableControl' then Locomotive.Views.EditableElements.ControlView
view = new view_class(model: element)
@$("#block-#{block.index} > fieldset > ol").append(view.render().el)

View File

@ -90,6 +90,7 @@ class Locomotive.Views.Pages.FormView extends Locomotive.Views.Shared.FormView
# Just re-connect the model and the views (+ redraw the file fields)
refresh_editable_elements: ->
@editable_elements_view.unbind_model()
@editable_elements_view.collection = @model.get('editable_elements')
@editable_elements_view.refresh()

View File

@ -1,97 +1,101 @@
module Locomotive
class EditableElement
include ::Mongoid::Document
include ::Mongoid::Document
## fields ##
field :slug
field :block
field :hint
field :priority, :type => Integer, :default => 0
field :fixed, :type => Boolean, :default => false
field :disabled, :type => Boolean, :default => false, :localize => true
field :from_parent, :type => Boolean, :default => false
field :locales, :type => Array, :default => []
## fields ##
field :slug
field :block
field :hint
field :priority, :type => Integer, :default => 0
field :fixed, :type => Boolean, :default => false
field :disabled, :type => Boolean, :default => false, :localize => true
field :from_parent, :type => Boolean, :default => false
field :locales, :type => Array, :default => []
## associations ##
embedded_in :page, :class_name => 'Locomotive::Page', :inverse_of => :editable_elements
## associations ##
embedded_in :page, :class_name => 'Locomotive::Page', :inverse_of => :editable_elements
## validations ##
validates_presence_of :slug
## validations ##
validates_presence_of :slug
## callbacks ##
after_save :propagate_content, :if => :fixed?
## callbacks ##
after_save :propagate_content, :if => :fixed?
## scopes ##
scope :by_priority, :order_by => [[:priority, :desc]]
## scopes ##
scope :by_priority, :order_by => [[:priority, :desc]]
## methods ##
## methods ##
def disabled?
!!self.disabled # the original method does not work quite well with the localization
end
def disabled?
!!self.disabled # the original method does not work quite well with the localization
end
# Determines if the current element can be edited in the back-office
#
def editable?
!self.disabled? && self.locales.include?(::Mongoid::Fields::I18n.locale.to_s) && (!self.fixed? || !self.from_parent?)
end
# Determines if the current element can be edited in the back-office
#
def editable?
!self.disabled? && self.locales.include?(::Mongoid::Fields::I18n.locale.to_s) && (!self.fixed? || !self.from_parent?)
end
def _run_rearrange_callbacks
# callback from page/tree. not needed in the editable elements
end
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
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 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 locales from_parent).include?(attr) }
self.from_parent = true
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 locales from_parent).include?(attr) }
self.from_parent = true
end
# Make sure the current locale is added to the list
# of locales for the current element so that we know
# in which languages the element was translated.
#
def add_current_locale
locale = ::Mongoid::Fields::I18n.locale.to_s
self.locales << locale unless self.locales.include?(locale)
end
# Make sure the current locale is added to the list
# of locales for the current element so that we know
# in which languages the element was translated.
#
def add_current_locale
locale = ::Mongoid::Fields::I18n.locale.to_s
self.locales << locale unless self.locales.include?(locale)
end
protected
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 _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
# Update the value (or content) of the elements matching the same block/slug
# as the current element in all the pages inheriting from the current page.
# This method is called only if the element has the "fixed" property set to true.
# It also needs to be overridden for each kind of elements (file, short text, ...etc)
#
def propagate_content
true
end
end
end

View File

@ -1,12 +1,15 @@
module Locomotive
class EditableFile < EditableElement
## behaviours ##
mount_uploader 'source', EditableFileUploader
replace_field 'source', ::String, true
## fields ##
field :default_source_url, :localize => true
## callbacks ##
after_save :propagate_content
## methods ##

View File

@ -1,6 +1,8 @@
module Locomotive
class EditableLongText < EditableShortText
## methods ##
def as_json(options = {})
Locomotive::EditableLongTextPresenter.new(self).as_json
end

View File

@ -26,7 +26,7 @@ module Locomotive
end
def enabled_editable_elements
self.editable_elements.by_priority.find_all(&:editable?) # { |el| !el.editable? }
self.editable_elements.by_priority.find_all(&:editable?)
end
def editable_elements_grouped_by_blocks
@ -70,7 +70,7 @@ module Locomotive
new_el = self.editable_elements.build({}, el.class)
new_el.copy_attributes_from(el)
else
existing_el.disabled = false # = { :disabled => false }
existing_el.disabled = false
end
end
end

View File

@ -49,4 +49,16 @@
%p.inline-hints {{hint}}
{{/if}}
= hidden_field_tag 'page[editable_elements_attributes][{{index}}][id]', '{{id}}', :id => 'page_editable_elements_attributes_{{index}}_id'
%script{ :type => 'text/html', :id => 'editable_control_input' }
%label{ :for => 'page_editable_elements_attributes_{{index}}_content' } {{label}}
= select_tag 'page[editable_elements_attributes][{{index}}][content]', raw('{{#each options}}<option value="{{this.value}}" {{#if this.selected}}selected="selected"{{/if}}>{{this.text}}</option>{{/each}}'), :id => 'page_editable_elements_attributes_{{index}}_content', :class => 'content'
{{#if hint}}
%p.inline-hints {{hint}}
{{/if}}
= hidden_field_tag 'page[editable_elements_attributes][{{index}}][id]', '{{id}}', :id => 'page_editable_elements_attributes_{{index}}_id'

View File

@ -20,7 +20,7 @@ x edit my site
x domains
x roles
x save
- fix other sections
x fix other sections
x edit my account
x create a new site
x create a new accounts
@ -32,7 +32,7 @@ x edit my site
x import/export
x export
x site picker
- content types
x content types
x move content instances into their own collection
x manage custom_fields
x automatic name
@ -65,7 +65,7 @@ x edit my site
x bug text formatting
x custom_fields: use the appropriate icon to drag select options
x bug ui with contents popup
- use list_or_group_entries instead of ordered_entries
x use list_or_group_entries instead of ordered_entries
x i18n
x add locales a site responds to
x locale switcher
@ -90,7 +90,7 @@ x i18n
x deployment
x fix integration problems
x pre-compile assets
- API
x API
x authentication from a token + controller to deliver a token
x api routes
x change api location
@ -115,6 +115,8 @@ x heroku module for locomotive
x remove the import / export scripts
x remove the cross domain authentication (use auth_token instead)
- where to put Locomotive::InlineEditorMiddleware ?
x global regions: keyword in editable element (http://www.mongodb.org/display/DOCS/Updating)
x override sort for contents
- bugs / ui tweaks
x unable to toggle the "required" check_boxes for content types
@ -139,7 +141,6 @@ BACKLOG:
- html view in the aloha popup
- editable elements should wrap a tag: div, h1, ...etc (default span)
- edit images (upload new ones, ...etc) => wait for aloha or send them an email ?
- global regions: keyword in editable element (http://www.mongodb.org/display/DOCS/Updating)
- cucumber features for admin pages (in progress)
(- duostack/doutcloud version)
- write my first tutorial about locomotive
@ -151,7 +152,7 @@ BACKLOG:
- sync data
- import only theme assets
- endless pagination
- override sort for contents
- tooltip to explain the difference between 1.) Admin 2.) Author 3.) Designer?
- [bushido] guiders / welcome page / devise cas authentication (SSO)

View File

@ -125,7 +125,7 @@ Scenario: Insert editable files
"""
My application file is /default.pdf
"""
Scenario: Render liquid variable used as default editable file name
Given a page named "hello-world" with the template:
"""
@ -135,7 +135,17 @@ Scenario: Render liquid variable used as default editable file name
When I view the rendered page at "/hello-world"
Then the rendered output should look like:
"""
My application file is /different-default.pdf
"""
Scenario: Simple select element
Given a page named "hello-world" with the template:
"""
{% block menuecontent %}{% editable_control 'menueposition', options: 'top=Top of the Page,bottom=Bottom of the Page' %}bottom{% endeditable_control %}{% endblock %}
"""
When I view the rendered page at "/hello-world"
Then the rendered output should look like:
"""
bottom
"""

View File

@ -1,5 +1,5 @@
When /^I follow image link "([^"]*)"$/ do |img_alt|
find(:xpath, "//img[@alt = '#{img_alt}']/parent::a").click()
find(:xpath, "//img[@alt = '#{img_alt}']/parent::a").click()
end
Then /^I should get a download with the filename "([^\"]*)"$/ do |filename|
@ -31,3 +31,11 @@ Then /^"([^"]*)" should( not)? be an option for "([^"]*)"(?: within "([^\"]*)")?
field_labeled(field).first(:xpath, ".//option[text() = '#{value}']").send(expectation, be_present)
end
end
Then /^"([^"]*)" should be selected for "([^"]*)"$/ do |value, field|
assert page.has_xpath?("//select[@name='#{field}' and option[@selected and contains(text(), '#{value}')]]")
end
When /^I reload the page$/ do
visit current_path
end

View File

@ -2,4 +2,4 @@ require 'locomotive/liquid/tags/editable/base'
require 'locomotive/liquid/tags/editable/short_text'
require 'locomotive/liquid/tags/editable/long_text'
require 'locomotive/liquid/tags/editable/file'
require 'locomotive/liquid/tags/editable/content'
require 'locomotive/liquid/tags/editable/control'

View File

@ -1,49 +0,0 @@
module Locomotive
module Liquid
module Tags
module Editable
class Content < ::Liquid::Tag
Syntax = /(#{::Liquid::Expression}+)?/
def initialize(tag_name, markup, tokens, context)
if markup =~ Syntax
@slug = $1
@options = { :inherit => false }
markup.scan(::Liquid::TagAttributes) { |key, value| @options[key.to_sym] = value.gsub(/"|'/, '') }
else
raise ::Liquid::SyntaxError.new("Syntax Error in 'content' - Valid syntax: slug")
end
super
end
def render(context)
page = context.registers[:page]
element = find_element(page)
if element.nil? && @options[:inherit] != false
while page.parent.present? && element.nil?
page = page.parent
element = find_element(page)
end
end
if element.present?
return element.content
else
raise ::Liquid::SyntaxError.new("Error in 'content' - Can't find editable element called `#{@slug}`")
end
end
def find_element(page)
page.editable_elements.where(:slug => @slug).first
end
end
::Liquid::Template.register_tag('content', Content)
end
end
end
end

View File

@ -6,8 +6,6 @@ module Locomotive
protected
protected
def default_element_attributes
if @nodelist.first.is_a?(String)
super.merge(:default_source_url => @nodelist.first.try(:to_s))

View File

@ -1,97 +0,0 @@
require 'spec_helper'
describe Locomotive::Liquid::Tags::Editable::Content do
before :each do
Locomotive::Liquid::Tags::Editable::Content.any_instance.stubs(:end_tag).returns(true)
end
context 'syntax' do
it 'should have a valid syntax' do
["slug", "slug, inherit: true"].each do |markup|
lambda do
Locomotive::Liquid::Tags::Editable::Content.new('content', markup, ["{% content %}"], {})
end.should_not raise_error
end
end
end
context 'output' do
before :each do
Locomotive::EditableElement.any_instance.stubs(:content).returns("test string")
end
context 'inheriting from a parent' do
before :each do
@parent = FactoryGirl.build(:page)
@child = FactoryGirl.build(:page)
@child.stubs(:parent).returns(@parent)
end
it 'should return the parents field if inherit is set' do
@element = @parent.editable_elements.create(:slug => 'test')
@child.stubs(:raw_template).returns("{% content test, inherit: true %}")
template = Liquid::Template.parse(@child.raw_template)
text = template.render!(liquid_context(:page => @child))
text.should match /test string/
end
it 'should raise an exception if it cant find the field' do
@child.stubs(:raw_template).returns("{% content test, inherit: true %}")
template = Liquid::Template.parse(@child.raw_template)
lambda do
template.render!(liquid_context(:page => @child))
end.should raise_error
end
after :each do
@parent.editable_elements.destroy_all
end
end
context 'reading from the same page' do
before :each do
@page = FactoryGirl.build(:page)
end
it 'should return the previously defined field' do
@element = @page.editable_elements.create(:slug => 'test')
@page.stubs(:raw_template).returns("{% content test %}")
template = Liquid::Template.parse(@page.raw_template)
text = template.render!(liquid_context(:page => @page))
text.should match /test string/
end
it 'should raise an exception if it wasnt defined' do
@page.stubs(:raw_template).returns("{% content test %}")
template = Liquid::Template.parse(@page.raw_template)
lambda do
template.render!(liquid_context(:page => @page))
end.should raise_error
end
after :each do
@page.editable_elements.destroy_all
end
end
end
# ___ helpers methods ___ #
def liquid_context(options = {})
::Liquid::Context.new({}, {},
{
:page => options[:page]
}, true)
end
end