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
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