conscript 0.1.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +23 -0
- data/README.md +7 -17
- data/lib/conscript/orm/activerecord.rb +22 -7
- data/lib/conscript/version.rb +1 -1
- data/spec/conscript/orm/activerecord_spec.rb +29 -10
- data/spec/support/widget_migration.rb +1 -0
- metadata +15 -14
data/CHANGELOG.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
## v0.2.1
|
2
|
+
|
3
|
+
* Fixed bug where associated `has_many` records with CarrierWave uploads were not duplicated correctly - uploaded files are now copied
|
4
|
+
* Fixed bug where destroying a draft would remove an uploaded file from its draft parent if they shared the same file.
|
5
|
+
* Fixed bug where drafts were destroyed before publish_Draft was successful
|
6
|
+
* Added an ActiveRecord error where `save` fails if drafts exist for an instance
|
7
|
+
|
8
|
+
## v0.2.0
|
9
|
+
|
10
|
+
* Now defines a `published` scope rather than a `default_scope`. This avoids a number of complexities especially around validations and polymorphic relationships.
|
11
|
+
|
12
|
+
## v0.1.1
|
13
|
+
|
14
|
+
* CarrierWave uplaods on associated records are now cloned correctly.
|
15
|
+
|
16
|
+
## v0.1.0
|
17
|
+
|
18
|
+
* Additional callbacks added to allow bespoke behaviour around the save as draft and publish draft lifecycles.
|
19
|
+
* Additional options added to remove previously documented limitations.
|
20
|
+
|
21
|
+
## v0.0.1
|
22
|
+
|
23
|
+
* First release.
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Conscript
|
2
2
|
|
3
|
-
Provides ActiveRecord models with
|
3
|
+
Provides ActiveRecord models with `drafts` and `published` scopes, and the functionality to create draft instances and publish them.
|
4
4
|
|
5
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
6
|
|
@@ -46,13 +46,16 @@ To use the drafts functionality, call `register_for_draft` in your ActiveRecord
|
|
46
46
|
register_for_draft
|
47
47
|
end
|
48
48
|
|
49
|
-
|
49
|
+
This registers `published` and `drafts` scopes on the model. You may use these scopes as with any other scope:
|
50
|
+
|
51
|
+
Article.published
|
52
|
+
|
53
|
+
Or:
|
50
54
|
|
51
55
|
Article.drafts
|
52
56
|
|
53
|
-
|
57
|
+
You'll therefore need to modify any existing code to use the `published` scope where you don't want to show drafts.
|
54
58
|
|
55
|
-
Article.unscoped.all
|
56
59
|
|
57
60
|
### Instance methods
|
58
61
|
|
@@ -127,19 +130,6 @@ This will result in uploads for drafts being stored in the same location as the
|
|
127
130
|
|
128
131
|
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.
|
129
132
|
|
130
|
-
|
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! }
|
136
|
-
|
137
|
-
Or if updating an existing draft:
|
138
|
-
|
139
|
-
Article.unscoped { draft.save! }
|
140
|
-
|
141
|
-
Otherwise you'll encounter a validation error.
|
142
|
-
|
143
133
|
## Contributing
|
144
134
|
|
145
135
|
1. Fork it
|
@@ -21,15 +21,13 @@ module Conscript
|
|
21
21
|
self.conscript_options[:ignore_attributes].map!(&:to_s)
|
22
22
|
self.conscript_options.update options.slice(:allow_update_with_drafts, :destroy_drafts_on_publish)
|
23
23
|
|
24
|
-
default_scope { where(is_draft: false) }
|
25
|
-
|
26
24
|
belongs_to :draft_parent, class_name: self
|
27
25
|
has_many :drafts, conditions: {is_draft: true}, class_name: self, foreign_key: :draft_parent_id, dependent: :destroy, inverse_of: :draft_parent
|
28
26
|
|
29
27
|
define_callbacks :publish_draft, :save_as_draft
|
30
28
|
|
31
29
|
before_save :check_no_drafts_exist if (self.conscript_options[:allow_update_with_drafts] == false)
|
32
|
-
set_callback :publish_draft, :
|
30
|
+
set_callback :publish_draft, :after, :destroy_all_drafts if (self.conscript_options[:destroy_drafts_on_publish] == true)
|
33
31
|
|
34
32
|
# Prevent deleting CarrierWave uploads which may be used by other instances. Uploaders must be mounted beforehand.
|
35
33
|
if self.respond_to? :uploaders
|
@@ -38,6 +36,10 @@ module Conscript
|
|
38
36
|
end
|
39
37
|
|
40
38
|
class_eval <<-RUBY
|
39
|
+
def self.published
|
40
|
+
where(is_draft: false)
|
41
|
+
end
|
42
|
+
|
41
43
|
def self.drafts
|
42
44
|
where(is_draft: true)
|
43
45
|
end
|
@@ -45,10 +47,15 @@ module Conscript
|
|
45
47
|
def save_as_draft!
|
46
48
|
run_callbacks :save_as_draft do
|
47
49
|
raise Conscript::Exception::AlreadyDraft if is_draft?
|
48
|
-
draft = new_record? ? self : dup(include: self.class.conscript_options[:associations])
|
50
|
+
draft = new_record? ? self : dup(include: self.class.conscript_options[:associations]) do |original, dup|
|
51
|
+
# Workaround for CarrierWave uploaders on associated records. Copy the uploaded files.
|
52
|
+
if dup.class.respond_to? :uploaders
|
53
|
+
dup.class.uploaders.keys.each {|uploader| dup.send(uploader.to_s + "=", original.send(uploader)) }
|
54
|
+
end
|
55
|
+
end
|
49
56
|
draft.is_draft = true
|
50
57
|
draft.draft_parent = self unless new_record?
|
51
|
-
|
58
|
+
draft.save!
|
52
59
|
draft
|
53
60
|
end
|
54
61
|
end
|
@@ -63,10 +70,16 @@ module Conscript
|
|
63
70
|
self.class.conscript_options[:associations].each do |association|
|
64
71
|
case reflections[association].macro
|
65
72
|
when :has_many
|
66
|
-
draft_parent.send(association.to_s + "=", self.send(association).collect {|child| child.dup
|
73
|
+
draft_parent.send(association.to_s + "=", self.send(association).collect {|child| child.dup do |original, dup|
|
74
|
+
# Workaround for CarrierWave uploaders on associated records. Copy the uploaded files.
|
75
|
+
if dup.class.respond_to? :uploaders
|
76
|
+
dup.class.uploaders.keys.each {|uploader| dup.send(uploader.to_s + "=", original.send(uploader)) }
|
77
|
+
end
|
78
|
+
end })
|
67
79
|
end
|
68
80
|
end
|
69
81
|
|
82
|
+
self.destroy
|
70
83
|
draft_parent.save!
|
71
84
|
end
|
72
85
|
draft_parent
|
@@ -79,6 +92,7 @@ module Conscript
|
|
79
92
|
|
80
93
|
private
|
81
94
|
def check_no_drafts_exist
|
95
|
+
errors[:base] << "Cannot save record while drafts exist"
|
82
96
|
drafts.count == 0
|
83
97
|
end
|
84
98
|
|
@@ -91,7 +105,8 @@ module Conscript
|
|
91
105
|
def clean_uploaded_files_for_draft!
|
92
106
|
self.class.uploaders.keys.each do |attribute|
|
93
107
|
filename = attributes[attribute.to_s]
|
94
|
-
|
108
|
+
cols = self.class.arel_table
|
109
|
+
self.send("remove_" + attribute.to_s + "!") if !draft_parent_id or self.class.where(cols[:id].eq(draft_parent_id).or(cols[:draft_parent_id].eq(draft_parent_id))).where(attribute => filename).count == 0
|
95
110
|
end
|
96
111
|
end
|
97
112
|
|
data/lib/conscript/version.rb
CHANGED
@@ -11,11 +11,6 @@ describe Conscript::ActiveRecord do
|
|
11
11
|
Widget.respond_to?(:register_for_draft).should == true
|
12
12
|
end
|
13
13
|
|
14
|
-
it "creates the default scope" do
|
15
|
-
Widget.should_receive(:default_scope).once
|
16
|
-
Widget.register_for_draft
|
17
|
-
end
|
18
|
-
|
19
14
|
it "creates a belongs_to association" do
|
20
15
|
Widget.should_receive(:belongs_to).once.with(:draft_parent, kind_of(Hash))
|
21
16
|
Widget.register_for_draft
|
@@ -60,7 +55,7 @@ describe Conscript::ActiveRecord do
|
|
60
55
|
Widget.register_for_draft(destroy_drafts_on_publish: true)
|
61
56
|
callback = Widget._publish_draft_callbacks.first
|
62
57
|
callback.filter.should == :destroy_all_drafts
|
63
|
-
callback.kind.should == :
|
58
|
+
callback.kind.should == :after
|
64
59
|
end
|
65
60
|
end
|
66
61
|
|
@@ -100,6 +95,14 @@ describe Conscript::ActiveRecord do
|
|
100
95
|
end
|
101
96
|
end
|
102
97
|
|
98
|
+
describe "#published" do
|
99
|
+
it "limits results to published" do
|
100
|
+
Widget.register_for_draft
|
101
|
+
Widget.should_receive(:where).once.with(is_draft: false)
|
102
|
+
Widget.published
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
103
106
|
describe "#drafts" do
|
104
107
|
it "limits results to drafts" do
|
105
108
|
Widget.register_for_draft
|
@@ -123,6 +126,7 @@ describe Conscript::ActiveRecord do
|
|
123
126
|
@subject.send(:check_no_drafts_exist).should == true
|
124
127
|
end
|
125
128
|
end
|
129
|
+
|
126
130
|
context "when drafts exist" do
|
127
131
|
before do
|
128
132
|
@subject.stub_chain(:drafts, :count).and_return(1)
|
@@ -131,6 +135,12 @@ describe Conscript::ActiveRecord do
|
|
131
135
|
it "returns false" do
|
132
136
|
@subject.send(:check_no_drafts_exist).should == false
|
133
137
|
end
|
138
|
+
|
139
|
+
it "adds an error" do
|
140
|
+
@subject.errors[:base].should be_empty
|
141
|
+
@subject.send(:check_no_drafts_exist)
|
142
|
+
@subject.errors[:base].should_not be_empty
|
143
|
+
end
|
134
144
|
end
|
135
145
|
end
|
136
146
|
|
@@ -235,7 +245,7 @@ describe Conscript::ActiveRecord do
|
|
235
245
|
context "and has associations" do
|
236
246
|
before do
|
237
247
|
@subject.save!
|
238
|
-
@subject.thingies.create(name: 'Thingy')
|
248
|
+
@subject.thingies.create(name: 'Thingy', file: 'test.jpg')
|
239
249
|
end
|
240
250
|
|
241
251
|
context "and the association is not specified in register_for_draft" do
|
@@ -250,17 +260,26 @@ describe Conscript::ActiveRecord do
|
|
250
260
|
end
|
251
261
|
|
252
262
|
context "and the association is specified in register_for_draft" do
|
253
|
-
|
263
|
+
it "duplicates the associated records" do
|
254
264
|
Widget.register_for_draft associations: :thingies
|
255
265
|
@duplicate = @subject.save_as_draft!
|
256
|
-
end
|
257
266
|
|
258
|
-
it "duplicates the associated records" do
|
259
267
|
@subject.thingies.count.should == 1
|
260
268
|
@duplicate.thingies.count.should == 1
|
261
269
|
@duplicate.thingies.first.name.should == @subject.thingies.first.name
|
262
270
|
@duplicate.thingies.first.id.should_not == @subject.thingies.first.id
|
263
271
|
end
|
272
|
+
|
273
|
+
it "copies the uploader attributes using the setter" do
|
274
|
+
Thingy.cattr_accessor :uploaders do
|
275
|
+
{file: nil}
|
276
|
+
end
|
277
|
+
|
278
|
+
Widget.register_for_draft associations: :thingies
|
279
|
+
|
280
|
+
Thingy.any_instance.should_receive(:file=)
|
281
|
+
@duplicate = @subject.save_as_draft!
|
282
|
+
end
|
264
283
|
end
|
265
284
|
end
|
266
285
|
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.1
|
4
|
+
version: 0.2.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-07-
|
12
|
+
date: 2013-07-12 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
16
|
-
requirement: &
|
16
|
+
requirement: &70350313257540 !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: *70350313257540
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rake
|
27
|
-
requirement: &
|
27
|
+
requirement: &70350313257120 !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: *70350313257120
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rspec
|
38
|
-
requirement: &
|
38
|
+
requirement: &70350313256600 !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: *70350313256600
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: sqlite3
|
49
|
-
requirement: &
|
49
|
+
requirement: &70350313256180 !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: *70350313256180
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: activerecord
|
60
|
-
requirement: &
|
60
|
+
requirement: &70350313255560 !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: *70350313255560
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: deep_cloneable
|
71
|
-
requirement: &
|
71
|
+
requirement: &70350313254920 !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: *70350313254920
|
80
80
|
description: Provides ActiveRecord models with draft instances, including associations
|
81
81
|
email:
|
82
82
|
- steve@stevelorek.com
|
@@ -86,6 +86,7 @@ extra_rdoc_files: []
|
|
86
86
|
files:
|
87
87
|
- .gitignore
|
88
88
|
- .rspec
|
89
|
+
- CHANGELOG.md
|
89
90
|
- Gemfile
|
90
91
|
- LICENSE.txt
|
91
92
|
- README.md
|