draftsman 0.5.1 → 0.6.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.
- checksums.yaml +4 -4
- data/.travis.yml +12 -0
- data/CHANGELOG.md +34 -0
- data/README.md +244 -181
- data/lib/draftsman/config.rb +4 -1
- data/lib/draftsman/draft.rb +117 -80
- data/lib/draftsman/frameworks/rspec.rb +1 -1
- data/lib/draftsman/model.rb +253 -235
- data/lib/draftsman/version.rb +1 -1
- data/lib/draftsman.rb +26 -5
- data/lib/generators/draftsman/templates/config/initializers/draftsman.rb +16 -7
- data/spec/draftsman_spec.rb +7 -9
- data/spec/dummy/app/controllers/application_controller.rb +2 -2
- data/spec/dummy/app/models/overridden_draft.rb +7 -0
- data/spec/dummy/app/models/talkative.rb +21 -43
- data/spec/dummy/db/schema.rb +1 -0
- data/spec/models/child_spec.rb +17 -17
- data/spec/models/draft_spec.rb +893 -305
- data/spec/models/enumable_spec.rb +3 -2
- data/spec/models/overridden_draft_spec.rb +41 -0
- data/spec/models/parent_spec.rb +10 -10
- data/spec/models/skipper_spec.rb +221 -219
- data/spec/models/talkative_spec.rb +107 -108
- data/spec/models/trashable_spec.rb +6 -10
- data/spec/models/vanilla_spec.rb +570 -229
- data/spec/models/whitelister_spec.rb +489 -348
- metadata +7 -10
- data/spec/dummy/db/migrate/20110208155312_set_up_test_tables.rb +0 -95
- data/spec/dummy/db/migrate/20150404203627_add_talkatives_table_to_tests.rb +0 -18
- data/spec/dummy/db/migrate/20150408234937_add_only_children.rb +0 -16
- data/spec/dummy/db/migrate/20160328184419_create_enumables.rb +0 -9
data/README.md
CHANGED
@@ -1,64 +1,76 @@
|
|
1
|
-
# Draftsman v0.
|
1
|
+
# Draftsman v0.6.0
|
2
2
|
|
3
|
-
|
4
|
-
need of simple drafts or a publishing approval queue, then Draftsman just might be what you need.
|
3
|
+
[](http://travis-ci.org/liveeditor/draftsman)
|
5
4
|
|
6
|
-
|
5
|
+
Draftsman is a Ruby gem that lets you create draft versions of your database
|
6
|
+
records. If you're developing a system in need of simple drafts or a publishing
|
7
|
+
approval queue, then Draftsman just might be what you need.
|
7
8
|
|
8
|
-
|
9
|
-
associations (for example, "publishing" a
|
9
|
+
- The largest risk at this time is functionality that assists with publishing
|
10
|
+
or reverting dependencies through associations (for example, "publishing" a
|
11
|
+
child also publishes its parent if it's a new item). We'll be putting this
|
10
12
|
functionality through its paces in the coming months.
|
11
|
-
|
12
|
-
|
13
|
+
- The RSpec tests are lacking in some areas, so I will be adding to those over
|
14
|
+
time as well. (Unfortunately, this gem was not developed with TDD best
|
15
|
+
practices because it was lifted from PaperTrail and modified from there.)
|
13
16
|
|
14
|
-
This gem is inspired by the [Kentouzu][1] gem, which is based heavily on
|
15
|
-
|
16
|
-
|
17
|
+
This gem is inspired by the [Kentouzu][1] gem, which is based heavily on
|
18
|
+
[PaperTrail][2]. In fact, much of the structure for this gem emulates PaperTrail
|
19
|
+
(because it works beautifully). You should definitely check out PaperTrail and
|
20
|
+
its source: it's a nice clean example of a gem that hooks into Rails and
|
21
|
+
Sinatra.
|
17
22
|
|
18
23
|
## Features
|
19
24
|
|
20
25
|
- Provides API for storing drafts of creations, updates, and destroys.
|
21
26
|
- A max of one draft per record (via `belongs_to` association).
|
22
27
|
- Does not store drafts for updates that don't change anything.
|
23
|
-
- Allows you to specify attributes (by inclusion or exclusion) that must change
|
24
|
-
|
25
|
-
-
|
26
|
-
|
28
|
+
- Allows you to specify attributes (by inclusion or exclusion) that must change
|
29
|
+
for a draft to be stored.
|
30
|
+
- Ability to query drafts based on the current drafted item, or query all
|
31
|
+
drafts polymorphically on the `drafts` table.
|
32
|
+
- `publish!` and `revert!` methods for drafts also handle any dependent drafts
|
33
|
+
so you don't end up with orphaned records.
|
27
34
|
- Allows you to get at every draft, even if the schema has since changed.
|
28
|
-
- Automatically records who was responsible via your controller. Draftsman
|
29
|
-
exists, but you can have it call any
|
30
|
-
|
31
|
-
- Allows you to store arbitrary
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
35
|
+
- Automatically records who was responsible via your controller. Draftsman
|
36
|
+
calls `current_user` by default if it exists, but you can have it call any
|
37
|
+
method you like.
|
38
|
+
- Allows you to store arbitrary model-level metadata with each draft (useful
|
39
|
+
for filtering).
|
40
|
+
- Allows you to store arbitrary controller-level information with each draft
|
41
|
+
(e.g., remote IP, current account ID).
|
42
|
+
- Only saves drafts when you explicitly tell it to via instance methods like
|
43
|
+
`save_draft` and `draft_destruction`.
|
44
|
+
- Stores everything in a single database table by default (generates migration
|
45
|
+
for you), or you can use separate tables for separate models.
|
46
|
+
- Supports custom draft classes so different models' drafts can have different
|
47
|
+
behavior.
|
37
48
|
- Supports custom name for `draft` association.
|
38
|
-
- Supports `before`, `after`, and `around` callbacks on each draft persistence
|
39
|
-
or `
|
49
|
+
- Supports `before`, `after`, and `around` callbacks on each draft persistence
|
50
|
+
method, such as `before_save_draft` or `around_draft_destruction`.
|
40
51
|
- Threadsafe.
|
41
52
|
|
42
53
|
## Compatibility
|
43
54
|
|
44
|
-
Compatible with ActiveRecord
|
55
|
+
Compatible with ActiveRecord 4 and 5.
|
45
56
|
|
46
|
-
Works well with Rails, Sinatra, or any other application that depends on
|
57
|
+
Works well with Rails, Sinatra, or any other application that depends on
|
58
|
+
ActiveRecord.
|
47
59
|
|
48
60
|
## Installation
|
49
61
|
|
50
|
-
### Rails
|
62
|
+
### Rails 4 and 5
|
51
63
|
|
52
64
|
Add Draftsman to your `Gemfile`.
|
53
65
|
|
54
66
|
```ruby
|
55
|
-
gem 'draftsman', '~> 0.
|
67
|
+
gem 'draftsman', '~> 0.6.0'
|
56
68
|
```
|
57
69
|
|
58
70
|
Or if you want to grab the latest from `master`:
|
59
71
|
|
60
72
|
```ruby
|
61
|
-
gem 'draftsman', :
|
73
|
+
gem 'draftsman', github: 'liveeditor/draftsman'
|
62
74
|
```
|
63
75
|
|
64
76
|
Generate a migration which will add a `drafts` table to your database.
|
@@ -78,16 +90,17 @@ Run the migration(s).
|
|
78
90
|
|
79
91
|
$ rake db:migrate
|
80
92
|
|
81
|
-
Add `draft_id`, `published_at`, and `trashed_at` attributes to the models you
|
82
|
-
optional if you don't want to store
|
93
|
+
Add `draft_id`, `published_at`, and `trashed_at` attributes to the models you
|
94
|
+
want to have drafts on. `trashed_at` is optional if you don't want to store
|
95
|
+
drafts for destroys.
|
83
96
|
|
84
97
|
$ rails g migration add_drafts_to_widgets draft_id:integer published_at:timestamp trashed_at:timestamp
|
85
98
|
$ rake db:migrate
|
86
99
|
|
87
100
|
Add `has_drafts` to the models you want to have drafts on.
|
88
101
|
|
89
|
-
Lastly, if your controllers have a `current_user` method, you can easily track
|
90
|
-
adding a controller filter.
|
102
|
+
Lastly, if your controllers have a `current_user` method, you can easily track
|
103
|
+
who is responsible for changes by adding a controller filter.
|
91
104
|
|
92
105
|
```ruby
|
93
106
|
class ApplicationController
|
@@ -97,39 +110,45 @@ end
|
|
97
110
|
|
98
111
|
### Sinatra
|
99
112
|
|
100
|
-
In order to configure Draftsman for usage with [Sinatra][5], your Sinatra app
|
101
|
-
It is also recommended to use the
|
102
|
-
|
103
|
-
ActiveRecord
|
113
|
+
In order to configure Draftsman for usage with [Sinatra][5], your Sinatra app
|
114
|
+
must be using `ActiveRecord` 4 or greater. It is also recommended to use the
|
115
|
+
[Sinatra ActiveRecord Extension][6] or something similar for managing your
|
116
|
+
application's ActiveRecord connection in a manner similar to the way Rails does.
|
117
|
+
If using the aforementioned Sinatra ActiveRecord Extension, steps for setting up
|
118
|
+
your app with Draftsman will look something like this:
|
104
119
|
|
105
120
|
Add Draftsman to your `Gemfile`.
|
106
121
|
|
107
122
|
```ruby
|
108
|
-
gem 'draftsman', :
|
123
|
+
gem 'draftsman', github: 'liveeditor/draftsman'
|
109
124
|
```
|
110
125
|
|
111
126
|
Generate a migration to add a `drafts` table to your database.
|
112
127
|
|
113
128
|
$ rake db:create_migration NAME=create_drafts
|
114
129
|
|
115
|
-
Copy contents of [`create_drafts.rb`][7] into the `create_drafts` migration that
|
116
|
-
directory.
|
130
|
+
Copy contents of [`create_drafts.rb`][7] into the `create_drafts` migration that
|
131
|
+
was generated into your `db/migrate` directory.
|
117
132
|
|
118
133
|
Run the migration(s).
|
119
134
|
|
120
135
|
$ rake db:migrate
|
121
136
|
|
122
|
-
Add `draft_id`, `published_at`, and `trashed_at` attributes to the models you
|
123
|
-
optional if you don't want to store
|
137
|
+
Add `draft_id`, `published_at`, and `trashed_at` attributes to the models you
|
138
|
+
want to have drafts on. (`trashed_at` is optional if you don't want to store
|
139
|
+
drafts for destroys.)
|
124
140
|
|
125
141
|
Add `has_drafts` to the models you want to have drafts on.
|
126
142
|
|
127
|
-
Draftsman provides a helper extension that acts
|
143
|
+
Draftsman provides a helper extension that acts similarly to the controller
|
144
|
+
mixin it provides for Rails applications.
|
128
145
|
|
129
|
-
It will set `Draftsman
|
130
|
-
|
146
|
+
It will set `Draftsman::Draft#whodunnit` to whatever is returned by a method
|
147
|
+
named `user_for_paper_trail`, which you can define inside your Sinatra
|
148
|
+
application. (By default, it attempts to invoke a method named `current_user`.)
|
131
149
|
|
132
|
-
If you're using the modular [`Sinatra::Base`][8] style of application, you will
|
150
|
+
If you're using the modular [`Sinatra::Base`][8] style of application, you will
|
151
|
+
need to register the extension:
|
133
152
|
|
134
153
|
```ruby
|
135
154
|
# my_app.rb
|
@@ -144,39 +163,54 @@ end
|
|
144
163
|
|
145
164
|
### `has_draft` Options
|
146
165
|
|
147
|
-
To get started, add a call to `has_drafts` to your model. `has_drafts` accepts
|
166
|
+
To get started, add a call to `has_drafts` to your model. `has_drafts` accepts
|
167
|
+
the following options:
|
148
168
|
|
149
169
|
##### `:class_name`
|
150
170
|
|
151
|
-
The name of a custom `Draft` class. This class should inherit from
|
152
|
-
|
171
|
+
The name of a custom `Draft` class. This class should inherit from
|
172
|
+
`Draftsman::Draft`. A global default can be set for this using
|
173
|
+
`Draftsman.draft_class_name=` if the default of `Draftsman::Draft` needs to be
|
174
|
+
overridden.
|
153
175
|
|
154
176
|
##### `:ignore`
|
155
177
|
|
156
|
-
An array of attributes for which an update to a `Draft` will not be stored if
|
178
|
+
An array of attributes for which an update to a `Draft` will not be stored if
|
179
|
+
they are the only ones changed.
|
157
180
|
|
158
181
|
##### `:only`
|
159
|
-
|
160
|
-
|
182
|
+
|
183
|
+
Inverse of `ignore` - a new `Draft` will be created only for these attributes if
|
184
|
+
supplied. It's recommended that you only specify optional attributes for this
|
185
|
+
(that can be empty).
|
161
186
|
|
162
187
|
##### `:skip`
|
163
|
-
|
164
|
-
|
165
|
-
|
188
|
+
|
189
|
+
Fields to ignore completely. As with `ignore`, updates to these fields will not
|
190
|
+
create a new `Draft`. In addition, these fields will not be included in the
|
191
|
+
serialized versions of the object whenever a new `Draft` is created.
|
166
192
|
|
167
193
|
##### `:meta`
|
168
|
-
|
169
|
-
|
170
|
-
|
194
|
+
|
195
|
+
A hash of extra data to store. You must add a column to the `drafts` table for
|
196
|
+
each key. Values are objects or `proc`s (which are called with `self`, i.e. the
|
197
|
+
model with the `has_drafts`). See `Draftsman::Controller.info_for_draftsman` for
|
198
|
+
an example of how to store data from the controller.
|
171
199
|
|
172
200
|
##### `:draft`
|
173
|
-
|
201
|
+
|
202
|
+
The name to use for the `draft` association shortcut method. Default is
|
203
|
+
`:draft`.
|
174
204
|
|
175
205
|
##### `:published_at`
|
176
|
-
|
206
|
+
|
207
|
+
The name to use for the method which returns the published timestamp. Default is
|
208
|
+
`published_at`.
|
177
209
|
|
178
210
|
##### `:trashed_at`
|
179
|
-
|
211
|
+
|
212
|
+
The name to use for the method which returns the soft delete timestamp. Default
|
213
|
+
is `trashed_at`.
|
180
214
|
|
181
215
|
### Drafted Item Class Methods
|
182
216
|
|
@@ -192,8 +226,9 @@ Widget.trashable?
|
|
192
226
|
|
193
227
|
### Drafted Item Instance Methods
|
194
228
|
|
195
|
-
When you call `has_drafts` in your model, you get the following methods. See the
|
196
|
-
context on where these methods fit into
|
229
|
+
When you call `has_drafts` in your model, you get the following methods. See the
|
230
|
+
"Basic Usage" section below for more context on where these methods fit into
|
231
|
+
your data's lifecycle.
|
197
232
|
|
198
233
|
```ruby
|
199
234
|
# Returns this widget's draft. You can customize the name of this association.
|
@@ -202,23 +237,20 @@ widget.draft
|
|
202
237
|
# Returns whether or not this widget has a draft.
|
203
238
|
widget.draft?
|
204
239
|
|
205
|
-
#
|
206
|
-
#
|
207
|
-
|
208
|
-
|
209
|
-
# Updates object and records a draft for an `update` event. If the draft is being updated to the object's original
|
210
|
-
# state, the draft is destroyed. Returns `true` or `false` depending on if the object passed validation and the save
|
211
|
-
# was successful.
|
212
|
-
widget.draft_update
|
240
|
+
# Saves record and records a draft for the object's creation or update. Much
|
241
|
+
# like `ActiveRecord`'s `#save`, returns `true` or `false` depending on whether
|
242
|
+
# or not the objects passed validation and the save was successful.
|
243
|
+
widget.save_draft
|
213
244
|
|
214
|
-
# Trashes object and records a draft for a `destroy` event. (The `trashed_at`
|
215
|
-
# this to work.)
|
245
|
+
# Trashes object and records a draft for a `destroy` event. (The `trashed_at`
|
246
|
+
# attribute must be set up on your model for this to work.)
|
216
247
|
widget.draft_destruction
|
217
248
|
|
218
|
-
# Returns whether or not this item has been published at any point in its
|
249
|
+
# Returns whether or not this item has been published at any point in its
|
250
|
+
# lifecycle.
|
219
251
|
widget.published?
|
220
252
|
|
221
|
-
# Returns whether or not this item has been trashed via
|
253
|
+
# Returns whether or not this item has been trashed via `#draft_destruction`.
|
222
254
|
widget.trashed?
|
223
255
|
```
|
224
256
|
|
@@ -233,17 +265,18 @@ Widget.trashed # Limits to items that have been drafted for deletion (but not
|
|
233
265
|
Widget.live # Limits to items that have not been drafted for deletion. Best used in an "admin" area in your application.
|
234
266
|
```
|
235
267
|
|
236
|
-
These scopes optionally take a `referenced_table_name` argument for constructing
|
237
|
-
eager loading or
|
268
|
+
These scopes optionally take a `referenced_table_name` argument for constructing
|
269
|
+
more advanced queries using `.includes` eager loading or `.joins`. This reduces
|
270
|
+
ambiguity both for SQL queries and for your Ruby code.
|
238
271
|
|
239
272
|
```ruby
|
240
|
-
# Query live widgets and gears without ambiguity.
|
273
|
+
# Query live `widgets` and `gears` without ambiguity.
|
241
274
|
Widget.live.includes(:gears, :sprockets).live(:gears)
|
242
275
|
```
|
243
276
|
|
244
277
|
### Draft Class Methods
|
245
278
|
|
246
|
-
The `Draftsman::Draft` class has the following
|
279
|
+
The `Draftsman::Draft` class has the following scopes:
|
247
280
|
|
248
281
|
```ruby
|
249
282
|
# Returns all drafts created by the `create` event.
|
@@ -264,11 +297,12 @@ And a `Draftsman::Draft` instance has these methods:
|
|
264
297
|
# Return the associated item in its state before the draft.
|
265
298
|
draft.item
|
266
299
|
|
267
|
-
# Return the object held by the draft.
|
300
|
+
# Return the object in its state held by the draft.
|
268
301
|
draft.reify
|
269
302
|
|
270
303
|
# Returns what changed in this draft. Similar to `ActiveModel::Dirty#changes`.
|
271
|
-
# Returns `nil` if your `drafts` table does not have an `object_changes` text
|
304
|
+
# Returns `nil` if your `drafts` table does not have an `object_changes` text
|
305
|
+
# column.
|
272
306
|
draft.changeset
|
273
307
|
|
274
308
|
# Returns whether or not this is a `create` event.
|
@@ -280,68 +314,73 @@ draft.update?
|
|
280
314
|
# Returns whether or not this is a `destroy` event.
|
281
315
|
draft.destroy?
|
282
316
|
|
283
|
-
# Publishes this draft's associated `item`, publishes its `item`'s dependencies,
|
284
|
-
#
|
285
|
-
# - For `
|
317
|
+
# Publishes this draft's associated `item`, publishes its `item`'s dependencies,
|
318
|
+
# and destroys itself.
|
319
|
+
# - For `create` drafts, adds a value for the `published_at` timestamp on the
|
320
|
+
# item and destroys the draft.
|
321
|
+
# - For `update` drafts, applies the drafted changes to the item and destroys
|
322
|
+
# the draft.
|
286
323
|
# - For `destroy` drafts, destroys the item and the draft.
|
287
324
|
draft.publish!
|
288
325
|
|
289
|
-
# Reverts this draft's associated `item` to its previous state, reverts its
|
326
|
+
# Reverts this draft's associated `item` to its previous state, reverts its
|
327
|
+
# `item`'s dependencies, and destroys itself.
|
290
328
|
# - For `create` drafts, destroys the draft and the item.
|
291
329
|
# - For `update` drafts, destroys the draft only.
|
292
|
-
# - For `destroy` drafts, destroys the draft and undoes the `trashed_at`
|
293
|
-
# for destroy, restores the
|
330
|
+
# - For `destroy` drafts, destroys the draft and undoes the `trashed_at`
|
331
|
+
# timestamp on the item. If a draft was drafted for destroy, restores the
|
332
|
+
# draft.
|
294
333
|
draft.revert!
|
295
334
|
|
296
|
-
# Returns related draft dependencies that would be along for the ride for a
|
335
|
+
# Returns related draft dependencies that would be along for the ride for a
|
336
|
+
# `publish!` action.
|
297
337
|
draft.draft_publication_dependencies
|
298
338
|
|
299
|
-
# Returns related draft dependencies that would be along for the ride for a
|
339
|
+
# Returns related draft dependencies that would be along for the ride for a
|
340
|
+
# `revert!` action.
|
300
341
|
draft.draft_reversion_dependencies
|
301
342
|
```
|
302
343
|
|
303
344
|
### Callbacks
|
304
345
|
|
305
|
-
Draftsman supports callbacks for draft
|
306
|
-
that `has_drafts`.
|
346
|
+
Draftsman supports callbacks for draft saves and destroys. These callbacks can
|
347
|
+
be defined in any model that `has_drafts`.
|
307
348
|
|
308
|
-
Draft callbacks work similarly to ActiveRecord callbacks; pass any functions
|
309
|
-
before/around/after a draft persistence method.
|
349
|
+
Draft callbacks work similarly to ActiveRecord callbacks; pass any functions
|
350
|
+
that you would like called before/around/after a draft persistence method.
|
310
351
|
|
311
352
|
Available callbacks:
|
312
353
|
```ruby
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
before_draft_update # called before draft is updated
|
318
|
-
around_draft_update # called function must yield to `draft_update`
|
319
|
-
after_draft_update # called after draft is updated
|
354
|
+
before_save_draft # called before draft is saved
|
355
|
+
around_save_draft # called function must yield to `save_draft`
|
356
|
+
after_draft_save # called after draft is saved
|
320
357
|
|
321
|
-
before_draft_destruction # called before
|
358
|
+
before_draft_destruction # called before item is destroyed as a draft
|
322
359
|
around_draft_destruction # called function must yield to `draft_destruction`
|
323
|
-
after_draft_destruction # called after
|
360
|
+
after_draft_destruction # called after item is destroyed as a draft
|
324
361
|
```
|
325
362
|
|
326
363
|
Note that callbacks must be defined after your call to `has_drafts`.
|
327
364
|
|
328
365
|
## Basic Usage
|
329
366
|
|
330
|
-
A basic `widgets` admin controller in Rails that saves all of the user's actions
|
331
|
-
this. It also presents all data in its
|
367
|
+
A basic `widgets` admin controller in Rails that saves all of the user's actions
|
368
|
+
as drafts would look something like this. It also presents all data in its
|
369
|
+
drafted form, if a draft exists.
|
332
370
|
|
333
371
|
```ruby
|
334
372
|
class Admin::WidgetsController < Admin::BaseController
|
335
|
-
before_action :find_widget, :
|
336
|
-
before_action :reify_widget, :
|
373
|
+
before_action :find_widget, only: [:show, :edit, :update, :destroy]
|
374
|
+
before_action :reify_widget, only: [:show, :edit]
|
337
375
|
|
338
376
|
def index
|
339
377
|
# The `live` scope gives us widgets that aren't in the trash.
|
340
|
-
# It's also strongly recommended that you eagerly-load the `draft`
|
341
|
-
# hitting your database for
|
378
|
+
# It's also strongly recommended that you eagerly-load the `draft`
|
379
|
+
# association via `includes` so you don't keep hitting your database for
|
380
|
+
# each draft.
|
342
381
|
@widgets = Widget.live.includes(:draft).order(:title)
|
343
382
|
|
344
|
-
# Load drafted versions of each widget
|
383
|
+
# Load drafted versions of each widget.
|
345
384
|
@widgets.map! { |widget| widget.draft.reify if widget.draft? }
|
346
385
|
end
|
347
386
|
|
@@ -355,12 +394,12 @@ class Admin::WidgetsController < Admin::BaseController
|
|
355
394
|
def create
|
356
395
|
@widget = Widget.new(widget_params)
|
357
396
|
|
358
|
-
# Instead of calling `save`, you call `
|
359
|
-
if @widget.
|
360
|
-
flash[:success] = '
|
361
|
-
redirect_to
|
397
|
+
# Instead of calling `save`, you call `save_draft` to save it as a draft.
|
398
|
+
if @widget.save_draft
|
399
|
+
flash[:success] = 'Draft of widget saved successfully.'
|
400
|
+
redirect_to [:admin, @widget]
|
362
401
|
else
|
363
|
-
flash[:error] = 'There was an error
|
402
|
+
flash[:error] = 'There was an error saving the draft.'
|
364
403
|
render :new
|
365
404
|
end
|
366
405
|
end
|
@@ -371,12 +410,12 @@ class Admin::WidgetsController < Admin::BaseController
|
|
371
410
|
def update
|
372
411
|
@widget.attributes = widget_params
|
373
412
|
|
374
|
-
# Instead of calling `
|
375
|
-
if @widget.
|
376
|
-
flash[:success] = '
|
377
|
-
redirect_to
|
413
|
+
# Instead of calling `update`, you call `save_draft` to save it as a draft.
|
414
|
+
if @widget.save_draft
|
415
|
+
flash[:success] = 'Draft of widget saved successfully.'
|
416
|
+
redirect_to [:admin, @widget]
|
378
417
|
else
|
379
|
-
flash[:error] = 'There was an error
|
418
|
+
flash[:error] = 'There was an error saving the draft.'
|
380
419
|
render :edit
|
381
420
|
end
|
382
421
|
end
|
@@ -390,31 +429,33 @@ class Admin::WidgetsController < Admin::BaseController
|
|
390
429
|
|
391
430
|
private
|
392
431
|
|
393
|
-
# Finds non-trashed widget by `params[:id]
|
432
|
+
# Finds non-trashed widget by `params[:id]`.
|
394
433
|
def find_widget
|
395
434
|
@widget = Widget.live.find(params[:id])
|
396
435
|
end
|
397
436
|
|
398
|
-
# If the widget has a draft, load that version of it
|
437
|
+
# If the widget has a draft, load that version of it.
|
399
438
|
def reify_widget
|
400
439
|
@widget = @widget.draft.reify if @widget.draft?
|
401
440
|
end
|
402
441
|
|
403
|
-
# Strong parameters
|
442
|
+
# Strong parameters for widget form.
|
404
443
|
def widget_params
|
405
444
|
params.require(:widget).permit(:title)
|
406
445
|
end
|
407
446
|
end
|
408
447
|
```
|
409
448
|
|
410
|
-
And "public" controllers (let's say read-only for this simple example) would
|
411
|
-
scope. This also allows items to be
|
449
|
+
And "public" controllers (let's say read-only for this simple example) would
|
450
|
+
ignore drafts entirely via the `published` scope. This also allows items to be
|
451
|
+
"trashed" for admins but still accessible to the public until that deletion is
|
412
452
|
committed.
|
413
453
|
|
414
454
|
```ruby
|
415
455
|
class WidgetsController < ApplicationController
|
416
456
|
def index
|
417
|
-
# The `published` scope gives us widgets that have been committed to be
|
457
|
+
# The `published` scope gives us widgets that have been committed to be
|
458
|
+
# viewed by non-admin users.
|
418
459
|
@widgets = Widget.published.order(:title)
|
419
460
|
end
|
420
461
|
|
@@ -424,18 +465,21 @@ class WidgetsController < ApplicationController
|
|
424
465
|
end
|
425
466
|
```
|
426
467
|
|
427
|
-
Obviously, you can use the scopes that Draftsman provides however you would like
|
468
|
+
Obviously, you can use the scopes that Draftsman provides however you would like
|
469
|
+
in any case.
|
428
470
|
|
429
|
-
Lastly, a `drafts` controller could be provided for admin users to see all
|
430
|
-
|
431
|
-
|
471
|
+
Lastly, a `drafts` controller could be provided for admin users to see all
|
472
|
+
drafts, no matter the type of record (thanks to ActiveRecord's polymorphic
|
473
|
+
associations). From there, they could choose to revert or publish any draft
|
474
|
+
listed, or any other workflow action that you would like for your application to
|
475
|
+
provide for drafts.
|
432
476
|
|
433
477
|
```ruby
|
434
478
|
class Admin::DraftsController < Admin::BaseController
|
435
|
-
before_action :find_draft, :
|
479
|
+
before_action :find_draft, only: [:show, :update, :destroy]
|
436
480
|
|
437
481
|
def index
|
438
|
-
@drafts = Draftsman::Draft.includes(:item).order(
|
482
|
+
@drafts = Draftsman::Draft.includes(:item).order(updated_at: :desc)
|
439
483
|
end
|
440
484
|
|
441
485
|
def show
|
@@ -443,19 +487,22 @@ class Admin::DraftsController < Admin::BaseController
|
|
443
487
|
|
444
488
|
# Post draft ID here to publish it
|
445
489
|
def update
|
446
|
-
# Call `draft_publication_dependencies` to check if any other drafted
|
447
|
-
# `@draft`.
|
490
|
+
# Call `draft_publication_dependencies` to check if any other drafted
|
491
|
+
# records should be published along with this `@draft`.
|
448
492
|
@dependencies = @draft.draft_publication_dependencies
|
449
493
|
|
450
|
-
# If you would like to warn the user about dependent drafts that would need
|
451
|
-
#
|
452
|
-
#
|
453
|
-
#
|
454
|
-
# with
|
494
|
+
# If you would like to warn the user about dependent drafts that would need
|
495
|
+
# to be published along with this one, you would implement an
|
496
|
+
# `app/views/drafts/update.html.erb` view template. In that view template,
|
497
|
+
# you could list the `@dependencies` and show a button posting back to this
|
498
|
+
# action with a name of `commit_publication`. (The button's being clicked
|
499
|
+
# indicates to your application that the user accepts that the dependencies
|
500
|
+
# should be published along with the `@draft`, thus avoiding orphaned
|
501
|
+
# records).
|
455
502
|
if @dependencies.empty? || params[:commit_publication]
|
456
503
|
@draft.publish!
|
457
504
|
flash[:success] = 'The draft was published successfully.'
|
458
|
-
redirect_to
|
505
|
+
redirect_to [:admin, :drafts]
|
459
506
|
else
|
460
507
|
# Renders `app/views/drafts/update.html.erb`
|
461
508
|
end
|
@@ -463,19 +510,22 @@ class Admin::DraftsController < Admin::BaseController
|
|
463
510
|
|
464
511
|
# Post draft ID here to revert it
|
465
512
|
def destroy
|
466
|
-
# Call `draft_reversion_dependencies` to check if any other drafted records
|
467
|
-
# `@draft`.
|
513
|
+
# Call `draft_reversion_dependencies` to check if any other drafted records
|
514
|
+
# should be reverted along with this `@draft`.
|
468
515
|
@dependencies = @draft.draft_reversion_dependencies
|
469
516
|
|
470
|
-
# If you would like to warn the user about dependent drafts that would need
|
471
|
-
#
|
472
|
-
#
|
473
|
-
#
|
474
|
-
# with
|
517
|
+
# If you would like to warn the user about dependent drafts that would need
|
518
|
+
# to be reverted along with this one, you would implement an
|
519
|
+
# `app/views/drafts/destroy.html.erb` view template. In that view template,
|
520
|
+
# you could list the `@dependencies` and show a button posting back to this
|
521
|
+
# action with a name of `commit_reversion`. (The button's being clicked
|
522
|
+
# indicates to your application that the user accepts that the dependencies
|
523
|
+
# should be reverted along with the `@draft`, thus avoiding orphaned
|
524
|
+
# records).
|
475
525
|
if @dependencies.empty? || params[:commit_reversion]
|
476
526
|
@draft.revert!
|
477
527
|
flash[:success] = 'The draft was reverted successfully.'
|
478
|
-
redirect_to
|
528
|
+
redirect_to [:admin, :drafts]
|
479
529
|
else
|
480
530
|
# Renders `app/views/drafts/destroy.html.erb`
|
481
531
|
end
|
@@ -497,8 +547,8 @@ If you would like your `Widget` to have callbacks, it might look something like
|
|
497
547
|
class Widget < ActiveRecord::Base
|
498
548
|
has_drafts
|
499
549
|
|
500
|
-
|
501
|
-
|
550
|
+
before_save_draft :say_hi
|
551
|
+
around_save_draft :surround_update
|
502
552
|
|
503
553
|
private
|
504
554
|
|
@@ -507,9 +557,13 @@ private
|
|
507
557
|
end
|
508
558
|
|
509
559
|
def surround_update
|
510
|
-
|
511
|
-
|
512
|
-
|
560
|
+
if self.persisted?
|
561
|
+
# do something before update
|
562
|
+
yield
|
563
|
+
# do something after update
|
564
|
+
else
|
565
|
+
yield
|
566
|
+
end
|
513
567
|
end
|
514
568
|
end
|
515
569
|
```
|
@@ -517,23 +571,29 @@ end
|
|
517
571
|
|
518
572
|
## Differences from PaperTrail
|
519
573
|
|
520
|
-
If you are familiar with the PaperTrail gem, some parts of the Draftsman gem
|
574
|
+
If you are familiar with the PaperTrail gem, some parts of the Draftsman gem
|
575
|
+
will look very familiar.
|
521
576
|
|
522
577
|
However, there are some differences:
|
523
578
|
|
524
|
-
* PaperTrail hooks into ActiveRecord callbacks so that versions can be saved
|
525
|
-
operations (
|
526
|
-
|
579
|
+
* PaperTrail hooks into ActiveRecord callbacks so that versions can be saved
|
580
|
+
automatically with your normal CRUD operations (`#save`, `#create`,
|
581
|
+
`#update`, `#destroy`, etc.). Draftsman requires that you explicitly call its
|
582
|
+
own CRUD methods in order to save a draft (`#save_draft` and
|
583
|
+
`draft_destruction`).
|
527
584
|
|
528
|
-
* PaperTrail's `Version#object` column looks "
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
585
|
+
* PaperTrail's `Version#object` column looks "backward" and records the
|
586
|
+
object's state _before_ the changes occurred. Because drafts record changes
|
587
|
+
as they will look in the future, they must work differently. Draftsman's
|
588
|
+
`Draft#object` records the object's state _after_ changes are applied to the
|
589
|
+
master object. *But* `destroy` drafts record the object as it was _before_ it
|
590
|
+
was destroyed (in case you want the option of reverting the destroy later and
|
591
|
+
restoring the drafted item back to its original state).
|
533
592
|
|
534
593
|
## Semantic Versioning
|
535
594
|
|
536
|
-
Like many Ruby gems, Draftsman honors the concepts behind
|
595
|
+
Like many Ruby gems, Draftsman honors the concepts behind
|
596
|
+
[semantic versioning][10]:
|
537
597
|
|
538
598
|
> Given a version number MAJOR.MINOR.PATCH, increment the:
|
539
599
|
>
|
@@ -543,35 +603,38 @@ Like many Ruby gems, Draftsman honors the concepts behind [semantic versioning][
|
|
543
603
|
|
544
604
|
## Contributing
|
545
605
|
|
546
|
-
If you feel like you can add something useful to Draftsman, then don't hesitate
|
547
|
-
fix/feature has a high chance of being
|
606
|
+
If you feel like you can add something useful to Draftsman, then don't hesitate
|
607
|
+
to contribute! To make sure your fix/feature has a high chance of being
|
608
|
+
included, please do the following:
|
548
609
|
|
549
610
|
1. Fork the repo.
|
550
611
|
|
551
612
|
2. Run `bundle install`.
|
552
613
|
|
553
|
-
3. `cd spec/dummy` and run `RAILS_ENV=test rake db:migrate` to apply test
|
614
|
+
3. `cd spec/dummy` and run `RAILS_ENV=test rake db:migrate` to apply test
|
615
|
+
database migrations.
|
554
616
|
|
555
|
-
4. Add at least one test for your change. Only refactoring and documentation
|
556
|
-
|
617
|
+
4. Add at least one test for your change. Only refactoring and documentation
|
618
|
+
changes require no new tests. If you are adding functionality or fixing a
|
619
|
+
bug, you need a test!
|
557
620
|
|
558
|
-
5. Make all tests pass by running `
|
621
|
+
5. Make all tests pass by running `rake`.
|
559
622
|
|
560
623
|
6. Push to your fork and submit a pull request.
|
561
624
|
|
562
|
-
I can't guarantee that I will accept the change, but if I don't, I will be sure
|
625
|
+
I can't guarantee that I will accept the change, but if I don't, I will be sure
|
626
|
+
to let you know why.
|
563
627
|
|
564
|
-
Here are some things that will increase the chance that your pull request is
|
565
|
-
Rails guide:
|
628
|
+
Here are some things that will increase the chance that your pull request is
|
629
|
+
accepted, taken straight from the Ruby on Rails guide:
|
566
630
|
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
* Update the documentation, guides, or whatever is affected by your contribution
|
631
|
+
- Use Rails idioms
|
632
|
+
- Include tests that fail without your code, and pass with it
|
633
|
+
- Update the documentation, guides, or whatever is affected by your
|
634
|
+
contribution
|
572
635
|
|
573
|
-
This gem is a work in progress. I am adding specs as I need features in my
|
574
|
-
work on features or find bugs!
|
636
|
+
This gem is a work in progress. I am adding specs as I need features in my
|
637
|
+
application. Please add missing ones as you work on features or find bugs!
|
575
638
|
|
576
639
|
## License
|
577
640
|
|
@@ -585,7 +648,7 @@ Draftsman is released under the [MIT License][9].
|
|
585
648
|
[4]: http://railscasts.com/episodes/416-form-objects
|
586
649
|
[5]: http://www.sinatrarb.com/
|
587
650
|
[6]: https://github.com/janko-m/sinatra-activerecord
|
588
|
-
[7]: https://raw.github.com/
|
651
|
+
[7]: https://raw.github.com/liveeditor/draftsman/master/lib/generators/draftsman/templates/create_drafts.rb
|
589
652
|
[8]: http://www.sinatrarb.com/intro.html#Modular%20vs.%20Classic%20Style
|
590
653
|
[9]: http://www.opensource.org/licenses/MIT
|
591
654
|
[10]: http://semver.org/
|