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 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
- ### Limitations
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
- For reasons of sanity:
137
+ Or if updating an existing draft:
122
138
 
123
- - You cannot make changes to an instance if it has drafts, this is because it would be difficult to propogate those changes down or provide visibility of the changes.
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
- before_save :check_no_drafts_exist
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
- raise Conscript::Exception::AlreadyDraft if is_draft?
41
- draft = new_record? ? self : dup(include: self.class.conscript_options[:associations])
42
- draft.is_draft = true
43
- draft.draft_parent = self unless new_record?
44
- self.class.base_class.unscoped { draft.save! }
45
- draft
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
- raise Conscript::Exception::NotADraft unless is_draft?
50
- return self.update_attribute(:is_draft, false) if !draft_parent_id
51
- ::ActiveRecord::Base.transaction do
52
- draft_parent.assign_attributes attributes_to_publish, without_protection: true
53
-
54
- self.class.conscript_options[:associations].each do |association|
55
- case reflections[association].macro
56
- when :has_many
57
- draft_parent.send(association.to_s + "=", self.send(association).collect {|child| child.dup })
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
- draft_parent.drafts.destroy_all
62
- draft_parent.save!
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
@@ -1,3 +1,3 @@
1
1
  module Conscript
2
- VERSION = "0.0.2"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -31,10 +31,46 @@ describe Conscript::ActiveRecord do
31
31
  Widget.register_for_draft
32
32
  end
33
33
 
34
- it "accepts options and merges them with defaults" do
35
- Widget.register_for_draft(associations: :owners, ignore_attributes: :custom_attribute)
36
- Widget.conscript_options[:associations].should == [:owners]
37
- Widget.conscript_options[:ignore_attributes].should == ["id", "type", "created_at", "updated_at", "draft_parent_id", "is_draft", "custom_attribute"]
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.2
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: &70297027681520 !ruby/object:Gem::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: *70297027681520
24
+ version_requirements: *70155455728440
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rake
27
- requirement: &70297027681100 !ruby/object:Gem::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: *70297027681100
35
+ version_requirements: *70155455727800
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rspec
38
- requirement: &70297027680500 !ruby/object:Gem::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: *70297027680500
46
+ version_requirements: *70155455727040
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: sqlite3
49
- requirement: &70297027679880 !ruby/object:Gem::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: *70297027679880
57
+ version_requirements: *70155455726620
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: activerecord
60
- requirement: &70297027679120 !ruby/object:Gem::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: *70297027679120
68
+ version_requirements: *70155455726120
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: deep_cloneable
71
- requirement: &70297027675000 !ruby/object:Gem::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: *70297027675000
79
+ version_requirements: *70155455725600
80
80
  description: Provides ActiveRecord models with draft instances, including associations
81
81
  email:
82
82
  - steve@stevelorek.com