draftsman 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +97 -0
- data/LICENSE +20 -0
- data/README.md +506 -0
- data/Rakefile +6 -0
- data/draftsman.gemspec +33 -0
- data/lib/draftsman/config.rb +13 -0
- data/lib/draftsman/draft.rb +289 -0
- data/lib/draftsman/frameworks/cucumber.rb +7 -0
- data/lib/draftsman/frameworks/rails.rb +58 -0
- data/lib/draftsman/frameworks/rspec.rb +16 -0
- data/lib/draftsman/frameworks/sinatra.rb +31 -0
- data/lib/draftsman/model.rb +428 -0
- data/lib/draftsman/serializers/json.rb +17 -0
- data/lib/draftsman/serializers/yaml.rb +17 -0
- data/lib/draftsman/version.rb +3 -0
- data/lib/draftsman.rb +101 -0
- data/lib/generators/draftsman/install_generator.rb +27 -0
- data/lib/generators/draftsman/templates/add_object_changes_column_to_drafts.rb +9 -0
- data/lib/generators/draftsman/templates/config/initializers/draftsman.rb +11 -0
- data/lib/generators/draftsman/templates/create_drafts.rb +22 -0
- data/spec/controllers/informants_controller_spec.rb +27 -0
- data/spec/controllers/users_controller_spec.rb +23 -0
- data/spec/controllers/whodunnits_controller_spec.rb +24 -0
- data/spec/draftsman_spec.rb +19 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/images/rails.png +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +20 -0
- data/spec/dummy/app/controllers/informants_controller.rb +8 -0
- data/spec/dummy/app/controllers/users_controller.rb +8 -0
- data/spec/dummy/app/controllers/whodunnits_controller.rb +8 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/helpers/messages_helper.rb +2 -0
- data/spec/dummy/app/mailers/.gitkeep +0 -0
- data/spec/dummy/app/models/bastard.rb +3 -0
- data/spec/dummy/app/models/child.rb +4 -0
- data/spec/dummy/app/models/parent.rb +5 -0
- data/spec/dummy/app/models/trashable.rb +3 -0
- data/spec/dummy/app/models/vanilla.rb +3 -0
- data/spec/dummy/app/models/whitelister.rb +3 -0
- data/spec/dummy/app/views/layouts/application.html.erb +15 -0
- data/spec/dummy/config/application.rb +37 -0
- data/spec/dummy/config/boot.rb +6 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +32 -0
- data/spec/dummy/config/environments/production.rb +73 -0
- data/spec/dummy/config/environments/test.rb +39 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +6 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/migrate/20110208155312_set_up_test_tables.rb +86 -0
- data/spec/dummy/db/schema.rb +106 -0
- data/spec/dummy/db/seeds.rb +7 -0
- data/spec/dummy/lib/assets/.gitkeep +0 -0
- data/spec/dummy/lib/tasks/.gitkeep +0 -0
- data/spec/dummy/log/.gitkeep +0 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/models/child_spec.rb +205 -0
- data/spec/models/draft_spec.rb +297 -0
- data/spec/models/parent_spec.rb +191 -0
- data/spec/models/trashable_spec.rb +164 -0
- data/spec/models/vanilla_spec.rb +201 -0
- data/spec/models/whitelister_spec.rb +262 -0
- data/spec/spec_helper.rb +52 -0
- 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
data/.rspec
ADDED
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
data/Gemfile
ADDED
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
|