has_draft 1.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 43802ee528c537f22d4b0996caa82708366eca1a
4
- data.tar.gz: 237e44c17d24fe2aa960a34123fb1f2b8e6637d9
2
+ SHA256:
3
+ metadata.gz: 29d15db9d31975799a93a854e620a005c6358befe684fdb7e1d5766324463d33
4
+ data.tar.gz: 85e9962ebe1ad2d7f76afe2a5b2be0f94cc2fb26d188ea746d0eed6330e66cd2
5
5
  SHA512:
6
- metadata.gz: ab07707560bffdecf87a35b439f783a7a53ded60a733b22423e55eabd7a221ec947f787785e71c5d3731a7fdadbdcf1401b084b3e1a52061b043d026e95cdf1a
7
- data.tar.gz: f6ebd0abcf1390d97c26109f397ee209a24bd1b3d78cd12a03264bd3e6a31c74d41b66b27d9ffbfd4ce3a4f8b8919336cb841a754400d76a37910d3bd71f4b95
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/README.md ADDED
@@ -0,0 +1,213 @@
1
+ # has_draft
2
+
3
+ [![CI](https://github.com/eigenben/has_draft/actions/workflows/ci.yml/badge.svg)](https://github.com/eigenben/has_draft/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/has_draft.svg)](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
@@ -1,3 +1,3 @@
1
1
  module HasDraft
2
- VERSION = "1.4.0"
2
+ VERSION = "2.0.0"
3
3
  end
data/lib/has_draft.rb CHANGED
@@ -1,31 +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
- cattr_accessor :draft_class_name, :draft_foreign_key, :draft_table_name, :draft_columns
12
-
13
- self.draft_class_name = options[:class_name] || 'Draft'
14
- self.draft_foreign_key = options[:foreign_key] || self.to_s.foreign_key
15
- self.draft_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_drafts#{table_name_suffix}"
14
+
15
+ class_attribute :draft_class_name, :draft_foreign_key, :draft_table_name, :instance_writer => false
16
16
 
17
17
  # Default parent association
18
- options[:belongs_to] = self.to_s.demodulize.underscore.to_sym if options[:belongs_to].nil?
18
+ options[:belongs_to] = self.name.demodulize.underscore.to_sym if options[:belongs_to].nil?
19
+
20
+ self.draft_class_name = options[:class_name] || 'Draft'
21
+ self.draft_foreign_key = (options[:foreign_key] || "#{options[:belongs_to]}_id").to_s
22
+ self.draft_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_drafts#{table_name_suffix}"
19
23
 
20
24
  # Create Relationship to Draft Copy
21
25
  class_eval do
22
- has_one :draft, :class_name => "#{self.to_s}::#{draft_class_name}",
26
+ has_one :draft, :class_name => "#{self.name}::#{draft_class_name}",
23
27
  :foreign_key => draft_foreign_key,
24
28
  :dependent => :destroy,
25
29
  :inverse_of => options[:belongs_to]
26
30
 
27
- scope :with_draft, lambda { includes(:draft).where("#{draft_table_name}.id IS NOT NULL") }
28
- scope :without_draft, lambda { includes(:draft).where("#{draft_table_name}.id IS NULL") }
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 }) }
29
33
  end
30
34
 
31
35
  # Default parent class to ActiveRecord::Base
@@ -33,95 +37,102 @@ module HasDraft
33
37
 
34
38
  # Dynamically Create Model::Draft Class
35
39
  const_set(draft_class_name, Class.new(options[:extends]))
36
-
37
- draft_class.cattr_accessor :original_class
40
+
41
+ draft_class.class_attribute :original_class, :instance_writer => false
38
42
  draft_class.original_class = self
39
43
  draft_class.table_name = draft_table_name
40
44
 
41
45
  # Draft Parent Association
42
- draft_class.belongs_to options[:belongs_to], :class_name => "::#{self.to_s}", :foreign_key => draft_foreign_key, :inverse_of => :draft
46
+ draft_class.belongs_to options[:belongs_to], :class_name => "::#{self.name}", :foreign_key => draft_foreign_key, :inverse_of => :draft
43
47
 
44
48
  # Block extension
45
49
  draft_class.class_eval(&block) if block_given?
46
-
47
- # Finally setup which columns to draft
48
- self.draft_columns = draft_class.new.attributes.keys - [draft_class.primary_key, draft_foreign_key, 'created_at', 'updated_at', inheritance_column]
49
50
  end
50
-
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
+
51
58
  def draft_class
52
59
  const_get(draft_class_name)
53
60
  end
54
61
  end
55
-
62
+
56
63
  module InstanceMethods
57
64
  def has_draft?
58
65
  !self.draft.nil?
59
66
  end
60
-
67
+
61
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?
62
70
  return false if perform_validation and !self.valid?
63
-
64
- instantiate_draft! unless has_draft?
65
- copy_attributes_to_draft
66
-
67
- 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)
68
75
  self.reload
69
76
  end
70
-
77
+
78
+ # Builds a draft copy of the record without saving it.
71
79
  def instantiate_draft
72
80
  self.build_draft
73
-
81
+
74
82
  copy_attributes_to_draft
75
83
  before_instantiate_draft
76
-
77
- self.draft.tap(&:save)
84
+
85
+ self.draft
78
86
  end
79
-
87
+
88
+ # Builds and saves a draft copy of the record. On a new record, the draft is saved along with it.
80
89
  def instantiate_draft!
81
- instantiate_draft do |draft|
90
+ instantiate_draft.tap do |draft|
82
91
  draft.save unless self.new_record?
83
92
  end
84
93
  end
85
-
94
+
86
95
  def copy_attributes_to_draft
87
96
  self.class.draft_columns.each do |attribute|
88
97
  self.draft.send("#{attribute}=", send(attribute))
89
98
  end
90
99
  self
91
100
  end
92
-
101
+
93
102
  def copy_attributes_from_draft
94
103
  self.class.draft_columns.each do |attribute|
95
104
  self.send("#{attribute}=", self.draft.send(attribute))
96
105
  end
97
106
  self
98
107
  end
99
-
108
+
100
109
  def before_instantiate_draft
101
110
  end
102
-
111
+
103
112
  def replace_with_draft!
104
113
  copy_attributes_from_draft
105
-
114
+
106
115
  before_replace_with_draft
107
-
116
+
108
117
  self.save unless self.new_record?
109
118
  self
110
119
  end
111
-
120
+
112
121
  def before_replace_with_draft
113
122
  end
114
-
123
+
115
124
  def destroy_draft!
116
125
  before_destroy_draft
117
-
126
+
118
127
  self.draft.destroy if self.draft
119
- self.draft(true)
128
+ self.reload_draft
120
129
  end
121
-
130
+
122
131
  def before_destroy_draft
123
132
  end
124
133
  end
125
134
  end
126
135
 
127
- require "has_draft/railtie"
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: 1.4.0
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: 2018-12-11 00:00:00.000000000 Z
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: 3.0.0
18
+ version: '6.1'
34
19
  type: :runtime
35
20
  prerelease: false
36
21
  version_requirements: !ruby/object:Gem::Requirement
37
22
  requirements:
38
23
  - - ">="
39
24
  - !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
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: 0.9.1
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: 10.1.0
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - "~>"
123
- - !ruby/object:Gem::Version
124
- version: 10.1.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,27 +30,18 @@ executables: []
129
30
  extensions: []
130
31
  extra_rdoc_files: []
131
32
  files:
132
- - Appraisals
133
- - Gemfile
134
- - Gemfile.lock
33
+ - CHANGELOG.md
135
34
  - LICENSE
136
- - README.rdoc
137
- - Rakefile
138
- - init.rb
35
+ - README.md
139
36
  - lib/has_draft.rb
140
- - lib/has_draft/railtie.rb
141
37
  - lib/has_draft/version.rb
142
- - spec/db/database.yml
143
- - spec/db/schema.rb
144
- - spec/db/test.sqlite3
145
- - spec/factories/article.rb
146
- - spec/has_draft_spec.rb
147
- - spec/spec_helper.rb
148
- homepage: http://github.com/rubiety/has_draft
38
+ homepage: https://github.com/eigenben/has_draft
149
39
  licenses:
150
40
  - MIT
151
- metadata: {}
152
- post_install_message:
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
153
45
  rdoc_options: []
154
46
  require_paths:
155
47
  - lib
@@ -157,17 +49,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
157
49
  requirements:
158
50
  - - ">="
159
51
  - !ruby/object:Gem::Version
160
- version: '0'
52
+ version: '3.0'
161
53
  required_rubygems_version: !ruby/object:Gem::Requirement
162
54
  requirements:
163
55
  - - ">="
164
56
  - !ruby/object:Gem::Version
165
- version: 1.3.4
57
+ version: '0'
166
58
  requirements: []
167
- rubyforge_project: has_draft
168
- rubygems_version: 2.5.1
169
- signing_key:
59
+ rubygems_version: 3.6.7
170
60
  specification_version: 4
171
- summary: Attached draft model to your ActiveRecord models.
61
+ summary: Attach drafts to your ActiveRecord models.
172
62
  test_files: []
173
- has_rdoc:
data/Appraisals DELETED
@@ -1,23 +0,0 @@
1
- appraise "3.0" do
2
- gem "rails", "~> 3.0.12"
3
- end
4
-
5
- appraise "3.1" do
6
- gem "rails", "~> 3.1.4"
7
- gem "i18n", "~> 0.6.0"
8
- gem "rack-cache", "~> 1.2.0"
9
- end
10
-
11
- appraise "3.2" do
12
- gem "rails", "~> 3.2.2"
13
- gem "i18n", "~> 0.6.0"
14
- gem "rack-cache", "~> 1.2.0"
15
- end
16
-
17
- appraise "4.0" do
18
- gem "rails", "~> 4.0.0"
19
- end
20
-
21
- appraise "5.0" do
22
- gem "rails", "~> 5.0.0"
23
- end
data/Gemfile DELETED
@@ -1,3 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- gemspec
data/Gemfile.lock DELETED
@@ -1,64 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- has_draft (1.4.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.3)
12
- activesupport (= 4.0.3)
13
- builder (~> 3.1.0)
14
- activerecord (4.0.3)
15
- activemodel (= 4.0.3)
16
- activerecord-deprecated_finders (~> 1.0.2)
17
- activesupport (= 4.0.3)
18
- arel (~> 4.0.0)
19
- activerecord-deprecated_finders (1.0.4)
20
- activesupport (4.0.3)
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.2)
30
- builder (3.1.4)
31
- diff-lcs (1.2.4)
32
- factory_girl (1.3.3)
33
- faker (0.9.5)
34
- i18n (~> 0.4)
35
- i18n (0.6.5)
36
- minitest (4.7.5)
37
- multi_json (1.13.1)
38
- rake (10.1.0)
39
- rspec (2.14.1)
40
- rspec-core (~> 2.14.0)
41
- rspec-expectations (~> 2.14.0)
42
- rspec-mocks (~> 2.14.0)
43
- rspec-core (2.14.5)
44
- rspec-expectations (2.14.3)
45
- diff-lcs (>= 1.1.3, < 2.0)
46
- rspec-mocks (2.14.3)
47
- sqlite3 (1.3.13)
48
- thread_safe (0.3.6)
49
- tzinfo (0.3.54)
50
-
51
- PLATFORMS
52
- ruby
53
-
54
- DEPENDENCIES
55
- appraisal (~> 0.4.0)
56
- factory_girl (~> 1.3.1)
57
- faker (~> 0.9.1)
58
- has_draft!
59
- rake (~> 10.1.0)
60
- rspec (~> 2.0)
61
- sqlite3 (~> 1.3.4)
62
-
63
- BUNDLED WITH
64
- 1.17.1
data/README.rdoc DELETED
@@ -1,227 +0,0 @@
1
- {<img src="https://secure.travis-ci.org/rubiety/has_draft.svg?branch=master" alt="Build Status" />}[http://travis-ci.org/rubiety/has_draft]
2
- {<img src="https://gemnasium.com/rubiety/has_draft.svg" alt="Dependency Status" />}[https://gemnasium.com/rubiety/has_draft]
3
- {<img src="https://codeclimate.com/github/rubiety/has_draft.svg" />}[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 supports ActiveRecord 3, 4, and 5.
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
@@ -1,3 +0,0 @@
1
- require "has_draft"
2
-
3
- HasDraft::Railtie.insert
@@ -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
@@ -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
@@ -1,111 +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
-
53
- it "should set the associations as inverses of each other" do
54
- @article.draft.article.should be @article
55
- article_draft = Article::Draft.find(@article.draft.id)
56
- article_draft.article.draft.should be article_draft
57
- end
58
- end
59
-
60
- context "when destroying an existing draft" do
61
- before do
62
- @article = Factory.create(:article_with_draft)
63
- @article.destroy_draft!
64
- end
65
-
66
- it "should associated draft" do
67
- @article.draft.should be_nil
68
- end
69
- end
70
-
71
- context "when replacing with draft" do
72
- before do
73
- @article = Factory.create(:article_with_draft)
74
- @article.replace_with_draft!
75
- end
76
-
77
- it "should still have draft" do
78
- @article.draft.should_not be_nil
79
- end
80
-
81
- it "should now have the same field values as draft" do
82
- Article.draft_columns.each do |column|
83
- @article.send(column).should == @article.draft.send(column)
84
- end
85
- end
86
- end
87
- end
88
-
89
- context "Draft class extends" do
90
-
91
- it "ActiveRecord::Base when no options are passed" do
92
- Article::Draft.superclass.should == ActiveRecord::Base
93
- end
94
-
95
- it "the class passed in with the :extends option" do
96
- BlogPost::Draft.superclass.should == Post
97
- end
98
- end
99
-
100
- context "Draft class belongs to" do
101
-
102
- it "self when :belongs_to is not specified" do
103
- Article::Draft.reflect_on_association(:article).macro.should == :belongs_to
104
- end
105
-
106
- it "the model passed in with the :belongs_to option" do
107
- BlogPost::Draft.reflect_on_association(:post).macro.should == :belongs_to
108
- end
109
- end
110
-
111
- 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}