conscript 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +20 -4
- data/lib/conscript/orm/activerecord.rb +35 -22
- data/lib/conscript/version.rb +1 -1
- data/spec/conscript/orm/activerecord_spec.rb +81 -11
- metadata +13 -13
data/README.md
CHANGED
@@ -95,6 +95,18 @@ A list of options are below:
|
|
95
95
|
|
96
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
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
|
+
- `:allow_update_with_drafts` (`false`) whether to allow an instance to be updated if it has draft instances
|
99
|
+
- `:destroy_drafts_on_publish` (`true`) whether to destroy all other drafts for an instance when publishing a draft
|
100
|
+
|
101
|
+
|
102
|
+
### Callbacks
|
103
|
+
|
104
|
+
Two extra callbacks are made available for you to wrap bespoke behaviour around the draft lifecycle:
|
105
|
+
|
106
|
+
- `save_as_draft`
|
107
|
+
- `publish_draft`
|
108
|
+
|
109
|
+
You can call `set_callback` with `:before`, `:after` or `:around` as normal.
|
98
110
|
|
99
111
|
|
100
112
|
### Using with CarrierWave
|
@@ -116,13 +128,17 @@ This will result in uploads for drafts being stored in the same location as the
|
|
116
128
|
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
129
|
|
118
130
|
|
119
|
-
###
|
131
|
+
### Gotchas
|
132
|
+
|
133
|
+
If saving drafts of models with `has_many` associations, the associated model should define a reciprocal `belongs_to` relationship (as normal). However, if it also has a presence validation e.g. `validates parent_model_name, presence: true` then you will encounter a limitation of ActiveRecord's default scopes. To avoid this when saving drafts with associations, wrap your save code with a `Model.unscoped` block, e.g:
|
134
|
+
|
135
|
+
Article.unscoped { draft = article.save_as_draft! }
|
120
136
|
|
121
|
-
|
137
|
+
Or if updating an existing draft:
|
122
138
|
|
123
|
-
|
124
|
-
- When you publish a draft, any other drafts for the same `draft_parent` are also destroyed.
|
139
|
+
Article.unscoped { draft.save! }
|
125
140
|
|
141
|
+
Otherwise you'll encounter a validation error.
|
126
142
|
|
127
143
|
## Contributing
|
128
144
|
|
@@ -10,20 +10,26 @@ module Conscript
|
|
10
10
|
cattr_accessor :conscript_options, :instance_accessor => false do
|
11
11
|
{
|
12
12
|
associations: [],
|
13
|
-
ignore_attributes: [self.primary_key, 'type', 'created_at', 'updated_at', 'draft_parent_id', 'is_draft']
|
13
|
+
ignore_attributes: [self.primary_key, 'type', 'created_at', 'updated_at', 'draft_parent_id', 'is_draft'],
|
14
|
+
allow_update_with_drafts: false,
|
15
|
+
destroy_drafts_on_publish: true
|
14
16
|
}
|
15
17
|
end
|
16
18
|
|
17
|
-
self.conscript_options.each_pair {|key, value| self.conscript_options[key] = Array(value) | Array(options[key]) }
|
19
|
+
self.conscript_options.slice(:associations, :ignore_attributes).each_pair {|key, value| self.conscript_options[key] = Array(value) | Array(options[key]) }
|
18
20
|
self.conscript_options[:associations].map!(&:to_sym)
|
19
21
|
self.conscript_options[:ignore_attributes].map!(&:to_s)
|
22
|
+
self.conscript_options.update options.slice(:allow_update_with_drafts, :destroy_drafts_on_publish)
|
20
23
|
|
21
24
|
default_scope { where(is_draft: false) }
|
22
25
|
|
23
26
|
belongs_to :draft_parent, class_name: self
|
24
27
|
has_many :drafts, conditions: {is_draft: true}, class_name: self, foreign_key: :draft_parent_id, dependent: :destroy, inverse_of: :draft_parent
|
25
28
|
|
26
|
-
|
29
|
+
define_callbacks :publish_draft, :save_as_draft
|
30
|
+
|
31
|
+
before_save :check_no_drafts_exist if (self.conscript_options[:allow_update_with_drafts] == false)
|
32
|
+
set_callback :publish_draft, :before, :destroy_all_drafts if (self.conscript_options[:destroy_drafts_on_publish] == true)
|
27
33
|
|
28
34
|
# Prevent deleting CarrierWave uploads which may be used by other instances. Uploaders must be mounted beforehand.
|
29
35
|
if self.respond_to? :uploaders
|
@@ -37,31 +43,34 @@ module Conscript
|
|
37
43
|
end
|
38
44
|
|
39
45
|
def save_as_draft!
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
+
run_callbacks :save_as_draft do
|
47
|
+
raise Conscript::Exception::AlreadyDraft if is_draft?
|
48
|
+
draft = new_record? ? self : dup(include: self.class.conscript_options[:associations])
|
49
|
+
draft.is_draft = true
|
50
|
+
draft.draft_parent = self unless new_record?
|
51
|
+
self.class.base_class.unscoped { draft.save! }
|
52
|
+
draft
|
53
|
+
end
|
46
54
|
end
|
47
55
|
|
48
56
|
def publish_draft
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
57
|
+
run_callbacks :publish_draft do
|
58
|
+
raise Conscript::Exception::NotADraft unless is_draft?
|
59
|
+
return self.update_attribute(:is_draft, false) if !draft_parent_id
|
60
|
+
::ActiveRecord::Base.transaction do
|
61
|
+
draft_parent.assign_attributes attributes_to_publish, without_protection: true
|
62
|
+
|
63
|
+
self.class.conscript_options[:associations].each do |association|
|
64
|
+
case reflections[association].macro
|
65
|
+
when :has_many
|
66
|
+
draft_parent.send(association.to_s + "=", self.send(association).collect {|child| child.dup })
|
67
|
+
end
|
58
68
|
end
|
59
|
-
end
|
60
69
|
|
61
|
-
|
62
|
-
|
70
|
+
draft_parent.save!
|
71
|
+
end
|
72
|
+
draft_parent
|
63
73
|
end
|
64
|
-
draft_parent
|
65
74
|
end
|
66
75
|
|
67
76
|
def uploader_store_param
|
@@ -85,6 +94,10 @@ module Conscript
|
|
85
94
|
self.send("remove_" + attribute.to_s + "!") if !draft_parent_id or draft_parent.drafts.where(attribute => filename).count == 0
|
86
95
|
end
|
87
96
|
end
|
97
|
+
|
98
|
+
def destroy_all_drafts
|
99
|
+
draft_parent.drafts.destroy_all if draft_parent_id
|
100
|
+
end
|
88
101
|
RUBY
|
89
102
|
end
|
90
103
|
end
|
data/lib/conscript/version.rb
CHANGED
@@ -31,10 +31,46 @@ describe Conscript::ActiveRecord do
|
|
31
31
|
Widget.register_for_draft
|
32
32
|
end
|
33
33
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
34
|
+
describe "options" do
|
35
|
+
it "accepts options and merges them with defaults" do
|
36
|
+
Widget.register_for_draft(associations: :owners, ignore_attributes: :custom_attribute)
|
37
|
+
Widget.conscript_options[:associations].should == [:owners]
|
38
|
+
Widget.conscript_options[:ignore_attributes].should == ["id", "type", "created_at", "updated_at", "draft_parent_id", "is_draft", "custom_attribute"]
|
39
|
+
end
|
40
|
+
|
41
|
+
describe ":allow_update_with_drafts" do
|
42
|
+
context "when true" do
|
43
|
+
it "does not register #check_no_drafts_exist as a callback" do
|
44
|
+
Widget.should_not_receive(:before_save)
|
45
|
+
Widget.register_for_draft(allow_update_with_drafts: true)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context "when false" do
|
50
|
+
it "registers #check_no_drafts_exist as a callback on before_save" do
|
51
|
+
Widget.should_receive(:before_save).once.with(:check_no_drafts_exist)
|
52
|
+
Widget.register_for_draft(allow_update_with_drafts: false)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe ":destroy_drafts_on_publish" do
|
58
|
+
context "when true" do
|
59
|
+
it "registers #destroy_all_drafts as a before callback on publish_draft" do
|
60
|
+
Widget.register_for_draft(destroy_drafts_on_publish: true)
|
61
|
+
callback = Widget._publish_draft_callbacks.first
|
62
|
+
callback.filter.should == :destroy_all_drafts
|
63
|
+
callback.kind.should == :before
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context "when false" do
|
68
|
+
it "does not register #destroy_all_drafts as a callback" do
|
69
|
+
Widget.register_for_draft(destroy_drafts_on_publish: false)
|
70
|
+
Widget._publish_draft_callbacks.should be_empty
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
38
74
|
end
|
39
75
|
|
40
76
|
describe "CarrierWave compatibility" do
|
@@ -98,6 +134,17 @@ describe Conscript::ActiveRecord do
|
|
98
134
|
end
|
99
135
|
end
|
100
136
|
|
137
|
+
describe "#destroy_all_drafts" do
|
138
|
+
context "with a draft_parent" do
|
139
|
+
it "destroys all drafts" do
|
140
|
+
@original = Widget.create
|
141
|
+
@draft = @original.save_as_draft!
|
142
|
+
@original.stub_chain(:drafts, :destroy_all).and_return('test')
|
143
|
+
@draft.send(:destroy_all_drafts).should == 'test'
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
101
148
|
describe "#clean_uploaded_files_for_draft!" do
|
102
149
|
before do
|
103
150
|
Widget.cattr_accessor :uploaders
|
@@ -270,13 +317,6 @@ describe Conscript::ActiveRecord do
|
|
270
317
|
-> { @duplicate.reload }.should raise_error(ActiveRecord::RecordNotFound)
|
271
318
|
end
|
272
319
|
|
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
320
|
context "where attributes were excluded in register_for_draft" do
|
281
321
|
before { Widget.register_for_draft ignore_attributes: :name }
|
282
322
|
|
@@ -350,4 +390,34 @@ describe Conscript::ActiveRecord do
|
|
350
390
|
end
|
351
391
|
end
|
352
392
|
end
|
393
|
+
|
394
|
+
describe "callbacks" do
|
395
|
+
describe "publish_draft" do
|
396
|
+
it "is defined" do
|
397
|
+
Widget.register_for_draft
|
398
|
+
Widget._publish_draft_callbacks.should_not == nil
|
399
|
+
end
|
400
|
+
|
401
|
+
it "can be set" do
|
402
|
+
Widget.any_instance.should_receive(:test_callback).once
|
403
|
+
Widget.register_for_draft
|
404
|
+
Widget.set_callback :publish_draft, :before, :test_callback
|
405
|
+
Widget.create(is_draft: true).publish_draft
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
describe "save_as_draft" do
|
410
|
+
it "is defined" do
|
411
|
+
Widget.register_for_draft
|
412
|
+
Widget._save_as_draft_callbacks.should_not == nil
|
413
|
+
end
|
414
|
+
|
415
|
+
it "can be set" do
|
416
|
+
Widget.any_instance.should_receive(:test_callback).once
|
417
|
+
Widget.register_for_draft
|
418
|
+
Widget.set_callback :save_as_draft, :before, :test_callback
|
419
|
+
Widget.new.save_as_draft!
|
420
|
+
end
|
421
|
+
end
|
422
|
+
end
|
353
423
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: conscript
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -13,7 +13,7 @@ date: 2013-07-09 00:00:00.000000000Z
|
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
16
|
-
requirement: &
|
16
|
+
requirement: &70155455728440 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 1.3.5
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70155455728440
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rake
|
27
|
-
requirement: &
|
27
|
+
requirement: &70155455727800 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70155455727800
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rspec
|
38
|
-
requirement: &
|
38
|
+
requirement: &70155455727040 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70155455727040
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: sqlite3
|
49
|
-
requirement: &
|
49
|
+
requirement: &70155455726620 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: '0'
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70155455726620
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: activerecord
|
60
|
-
requirement: &
|
60
|
+
requirement: &70155455726120 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ~>
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: 3.2.13
|
66
66
|
type: :runtime
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *70155455726120
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: deep_cloneable
|
71
|
-
requirement: &
|
71
|
+
requirement: &70155455725600 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - ~>
|
@@ -76,7 +76,7 @@ dependencies:
|
|
76
76
|
version: 1.5.2
|
77
77
|
type: :runtime
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *70155455725600
|
80
80
|
description: Provides ActiveRecord models with draft instances, including associations
|
81
81
|
email:
|
82
82
|
- steve@stevelorek.com
|