class Draftsman::Draft

Public Class Methods

object_changes_col_is_json?() click to toggle source

Returns whether the `object_changes` column is using the `json` type supported by PostgreSQL.

# File lib/draftsman/draft.rb, line 33
def self.object_changes_col_is_json?
  @object_changes_col_is_json ||= columns_hash['object_changes'].type == :json
end
object_changes_col_present?() click to toggle source

Returns whether or not this class has an `object_changes` column.

# File lib/draftsman/draft.rb, line 27
def self.object_changes_col_present?
  column_names.include?('object_changes')
end
object_col_is_json?() click to toggle source

Returns whether the `object` column is using the `json` type supported by PostgreSQL.

# File lib/draftsman/draft.rb, line 22
def self.object_col_is_json?
  @object_col_is_json ||= Draftsman.stash_drafted_changes? && columns_hash['object'].type == :json
end
previous_draft_col_is_json?() click to toggle source

Returns whether the `previous_draft` column is using the `json` type supported by PostgreSQL.

# File lib/draftsman/draft.rb, line 38
def self.previous_draft_col_is_json?
  @previous_draft_col_is_json ||= columns_hash['previous_draft'].type == :json
end
with_item_keys(item_type, item_id) click to toggle source
# File lib/draftsman/draft.rb, line 16
def self.with_item_keys(item_type, item_id)
  scoped conditions: { item_type: item_type, item_id: item_id }
end

Public Instance Methods

changeset() click to toggle source

Returns what changed in this draft. Similar to `ActiveModel::Dirty#changes`. Returns `nil` if your `drafts` table does not have an `object_changes` text column.

# File lib/draftsman/draft.rb, line 45
def changeset
  return nil unless self.class.object_changes_col_present?
  @changeset ||= load_changeset
end
create?() click to toggle source

Returns whether or not this is a `create` event.

# File lib/draftsman/draft.rb, line 51
def create?
  self.event == 'create'
end
destroy?() click to toggle source

Returns whether or not this is a `destroy` event.

# File lib/draftsman/draft.rb, line 56
def destroy?
  self.event == 'destroy'
end
draft_publication_dependencies() click to toggle source

Returns related draft dependencies that would be along for the ride for a `publish!` action.

# File lib/draftsman/draft.rb, line 62
def draft_publication_dependencies
  dependencies = []

  my_item =
    if Draftsman.stash_drafted_changes? && self.item.draft?
      self.item.draft.reify
    else
      self.item
    end

  case self.event.to_sym
  when :create, :update
    associations = my_item.class.reflect_on_all_associations(:belongs_to)

    associations.each do |association|
      association_class =
        if association.options.key?(:polymorphic)
          my_item.send(association.foreign_key.sub('_id', '_type')).constantize
        else
          association.klass
        end

      if association_class.draftable? && association.name != association_class.draft_association_name.to_sym
        dependency = my_item.send(association.name)
        dependencies << dependency.draft if dependency.present? && dependency.draft? && dependency.draft.create?
      end
    end
  when :destroy
    associations = my_item.class.reflect_on_all_associations(:has_one) + my_item.class.reflect_on_all_associations(:has_many)

    associations.each do |association|
      if association.klass.draftable?
        # Reconcile different association types into an array, even if `has_one` produces a single-item
        associated_dependencies =
          case association.macro
          when :has_one
            my_item.send(association.name).present? ? [my_item.send(association.name)] : []
          when :has_many
            my_item.send(association.name)
          end

        associated_dependencies.each do |dependency|
          dependencies << dependency.draft if dependency.draft?
        end
      end
    end
  end

  dependencies
end
draft_reversion_dependencies() click to toggle source

Returns related draft dependencies that would be along for the ride for a `revert!` action.

# File lib/draftsman/draft.rb, line 115
def draft_reversion_dependencies
  dependencies = []

  case self.event.to_sym
  when :create
    associations = self.item.class.reflect_on_all_associations(:has_one) + self.item.class.reflect_on_all_associations(:has_many)

    associations.each do |association|
      if association.klass.draftable?
        # Reconcile different association types into an array, even if
        # `has_one` produces a single-item
        associated_dependencies =
          case association.macro
          when :has_one
            self.item.send(association.name).present? ? [self.item.send(association.name)] : []
          when :has_many
            self.item.send(association.name)
          end

        associated_dependencies.each do |dependency|
          dependencies << dependency.draft if dependency.draft?
        end
      end
    end
  when :destroy
    associations = self.item.class.reflect_on_all_associations(:belongs_to)

    associations.each do |association|
      association_class =
        if association.options.key?(:polymorphic)
          self.item.send(association.foreign_key.sub('_id', '_type')).constantize
        else
          association.klass
        end

      if association_class.draftable? && association_class.trashable? && association.name != association_class.draft_association_name.to_sym
        dependency = self.item.send(association.name)
        dependencies << dependency.draft if dependency.present? && dependency.draft? && dependency.draft.destroy?
      end
    end
  end

  dependencies
end
publish!() click to toggle source

Publishes this draft's associated `item`, publishes its `item`'s dependencies, and destroys itself.

  • For `create` drafts, adds a value for the `published_at` timestamp on the item and destroys the draft.

  • For `update` drafts, applies the drafted changes to the item and destroys the draft.

  • For `destroy` drafts, destroys the item and the draft.

# File lib/draftsman/draft.rb, line 167
def publish!
  ActiveRecord::Base.transaction do
    case self.event.to_sym
    when :create, :update
      # Parents must be published too
      self.draft_publication_dependencies.each { |dependency| dependency.publish! }

      # Update drafts need to copy over data to main record
      self.item.attributes = self.reify.attributes if Draftsman.stash_drafted_changes? && self.update?

      # Write `published_at` attribute
      self.item.send("#{self.item.class.published_at_attribute_name}=", current_time_from_proper_timezone)

      # Clear out draft
      self.item.send("#{self.item.class.draft_association_name}_id=", nil)

      self.item.save(validate: false)
      self.item.reload

      # Destroy draft
      self.destroy
    when :destroy
      self.item.destroy
    end
  end
end
reify() click to toggle source

Returns instance of item converted to its drafted state.

Example usage:

`@category = @category.draft.reify if @category.draft?`
# File lib/draftsman/draft.rb, line 199
def reify
  # This appears to be necessary if for some reason the draft's model
  # hasn't been loaded (such as when done in the console).
  unless defined? self.item_type
    require self.item_type.underscore
  end

  without_identity_map do
    # Create draft doesn't require reification.
    if self.create?
      self.item
    # If a previous draft is stashed, restore that.
    elsif self.previous_draft.present?
      reify_previous_draft.reify
    # Prefer changeset for refication if it's present.
    elsif self.changeset.present? && self.changeset.any?
      self.changeset.each do |key, value|
        # Skip counter_cache columns
        if self.item.respond_to?("#{key}=") && !key.end_with?('_count')
          self.item.send("#{key}=", value.last)
        elsif !key.end_with?('_count')
          logger.warn("Attribute #{key} does not exist on #{self.item_type} (Draft ID: #{self.id}).")
        end
      end

      self.item.send("#{self.item.class.draft_association_name}=", self)
      self.item
    # Reify based on object if it's all that's available.
    elsif self.object.present?
      attrs = self.class.object_col_is_json? ? self.object : Draftsman.serializer.load(self.object)
      self.item.class.unserialize_attributes_for_draftsman(attrs)

      attrs.each do |key, value|
        # Skip counter_cache columns
        if self.item.respond_to?("#{key}=") && !key.end_with?('_count')
          self.item.send("#{key}=", value)
        elsif !key.end_with?('_count')
          logger.warn("Attribute #{key} does not exist on #{self.item_type} (Draft ID: #{self.id}).")
        end
      end

      self.item.send("#{self.item.class.draft_association_name}=", self)
      self.item
    end
  end
end
revert!() click to toggle source

Reverts this draft.

  • For create drafts, destroys the draft and the item.

  • For update drafts, destroys the draft only.

  • For destroy drafts, destroys the draft and undoes the `trashed_at` timestamp on the item. If a previous draft was drafted for destroy, restores the draft.

# File lib/draftsman/draft.rb, line 252
def revert!
  ActiveRecord::Base.transaction do
    case self.event.to_sym
    when :create
      self.item.destroy
      self.destroy
    when :update
      # If we're not stashing changes, we need to restore original values from
      # the changeset.
      if self.class.object_changes_col_present? && !Draftsman.stash_drafted_changes?
        self.changeset.each do |attr, values|
          self.item.send("#{attr}=", values.first) if self.item.respond_to?(attr)
        end
      end
      # Then clear out the draft ID.
      self.item.send("#{self.item.class.draft_association_name}_id=", nil)
      self.item.save!(validate: false, touch: false)
      # Then destroy draft.
      self.destroy
    when :destroy
      # Parents must be restored too
      self.draft_reversion_dependencies.each { |dependency| dependency.revert! }

      # Restore previous draft if one was stashed away
      if self.previous_draft.present?
        prev_draft = reify_previous_draft
        prev_draft.save!

        self.item.class.where(id: self.item).update_all "#{self.item.class.draft_association_name}_id".to_sym => prev_draft.id,
                                                        self.item.class.trashed_at_attribute_name => nil
      else
        self.item.class.where(id: self.item).update_all "#{self.item.class.draft_association_name}_id".to_sym => nil,
                                                        self.item.class.trashed_at_attribute_name => nil
      end

      self.destroy
    end
  end
end
update?() click to toggle source

Returns whether or not this is an `update` event.

# File lib/draftsman/draft.rb, line 293
def update?
  self.event.to_sym == :update
end

Private Instance Methods

load_changeset() click to toggle source
# File lib/draftsman/draft.rb, line 326
def load_changeset
  changes = HashWithIndifferentAccess.new(object_changes_deserialized)
  self.item_type.constantize.unserialize_draft_attribute_changes(changes)
  changes
rescue
  {}
end
object_changes_deserialized() click to toggle source
# File lib/draftsman/draft.rb, line 334
def object_changes_deserialized
  if self.class.object_changes_col_is_json?
    self.object_changes
  else
    Draftsman.serializer.load(self.object_changes)
  end
end
reify_previous_draft() click to toggle source

Restores previous draft and returns it.

# File lib/draftsman/draft.rb, line 300
def reify_previous_draft
  draft = self.class.new

  without_identity_map do
    attrs = self.class.object_col_is_json? ? self.previous_draft : Draftsman.serializer.load(self.previous_draft)

    attrs.each do |key, value|
      if key.to_sym != :id && draft.respond_to?("#{key}=")
        draft.send("#{key}=", value)
      elsif key.to_sym != :id
        logger.warn("Attribute #{key} does not exist on #{item_type} (Draft ID: #{self.id}).")
      end
    end
  end

  draft
end
without_identity_map(&block) click to toggle source
# File lib/draftsman/draft.rb, line 318
def without_identity_map(&block)
  if defined?(ActiveRecord::IdentityMap) && ActiveRecord::IdentityMap.respond_to?(:without)
    ActiveRecord::IdentityMap.without(&block)
  else
    block.call
  end
end