conscript 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 a `drafts` scope, and the functionality to create draft instances and publish them.
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
- A `default_scope` is then set to exclude all draft instances from finders. You may use the `drafts` scope to access them:
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
- If you need to access drafts and original instances together, use `unscoped` as you would any other `default_scope`:
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, :before, :destroy_all_drafts if (self.conscript_options[:destroy_drafts_on_publish] == true)
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
- self.class.base_class.unscoped { draft.save! }
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
- self.send("remove_" + attribute.to_s + "!") if !draft_parent_id or draft_parent.drafts.where(attribute => filename).count == 0
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
 
@@ -1,3 +1,3 @@
1
1
  module Conscript
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.1"
3
3
  end
@@ -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 == :before
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
- before do
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
@@ -11,6 +11,7 @@ class WidgetMigration < ActiveRecord::Migration
11
11
  create_table :thingies, :force => true do |t|
12
12
  t.references :widget
13
13
  t.string :name
14
+ t.string :file
14
15
  t.timestamps
15
16
  end
16
17
  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.0
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-09 00:00:00.000000000Z
12
+ date: 2013-07-12 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
16
- requirement: &70155455728440 !ruby/object:Gem::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: *70155455728440
24
+ version_requirements: *70350313257540
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rake
27
- requirement: &70155455727800 !ruby/object:Gem::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: *70155455727800
35
+ version_requirements: *70350313257120
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rspec
38
- requirement: &70155455727040 !ruby/object:Gem::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: *70155455727040
46
+ version_requirements: *70350313256600
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: sqlite3
49
- requirement: &70155455726620 !ruby/object:Gem::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: *70155455726620
57
+ version_requirements: *70350313256180
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: activerecord
60
- requirement: &70155455726120 !ruby/object:Gem::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: *70155455726120
68
+ version_requirements: *70350313255560
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: deep_cloneable
71
- requirement: &70155455725600 !ruby/object:Gem::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: *70155455725600
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