drafter 0.2.8 → 0.3.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/Gemfile CHANGED
@@ -9,6 +9,7 @@ gem "diffy"
9
9
  # Include everything needed to run rake, tests, features, etc.
10
10
  group :development do
11
11
  gem "sqlite3"
12
+ gem "database_cleaner"
12
13
  gem "debugger"
13
14
  gem "minitest", ">= 0"
14
15
  gem "bundler", "~> 1.0.0"
data/Gemfile.lock CHANGED
@@ -35,6 +35,7 @@ GEM
35
35
  activemodel (>= 3.2.0)
36
36
  activesupport (>= 3.2.0)
37
37
  columnize (0.3.6)
38
+ database_cleaner (0.7.1)
38
39
  debugger (1.1.1)
39
40
  columnize (>= 0.3.1)
40
41
  debugger-linecache (~> 1.1)
@@ -119,6 +120,7 @@ DEPENDENCIES
119
120
  activerecord
120
121
  bundler (~> 1.0.0)
121
122
  carrierwave
123
+ database_cleaner
122
124
  debugger
123
125
  diffy
124
126
  jeweler (~> 1.6.4)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.8
1
+ 0.3.0
data/drafter.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "drafter"
8
- s.version = "0.2.8"
8
+ s.version = "0.3.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["futurechimp"]
12
- s.date = "2012-08-29"
12
+ s.date = "2012-09-05"
13
13
  s.description = "A"
14
14
  s.email = "dave.hrycyszyn@headlondon.com"
15
15
  s.extra_rdoc_files = [
@@ -33,12 +33,16 @@ Gem::Specification.new do |s|
33
33
  "lib/drafter/draft_upload.rb",
34
34
  "lib/drafter/draft_uploader.rb",
35
35
  "lib/drafter/draftable.rb",
36
+ "lib/drafter/id_hash.rb",
37
+ "lib/drafter/subdrafts.rb",
36
38
  "test/drafter/test_apply.rb",
37
39
  "test/drafter/test_creation.rb",
38
40
  "test/drafter/test_diffing.rb",
39
41
  "test/drafter/test_draft.rb",
40
42
  "test/drafter/test_draft_upload.rb",
41
43
  "test/drafter/test_draftable.rb",
44
+ "test/drafter/test_id_hash.rb",
45
+ "test/drafter/test_subdrafts.rb",
42
46
  "test/fixtures/bar.txt",
43
47
  "test/fixtures/foo.txt",
44
48
  "test/helper.rb",
@@ -62,6 +66,7 @@ Gem::Specification.new do |s|
62
66
  s.add_runtime_dependency(%q<activerecord>, [">= 0"])
63
67
  s.add_runtime_dependency(%q<diffy>, [">= 0"])
64
68
  s.add_development_dependency(%q<sqlite3>, [">= 0"])
69
+ s.add_development_dependency(%q<database_cleaner>, [">= 0"])
65
70
  s.add_development_dependency(%q<debugger>, [">= 0"])
66
71
  s.add_development_dependency(%q<minitest>, [">= 0"])
67
72
  s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
@@ -74,6 +79,7 @@ Gem::Specification.new do |s|
74
79
  s.add_dependency(%q<activerecord>, [">= 0"])
75
80
  s.add_dependency(%q<diffy>, [">= 0"])
76
81
  s.add_dependency(%q<sqlite3>, [">= 0"])
82
+ s.add_dependency(%q<database_cleaner>, [">= 0"])
77
83
  s.add_dependency(%q<debugger>, [">= 0"])
78
84
  s.add_dependency(%q<minitest>, [">= 0"])
79
85
  s.add_dependency(%q<bundler>, ["~> 1.0.0"])
@@ -87,6 +93,7 @@ Gem::Specification.new do |s|
87
93
  s.add_dependency(%q<activerecord>, [">= 0"])
88
94
  s.add_dependency(%q<diffy>, [">= 0"])
89
95
  s.add_dependency(%q<sqlite3>, [">= 0"])
96
+ s.add_dependency(%q<database_cleaner>, [">= 0"])
90
97
  s.add_dependency(%q<debugger>, [">= 0"])
91
98
  s.add_dependency(%q<minitest>, [">= 0"])
92
99
  s.add_dependency(%q<bundler>, ["~> 1.0.0"])
data/lib/drafter/apply.rb CHANGED
@@ -4,11 +4,12 @@ module Drafter
4
4
  module Apply
5
5
  extend ActiveSupport::Concern
6
6
 
7
- # Apply the draft data to the draftable, without saving it..
7
+ # Apply the draft data to the draftable, without saving it.
8
8
  def apply_draft
9
9
  if draft
10
10
  restore_attrs
11
11
  restore_files
12
+ restore_subdrafts
12
13
  end
13
14
  self
14
15
  end
@@ -36,6 +37,60 @@ module Drafter
36
37
  end
37
38
  end
38
39
 
40
+ # It's possible to easily restore subdrafts except in one case we've
41
+ # run into so far: the case where you try to restore a subdraft which
42
+ # is defined using Single Table Inheritance (STI) and which also has a
43
+ # polymorphic belongs_to relationship with the draftable class.
44
+ #
45
+ # In those edge cases, we need to do a dirty little hack, using the
46
+ # :polymorphic_as option from the class's draftable method to manually
47
+ # set the association.
48
+ #
49
+ # This won't be clear enough to make sense 5 minutes from now, so here are
50
+ # the details spelled out more concretely. Let's say you've got these
51
+ # classes:
52
+ #
53
+ # class Article < ActiveRecord::Base
54
+ # draftable
55
+ # has_many :likes, :as => :likeable
56
+ # has_many :really_likes
57
+ # end
58
+ #
59
+ # class Like < ActiveRecord::Base
60
+ # draftable
61
+ # belongs_to :likeable, :polymorphic => true
62
+ # end
63
+ #
64
+ # class ReallyLike < Like
65
+ # end
66
+ #
67
+ # This setup will actually work fine for subdraft restoration, right up
68
+ # until you add "validates_presence_of :likeable" to Like. At that point,
69
+ # you'll be told that really_likes is invalid whenever you try to approve
70
+ # something, because the polymorphic STI "really_likable" won't be able
71
+ # to figure out what its likeable id is when it saves during the approval
72
+ # process.
73
+ #
74
+ # The hack basically involves explicitly telling draftable in the Like
75
+ # class what its polymorph relation is:
76
+ #
77
+ # class Like < ActiveRecord::Base
78
+ # draftable :polymorphic_as => :likeable
79
+ # ...
80
+ #
81
+ # See the models setup at tests/support/models.rb for the working setup.
82
+ #
83
+ def restore_subdrafts
84
+ draft.subdrafts.each_with_index do |subdraft, index|
85
+ inflated_object = subdraft.build_draftable
86
+ self.send(subdraft.parent_association_name.to_sym) << inflated_object
87
+ # THE HACK
88
+ if inflated_object.class.polymorphic_as
89
+ inflated_object.send("#{inflated_object.class.polymorphic_as}=", self)
90
+ end
91
+ end
92
+ end
93
+
39
94
  # We don't want to copy all the draft's columns into the draftable
40
95
  # objects attributes.
41
96
  #
@@ -11,37 +11,54 @@ module Drafter
11
11
  end
12
12
 
13
13
  # Build and save the draft when told to do so.
14
- def save_draft
15
- if self.valid?
16
- attrs = self.attributes
17
- if self.draft
18
- draft.data = attrs
19
- else
20
- self.build_draft(:data => attrs)
21
- end
22
- # https://github.com/rails/rails/issues/617
23
- draft.draftable_type = self.class.to_s
24
- self.draft.save!
25
- uploads = build_draft_uploads(attrs)
26
- self.draft
14
+ def save_draft(parent_draft=nil, parent_association_name=nil)
15
+ if valid?
16
+ do_create_draft(parent_draft, parent_association_name)
17
+ create_subdrafts
27
18
  end
19
+ return self.draft.reload if self.draft
28
20
  end
29
21
 
30
22
  private
31
23
 
24
+ def do_create_draft(parent_draft=nil, parent_association_name=nil)
25
+ serialize_attributes_to_draft
26
+ attach_to_parent_draft(parent_draft, parent_association_name)
27
+ unfuck_sti
28
+ draft.save!
29
+ build_draft_uploads
30
+ draft
31
+ end
32
+
33
+ # https://github.com/rails/rails/issues/617
34
+ def unfuck_sti
35
+ draft.draftable_type = self.class.to_s
36
+ end
37
+
38
+ # Set up the draft object, setting its :data attribute to the serialized
39
+ # hash of this object's attributes.
40
+ #
41
+ def serialize_attributes_to_draft
42
+ if self.draft
43
+ draft.data = self.attributes
44
+ else
45
+ self.build_draft(:data => self.attributes)
46
+ end
47
+ end
48
+
32
49
  # Loop through and create DraftUpload objects for any Carrierwave
33
50
  # uploaders mounted on this draftable object.
34
51
  #
35
52
  # @param [Hash] attrs the attributes to loop through
36
53
  # @return [Array<DraftUpload>] an array of unsaved DraftUpload objects.
37
- def build_draft_uploads(attrs)
38
- draft_uploads = []
39
- attrs.keys.each do |key|
40
- if self.respond_to?(key) && self.send(key).is_a?(CarrierWave::Uploader::Base)
54
+ def build_draft_uploads
55
+ self.attributes.keys.each do |key|
56
+ if (self.respond_to?(key) &&
57
+ self.send(key).is_a?(CarrierWave::Uploader::Base) &&
58
+ self.send(key).file)
41
59
  self.draft.draft_uploads << build_draft_upload(key)
42
60
  end
43
61
  end
44
- draft_uploads
45
62
  end
46
63
 
47
64
  # Get a reference to the CarrierWave uploader mounted on the
@@ -61,5 +78,18 @@ module Drafter
61
78
  draft_upload
62
79
  end
63
80
 
81
+ # Attach the draft object to a parent draft object, if there is one.
82
+ # The parent_draft may be for an Article which has_many :comments,
83
+ # as an example.
84
+ #
85
+ # @param [Draft] parent_draft the draft that this draft is associated with.
86
+ # @param [Symbol] relation the name of the has_many (or has_one) association.
87
+ def attach_to_parent_draft(parent_draft, relation)
88
+ if parent_draft && relation
89
+ draft.parent = parent_draft
90
+ draft.parent_association_name = relation
91
+ end
92
+ end
93
+
64
94
  end
65
95
  end
@@ -9,7 +9,7 @@ module Diffing
9
9
  # [Diffy::Diff].
10
10
  def differences(attr, options={:format => :html})
11
11
  if self.draft
12
- Diffy::Diff.new(self.send(attr), self.draft(attr)).to_s(options[:format])
12
+ Diffy::Diff.new(self.send(attr), self.draft.send(attr)).to_s(options[:format])
13
13
  end
14
14
  end
15
15
 
data/lib/drafter/draft.rb CHANGED
@@ -17,6 +17,15 @@ class Draft < ActiveRecord::Base
17
17
  has_many :draft_uploads
18
18
  belongs_to :draftable, :polymorphic => true
19
19
 
20
+ # Drafts are nestable, e.g. an Article's draft can have multiple
21
+ # Comment drafts attached, and the whole thing can be approved at once.
22
+ #
23
+ belongs_to :parent, :class_name => "Draft"
24
+
25
+ # Looked at from the other end, the parent draft should be able to address
26
+ # its subdrafts.
27
+ #
28
+ has_many :subdrafts, :class_name => "Draft", :foreign_key => "parent_id", :dependent => :destroy # << test that
20
29
 
21
30
  # Store serialized data for the associated draftable as a Hash of
22
31
  # attributes.
@@ -63,4 +72,10 @@ class Draft < ActiveRecord::Base
63
72
  draftabl
64
73
  end
65
74
 
75
+ def approve_subdrafts
76
+ subdrafts.each do |subdraft|
77
+ subdraft.approve!
78
+ end
79
+ end
80
+
66
81
  end
@@ -6,6 +6,10 @@ module Drafter
6
6
  # Overrides the +draftable+ method to define the +draftable?+ class method.
7
7
  module ClassMethods
8
8
  def draftable(options={})
9
+ super(options)
10
+
11
+ cattr_accessor :polymorphic_as
12
+ self.polymorphic_as = options[:polymorphic_as]
9
13
 
10
14
  class << self
11
15
  def draftable?
@@ -0,0 +1,46 @@
1
+ module IdHash
2
+
3
+
4
+ # Checks the state of the object to see whether it's an unapproved draft
5
+ # (in which case it has no :id), or whether it's an approved object which
6
+ # is being edited or updated (in which case we can address it by :id).
7
+ #
8
+ # This is primarily useful in your controllers. Sometimes, you might want to
9
+ # identify the object by its :id; other times, you're dealing with an object
10
+ # that only has a Draft but no actual object identity, in which case you'll
11
+ # want to retrieve the Draft object by its :draft_id, and call
12
+ # @draft.build_draftable on that.
13
+ #
14
+ # @return [Hash] a hash with either :draft_id => this object's draft's id,
15
+ # or :id => this object's id.
16
+ def id_hash
17
+ if !new_record?
18
+ { :id => self.to_param }
19
+ elsif new_record? && draft
20
+ { :draft_id => draft.to_param }
21
+ else
22
+ raise "It's not clear what kind of id you're looking for."
23
+ end
24
+ end
25
+
26
+ # When you're attaching sub-objects to a parent object, you'll sometimes need
27
+ # to send the :id and :draft_id in another context, e.g. as :article_id and/or
28
+ # :draft_article_id.
29
+ #
30
+ #
31
+ # @param [Symbol] sym the symbol you want to inject into your id to provide
32
+ # context.
33
+ # @return [Hash] a hash with e.g. :article_id or :draft_article_id, with the
34
+ # appropriate values set (as per id_hash above).
35
+ def id_hash_as(sym)
36
+ if !new_record?
37
+ { "#{sym}_id".to_sym => self.to_param }
38
+ elsif new_record? && draft
39
+ { "draft_#{sym}_id".to_sym => draft.to_param }
40
+ else
41
+ raise "It's not clear what kind of id you're looking for."
42
+ end
43
+ end
44
+
45
+ end
46
+
@@ -0,0 +1,23 @@
1
+ module Subdrafts
2
+
3
+ private
4
+
5
+ def create_subdrafts
6
+ relations = self.class.approves_drafts_for
7
+ unless relations.empty?
8
+ relations.each do |relation|
9
+ create_subdrafts_for(relation)
10
+ end
11
+ end
12
+ end
13
+
14
+ def create_subdrafts_for(relation)
15
+ objects = self.send(relation)
16
+ unless objects.empty?
17
+ objects.each do |object|
18
+ object.save_draft(self.draft, relation)
19
+ end
20
+ end
21
+ end
22
+
23
+ end
data/lib/drafter.rb CHANGED
@@ -16,6 +16,8 @@ module Drafter
16
16
  autoload :Draft
17
17
  autoload :Draftable
18
18
  autoload :DraftUpload
19
+ autoload :IdHash
20
+ autoload :Subdrafts
19
21
 
20
22
  class << self
21
23
  delegate :config, :configure, :to => Draft
@@ -23,12 +25,26 @@ module Drafter
23
25
  end
24
26
 
25
27
  included do
26
- include Apply
27
- include Creation
28
- include Diffing
29
28
  include Draftable
30
29
  end
31
30
 
31
+ module ClassMethods
32
+
33
+ def draftable(options={})
34
+ include Apply
35
+ include Creation
36
+ include Diffing
37
+ include Draftable
38
+ include IdHash
39
+ include Subdrafts
40
+ end
41
+
42
+ def approves_drafts_for(*associations)
43
+ cattr_accessor :approves_drafts_for
44
+ self.approves_drafts_for = associations
45
+ end
46
+ end
47
+
32
48
  end
33
49
 
34
50
  ActiveRecord::Base.class_eval{ include Drafter }
@@ -9,6 +9,42 @@ class TestCreation < Minitest::Unit::TestCase
9
9
  :upload => file_upload)
10
10
  end
11
11
 
12
+ describe "for an unsaved article" do
13
+ before do
14
+ @draft = @article.save_draft
15
+ end
16
+
17
+ describe "with no comments" do
18
+ before do
19
+ @article = @draft.build_draftable
20
+ @article.apply_draft
21
+ end
22
+
23
+ it "should restore the article, unsaved" do
24
+ assert @article.new_record?
25
+ assert_equal(Article, @article.class)
26
+ end
27
+ end
28
+
29
+ describe "with comments" do
30
+ before do
31
+ @article = @draft.build_draftable
32
+ @article.comments.build(:text => "I'm a comment on a draft article")
33
+ @draft = @article.save_draft
34
+ @article = @draft.build_draftable
35
+ end
36
+
37
+ it "should restore the article, unsaved" do
38
+ assert @article.new_record?
39
+ assert_equal(Article, @article.class)
40
+ end
41
+
42
+ it "should restore the comments" do
43
+ assert_equal(1, @article.comments.length)
44
+ end
45
+ end
46
+ end
47
+
12
48
  describe "to an existing article" do
13
49
  before do
14
50
  @article.save!
@@ -38,11 +74,67 @@ class TestCreation < Minitest::Unit::TestCase
38
74
  assert_equal("changed", @article.text)
39
75
  end
40
76
 
41
- it "should restore the draft upload" do
77
+ it "should restore the draft upload" do
42
78
  assert_equal("bar.txt", @article.upload.filename)
43
79
  end
80
+
81
+ describe "which has a subdraft attached" do
82
+ before do
83
+ @article.comments.build(:text => "I'm a comment", :upload => file_upload)
84
+ @draft_count = Draft.count
85
+ @article.save_draft
86
+ @reloaded = Article.find(@article.to_param)
87
+ end
88
+
89
+ it "should not yet have any comments attached" do
90
+ assert_equal(0, @reloaded.comments.length)
91
+ end
92
+
93
+ describe "applying the draft" do
94
+ before do
95
+ @reloaded.apply_draft
96
+ end
97
+
98
+ it "should be able to restore the comment" do
99
+ assert_equal(1, @reloaded.comments.length)
100
+ end
101
+ end
102
+ end
103
+
104
+ describe "which has two subdrafts attached" do
105
+ before do
106
+ @article.comments.build(:text => "I'm a comment", :upload => file_upload)
107
+ @article.comments.build(:text => "I'm another comment", :upload => file_upload("bar.txt"))
108
+ @draft_count = Draft.count
109
+ @article.save_draft
110
+ @reloaded = Article.find(@article.to_param)
111
+ end
112
+
113
+ it "should not yet have any comments attached" do
114
+ assert_equal(0, @reloaded.comments.length)
115
+ end
116
+
117
+ describe "applying the draft" do
118
+ before do
119
+ @reloaded.apply_draft
120
+ end
121
+
122
+ it "should be able to restore the comment" do
123
+ assert_equal(2, @reloaded.comments.length)
124
+ end
125
+
126
+ it "should properly restore the first file, foo.txt" do
127
+ contents = File.new(@reloaded.comments.first.upload.path).read
128
+ assert_equal(file_upload.read, contents)
129
+ end
130
+
131
+ it "should properly restore the second file, bar.txt" do
132
+ contents = File.new(@reloaded.comments.last.upload.path).read
133
+ assert_equal(file_upload("bar.txt").read, contents)
134
+ end
135
+ end
136
+ end
44
137
  end
45
138
  end
46
139
  end
47
-
48
140
  end
@@ -61,7 +61,6 @@ class TestCreation < Minitest::Unit::TestCase
61
61
  it "should update the DraftUpload in place" do
62
62
  assert_equal(@draft_upload_count, DraftUpload.count)
63
63
  end
64
-
65
64
  end
66
65
  end
67
66
 
@@ -159,6 +158,47 @@ class TestCreation < Minitest::Unit::TestCase
159
158
  assert_equal(1, @article.errors.count)
160
159
  end
161
160
  end
161
+
162
+ # We may need to save this for later.
163
+ #
164
+ # If we could get these tests working, we could call save_draft on any
165
+ # object in an association chain and have it save properly both up and
166
+ # down the chain.
167
+ #
168
+ # For the moment, it should work if you save from the top of the chain.
169
+ # Given the time constraints right now, let's use that for the moment.
170
+ #
171
+ # describe "a class which may belongs_to something else, e.g. a Comment" do
172
+ # describe "when no associated class is set, e.g. there's no Article" do
173
+ # before do
174
+ # @comment = Comment.new(:text => "foo")
175
+ # @draft_count = Draft.count
176
+ # @comment.save_draft
177
+ # end
178
+
179
+ # it "should save a draft" do
180
+ # assert_equal(@draft_count + 1, Draft.count)
181
+ # end
182
+ # end
183
+
184
+ # describe "when there's an article" do
185
+ # before do
186
+ # @article = Article.create(:text => "I'm an article")
187
+ # @comment = Comment.new(:text => "I belong to the article")
188
+ # @comment.article = @article
189
+ # @draft_count = Draft.count
190
+ # @comment.save_draft
191
+ # end
192
+
193
+ # it "should save a draft" do
194
+ # assert_equal(@draft_count + 1, Draft.count)
195
+ # end
196
+
197
+ # it "should add the @comment to the @article's subdrafts" do
198
+ # assert @article.draft.subdrafts.include? Draft.last
199
+ # end
200
+ # end
201
+ # end
162
202
  end
163
203
 
164
204
  end
@@ -26,9 +26,13 @@ class TestDrafter < MiniTest::Unit::TestCase
26
26
  end
27
27
 
28
28
  describe "for a string attribute" do
29
+ it "should not nullify the @article" do
30
+ assert @article
31
+ end
29
32
  # return an html diff string for "foo" and "superfoo"
30
33
  it "should return an HTML diff between the draft and live object" do
31
- assert (@article.reload.differences(:text)).include?(%Q(<li class=\"del\"><del>f<strong>oo</strong></del></li>))
34
+
35
+ assert (@article.reload.differences(:text)).include?(%Q(<li class=\"ins\"><ins><strong>super</strong>foo</ins></li>))
32
36
  end
33
37
  end
34
38
  end