class DraftApprove::Serialization::Json::DraftChangesProxy

Json implementation of DraftApproveProxy. Clients should not need to worry about the specific implementation details of this class, and should refer to the DraftApprove::DraftChangesProxy module details of the public API.

It is often most convenient to use the +DraftTransaction#draft_proxy_for+ method to construct a DraftApproveProxy instance. This will ensure the correct implementation of DraftApproveProxy is used.

@api private

@see DraftApprove::DraftChangesProxy @see DraftTransaction#draft_proxy_for

Public Instance Methods

<=>(other) click to toggle source

Override comparable for DraftChangesProxy objects. This is so operators such as + and - work accurately when an array of DraftChangesProxy objects are being returned. It also makes testing easier.

@return [Integer] 0 if the given object is a DraftChangesProxy

which refers to the same +Draft+ (if any), the same draftable
(if any), the same draftable class, and the same +DraftTransaction+.
Non-zero otherwise.
# File lib/draft_approve/serialization/json/draft_changes_proxy.rb, line 142
def <=>(other)
  return -1 unless other.is_a?(self.class)

  [:draft, :draftable, :draftable_class, :draft_transaction].each do |method|
    comp = self.public_send(method) <=> other.public_send(method)
    return -1 if comp.nil?
    return comp unless comp.zero?
  end

  # Checked all attributes, and all are equal
  return 0
end
association_changed?(association_name) click to toggle source

Whether any changes will occur to the given association of the proxied Draft or draftable object.

@param association_name [String]

@return [Boolean] true if any objects will be added to this

association, removed from this association, or existing associations
changed in any way. +false+ otherwise.

@see DraftApprove::DraftChangesProxy#association_changed?

# File lib/draft_approve/serialization/json/draft_changes_proxy.rb, line 67
def association_changed?(association_name)
  # Create hash with default block for auto-memoization
  @association_changed_memo ||= Hash.new do |hash, association_name|
    hash[association_name] = begin
      (
        associations_added(association_name).present? ||
        associations_updated(association_name).present? ||
        associations_removed(association_name).present?
      )
    end
  end

  @association_changed_memo[association_name.to_s]
end
associations_added(association_name) click to toggle source

All associated objects which will be added to the given association of the proxied Draft or draftable object.

@param association_name [String]

@return [Array<DraftChangesProxy>] DraftChangesProxy objects for each

object which will be added to the given association

@see DraftApprove::DraftChangesProxy#associations_added

# File lib/draft_approve/serialization/json/draft_changes_proxy.rb, line 91
def associations_added(association_name)
  @associations_added_memo ||= Hash.new do |hash, association_name|
    hash[association_name] = association_values(association_name, :created)
  end

  @associations_added_memo[association_name.to_s]
end
associations_removed(association_name) click to toggle source

All associated objects which will be removed from the given association of the proxied Draft or draftable object.

@param association_name [String]

@return [Array<DraftChangesProxy>] DraftChangesProxy objects for each

object which will be removed from the given association

@see DraftApprove::DraftChangesProxy#associations_removed

# File lib/draft_approve/serialization/json/draft_changes_proxy.rb, line 125
def associations_removed(association_name)
  @associations_removed_memo ||= Hash.new do |hash, association_name|
    hash[association_name] = association_values(association_name, :deleted)
  end

  @associations_removed_memo[association_name.to_s]
end
associations_updated(association_name) click to toggle source

All associated objects which have been updated, but remain the proxied Draft or draftable object.

@param association_name [String]

@return [Array<DraftChangesProxy>] DraftChangesProxy objects for each

object which will be added to the given association

@see DraftApprove::DraftChangesProxy#associations_updated

# File lib/draft_approve/serialization/json/draft_changes_proxy.rb, line 108
def associations_updated(association_name)
  @associations_updated_memo ||= Hash.new do |hash, association_name|
    hash[association_name] = association_values(association_name, :updated)
  end

  @associations_updated_memo[association_name.to_s]
end
hash() click to toggle source

Override hash for DraftChangesProxy objects. This is so operators such as + and - work accurately when an array of DraftChangesProxy objects are being returned. It also makes testing easier.

@return [Integer] a hash of all the +DraftChangeProxy+s attributes

# File lib/draft_approve/serialization/json/draft_changes_proxy.rb, line 162
def hash
  [@draft, @draftable, @draftable_class, @draft_transaction].hash
end
new_value(attribute_name) click to toggle source

The new, drafted value for the given attribute on the proxied Draft or draftable object. If no changes have been drafted for the given attribute, then returns the currently persisted value for the attribute.

@param attribute_name [String]

@return [Object, nil] the new value of the given attribute, or the

currently persisted value if there are no draft changes for the
attribute

@see DraftApprove::DraftChangesProxy#new_value

# File lib/draft_approve/serialization/json/draft_changes_proxy.rb, line 38
def new_value(attribute_name)
  # Create hash with default block for auto-memoization
  @new_values_memo ||= Hash.new do |hash, attribute|
    hash[attribute] = begin
      association = @draftable_class.reflect_on_association(attribute)
      if association.blank?
        new_value_simple_attribute(attribute)
      elsif association.belongs_to?
        new_value_belongs_to_assocation(attribute)
      else
        new_value_non_belongs_to_assocation(attribute)
      end
    end
  end

  # Get memoized value, or calculate and store it
  @new_values_memo[attribute_name.to_s]
end

Private Instance Methods

association_values(association_name, mode) click to toggle source

Helper to get the associations which have been created or deleted as a result of this draft transaction

# File lib/draft_approve/serialization/json/draft_changes_proxy.rb, line 232
        def association_values(association_name, mode)
          association_name = association_name.to_s

          association = @draftable_class.reflect_on_association(association_name)
          if association.blank? || association.belongs_to?
            raise(ArgumentError, "#{association_name} must be a has_many or has_one association")
          end

          associated_class_name = association.class_name
          associated_attribute_name = association.inverse_of.name

          # If we are proxying a concrete object, all associations will point
          # directly at it, otherwise we are proxying a CREATE draft and
          # associations will point at the draft
          required_object = (@draftable.present? ? @draftable : @draft)

          case mode
          when :created
            # Looking for newly created associations, so we want to find
            # objects where the new value (index=1) of the associated
            # attribute points at this object.
            # eg. if looking for new memberships for Person 1, we want to find
            # Membership objects where the json changes look like this:
            # { "person" => [nil, { "TYPE" => "Person", "ID" => 1 }] }
            json_query_str_type = "{#{associated_attribute_name},1,#{Constants::TYPE}}"
            json_query_str_id = "{#{associated_attribute_name},1,#{Constants::ID}}"

            created_associations = @draft_transaction.drafts.where(
              draftable_type: associated_class_name
            ).where(
              <<~SQL
                draft_changes #>> '#{json_query_str_type}' = '#{required_object.class.name}'
                AND
                draft_changes #>> '#{json_query_str_id}' = '#{required_object.id}'
              SQL
            )

            return draft_proxy_for(created_associations)
          when :updated
            # Looking for associations which have draft updates but which still
            # point at this object (if it didn't previously point at this object
            # the change is a new association, so covered by the association
            # created case - if it no longer points at this object the
            # association has been broken, so covered by the association deleted
            # case)
            required_proxy = draft_proxy_for(required_object)

            updated_associations = current_value(association_name).select do |proxy|
              proxy.changed? &&
                proxy.new_value(associated_attribute_name) == required_proxy
            end

            return draft_proxy_for(updated_associations)
          when :deleted
            # Looking for current associations which have either been drafted
            # for complete deletion, or which have had their reference changed
            # to no longer point at this object
            required_proxy = draft_proxy_for(required_object)

            deleted_associations = current_value(association_name).select do |proxy|
              proxy.delete? ||
                proxy.new_value(associated_attribute_name) != required_proxy
            end

            return draft_proxy_for(deleted_associations)
          else
            raise(ArgumentError, "Unrecognised mode #{mode}")
          end
        end
draft_proxy_for(object) click to toggle source

Helper to get a draft proxy for any object before returning it

# File lib/draft_approve/serialization/json/draft_changes_proxy.rb, line 303
def draft_proxy_for(object)
  if object.respond_to?(:map)
    # Object is a collection (likely an ActiveRecord collection), recursively call draft_proxy_for
    return object.map { |o| draft_proxy_for(o) }
  elsif (object.is_a? Draft) || (object.respond_to?(:draftable?) && object.draftable?)
    # Object is a draft or a draftable, wrap it in a DraftChangesProxy
    return self.class.new(object, @draft_transaction)
  else
    return object
  end
end
new_value_belongs_to_assocation(attribute_name) click to toggle source

Helper to get the new value of a belongs_to association as a result of this draft transaction

# File lib/draft_approve/serialization/json/draft_changes_proxy.rb, line 186
def new_value_belongs_to_assocation(attribute_name)
  attribute_name = attribute_name.to_s

  # This attribute is an association where the 'belongs_to' is on this
  # class...
  if @draft.blank? || !@draft.draft_changes.has_key?(attribute_name)
    # Either no draft, or no changes for this attribute
    return draft_proxy_for(@draftable.public_send(attribute_name))
  else
    # Draft changes have been made on this attribute...
    new_value = @draft.draft_changes[attribute_name][1]

    if new_value.blank?
      return nil  # The association link has been removed on the draft
    else
      new_value_class = Object.const_get(new_value[Constants::TYPE])
      new_value_object = new_value_class.find(new_value[Constants::ID])
      return draft_proxy_for(new_value_object)
    end
  end
end
new_value_non_belongs_to_assocation(association_name) click to toggle source

Helper to get the new value of has_one / has_many associations (ie. get all the objects the association would return after this draft dransaction has been applied)

# File lib/draft_approve/serialization/json/draft_changes_proxy.rb, line 211
def new_value_non_belongs_to_assocation(association_name)
  association_name = association_name.to_s

  associated_instances = []

  # Starting point is all objects already associated
  associated_instances += current_value(association_name)

  # Add any new objects which will be created by this transaction and
  # refer to this object
  associated_instances += associations_added(association_name)

  # Finally remove any associations which will be deleted or not
  # refer to this object anymore as a reuslt of this transaction
  associated_instances -= associations_removed(association_name)

  return associated_instances
end
new_value_simple_attribute(attribute_name) click to toggle source

Helper to get the new value of a simple attribute as a result of this draft transaction

# File lib/draft_approve/serialization/json/draft_changes_proxy.rb, line 170
def new_value_simple_attribute(attribute_name)
  attribute_name = attribute_name.to_s

  # This attribute is a simple value (not an association)
  if @draft.blank? || !@draft.draft_changes.has_key?(attribute_name)
    # Either no draft, or no changes for this attribute
    return draft_proxy_for(@draftable.public_send(attribute_name))
  else
    # Draft changes have been made on this attribute...
    new_value = @draft.draft_changes[attribute_name][1]
    return draft_proxy_for(new_value)
  end
end