lib/spontaneous/field/base.rb
module Spontaneous
module Field
class Base
module ClassMethods
def has_editor(js_class = ui_class)
define_singleton_method(:editor_class) { js_class }
end
def register(*labels)
labels = self.labels if labels.empty?
# logger.debug("Registering #{self} as #{labels.join(", ")}")
Field.register(self, *labels)
self
end
def labels
[self.name.demodulize.gsub(/Field$/, '').underscore]
end
def inherited(subclass, real_caller = nil)
if self.respond_to?(:editor_class)
editor_class = self.editor_class
subclass.singleton_class.send(:define_method, :editor_class) do
editor_class
end
end
end
def prototype=(prototype)
@prototype = prototype
end
def prototype
@prototype
end
def accepts
%w(text/.+)
end
def accepts?(mime_type)
accepts.find do |pattern|
Regexp.new(pattern).match(mime_type)
end
end
def default_options
{}
end
# Provides the ability for specific field types to customize the schema values
# they return to the UI
def export(user)
{}
end
# Allows for field type classes to map a human readable default value
# to the correct serialized value
def make_default_value(instance, value)
value
end
end
extend ClassMethods
include Spontaneous::Model::Core::ContentHash::FieldMethods
attr_accessor :owner, :name, :unprocessed_value, :template_params, :version
attr_accessor :prototype
def initialize(params={}, default_values=true)
@default_values = default_values
@processed_values = {}
deserialize(params, default_values)
@values = nil
end
def processed_values
@values ||= processed_values_with_fallback
end
alias_method :values, :processed_values
ValueHash = Spontaneous::Collections::HashWithFallback
def processed_values_with_fallback
return @processed_values if owner.nil? || prototype.fallback.nil?
fallback = owner.fields[prototype.fallback]
if fallback.nil?
logger.warn("Missing field '#{prototype.fallback}' specified as fallback for field #{owner.class}::#{name}")
return @processed_values
end
test = proc { |val| self.blank? }
ValueHash.new(fallback, test).update(@processed_values)
end
def [](key)
processed_values[key]
end
def id
[owner.id, schema_id].join("/")
end
# This is used exclusively to compute the filename/path of
# temp files.
#
# To avoid creating a deep hierarchy under /media/tmp
# which would be hard to cleanup we replace all dir separators
# with underscores.
#
# The timestamp is added because otherwise serial modifications
# to the same field might overwrite the value (though this is
# unlikely)
def media_id
ids = [owner.id, schema_id, timestamp]
ids.join("_").gsub(/\//, "_")
end
def writable?(user)
owner.field_writable?(user, name)
end
# If a field type needs to do some long running processing
# then it should declare itself as asynchronous so as not to tie up
# the CMS process.
def asynchronous?
false
end
def pending_value=(value)
set_pending_value(value, Spontaneous::Site.instance)
end
def set_pending_value(value, site)
values[:__pending__] = {
:value => value,
:version => version + 1,
:timestamp => timestamp
}
end
# A timestamp value consistent for a particular field instance
#
# This is used to solve conflicts for async updates and to
# tag a tempfile to a particular pending value.
def timestamp
@timestamp ||= Spontaneous::Field.timestamp
end
def pending_value
values[:__pending__]
end
def has_pending_value?
values.key?(:__pending__) && values[:__pending__].key?(:value)
end
def clear_pending_value
values.delete(:__pending__)
end
def process_pending_value(site = Spontaneous::Site.instance)
if (pending = process_pending_value!(site))
cleanup_pending_value!(pending)
end
save
end
def process_pending_value!(site)
if has_pending_value?
pending = pending_value
@previous_values = values.dup
set_value!(pending_value[:value], true, site)
pending
end
end
# Ensures that this update can still run
def invalid_update?
return true if reload.nil?
false
end
# Ensures that the pending value we have hasn't been superceded by
# a later one.
def conflicted_update?
return false if is_valid_pending_value?
self.processed_values = @previous_values
true
end
# Reloads the field and compares the timestamps -- if our timestamp
# is the same or greater than the reloaded value then we are the
# most up-to-date update available. If not then we're not and
# should abort.
def is_valid_pending_value?
return true if @previous_values.nil?
reloaded = reload
pending = @previous_values[:__pending__] || {}
p1 = pending[:timestamp] || 0
p2 = (reloaded.pending_value || {})[:timestamp] || 0
if p1 >= p2
true
else
@previous_values = reloaded.values
false
end
end
def cleanup_pending_value!(pending)
clear_pending_value
if pending && (v = pending[:value]) && v.is_a?(Hash)
if (tempfile = v[:tempfile]) && ::File.exist?(tempfile)
FileUtils.rm_r(::File.dirname(tempfile))
end
end
end
def reload
owner.model.scope! do
Spontaneous::Field.find(owner.model, id)
end
end
# Called by Field::Update before launching the background
# task that updates the field values.
def before_asynchronous_update
end
def page_lock_description
"Updating to new value"
end
def outputs
[:html, :plain]
end
def process_value(value, site)
@modified = (@initial_value != value)
increment_version if @modified
self.processed_values = generate_outputs(@unprocessed_value, site)
end
def set_value(v, process = true)
set_value!(v, process, Spontaneous::Site.instance)
save
end
def set_value!(v, process = true, site = nil)
set_unprocessed_value(v)
process_value(v, site) if process
end
def modified!
owner.field_modified!(self) if owner
end
def increment_version
self.version += 1
end
def version
@version ||= 0
end
def pending_version
return version unless has_pending_value?
pending_value[:version]
end
def conflicts_version?(v)
(version != v) && (pending_version != v)
end
# value used to show conflicts between the current value and the value they're attempting to enter
def conflicted_value
unprocessed_value
end
def generate_outputs(value, site)
values = {}
value = preprocess(value, site)
outputs.each do |output|
process_method = "generate_#{output}"
values[output] = \
if respond_to?(process_method)
send(process_method, value, site)
else
generate(output, value, site)
end
end
values
end
# should be overwritten in subclasses that actually do something
# with the field value
def preprocess(value, site)
value
end
HTML_ESCAPE_TABLE = {
'&' => '&'
}
def escape_html(value)
value.to_s.gsub(%r{[#{HTML_ESCAPE_TABLE.keys.join}]}) { |s| HTML_ESCAPE_TABLE[s] }
end
def generate(output, value, site)
value
end
# attr_accessor :values
# override this to return custom values derived from (un)processed_value
# alias_method :value, :processed_value
def value(format=:html)
format = format.to_sym
return unprocessed_value unless processed_values.key?(format)
processed_values[format]
end
alias_method :processed_value, :value
def image?
false
end
def indexable_value
unprocessed_value
end
def to_s(format = :html)
value(format).to_s
end
def render(format = :html, locals = {}, *args)
value(format)
end
alias_method :render_inline, :render
def render_using(renderer, format = :html, locals = {}, *args)
render(format, locals)
end
alias_method :render_inline_using, :render_using
def to_html(locals = {})
value(:html)
end
def value=(value)
self.set_value value, true
end
alias_method :unprocessed_value=, :value=
def save
owner.field_modified!(self) if owner
end
def mark_unmodified
@modified = false
end
def modified?
@modified || false
end
def schema_id
self.prototype.schema_id
end
def schema_name
self.prototype.schema_name
end
def schema_owner
self.prototype.owner
end
def site
owner.try(:site)
end
def owner_sid
schema_owner.schema_id
end
def serialize_db
S::Field.serialize_field(self)
end
# def start_inline_edit_marker
# "spontaneous:previewedit:start:field id:#{owner.id} name:#{self.name}"
# end
# def end_inline_edit_marker
# "spontaneous:previewedit:end:field id:#{owner.id} name:#{self.name}"
# end
def export(user)
{
:name => name.to_s,
:id => schema_id.to_s,
:unprocessed_value => unprocessed_value,
:processed_value => ui_preview_value,
:version => version
}
end
def ui_preview_value
value(:html)
end
def inspect
%(#<#{self.class.name}:#{self.object_id} #{self.serialize_db.inspect}>)
end
def blank?
unprocessed_value.blank?
end
def empty?
blank?
end
def or(field)
return field if self.blank?
self
end
alias_method :'/', :or
alias_method :'|', :or
def versions
owner.field_versions(self)
end
def previous_version
versions.first
end
def create_version
Spontaneous::Field::FieldVersion.create(
:content_id => owner.id,
:field_sid => self.schema_id.to_s,
:version => version,
:value => @initial_value,
:user => owner.current_editor)
mark_unmodified
end
def <=>(o)
unprocessed_value <=> o.unprocessed_value
end
def ==(o)
eql?(o)
end
def eql?(o)
super || (o.class == self.class &&
o.id == id &&
o.unprocessed_value == unprocessed_value &&
o.values == values)
end
def hash
id.hash
end
protected
def deserialize(params={}, default_values=true)
data = params.dup
unprocessed_value = data.delete(:unprocessed_value) || ""
processed_values = data.delete(:processed_values) || {}
set_unprocessed_value(unprocessed_value)
@processed_values = processed_values
set_value(unprocessed_value, default_values)
data.each do |property, value|
setter = "#{property}="
self.send(setter, value) if respond_to?(setter)
end
end
def processed_values=(values)
@values = nil
@processed_values = values
end
def set_unprocessed_value(new_value, preprocessed = false)
# initial_value should only be set once so that it can act as a test for field modification
@initial_value ||= new_value
@unprocessed_value = new_value
end
def method_missing(method, *args)
if outputs.include?(method)
value(method)
else
super
end
end
end
end
end