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:
dinedine 2010-08-31 23:53:30 +02:00
parent 335d7a1aac
commit 35b4e5358c
25 changed files with 306 additions and 81 deletions

View File

@ -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'

View File

@ -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:

View File

@ -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

View File

@ -0,0 +1,9 @@
class EditableFile < EditableElement
mount_uploader :source, EditableFileUploader
def content
self.source? ? self.source.url : self.default_content
end
end

View File

@ -0,0 +1,3 @@
class EditableLongText < EditableShortText
end

View 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

View File

@ -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

View File

@ -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

View 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

View File

@ -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

View File

@ -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?
= g.input :content, :label => el.slug.humanize, :hint => el.hint, :as => :text, :input_html => { :class => 'html' }
- 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
&nbsp;/&nbsp;
!= t('admin.pages.form.delete_file')
= g.check_box :remove_source

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -113,4 +113,17 @@ Scenario: Combine inheritance and update
My application says Hello world
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
"""

View File

@ -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/long_text'
require 'locomotive/liquid/tags/editable/file'

View 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

View 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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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; }

View File

@ -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 ___ */