draftsman 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +3 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +5 -0
  7. data/Gemfile +3 -0
  8. data/Gemfile.lock +97 -0
  9. data/LICENSE +20 -0
  10. data/README.md +506 -0
  11. data/Rakefile +6 -0
  12. data/draftsman.gemspec +33 -0
  13. data/lib/draftsman/config.rb +13 -0
  14. data/lib/draftsman/draft.rb +289 -0
  15. data/lib/draftsman/frameworks/cucumber.rb +7 -0
  16. data/lib/draftsman/frameworks/rails.rb +58 -0
  17. data/lib/draftsman/frameworks/rspec.rb +16 -0
  18. data/lib/draftsman/frameworks/sinatra.rb +31 -0
  19. data/lib/draftsman/model.rb +428 -0
  20. data/lib/draftsman/serializers/json.rb +17 -0
  21. data/lib/draftsman/serializers/yaml.rb +17 -0
  22. data/lib/draftsman/version.rb +3 -0
  23. data/lib/draftsman.rb +101 -0
  24. data/lib/generators/draftsman/install_generator.rb +27 -0
  25. data/lib/generators/draftsman/templates/add_object_changes_column_to_drafts.rb +9 -0
  26. data/lib/generators/draftsman/templates/config/initializers/draftsman.rb +11 -0
  27. data/lib/generators/draftsman/templates/create_drafts.rb +22 -0
  28. data/spec/controllers/informants_controller_spec.rb +27 -0
  29. data/spec/controllers/users_controller_spec.rb +23 -0
  30. data/spec/controllers/whodunnits_controller_spec.rb +24 -0
  31. data/spec/draftsman_spec.rb +19 -0
  32. data/spec/dummy/Rakefile +7 -0
  33. data/spec/dummy/app/assets/images/rails.png +0 -0
  34. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  35. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  36. data/spec/dummy/app/controllers/application_controller.rb +20 -0
  37. data/spec/dummy/app/controllers/informants_controller.rb +8 -0
  38. data/spec/dummy/app/controllers/users_controller.rb +8 -0
  39. data/spec/dummy/app/controllers/whodunnits_controller.rb +8 -0
  40. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  41. data/spec/dummy/app/helpers/messages_helper.rb +2 -0
  42. data/spec/dummy/app/mailers/.gitkeep +0 -0
  43. data/spec/dummy/app/models/bastard.rb +3 -0
  44. data/spec/dummy/app/models/child.rb +4 -0
  45. data/spec/dummy/app/models/parent.rb +5 -0
  46. data/spec/dummy/app/models/trashable.rb +3 -0
  47. data/spec/dummy/app/models/vanilla.rb +3 -0
  48. data/spec/dummy/app/models/whitelister.rb +3 -0
  49. data/spec/dummy/app/views/layouts/application.html.erb +15 -0
  50. data/spec/dummy/config/application.rb +37 -0
  51. data/spec/dummy/config/boot.rb +6 -0
  52. data/spec/dummy/config/database.yml +25 -0
  53. data/spec/dummy/config/environment.rb +5 -0
  54. data/spec/dummy/config/environments/development.rb +32 -0
  55. data/spec/dummy/config/environments/production.rb +73 -0
  56. data/spec/dummy/config/environments/test.rb +39 -0
  57. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  58. data/spec/dummy/config/initializers/inflections.rb +15 -0
  59. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  60. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  61. data/spec/dummy/config/initializers/session_store.rb +8 -0
  62. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  63. data/spec/dummy/config/locales/en.yml +5 -0
  64. data/spec/dummy/config/routes.rb +6 -0
  65. data/spec/dummy/config.ru +4 -0
  66. data/spec/dummy/db/migrate/20110208155312_set_up_test_tables.rb +86 -0
  67. data/spec/dummy/db/schema.rb +106 -0
  68. data/spec/dummy/db/seeds.rb +7 -0
  69. data/spec/dummy/lib/assets/.gitkeep +0 -0
  70. data/spec/dummy/lib/tasks/.gitkeep +0 -0
  71. data/spec/dummy/log/.gitkeep +0 -0
  72. data/spec/dummy/public/404.html +26 -0
  73. data/spec/dummy/public/422.html +26 -0
  74. data/spec/dummy/public/500.html +25 -0
  75. data/spec/dummy/public/favicon.ico +0 -0
  76. data/spec/dummy/script/rails +6 -0
  77. data/spec/models/child_spec.rb +205 -0
  78. data/spec/models/draft_spec.rb +297 -0
  79. data/spec/models/parent_spec.rb +191 -0
  80. data/spec/models/trashable_spec.rb +164 -0
  81. data/spec/models/vanilla_spec.rb +201 -0
  82. data/spec/models/whitelister_spec.rb +262 -0
  83. data/spec/spec_helper.rb +52 -0
  84. metadata +304 -0
@@ -0,0 +1,428 @@
1
+ module Draftsman
2
+ module Model
3
+
4
+ def self.included(base)
5
+ base.send :extend, ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ # Declare this in your model to enable the Draftsman API for it. A draft of the model is available in the `draft`
10
+ # association (if one exists).
11
+ #
12
+ # Options:
13
+ #
14
+ # :class_name
15
+ # The name of a custom `Draft` class. This class should inherit from `Draftsman::Draft`. A global default can be
16
+ # set for this using `Draftsman.draft_class_name=` if the default of `Draftsman::Draft` needs to be overridden.
17
+ #
18
+ # :ignore
19
+ # An array of attributes for which an update to a `Draft` will not be stored if they are the only ones changed.
20
+ #
21
+ # :only
22
+ # Inverse of `ignore` - a new `Draft` will be created only for these attributes if supplied. It's recommended that
23
+ # you only specify optional attributes for this (that can be empty).
24
+ #
25
+ # :skip
26
+ # Fields to ignore completely. As with `ignore`, updates to these fields will not create a new `Draft`. In
27
+ # addition, these fields will not be included in the serialized versions of the object whenever a new `Draft` is
28
+ # created.
29
+ #
30
+ # :meta
31
+ # A hash of extra data to store. You must add a column to the `drafts` table for each key. Values are objects or
32
+ # `procs` (which are called with `self`, i.e. the model with the `has_drafts`). See
33
+ # `Draftsman::Controller.info_for_draftsman` for an example of how to store data from the controller.
34
+ #
35
+ # :draft
36
+ # The name to use for the `draft` association shortcut method. Default is `:draft`.
37
+ #
38
+ # :published_at
39
+ # The name to use for the method which returns the published timestamp. Default is `published_at`.
40
+ #
41
+ # :trashed_at
42
+ # The name to use for the method which returns the soft delete timestamp. Default is `trashed_at`.
43
+ def has_drafts(options = {})
44
+ # Lazily include the instance methods so we don't clutter up
45
+ # any more ActiveRecord models than we need to.
46
+ send :include, InstanceMethods
47
+
48
+ class_attribute :draftsman_options
49
+ self.draftsman_options = options.dup
50
+
51
+ class_attribute :draft_association_name
52
+ self.draft_association_name = options[:draft] || :draft
53
+
54
+ class_attribute :draft_class_name
55
+ self.draft_class_name = options[:class_name] || Draftsman.draft_class_name
56
+
57
+ [:ignore, :skip, :only].each do |key|
58
+ draftsman_options[key] = ([draftsman_options[key]].flatten.compact || []).map(&:to_s)
59
+ end
60
+
61
+ draftsman_options[:ignore] << "#{self.draft_association_name}_id"
62
+
63
+ draftsman_options[:meta] ||= {}
64
+
65
+ attr_accessor :draftsman_event
66
+
67
+ class_attribute :published_at_attribute_name
68
+ self.published_at_attribute_name = options[:published_at] || :published_at
69
+
70
+ class_attribute :trashed_at_attribute_name
71
+ self.trashed_at_attribute_name = options[:trashed_at] || :trashed_at
72
+
73
+ # `belongs_to :draft` association
74
+ belongs_to self.draft_association_name, :class_name => self.draft_class_name, :dependent => :destroy
75
+
76
+ # Scopes
77
+ scope :drafted, lambda { where("#{self.draft_association_name}_id IS NOT NULL") }
78
+ scope :published, lambda { where("#{self.published_at_attribute_name} IS NOT NULL") }
79
+ scope :trashed, lambda { where("#{self.trashed_at_attribute_name} IS NOT NULL") }
80
+ scope :live, lambda { where("#{self.trashed_at_attribute_name} IS NULL") }
81
+ end
82
+
83
+ # Returns whether or not `has_drafts` has been called on this model.
84
+ def draftable?
85
+ method_defined?(:draftsman_options)
86
+ end
87
+
88
+ # Serializes attribute changes for `Draft#object_changes` attribute.
89
+ def serialize_draft_attribute_changes(changes)
90
+ serialized_attributes.each do |key, coder|
91
+ if changes.key?(key)
92
+ coder = Draftsman::Serializers::Yaml unless coder.respond_to?(:dump) # Fall back to YAML if `coder` has no `dump` method
93
+ old_value, new_value = changes[key]
94
+ changes[key] = [coder.dump(old_value), coder.dump(new_value)]
95
+ end
96
+ end
97
+ end
98
+
99
+ # Used for `Draft#object` attribute
100
+ def serialize_attributes_for_draftsman(attributes)
101
+ serialized_attributes.each do |key, coder|
102
+ if attributes.key?(key)
103
+ coder = Draftsman::Serializers::Yaml unless coder.respond_to?(:dump) # Fall back to YAML if `coder` has no `dump` method
104
+ attributes[key] = coder.dump(attributes[key])
105
+ end
106
+ end
107
+ end
108
+
109
+ # Returns whether or not a `trashed_at` timestamp is set up on this model.
110
+ def trashable?
111
+ draftable? && method_defined?(self.trashed_at_attribute_name)
112
+ end
113
+
114
+ # Unserializes attribute changes for `Draft#object_changes` attribute.
115
+ def unserialize_draft_attribute_changes(changes)
116
+ serialized_attributes.each do |key, coder|
117
+ if changes.key?(key)
118
+ coder = Draftsman::Serializers::Yaml unless coder.respond_to?(:dump)
119
+ old_value, new_value = changes[key]
120
+ changes[key] = [coder.load(old_value), coder.load(new_value)]
121
+ end
122
+ end
123
+ end
124
+
125
+ # Used for `Draft#object` attribute
126
+ def unserialize_attributes_for_draftsman(attributes)
127
+ serialized_attributes.each do |key, coder|
128
+ if attributes.key?(key)
129
+ coder = Draftsman::Serializers::Yaml unless coder.respond_to?(:dump)
130
+ attributes[key] = coder.load(attributes[key])
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ module InstanceMethods
137
+ # Returns whether or not this item has a draft.
138
+ def draft?
139
+ send(self.class.draft_association_name).present?
140
+ end
141
+
142
+ def draft_at(timestamp, reify_options = {})
143
+ v = send(self.class.versions_association_name).following(timestamp).first
144
+ v ? v.reify(reify_options) : self
145
+ end
146
+
147
+ # Creates object and records a draft for the object's creation. Returns `true` or `false` depending on whether or not
148
+ # the objects passed validation and the save was successful.
149
+ def draft_creation
150
+ transaction do
151
+ # We want to save the draft after create
152
+ return false unless self.save
153
+
154
+ data = {
155
+ :item => self,
156
+ :event => 'create',
157
+ :whodunnit => Draftsman.whodunnit,
158
+ :object => object_to_string_for_draft
159
+ }
160
+ data[:object_changes] = Draftsman.serializer.dump(changes_for_draftsman(previous_changes: true)) if track_object_changes_for_draft?
161
+ data = merge_metadata_for_draft(data)
162
+
163
+ send "build_#{self.class.draft_association_name}", data
164
+
165
+ if send(self.class.draft_association_name).save
166
+ write_attribute "#{self.class.draft_association_name}_id", send(self.class.draft_association_name).id
167
+ self.update_column "#{self.class.draft_association_name}_id", send(self.class.draft_association_name).id
168
+ return true
169
+ else
170
+ raise ActiveRecord::Rollback and return false
171
+ end
172
+ end
173
+ end
174
+
175
+ # Trashes object and records a draft for a `destroy` event.
176
+ def draft_destroy
177
+ transaction do
178
+ data = {
179
+ :item => self,
180
+ :event => 'destroy',
181
+ :whodunnit => Draftsman.whodunnit,
182
+ :object => object_to_string_for_draft
183
+ }
184
+
185
+ # Stash previous draft in case it needs to be reverted later
186
+ if self.draft?
187
+ data[:previous_draft] = Draftsman.serializer.dump(send(self.class.draft_association_name).attributes)
188
+ end
189
+
190
+ data = merge_metadata_for_draft(data)
191
+
192
+ if send(self.class.draft_association_name).present?
193
+ send(self.class.draft_association_name).update_attributes! data
194
+ else
195
+ send("build_#{self.class.draft_association_name}", data)
196
+ send(self.class.draft_association_name).save!
197
+ send "#{self.class.draft_association_name}_id=", send(self.class.draft_association_name).id
198
+ self.update_column "#{self.class.draft_association_name}_id", send(self.class.draft_association_name).id
199
+ end
200
+
201
+ trash!
202
+
203
+ # Mock `dependent: :destroy` behavior for all trashable associations
204
+ dependent_associations = self.class.reflect_on_all_associations(:has_one) + self.class.reflect_on_all_associations(:has_many)
205
+
206
+ dependent_associations.each do |association|
207
+
208
+ if association.klass.draftable? && association.options.has_key?(:dependent) && association.options[:dependent] == :destroy
209
+ dependents = association.macro == :has_one ? [self.send(association.name)] : self.send(association.name)
210
+
211
+ dependents.each do |dependent|
212
+ dependent.draft_destroy unless dependent.draft? && dependent.send(dependent.class.draft_association_name).destroy?
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
218
+
219
+ # Updates object and records a draft for an `update` event. If the draft is being updated to the object's original
220
+ # state, the draft is destroyed. Returns `true` or `false` depending on if the object passed validation and the save
221
+ # was successful.
222
+ def draft_update
223
+ transaction do
224
+ save_only_columns_for_draft
225
+
226
+ # We want to save the draft before update
227
+ return false unless self.valid?
228
+
229
+ # If updating a creation draft, also update this item
230
+ if self.draft? && send(self.class.draft_association_name).create?
231
+ data = {
232
+ :item => self,
233
+ :whodunnit => Draftsman.whodunnit,
234
+ :object => object_to_string_for_draft
235
+ }
236
+
237
+ if track_object_changes_for_draft?
238
+ data[:object_changes] = Draftsman.serializer.dump(changes_for_draftsman(changed_from: self.send(self.class.draft_association_name).changeset))
239
+ end
240
+
241
+ data = merge_metadata_for_draft(data)
242
+ send(self.class.draft_association_name).update_attributes data
243
+
244
+ self.save
245
+ # Destroy the draft if this record has changed back to the original record
246
+ elsif changed_to_original_for_draft?
247
+ send(self.class.draft_association_name).destroy
248
+ send "#{self.class.draft_association_name}_id=", nil
249
+ self.update_column "#{self.class.draft_association_name}_id", nil
250
+ true
251
+ # Save a draft if record is changed notably
252
+ elsif changed_notably_for_draft?
253
+ data = {
254
+ :item => self,
255
+ :whodunnit => Draftsman.whodunnit,
256
+ :object => object_to_string_for_draft
257
+ }
258
+ data = merge_metadata_for_draft(data)
259
+
260
+ # If there's already a draft, update it
261
+ if send(self.class.draft_association_name).present?
262
+ if track_object_changes_for_draft?
263
+ data[:object_changes] = Draftsman.serializer.dump(changes_for_draftsman)
264
+ end
265
+
266
+ send(self.class.draft_association_name).update_attributes data
267
+ # If there's not draft, create an update draft
268
+ else
269
+ data[:event] = 'update'
270
+ data[:object_changes] = Draftsman.serializer.dump(changes_for_draftsman) if track_object_changes_for_draft?
271
+ send "build_#{self.class.draft_association_name}", data
272
+
273
+ if send(self.class.draft_association_name).save
274
+ write_attribute "#{self.class.draft_association_name}_id", send(self.class.draft_association_name).id
275
+ self.update_column "#{self.class.draft_association_name}_id", send(self.class.draft_association_name).id
276
+ else
277
+ raise ActiveRecord::Rollback and return false
278
+ end
279
+ end
280
+ # If record is a draft and not changed notably, then update the draft
281
+ elsif self.draft?
282
+ data = {
283
+ :item => self,
284
+ :whodunnit => Draftsman.whodunnit,
285
+ :object => object_to_string_for_draft
286
+ }
287
+ data[:object_changes] = Draftsman.serializer.dump(changes_for_draftsman(changed_from: @object.draft.changeset)) if track_object_changes_for_draft?
288
+ data = merge_metadata_for_draft(data)
289
+
290
+ send(self.class.draft_association_name).update_attributes data
291
+ # Otherwise, just save the record
292
+ else
293
+ self.save
294
+ end
295
+ end
296
+ rescue Exception => e
297
+ false
298
+ end
299
+
300
+ # Returns serialized object representing this drafted item.
301
+ def object_to_string_for_draft(object = nil)
302
+ object ||= self
303
+
304
+ _attrs = object.attributes.except(*self.class.draftsman_options[:skip]).tap do |attributes|
305
+ self.class.serialize_attributes_for_draftsman attributes
306
+ end
307
+
308
+ Draftsman.serializer.dump(_attrs)
309
+ end
310
+
311
+ # Returns whether or not this item has been published at any point in its lifecycle.
312
+ def published?
313
+ self.published_at.present?
314
+ end
315
+
316
+ # Returns whether or not this item has been trashed
317
+ def trashed?
318
+ send(self.class.trashed_at_attribute_name).present?
319
+ end
320
+
321
+ private
322
+
323
+ # Returns changes on this object, excluding attributes defined in the options for `:ignore` and `:skip`.
324
+ def changed_and_not_ignored_for_draft(options = {})
325
+ options[:previous_changes] ||= false
326
+
327
+ my_changed = options[:previous_changes] ? previous_changes.keys : self.changed
328
+
329
+ ignore = self.class.draftsman_options[:ignore]
330
+ skip = self.class.draftsman_options[:skip]
331
+ my_changed - ignore - skip
332
+ end
333
+
334
+ # Returns whether or not this instance has changes that should trigger a new draft.
335
+ def changed_notably_for_draft?
336
+ notably_changed_attributes_for_draft.any?
337
+ end
338
+
339
+ # Returns whether or not the updates change this draft back to the original state
340
+ def changed_to_original_for_draft?
341
+ send(self.draft_association_name).present? && send(self.class.draft_association_name).update? && !changed_notably_for_draft?
342
+ end
343
+
344
+ # Returns array of attributes that have changed for the object.
345
+ def changes_for_draftsman(options = {})
346
+ options[:changed_from] ||= {}
347
+ options[:previous_changes] ||= false
348
+
349
+ my_changes = options[:previous_changes] ? self.previous_changes : self.changes
350
+
351
+ new_changes = my_changes.delete_if do |key, value|
352
+ !notably_changed_attributes_for_draft(previous_changes: options[:previous_changes]).include?(key)
353
+ end.tap do |changes|
354
+ self.class.serialize_draft_attribute_changes(changes) # Use serialized value for attributes when necessary
355
+ end
356
+
357
+ new_changes.each do |attribute, value|
358
+ new_changes[attribute][0] = options[:changed_from][attribute][0] if options[:changed_from].has_key?(attribute)
359
+ end
360
+
361
+ # We need to merge any previous changes so they are not lost on further updates before committing or
362
+ # reverting
363
+ options[:changed_from].merge new_changes
364
+ end
365
+
366
+ # Returns draft class.
367
+ def draft_class
368
+ self.draft_class_name.constantize
369
+ end
370
+
371
+ # Merges model-level metadata from `meta` and `controller_info` into draft object.
372
+ def merge_metadata_for_draft(data)
373
+ # First, we merge the model-level metadata in `meta`.
374
+ draftsman_options[:meta].each do |attribute, value|
375
+ data[attribute] =
376
+ if value.respond_to?(:call)
377
+ value.call(self)
378
+ elsif value.is_a?(Symbol) && respond_to?(value)
379
+ # if it is an attribute that is changing, be sure to grab the current version
380
+ if has_attribute?(value) && send("#{value}_changed?".to_sym)
381
+ send("#{value}_was".to_sym)
382
+ else
383
+ send(value)
384
+ end
385
+ else
386
+ value
387
+ end
388
+ end
389
+
390
+ # Second, we merge any extra data from the controller (if available).
391
+ data.merge(Draftsman.controller_info || {})
392
+ end
393
+
394
+ # Returns array of attributes that were changed to trigger a draft.
395
+ def notably_changed_attributes_for_draft(options = {})
396
+ options[:previous_changes] ||= false
397
+
398
+ only = self.class.draftsman_options[:only]
399
+ only.empty? ? changed_and_not_ignored_for_draft(previous_changes: options[:previous_changes]) : (changed_and_not_ignored_for_draft(previous_changes: options[:previous_changes]) & only)
400
+ end
401
+
402
+ # Save columns outside of the `only` option directly to master table
403
+ def save_only_columns_for_draft
404
+ if self.class.draftsman_options[:only].any?
405
+ only_changes = {}
406
+ only_changed_attributes = self.changed - self.class.draftsman_options[:only]
407
+
408
+ only_changed_attributes.each do |attribute|
409
+ only_changes[attribute] = self.changes[attribute].last
410
+ end
411
+
412
+ self.update_columns only_changes if only_changes.any?
413
+ end
414
+ end
415
+
416
+ # Returns whether or not the draft class includes an `object_changes` attribute.
417
+ def track_object_changes_for_draft?
418
+ draft_class.column_names.include? 'object_changes'
419
+ end
420
+
421
+ # Sets `trashed_at` attribute to now and saves to the database immediately.
422
+ def trash!
423
+ write_attribute self.class.trashed_at_attribute_name, Time.now
424
+ self.update_column self.class.trashed_at_attribute_name, send(self.class.trashed_at_attribute_name)
425
+ end
426
+ end
427
+ end
428
+ end
@@ -0,0 +1,17 @@
1
+ require 'active_support/json'
2
+
3
+ module Draftsman
4
+ module Serializers
5
+ module Json
6
+ extend self # makes all instance methods become module methods as well
7
+
8
+ def load(string)
9
+ ActiveSupport::JSON.decode string
10
+ end
11
+
12
+ def dump(object)
13
+ ActiveSupport::JSON.encode object
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ require 'yaml'
2
+
3
+ module Draftsman
4
+ module Serializers
5
+ module Yaml
6
+ extend self # makes all instance methods become module methods as well
7
+
8
+ def load(string)
9
+ YAML.load string
10
+ end
11
+
12
+ def dump(object)
13
+ YAML.dump object
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module Draftsman
2
+ VERSION = '0.1.0'
3
+ end
data/lib/draftsman.rb ADDED
@@ -0,0 +1,101 @@
1
+ require 'draftsman/config'
2
+ require 'draftsman/model'
3
+
4
+ # Require all frameworks and serializers
5
+ Dir[File.join(File.dirname(__FILE__), 'draftsman', 'frameworks', '*.rb')].each { |file| require file }
6
+ Dir[File.join(File.dirname(__FILE__), 'draftsman', 'serializers', '*.rb')].each { |file| require file }
7
+
8
+ # Draftsman's module methods can be called in both models and controllers.
9
+ module Draftsman
10
+ # Returns whether or not ActiveRecord is configured to require mass assignment whitelisting via `attr_accessible`.
11
+ def self.active_record_protected_attributes?
12
+ @active_record_protected_attributes ||= ActiveRecord::VERSION::STRING.to_f < 4.0 || defined?(ProtectedAttributes)
13
+ end
14
+
15
+ # Returns any information from the controller that you want Draftsman to store.
16
+ #
17
+ # See `Draftsman::Controller#info_for_draftsman`.
18
+ def self.controller_info
19
+ draftsman_store[:controller_info]
20
+ end
21
+
22
+ # Sets any information from the controller that you want Draftsman to store. By default, this is set automatically by
23
+ # a before filter.
24
+ def self.controller_info=(value)
25
+ draftsman_store[:controller_info] = value
26
+ end
27
+
28
+ # Returns default class name used for drafts.
29
+ def self.draft_class_name
30
+ draftsman_store[:draft_class_name]
31
+ end
32
+
33
+ # Sets default class name to use for drafts.
34
+ def self.draft_class_name=(class_name)
35
+ draftsman_store[:draft_class_name] = class_name
36
+ end
37
+
38
+ # Set the field which records when a draft was created.
39
+ def self.timestamp_field=(field_name)
40
+ Draftsman.config.timestamp_field = field_name
41
+ end
42
+
43
+ # Returns the field which records when a draft was created.
44
+ def self.timestamp_field
45
+ Draftsman.config.timestamp_field
46
+ end
47
+
48
+ # Returns serializer to use for `object`, `object_changes`, and `previous_draft` columns.
49
+ def self.serializer
50
+ Draftsman.config.serializer
51
+ end
52
+
53
+ # Sets serializer to use for `object`, `object_changes`, and `previous_draft` columns.
54
+ def self.serializer=(value)
55
+ Draftsman.config.serializer = value
56
+ end
57
+
58
+ # Returns who is reponsible for any changes that occur.
59
+ def self.whodunnit
60
+ draftsman_store[:whodunnit]
61
+ end
62
+
63
+ # Sets who is responsible for any changes that occur.
64
+ # You would normally use this in a migration or on the console,
65
+ # when working with models directly. In a controller, it is set
66
+ # automatically to the `current_user`.
67
+ def self.whodunnit=(value)
68
+ draftsman_store[:whodunnit] = value
69
+ end
70
+
71
+ private
72
+
73
+ # Thread-safe hash to hold Draftman's data. Initializing with needed default values.
74
+ def self.draftsman_store
75
+ Thread.current[:draft] ||= { :draft_class_name => 'Draftsman::Draft' }
76
+ end
77
+
78
+ # Returns Draftman's configuration object.
79
+ def self.config
80
+ @@config ||= Draftsman::Config.instance
81
+ end
82
+
83
+ def self.configure
84
+ yield config
85
+ end
86
+ end
87
+
88
+ # Draft model class.
89
+ require 'draftsman/draft'
90
+
91
+ # Inject `Draftsman::Model` into ActiveRecord classes.
92
+ ActiveSupport.on_load(:active_record) do
93
+ include Draftsman::Model
94
+ end
95
+
96
+ # Inject `Draftsman::Rails::Controller` into Rails controllers.
97
+ if defined?(ActionController)
98
+ ActiveSupport.on_load(:action_controller) do
99
+ include Draftsman::Rails::Controller
100
+ end
101
+ end
@@ -0,0 +1,27 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+ require 'rails/generators/active_record'
4
+
5
+ module Draftsman
6
+ class InstallGenerator < ::Rails::Generators::Base
7
+ include ::Rails::Generators::Migration
8
+
9
+ desc 'Generates (but does not run) a migration to add a drafts table.'
10
+ source_root File.expand_path('../templates', __FILE__)
11
+ class_option :skip_initializer, :type => :boolean, :default => false, :desc => 'Skip generation of the boilerplate initializer at `config/initializers/draftsman.rb`.'
12
+ class_option :with_changes, :type => :boolean, :default => false, :desc => 'Store changeset (diff) with each draft'
13
+
14
+ def create_migration_file
15
+ migration_template 'create_drafts.rb', 'db/migrate/create_drafts.rb'
16
+ migration_template 'add_object_changes_column_to_drafts.rb', 'db/migrate/add_object_changes_column_to_drafts.rb' if options.with_changes?
17
+ end
18
+
19
+ def self.next_migration_number(dirname)
20
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
21
+ end
22
+
23
+ def copy_config
24
+ template 'config/initializers/draftsman.rb' unless options.skip_initializer?
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,9 @@
1
+ class AddObjectChangesColumnToDrafts < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :drafts, :object_changes, :text
4
+ end
5
+
6
+ def self.down
7
+ remove_column :drafts, :object_changes
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ # Override global `draft` class. For example, perhaps you want your own class at `app/models/draft.rb` that adds
2
+ # extra attributes, validations, associations, etc. Be sure that this new model class extends `Draftsman::Draft`.
3
+ # Draftsman.draft_class_name = 'Draftsman::Draft'
4
+
5
+ # Serializer for `object`, `object_changes`, and `previous_draft` columns. To use the JSON serializer, change to
6
+ # `Draftsman::Serializers::Json`. You could implement your own serializer if you really wanted to. See files in
7
+ # `lib/draftsman/serializers`.
8
+ # Draftsman.serializer = Draftsman::Serializers::Json
9
+
10
+ # Field which records when a draft was created.
11
+ # Draftsman.timestamp_field = :created_at
@@ -0,0 +1,22 @@
1
+ class CreateDrafts < ActiveRecord::Migration
2
+ def change
3
+ create_table :drafts do |t|
4
+ t.string :item_type, :null => false
5
+ t.integer :item_id, :null => false
6
+ t.string :event, :null => false
7
+ t.string :whodunnit# :null => false
8
+ t.text :object
9
+ t.text :previous_draft
10
+ t.timestamps
11
+ end
12
+
13
+ change_table :drafts do |t|
14
+ t.index :item_type
15
+ t.index :item_id
16
+ t.index :event
17
+ t.index :whodunnit
18
+ t.index :created_at
19
+ t.index :updated_at
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ # Tests controller `info_for_draftsman` method
4
+ describe InformantsController do
5
+ let(:trashable) { Trashable.create!(:name => 'Bob') }
6
+
7
+ describe :create do
8
+ before { post :create }
9
+ subject { Draftsman::Draft.last }
10
+ its(:ip) { should eql '123.45.67.89' }
11
+ its(:user_agent) { should eql '007' }
12
+ end
13
+
14
+ describe :update do
15
+ before { put :update, :id => trashable.id }
16
+ subject { Draftsman::Draft.last }
17
+ its(:ip) { should eql '123.45.67.89' }
18
+ its(:user_agent) { should eql '007' }
19
+ end
20
+
21
+ describe :destroy do
22
+ before { delete :destroy, :id => trashable.id }
23
+ subject { Draftsman::Draft.last }
24
+ its(:ip) { should eql '123.45.67.89' }
25
+ its(:user_agent) { should eql '007' }
26
+ end
27
+ end