conscript 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +133 -0
- data/Rakefile +5 -0
- data/conscript.gemspec +27 -0
- data/lib/conscript.rb +6 -0
- data/lib/conscript/exception/already_draft.rb +6 -0
- data/lib/conscript/exception/not_a_draft.rb +6 -0
- data/lib/conscript/orm/activerecord.rb +93 -0
- data/lib/conscript/version.rb +3 -0
- data/spec/conscript/orm/activerecord_spec.rb +354 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/support/thingy.rb +3 -0
- data/spec/support/widget.rb +3 -0
- data/spec/support/widget_migration.rb +22 -0
- metadata +134 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Steve Lorek
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
# Conscript
|
2
|
+
|
3
|
+
Provides ActiveRecord models with a `drafts` scope, and the functionality to create draft instances and publish them.
|
4
|
+
|
5
|
+
Existing instances may have one or more draft versions which are initially created by duplication, including any required associations. A draft may then be published, overwriting the original instance.
|
6
|
+
|
7
|
+
Alternatively, draft instances may be created from scratch and published later.
|
8
|
+
|
9
|
+
The approach of the gem differs from others in that it does not create extra tables or serialise objects; instead it uses ActiveRecord's built-in scoping so that drafts have all of the same functionality as any other instance.
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
gem 'conscript'
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install conscript
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
All models which wish to register for the draft must add two columns to their database schema, as illustrated in this example migration:
|
28
|
+
|
29
|
+
class AddDraftColumns < ActiveRecord::Migration
|
30
|
+
def self.up
|
31
|
+
add_column :table_name, :draft_parent_id, :integer
|
32
|
+
add_column :table_name, :is_draft, :boolean, default: false
|
33
|
+
add_index :table_name, :draft_parent_id
|
34
|
+
add_index :table_name, :is_draft
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.down
|
38
|
+
remove_column :table_name, :draft_parent_id
|
39
|
+
remove_column :table_name, :is_draft
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
To use the drafts functionality, call `register_for_draft` in your ActiveRecord model:
|
44
|
+
|
45
|
+
class Article < ActiveRecord::Base
|
46
|
+
register_for_draft
|
47
|
+
end
|
48
|
+
|
49
|
+
A `default_scope` is then set to exclude all draft instances from finders. You may use the `drafts` scope to access them:
|
50
|
+
|
51
|
+
Article.drafts
|
52
|
+
|
53
|
+
If you need to access drafts and original instances together, use `unscoped` as you would any other `default_scope`:
|
54
|
+
|
55
|
+
Article.unscoped.all
|
56
|
+
|
57
|
+
### Instance methods
|
58
|
+
|
59
|
+
Draft instances may optionally be created from existing instances:
|
60
|
+
|
61
|
+
article = Article.first
|
62
|
+
draft = article.save_as_draft!
|
63
|
+
|
64
|
+
Or you can create drafts from scratch:
|
65
|
+
|
66
|
+
Article.new.save_as_draft!
|
67
|
+
|
68
|
+
You can access the original instance from the draft:
|
69
|
+
|
70
|
+
draft.draft_parent == article # => true
|
71
|
+
|
72
|
+
And you can also access all of an instance's drafts:
|
73
|
+
|
74
|
+
article.drafts # => [draft]
|
75
|
+
|
76
|
+
To determine whether an instance is a draft:
|
77
|
+
|
78
|
+
article.is_draft? # => false
|
79
|
+
draft.is_draft? # => true
|
80
|
+
|
81
|
+
Publish a draft to overwrite the original instance:
|
82
|
+
|
83
|
+
draft.publish_draft # returns the original, updated instance
|
84
|
+
|
85
|
+
|
86
|
+
### Options
|
87
|
+
|
88
|
+
By default, drafts are created with `ActiveRecord::Base#dup`, i.e. a shallow copy of all attributes, without any associations.
|
89
|
+
|
90
|
+
Options may be passed to the `register_for_draft` method, e.g:
|
91
|
+
|
92
|
+
register_for_draft associations: :tags, ignore_attributes: :cached_slug
|
93
|
+
|
94
|
+
A list of options are below:
|
95
|
+
|
96
|
+
- `:associations` an array of `has_many` association names to duplicate. These will be copied to the draft and overwrite the original instance's when published. Deep cloning is possible thanks to the [`deep_cloneable`](https://github.com/moiristo/deep_cloneable) gem. Refer to the `deep_cloneable` documentation to get an idea of how far you can go with this. Please note: `belongs_to` associations aren't supported as these should be drafted separately.
|
97
|
+
- `:ignore_attributes` an array of attribute names which should _not_ be duplicated. Timestamps and STI `type` columns are excluded by default. Don't include association names here.
|
98
|
+
|
99
|
+
|
100
|
+
### Using with CarrierWave
|
101
|
+
|
102
|
+
Conscript supports [CarrierWave](https://github.com/carrierwaveuploader/carrierwave) uploads, but there's a couple of things you should be aware of.
|
103
|
+
|
104
|
+
First, you must ensure `register_for_draft` is called _after_ any calls to `mount_uploader`.
|
105
|
+
|
106
|
+
Then, in your uploaders where `store_dir` is defined, if you are organising file storage by model instance (e.g. `#to_param`) then you should use the new model method `uploader_store_param` to define the unique location, e.g:
|
107
|
+
|
108
|
+
class ArticleImageUploader < ImageUploader
|
109
|
+
def store_dir
|
110
|
+
"public/images/articles/#{model.uploader_store_param}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
This will result in uploads for drafts being stored in the same location as the original instance. This is because Conscript does not want to have to worry about moving files when publishing an instance.
|
115
|
+
|
116
|
+
Conscript also overrides CarrierWave's `#destroy` callbacks to ensure that no other instance is using the same file before deleting it from the filesystem. Otherwise this can happen when you delete a draft with the same file as the original instance.
|
117
|
+
|
118
|
+
|
119
|
+
### Limitations
|
120
|
+
|
121
|
+
For reasons of sanity:
|
122
|
+
|
123
|
+
- You cannot make changes to an instance if it has drafts, this is because it would be difficult to propogate those changes down or provide visibility of the changes.
|
124
|
+
- When you publish a draft, any other drafts for the same `draft_parent` are also destroyed.
|
125
|
+
|
126
|
+
|
127
|
+
## Contributing
|
128
|
+
|
129
|
+
1. Fork it
|
130
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
131
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
132
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
133
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/conscript.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'conscript/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "conscript"
|
8
|
+
spec.version = Conscript::VERSION
|
9
|
+
spec.authors = ["Steve Lorek"]
|
10
|
+
spec.email = ["steve@stevelorek.com"]
|
11
|
+
spec.description = %q{Provides ActiveRecord models with draft instances, including associations}
|
12
|
+
spec.summary = %q{Provides ActiveRecord models with draft instances, including associations}
|
13
|
+
spec.homepage = "http://github.com/slorek/conscript"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3.5"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
spec.add_development_dependency "sqlite3"
|
25
|
+
spec.add_runtime_dependency "activerecord", "~> 3.2.13"
|
26
|
+
spec.add_runtime_dependency "deep_cloneable", "~> 1.5.2"
|
27
|
+
end
|
data/lib/conscript.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'deep_cloneable'
|
3
|
+
require 'conscript/exception/already_draft'
|
4
|
+
require 'conscript/exception/not_a_draft'
|
5
|
+
|
6
|
+
module Conscript
|
7
|
+
module ActiveRecord
|
8
|
+
def register_for_draft(options = {})
|
9
|
+
|
10
|
+
cattr_accessor :conscript_options, :instance_accessor => false do
|
11
|
+
{
|
12
|
+
associations: [],
|
13
|
+
ignore_attributes: [self.primary_key, 'type', 'created_at', 'updated_at', 'draft_parent_id', 'is_draft']
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
self.conscript_options.each_pair {|key, value| self.conscript_options[key] = Array(value) | Array(options[key]) }
|
18
|
+
self.conscript_options[:associations].map!(&:to_sym)
|
19
|
+
self.conscript_options[:ignore_attributes].map!(&:to_s)
|
20
|
+
|
21
|
+
default_scope { where(is_draft: false) }
|
22
|
+
|
23
|
+
belongs_to :draft_parent, class_name: self
|
24
|
+
has_many :drafts, conditions: {is_draft: true}, class_name: self, foreign_key: :draft_parent_id, dependent: :destroy, inverse_of: :draft_parent
|
25
|
+
|
26
|
+
before_save :check_no_drafts_exist
|
27
|
+
|
28
|
+
# Prevent deleting CarrierWave uploads which may be used by other instances. Uploaders must be mounted beforehand.
|
29
|
+
if self.respond_to? :uploaders
|
30
|
+
self.uploaders.keys.each {|attribute| skip_callback :commit, :after, :"remove_#{attribute}!" }
|
31
|
+
after_commit :clean_uploaded_files_for_draft!, :on => :destroy
|
32
|
+
end
|
33
|
+
|
34
|
+
class_eval <<-RUBY
|
35
|
+
def self.drafts
|
36
|
+
where(is_draft: true)
|
37
|
+
end
|
38
|
+
|
39
|
+
def save_as_draft!
|
40
|
+
raise Conscript::Exception::AlreadyDraft if is_draft?
|
41
|
+
draft = new_record? ? self : dup(include: self.class.conscript_options[:associations])
|
42
|
+
draft.is_draft = true
|
43
|
+
draft.draft_parent = self unless new_record?
|
44
|
+
draft.save!
|
45
|
+
draft
|
46
|
+
end
|
47
|
+
|
48
|
+
def publish_draft
|
49
|
+
raise Conscript::Exception::NotADraft unless is_draft?
|
50
|
+
return self.update_attribute(:is_draft, false) if !draft_parent_id
|
51
|
+
::ActiveRecord::Base.transaction do
|
52
|
+
draft_parent.assign_attributes attributes_to_publish, without_protection: true
|
53
|
+
|
54
|
+
self.class.conscript_options[:associations].each do |association|
|
55
|
+
case reflections[association].macro
|
56
|
+
when :has_many
|
57
|
+
draft_parent.send(association.to_s + "=", self.send(association).collect {|child| child.dup })
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
draft_parent.drafts.destroy_all
|
62
|
+
draft_parent.save!
|
63
|
+
end
|
64
|
+
draft_parent
|
65
|
+
end
|
66
|
+
|
67
|
+
def uploader_store_param
|
68
|
+
draft_parent_id.nil? ? to_param : draft_parent.to_param
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
def check_no_drafts_exist
|
73
|
+
drafts.count == 0
|
74
|
+
end
|
75
|
+
|
76
|
+
def attributes_to_publish
|
77
|
+
attributes.reject {|attribute| self.class.conscript_options[:ignore_attributes].include?(attribute) }
|
78
|
+
end
|
79
|
+
|
80
|
+
# Clean up CarrierWave uploads if there are no other instances using the files.
|
81
|
+
#
|
82
|
+
def clean_uploaded_files_for_draft!
|
83
|
+
self.class.uploaders.keys.each do |attribute|
|
84
|
+
filename = attributes[attribute.to_s]
|
85
|
+
self.send("remove_" + attribute.to_s + "!") if !draft_parent_id or draft_parent.drafts.where(attribute => filename).count == 0
|
86
|
+
end
|
87
|
+
end
|
88
|
+
RUBY
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
ActiveRecord::Base.extend Conscript::ActiveRecord
|
@@ -0,0 +1,354 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Conscript::ActiveRecord do
|
4
|
+
|
5
|
+
before(:all) { WidgetMigration.up }
|
6
|
+
after(:all) { WidgetMigration.down }
|
7
|
+
after { Widget.unscoped.delete_all }
|
8
|
+
|
9
|
+
describe "#register_for_draft" do
|
10
|
+
it "is defined on subclasses of ActiveRecord::Base" do
|
11
|
+
Widget.respond_to?(:register_for_draft).should == true
|
12
|
+
end
|
13
|
+
|
14
|
+
it "creates the default scope" do
|
15
|
+
Widget.should_receive(:default_scope).once
|
16
|
+
Widget.register_for_draft
|
17
|
+
end
|
18
|
+
|
19
|
+
it "creates a belongs_to association" do
|
20
|
+
Widget.should_receive(:belongs_to).once.with(:draft_parent, kind_of(Hash))
|
21
|
+
Widget.register_for_draft
|
22
|
+
end
|
23
|
+
|
24
|
+
it "creates a has_many association" do
|
25
|
+
Widget.should_receive(:has_many).once.with(:drafts, kind_of(Hash))
|
26
|
+
Widget.register_for_draft
|
27
|
+
end
|
28
|
+
|
29
|
+
it "creates a before_save callback" do
|
30
|
+
Widget.should_receive(:before_save).once.with(:check_no_drafts_exist)
|
31
|
+
Widget.register_for_draft
|
32
|
+
end
|
33
|
+
|
34
|
+
it "accepts options and merges them with defaults" do
|
35
|
+
Widget.register_for_draft(associations: :owners, ignore_attributes: :custom_attribute)
|
36
|
+
Widget.conscript_options[:associations].should == [:owners]
|
37
|
+
Widget.conscript_options[:ignore_attributes].should == ["id", "type", "created_at", "updated_at", "draft_parent_id", "is_draft", "custom_attribute"]
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "CarrierWave compatibility" do
|
41
|
+
context "where no uploaders are defined on the class" do
|
42
|
+
it "does not try to skip callbacks" do
|
43
|
+
Widget.should_not_receive :skip_callback
|
44
|
+
Widget.register_for_draft
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "where uploaders are defined on the class" do
|
49
|
+
before do
|
50
|
+
Widget.cattr_accessor :uploaders
|
51
|
+
Widget.uploaders = {file: nil}
|
52
|
+
end
|
53
|
+
|
54
|
+
it "disables the provided remove_#attribute callback behaviour" do
|
55
|
+
Widget.should_receive(:skip_callback).with(:commit, :after, :remove_file!)
|
56
|
+
Widget.register_for_draft
|
57
|
+
end
|
58
|
+
|
59
|
+
it "registers a callback to #clean_uploaded_files_for_draft" do
|
60
|
+
Widget.should_receive(:after_commit).with(:clean_uploaded_files_for_draft!, :on => :destroy)
|
61
|
+
Widget.register_for_draft
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "#drafts" do
|
68
|
+
it "limits results to drafts" do
|
69
|
+
Widget.register_for_draft
|
70
|
+
Widget.should_receive(:where).once.with(is_draft: true)
|
71
|
+
Widget.drafts
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "#check_no_drafts_exist" do
|
76
|
+
before do
|
77
|
+
Widget.register_for_draft
|
78
|
+
@subject = Widget.new
|
79
|
+
end
|
80
|
+
|
81
|
+
context "when no drafts exist" do
|
82
|
+
before do
|
83
|
+
@subject.stub_chain(:drafts, :count).and_return(0)
|
84
|
+
end
|
85
|
+
|
86
|
+
it "returns true" do
|
87
|
+
@subject.send(:check_no_drafts_exist).should == true
|
88
|
+
end
|
89
|
+
end
|
90
|
+
context "when drafts exist" do
|
91
|
+
before do
|
92
|
+
@subject.stub_chain(:drafts, :count).and_return(1)
|
93
|
+
end
|
94
|
+
|
95
|
+
it "returns false" do
|
96
|
+
@subject.send(:check_no_drafts_exist).should == false
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe "#clean_uploaded_files_for_draft!" do
|
102
|
+
before do
|
103
|
+
Widget.cattr_accessor :uploaders
|
104
|
+
Widget.uploaders = {file: nil}
|
105
|
+
Widget.register_for_draft
|
106
|
+
end
|
107
|
+
|
108
|
+
context "where files are not shared with any other instances" do
|
109
|
+
before do
|
110
|
+
@original = Widget.create(file: 'test.jpg')
|
111
|
+
@duplicate = @original.save_as_draft!
|
112
|
+
@duplicate.file = 'another_file.jpg'
|
113
|
+
@duplicate.save
|
114
|
+
@original.reload
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should not attempt to remove the file" do
|
118
|
+
@duplicate.should_receive(:remove_file!)
|
119
|
+
@duplicate.destroy
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context "where files are shared with other instances" do
|
124
|
+
before do
|
125
|
+
@original = Widget.create(file: 'test.jpg')
|
126
|
+
@duplicate = @original.save_as_draft!
|
127
|
+
@duplicate.file.should == 'test.jpg'
|
128
|
+
@original.reload
|
129
|
+
end
|
130
|
+
|
131
|
+
it "should not attempt to remove the file" do
|
132
|
+
@duplicate.should_not_receive(:remove_file!)
|
133
|
+
@duplicate.destroy
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "#save_as_draft!" do
|
139
|
+
before { Widget.register_for_draft }
|
140
|
+
|
141
|
+
context "where the instance is a draft" do
|
142
|
+
it "raises an exception" do
|
143
|
+
-> { Widget.new(is_draft: true).save_as_draft! }.should raise_error Conscript::Exception::AlreadyDraft
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
context "where the instance is not a draft" do
|
148
|
+
before do
|
149
|
+
@subject = Widget.new
|
150
|
+
end
|
151
|
+
|
152
|
+
context "and is a new record" do
|
153
|
+
before do
|
154
|
+
@subject.stub(:new_record?).and_return(true)
|
155
|
+
end
|
156
|
+
|
157
|
+
it "saves as a draft" do
|
158
|
+
@subject.should_receive("is_draft=").once.with(true)
|
159
|
+
@subject.should_not_receive("draft_parent=")
|
160
|
+
@subject.should_receive("save!").once
|
161
|
+
@subject.save_as_draft!
|
162
|
+
end
|
163
|
+
|
164
|
+
it "returns the instance" do
|
165
|
+
@subject.save_as_draft!.should == @subject
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
context "and is persisted" do
|
170
|
+
before do
|
171
|
+
@subject.stub(:new_record?).and_return(false)
|
172
|
+
@duplicate = Widget.new
|
173
|
+
@subject.should_receive(:dup).once.and_return(@duplicate)
|
174
|
+
end
|
175
|
+
|
176
|
+
it "saves a duplicate record as a draft" do
|
177
|
+
@duplicate.should_receive("is_draft=").once.with(true)
|
178
|
+
@duplicate.should_receive("draft_parent=").once.with(@subject)
|
179
|
+
@duplicate.should_receive("save!").once
|
180
|
+
@subject.save_as_draft!
|
181
|
+
end
|
182
|
+
|
183
|
+
it "returns the duplicate instance" do
|
184
|
+
@subject.save_as_draft!.should == @duplicate
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
context "and has associations" do
|
189
|
+
before do
|
190
|
+
@associated = Thingy.create(name: 'Thingy')
|
191
|
+
@subject.thingies << @associated
|
192
|
+
@subject.save
|
193
|
+
end
|
194
|
+
|
195
|
+
context "and the association is not specified in register_for_draft" do
|
196
|
+
before do
|
197
|
+
@duplicate = @subject.save_as_draft!
|
198
|
+
end
|
199
|
+
|
200
|
+
it "does not duplicate the associated records" do
|
201
|
+
@duplicate.thingies.count.should == 0
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
context "and the association is specified in register_for_draft" do
|
206
|
+
before do
|
207
|
+
Widget.register_for_draft associations: :thingies
|
208
|
+
@duplicate = @subject.save_as_draft!
|
209
|
+
end
|
210
|
+
|
211
|
+
it "duplicates the associated records" do
|
212
|
+
@subject.thingies.count.should == 1
|
213
|
+
@duplicate.thingies.count.should == 1
|
214
|
+
@duplicate.thingies.first.name.should == @subject.thingies.first.name
|
215
|
+
@duplicate.thingies.first.id.should_not == @subject.thingies.first.id
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
describe "#publish_draft" do
|
223
|
+
before { Widget.register_for_draft }
|
224
|
+
|
225
|
+
context "where the instance is not a draft" do
|
226
|
+
it "raises an exception" do
|
227
|
+
-> { Widget.new.publish_draft }.should raise_error Conscript::Exception::NotADraft
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
context "where the instance is a draft" do
|
232
|
+
context "and has no parent" do
|
233
|
+
before do
|
234
|
+
@subject = Widget.new.save_as_draft!
|
235
|
+
end
|
236
|
+
|
237
|
+
it "sets is_draft to false and saves" do
|
238
|
+
@subject.is_draft?.should == true
|
239
|
+
@subject.publish_draft
|
240
|
+
@subject.is_draft?.should == false
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
context "and has a parent instance" do
|
245
|
+
before do
|
246
|
+
@original = Widget.create(name: 'Old name')
|
247
|
+
@duplicate = @original.save_as_draft!
|
248
|
+
end
|
249
|
+
|
250
|
+
it "copies the attributes to the parent" do
|
251
|
+
@duplicate.name = 'New name'
|
252
|
+
@duplicate.publish_draft
|
253
|
+
@original.reload
|
254
|
+
@original.name.should == 'New name'
|
255
|
+
end
|
256
|
+
|
257
|
+
it "does not copy the ID, type, draft or timestamp attributes" do
|
258
|
+
@duplicate.name = 'New name'
|
259
|
+
@duplicate.publish_draft
|
260
|
+
@original.reload
|
261
|
+
@original.id.should_not == @duplicate.id
|
262
|
+
@original.created_at.should_not == @duplicate.created_at
|
263
|
+
@original.updated_at.should_not == @duplicate.updated_at
|
264
|
+
@original.is_draft.should_not == @duplicate.is_draft
|
265
|
+
@original.draft_parent_id.should_not == @duplicate.draft_parent_id
|
266
|
+
end
|
267
|
+
|
268
|
+
it "destroys the draft instance" do
|
269
|
+
@duplicate.publish_draft
|
270
|
+
-> { @duplicate.reload }.should raise_error(ActiveRecord::RecordNotFound)
|
271
|
+
end
|
272
|
+
|
273
|
+
it "destroys the parent's other drafts" do
|
274
|
+
3.times { @original.save_as_draft! }
|
275
|
+
@original.drafts.count.should == 4
|
276
|
+
@duplicate.publish_draft
|
277
|
+
@original.drafts.count.should == 0
|
278
|
+
end
|
279
|
+
|
280
|
+
context "where attributes were excluded in register_for_draft" do
|
281
|
+
before { Widget.register_for_draft ignore_attributes: :name }
|
282
|
+
|
283
|
+
it "does not copy the excluded attributes" do
|
284
|
+
@duplicate.name = 'New name'
|
285
|
+
@duplicate.publish_draft
|
286
|
+
@original.reload
|
287
|
+
@original.name.should == 'Old name'
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
describe "copying associations" do
|
292
|
+
def setup
|
293
|
+
@original.thingies.count.should == 0
|
294
|
+
@associated = Thingy.create(name: 'Thingy')
|
295
|
+
@duplicate.thingies << @associated
|
296
|
+
@duplicate.save!
|
297
|
+
@duplicate.publish_draft
|
298
|
+
@original.reload
|
299
|
+
end
|
300
|
+
|
301
|
+
context "when associations were specified in register_for_draft" do
|
302
|
+
before do
|
303
|
+
Widget.register_for_draft associations: :thingies
|
304
|
+
setup
|
305
|
+
end
|
306
|
+
|
307
|
+
it "copies has_many associations to the parent" do
|
308
|
+
@original.thingies.count.should == 1
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
context "when no associations were specified in register_for_draft" do
|
313
|
+
before do
|
314
|
+
Widget.register_for_draft associations: nil
|
315
|
+
setup
|
316
|
+
end
|
317
|
+
|
318
|
+
it "does not copy associations to the parent" do
|
319
|
+
@original.thingies.count.should == 0
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
describe "#uploader_store_param" do
|
328
|
+
before do
|
329
|
+
Widget.register_for_draft
|
330
|
+
@original = Widget.create
|
331
|
+
@duplicate = @original.save_as_draft!
|
332
|
+
@draft = Widget.new.save_as_draft!
|
333
|
+
end
|
334
|
+
|
335
|
+
context "where the instance is not a draft" do
|
336
|
+
it "returns #to_param" do
|
337
|
+
@original.uploader_store_param.should == @original.to_param
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
context "where the instance is a draft" do
|
342
|
+
context "and it has no draft_parent" do
|
343
|
+
it "returns #to_param" do
|
344
|
+
@draft.uploader_store_param.should == @draft.to_param
|
345
|
+
end
|
346
|
+
end
|
347
|
+
context "and it has a draft_parent" do
|
348
|
+
it "returns draft_parent#to_param" do
|
349
|
+
@duplicate.uploader_store_param.should == @original.to_param
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
class WidgetMigration < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :widgets, :force => true do |t|
|
4
|
+
t.references :draft_parent
|
5
|
+
t.boolean "is_draft", :default => false
|
6
|
+
t.string :name
|
7
|
+
t.string :file
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
|
11
|
+
create_table :thingies, :force => true do |t|
|
12
|
+
t.references :widget
|
13
|
+
t.string :name
|
14
|
+
t.timestamps
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.down
|
19
|
+
drop_table :widgets
|
20
|
+
drop_table :thingies
|
21
|
+
end
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: conscript
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Steve Lorek
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-07-05 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: &70164099503280 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.3.5
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70164099503280
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rake
|
27
|
+
requirement: &70164099502860 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70164099502860
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rspec
|
38
|
+
requirement: &70164099502400 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70164099502400
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: sqlite3
|
49
|
+
requirement: &70164099501940 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70164099501940
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: activerecord
|
60
|
+
requirement: &70164099501420 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ~>
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 3.2.13
|
66
|
+
type: :runtime
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70164099501420
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: deep_cloneable
|
71
|
+
requirement: &70164099500900 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ~>
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 1.5.2
|
77
|
+
type: :runtime
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *70164099500900
|
80
|
+
description: Provides ActiveRecord models with draft instances, including associations
|
81
|
+
email:
|
82
|
+
- steve@stevelorek.com
|
83
|
+
executables: []
|
84
|
+
extensions: []
|
85
|
+
extra_rdoc_files: []
|
86
|
+
files:
|
87
|
+
- .gitignore
|
88
|
+
- .rspec
|
89
|
+
- Gemfile
|
90
|
+
- LICENSE.txt
|
91
|
+
- README.md
|
92
|
+
- Rakefile
|
93
|
+
- conscript.gemspec
|
94
|
+
- lib/conscript.rb
|
95
|
+
- lib/conscript/exception/already_draft.rb
|
96
|
+
- lib/conscript/exception/not_a_draft.rb
|
97
|
+
- lib/conscript/orm/activerecord.rb
|
98
|
+
- lib/conscript/version.rb
|
99
|
+
- spec/conscript/orm/activerecord_spec.rb
|
100
|
+
- spec/spec_helper.rb
|
101
|
+
- spec/support/thingy.rb
|
102
|
+
- spec/support/widget.rb
|
103
|
+
- spec/support/widget_migration.rb
|
104
|
+
homepage: http://github.com/slorek/conscript
|
105
|
+
licenses:
|
106
|
+
- MIT
|
107
|
+
post_install_message:
|
108
|
+
rdoc_options: []
|
109
|
+
require_paths:
|
110
|
+
- lib
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
112
|
+
none: false
|
113
|
+
requirements:
|
114
|
+
- - ! '>='
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
+
none: false
|
119
|
+
requirements:
|
120
|
+
- - ! '>='
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
123
|
+
requirements: []
|
124
|
+
rubyforge_project:
|
125
|
+
rubygems_version: 1.8.10
|
126
|
+
signing_key:
|
127
|
+
specification_version: 3
|
128
|
+
summary: Provides ActiveRecord models with draft instances, including associations
|
129
|
+
test_files:
|
130
|
+
- spec/conscript/orm/activerecord_spec.rb
|
131
|
+
- spec/spec_helper.rb
|
132
|
+
- spec/support/thingy.rb
|
133
|
+
- spec/support/widget.rb
|
134
|
+
- spec/support/widget_migration.rb
|