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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: eb576cc8ff96c4d3e43ffd8c08470200e5968c0f
4
+ data.tar.gz: 3759e4dd3b0e09007c0ce94b6f8cc11f9f49e503
5
+ SHA512:
6
+ metadata.gz: 68ae774e981b6f401b4230d0ee313158c679ebf2ebb6accde016c92e280d24bec28e24da0ac30901ec0f3ed74df53b08b91905175425dd25efdbe8d4e696b7d7
7
+ data.tar.gz: 8366dfbccdb59440839a2a184dffa4543cc126db74bce1f01efe96d20f960025ba4a38063d6e581104cc2d8df40a8dab4a76ae23491b63f45ad7b6eec7dc190a
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ *.gem
2
+ .bundle
3
+ pkg/*
4
+ .DS_Store
5
+ ._*
6
+ .idea/
7
+ .redcar/
8
+ spec/dummy/tmp/
9
+ spec/dummy/db/*.sqlite3
10
+ spec/dummy/log/*.log
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --tty
2
+ --colour
3
+ --format p
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ draftsman
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.0.0-p247
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # CHANGELOG
2
+
3
+ ## v0.1.0
4
+
5
+ * Initial release.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,97 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ draftsman (0.1.0)
5
+ activerecord (>= 3.0, < 5.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ actionpack (4.0.1)
11
+ activesupport (= 4.0.1)
12
+ builder (~> 3.1.0)
13
+ erubis (~> 2.7.0)
14
+ rack (~> 1.5.2)
15
+ rack-test (~> 0.6.2)
16
+ activemodel (4.0.1)
17
+ activesupport (= 4.0.1)
18
+ builder (~> 3.1.0)
19
+ activerecord (4.0.1)
20
+ activemodel (= 4.0.1)
21
+ activerecord-deprecated_finders (~> 1.0.2)
22
+ activesupport (= 4.0.1)
23
+ arel (~> 4.0.0)
24
+ activerecord-deprecated_finders (1.0.3)
25
+ activesupport (4.0.1)
26
+ i18n (~> 0.6, >= 0.6.4)
27
+ minitest (~> 4.2)
28
+ multi_json (~> 1.3)
29
+ thread_safe (~> 0.1)
30
+ tzinfo (~> 0.3.37)
31
+ arel (4.0.1)
32
+ atomic (1.1.14)
33
+ builder (3.1.4)
34
+ capybara (2.1.0)
35
+ mime-types (>= 1.16)
36
+ nokogiri (>= 1.3.3)
37
+ rack (>= 1.0.0)
38
+ rack-test (>= 0.5.4)
39
+ xpath (~> 2.0)
40
+ diff-lcs (1.2.5)
41
+ erubis (2.7.0)
42
+ i18n (0.6.5)
43
+ mime-types (2.0)
44
+ mini_portile (0.5.2)
45
+ minitest (4.7.5)
46
+ multi_json (1.8.2)
47
+ nokogiri (1.6.0)
48
+ mini_portile (~> 0.5.0)
49
+ rack (1.5.2)
50
+ rack-protection (1.5.1)
51
+ rack
52
+ rack-test (0.6.2)
53
+ rack (>= 1.0)
54
+ railties (4.0.1)
55
+ actionpack (= 4.0.1)
56
+ activesupport (= 4.0.1)
57
+ rake (>= 0.8.7)
58
+ thor (>= 0.18.1, < 2.0)
59
+ rake (10.1.0)
60
+ rspec-core (2.14.7)
61
+ rspec-expectations (2.14.4)
62
+ diff-lcs (>= 1.1.3, < 2.0)
63
+ rspec-mocks (2.14.4)
64
+ rspec-rails (2.14.0)
65
+ actionpack (>= 3.0)
66
+ activesupport (>= 3.0)
67
+ railties (>= 3.0)
68
+ rspec-core (~> 2.14.0)
69
+ rspec-expectations (~> 2.14.0)
70
+ rspec-mocks (~> 2.14.0)
71
+ shoulda-matchers (2.4.0)
72
+ activesupport (>= 3.0.0)
73
+ sinatra (1.4.4)
74
+ rack (~> 1.4)
75
+ rack-protection (~> 1.4)
76
+ tilt (~> 1.3, >= 1.3.4)
77
+ sqlite3 (1.3.8)
78
+ thor (0.18.1)
79
+ thread_safe (0.1.3)
80
+ atomic
81
+ tilt (1.4.1)
82
+ tzinfo (0.3.38)
83
+ xpath (2.0.0)
84
+ nokogiri (~> 1.3)
85
+
86
+ PLATFORMS
87
+ ruby
88
+
89
+ DEPENDENCIES
90
+ capybara
91
+ draftsman!
92
+ railties (>= 3.0, < 5.0)
93
+ rake
94
+ rspec-rails
95
+ shoulda-matchers
96
+ sinatra (~> 1.0)
97
+ sqlite3 (~> 1.2)
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 Minimal Orange, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,506 @@
1
+ # Draftsman v0.1.0 (alpha)
2
+
3
+ Draftsman is a Ruby gem that lets you create draft versions of your database records. If you're developing a system in
4
+ need of simple drafts or a publishing approval queue, then Draftsman just might be what you need.
5
+
6
+ **This gem is still considered experimental**, so proceed with caution.
7
+
8
+ * The largest risk at this time is functionality that assists with publishing or reverting dependencies through
9
+ associations (for example, "publishing" a child also publishes its parent if it's a new item). I'll be putting this
10
+ functionality through its paces in the coming months.
11
+ * The RSpec tests are lacking in some areas, so I will be adding to those over time as well. (Unfortunately, this gem
12
+ was not developed with TDD best practices because it was lifted from PaperTrail and modified from there.)
13
+
14
+ This gem is inspired by the [Kentouzu][1] gem, which is based heavily on [PaperTrail][2]. In fact, much of the code for
15
+ this gem was lifted line for line from PaperTrail (because it works beautifully). You should definitely check out
16
+ PaperTrail and its source: it's a nice clean example of a gem that hooks into Rails and Sinatra.
17
+
18
+ ## Features
19
+
20
+ - Provides API for storing drafts of creations, updates, and destroys.
21
+ - A max of one draft per record (via `belongs_to` association).
22
+ - Does not store drafts for updates that don't change anything.
23
+ - Allows you to specify attributes (by inclusion or exclusion) that must change for a draft to be stored.
24
+ - Ability to query drafts based on the current drafted item, or query all drafts polymorphically on the `drafts` table.
25
+ - `publish!` and `revert!` methods for drafts also handle any dependent drafts so you don't end up with orphaned
26
+ records.
27
+ - Allows you to get at every draft, even if the schema has since changed.
28
+ - Automatically records who was responsible via your controller. Draftsman calls `current_user` by default if it
29
+ exists, but you can have it call any method you like.
30
+ - Allows you to store arbitrary model-level metadata with each draft (useful for filtering).
31
+ - Allows you to store arbitrary controller-level information with each draft (e.g., remote IP, current account ID).
32
+ - Only saves drafts when you explicitly tell it to via instance methods like `draft_creation`, `draft_update`, and
33
+ `draft_destroy`.
34
+ - Stores everything in a single database table by default (generates migration for you), or you can use separate tables
35
+ for separate models.
36
+ - Supports custom draft classes so different models' drafts can have different behavior.
37
+ - Supports custom name for `draft` association.
38
+ - Threadsafe.
39
+
40
+ ## Compatibility
41
+
42
+ Compatible with ActiveRecord 3 and 4.
43
+
44
+ Works well with Rails, Sinatra, or any other application that depends on ActiveRecord.
45
+
46
+ ## Installation
47
+
48
+ ### Rails 3 & 4
49
+
50
+ Add Draftsman to your `Gemfile`.
51
+
52
+ ```ruby
53
+ gem 'draftsman', '0.1.0'
54
+ ```
55
+
56
+ Or if you want to grab the latest from `master`:
57
+
58
+ ```ruby
59
+ gem 'draftsman', :github => 'minimalorange/draftsman'
60
+ ```
61
+
62
+ Generate a migration which will add a `drafts` table to your database.
63
+
64
+ rails g draftsman:install
65
+
66
+ You can pass zero, one, or both of these options to the generator:
67
+
68
+ $ rails g draftsman:install --skip-initializer # Skip generation of the boilerplate initializer at
69
+ # `config/initializers/draftsman.rb`.
70
+
71
+ $ rails g draftsman:install --with-changes # Store changeset (diff) with each draft
72
+
73
+ Run the migration(s).
74
+
75
+ $ rake db:migrate
76
+
77
+ Add `draft_id`, `published_at`, and `trashed_at` attributes to the models you want to have drafts on. `trashed_at` is
78
+ optional if you don't want to store drafts for destroys.
79
+
80
+ $ rails g migration add_draft_id_published_at_trashed_at_to_widgets draft_id:integer published_at:timestamp trashed_at:timestamp
81
+ $ rake db:migrate
82
+
83
+ Add `has_drafts` to the models you want to have drafts on.
84
+
85
+ ### Sinatra
86
+
87
+ In order to configure Draftsman for usage with [Sinatra][5], your Sinatra app must be using `ActiveRecord` 3 or greater.
88
+ It is also recommended to use the [Sinatra ActiveRecord Extension][6] or something similar for managing your
89
+ application's ActiveRecord connection in a manner similar to the way Rails does. If using the aforementioned Sinatra
90
+ ActiveRecord Extension, steps for setting up your app with Draftsman will look something like this:
91
+
92
+ Add Draftsman to your `Gemfile`.
93
+
94
+ ```ruby
95
+ gem 'draftsman', :github => 'minimalorange/draftsman'
96
+ ```
97
+
98
+ Generate a migration to add a `drafts` table to your database.
99
+
100
+ $ rake db:create_migration NAME=create_drafts
101
+
102
+ Copy contents of [`create_drafts.rb`][7] into the `create_drafts` migration that was generated into your `db/migrate`
103
+ directory.
104
+
105
+ Run the migration(s).
106
+
107
+ $ rake db:migrate
108
+
109
+ Add `draft_id`, `published_at`, and `trashed_at` attributes to the models you want to have drafts on. (`trashed_at` is
110
+ optional if you don't want to store drafts for destroys.)
111
+
112
+ Add `has_drafts` to the models you want to have drafts on.
113
+
114
+ Draftsman provides a helper extension that acts similar to the controller mixin it provides for Rails applications.
115
+
116
+ It will set `Draftsman.whodunnit` to whatever is returned by a method named `user_for_paper_trail`, which you can define
117
+ inside your Sinatra Application. (By default, it attempts to invoke a method named `current_user`.)
118
+
119
+ If you're using the modular [`Sinatra::Base`][8] style of application, you will need to register the extension:
120
+
121
+ ```ruby
122
+ # my_app.rb
123
+ require 'sinatra/base'
124
+
125
+ class MyApp < Sinatra::Base
126
+ register Draftsman::Sinatra
127
+ end
128
+ ```
129
+
130
+ ## API Summary
131
+
132
+ ### `has_draft` Options
133
+
134
+ To get started, add a call to `has_drafts` to your model. `has_drafts` accepts the following options:
135
+
136
+ ##### `:class_name`
137
+
138
+ The name of a custom `Draft` class. This class should inherit from `Draftsman::Draft`. A global default can be
139
+ set for this using `Draftsman.draft_class_name=` if the default of `Draftsman::Draft` needs to be overridden.
140
+
141
+ ##### `:ignore`
142
+
143
+ An array of attributes for which an update to a `Draft` will not be stored if they are the only ones changed.
144
+
145
+ ##### `:only`
146
+ Inverse of `ignore` - a new `Draft` will be created only for these attributes if supplied. It's recommended that
147
+ you only specify optional attributes for this (that can be empty).
148
+
149
+ ##### `:skip`
150
+ Fields to ignore completely. As with `ignore`, updates to these fields will not create a new `Draft`. In
151
+ addition, these fields will not be included in the serialized versions of the object whenever a new `Draft` is
152
+ created.
153
+
154
+ ##### `:meta`
155
+ A hash of extra data to store. You must add a column to the `drafts` table for each key. Values are objects or
156
+ `proc`s (which are called with `self`, i.e. the model with the `has_drafts`). See
157
+ `Draftsman::Controller.info_for_draftsman` for an example of how to store data from the controller.
158
+
159
+ ##### `:draft`
160
+ The name to use for the `draft` association shortcut method. Default is `:draft`.
161
+
162
+ ##### `:published_at`
163
+ The name to use for the method which returns the published timestamp. Default is `published_at`.
164
+
165
+ ##### `:trashed_at`
166
+ The name to use for the method which returns the soft delete timestamp. Default is `trashed_at`.
167
+
168
+ ### Drafted Item Class Methods
169
+
170
+ When you install the Draftsman gem, you get these methods on each model class:
171
+
172
+ ```ruby
173
+ # Returns whether or not `has_draft` has been called on the model.
174
+ Widget.draftable?
175
+
176
+ # Returns whether or not a `trashed_at` timestamp is set up on this model.
177
+ Widget.trashable?
178
+ ```
179
+
180
+ ### Drafted Item Instance Methods
181
+
182
+ When you call `has_drafts` in your model, you get the following methods. See the "Basic Usage" section below for more
183
+ context on where these methods fit into your data's lifecycle.
184
+
185
+ ```ruby
186
+ # Returns this widget's draft. You can customize the name of this association.
187
+ widget.draft
188
+
189
+ # Returns whether or not this widget has a draft.
190
+ widget.draft?
191
+
192
+ # Creates object and records a draft for the object's creation. Returns `true` or `false` depending on whether or not
193
+ # the objects passed validation and the save was successful.
194
+ widget.draft_creation
195
+
196
+ # Updates object and records a draft for an `update` event. If the draft is being updated to the object's original
197
+ # state, the draft is destroyed. Returns `true` or `false` depending on if the object passed validation and the save
198
+ # was successful.
199
+ widget.draft_update
200
+
201
+ # Trashes object and records a draft for a `destroy` event. (The `trashed_at` attribute must be set up on your model for
202
+ # this to work.)
203
+ widget.draft_destroy
204
+
205
+ # Returns whether or not this item has been published at any point in its lifecycle.
206
+ widget.published?
207
+
208
+ # Sets `:published_at` attribute to now and saves to the database immediately.
209
+ widget.publish!
210
+
211
+ # Returns whether or not this item has been trashed via `draft_destroy`
212
+ widget.trashed?
213
+ ```
214
+
215
+ ### Drafted Item Scopes
216
+
217
+ You also get these scopes added to your model for your querying enjoyment:
218
+
219
+ ```ruby
220
+ Widget.drafted # Limits to items that have drafts. Best used in an "admin" area in your application.
221
+ Widget.published # Limits to items that have been published at some point in their lifecycles. Best used in a "public" area in your application.
222
+ Widget.trashed # Limits to items that have been drafted for deletion (but not fully committed for deletion). Best used in an "admin" area in your application.
223
+ Widget.live # Limits to items that have not been drafted for deletion. Best used in an "admin" area in your application.
224
+ ```
225
+
226
+ ### Draft Class Methods
227
+
228
+ The `Draftsman::Draft` class has the following methods:
229
+
230
+ ```ruby
231
+ # Returns all drafts created by the `create` event.
232
+ Draftsman::Draft.creates
233
+
234
+ # Returns all drafts created by the `update` event.
235
+ Draftsman::Draft.updates
236
+
237
+ # Returns all drafts created by the `destroy` event.
238
+ Draftsman::Draft.destroys
239
+ ```
240
+
241
+ ### Draft Instance Methods
242
+
243
+ And a `Draftsman::Draft` instance has these methods:
244
+
245
+ ```ruby
246
+ # Return the associated item in its state before the draft.
247
+ draft.item
248
+
249
+ # Return the object held by the draft.
250
+ draft.reify
251
+
252
+ # Returns what changed in this draft. Similar to `ActiveModel::Dirty#changes`.
253
+ # Returns `nil` if your `drafts` table does not have an `object_changes` text column.
254
+ draft.changeset
255
+
256
+ # Returns whether or not this is a `create` event.
257
+ draft.create?
258
+
259
+ # Returns whether or not this is an `update` event.
260
+ draft.update?
261
+
262
+ # Returns whether or not this is a `destroy` event.
263
+ draft.destroy?
264
+
265
+ # Publishes this draft's associated `item`, publishes its `item`'s dependencies, and destroys itself.
266
+ # - For `create` drafts, adds a value for the `published_at` timestamp on the item and destroys the draft.
267
+ # - For `update` drafts, applies the drafted changes to the item and destroys the draft.
268
+ # - For `destroy` drafts, destroys the item and the draft.
269
+ draft.publish!
270
+
271
+ # Reverts this draft's associated `item` to its previous state, reverts its `item`'s dependencies, and destroys itself.
272
+ # - For `create` drafts, destroys the draft and the item.
273
+ # - For `update` drafts, destroys the draft only.
274
+ # - For `destroy` drafts, destroys the draft and undoes the `trashed_at` timestamp on the item. If a draft was drafted
275
+ # for destroy, restores the draft.
276
+ draft.revert!
277
+
278
+ # Returns related draft dependencies that would be along for the ride for a `publish!` action.
279
+ draft.draft_publication_dependencies
280
+
281
+ # Returns related draft dependencies that would be along for the ride for a `revert!` action.
282
+ draft.draft_reversion_dependencies
283
+ ```
284
+
285
+ ## Basic Usage
286
+
287
+ A basic `widgets` admin controller in Rails that saves all of the user's actions as drafts would look something like
288
+ this. It also presents all data in its drafted form, if a draft exists.
289
+
290
+ ```ruby
291
+ class Admin::WidgetsController < Admin::BaseController
292
+ before_filter :find_widget, :only => [:show, :edit, :update, :destroy]
293
+ before_filter :reify_widget, :only => [:show, :edit]
294
+
295
+ def index
296
+ # The `live` scope gives us widgets that aren't in the trash.
297
+ # It's also strongly recommended that you eagerly-load the `draft` association via `includes` so you don't keep
298
+ # hitting your database for each draft.
299
+ @widgets = Widget.live.includes(:draft).order(:title)
300
+
301
+ # Load drafted versions of each widget
302
+ @widgets.map! { |widget| widget.draft.reify if widget.draft? }
303
+ end
304
+
305
+ def show
306
+ end
307
+
308
+ def new
309
+ @widget = Widget.new
310
+ end
311
+
312
+ def create
313
+ @widget = Widget.new(widget_params)
314
+
315
+ # Instead of calling `save`, you call `draft_creation` to save it as a draft
316
+ if @widget.draft_creation
317
+ flash[:success] = 'A draft of the new widget was saved successfully.'
318
+ redirect_to admin_widgets_path
319
+ else
320
+ flash[:error] = 'There was an error creating the widget. Please review the errors below and try again.'
321
+ render :new
322
+ end
323
+ end
324
+
325
+ def edit
326
+ end
327
+
328
+ def update
329
+ @widget.attributes = widget_params
330
+
331
+ # Instead of calling `update_attributes`, you call `draft_update` to save it as a draft
332
+ if @widget.draft_update
333
+ flash[:success] = 'A draft of the widget update was saved successfully.'
334
+ redirect_to admin_widgets_path
335
+ else
336
+ flash[:error] = 'There was an error updating the widget. Please review the errors below and try again.'
337
+ render :edit
338
+ end
339
+ end
340
+
341
+ def destroy
342
+ # Instead of calling `destroy`, you call `draft_destroy` to "trash" it as a draft
343
+ @widget.draft_destroy
344
+ flash[:success] = 'The widget was moved to the trash.'
345
+ redirect_to admin_widgets_path
346
+ end
347
+
348
+ private
349
+
350
+ # Finds non-trashed widget by `params[:id]`
351
+ def find_widget
352
+ @widget = Widget.live.find(params[:id])
353
+ end
354
+
355
+ # If the widget has a draft, load that version of it
356
+ def reify_widget
357
+ @widget = @widget.draft.reify if @widget.draft?
358
+ end
359
+
360
+ # Strong parameters in Rails 4+
361
+ def widget_params
362
+ params.require(:widget).permit(:title)
363
+ end
364
+ end
365
+ ```
366
+
367
+ And "public" controllers (let's say read-only for this simple example) would ignore drafts entirely via the `published`
368
+ scope. This also allows items to be "trashed" for admins but still accessible to the public until that deletion is
369
+ committed.
370
+
371
+ ```ruby
372
+ class WidgetsController < ApplicationController
373
+ def index
374
+ # The `published` scope gives us widgets that have been committed to be viewed by non-admin users.
375
+ @widgets = Widget.published.order(:title)
376
+ end
377
+
378
+ def show
379
+ @widget = Widget.published.find(params[:id])
380
+ end
381
+ end
382
+ ```
383
+
384
+ Obviously, you can use the scopes that Draftsman provides however you would like in any case.
385
+
386
+ Lastly, a `drafts` controller could be provided for admin users to see all drafts, no matter the type of record (thanks
387
+ to ActiveRecord's polymorphic associations). From there, they could choose to revert or publish any draft listed, or any
388
+ other workflow action that you would like for your application to provide for drafts.
389
+
390
+ ```ruby
391
+ class Admin::DraftsController < Admin::BaseController
392
+ before_filter :find_draft, :only => [:show, :update, :destroy]
393
+
394
+ def index
395
+ @drafts = Draftsman::Draft.includes(:item).order('updated_at DESC')
396
+ end
397
+
398
+ def show
399
+ end
400
+
401
+ # Post draft ID here to publish it
402
+ def update
403
+ # Call `draft_publication_dependencies` to check if any other drafted records should be published along with this
404
+ # `@draft`.
405
+ @dependencies = @draft.draft_publication_dependencies
406
+
407
+ # If you would like to warn the user about dependent drafts that would need to be published along with this one, you
408
+ # would implement an `app/views/drafts/update.html.erb` view template. In that view template, you could list the
409
+ # `@dependencies` and show a button posting back to this action with a name of `commit_publication`. (The button's
410
+ # being clicked indicates to your application that the user accepts that the dependencies should be published along
411
+ # with the `@draft`, thus avoiding orphaned records).
412
+ if @dependencies.empty? || params[:commit_publication]
413
+ @draft.publish!
414
+ flash[:success] = 'The draft was published successfully.'
415
+ redirect_to admin_drafts_path
416
+ else
417
+ # Renders `app/views/drafts/update.html.erb`
418
+ end
419
+ end
420
+
421
+ # Post draft ID here to revert it
422
+ def destroy
423
+ # Call `draft_reversion_dependencies` to check if any other drafted records should be reverted along with this
424
+ # `@draft`.
425
+ @dependencies = @draft.draft_reversion_dependencies
426
+
427
+ # If you would like to warn the user about dependent drafts that would need to be reverted along with this one, you
428
+ # would implement an `app/views/drafts/destroy.html.erb` view template. In that view template, you could list the
429
+ # `@dependencies` and show a button posting back to this action with a name of `commit_reversion`. (The button's
430
+ # being clicked indicates to your application that the user accepts that the dependencies should be reverted along
431
+ # with the `@draft`, thus avoiding orphaned records).
432
+ if @dependencies.empty? || params[:commit_reversion]
433
+ @draft.revert!
434
+ flash[:success] = 'The draft was reverted successfully.'
435
+ redirect_to admin_drafts_path
436
+ else
437
+ # Renders `app/views/drafts/destroy.html.erb`
438
+ end
439
+ end
440
+
441
+ private
442
+
443
+ # Finds draft by `params[:id]`.
444
+ def find_draft
445
+ @draft = Draftsman::Draft.find(params[:id])
446
+ end
447
+ end
448
+
449
+ ```
450
+
451
+ ## Differences from PaperTrail
452
+
453
+ If you are familiar with the PaperTrail gem, some parts of the Draftsman gem will look very familiar.
454
+
455
+ However, there are some differences:
456
+
457
+ * PaperTrail hooks into ActiveRecord callbacks so that versions can be saved automatically with your normal CRUD
458
+ operations (`save`, `create`, `update_attributes`, `destroy`, etc.). Draftsman requires that you explicitly call its
459
+ own CRUD methods in order to save a draft (`draft_creation`, `draft_update`, and `draft_destroy`).
460
+
461
+ * PaperTrail's `Version#object` column looks "backwards" and records the object's state _before_ the changes occurred.
462
+ Because drafts record changes as they will look in the future, they must work differently. Draftsman's `Draft#object`
463
+ records the object's state _after_ changes are applied to the master object. *But* `destroy` drafts record the object
464
+ as it was _before_ it was destroyed (in case you want the option of reverting the destroy later and restoring the
465
+ drafted item back to its original state).
466
+
467
+ ## Contributing
468
+
469
+ If you feel like you can add something useful to Draftsman, then don't hesitate to contribute! To make sure your
470
+ fix/feature has a high chance of being included, please do the following:
471
+
472
+ 1. Fork the repo.
473
+
474
+ 2. Run `bundle install`.
475
+
476
+ 3. `cd spec/dummy` and run `RAILS_ENV=test rake db:migrate` to apply test database migrations.
477
+
478
+ 4. Add at least one test for your change. Only refactoring and documentation changes require no new tests. If you are
479
+ adding functionality or fixing a bug, you need a test!
480
+
481
+ 5. Make all tests pass by running `rspec spec`.
482
+
483
+ 6. Push to your fork and submit a pull request.
484
+
485
+ I can't guarantee that I will accept the change, but if I don't, I will be sure to let you know why.
486
+
487
+ Here are some things that will increase the chance that your pull request is accepted, taken straight from the Ruby on
488
+ Rails guide:
489
+
490
+ * Use Rails idioms
491
+ * Because this gem is currently designed to run with Rails 3, use Ruby 1.8-supported syntax (e,g.,
492
+ `item.where(:foo => :bar)`, instead of the newer Ruby 1.9-style `item.where(foo: :bar)`)
493
+ * Include tests that fail without your code, and pass with it
494
+ * Update the documentation, guides, or whatever is affected by your contribution
495
+
496
+ This gem is a work in progress. I am adding specs as I need features in my application. Please add missing ones as you
497
+ work on features or find bugs!
498
+
499
+
500
+ [1]: https://github.com/seaneshbaugh/kentouzu
501
+ [2]: https://github.com/airblade/paper_trail
502
+ [4]: http://railscasts.com/episodes/416-form-objects
503
+ [5]: http://www.sinatrarb.com/
504
+ [6]: https://github.com/janko-m/sinatra-activerecord
505
+ [7]: https://raw.github.com/minimalorange/draftsman/master/lib/generators/draftsman/templates/create_drafts.rb
506
+ [8]: http://www.sinatrarb.com/intro.html#Modular%20vs.%20Classic%20Style
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec