conscript 0.1.0 → 0.2.1

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.
@@ -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