draftsman 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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