diff --git a/README.md b/README.md index 7ed91dc..487879a 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ Add `has_drafts` to the models you want to have drafts on. Draftsman provides a helper extension that acts similarly to the controller mixin it provides for Rails applications. -It will set `Draftsman::Draft#whodunnit` to whatever is returned by a method +It will set `Draftsman::Single::Draft#whodunnit` to whatever is returned by a method named `user_for_paper_trail`, which you can define inside your Sinatra application. (By default, it attempts to invoke a method named `current_user`.) @@ -169,8 +169,8 @@ the following options: ##### `:class_name` The name of a custom `Draft` class. This class should inherit from -`Draftsman::Draft`. A global default can be set for this using -`Draftsman.draft_class_name=` if the default of `Draftsman::Draft` needs to be +`Draftsman::Single::Draft`. A global default can be set for this using +`Draftsman.draft_class_name=` if the default of `Draftsman::Single::Draft` needs to be overridden. ##### `:ignore` @@ -276,22 +276,22 @@ Widget.live.includes(:gears, :sprockets).live(:gears) ### Draft Class Methods -The `Draftsman::Draft` class has the following scopes: +The `Draftsman::Single::Draft` class has the following scopes: ```ruby # Returns all drafts created by the `create` event. -Draftsman::Draft.creates +Draftsman::Single::Draft.creates # Returns all drafts created by the `update` event. -Draftsman::Draft.updates +Draftsman::Single::Draft.updates # Returns all drafts created by the `destroy` event. -Draftsman::Draft.destroys +Draftsman::Single::Draft.destroys ``` ### Draft Instance Methods -And a `Draftsman::Draft` instance has these methods: +And a `Draftsman::Single::Draft` instance has these methods: ```ruby # Return the associated item in its state before the draft. @@ -479,7 +479,7 @@ class Admin::DraftsController < Admin::BaseController before_action :find_draft, only: [:show, :update, :destroy] def index - @drafts = Draftsman::Draft.includes(:item).order(updated_at: :desc) + @drafts = Draftsman::Single::Draft.includes(:item).order(updated_at: :desc) end def show @@ -535,7 +535,7 @@ private # Finds draft by `params[:id]`. def find_draft - @draft = Draftsman::Draft.find(params[:id]) + @draft = Draftsman::Single::Draft.find(params[:id]) end end diff --git a/lib/draftsman.rb b/lib/draftsman.rb index 6c8a9cb..edfa1d8 100644 --- a/lib/draftsman.rb +++ b/lib/draftsman.rb @@ -49,13 +49,23 @@ def self.controller_info=(value) end # Returns default class name used for drafts. - def self.draft_class_name - draftsman_store[:draft_class_name] + def self.single_draft_class_name + draftsman_store[:single_draft_class_name] + end + + # Returns default class name used for drafts. + def self.multiple_draft_class_name + draftsman_store[:multiple_draft_class_name] + end + + # Sets default class name to use for drafts. + def self.single_draft_class_name=(class_name) + draftsman_store[:single_draft_class_name] = class_name end # Sets default class name to use for drafts. - def self.draft_class_name=(class_name) - draftsman_store[:draft_class_name] = class_name + def self.multiple_draft_class_name=(class_name) + draftsman_store[:multiple_draft_class_name] = class_name end # Set the field which records when a draft was created. @@ -116,7 +126,7 @@ def self.whodunnit_field=(field_name) # Thread-safe hash to hold Draftman's data. Initializing with needed default values. def self.draftsman_store - Thread.current[:draft] ||= { draft_class_name: 'Draftsman::Draft' } + Thread.current[:draft] ||= { single_draft_class_name: 'Draftsman::Single::Draft', multiple_draft_class_name: 'Draftsman::Multiple::Draft' } end # Returns Draftman's configuration object. @@ -130,7 +140,8 @@ def self.configure end # Draft model class. -require 'draftsman/draft' +require 'draftsman/single/draft' +require 'draftsman/multiple/draft' # Inject `Draftsman::Model` into ActiveRecord classes. ActiveSupport.on_load(:active_record) do diff --git a/lib/draftsman/model.rb b/lib/draftsman/model.rb index 30eb2f8..dec0ee4 100644 --- a/lib/draftsman/model.rb +++ b/lib/draftsman/model.rb @@ -1,4 +1,6 @@ require 'draftsman/attributes_serialization' +require 'draftsman/single/instance_methods' +require 'draftsman/multiple/instance_methods' module Draftsman module Model @@ -15,8 +17,8 @@ module ClassMethods # # :class_name # The name of a custom `Draft` class. This class should inherit from - # `Draftsman::Draft`. A global default can be set for this using - # `Draftsman.draft_class_name=` if the default of `Draftsman::Draft` needs + # `Draftsman::Single::Draft`. A global default can be set for this using + # `Draftsman.draft_class_name=` if the default of `Draftsman::Single::Draft` needs # to be overridden. # # :ignore @@ -45,6 +47,10 @@ module ClassMethods # The name to use for the `draft` association shortcut method. Default is # `:draft`. # + # :drafts + # The name to use for the `drafts` association shortcut method if using multiple drafts. Default is + # `:drafts`. + # # :published_at # The name to use for the method which returns the published timestamp. # Default is `published_at`. @@ -53,9 +59,14 @@ module ClassMethods # The name to use for the method which returns the soft delete timestamp. # Default is `trashed_at`. def has_drafts(options = {}) + + class_attribute :multiple + self.multiple = options[:multiple] || false + # Lazily include the instance methods so we don't clutter up # any more ActiveRecord models than we need to. - send :include, InstanceMethods + send :include, Draftsman::Multiple::InstanceMethods if self.multiple + send :include, Draftsman::Single::InstanceMethods if !self.multiple send :extend, AttributesSerialization # Define before/around/after callbacks on each drafted model @@ -68,16 +79,25 @@ def has_drafts(options = {}) self.draftsman_options = options.dup class_attribute :draft_association_name - self.draft_association_name = options[:draft] || :draft + if self.multiple + self.draft_association_name = options[:drafts] || :drafts + else + self.draft_association_name = options[:draft] || :draft + end class_attribute :draft_class_name - self.draft_class_name = options[:class_name] || Draftsman.draft_class_name + self.draft_class_name = options[:class_name] || Draftsman.multiple_draft_class_name if self.multiple + self.draft_class_name = options[:class_name] || Draftsman.single_draft_class_name if !self.multiple [:ignore, :skip, :only].each do |key| draftsman_options[key] = ([draftsman_options[key]].flatten.compact || []).map(&:to_s) end - draftsman_options[:ignore] << "#{self.draft_association_name}_id" + if self.multiple + draftsman_options[:ignore] << "#{self.draft_association_name}_count" + else + draftsman_options[:ignore] << "#{self.draft_association_name}_id" + end draftsman_options[:meta] ||= {} @@ -89,13 +109,22 @@ def has_drafts(options = {}) class_attribute :trashed_at_attribute_name self.trashed_at_attribute_name = options[:trashed_at] || :trashed_at - # `belongs_to :draft` association - belongs_to(self.draft_association_name, class_name: self.draft_class_name, dependent: :destroy, optional: true) + if self.multiple + # `has_many :drafts` association + has_many(self.draft_association_name, class_name: self.draft_class_name, dependent: :destroy, as: :item) + else + # `belongs_to :draft` association + belongs_to(self.draft_association_name, class_name: self.draft_class_name, dependent: :destroy, optional: true) + end # Scopes scope :drafted, -> (referenced_table_name = nil) { referenced_table_name = referenced_table_name.present? ? referenced_table_name : table_name - where.not(referenced_table_name => { "#{self.draft_association_name}_id" => nil }) + if self.multiple + where.not(referenced_table_name => { "#{self.draft_association_name}_count" => 0 }) + else + where.not(referenced_table_name => { "#{self.draft_association_name}_id" => nil }) + end } scope :published, -> (referenced_table_name = nil) { @@ -130,356 +159,5 @@ def trashable? end end - module InstanceMethods - # Returns whether or not this item has a draft. - def draft? - send(self.class.draft_association_name).present? - end - - # DEPRECATED: Use `#draft_save` instead. - def draft_creation - ActiveSupport::Deprecation.warn('`#draft_creation` is deprecated and will be removed from Draftsman 1.0. Use `#save_draft` instead.') - _draft_creation - end - - # DEPRECATED: Use `#draft_destruction` instead. - def draft_destroy - ActiveSupport::Deprecation.warn('`#draft_destroy` is deprecated and will be removed from Draftsman 1.0. Use `draft_destruction` instead.') - - run_callbacks :draft_destroy do - _draft_destruction - end - end - - # Trashes object and records a draft for a `destroy` event. - def draft_destruction - run_callbacks :draft_destruction do - _draft_destruction - end - end - - # DEPRECATED: Use `#draft_save` instead. - def draft_update - ActiveSupport::Deprecation.warn('`#draft_update` is deprecated and will be removed from Draftsman 1.0. Use `#save_draft` instead.') - _draft_update - end - - # Returns serialized object representing this drafted item. - def object_attrs_for_draft_record(object = nil) - object ||= self - - attrs = object.attributes.except(*self.class.draftsman_options[:skip]).tap do |attributes| - self.class.serialize_attributes_for_draftsman(attributes) - end - - if self.class.draft_class.object_col_is_json? - attrs - else - Draftsman.serializer.dump(attrs) - end - end - - # Returns whether or not this item has been published at any point in its lifecycle. - def published? - self.published_at.present? - end - - # Creates or updates draft depending on state of this item and if it has - # any drafts. - # - # - If a completely new record, persists this item to the database and - # records a `create` draft. - # - If an existing record with an existing `create` draft, updates the - # record and the existing `create` draft. - # - If an existing record with no existing draft, records changes in an - # `update` draft. - # - If an existing record with an existing draft (`create` or `update`), - # updated back to its original undrafted state, removes associated - # `draft record`. - # - # Returns `true` or `false` depending on if the object passed validation - # and the save was successful. - def save_draft - run_callbacks :save_draft do - if self.new_record? - _draft_creation - else - _draft_update - end - end - end - - # Returns whether or not this item has been trashed - def trashed? - send(self.class.trashed_at_attribute_name).present? - end - - private - - # Creates object and records a draft for the object's creation. Returns - # `true` or `false` depending on whether or not the objects passed - # validation and the save was successful. - def _draft_creation - transaction do - # TODO: Remove callback wrapper in v1.0. - run_callbacks :draft_creation do - # We want to save the draft after create - return false unless self.save - - # Build data to store in draft record. - data = { - item: self, - event: :create, - } - data[:object] = object_attrs_for_draft_record if Draftsman.stash_drafted_changes? - data[Draftsman.whodunnit_field] = Draftsman.whodunnit - data[:object_changes] = serialized_draft_changeset(changes_for_draftsman(:create)) if track_object_changes_for_draft? - data = merge_metadata_for_draft(data) - send("build_#{self.class.draft_association_name}", data) - - if send(self.class.draft_association_name).save - fk = "#{self.class.draft_association_name}_id" - id = send(self.class.draft_association_name).id - self.update_column(fk, id) - else - raise ActiveRecord::Rollback and return false - end - end - end - - return true - end - - # This is only abstracted away at this moment because of the - # `draft_destroy` deprecation. Move all of this logic back into - # `draft_destruction` after `draft_destroy is removed.` - def _draft_destruction - transaction do - data = { - item: self, - event: :destroy - } - data[:object] = object_attrs_for_draft_record if Draftsman.stash_drafted_changes? - data[Draftsman.whodunnit_field] = Draftsman.whodunnit - - # Stash previous draft in case it needs to be reverted later - if self.draft? - attrs = send(self.class.draft_association_name).attributes - - data[:previous_draft] = - if self.class.draft_class.previous_draft_col_is_json? - attrs - else - Draftsman.serializer.dump(attrs) - end - end - - data = merge_metadata_for_draft(data) - - if send(self.class.draft_association_name).present? - send(self.class.draft_association_name).update!(data) - else - send("build_#{self.class.draft_association_name}", data) - send(self.class.draft_association_name).save! - send("#{self.class.draft_association_name}_id=", send(self.class.draft_association_name).id) - self.update_column("#{self.class.draft_association_name}_id", send(self.class.draft_association_name).id) - end - - trash! - - # Mock `dependent: :destroy` behavior for all trashable associations - dependent_associations = self.class.reflect_on_all_associations(:has_one) + self.class.reflect_on_all_associations(:has_many) - - dependent_associations.each do |association| - if association.klass.draftable? && association.options.has_key?(:dependent) && association.options[:dependent] == :destroy - dependents = self.send(association.name) - dependents = [dependents] if (dependents && association.macro == :has_one) - - if dependents - dependents.each do |dependent| - dependent.draft_destruction unless dependent.draft? && dependent.send(dependent.class.draft_association_name).destroy? - end - end - end - end - end - end - - # Updates object and records a draft for an `update` event. If the draft - # is being updated to the object's original state, the draft is destroyed. - # Returns `true` or `false` depending on if the object passed validation - # and the save was successful. - def _draft_update - # TODO: Remove callback wrapper in v1.0. - transaction do - run_callbacks :draft_update do - # Run validations. - return false unless self.valid? - - # If updating a create draft, also update this item. - if self.draft? && send(self.class.draft_association_name).create? - the_changes = changes_for_draftsman(:create) - data = { item: self } - data[Draftsman.whodunnit_field] = Draftsman.whodunnit - data[:object] = object_attrs_for_draft_record if Draftsman.stash_drafted_changes? - data[:object_changes] = serialized_draft_changeset(the_changes) if track_object_changes_for_draft? - - data = merge_metadata_for_draft(data) - send(self.class.draft_association_name).update(data) - save - else - the_changes = changes_for_draftsman(:update) - save_only_columns_for_draft if Draftsman.stash_drafted_changes? - - # Destroy the draft if this record has changed back to the - # original values. - if self.draft? && the_changes.empty? - nilified_draft = send(self.class.draft_association_name) - touch = changed? - send("#{self.class.draft_association_name}_id=", nil) - save(touch: touch) - nilified_draft.destroy - # Save an update draft if record is changed notably. - elsif !the_changes.empty? - data = { item: self, event: :update } - data[Draftsman.whodunnit_field] = Draftsman.whodunnit - data[:object] = object_attrs_for_draft_record if Draftsman.stash_drafted_changes? - data[:object_changes] = serialized_draft_changeset(the_changes) if track_object_changes_for_draft? - data = merge_metadata_for_draft(data) - - # If there's already a draft, update it. - if self.draft? - send(self.class.draft_association_name).update(data) - - if Draftsman.stash_drafted_changes? - update_skipped_attributes - else - self.save - end - # If there's not an existing draft, create an update draft. - else - send("build_#{self.class.draft_association_name}", data) - - if send(self.class.draft_association_name).save - update_column("#{self.class.draft_association_name}_id", send(self.class.draft_association_name).id) - - if Draftsman.stash_drafted_changes? - update_skipped_attributes - else - self.save - end - else - raise ActiveRecord::Rollback and return false - end - end - # Otherwise, just save the record. - else - self.save - end - end - end - end - rescue Exception => e - false - end - - # Returns hash of attributes that have changed for the object, similar to - # how ActiveRecord's `changes` works. - def changes_for_draftsman(event) - the_changes = {} - ignore = self.class.draftsman_options[:ignore] - skip = self.class.draftsman_options[:skip] - only = self.class.draftsman_options[:only] - draftable_attrs = self.attributes.keys - ignore - skip - draftable_attrs = draftable_attrs & only if only.present? - - # If there's already an update draft, get its changes and reconcile them - # manually. - if event == :update - # Collect all attributes' previous and new values. - draftable_attrs.each do |attr| - if self.draft? && self.draft.changeset && self.draft.changeset.key?(attr) - the_changes[attr] = [self.draft.changeset[attr].first, send(attr)] - else - the_changes[attr] = [self.send("#{attr}_was"), send(attr)] - end - end - # If there is no draft or it's for a create, then all draftable - # attributes are the changes. - else - draftable_attrs.each { |attr| the_changes[attr] = [nil, send(attr)] } - end - - # Purge attributes that haven't changed. - the_changes.delete_if { |key, value| value.first == value.last } - end - - # Merges model-level metadata from `meta` and `controller_info` into draft object. - def merge_metadata_for_draft(data) - # First, we merge the model-level metadata in `meta`. - draftsman_options[:meta].each do |attribute, value| - data[attribute] = - if value.respond_to?(:call) - value.call(self) - elsif value.is_a?(Symbol) && respond_to?(value) - # if it is an attribute that is changing, be sure to grab the current version - if has_attribute?(value) && send("#{value}_changed?".to_sym) - send("#{value}_was".to_sym) - else - send(value) - end - else - value - end - end - - # Second, we merge any extra data from the controller (if available). - data.merge(Draftsman.controller_info || {}) - end - - # Save columns outside of the `only` option directly to master table - def save_only_columns_for_draft - if self.class.draftsman_options[:only].any? - only_changes = {} - only_changed_attributes = self.attributes.keys - self.class.draftsman_options[:only] - - only_changed_attributes.each do |key| - only_changes[key] = send(key) if changed.include?(key) - end - - self.update_columns(only_changes) if only_changes.any? - end - end - - # Returns changeset data in format appropriate for `object_changes` - # column. - def serialized_draft_changeset(my_changes) - self.class.draft_class.object_changes_col_is_json? ? my_changes : Draftsman.serializer.dump(my_changes) - end - - # Returns whether or not the draft class includes an `object_changes` attribute. - def track_object_changes_for_draft? - self.class.draft_class.column_names.include?('object_changes') - end - - # Sets `trashed_at` attribute to now and saves to the database immediately. - def trash! - self.update_column(self.class.trashed_at_attribute_name, Time.now) - end - - # Updates skipped attributes' values on this model. - def update_skipped_attributes - # Skip over this if nothing's being skipped. - skipped_changed = changed_attributes.keys & draftsman_options[:skip] - return true unless skipped_changed.present? - - keys = self.attributes.keys.select { |key| draftsman_options[:skip].include?(key) } - attrs = {} - keys.each { |key| attrs[key] = self.send(key) } - - self.reload - self.update(attrs) - end - end end end diff --git a/lib/draftsman/multiple/draft.rb b/lib/draftsman/multiple/draft.rb new file mode 100644 index 0000000..1f34bcd --- /dev/null +++ b/lib/draftsman/multiple/draft.rb @@ -0,0 +1,8 @@ +require 'draftsman/shared_draft_methods.rb' + + +class Draftsman::Multiple::Draft < Draftsman::SharedDraftMethods + + belongs_to :item, polymorphic: true, counter_cache: true + +end diff --git a/lib/draftsman/multiple/instance_methods.rb b/lib/draftsman/multiple/instance_methods.rb new file mode 100644 index 0000000..caa2841 --- /dev/null +++ b/lib/draftsman/multiple/instance_methods.rb @@ -0,0 +1,132 @@ +require 'draftsman/shared_instance_methods' + +module Draftsman + module Multiple + module InstanceMethods + + include Draftsman::SharedInstanceMethods + + # Returns whether or not this item has drafts. + def has_drafts? + send(self.class.draft_association_name).count > 0 + end + + private + + # Creates object and records a draft for the object's creation. Returns + # `true` or `false` depending on whether or not the objects passed + # validation and the save was successful. + def _draft_creation + transaction do + # TODO: Remove callback wrapper in v1.0. + run_callbacks :draft_creation do + # We want to save the draft after create + return false unless self.save + # Build data to store in draft record. + data = { + item: self, + event: :create, + } + + data[:object] = object_attrs_for_draft_record if Draftsman.stash_drafted_changes? + data[Draftsman.whodunnit_field] = Draftsman.whodunnit + data[:object_changes] = serialized_draft_changeset(changes_for_draftsman(:create)) if track_object_changes_for_draft? + data = merge_metadata_for_draft(data) + + draft = send(self.class.draft_association_name).new(data) + + if !draft.save + raise ActiveRecord::Rollback and return false + end + end + end + + return true + end + + # This is only abstracted away at this moment because of the + # `draft_destroy` deprecation. Move all of this logic back into + # `draft_destruction` after `draft_destroy is removed.` + def _draft_destruction + transaction do + + data = { + item: self, + event: :destroy + } + + data[:object] = object_attrs_for_draft_record if Draftsman.stash_drafted_changes? + data[Draftsman.whodunnit_field] = Draftsman.whodunnit + data = merge_metadata_for_draft(data) + + draft = send(self.class.draft_association_name).new(data) + draft.save! + + trash! + + # Mock `dependent: :destroy` behavior for all trashable associations + dependent_associations = self.class.reflect_on_all_associations(:has_one) + self.class.reflect_on_all_associations(:has_many) + + dependent_associations.each do |association| + if association.klass.draftable? && association.options.has_key?(:dependent) && association.options[:dependent] == :destroy + dependents = self.send(association.name) + dependents = [dependents] if (dependents && association.macro == :has_one) + + if dependents + dependents.each do |dependent| + dependent.draft_destruction unless dependent.has_drafts && dependent.send(dependent.class.draft_association_name).destroy? + end + end + end + end + end + end + + # Updates object and records a draft for an `update` event. If the draft + # is being updated to the object's original state, the draft is destroyed. + # Returns `true` or `false` depending on if the object passed validation + # and the save was successful. + def _draft_update + # TODO: Remove callback wrapper in v1.0. + transaction do + run_callbacks :draft_update do + # Run validations. + return false unless self.valid? + + the_changes = changes_for_draftsman(:update) + save_only_columns_for_draft if Draftsman.stash_drafted_changes? + + if !the_changes.empty? + data = { item: self, event: :update } + data[Draftsman.whodunnit_field] = Draftsman.whodunnit + data[:object] = object_attrs_for_draft_record if Draftsman.stash_drafted_changes? + data[:object_changes] = serialized_draft_changeset(the_changes) if track_object_changes_for_draft? + data = merge_metadata_for_draft(data) + + # create an update draft. + draft = send(self.class.draft_association_name).new(data) + + if draft.save + if Draftsman.stash_drafted_changes? + update_skipped_attributes + else + self.save + end + else + raise ActiveRecord::Rollback and return false + end + # Otherwise, just save the record. + else + self.save + end + end + end + rescue Exception => e + logger.error e.message + logger.error e.backtrace.join("\n") + false + end + + end + end +end diff --git a/lib/draftsman/draft.rb b/lib/draftsman/shared_draft_methods.rb similarity index 72% rename from lib/draftsman/draft.rb rename to lib/draftsman/shared_draft_methods.rb index 1470ccd..b23e109 100644 --- a/lib/draftsman/draft.rb +++ b/lib/draftsman/shared_draft_methods.rb @@ -1,6 +1,6 @@ -class Draftsman::Draft < ActiveRecord::Base - # Associations - belongs_to :item, polymorphic: true +class Draftsman::SharedDraftMethods < ActiveRecord::Base + + self.table_name = 'drafts' # Validations validates :event, presence: true @@ -63,11 +63,11 @@ def draft_publication_dependencies dependencies = [] my_item = - if Draftsman.stash_drafted_changes? && self.item.draft? - self.item.draft.reify - else - self.item - end + if Draftsman.stash_drafted_changes? && self.item.draft? + self.item.draft.reify + else + self.item + end case self.event.to_sym when :create, :update @@ -75,11 +75,11 @@ def draft_publication_dependencies 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.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) @@ -93,12 +93,12 @@ def draft_publication_dependencies 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 + 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? @@ -124,12 +124,12 @@ def draft_reversion_dependencies # 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 + 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? @@ -141,11 +141,11 @@ def draft_reversion_dependencies 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.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) @@ -169,16 +169,16 @@ def publish! case self.event.to_sym when :create, :update # Parents must be published too - self.draft_publication_dependencies.each { |dependency| dependency.publish! } + self.draft_publication_dependencies.each { |dependency| dependency.publish! } if !self.item.class.multiple # Update drafts need to copy over data to main record - self.item.attributes = self.reify.attributes if Draftsman.stash_drafted_changes? && self.update? + reify 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.send("#{self.item.class.draft_association_name}_id=", nil) if !self.item.class.multiple self.item.save(validate: false) self.item.reload @@ -195,7 +195,10 @@ def publish! # # Example usage: # + # # for single draft # `@category = @category.draft.reify if @category.draft?` + # # for multiple drafts + # `@category = @category.drafts.last.reify if @category.has_drafts?` 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). @@ -207,38 +210,15 @@ def reify # Create draft doesn't require reification. if self.create? self.item - # If a previous draft is stashed, restore that. + # 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. + reify_previous_draft + # 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. + reify_changeset + # 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 + reify_object end end end @@ -264,24 +244,27 @@ def revert! end end # Then clear out the draft ID. - self.item.send("#{self.item.class.draft_association_name}_id=", nil) + self.item.send("#{self.item.class.draft_association_name}_id=", nil) if !self.item.class.multiple 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! } + self.draft_reversion_dependencies.each { |dependency| dependency.revert! } if !self.item.class.multiple - # Restore previous draft if one was stashed away - if self.previous_draft.present? - prev_draft = reify_previous_draft - prev_draft.save! + if !self.item.class.multiple + # 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 + 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 end self.destroy @@ -294,7 +277,7 @@ def update? self.event.to_sym == :update end -private + private # Restores previous draft and returns it. def reify_previous_draft @@ -338,4 +321,36 @@ def object_changes_deserialized Draftsman.serializer.load(self.object_changes) end end + + # override this if you want custom reifying behavior + def reify_changeset + + 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 + end + + # override this if you want custom reifying behavior + def reify_object + 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) if !self.item.class.multiple + self.item + end end diff --git a/lib/draftsman/shared_instance_methods.rb b/lib/draftsman/shared_instance_methods.rb new file mode 100644 index 0000000..ce74623 --- /dev/null +++ b/lib/draftsman/shared_instance_methods.rb @@ -0,0 +1,185 @@ +module Draftsman + module SharedInstanceMethods + + # DEPRECATED: Use `#draft_save` instead. + def draft_creation + ActiveSupport::Deprecation.warn('`#draft_creation` is deprecated and will be removed from Draftsman 1.0. Use `#save_draft` instead.') + _draft_creation + end + + # DEPRECATED: Use `#draft_destruction` instead. + def draft_destroy + ActiveSupport::Deprecation.warn('`#draft_destroy` is deprecated and will be removed from Draftsman 1.0. Use `draft_destruction` instead.') + + run_callbacks :draft_destroy do + _draft_destruction + end + end + + # Trashes object and records a draft for a `destroy` event. + def draft_destruction + run_callbacks :draft_destruction do + _draft_destruction + end + end + + # DEPRECATED: Use `#draft_save` instead. + def draft_update + ActiveSupport::Deprecation.warn('`#draft_update` is deprecated and will be removed from Draftsman 1.0. Use `#save_draft` instead.') + _draft_update + end + + # Returns serialized object representing this drafted item. + def object_attrs_for_draft_record(object = nil) + object ||= self + + attrs = object.attributes.except(*self.class.draftsman_options[:skip]).tap do |attributes| + self.class.serialize_attributes_for_draftsman(attributes) + end + + if self.class.draft_class.object_col_is_json? + attrs + else + Draftsman.serializer.dump(attrs) + end + end + + # Returns whether or not this item has been published at any point in its lifecycle. + def published? + self.published_at.present? + end + + # Creates or updates draft depending on state of this item and if it has + # any drafts. + # + # - If a completely new record, persists this item to the database and + # records a `create` draft. + # - If an existing record with an existing `create` draft, updates the + # record and the existing `create` draft. + # - If an existing record with no existing draft, records changes in an + # `update` draft. + # - If an existing record with an existing draft (`create` or `update`), + # updated back to its original undrafted state, removes associated + # `draft record`. + # + # Returns `true` or `false` depending on if the object passed validation + # and the save was successful. + def save_draft + run_callbacks :save_draft do + if self.new_record? + _draft_creation + else + _draft_update + end + end + end + + # Returns whether or not this item has been trashed + def trashed? + send(self.class.trashed_at_attribute_name).present? + end + + private + # Returns hash of attributes that have changed for the object, similar to + # how ActiveRecord's `changes` works. + def changes_for_draftsman(event) + the_changes = {} + ignore = self.class.draftsman_options[:ignore] + skip = self.class.draftsman_options[:skip] + only = self.class.draftsman_options[:only] + draftable_attrs = self.attributes.keys - ignore - skip + draftable_attrs = draftable_attrs & only if only.present? + + # If there's already an update draft, get its changes and reconcile them + # manually. + if event == :update + # Collect all attributes' previous and new values. + draftable_attrs.each do |attr| + if self.multiple + the_changes[attr] = [self.send("#{attr}_was"), send(attr)] + else + if self.draft? && self.draft.changeset && self.draft.changeset.key?(attr) + the_changes[attr] = [self.draft.changeset[attr].first, send(attr)] + else + the_changes[attr] = [self.send("#{attr}_was"), send(attr)] + end + end + end + # If there is no draft or it's for a create, then all draftable + # attributes are the changes. + else + draftable_attrs.each { |attr| the_changes[attr] = [nil, send(attr)] } + end + + # Purge attributes that haven't changed. + the_changes.delete_if { |key, value| value.first == value.last } + end + + # Merges model-level metadata from `meta` and `controller_info` into draft object. + def merge_metadata_for_draft(data) + # First, we merge the model-level metadata in `meta`. + draftsman_options[:meta].each do |attribute, value| + data[attribute] = + if value.respond_to?(:call) + value.call(self) + elsif value.is_a?(Symbol) && respond_to?(value) + # if it is an attribute that is changing, be sure to grab the current version + if has_attribute?(value) && send("#{value}_changed?".to_sym) + send("#{value}_was".to_sym) + else + send(value) + end + else + value + end + end + + # Second, we merge any extra data from the controller (if available). + data.merge(Draftsman.controller_info || {}) + end + + # Save columns outside of the `only` option directly to master table + def save_only_columns_for_draft + if self.class.draftsman_options[:only].any? + only_changes = {} + only_changed_attributes = self.attributes.keys - self.class.draftsman_options[:only] + + only_changed_attributes.each do |key| + only_changes[key] = send(key) if changed.include?(key) + end + + self.update_columns(only_changes) if only_changes.any? + end + end + + # Returns changeset data in format appropriate for `object_changes` + # column. + def serialized_draft_changeset(my_changes) + self.class.draft_class.object_changes_col_is_json? ? my_changes : Draftsman.serializer.dump(my_changes) + end + + # Returns whether or not the draft class includes an `object_changes` attribute. + def track_object_changes_for_draft? + self.class.draft_class.column_names.include?('object_changes') + end + + # Sets `trashed_at` attribute to now and saves to the database immediately. + def trash! + self.update_column(self.class.trashed_at_attribute_name, Time.now) + end + + # Updates skipped attributes' values on this model. + def update_skipped_attributes + # Skip over this if nothing's being skipped. + skipped_changed = changed_attributes.keys & draftsman_options[:skip] + return true unless skipped_changed.present? + + keys = self.attributes.keys.select { |key| draftsman_options[:skip].include?(key) } + attrs = {} + keys.each { |key| attrs[key] = self.send(key) } + + self.reload + self.update(attrs) + end + end +end diff --git a/lib/draftsman/single/draft.rb b/lib/draftsman/single/draft.rb new file mode 100644 index 0000000..8cd0786 --- /dev/null +++ b/lib/draftsman/single/draft.rb @@ -0,0 +1,7 @@ +require 'draftsman/shared_draft_methods.rb' + +class Draftsman::Single::Draft < Draftsman::SharedDraftMethods + + belongs_to :item, polymorphic: true + +end diff --git a/lib/draftsman/single/instance_methods.rb b/lib/draftsman/single/instance_methods.rb new file mode 100644 index 0000000..31e2609 --- /dev/null +++ b/lib/draftsman/single/instance_methods.rb @@ -0,0 +1,185 @@ +require 'draftsman/shared_instance_methods' + +module Draftsman + module Single + module InstanceMethods + + include Draftsman::SharedInstanceMethods + + # Returns whether or not this item has a draft. + def draft? + send(self.class.draft_association_name).present? + end + + private + + # Creates object and records a draft for the object's creation. Returns + # `true` or `false` depending on whether or not the objects passed + # validation and the save was successful. + def _draft_creation + transaction do + # TODO: Remove callback wrapper in v1.0. + run_callbacks :draft_creation do + # We want to save the draft after create + return false unless self.save + + # Build data to store in draft record. + data = { + item: self, + event: :create, + } + data[:object] = object_attrs_for_draft_record if Draftsman.stash_drafted_changes? + data[Draftsman.whodunnit_field] = Draftsman.whodunnit + data[:object_changes] = serialized_draft_changeset(changes_for_draftsman(:create)) if track_object_changes_for_draft? + data = merge_metadata_for_draft(data) + send("build_#{self.class.draft_association_name}", data) + + if send(self.class.draft_association_name).save + fk = "#{self.class.draft_association_name}_id" + id = send(self.class.draft_association_name).id + self.update_column(fk, id) + else + raise ActiveRecord::Rollback and return false + end + end + end + + return true + end + + # This is only abstracted away at this moment because of the + # `draft_destroy` deprecation. Move all of this logic back into + # `draft_destruction` after `draft_destroy is removed.` + def _draft_destruction + transaction do + data = { + item: self, + event: :destroy + } + data[:object] = object_attrs_for_draft_record if Draftsman.stash_drafted_changes? + data[Draftsman.whodunnit_field] = Draftsman.whodunnit + + # Stash previous draft in case it needs to be reverted later + if self.draft? + attrs = send(self.class.draft_association_name).attributes + + data[:previous_draft] = + if self.class.draft_class.previous_draft_col_is_json? + attrs + else + Draftsman.serializer.dump(attrs) + end + end + + data = merge_metadata_for_draft(data) + + if send(self.class.draft_association_name).present? + send(self.class.draft_association_name).update!(data) + else + send("build_#{self.class.draft_association_name}", data) + send(self.class.draft_association_name).save! + send("#{self.class.draft_association_name}_id=", send(self.class.draft_association_name).id) + self.update_column("#{self.class.draft_association_name}_id", send(self.class.draft_association_name).id) + end + + trash! + + # Mock `dependent: :destroy` behavior for all trashable associations + dependent_associations = self.class.reflect_on_all_associations(:has_one) + self.class.reflect_on_all_associations(:has_many) + + dependent_associations.each do |association| + if association.klass.draftable? && association.options.has_key?(:dependent) && association.options[:dependent] == :destroy + dependents = self.send(association.name) + dependents = [dependents] if (dependents && association.macro == :has_one) + + if dependents + dependents.each do |dependent| + dependent.draft_destruction unless dependent.draft? && dependent.send(dependent.class.draft_association_name).destroy? + end + end + end + end + end + end + + # Updates object and records a draft for an `update` event. If the draft + # is being updated to the object's original state, the draft is destroyed. + # Returns `true` or `false` depending on if the object passed validation + # and the save was successful. + def _draft_update + # TODO: Remove callback wrapper in v1.0. + transaction do + run_callbacks :draft_update do + # Run validations. + return false unless self.valid? + + # If updating a create draft, also update this item. + if self.draft? && send(self.class.draft_association_name).create? + the_changes = changes_for_draftsman(:create) + data = { item: self } + data[Draftsman.whodunnit_field] = Draftsman.whodunnit + data[:object] = object_attrs_for_draft_record if Draftsman.stash_drafted_changes? + data[:object_changes] = serialized_draft_changeset(the_changes) if track_object_changes_for_draft? + + data = merge_metadata_for_draft(data) + send(self.class.draft_association_name).update(data) + save + else + the_changes = changes_for_draftsman(:update) + save_only_columns_for_draft if Draftsman.stash_drafted_changes? + + # Destroy the draft if this record has changed back to the + # original values. + if self.draft? && the_changes.empty? + nilified_draft = send(self.class.draft_association_name) + touch = changed? + send("#{self.class.draft_association_name}_id=", nil) + save(touch: touch) + nilified_draft.destroy + # Save an update draft if record is changed notably. + elsif !the_changes.empty? + data = { item: self, event: :update } + data[Draftsman.whodunnit_field] = Draftsman.whodunnit + data[:object] = object_attrs_for_draft_record if Draftsman.stash_drafted_changes? + data[:object_changes] = serialized_draft_changeset(the_changes) if track_object_changes_for_draft? + data = merge_metadata_for_draft(data) + + # If there's already a draft, update it. + if self.draft? + send(self.class.draft_association_name).update(data) + + if Draftsman.stash_drafted_changes? + update_skipped_attributes + else + self.save + end + # If there's not an existing draft, create an update draft. + else + send("build_#{self.class.draft_association_name}", data) + + if send(self.class.draft_association_name).save + update_column("#{self.class.draft_association_name}_id", send(self.class.draft_association_name).id) + + if Draftsman.stash_drafted_changes? + update_skipped_attributes + else + self.save + end + else + raise ActiveRecord::Rollback and return false + end + end + # Otherwise, just save the record. + else + self.save + end + end + end + end + rescue Exception => e + false + end + + end + end +end diff --git a/lib/generators/draftsman/templates/config/initializers/draftsman.rb b/lib/generators/draftsman/templates/config/initializers/draftsman.rb index 6bfe9e1..cddca6a 100644 --- a/lib/generators/draftsman/templates/config/initializers/draftsman.rb +++ b/lib/generators/draftsman/templates/config/initializers/draftsman.rb @@ -1,7 +1,7 @@ # Override global `draft` class. For example, perhaps you want your own class at # `app/models/draft.rb` that adds extra attributes, validations, associations, -# methods, etc. Be sure that this new model class extends `Draftsman::Draft`. -# Draftsman.draft_class_name = 'Draftsman::Draft' +# methods, etc. Be sure that this new model class extends `Draftsman::Single::Draft` or `Draftsman::Multiple::Draft`. +# Draftsman.draft_class_name = 'Draftsman::Single::Draft' # Serializer for `object`, `object_changes`, and `previous_draft` columns. To # use the JSON serializer, change to `Draftsman::Serializers::Json`. You could @@ -18,7 +18,7 @@ # Field which records who last recorded the draft. # Draftsman.whodunnit_field = :whodunnit -# Whether or not to stash draft data in the `Draftsman::Draft` record. If set to +# Whether or not to stash draft data in the `Draftsman::Single::Draft` record. If set to # `false`, all changes will be persisted to the main record and will not be # persisted to the draft record's `object` column. # Draftsman.stash_drafted_changes = true diff --git a/spec/controllers/informants_controller_spec.rb b/spec/controllers/informants_controller_spec.rb index 91b3177..ce870e0 100644 --- a/spec/controllers/informants_controller_spec.rb +++ b/spec/controllers/informants_controller_spec.rb @@ -6,7 +6,7 @@ describe 'create' do before { post :create } - subject { Draftsman::Draft.last } + subject { Draftsman::Single::Draft.last } it 'records `ip` from custom `info_for_draftsman`' do expect(subject.ip).to eql '123.45.67.89' @@ -19,7 +19,7 @@ describe 'update' do before { put :update, params: { id: trashable.id } } - subject { Draftsman::Draft.last } + subject { Draftsman::Single::Draft.last } it 'records `ip` from custom `info_for_draftsman`' do expect(subject.ip).to eql '123.45.67.89' @@ -32,7 +32,7 @@ describe 'destroy' do before { delete :destroy, params: { id: trashable.id } } - subject { Draftsman::Draft.last } + subject { Draftsman::Single::Draft.last } it 'records `ip` from custom `info_for_draftsman`' do expect(subject.ip).to eql '123.45.67.89' diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index b6d3a9f..7ed27d0 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -5,7 +5,7 @@ describe 'create' do before { post :create } - subject { Draftsman::Draft.last } + subject { Draftsman::Single::Draft.last } it 'records user name via `user_for_draftsman`' do expect(subject.whodunnit).to eql 'A User' @@ -14,7 +14,7 @@ describe 'update' do before { put :update, params: { id: trashable.id } } - subject { return Draftsman::Draft.last } + subject { return Draftsman::Single::Draft.last } it 'records user name via `user_for_draftsman`' do expect(subject.whodunnit).to eql 'A User' @@ -23,7 +23,7 @@ describe 'destroy' do before { delete :destroy, params: { id: trashable.id } } - subject { return Draftsman::Draft.last } + subject { return Draftsman::Single::Draft.last } it 'records user name via `user_for_draftsman`' do expect(subject.whodunnit).to eql 'A User' diff --git a/spec/controllers/whodunnits_controller_spec.rb b/spec/controllers/whodunnits_controller_spec.rb index 81a4fbe..72ec4d6 100644 --- a/spec/controllers/whodunnits_controller_spec.rb +++ b/spec/controllers/whodunnits_controller_spec.rb @@ -6,7 +6,7 @@ describe 'create' do before { post :create } - subject { Draftsman::Draft.last } + subject { Draftsman::Single::Draft.last } it 'records `current_user` via `user_for_draftsman' do expect(subject.whodunnit).to eql '153' @@ -15,7 +15,7 @@ describe 'update' do before { put :update, params: { id: trashable.id } } - subject { Draftsman::Draft.last } + subject { Draftsman::Single::Draft.last } it 'records `current_user` via `user_for_draftsman' do expect(subject.whodunnit).to eql '153' @@ -24,7 +24,7 @@ describe 'destroy' do before { delete :destroy, params: { id: trashable.id } } - subject { Draftsman::Draft.last } + subject { Draftsman::Single::Draft.last } it 'records `current_user` via `user_for_draftsman' do expect(subject.whodunnit).to eql '153' diff --git a/spec/dummy/app/models/overridden_draft.rb b/spec/dummy/app/models/overridden_draft.rb index ef09fcf..3b90687 100644 --- a/spec/dummy/app/models/overridden_draft.rb +++ b/spec/dummy/app/models/overridden_draft.rb @@ -1,6 +1,6 @@ -require 'draftsman/draft' +require 'draftsman/single/draft' -class OverriddenDraft < Draftsman::Draft +class OverriddenDraft < Draftsman::Single::Draft def im_overridden true end diff --git a/spec/models/child_spec.rb b/spec/models/child_spec.rb index 0ad92b1..93cf7a9 100644 --- a/spec/models/child_spec.rb +++ b/spec/models/child_spec.rb @@ -34,15 +34,15 @@ end it 'destroys 2 drafts overall' do - expect { subject }.to change(Draftsman::Draft, :count).by(-2) + expect { subject }.to change(Draftsman::Single::Draft, :count).by(-2) end it "destroys the child's draft" do - expect { subject }.to change(Draftsman::Draft.where(:item_type => 'Child'), :count).by(-1) + expect { subject }.to change(Draftsman::Single::Draft.where(:item_type => 'Child'), :count).by(-1) end it "destroys the parent's draft" do - expect { subject }.to change(Draftsman::Draft.where(:item_type => 'Parent'), :count).by(-1) + expect { subject }.to change(Draftsman::Single::Draft.where(:item_type => 'Parent'), :count).by(-1) end end @@ -65,11 +65,11 @@ end it 'destroys 1 draft overall' do - expect { subject }.to change(Draftsman::Draft, :count).by(-1) + expect { subject }.to change(Draftsman::Single::Draft, :count).by(-1) end it "destroys the child's draft" do - expect { subject }.to change(Draftsman::Draft.where(:item_type => 'Child'), :count).by(-1) + expect { subject }.to change(Draftsman::Single::Draft.where(:item_type => 'Child'), :count).by(-1) end end end @@ -92,11 +92,11 @@ end it 'destroys 1 draft overall' do - expect { subject }.to change(Draftsman::Draft, :count).by(-1) + expect { subject }.to change(Draftsman::Single::Draft, :count).by(-1) end it "destroys the child's draft" do - expect { subject }.to change(Draftsman::Draft.where(:item_type => 'Child'), :count).by(-1) + expect { subject }.to change(Draftsman::Single::Draft.where(:item_type => 'Child'), :count).by(-1) end end @@ -127,15 +127,15 @@ end it 'destroys 2 drafts overall' do - expect { subject }.to change(Draftsman::Draft, :count).by(-2) + expect { subject }.to change(Draftsman::Single::Draft, :count).by(-2) end it "destroys the parent's draft" do - expect { subject }.to change(Draftsman::Draft.where(:item_type => 'Parent'), :count).by(-1) + expect { subject }.to change(Draftsman::Single::Draft.where(:item_type => 'Parent'), :count).by(-1) end it "destroys the child's draft" do - expect { subject }.to change(Draftsman::Draft.where(:item_type => 'Child'), :count).by(-1) + expect { subject }.to change(Draftsman::Single::Draft.where(:item_type => 'Child'), :count).by(-1) end it 'removes the parent from the trash' do diff --git a/spec/models/draft_spec.rb b/spec/models/draft_spec.rb index de65665..819f565 100644 --- a/spec/models/draft_spec.rb +++ b/spec/models/draft_spec.rb @@ -1,23 +1,23 @@ require 'spec_helper' -describe Draftsman::Draft do +describe Draftsman::Single::Draft do let(:trashable) { Trashable.new(name: 'Bob') } describe '.object_col_is_json?' do it 'does not have a JSON object column' do - expect(Draftsman::Draft.object_col_is_json?).to eql false + expect(Draftsman::Single::Draft.object_col_is_json?).to eql false end end describe '.object_changes_col_is_json?' do it 'does not have a JSON object_changes column' do - expect(Draftsman::Draft.object_changes_col_is_json?).to eql false + expect(Draftsman::Single::Draft.object_changes_col_is_json?).to eql false end end describe '.previous_draft_col_is_json?' do it 'does not have a JSON previous_draft column' do - expect(Draftsman::Draft.previous_draft_col_is_json?).to eql false + expect(Draftsman::Single::Draft.previous_draft_col_is_json?).to eql false end end @@ -518,7 +518,7 @@ end it 'deletes the draft record' do - expect { trashable.draft.publish! }.to change(Draftsman::Draft, :count).by(-1) + expect { trashable.draft.publish! }.to change(Draftsman::Single::Draft, :count).by(-1) end end # with `create` draft @@ -574,7 +574,7 @@ end it 'destroys the draft' do - expect { trashable.draft.publish! }.to change(Draftsman::Draft, :count).by(-1) + expect { trashable.draft.publish! }.to change(Draftsman::Single::Draft, :count).by(-1) end it 'does not delete the associated item' do @@ -590,7 +590,7 @@ end it 'destroys the draft' do - expect { trashable.draft.publish! }.to change(Draftsman::Draft, :count).by(-1) + expect { trashable.draft.publish! }.to change(Draftsman::Single::Draft, :count).by(-1) end it 'deletes the associated item' do @@ -605,7 +605,7 @@ end it 'destroys the draft' do - expect { trashable.draft.publish! }.to change(Draftsman::Draft, :count).by(-1) + expect { trashable.draft.publish! }.to change(Draftsman::Single::Draft, :count).by(-1) end it 'deletes the associated item' do @@ -662,7 +662,7 @@ end it 'deletes the draft record' do - expect { trashable.draft.publish! }.to change(Draftsman::Draft, :count).by(-1) + expect { trashable.draft.publish! }.to change(Draftsman::Single::Draft, :count).by(-1) end end @@ -718,7 +718,7 @@ end it 'destroys the draft' do - expect { trashable.draft.publish! }.to change(Draftsman::Draft, :count).by(-1) + expect { trashable.draft.publish! }.to change(Draftsman::Single::Draft, :count).by(-1) end it 'does not delete the associated item' do @@ -734,7 +734,7 @@ end it 'destroys the draft' do - expect { trashable.draft.publish! }.to change(Draftsman::Draft, :count).by(-1) + expect { trashable.draft.publish! }.to change(Draftsman::Single::Draft, :count).by(-1) end it 'deletes the associated item' do @@ -749,7 +749,7 @@ end it 'destroys the draft' do - expect { trashable.draft.publish! }.to change(Draftsman::Draft, :count).by(-1) + expect { trashable.draft.publish! }.to change(Draftsman::Single::Draft, :count).by(-1) end it 'deletes the associated item' do @@ -770,7 +770,7 @@ end it 'destroys the draft' do - expect { trashable.draft.revert! }.to change(Draftsman::Draft, :count).by(-1) + expect { trashable.draft.revert! }.to change(Draftsman::Single::Draft, :count).by(-1) end it 'destroys associated item' do @@ -810,7 +810,7 @@ end it 'destroys the draft record' do - expect { trashable.draft.revert! }.to change(Draftsman::Draft, :count).by(-1) + expect { trashable.draft.revert! }.to change(Draftsman::Single::Draft, :count).by(-1) end it 'does not destroy the associated item' do @@ -855,7 +855,7 @@ end it 'destroys the draft record' do - expect { trashable.draft.revert! }.to change(Draftsman::Draft, :count).by(-1) + expect { trashable.draft.revert! }.to change(Draftsman::Single::Draft, :count).by(-1) end it 'does not destroy the associated item' do @@ -899,11 +899,11 @@ end it 'destroys the `destroy` draft record' do - expect { trashable.draft.revert! }.to change(Draftsman::Draft.where(event: :destroy), :count).by(-1) + expect { trashable.draft.revert! }.to change(Draftsman::Single::Draft.where(event: :destroy), :count).by(-1) end it 'reifies the previous `create` draft record' do - expect { trashable.draft.revert! }.to change(Draftsman::Draft.where(event: :create), :count).by(1) + expect { trashable.draft.revert! }.to change(Draftsman::Single::Draft.where(event: :create), :count).by(1) end it 'does not destroy the associated item' do @@ -930,7 +930,7 @@ end it 'destroys the draft' do - expect { trashable.draft.revert! }.to change(Draftsman::Draft, :count).by(-1) + expect { trashable.draft.revert! }.to change(Draftsman::Single::Draft, :count).by(-1) end it 'destroys associated item' do @@ -970,7 +970,7 @@ end it 'destroys the draft record' do - expect { trashable.draft.revert! }.to change(Draftsman::Draft, :count).by(-1) + expect { trashable.draft.revert! }.to change(Draftsman::Single::Draft, :count).by(-1) end it 'does not destroy the associated item' do @@ -1015,7 +1015,7 @@ end it 'destroys the draft record' do - expect { trashable.draft.revert! }.to change(Draftsman::Draft, :count).by(-1) + expect { trashable.draft.revert! }.to change(Draftsman::Single::Draft, :count).by(-1) end it 'does not destroy the associated item' do @@ -1059,11 +1059,11 @@ end it 'destroys the `destroy` draft record' do - expect { trashable.draft.revert! }.to change(Draftsman::Draft.where(event: :destroy), :count).by(-1) + expect { trashable.draft.revert! }.to change(Draftsman::Single::Draft.where(event: :destroy), :count).by(-1) end it 'reifies the previous `create` draft record' do - expect { trashable.draft.revert! }.to change(Draftsman::Draft.where(event: :create), :count).by(1) + expect { trashable.draft.revert! }.to change(Draftsman::Single::Draft.where(event: :create), :count).by(1) end it 'does not destroy the associated item' do diff --git a/spec/models/overridden_draft_spec.rb b/spec/models/overridden_draft_spec.rb index 6c560a9..356f707 100644 --- a/spec/models/overridden_draft_spec.rb +++ b/spec/models/overridden_draft_spec.rb @@ -32,9 +32,9 @@ class Vanilla < ActiveRecord::Base end describe '#draft.class.name' do - it 'has the default `Draftsman::Draft` record as its draft' do + it 'has the default `Draftsman::Single::Draft` record as its draft' do vanilla.save_draft - expect(vanilla.reload.draft.class.name).to eql 'Draftsman::Draft' + expect(vanilla.reload.draft.class.name).to eql 'Draftsman::Single::Draft' end end end diff --git a/spec/models/parent_spec.rb b/spec/models/parent_spec.rb index 29c7edd..f35cc8d 100644 --- a/spec/models/parent_spec.rb +++ b/spec/models/parent_spec.rb @@ -34,11 +34,11 @@ end it 'destroys 1 draft' do - expect { subject }.to change(Draftsman::Draft, :count).by(-1) + expect { subject }.to change(Draftsman::Single::Draft, :count).by(-1) end it "destroys the parent's draft" do - expect { subject }.to change(Draftsman::Draft.where(:item_type => 'Parent'), :count).by(-1) + expect { subject }.to change(Draftsman::Single::Draft.where(:item_type => 'Parent'), :count).by(-1) end end @@ -60,7 +60,7 @@ end it 'destroys 2 drafts' do - expect { subject }.to change(Draftsman::Draft, :count).by(-2) + expect { subject }.to change(Draftsman::Single::Draft, :count).by(-2) end end end @@ -83,7 +83,7 @@ end it 'destroys both drafts' do - expect { subject }.to change(Draftsman::Draft, :count).by(-2) + expect { subject }.to change(Draftsman::Single::Draft, :count).by(-2) end end @@ -113,11 +113,11 @@ end it "keeps the child's draft" do - expect { subject }.to_not change(Draftsman::Draft.where(:item_type => 'Child'), :count) + expect { subject }.to_not change(Draftsman::Single::Draft.where(:item_type => 'Child'), :count) end it "deletes the parent's draft" do - expect { subject }.to change(Draftsman::Draft.where(:item_type => 'Parent'), :count).by(-1) + expect { subject }.to change(Draftsman::Single::Draft.where(:item_type => 'Parent'), :count).by(-1) end it "keeps the child's draft" do diff --git a/spec/models/skipper_spec.rb b/spec/models/skipper_spec.rb index 28ad5f5..a7f8a57 100644 --- a/spec/models/skipper_spec.rb +++ b/spec/models/skipper_spec.rb @@ -102,7 +102,7 @@ end it 'creates a new draft' do - expect { subject }.to change(Draftsman::Draft, :count).by(1) + expect { subject }.to change(Draftsman::Single::Draft, :count).by(1) end it 'has a newer `updated_at`' do @@ -143,7 +143,7 @@ end it 'destroys the draft' do - expect { subject }.to change(Draftsman::Draft.where(:id => skipper.draft_id), :count).by(-1) + expect { subject }.to change(Draftsman::Single::Draft.where(:id => skipper.draft_id), :count).by(-1) end it 'has a newer `updated_at`' do @@ -193,7 +193,7 @@ end it 'updates the existing draft' do - expect { subject }.to_not change(Draftsman::Draft.where(:id => skipper.draft_id), :count) + expect { subject }.to_not change(Draftsman::Single::Draft.where(:id => skipper.draft_id), :count) end it "updates the draft's `name`" do @@ -258,7 +258,7 @@ end it "doesn't change the number of drafts" do - expect { subject }.to_not change(Draftsman::Draft.where(:id => skipper.draft_id), :count) + expect { subject }.to_not change(Draftsman::Single::Draft.where(:id => skipper.draft_id), :count) end it 'has the original `updated_at`' do @@ -310,7 +310,7 @@ end it 'updates the existing draft' do - expect { subject }.to_not change(Draftsman::Draft.where(:id => skipper.draft_id), :count) + expect { subject }.to_not change(Draftsman::Single::Draft.where(:id => skipper.draft_id), :count) end it "updates the draft's `name`" do @@ -357,7 +357,7 @@ end it 'updates the existing draft' do - expect { subject }.to_not change(Draftsman::Draft.where(:id => skipper.draft_id), :count) + expect { subject }.to_not change(Draftsman::Single::Draft.where(:id => skipper.draft_id), :count) end it "keeps the draft's `name`" do @@ -404,7 +404,7 @@ end it "doesn't change the number of drafts" do - expect { subject }.to_not change(Draftsman::Draft.where(:id => skipper.draft_id), :count) + expect { subject }.to_not change(Draftsman::Single::Draft.where(:id => skipper.draft_id), :count) end it "does not update the draft's `name`" do diff --git a/spec/models/trashable_spec.rb b/spec/models/trashable_spec.rb index b2c7eaa..7b11c12 100644 --- a/spec/models/trashable_spec.rb +++ b/spec/models/trashable_spec.rb @@ -65,7 +65,7 @@ end it 'keeps the associated draft' do - expect { subject }.to_not change(Draftsman::Draft.where(:id => trashable.draft_id), :count) + expect { subject }.to_not change(Draftsman::Single::Draft.where(:id => trashable.draft_id), :count) end it 'retains its `name` in the draft' do @@ -129,7 +129,7 @@ end it 'keeps the associated draft' do - expect { subject }.to_not change(Draftsman::Draft.where(:id => trashable.draft_id), :count) + expect { subject }.to_not change(Draftsman::Single::Draft.where(:id => trashable.draft_id), :count) end it "retains the updated draft's name in the draft" do @@ -190,7 +190,7 @@ end it 'creates a draft' do - expect { subject }.to change(Draftsman::Draft, :count).by(1) + expect { subject }.to change(Draftsman::Single::Draft, :count).by(1) end end end diff --git a/spec/models/vanilla_spec.rb b/spec/models/vanilla_spec.rb index 94948e5..5d60be4 100644 --- a/spec/models/vanilla_spec.rb +++ b/spec/models/vanilla_spec.rb @@ -108,7 +108,7 @@ end it 'creates a new draft' do - expect { vanilla.save_draft }.to change(Draftsman::Draft, :count).by(1) + expect { vanilla.save_draft }.to change(Draftsman::Single::Draft, :count).by(1) end it 'has the original `updated_at`' do @@ -153,7 +153,7 @@ end it 'destroys the draft' do - expect { vanilla.save_draft }.to change(Draftsman::Draft.where(id: vanilla.draft_id), :count).by(-1) + expect { vanilla.save_draft }.to change(Draftsman::Single::Draft.where(id: vanilla.draft_id), :count).by(-1) end it 'has the original `updated_at`' do @@ -200,7 +200,7 @@ end it 'updates the existing draft' do - expect { vanilla.save_draft }.to_not change(Draftsman::Draft.where(id: vanilla.draft_id), :count) + expect { vanilla.save_draft }.to_not change(Draftsman::Single::Draft.where(id: vanilla.draft_id), :count) end it "updates the draft's `name`" do @@ -260,7 +260,7 @@ end it "doesn't change the number of drafts" do - expect { vanilla.save_draft }.to_not change(Draftsman::Draft.where(id: vanilla.draft_id), :count) + expect { vanilla.save_draft }.to_not change(Draftsman::Single::Draft.where(id: vanilla.draft_id), :count) end it 'has the original `updated_at`' do @@ -313,7 +313,7 @@ end it 'updates the existing draft' do - expect { vanilla.save_draft }.to_not change(Draftsman::Draft.where(id: vanilla.draft_id), :count) + expect { vanilla.save_draft }.to_not change(Draftsman::Single::Draft.where(id: vanilla.draft_id), :count) end it "updates the draft's `name`" do @@ -371,7 +371,7 @@ end it "doesn't change the number of drafts" do - expect { vanilla.save_draft }.to_not change(Draftsman::Draft.where(id: vanilla.draft_id), :count) + expect { vanilla.save_draft }.to_not change(Draftsman::Single::Draft.where(id: vanilla.draft_id), :count) end it "does not update the draft's `name`" do @@ -435,7 +435,7 @@ end it 'creates a new draft' do - expect { vanilla.save_draft }.to change(Draftsman::Draft, :count).by(1) + expect { vanilla.save_draft }.to change(Draftsman::Single::Draft, :count).by(1) end it 'has a new `updated_at`' do @@ -481,7 +481,7 @@ end it 'destroys the draft' do - expect { vanilla.save_draft }.to change(Draftsman::Draft.where(id: vanilla.draft_id), :count).by(-1) + expect { vanilla.save_draft }.to change(Draftsman::Single::Draft.where(id: vanilla.draft_id), :count).by(-1) end it 'has a new `updated_at`' do @@ -524,7 +524,7 @@ end it 'updates the existing draft' do - expect { vanilla.save_draft }.to_not change(Draftsman::Draft.where(id: vanilla.draft_id), :count) + expect { vanilla.save_draft }.to_not change(Draftsman::Single::Draft.where(id: vanilla.draft_id), :count) end it "updates the draft's `name`" do @@ -576,7 +576,7 @@ end it "doesn't change the number of drafts" do - expect { vanilla.save_draft }.to_not change(Draftsman::Draft.where(id: vanilla.draft_id), :count) + expect { vanilla.save_draft }.to_not change(Draftsman::Single::Draft.where(id: vanilla.draft_id), :count) end it 'has the original `updated_at`' do @@ -628,7 +628,7 @@ end it 'updates the existing draft' do - expect { vanilla.save_draft }.to_not change(Draftsman::Draft.where(id: vanilla.draft_id), :count) + expect { vanilla.save_draft }.to_not change(Draftsman::Single::Draft.where(id: vanilla.draft_id), :count) end it "updates the draft's `name`" do @@ -683,7 +683,7 @@ end it "doesn't change the number of drafts" do - expect { vanilla.save_draft }.to_not change(Draftsman::Draft.where(id: vanilla.draft_id), :count) + expect { vanilla.save_draft }.to_not change(Draftsman::Single::Draft.where(id: vanilla.draft_id), :count) end it "does not update the draft's `name`" do diff --git a/spec/models/whitelister_spec.rb b/spec/models/whitelister_spec.rb index f38db03..476637e 100644 --- a/spec/models/whitelister_spec.rb +++ b/spec/models/whitelister_spec.rb @@ -72,7 +72,7 @@ end it 'creates a new draft' do - expect { whitelister.save_draft }.to change(Draftsman::Draft, :count).by(1) + expect { whitelister.save_draft }.to change(Draftsman::Single::Draft, :count).by(1) end it 'has an `update` draft' do @@ -124,7 +124,7 @@ end it 'destroys the draft' do - expect { whitelister.save_draft }.to change(Draftsman::Draft.where(id: whitelister.draft_id), :count).by(-1) + expect { whitelister.save_draft }.to change(Draftsman::Single::Draft.where(id: whitelister.draft_id), :count).by(-1) end end end @@ -171,7 +171,7 @@ end it 'updates the existing draft' do - expect { whitelister.save_draft }.to_not change(Draftsman::Draft.where(id: whitelister.draft_id), :count) + expect { whitelister.save_draft }.to_not change(Draftsman::Single::Draft.where(id: whitelister.draft_id), :count) end it "updates the draft's `name`" do @@ -224,7 +224,7 @@ end it "doesn't change the number of drafts" do - expect { whitelister.save_draft }.to_not change(Draftsman::Draft.where(id: whitelister.draft_id), :count) + expect { whitelister.save_draft }.to_not change(Draftsman::Single::Draft.where(id: whitelister.draft_id), :count) end end end @@ -280,7 +280,7 @@ end it 'updates the existing draft' do - expect { whitelister.save_draft }.to_not change(Draftsman::Draft.where(id: whitelister.draft_id), :count) + expect { whitelister.save_draft }.to_not change(Draftsman::Single::Draft.where(id: whitelister.draft_id), :count) end it "updates its draft's `name`" do @@ -327,7 +327,7 @@ end it "doesn't change the number of drafts" do - expect { whitelister.save_draft }.to_not change(Draftsman::Draft.where(id: whitelister.draft_id), :count) + expect { whitelister.save_draft }.to_not change(Draftsman::Single::Draft.where(id: whitelister.draft_id), :count) end it "does not update its draft's `name`" do @@ -388,7 +388,7 @@ end it 'does not create a draft' do - expect { whitelister.save_draft }.to_not change(Draftsman::Draft, :count) + expect { whitelister.save_draft }.to_not change(Draftsman::Single::Draft, :count) end # Not affected by this customization @@ -438,7 +438,7 @@ end it 'updates the existing draft' do - expect { whitelister.save_draft }.to_not change(Draftsman::Draft.where(id: whitelister.draft_id), :count) + expect { whitelister.save_draft }.to_not change(Draftsman::Single::Draft.where(id: whitelister.draft_id), :count) end it "updates its draft's `ignored` attribute" do @@ -505,7 +505,7 @@ end it 'updates the existing draft' do - expect { whitelister.save_draft }.to_not change(Draftsman::Draft.where(id: whitelister.draft_id), :count) + expect { whitelister.save_draft }.to_not change(Draftsman::Single::Draft.where(id: whitelister.draft_id), :count) end it "updates its draft's `name`" do @@ -564,7 +564,7 @@ end it "doesn't change the number of drafts" do - expect { whitelister.save_draft }.to_not change(Draftsman::Draft.where(id: whitelister.draft_id), :count) + expect { whitelister.save_draft }.to_not change(Draftsman::Single::Draft.where(id: whitelister.draft_id), :count) end it "does not update its draft's `name`" do