conscript 0.0.2 → 0.1.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.
- 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
|