module Draftsman::Model::InstanceMethods
Public Instance Methods
Returns whether or not this item has a draft.
# File lib/draftsman/model.rb, line 139 def draft? send(self.class.draft_association_name).present? end
DEPRECATED: Use `#draft_save` instead.
# File lib/draftsman/model.rb, line 144 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.
# File lib/draftsman/model.rb, line 150 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.
# File lib/draftsman/model.rb, line 159 def draft_destruction run_callbacks :draft_destruction do _draft_destruction end end
DEPRECATED: Use `#draft_save` instead.
# File lib/draftsman/model.rb, line 166 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.
# File lib/draftsman/model.rb, line 172 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.
# File lib/draftsman/model.rb, line 187 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.
# File lib/draftsman/model.rb, line 206 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
# File lib/draftsman/model.rb, line 217 def trashed? send(self.class.trashed_at_attribute_name).present? end
Private Instance Methods
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.
# File lib/draftsman/model.rb, line 226 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.`
# File lib/draftsman/model.rb, line 260 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.
# File lib/draftsman/model.rb, line 316 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.
# File lib/draftsman/model.rb, line 392 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.
# File lib/draftsman/model.rb, line 422 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
# File lib/draftsman/model.rb, line 445 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.
# File lib/draftsman/model.rb, line 460 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.
# File lib/draftsman/model.rb, line 465 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.
# File lib/draftsman/model.rb, line 470 def trash! self.update_column(self.class.trashed_at_attribute_name, Time.now) end
Updates skipped attributes' values on this model.
# File lib/draftsman/model.rb, line 475 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