drafter 0.2.8 → 0.3.0

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