SpontaneousCMS/spontaneous

View on GitHub
lib/spontaneous/model/core/content_hash.rb

Summary

Maintainability
A
35 mins
Test Coverage
# encoding: UTF-8

require 'digest/md5'

module Spontaneous::Model::Core
  module ContentHash
    extend Spontaneous::Concern

    def self.calculate(*values)
      values = Array(values).flatten
      md5 = Digest::MD5.new
      values.each do |value|
        md5.update(value.to_s)
      end
      md5.hexdigest
    end

    class ContentHashChange
      def initialize(origin)
        @origin = origin
      end

      def propagate
        return if owner.nil?
        content = owner

        content.mapper.clean_scope! do
          while propagate_to?(content)
            content.recalculate_content_hash!
            break if content == @origin.page
            content = content.owner
          end
        end
      end

      def propagate_to?(content)
        !(content.nil? || (@origin.page? && content.page?)) && content.modification_tracking_enabled?
      end

      def owner
        @origin.owner
      end
    end

    included do
      cascading_change :content_hash do |origin, old_value, new_value|
        ContentHashChange.new(origin)
      end
    end

    def content_hash_changed?
      content_hash_changed
    end

    alias_method :has_unpublished_changes?, :content_hash_changed?

    def content_hash
      super || calculate_content_hash
    end

    def calculate_content_hash
      Spontaneous::Model::Core::ContentHash.calculate(content_hash_dependencies { |el| el.content_hash })
    end

    def calculate_content_hash!
      Spontaneous::Model::Core::ContentHash.calculate(content_hash_dependencies { |el| el.calculate_content_hash! })
    end

    def content_hash_dependencies(&calculation)
      fields = fields_with_consistent_order.map(&calculation)
      boxes = boxes_with_consistent_order.reject(&:generated?).map(&calculation)
      content_hash_columns.concat(fields).concat(boxes)
    end

    def content_hash_columns
      [schema_id, hidden?, target_id, owner_id, try(:touched_at)]
    end

    def before_save
      # Only recalculate the content hash if we're in the editable dataset, otherwise
      # the published data can end up with a different content hash even after being published
      recalculate_content_hash if modification_tracking_enabled?
      super
      true
    end

    def recalculate_content_hash
      attrs, changed = content_hash_attributes
      set(attrs) if changed
    end

    # Update the instances content hash by writing direct to the db & without
    # triggering any futher cascading changes. Without the #reload call
    # existing cached versions of the content tree may be used and changes
    # will not propagate
    def recalculate_content_hash!
      attrs, changed = content_hash_attributes
      model.where(id: id).update(attrs) if changed
    end

    def content_hash_attributes
      db, current, published = self[:content_hash], calculate_content_hash, published_content_hash
      changed = published.blank? || current != published
      [{content_hash: current, content_hash_changed: changed, content_hash_changed_at: ::Sequel.datetime_class.now}, db != current]
    end

    def modification_tracking_enabled?
      mapper.editable? #&& !@modification_tracking_disabled
    end

    module PageMethods
      # Make page modification state depend on its path
      def content_hash_dependencies
        super.push(slug)
      end
    end

    module BoxMethods
      def generated?
        _prototype.generated?
      end

      def content_hash
        _content_hash { |el| el.content_hash }
      end

      # boxes have no persisted value for their content hash
      alias_method :calculate_content_hash, :content_hash

      def calculate_content_hash!
        _content_hash { |el| el.calculate_content_hash! }
      end

      def _content_hash(&calculation)
        return "" if fields.empty? && empty?
        fields = fields_with_consistent_order.map(&calculation)
        entry_hashes = map(&calculation)
        Spontaneous::Model::Core::ContentHash.calculate(fields, entry_hashes)
      end
    end

    module FieldMethods
      def content_hash
        Spontaneous::Model::Core::ContentHash.calculate(unprocessed_value)
      end

      alias_method :calculate_content_hash,  :content_hash
      alias_method :calculate_content_hash!, :content_hash
    end

    module PagePieceMethods
      # Because pages all publish independently we don't want the content hash
      # of boxes to change if a contained page is modified, so make the hash
      # of pages inside boxes only depend on the id (so the box hash does change
      # when the page is added, moved or deleted)
      def content_hash
        return nil if page_target.never_published?
        Spontaneous::Model::Core::ContentHash.calculate(id)
      end
      alias_method :calculate_content_hash,  :content_hash
      alias_method :calculate_content_hash!, :content_hash
    end
  end
end