conscript 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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