has_draft 1.3.0 → 2.0.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 +6 -14
- data/CHANGELOG.md +29 -0
- data/LICENSE +19 -17
- data/README.md +213 -0
- data/lib/has_draft/version.rb +1 -1
- data/lib/has_draft.rb +59 -47
- metadata +23 -133
- data/Appraisals +0 -15
- data/Gemfile +0 -3
- data/Gemfile.lock +0 -63
- data/README.rdoc +0 -227
- data/Rakefile +0 -30
- data/init.rb +0 -3
- data/lib/has_draft/railtie.rb +0 -19
- data/spec/db/database.yml +0 -21
- data/spec/db/schema.rb +0 -32
- data/spec/db/test.sqlite3 +0 -0
- data/spec/factories/article.rb +0 -20
- data/spec/has_draft_spec.rb +0 -105
- data/spec/spec_helper.rb +0 -34
checksums.yaml
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
metadata.gz: !binary |-
|
|
9
|
-
NTUzNzRkMjkyMWIyYzBkOWFjYTFlZTMzZjkxNWI4ZjYzMTE0MTM5NDc2NGE0
|
|
10
|
-
NWNiYTUwOWZkYjViOTFmOTQxYzQyYjY0YmMxYmM0MTBkMTRhN2QzYzJjNTQ2
|
|
11
|
-
YjhlMWFiM2VjMzhiMzI3Mzk3MTA5YWYxZTJmODRhZjgzYzU4NDM=
|
|
12
|
-
data.tar.gz: !binary |-
|
|
13
|
-
Y2JkNDE3ZTZiZmU5M2EzMmVjNTE2MDc5MTVmZjFiOTc0OTU2YTk4Mjg4YjY0
|
|
14
|
-
NzgxZmMwODIxMmMyMzNiNjk1OTI5NWU5NDQxZDMzYjFmNDYzYjdhMjdmMDNj
|
|
15
|
-
NDA1ZTdmY2NhOTlmNTdlNWFjYjIwOGMwOGQ4OGRiOTFjOWY0ZDM=
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 29d15db9d31975799a93a854e620a005c6358befe684fdb7e1d5766324463d33
|
|
4
|
+
data.tar.gz: 85e9962ebe1ad2d7f76afe2a5b2be0f94cc2fb26d188ea746d0eed6330e66cd2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c20a9d20993e59c7161b1225c99b27c03341c7efb11762f34ef33a0cbdab616ce8a138f45eb97a415ca3b46e72aa624266e6af679abf25ff2be166dcded0f4c2
|
|
7
|
+
data.tar.gz: 9e4739990706a7fc0bb6500ec0a2caab469cca88550be61ab050f70effc6d611e20e67e41fc2e678fcd3e3601f4a93f413123f20b8178a36806c314148bbaca2
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
Supports Rails (ActiveRecord) 6.1 through 8.1 on Ruby 3.0 and later.
|
|
6
|
+
|
|
7
|
+
### Breaking changes
|
|
8
|
+
|
|
9
|
+
- Requires ActiveRecord 6.1+ and Ruby 3.0+.
|
|
10
|
+
- `instantiate_draft` builds the draft without saving it, as originally intended.
|
|
11
|
+
`instantiate_draft!` still saves it.
|
|
12
|
+
- On a new record, `instantiate_draft!` no longer saves the record. The draft is saved along with the
|
|
13
|
+
record instead. `save_to_draft` raises `ActiveRecord::RecordNotSaved`.
|
|
14
|
+
- The foreign key defaults to `"#{belongs_to}_id"`, so with `belongs_to: :element` it's
|
|
15
|
+
`element_id` rather than one named after the including class. This is unchanged when `:belongs_to`
|
|
16
|
+
isn't given.
|
|
17
|
+
- The Railtie and the `init.rb` files are gone. The gem hooks into ActiveRecord when it's required.
|
|
18
|
+
|
|
19
|
+
### Fixes
|
|
20
|
+
|
|
21
|
+
- `save_to_draft`, `destroy_draft!` and the `with_draft`/`without_draft` scopes work on modern Rails.
|
|
22
|
+
- `save_to_draft` returns false when the draft fails to save.
|
|
23
|
+
- Defining a model no longer needs a database connection. Drafted columns are worked out on first use.
|
|
24
|
+
- Columns that only exist on the draft table are no longer copied, which used to raise `NoMethodError`.
|
|
25
|
+
- A symbol `:foreign_key` is no longer copied as if it were a drafted column.
|
|
26
|
+
|
|
27
|
+
## 1.4.0 and earlier
|
|
28
|
+
|
|
29
|
+
See the [commit history](https://github.com/eigenben/has_draft/commits/master).
|
data/LICENSE
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
The MIT License (MIT)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
a copy of this software and associated documentation files (the
|
|
5
|
-
"Software"), to deal in the Software without restriction, including
|
|
6
|
-
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
-
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
-
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
-
the following conditions:
|
|
3
|
+
Copyright (c) 2013 Ben Hughes
|
|
10
4
|
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, 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,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
13
22
|
|
|
14
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
-
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
-
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
-
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# has_draft
|
|
2
|
+
|
|
3
|
+
[](https://github.com/eigenben/has_draft/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/has_draft)
|
|
5
|
+
|
|
6
|
+
Gives your ActiveRecord models a draft copy that lives alongside the live record. Typical uses:
|
|
7
|
+
|
|
8
|
+
- A draft and a live version of a page, where edits are previewed before they're published
|
|
9
|
+
- An approval workflow, where the live copy stays active while a draft waits for review
|
|
10
|
+
|
|
11
|
+
Drafts are stored in a separate, nearly identical table, joined to the live record by a `has_one`.
|
|
12
|
+
Your existing table and queries don't change: `Page.all` still returns only live pages, with no extra
|
|
13
|
+
conditions needed. The draft table is backed by a model that `has_draft` creates for you as a constant
|
|
14
|
+
on the original class, so if `Page` has a draft, `Page::Draft` is the model for the `page_drafts` table.
|
|
15
|
+
|
|
16
|
+
Because a draft is a real model with real columns, it can have its own validations, associations and
|
|
17
|
+
indexes, and you can query drafts with SQL like any other table. The trade-off is that the draft table
|
|
18
|
+
has to be kept in step with the live table: when you add a column to one, add it to the other.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Requires Rails (ActiveRecord) 6.1 or later and Ruby 3.0 or later.
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
gem "has_draft"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Basic example
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
class CreateArticles < ActiveRecord::Migration[7.1]
|
|
32
|
+
def change
|
|
33
|
+
[:articles, :article_drafts].each do |table_name|
|
|
34
|
+
create_table table_name do |t|
|
|
35
|
+
t.references :article if table_name == :article_drafts
|
|
36
|
+
|
|
37
|
+
t.string :title
|
|
38
|
+
t.text :summary
|
|
39
|
+
t.text :body
|
|
40
|
+
t.date :post_date
|
|
41
|
+
t.timestamps
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class Article < ApplicationRecord
|
|
48
|
+
has_draft
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Class methods and scopes:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
Article.draft_class # => Article::Draft
|
|
56
|
+
Article.with_draft # articles that have a draft
|
|
57
|
+
Article.without_draft # articles that don't
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Working with drafts:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
article = Article.create!(title: "My Title", body: "Full body", post_date: Date.today)
|
|
64
|
+
|
|
65
|
+
article.has_draft? # => false
|
|
66
|
+
article.instantiate_draft! # copies the article into a new, saved draft
|
|
67
|
+
article.has_draft? # => true
|
|
68
|
+
article.draft # => #<Article::Draft ...>
|
|
69
|
+
|
|
70
|
+
article.draft.update!(title: "New Title")
|
|
71
|
+
article.replace_with_draft! # copies the draft onto the article and saves it
|
|
72
|
+
article.title # => "New Title"
|
|
73
|
+
|
|
74
|
+
article.destroy_draft!
|
|
75
|
+
article.has_draft? # => false
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`save_to_draft` saves a record's unsaved changes into its draft (creating the draft if needed) instead
|
|
79
|
+
of the live table, then reloads the live record:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
article.title = "Work in progress"
|
|
83
|
+
article.save_to_draft # validates the article first; returns false if invalid
|
|
84
|
+
article.title # => "New Title"
|
|
85
|
+
article.draft.title # => "Work in progress"
|
|
86
|
+
|
|
87
|
+
article.save_to_draft(false) # skip validation
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Drafts belong to a saved record. On a new record, `instantiate_draft!` builds the draft and it's saved
|
|
91
|
+
along with the record, while `save_to_draft` raises `ActiveRecord::RecordNotSaved`.
|
|
92
|
+
`instantiate_draft` (without the bang) builds the draft without saving it.
|
|
93
|
+
|
|
94
|
+
Every column the two tables share is copied, except the primary key, the foreign key, `created_at`,
|
|
95
|
+
`updated_at` and the STI type column. Columns that only exist on the draft table (say, an `approved_at`
|
|
96
|
+
for a review workflow) are left alone. `Article.draft_columns` lists the columns that are copied.
|
|
97
|
+
|
|
98
|
+
## Custom options
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
class Article < ApplicationRecord
|
|
102
|
+
has_draft class_name: "Copy", foreign_key: :news_article_id, table_name: "article_copies"
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
| Option | Default |
|
|
107
|
+
| -------------- | ---------------------------------------- |
|
|
108
|
+
| `:class_name` | `"Draft"` (defined as `Article::Draft`) |
|
|
109
|
+
| `:table_name` | `"article_drafts"` |
|
|
110
|
+
| `:belongs_to` | `:article`, the draft's parent association |
|
|
111
|
+
| `:foreign_key` | `"#{belongs_to}_id"`, so `"article_id"` |
|
|
112
|
+
| `:extends` | `ActiveRecord::Base`, the draft class's superclass |
|
|
113
|
+
|
|
114
|
+
## Single table inheritance
|
|
115
|
+
|
|
116
|
+
Subclasses draft into the base class's draft table. Use `:belongs_to` so every draft class shares the
|
|
117
|
+
same parent association, and `:extends` to share behavior between draft classes:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
class CreateElements < ActiveRecord::Migration[7.1]
|
|
121
|
+
def change
|
|
122
|
+
[:elements, :element_drafts].each do |table_name|
|
|
123
|
+
create_table table_name do |t|
|
|
124
|
+
t.references :element if table_name == :element_drafts
|
|
125
|
+
|
|
126
|
+
t.string :title
|
|
127
|
+
t.text :content
|
|
128
|
+
t.string :type
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
class Element < ApplicationRecord
|
|
135
|
+
# Uses the element_drafts table, with each subclass's draft class stored via STI
|
|
136
|
+
class Draft < ApplicationRecord
|
|
137
|
+
def icon_path
|
|
138
|
+
"images/default.png"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
class BlueElement < Element
|
|
144
|
+
has_draft belongs_to: :element, extends: Element::Draft do
|
|
145
|
+
def icon_path
|
|
146
|
+
"images/blue.png"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
class UnknownElement < Element
|
|
152
|
+
has_draft belongs_to: :element, extends: Element::Draft
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Now `draft.element` works on any element's draft, without needing to know to call
|
|
157
|
+
`draft.blue_element` or `draft.unknown_element`. Calling `has_draft` in a class whose parent already
|
|
158
|
+
called it does nothing.
|
|
159
|
+
|
|
160
|
+
## Callbacks
|
|
161
|
+
|
|
162
|
+
Define any of these methods to hook into the draft lifecycle:
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
class Article < ApplicationRecord
|
|
166
|
+
has_draft
|
|
167
|
+
|
|
168
|
+
def before_instantiate_draft
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def before_replace_with_draft
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def before_destroy_draft
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Extending the draft class
|
|
180
|
+
|
|
181
|
+
Since you don't define the draft class yourself, pass a block to `has_draft` to run code in its context:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
class Article < ApplicationRecord
|
|
185
|
+
belongs_to :user
|
|
186
|
+
|
|
187
|
+
has_draft do
|
|
188
|
+
belongs_to :last_updated_user, class_name: "User", optional: true
|
|
189
|
+
|
|
190
|
+
def approve!
|
|
191
|
+
update!(approved_at: Time.current)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Development
|
|
198
|
+
|
|
199
|
+
```sh
|
|
200
|
+
bundle install
|
|
201
|
+
bundle exec rake
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
To test against a specific Rails version, use one of the gemfiles in `gemfiles/`:
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
BUNDLE_GEMFILE=gemfiles/rails_6.1.gemfile bundle install
|
|
208
|
+
BUNDLE_GEMFILE=gemfiles/rails_6.1.gemfile bundle exec rake
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
MIT
|
data/lib/has_draft/version.rb
CHANGED
data/lib/has_draft.rb
CHANGED
|
@@ -1,27 +1,35 @@
|
|
|
1
|
+
require "active_support"
|
|
2
|
+
require "active_record"
|
|
3
|
+
require "has_draft/version"
|
|
4
|
+
|
|
1
5
|
module HasDraft
|
|
2
6
|
def self.included(base)
|
|
3
7
|
base.extend ClassMethods
|
|
4
8
|
end
|
|
5
|
-
|
|
9
|
+
|
|
6
10
|
module ClassMethods
|
|
7
11
|
def has_draft(options = {}, &block)
|
|
8
12
|
return if self.included_modules.include?(HasDraft::InstanceMethods)
|
|
9
13
|
include HasDraft::InstanceMethods
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
|
|
15
|
+
class_attribute :draft_class_name, :draft_foreign_key, :draft_table_name, :instance_writer => false
|
|
16
|
+
|
|
17
|
+
# Default parent association
|
|
18
|
+
options[:belongs_to] = self.name.demodulize.underscore.to_sym if options[:belongs_to].nil?
|
|
19
|
+
|
|
13
20
|
self.draft_class_name = options[:class_name] || 'Draft'
|
|
14
|
-
self.draft_foreign_key = options[:foreign_key] ||
|
|
21
|
+
self.draft_foreign_key = (options[:foreign_key] || "#{options[:belongs_to]}_id").to_s
|
|
15
22
|
self.draft_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_drafts#{table_name_suffix}"
|
|
16
|
-
|
|
23
|
+
|
|
17
24
|
# Create Relationship to Draft Copy
|
|
18
25
|
class_eval do
|
|
19
|
-
has_one :draft, :class_name => "#{self.
|
|
26
|
+
has_one :draft, :class_name => "#{self.name}::#{draft_class_name}",
|
|
20
27
|
:foreign_key => draft_foreign_key,
|
|
21
|
-
:dependent => :destroy
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
scope :
|
|
28
|
+
:dependent => :destroy,
|
|
29
|
+
:inverse_of => options[:belongs_to]
|
|
30
|
+
|
|
31
|
+
scope :with_draft, lambda { includes(:draft).where.not(draft_table_name => { draft_class.primary_key => nil }) }
|
|
32
|
+
scope :without_draft, lambda { includes(:draft).where(draft_table_name => { draft_class.primary_key => nil }) }
|
|
25
33
|
end
|
|
26
34
|
|
|
27
35
|
# Default parent class to ActiveRecord::Base
|
|
@@ -29,98 +37,102 @@ module HasDraft
|
|
|
29
37
|
|
|
30
38
|
# Dynamically Create Model::Draft Class
|
|
31
39
|
const_set(draft_class_name, Class.new(options[:extends]))
|
|
32
|
-
|
|
33
|
-
draft_class.
|
|
40
|
+
|
|
41
|
+
draft_class.class_attribute :original_class, :instance_writer => false
|
|
34
42
|
draft_class.original_class = self
|
|
35
43
|
draft_class.table_name = draft_table_name
|
|
36
44
|
|
|
37
|
-
# Default parent association
|
|
38
|
-
options[:belongs_to] = self.to_s.demodulize.underscore.to_sym if options[:belongs_to].nil?
|
|
39
|
-
|
|
40
45
|
# Draft Parent Association
|
|
41
|
-
draft_class.belongs_to options[:belongs_to], :class_name => "::#{self.
|
|
42
|
-
|
|
46
|
+
draft_class.belongs_to options[:belongs_to], :class_name => "::#{self.name}", :foreign_key => draft_foreign_key, :inverse_of => :draft
|
|
47
|
+
|
|
43
48
|
# Block extension
|
|
44
49
|
draft_class.class_eval(&block) if block_given?
|
|
45
|
-
|
|
46
|
-
# Finally setup which columns to draft
|
|
47
|
-
self.draft_columns = draft_class.new.attributes.keys - [draft_class.primary_key, draft_foreign_key, 'created_at', 'updated_at', inheritance_column]
|
|
48
50
|
end
|
|
49
|
-
|
|
51
|
+
|
|
52
|
+
# Columns copied between the live record and its draft: those the two share, minus keys and
|
|
53
|
+
# timestamps. Computed on first use so defining the model doesn't need a database connection.
|
|
54
|
+
def draft_columns
|
|
55
|
+
@draft_columns ||= (draft_class.attribute_types.keys & attribute_types.keys) - [draft_class.primary_key, draft_foreign_key, 'created_at', 'updated_at', inheritance_column]
|
|
56
|
+
end
|
|
57
|
+
|
|
50
58
|
def draft_class
|
|
51
59
|
const_get(draft_class_name)
|
|
52
60
|
end
|
|
53
61
|
end
|
|
54
|
-
|
|
62
|
+
|
|
55
63
|
module InstanceMethods
|
|
56
64
|
def has_draft?
|
|
57
65
|
!self.draft.nil?
|
|
58
66
|
end
|
|
59
|
-
|
|
67
|
+
|
|
60
68
|
def save_to_draft(perform_validation = true)
|
|
69
|
+
raise ActiveRecord::RecordNotSaved.new("Can't save a draft of a new record; save the record first", self) if self.new_record?
|
|
61
70
|
return false if perform_validation and !self.valid?
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
self.draft.save(perform_validation)
|
|
71
|
+
|
|
72
|
+
has_draft? ? copy_attributes_to_draft : instantiate_draft
|
|
73
|
+
|
|
74
|
+
return false unless self.draft.save(:validate => perform_validation)
|
|
67
75
|
self.reload
|
|
68
76
|
end
|
|
69
|
-
|
|
77
|
+
|
|
78
|
+
# Builds a draft copy of the record without saving it.
|
|
70
79
|
def instantiate_draft
|
|
71
80
|
self.build_draft
|
|
72
|
-
|
|
81
|
+
|
|
73
82
|
copy_attributes_to_draft
|
|
74
83
|
before_instantiate_draft
|
|
75
|
-
|
|
76
|
-
self.draft
|
|
84
|
+
|
|
85
|
+
self.draft
|
|
77
86
|
end
|
|
78
|
-
|
|
87
|
+
|
|
88
|
+
# Builds and saves a draft copy of the record. On a new record, the draft is saved along with it.
|
|
79
89
|
def instantiate_draft!
|
|
80
|
-
instantiate_draft do |draft|
|
|
90
|
+
instantiate_draft.tap do |draft|
|
|
81
91
|
draft.save unless self.new_record?
|
|
82
92
|
end
|
|
83
93
|
end
|
|
84
|
-
|
|
94
|
+
|
|
85
95
|
def copy_attributes_to_draft
|
|
86
96
|
self.class.draft_columns.each do |attribute|
|
|
87
97
|
self.draft.send("#{attribute}=", send(attribute))
|
|
88
98
|
end
|
|
89
99
|
self
|
|
90
100
|
end
|
|
91
|
-
|
|
101
|
+
|
|
92
102
|
def copy_attributes_from_draft
|
|
93
103
|
self.class.draft_columns.each do |attribute|
|
|
94
104
|
self.send("#{attribute}=", self.draft.send(attribute))
|
|
95
105
|
end
|
|
96
106
|
self
|
|
97
107
|
end
|
|
98
|
-
|
|
108
|
+
|
|
99
109
|
def before_instantiate_draft
|
|
100
110
|
end
|
|
101
|
-
|
|
111
|
+
|
|
102
112
|
def replace_with_draft!
|
|
103
113
|
copy_attributes_from_draft
|
|
104
|
-
|
|
114
|
+
|
|
105
115
|
before_replace_with_draft
|
|
106
|
-
|
|
116
|
+
|
|
107
117
|
self.save unless self.new_record?
|
|
108
118
|
self
|
|
109
119
|
end
|
|
110
|
-
|
|
120
|
+
|
|
111
121
|
def before_replace_with_draft
|
|
112
122
|
end
|
|
113
|
-
|
|
123
|
+
|
|
114
124
|
def destroy_draft!
|
|
115
125
|
before_destroy_draft
|
|
116
|
-
|
|
126
|
+
|
|
117
127
|
self.draft.destroy if self.draft
|
|
118
|
-
self.
|
|
128
|
+
self.reload_draft
|
|
119
129
|
end
|
|
120
|
-
|
|
130
|
+
|
|
121
131
|
def before_destroy_draft
|
|
122
132
|
end
|
|
123
133
|
end
|
|
124
134
|
end
|
|
125
135
|
|
|
126
|
-
|
|
136
|
+
ActiveSupport.on_load(:active_record) do
|
|
137
|
+
include HasDraft
|
|
138
|
+
end
|
metadata
CHANGED
|
@@ -1,127 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: has_draft
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ben Hughes
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
|
-
- !ruby/object:Gem::Dependency
|
|
14
|
-
name: activesupport
|
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
|
16
|
-
requirements:
|
|
17
|
-
- - ! '>='
|
|
18
|
-
- !ruby/object:Gem::Version
|
|
19
|
-
version: 3.0.0
|
|
20
|
-
type: :runtime
|
|
21
|
-
prerelease: false
|
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
-
requirements:
|
|
24
|
-
- - ! '>='
|
|
25
|
-
- !ruby/object:Gem::Version
|
|
26
|
-
version: 3.0.0
|
|
27
12
|
- !ruby/object:Gem::Dependency
|
|
28
13
|
name: activerecord
|
|
29
14
|
requirement: !ruby/object:Gem::Requirement
|
|
30
15
|
requirements:
|
|
31
|
-
- -
|
|
16
|
+
- - ">="
|
|
32
17
|
- !ruby/object:Gem::Version
|
|
33
|
-
version:
|
|
18
|
+
version: '6.1'
|
|
34
19
|
type: :runtime
|
|
35
20
|
prerelease: false
|
|
36
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
22
|
requirements:
|
|
38
|
-
- -
|
|
39
|
-
- !ruby/object:Gem::Version
|
|
40
|
-
version: 3.0.0
|
|
41
|
-
- !ruby/object:Gem::Dependency
|
|
42
|
-
name: rspec
|
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
|
44
|
-
requirements:
|
|
45
|
-
- - ~>
|
|
46
|
-
- !ruby/object:Gem::Version
|
|
47
|
-
version: '2.0'
|
|
48
|
-
type: :development
|
|
49
|
-
prerelease: false
|
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
-
requirements:
|
|
52
|
-
- - ~>
|
|
53
|
-
- !ruby/object:Gem::Version
|
|
54
|
-
version: '2.0'
|
|
55
|
-
- !ruby/object:Gem::Dependency
|
|
56
|
-
name: factory_girl
|
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
|
58
|
-
requirements:
|
|
59
|
-
- - ~>
|
|
60
|
-
- !ruby/object:Gem::Version
|
|
61
|
-
version: 1.3.1
|
|
62
|
-
type: :development
|
|
63
|
-
prerelease: false
|
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
-
requirements:
|
|
66
|
-
- - ~>
|
|
67
|
-
- !ruby/object:Gem::Version
|
|
68
|
-
version: 1.3.1
|
|
69
|
-
- !ruby/object:Gem::Dependency
|
|
70
|
-
name: faker
|
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
|
72
|
-
requirements:
|
|
73
|
-
- - ~>
|
|
23
|
+
- - ">="
|
|
74
24
|
- !ruby/object:Gem::Version
|
|
75
|
-
version:
|
|
76
|
-
type: :development
|
|
77
|
-
prerelease: false
|
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
-
requirements:
|
|
80
|
-
- - ~>
|
|
81
|
-
- !ruby/object:Gem::Version
|
|
82
|
-
version: 0.9.1
|
|
83
|
-
- !ruby/object:Gem::Dependency
|
|
84
|
-
name: sqlite3
|
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
|
86
|
-
requirements:
|
|
87
|
-
- - ~>
|
|
88
|
-
- !ruby/object:Gem::Version
|
|
89
|
-
version: 1.3.4
|
|
90
|
-
type: :development
|
|
91
|
-
prerelease: false
|
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
-
requirements:
|
|
94
|
-
- - ~>
|
|
95
|
-
- !ruby/object:Gem::Version
|
|
96
|
-
version: 1.3.4
|
|
97
|
-
- !ruby/object:Gem::Dependency
|
|
98
|
-
name: appraisal
|
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
|
100
|
-
requirements:
|
|
101
|
-
- - ~>
|
|
102
|
-
- !ruby/object:Gem::Version
|
|
103
|
-
version: 0.4.0
|
|
104
|
-
type: :development
|
|
105
|
-
prerelease: false
|
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
-
requirements:
|
|
108
|
-
- - ~>
|
|
109
|
-
- !ruby/object:Gem::Version
|
|
110
|
-
version: 0.4.0
|
|
111
|
-
- !ruby/object:Gem::Dependency
|
|
112
|
-
name: rake
|
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
|
114
|
-
requirements:
|
|
115
|
-
- - ! '>='
|
|
116
|
-
- !ruby/object:Gem::Version
|
|
117
|
-
version: '0'
|
|
118
|
-
type: :development
|
|
119
|
-
prerelease: false
|
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
-
requirements:
|
|
122
|
-
- - ! '>='
|
|
123
|
-
- !ruby/object:Gem::Version
|
|
124
|
-
version: '0'
|
|
25
|
+
version: '6.1'
|
|
125
26
|
description: Allows for your ActiveRecord models to have drafts which are stored in
|
|
126
27
|
a separate duplicate table.
|
|
127
28
|
email: ben@railsgarden.com
|
|
@@ -129,44 +30,33 @@ executables: []
|
|
|
129
30
|
extensions: []
|
|
130
31
|
extra_rdoc_files: []
|
|
131
32
|
files:
|
|
132
|
-
-
|
|
133
|
-
- lib/has_draft/version.rb
|
|
134
|
-
- lib/has_draft.rb
|
|
135
|
-
- spec/db/database.yml
|
|
136
|
-
- spec/db/schema.rb
|
|
137
|
-
- spec/db/test.sqlite3
|
|
138
|
-
- spec/factories/article.rb
|
|
139
|
-
- spec/has_draft_spec.rb
|
|
140
|
-
- spec/spec_helper.rb
|
|
141
|
-
- Appraisals
|
|
142
|
-
- Gemfile
|
|
143
|
-
- Gemfile.lock
|
|
33
|
+
- CHANGELOG.md
|
|
144
34
|
- LICENSE
|
|
145
|
-
-
|
|
146
|
-
-
|
|
147
|
-
-
|
|
148
|
-
homepage:
|
|
149
|
-
licenses:
|
|
150
|
-
|
|
151
|
-
|
|
35
|
+
- README.md
|
|
36
|
+
- lib/has_draft.rb
|
|
37
|
+
- lib/has_draft/version.rb
|
|
38
|
+
homepage: https://github.com/eigenben/has_draft
|
|
39
|
+
licenses:
|
|
40
|
+
- MIT
|
|
41
|
+
metadata:
|
|
42
|
+
source_code_uri: https://github.com/eigenben/has_draft
|
|
43
|
+
changelog_uri: https://github.com/eigenben/has_draft/blob/master/CHANGELOG.md
|
|
44
|
+
bug_tracker_uri: https://github.com/eigenben/has_draft/issues
|
|
152
45
|
rdoc_options: []
|
|
153
46
|
require_paths:
|
|
154
47
|
- lib
|
|
155
48
|
required_ruby_version: !ruby/object:Gem::Requirement
|
|
156
49
|
requirements:
|
|
157
|
-
- -
|
|
50
|
+
- - ">="
|
|
158
51
|
- !ruby/object:Gem::Version
|
|
159
|
-
version: '0'
|
|
52
|
+
version: '3.0'
|
|
160
53
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
161
54
|
requirements:
|
|
162
|
-
- -
|
|
55
|
+
- - ">="
|
|
163
56
|
- !ruby/object:Gem::Version
|
|
164
|
-
version:
|
|
57
|
+
version: '0'
|
|
165
58
|
requirements: []
|
|
166
|
-
|
|
167
|
-
rubygems_version: 2.0.6
|
|
168
|
-
signing_key:
|
|
59
|
+
rubygems_version: 3.6.7
|
|
169
60
|
specification_version: 4
|
|
170
|
-
summary:
|
|
61
|
+
summary: Attach drafts to your ActiveRecord models.
|
|
171
62
|
test_files: []
|
|
172
|
-
has_rdoc:
|
data/Appraisals
DELETED
data/Gemfile
DELETED
data/Gemfile.lock
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
PATH
|
|
2
|
-
remote: .
|
|
3
|
-
specs:
|
|
4
|
-
has_draft (1.2.0)
|
|
5
|
-
activerecord (>= 3.0.0)
|
|
6
|
-
activesupport (>= 3.0.0)
|
|
7
|
-
|
|
8
|
-
GEM
|
|
9
|
-
remote: https://rubygems.org/
|
|
10
|
-
specs:
|
|
11
|
-
activemodel (4.0.0)
|
|
12
|
-
activesupport (= 4.0.0)
|
|
13
|
-
builder (~> 3.1.0)
|
|
14
|
-
activerecord (4.0.0)
|
|
15
|
-
activemodel (= 4.0.0)
|
|
16
|
-
activerecord-deprecated_finders (~> 1.0.2)
|
|
17
|
-
activesupport (= 4.0.0)
|
|
18
|
-
arel (~> 4.0.0)
|
|
19
|
-
activerecord-deprecated_finders (1.0.3)
|
|
20
|
-
activesupport (4.0.0)
|
|
21
|
-
i18n (~> 0.6, >= 0.6.4)
|
|
22
|
-
minitest (~> 4.2)
|
|
23
|
-
multi_json (~> 1.3)
|
|
24
|
-
thread_safe (~> 0.1)
|
|
25
|
-
tzinfo (~> 0.3.37)
|
|
26
|
-
appraisal (0.4.1)
|
|
27
|
-
bundler
|
|
28
|
-
rake
|
|
29
|
-
arel (4.0.0)
|
|
30
|
-
atomic (1.1.14)
|
|
31
|
-
builder (3.1.4)
|
|
32
|
-
diff-lcs (1.2.4)
|
|
33
|
-
factory_girl (1.3.3)
|
|
34
|
-
faker (0.9.5)
|
|
35
|
-
i18n (~> 0.4)
|
|
36
|
-
i18n (0.6.5)
|
|
37
|
-
minitest (4.7.5)
|
|
38
|
-
multi_json (1.8.0)
|
|
39
|
-
rake (10.1.0)
|
|
40
|
-
rspec (2.14.1)
|
|
41
|
-
rspec-core (~> 2.14.0)
|
|
42
|
-
rspec-expectations (~> 2.14.0)
|
|
43
|
-
rspec-mocks (~> 2.14.0)
|
|
44
|
-
rspec-core (2.14.5)
|
|
45
|
-
rspec-expectations (2.14.3)
|
|
46
|
-
diff-lcs (>= 1.1.3, < 2.0)
|
|
47
|
-
rspec-mocks (2.14.3)
|
|
48
|
-
sqlite3 (1.3.8)
|
|
49
|
-
thread_safe (0.1.3)
|
|
50
|
-
atomic
|
|
51
|
-
tzinfo (0.3.37)
|
|
52
|
-
|
|
53
|
-
PLATFORMS
|
|
54
|
-
ruby
|
|
55
|
-
|
|
56
|
-
DEPENDENCIES
|
|
57
|
-
appraisal (~> 0.4.0)
|
|
58
|
-
factory_girl (~> 1.3.1)
|
|
59
|
-
faker (~> 0.9.1)
|
|
60
|
-
has_draft!
|
|
61
|
-
rake
|
|
62
|
-
rspec (~> 2.0)
|
|
63
|
-
sqlite3 (~> 1.3.4)
|
data/README.rdoc
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
{<img src="https://secure.travis-ci.org/rubiety/has_draft.png?branch=master" alt="Build Status" />}[http://travis-ci.org/rubiety/has_draft]
|
|
2
|
-
{<img src="https://gemnasium.com/rubiety/has_draft.png" alt="Dependency Status" />}[https://gemnasium.com/rubiety/has_draft]
|
|
3
|
-
{<img src="https://codeclimate.com/github/rubiety/has_draft.png" />}[https://codeclimate.com/github/rubiety/has_draft]
|
|
4
|
-
|
|
5
|
-
== Has Draft
|
|
6
|
-
|
|
7
|
-
Allows for multiple "drafts" of a model which can be useful when developing:
|
|
8
|
-
* Draft/Live Version of Pages, for examples
|
|
9
|
-
* A workflow system whereby a live copy may need to be active while a draft copy is
|
|
10
|
-
awaiting approval.
|
|
11
|
-
|
|
12
|
-
This was built to be able to be tacked on to existing models, so the data schema doesn't need to change
|
|
13
|
-
at all for the model this is applied to. Drafts are actually stored in a nearly-identical table
|
|
14
|
-
and there is a has_one relationship to this. This separation allows the base model to really be treated
|
|
15
|
-
just as before without having to apply conditions in queries to make sure you are really getting the
|
|
16
|
-
"live" (non-draft) copy: Page.all will still only return the non-draft pages. This separate table is backed by
|
|
17
|
-
a model created on the fly as a constant on the original model class. For example if a Page has_draft,
|
|
18
|
-
a Page::Draft class will exist as the model for the page_drafts table.
|
|
19
|
-
|
|
20
|
-
== Installation
|
|
21
|
-
|
|
22
|
-
This gem requires ActiveRecord 3.0.
|
|
23
|
-
|
|
24
|
-
In your Gemfile:
|
|
25
|
-
|
|
26
|
-
gem "has_draft"
|
|
27
|
-
|
|
28
|
-
== Basic Example
|
|
29
|
-
|
|
30
|
-
## First Migration (If Creating base model and drafts at the same time):
|
|
31
|
-
class InitialSchema < ActiveRecord::Migration
|
|
32
|
-
|
|
33
|
-
[:articles, :article_drafts].each do |table_name|
|
|
34
|
-
create_table table_name, :force => true do |t|
|
|
35
|
-
t.references :article if table_name == :article_drafts
|
|
36
|
-
|
|
37
|
-
t.string :title
|
|
38
|
-
t.text :summary
|
|
39
|
-
t.text :body
|
|
40
|
-
t.date :post_date
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
## Model Class
|
|
48
|
-
class Article < ActiveRecord::Base
|
|
49
|
-
has_draft
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
## Exposed Class Methods & Scopes:
|
|
53
|
-
Article.draft_class
|
|
54
|
-
=> Article::Draft
|
|
55
|
-
Article.with_draft.all
|
|
56
|
-
=> (Articles that have an associated draft)
|
|
57
|
-
Article.without_draft.all
|
|
58
|
-
=> (Articles with no associated draft)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
## Usage Examples:
|
|
63
|
-
|
|
64
|
-
article = Article.create(
|
|
65
|
-
:title => "My Title",
|
|
66
|
-
:summary => "Information here.",
|
|
67
|
-
:body => "Full body",
|
|
68
|
-
:post_date => Date.today
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
article.has_draft?
|
|
72
|
-
=> false
|
|
73
|
-
|
|
74
|
-
article.instantiate_draft!
|
|
75
|
-
|
|
76
|
-
article.has_draft?
|
|
77
|
-
=> true
|
|
78
|
-
|
|
79
|
-
article.draft
|
|
80
|
-
=> Article::Draft Instance
|
|
81
|
-
|
|
82
|
-
article.draft.update_attributes(
|
|
83
|
-
:title => "New Title"
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
article.replace_with_draft!
|
|
87
|
-
|
|
88
|
-
article.title
|
|
89
|
-
=> "New Title"
|
|
90
|
-
|
|
91
|
-
article.destroy_draft!
|
|
92
|
-
|
|
93
|
-
article.has_draft?
|
|
94
|
-
=> false
|
|
95
|
-
|
|
96
|
-
== Custom Options
|
|
97
|
-
|
|
98
|
-
## First Migration (If Creating base model and drafts at the same time):
|
|
99
|
-
class InitialSchema < ActiveRecord::Migration
|
|
100
|
-
|
|
101
|
-
[:articles, :article_copies].each do |table_name|
|
|
102
|
-
create_table table_name, :force => true do |t|
|
|
103
|
-
t.integer :news_article_id if table_name == :article_copies
|
|
104
|
-
|
|
105
|
-
t.string :title
|
|
106
|
-
t.text :summary
|
|
107
|
-
t.text :body
|
|
108
|
-
t.date :post_date
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
## Model Class
|
|
115
|
-
class Article < ActiveRecord::Base
|
|
116
|
-
has_draft :class_name => 'Copy', :foreign_key => :news_article_id, :table_name => 'article_copies'
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
## Single Table Inheritance Example
|
|
120
|
-
class InitialSchema < ActiveRecord::Migration
|
|
121
|
-
|
|
122
|
-
[:elements, :element_drafts].each do |table_name|
|
|
123
|
-
create_table table_name, :force => true do |t|
|
|
124
|
-
t.integer :element_id if table_name == :element_drafts
|
|
125
|
-
|
|
126
|
-
t.string :title
|
|
127
|
-
t.text :content
|
|
128
|
-
t.string :type
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
class Element < ActiveRecord::Base
|
|
135
|
-
class Draft < ActiveRecord::Base
|
|
136
|
-
def element_icon_path
|
|
137
|
-
'assets/images/default.png'
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
module HasDraftCallbacks
|
|
142
|
-
def before_instantiate_draft
|
|
143
|
-
# Need to append ::Draft so that it knows to use the Draft class
|
|
144
|
-
self.draft.type = self.type + '::Draft'
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def before_replace_with_draft
|
|
148
|
-
# We are storing drafts with the ::Draft on the end so we have to strip it
|
|
149
|
-
self.type = self.draft.type.split(':')[0]
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
class BlueElement < Element
|
|
155
|
-
has_draft :belongs_to => :element, :extends => Element::Draft do
|
|
156
|
-
def element_icon_path
|
|
157
|
-
'assets/images/blue.png'
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
include Element::HasDraftCallbacks
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
class UnknownElement < Element
|
|
165
|
-
has_draft :belongs_to => :element, :extends => Element::Draft
|
|
166
|
-
|
|
167
|
-
include Element::HasDraftCallbacks
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
This allows us to call element_draft.element on any subclass of Element::Draft
|
|
171
|
-
instead of needing to know to call element_draft.blue_element or element_draft.unknown_element.
|
|
172
|
-
|
|
173
|
-
Note: calling has_draft will have no effect if the parent class has already executed has_draft.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
== Method Callbacks
|
|
178
|
-
|
|
179
|
-
There are three callbacks you can specify directly as methods:
|
|
180
|
-
|
|
181
|
-
class Article < ActiveRecord::Base
|
|
182
|
-
has_draft
|
|
183
|
-
|
|
184
|
-
def before_instantiate_draft
|
|
185
|
-
# Do Something
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
def before_replace_with_draft
|
|
189
|
-
# Do Something
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def before_destroy_draft
|
|
193
|
-
# Do Something
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
== Extending the Draft Class
|
|
200
|
-
|
|
201
|
-
Because you don't directly define the draft class, you can specify a block of code to be run in its
|
|
202
|
-
context by passing a block to has_draft:
|
|
203
|
-
|
|
204
|
-
class Article < ActiveRecord::Base
|
|
205
|
-
belongs_to :user
|
|
206
|
-
|
|
207
|
-
has_draft do
|
|
208
|
-
belongs_to :last_updated_user
|
|
209
|
-
|
|
210
|
-
def approve!
|
|
211
|
-
self.approved_at = Time.now
|
|
212
|
-
self.save
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
== Running Tests
|
|
219
|
-
|
|
220
|
-
This gem uses appraisal to test with different versions of the dependencies. See Appraisal first for which versions are tested.
|
|
221
|
-
|
|
222
|
-
# Just the gems locked in Gemfile.lock
|
|
223
|
-
$ bundle exec rake test
|
|
224
|
-
|
|
225
|
-
# All of the Appraisals:
|
|
226
|
-
$ bundle exec rake all
|
|
227
|
-
|
data/Rakefile
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
require 'rubygems'
|
|
2
|
-
require 'bundler/setup'
|
|
3
|
-
|
|
4
|
-
require 'rake'
|
|
5
|
-
require 'rspec/core/rake_task'
|
|
6
|
-
require 'appraisal'
|
|
7
|
-
|
|
8
|
-
Bundler::GemHelper.install_tasks
|
|
9
|
-
|
|
10
|
-
desc 'Default: run unit tests.'
|
|
11
|
-
task :default => [:clean, :all]
|
|
12
|
-
|
|
13
|
-
desc "Run Specs against all Appraisals"
|
|
14
|
-
task :all => :spec do
|
|
15
|
-
Rake::Task["appraisal:install"].execute
|
|
16
|
-
system("bundle exec rake -s appraisal spec")
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
desc "Run Specs"
|
|
20
|
-
RSpec::Core::RakeTask.new(:spec) do |t|
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
task :test => :spec
|
|
24
|
-
|
|
25
|
-
desc "Clean up files."
|
|
26
|
-
task :clean do |t|
|
|
27
|
-
FileUtils.rm_rf "tmp"
|
|
28
|
-
Dir.glob("spec/db/*.sqlite3").each {|f| FileUtils.rm f }
|
|
29
|
-
Dir.glob("has_draft-*.gem").each {|f| FileUtils.rm f }
|
|
30
|
-
end
|
data/init.rb
DELETED
data/lib/has_draft/railtie.rb
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
module HasDraft
|
|
2
|
-
if defined?(Rails::Railtie)
|
|
3
|
-
require "rails"
|
|
4
|
-
|
|
5
|
-
class Railtie < Rails::Railtie
|
|
6
|
-
initializer "has_draft.extend_active_record" do
|
|
7
|
-
ActiveSupport.on_load(:active_record) do
|
|
8
|
-
HasDraft::Railtie.insert
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
class Railtie
|
|
15
|
-
def self.insert
|
|
16
|
-
ActiveRecord::Base.send(:include, HasDraft)
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
data/spec/db/database.yml
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
sqlite:
|
|
2
|
-
adapter: sqlite
|
|
3
|
-
database: spec/db/test.sqlite
|
|
4
|
-
|
|
5
|
-
sqlite3:
|
|
6
|
-
adapter: sqlite3
|
|
7
|
-
database: spec/db/test.sqlite3
|
|
8
|
-
|
|
9
|
-
postgresql:
|
|
10
|
-
adapter: postgresql
|
|
11
|
-
username: postgres
|
|
12
|
-
password: postgres
|
|
13
|
-
database: has_draft_plugin_test
|
|
14
|
-
min_messages: ERROR
|
|
15
|
-
|
|
16
|
-
mysql:
|
|
17
|
-
adapter: mysql
|
|
18
|
-
host: localhost
|
|
19
|
-
username: root
|
|
20
|
-
password:
|
|
21
|
-
database: has_draft_plugin_test
|
data/spec/db/schema.rb
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
ActiveRecord::Schema.define(:version => 0) do
|
|
2
|
-
|
|
3
|
-
[:articles, :article_drafts].each do |table_name|
|
|
4
|
-
create_table table_name, :force => true do |t|
|
|
5
|
-
t.references :article if table_name == :article_drafts
|
|
6
|
-
|
|
7
|
-
t.string :title
|
|
8
|
-
t.text :summary
|
|
9
|
-
t.text :body
|
|
10
|
-
t.date :post_date
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
[:posts, :post_drafts].each do |table_name|
|
|
15
|
-
create_table table_name, :force => true do |t|
|
|
16
|
-
t.references :post if table_name == :post_drafts
|
|
17
|
-
|
|
18
|
-
t.string :title
|
|
19
|
-
t.text :body
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
[:pages, :page_drafts].each do |table_name|
|
|
24
|
-
create_table table_name, :force => true do |t|
|
|
25
|
-
t.references :page if table_name == :page_drafts
|
|
26
|
-
|
|
27
|
-
t.string :title
|
|
28
|
-
t.text :body
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
end
|
data/spec/db/test.sqlite3
DELETED
|
Binary file
|
data/spec/factories/article.rb
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
require "factory_girl"
|
|
2
|
-
|
|
3
|
-
Factory.define(:article) do |f|
|
|
4
|
-
f.title { Faker::Lorem.sentence }
|
|
5
|
-
f.summary { Faker::Lorem.paragraphs.first }
|
|
6
|
-
f.body { Faker::Lorem.paragraphs.join("\n\n") }
|
|
7
|
-
f.post_date { 1.day.ago.to_date }
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
Factory.define(:article_draft, :class => Article::Draft) do |f|
|
|
11
|
-
f.association(:article)
|
|
12
|
-
f.title { Faker::Lorem.sentence }
|
|
13
|
-
f.summary { Faker::Lorem.paragraphs.first }
|
|
14
|
-
f.body { Faker::Lorem.paragraphs.join("\n\n") }
|
|
15
|
-
f.post_date { 1.day.ago.to_date }
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
Factory.define(:article_with_draft, :parent => :article) do |f|
|
|
19
|
-
f.association(:draft, :factory => :article_draft)
|
|
20
|
-
end
|
data/spec/has_draft_spec.rb
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
require "spec_helper"
|
|
2
|
-
|
|
3
|
-
describe HasDraft do
|
|
4
|
-
before(:all) do
|
|
5
|
-
ActiveSupport::Deprecation.silenced = true
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
context "Model with has_draft" do
|
|
9
|
-
it "should expose #draft_class_name as Draft" do
|
|
10
|
-
Article.draft_class_name.should == "Draft"
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
it "should expose #draft_class as Article::Draft" do
|
|
14
|
-
Article.draft_class.should == Article::Draft
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
it "should default #draft_foreign_key as article_id" do
|
|
18
|
-
Article.draft_foreign_key.should == "article_id"
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
it "should default #draft_table_name as article_drafts" do
|
|
22
|
-
Article.draft_table_name.should == "article_drafts"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
describe "Draft Model" do
|
|
26
|
-
it "should be defined under the Article namespace" do
|
|
27
|
-
Article.constants.map(&:to_s).should include('Draft')
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
it "should expose #original_class as Article" do
|
|
31
|
-
Article::Draft.original_class.should == Article
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
context "an article" do
|
|
37
|
-
before { @article = Factory.create(:article) }
|
|
38
|
-
|
|
39
|
-
context "when instantiating a new draft" do
|
|
40
|
-
before { @article.instantiate_draft! }
|
|
41
|
-
|
|
42
|
-
it "should create draft" do
|
|
43
|
-
@article.draft.should_not be_nil
|
|
44
|
-
@article.draft.should_not be_new_record
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
it "should copy draft fields" do
|
|
48
|
-
Article.draft_columns.each do |column|
|
|
49
|
-
@article.send(column).should == @article.draft.send(column)
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
context "when destroying an existing draft" do
|
|
55
|
-
before do
|
|
56
|
-
@article = Factory.create(:article_with_draft)
|
|
57
|
-
@article.destroy_draft!
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
it "should associated draft" do
|
|
61
|
-
@article.draft.should be_nil
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
context "when replacing with draft" do
|
|
66
|
-
before do
|
|
67
|
-
@article = Factory.create(:article_with_draft)
|
|
68
|
-
@article.replace_with_draft!
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
it "should still have draft" do
|
|
72
|
-
@article.draft.should_not be_nil
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
it "should now have the same field values as draft" do
|
|
76
|
-
Article.draft_columns.each do |column|
|
|
77
|
-
@article.send(column).should == @article.draft.send(column)
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
context "Draft class extends" do
|
|
84
|
-
|
|
85
|
-
it "ActiveRecord::Base when no options are passed" do
|
|
86
|
-
Article::Draft.superclass.should == ActiveRecord::Base
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
it "the class passed in with the :extends option" do
|
|
90
|
-
BlogPost::Draft.superclass.should == Post
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
context "Draft class belongs to" do
|
|
95
|
-
|
|
96
|
-
it "self when :belongs_to is not specified" do
|
|
97
|
-
Article::Draft.reflect_on_association(:article).macro.should == :belongs_to
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
it "the model passed in with the :belongs_to option" do
|
|
101
|
-
BlogPost::Draft.reflect_on_association(:post).macro.should == :belongs_to
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
end
|
data/spec/spec_helper.rb
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
require "rubygems"
|
|
2
|
-
require "rspec"
|
|
3
|
-
require "factory_girl"
|
|
4
|
-
require "faker"
|
|
5
|
-
require "active_record"
|
|
6
|
-
require "active_support"
|
|
7
|
-
|
|
8
|
-
# Establish DB Connection
|
|
9
|
-
config = YAML::load(IO.read(File.join(File.dirname(__FILE__), 'db', 'database.yml')))
|
|
10
|
-
ActiveRecord::Base.configurations = {'test' => config[ENV['DB'] || 'sqlite3']}
|
|
11
|
-
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
|
|
12
|
-
|
|
13
|
-
# Load Test Schema into the Database
|
|
14
|
-
load(File.dirname(__FILE__) + "/db/schema.rb")
|
|
15
|
-
|
|
16
|
-
require File.dirname(__FILE__) + '/../init'
|
|
17
|
-
|
|
18
|
-
# Example has_draft Model:
|
|
19
|
-
class Article < ActiveRecord::Base
|
|
20
|
-
has_draft
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# Empty class to test ability of Draft class to extend other classes
|
|
24
|
-
class Post < ActiveRecord::Base
|
|
25
|
-
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Model for testing ability to have the draft extend a different class
|
|
29
|
-
class BlogPost < Post
|
|
30
|
-
has_draft :extends => Post, :belongs_to => :post
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Load Factories:
|
|
34
|
-
Dir[File.join(File.dirname(__FILE__), "factories/**/*.rb")].each {|f| require f}
|