editable_file tag is implemented + remove not used editable elements all in once for better performance + default content inherits from the content of the parent element
This commit is contained in:
parent
335d7a1aac
commit
35b4e5358c
4
Gemfile
4
Gemfile
@ -3,8 +3,8 @@ source 'http://rubygems.org'
|
||||
|
||||
gem 'rails', '3.0.0.rc'
|
||||
|
||||
# gem 'liquid', :path => '../gems/liquid' # local
|
||||
gem 'liquid', :git => 'git://github.com/locomotivecms/liquid.git', :ref => '9ec570927f5281e1f397'
|
||||
gem 'liquid', :path => '../gems/liquid' # local
|
||||
# gem 'liquid', :git => 'git://github.com/locomotivecms/liquid.git', :ref => '9ec570927f5281e1f397'
|
||||
|
||||
gem 'bson_ext', '1.0.4'
|
||||
gem 'mongoid', '2.0.0.beta.16'
|
||||
|
12
Gemfile.lock
12
Gemfile.lock
@ -11,13 +11,6 @@ GIT
|
||||
specs:
|
||||
custom_fields (0.0.0.1)
|
||||
|
||||
GIT
|
||||
remote: git://github.com/locomotivecms/liquid.git
|
||||
revision: 9ec5709
|
||||
ref: 9ec570927f5281e1f397
|
||||
specs:
|
||||
liquid (2.1.3)
|
||||
|
||||
GIT
|
||||
remote: http://github.com/ianwhite/pickle.git
|
||||
revision: 65ba8b7
|
||||
@ -28,6 +21,11 @@ GIT
|
||||
rspec (>= 1.3)
|
||||
yard
|
||||
|
||||
PATH
|
||||
remote: /Users/didier/Desktop/NoCoffee/LocomotiveCMS/gems/liquid
|
||||
specs:
|
||||
liquid (2.1.3)
|
||||
|
||||
GEM
|
||||
remote: http://rubygems.org/
|
||||
specs:
|
||||
|
@ -3,10 +3,10 @@ class EditableElement
|
||||
include Mongoid::Document
|
||||
|
||||
## fields ##
|
||||
field :kind
|
||||
# field :kind
|
||||
field :slug
|
||||
field :block
|
||||
field :content
|
||||
# field :content
|
||||
field :default_content
|
||||
field :hint
|
||||
field :disabled, :type => Boolean, :default => false
|
||||
@ -20,12 +20,12 @@ class EditableElement
|
||||
|
||||
## methods ##
|
||||
|
||||
def content
|
||||
self.read_attribute(:content).blank? ? self.default_content : self.read_attribute(:content)
|
||||
end
|
||||
|
||||
def short_text?; self.kind == 'ShortText'; end
|
||||
|
||||
def long_text?; self.kind == 'LongText'; end
|
||||
# def content
|
||||
# self.read_attribute(:content).blank? ? self.default_content : self.read_attribute(:content)
|
||||
# end
|
||||
#
|
||||
# def short_text?; self._type == 'EditableShortText'; end
|
||||
#
|
||||
# def long_text?; self._type == 'EditableLongText'; end
|
||||
|
||||
end
|
9
app/models/editable_file.rb
Normal file
9
app/models/editable_file.rb
Normal file
@ -0,0 +1,9 @@
|
||||
class EditableFile < EditableElement
|
||||
|
||||
mount_uploader :source, EditableFileUploader
|
||||
|
||||
def content
|
||||
self.source? ? self.source.url : self.default_content
|
||||
end
|
||||
|
||||
end
|
3
app/models/editable_long_text.rb
Normal file
3
app/models/editable_long_text.rb
Normal file
@ -0,0 +1,3 @@
|
||||
class EditableLongText < EditableShortText
|
||||
|
||||
end
|
12
app/models/editable_short_text.rb
Normal file
12
app/models/editable_short_text.rb
Normal file
@ -0,0 +1,12 @@
|
||||
class EditableShortText < EditableElement
|
||||
|
||||
## fields ##
|
||||
field :content
|
||||
|
||||
## methods ##
|
||||
|
||||
def content
|
||||
self.read_attribute(:content).blank? ? self.default_content : self.read_attribute(:content)
|
||||
end
|
||||
|
||||
end
|
@ -8,6 +8,13 @@ module Models
|
||||
included do
|
||||
embeds_many :editable_elements
|
||||
|
||||
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
|
||||
|
||||
@ -26,21 +33,26 @@ module Models
|
||||
end
|
||||
|
||||
def editable_elements_grouped_by_blocks
|
||||
groups = self.editable_elements.group_by(&:block)
|
||||
groups.delete_if { |block, elements| elements.all? { |el| el.disabled? } }
|
||||
all_enabled = self.editable_elements.reject { |el| el.disabled? }
|
||||
groups = all_enabled.group_by(&:block)
|
||||
groups.delete_if { |block, elements| elements.empty? }
|
||||
end
|
||||
|
||||
def find_editable_element(block, slug)
|
||||
self.editable_elements.detect { |el| el.block == block && el.slug == slug }
|
||||
end
|
||||
|
||||
def add_or_update_editable_element(attributes)
|
||||
def find_editable_files
|
||||
self.editable_elements.find_all { |el| el.respond_to?(:source) }
|
||||
end
|
||||
|
||||
def add_or_update_editable_element(attributes, type)
|
||||
element = self.find_editable_element(attributes[:block], attributes[:slug])
|
||||
|
||||
if element
|
||||
element.attributes = attributes
|
||||
else
|
||||
self.editable_elements.build(attributes)
|
||||
self.editable_elements.build(attributes, type)
|
||||
end
|
||||
end
|
||||
|
||||
@ -50,18 +62,52 @@ module Models
|
||||
|
||||
def merge_editable_elements_from_page(source)
|
||||
source.editable_elements.each do |el|
|
||||
puts "\t*** merging #{el.class} / #{el.slug} / #{el.block} / #{el.disabled?} / #{el.from_parent?}"
|
||||
|
||||
next if el.disabled?
|
||||
|
||||
existing_el = self.find_editable_element(el.block, el.slug)
|
||||
|
||||
if existing_el.nil? # new one from parents
|
||||
self.editable_elements.build(el.attributes.merge(:from_parent => true))
|
||||
new_attributes = el.attributes.merge(:from_parent => true)
|
||||
new_attributes[:default_content] = el.content
|
||||
|
||||
foo = self.editable_elements.build(new_attributes, el.class)
|
||||
puts "\t\t*** building #{foo.inspect} / #{foo.valid?} / #{foo.errors.full_messages.inspect}"
|
||||
else
|
||||
existing_el.disabled = false
|
||||
existing_el.attributes = { :disabled => false, :default_content => el.content }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def remove_disabled_editable_elements
|
||||
return unless self.editable_elements.any? { |el| el.disabled? }
|
||||
|
||||
puts "*** removing #{self.editable_elements.find_all { |el| el.disabled? }.size} elements"
|
||||
|
||||
# super fast way to remove useless elements all in once (TODO callbacks)
|
||||
self.collection.update(self._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
|
||||
|
@ -93,9 +93,11 @@ module Models
|
||||
end
|
||||
|
||||
direct_descendants.each do |page|
|
||||
puts "*** #{page.fullpath} descendant of #{self.fullpath}"
|
||||
page.send(:_parse_and_serialize_template, { :cached_parent => self, :cached_pages => cached })
|
||||
|
||||
page.send(:_update_direct_template_descendants, template_descendants, cached)
|
||||
puts "-------- done -----------"
|
||||
end
|
||||
end
|
||||
|
||||
|
11
app/uploaders/editable_file_uploader.rb
Normal file
11
app/uploaders/editable_file_uploader.rb
Normal file
@ -0,0 +1,11 @@
|
||||
class EditableFileUploader < ::CarrierWave::Uploader::Base
|
||||
|
||||
def store_dir
|
||||
"sites/#{model.page.site_id}/pages/#{model.page.id}/files"
|
||||
end
|
||||
|
||||
def cache_dir
|
||||
"#{Rails.root}/tmp/uploads"
|
||||
end
|
||||
|
||||
end
|
@ -22,7 +22,7 @@
|
||||
= form.custom_input field._alias.to_sym, :label => field.label, :hint => field.hint, :css => 'file' do
|
||||
= form.file_field field._name.to_sym
|
||||
- if form.object.send(:"#{field._name}?")
|
||||
%p
|
||||
%p.remove
|
||||
%strong
|
||||
= link_to File.basename(form.object.send(field._name).url), form.object.send(field._name).url
|
||||
%span
|
||||
|
@ -17,7 +17,19 @@
|
||||
%ol
|
||||
- elements.each_with_index do |el, index|
|
||||
= f.fields_for 'editable_elements', el, :child_index => el._index do |g|
|
||||
- if el.short_text?
|
||||
= g.input :content, :label => el.slug.humanize, :hint => el.hint
|
||||
- elsif el.long_text?
|
||||
- case el
|
||||
- when EditableLongText
|
||||
= g.input :content, :label => el.slug.humanize, :hint => el.hint, :as => :text, :input_html => { :class => 'html' }
|
||||
- when EditableShortText
|
||||
= g.input :content, :label => el.slug.humanize, :hint => el.hint
|
||||
- when EditableFile
|
||||
= g.custom_input :source, :label => el.slug.humanize, :hint => el.hint, :css => 'file' do
|
||||
= g.file_field :source
|
||||
- if el.source?
|
||||
%p.remove
|
||||
%strong
|
||||
= link_to File.basename(el.source.url), el.source.url
|
||||
%span
|
||||
/
|
||||
!= t('admin.pages.form.delete_file')
|
||||
= g.check_box :remove_source
|
@ -8,7 +8,7 @@
|
||||
|
||||
%p!= t('.help')
|
||||
|
||||
= semantic_form_for @page, :url => admin_page_url(@page), :html => { :class => 'save-with-shortcut' } do |form|
|
||||
= semantic_form_for @page, :url => admin_page_url(@page), :html => { :class => 'save-with-shortcut', :multipart => true } do |form|
|
||||
|
||||
= render 'form', :f => form
|
||||
|
||||
|
@ -99,6 +99,7 @@ en:
|
||||
help: "The page title can be updated by clicking it."
|
||||
ask_for_title: "Please type the new page title"
|
||||
form:
|
||||
delete_file: Delete file
|
||||
default_block: Default
|
||||
cache_strategy:
|
||||
none: None
|
||||
|
@ -99,6 +99,7 @@ fr:
|
||||
help: "Le titre de la page est modifiable en cliquant dessus."
|
||||
ask_for_title: "Veuillez entrer le nouveau titre"
|
||||
form:
|
||||
delete_file: Supprimer fichier
|
||||
default_block: Défaut
|
||||
cache_strategy:
|
||||
none: Aucun
|
||||
|
6
doc/TODO
6
doc/TODO
@ -8,7 +8,11 @@ x theme assets selector in page editor
|
||||
x saving page in ajax
|
||||
x editable_long_text tag
|
||||
x blocking issue when modifying the parent of 2 templates => one of the 2 children has reference of the first child
|
||||
- editable_file tag
|
||||
x editable_file tag
|
||||
x stylish file field
|
||||
x remove not used editable element all in once
|
||||
x default content from parent editable element
|
||||
x unable to upload/remove editable file
|
||||
|
||||
- refactor slugify method (use parameterize + create a module)
|
||||
- [content types] the "display column" selector should not include file types
|
||||
|
@ -114,3 +114,16 @@ Scenario: Combine inheritance and update
|
||||
Another Main
|
||||
Default sidebar title
|
||||
"""
|
||||
|
||||
Scenario: Insert editable files
|
||||
Given a page named "hello-world" with the template:
|
||||
"""
|
||||
My application file is {% editable_file 'a_file', hint: 'please enter a new file' %}/default.pdf{% endeditable_file %}
|
||||
"""
|
||||
When I view the rendered page at "/hello-world"
|
||||
Then the rendered output should look like:
|
||||
"""
|
||||
My application file is /default.pdf
|
||||
"""
|
||||
|
||||
|
||||
|
@ -1,2 +1,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'
|
60
lib/locomotive/liquid/tags/editable/base.rb
Normal file
60
lib/locomotive/liquid/tags/editable/base.rb
Normal file
@ -0,0 +1,60 @@
|
||||
module Locomotive
|
||||
module Liquid
|
||||
module Tags
|
||||
module Editable
|
||||
class Base < ::Liquid::Block
|
||||
|
||||
Syntax = /(#{::Liquid::QuotedFragment})(\s*,\s*#{::Liquid::Expression}+)?/
|
||||
|
||||
def initialize(tag_name, markup, tokens, context)
|
||||
if markup =~ Syntax
|
||||
@slug = $1.gsub('\'', '')
|
||||
@options = {}
|
||||
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>)")
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def end_tag
|
||||
@context[:page].add_or_update_editable_element({
|
||||
:block => @context[:current_block].try(:name),
|
||||
:slug => @slug,
|
||||
:hint => @options[:hint],
|
||||
:default_content => @nodelist.first.to_s,
|
||||
:disabled => false,
|
||||
:from_parent => false
|
||||
}, document_type)
|
||||
end
|
||||
|
||||
def render(context)
|
||||
current_page = context.registers[:page]
|
||||
|
||||
element = current_page.find_editable_element(context['block'].try(:name), @slug)
|
||||
|
||||
if element
|
||||
render_element(element)
|
||||
else
|
||||
Locomotive.logger "[editable element] missing element #{context[:block].name} / #{@slug}"
|
||||
''
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def render_element(element)
|
||||
raise 'FIXME: has to be overidden'
|
||||
end
|
||||
|
||||
def document_type
|
||||
raise 'FIXME: has to be overidden'
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
23
lib/locomotive/liquid/tags/editable/file.rb
Normal file
23
lib/locomotive/liquid/tags/editable/file.rb
Normal file
@ -0,0 +1,23 @@
|
||||
module Locomotive
|
||||
module Liquid
|
||||
module Tags
|
||||
module Editable
|
||||
class File < Base
|
||||
|
||||
protected
|
||||
|
||||
def render_element(element)
|
||||
element.source? ? element.source.url : element.default_content
|
||||
end
|
||||
|
||||
def document_type
|
||||
EditableFile
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
::Liquid::Template.register_tag('editable_file', File)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -3,6 +3,13 @@ module Locomotive
|
||||
module Tags
|
||||
module Editable
|
||||
class LongText < ShortText
|
||||
|
||||
protected
|
||||
|
||||
def document_type
|
||||
EditableLongText
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
::Liquid::Template.register_tag('editable_long_text', LongText)
|
||||
|
@ -2,51 +2,16 @@ module Locomotive
|
||||
module Liquid
|
||||
module Tags
|
||||
module Editable
|
||||
class ShortText < ::Liquid::Block
|
||||
|
||||
Syntax = /(#{::Liquid::QuotedFragment})(\s*,\s*#{::Liquid::Expression}+)?/
|
||||
|
||||
def initialize(tag_name, markup, tokens, context)
|
||||
if markup =~ Syntax
|
||||
@slug = $1.gsub('\'', '')
|
||||
@options = {}
|
||||
markup.scan(::Liquid::TagAttributes) { |key, value| @options[key.to_sym] = value.gsub(/^'/, '').gsub(/'$/, '') }
|
||||
else
|
||||
raise ::Liquid::SyntaxError.new("Syntax Error in 'editable_short_text' - Valid syntax: editable_short_text <slug>(, <options>)")
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def end_tag
|
||||
@context[:page].add_or_update_editable_element({
|
||||
:kind => self.kind,
|
||||
:block => @context[:current_block].try(:name),
|
||||
:slug => @slug,
|
||||
:hint => @options[:hint],
|
||||
:default_content => @nodelist.first.to_s,
|
||||
:disabled => false,
|
||||
:from_parent => false
|
||||
})
|
||||
end
|
||||
|
||||
def render(context)
|
||||
current_page = context.registers[:page]
|
||||
|
||||
element = current_page.find_editable_element(context['block'].try(:name), @slug)
|
||||
|
||||
if element
|
||||
element.content
|
||||
else
|
||||
Locomotive.logger "[editable short text] missing editable short text #{context[:block].name} / #{@slug}"
|
||||
''
|
||||
end
|
||||
end
|
||||
class ShortText < Base
|
||||
|
||||
protected
|
||||
|
||||
def kind
|
||||
self.class.name.demodulize
|
||||
def render_element(element)
|
||||
element.content
|
||||
end
|
||||
|
||||
def document_type
|
||||
EditableShortText
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -8,6 +8,8 @@ module Locomotive
|
||||
|
||||
parent_page = @context[:parent_page]
|
||||
|
||||
@context[:page].merge_editable_elements_from_page(parent_page)
|
||||
|
||||
@context[:snippets] = parent_page.snippet_dependencies
|
||||
@context[:templates] = ([*parent_page.template_dependencies] + [parent_page.id]).compact
|
||||
end
|
||||
@ -29,8 +31,6 @@ module Locomotive
|
||||
|
||||
raise PageNotFound.new("Page with fullpath '#{@template_name}' was not found") if @context[:parent_page].nil?
|
||||
|
||||
@context[:page].merge_editable_elements_from_page(@context[:parent_page])
|
||||
|
||||
@context[:parent_page].template
|
||||
end
|
||||
|
||||
|
@ -15,3 +15,58 @@ module Mongoid #:nodoc:
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
# http://github.com/emk/mongoid/blob/503e346b1b7b250d682a12332ad9d5872f1575e6/lib/mongoid/atomicity.rb
|
||||
module Mongoid #:nodoc:
|
||||
module Atomicity #:nodoc:
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def _updates
|
||||
processed = {}
|
||||
|
||||
_children.inject({ "$set" => _sets, "$pushAll" => {}, :other => {} }) do |updates, child|
|
||||
changes = child._sets
|
||||
updates["$set"].update(changes)
|
||||
unless changes.empty?
|
||||
processed[child._conficting_modification_key] = true
|
||||
end
|
||||
|
||||
# MongoDB does not allow "conflicting modifications" to be
|
||||
# performed in a single operation. Conflicting modifications are
|
||||
# detected by the 'haveConflictingMod' function in MongoDB.
|
||||
# Examination of the code suggests that two modifications (a $set
|
||||
# and a $pushAll, for example) conflict if (1) the key paths being
|
||||
# modified are equal or (2) one key path is a prefix of the other.
|
||||
# So a $set of 'addresses.0.street' will conflict with a $pushAll
|
||||
# to 'addresses', and we will need to split our update into two
|
||||
# pieces. We do not, however, attempt to match MongoDB's logic
|
||||
# exactly. Instead, we assume that two updates conflict if the
|
||||
# first component of the two key paths matches.
|
||||
if processed.has_key?(child._conficting_modification_key)
|
||||
target = :other
|
||||
else
|
||||
target = "$pushAll"
|
||||
end
|
||||
|
||||
child._pushes.each do |attr, val|
|
||||
if updates[target].has_key?(attr)
|
||||
updates[target][attr] << val
|
||||
else
|
||||
updates[target].update({attr => [val]})
|
||||
end
|
||||
end
|
||||
updates
|
||||
end.delete_if do |key, value|
|
||||
value.empty?
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
# Get the key used to check for conflicting modifications. For now, we
|
||||
# just use the first component of _path, and discard the first period
|
||||
# and everything that follows.
|
||||
def _conficting_modification_key
|
||||
_path.sub(/\..*/, '')
|
||||
end
|
||||
end
|
||||
end
|
@ -59,5 +59,6 @@
|
||||
|
||||
#editable-elements .wrapper ul li fieldset ol { margin-top: 0px; border-top: 0px; background: #EBEDF4; }
|
||||
#editable-elements .wrapper ul li fieldset ol li label { padding-left: 0px; padding-right: 3em; }
|
||||
#editable-elements .wrapper ul li fieldset ol li p.remove,
|
||||
#editable-elements .wrapper ul li fieldset ol li p.inline-hints { margin-left: 13.3em; }
|
||||
|
||||
|
@ -380,22 +380,22 @@ form.formtastic fieldset.validations ol li.added em.key {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
/* ___ content instance ___ */
|
||||
/* ___ content instance / editable elements___ */
|
||||
|
||||
form.content_instance fieldset ol li.text textarea {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
form.content_instance fieldset ol li.file p {
|
||||
form.formtastic fieldset ol li.file p.remove {
|
||||
margin: 5px 0 0 20%;
|
||||
}
|
||||
|
||||
form.content_instance fieldset ol li.file p a {
|
||||
form.formtastic fieldset ol li.file p.remove a {
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
form.content_instance fieldset ol li.file p a:hover { text-decoration: underline; }
|
||||
form.formtastic fieldset ol li.file p.remove a:hover { text-decoration: underline; }
|
||||
|
||||
/* ___ my account ___ */
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user