Class: Draftsman::Draft

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
lib/draftsman/draft.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.object_changes_col_is_json?Boolean

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

Returns:

  • (Boolean)


33
34
35
# 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?Boolean

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

Returns:

  • (Boolean)


27
28
29
# File 'lib/draftsman/draft.rb', line 27

def self.object_changes_col_present?
  column_names.include?('object_changes')
end

.object_col_is_json?Boolean

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

Returns:

  • (Boolean)


22
23
24
# 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?Boolean

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

Returns:

  • (Boolean)


38
39
40
# 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) ⇒ Object



16
17
18
# 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

Instance Method Details

#changesetObject

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.



45
46
47
48
# File 'lib/draftsman/draft.rb', line 45

def changeset
  return nil unless self.class.object_changes_col_present?
  @changeset ||= load_changeset
end

#create?Boolean

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

Returns:

  • (Boolean)


51
52
53
# File 'lib/draftsman/draft.rb', line 51

def create?
  self.event == 'create'
end

#destroy?Boolean

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

Returns:

  • (Boolean)


56
57
58
# File 'lib/draftsman/draft.rb', line 56

def destroy?
  self.event == 'destroy'
end

#draft_publication_dependenciesObject

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



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# 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_dependenciesObject

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



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# 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!Object

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.



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# 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

#reifyObject

Returns instance of item converted to its drafted state.

Example usage:

`@category = @category.draft.reify if @category.draft?`


199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# 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!Object

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.



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# 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?Boolean

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

Returns:

  • (Boolean)


293
294
295
# File 'lib/draftsman/draft.rb', line 293

def update?
  self.event.to_sym == :update
end