SpontaneousCMS/spontaneous

View on GitHub
lib/spontaneous/field/base.rb

Summary

Maintainability
D
2 days
Test Coverage
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