has_draft 1.0.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.
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,10 @@
1
+ == [2010-10-15] 1.0.1 Minor fix to homepage URL
2
+
3
+ == [2010-10-15] 1.0.0 Major Updates
4
+
5
+ * Finally updated to be a Gem
6
+ * Internal refactoring
7
+ * Converted tests to RSpec
8
+ * Added support for Rails 3.0
9
+
10
+ == 0.1.0 Initial Version
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 [name of plugin creator]
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,154 @@
1
+ == Has Draft
2
+
3
+ Allows for multiple "drafts" of a model which can be useful when developing:
4
+ * Draft/Live Version of Pages, for examples
5
+ * A workflow system whereby a live copy may need to be active while a draft copy is
6
+ awaiting approval.
7
+
8
+ This was built to be able to be tacked on to existing models, so the data schema doesn't need to change
9
+ at all for the model this is applied to. Drafts are actually stored in a nearly-identical table
10
+ and there is a has_one relationship to this. This separation allows the base model to really be treated
11
+ just as before without having to apply conditions in queries to make sure you are really getting the
12
+ "live" (non-draft) copy: Page.all will still only return the non-draft pages. This separate table is backed by
13
+ a model created on the fly as a constant on the original model class. For example if a Page has_draft,
14
+ a Page::Draft class will exist as the model for the page_drafts table.
15
+
16
+ == Installation
17
+
18
+ This gem requires ActiveRecord 3.0.
19
+
20
+ In your Gemfile:
21
+
22
+ gem "has_draft"
23
+
24
+ == Basic Example
25
+
26
+ ## First Migration (If Creating base model and drafts at the same time):
27
+ class InitialSchema < ActiveRecord::Migration
28
+
29
+ [:articles, :article_drafts].each do |table_name|
30
+ create_table table_name, :force => true do |t|
31
+ t.references :article if table_name == :article_drafts
32
+
33
+ t.string :title
34
+ t.text :summary
35
+ t.text :body
36
+ t.date :post_date
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+
43
+ ## Model Class
44
+ class Article < ActiveRecord::Base
45
+ has_draft
46
+ end
47
+
48
+ ## Exposed Class Methods & Scopes:
49
+ Article.draft_class
50
+ => Article::Draft
51
+ Article.with_draft.all
52
+ => (Articles that have an associated draft)
53
+ Article.without_draft.all
54
+ => (Articles with no associated draft)
55
+
56
+
57
+
58
+ ## Usage Examples:
59
+
60
+ article = Article.create(
61
+ :title => "My Title",
62
+ :summary => "Information here.",
63
+ :body => "Full body",
64
+ :post_date => Date.today
65
+ )
66
+
67
+ article.has_draft?
68
+ => false
69
+
70
+ article.instantiate_draft!
71
+
72
+ article.has_draft?
73
+ => true
74
+
75
+ article.draft
76
+ => Article::Draft Instance
77
+
78
+ article.draft.update_attributes(
79
+ :title => "New Title"
80
+ )
81
+
82
+ article.replace_with_draft!
83
+
84
+ article.title
85
+ => "New Title"
86
+
87
+ article.destroy_draft!
88
+
89
+ article.has_draft?
90
+ => false
91
+
92
+ == Custom Options
93
+
94
+ ## First Migration (If Creating base model and drafts at the same time):
95
+ class InitialSchema < ActiveRecord::Migration
96
+
97
+ [:articles, :article_copies].each do |table_name|
98
+ create_table table_name, :force => true do |t|
99
+ t.integer :news_article_id if table_name == :article_copies
100
+
101
+ t.string :title
102
+ t.text :summary
103
+ t.text :body
104
+ t.date :post_date
105
+ end
106
+ end
107
+
108
+ end
109
+
110
+ ## Model Class
111
+ class Article < ActiveRecord::Base
112
+ has_draft :class_name => 'Copy', :foreign_key => :news_article_id, :table_name => 'article_copies'
113
+ end
114
+
115
+ == Method Callbacks
116
+
117
+ There are three callbacks you can specify directly as methods:
118
+
119
+ class Article < ActiveRecord::Base
120
+ has_draft
121
+
122
+ def before_instantiate_draft
123
+ # Do Something
124
+ end
125
+
126
+ def before_replace_with_draft
127
+ # Do Something
128
+ end
129
+
130
+ def before_destroy_draft
131
+ # Do Something
132
+ end
133
+ end
134
+
135
+
136
+
137
+ == Extending the Draft Class
138
+
139
+ Because you don't directly define the draft class, you can specify a block of code to be run in its
140
+ context by passing a block to has_draft:
141
+
142
+ class Article < ActiveRecord::Base
143
+ belongs_to :user
144
+
145
+ has_draft do
146
+ belongs_to :last_updated_user
147
+
148
+ def approve!
149
+ self.approved_at = Time.now
150
+ self.save
151
+ end
152
+ end
153
+
154
+ end
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require "rubygems"
2
+ require "rake"
3
+ require "rake/rdoctask"
4
+
5
+ desc "Generate documentation for the plugin."
6
+ Rake::RDocTask.new(:rdoc) do |rdoc|
7
+ rdoc.rdoc_dir = "rdoc"
8
+ rdoc.title = "has_draft"
9
+ rdoc.options << "--line-numbers" << "--inline-source"
10
+ rdoc.rdoc_files.include('README')
11
+ rdoc.rdoc_files.include('lib/**/*.rb')
12
+ end
13
+
14
+ Dir["#{File.dirname(__FILE__)}/lib/tasks/*.rake"].sort.each { |ext| load ext }
data/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ require "has_draft"
2
+
3
+ HasDraft::Railtie.insert
@@ -0,0 +1,19 @@
1
+ module HasDraft
2
+ if defined?(Rails::Railtie)
3
+ require "rails"
4
+
5
+ class Railtie < Rails::Railtie
6
+ initializer "has_draft.extend_active_record" do
7
+ ActiveSupport.on_load(:active_record) do
8
+ HasDraft::Railtie.insert
9
+ end
10
+ end
11
+ end
12
+ end
13
+
14
+ class Railtie
15
+ def self.insert
16
+ ActiveRecord::Base.send(:include, HasDraft)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ module HasDraft
2
+ VERSION = "1.0.1"
3
+ end
data/lib/has_draft.rb ADDED
@@ -0,0 +1,120 @@
1
+ module HasDraft
2
+ def self.included(base)
3
+ base.extend ClassMethods
4
+ end
5
+
6
+ module ClassMethods
7
+ def has_draft(options = {}, &block)
8
+ return if self.included_modules.include?(HasDraft::InstanceMethods)
9
+ include HasDraft::InstanceMethods
10
+
11
+ cattr_accessor :draft_class_name, :draft_foreign_key, :draft_table_name, :draft_columns
12
+
13
+ self.draft_class_name = options[:class_name] || 'Draft'
14
+ self.draft_foreign_key = options[:foreign_key] || self.to_s.foreign_key
15
+ self.draft_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_drafts#{table_name_suffix}"
16
+
17
+ # Create Relationship to Draft Copy
18
+ class_eval do
19
+ has_one :draft, :class_name => "#{self.to_s}::#{draft_class_name}",
20
+ :foreign_key => draft_foreign_key,
21
+ :dependent => :destroy
22
+
23
+ named_scope :with_draft, :include => [:draft], :conditions => "#{draft_table_name}.id IS NOT NULL"
24
+ named_scope :without_draft, :include => [:draft], :conditions => "#{draft_table_name}.id IS NULL"
25
+ end
26
+
27
+ # Dynamically Create Model::Draft Class
28
+ const_set(draft_class_name, Class.new(ActiveRecord::Base))
29
+
30
+ draft_class.cattr_accessor :original_class
31
+ draft_class.original_class = self
32
+ draft_class.set_table_name(draft_table_name)
33
+
34
+ # Draft Parent Association
35
+ draft_class.belongs_to self.to_s.demodulize.underscore.to_sym, :class_name => "::#{self.to_s}", :foreign_key => draft_foreign_key
36
+
37
+ # Block extension
38
+ draft_class.class_eval(&block) if block_given?
39
+
40
+ # Finally setup which columns to draft
41
+ self.draft_columns = draft_class.new.attributes.keys - [draft_class.primary_key, draft_foreign_key, 'created_at', 'updated_at', inheritance_column]
42
+ end
43
+
44
+ def draft_class
45
+ const_get(draft_class_name)
46
+ end
47
+ end
48
+
49
+ module InstanceMethods
50
+ def has_draft?
51
+ !self.draft.nil?
52
+ end
53
+
54
+ def save_to_draft(perform_validation = true)
55
+ return false if perform_validation and !self.valid?
56
+
57
+ instantiate_draft! unless has_draft?
58
+ copy_attributes_to_draft
59
+
60
+ self.draft.save(perform_validation)
61
+ self.reload
62
+ end
63
+
64
+ def instantiate_draft
65
+ self.draft = self.build_draft
66
+
67
+ copy_attributes_to_draft
68
+ before_instantiate_draft
69
+
70
+ self.draft
71
+ end
72
+
73
+ def instantiate_draft!
74
+ returning instantiate_draft do |draft|
75
+ draft.save unless self.new_record?
76
+ end
77
+ end
78
+
79
+ def copy_attributes_to_draft
80
+ self.class.draft_columns.each do |attribute|
81
+ self.draft.send("#{attribute}=", send(attribute))
82
+ end
83
+ self
84
+ end
85
+
86
+ def copy_attributes_from_draft
87
+ self.class.draft_columns.each do |attribute|
88
+ self.send("#{attribute}=", self.draft.send(attribute))
89
+ end
90
+ self
91
+ end
92
+
93
+ def before_instantiate_draft
94
+ end
95
+
96
+ def replace_with_draft!
97
+ copy_attributes_from_draft
98
+
99
+ before_replace_with_draft
100
+
101
+ self.save unless self.new_record?
102
+ self
103
+ end
104
+
105
+ def before_replace_with_draft
106
+ end
107
+
108
+ def destroy_draft!
109
+ before_destroy_draft
110
+
111
+ self.draft.destroy if self.draft
112
+ self.draft(true)
113
+ end
114
+
115
+ def before_destroy_draft
116
+ end
117
+ end
118
+ end
119
+
120
+ require "has_draft/railtie"
@@ -0,0 +1,21 @@
1
+ sqlite:
2
+ adapter: sqlite
3
+ database: spec/db/test.sqlite
4
+
5
+ sqlite3:
6
+ adapter: sqlite3
7
+ database: spec/db/test.sqlite3
8
+
9
+ postgresql:
10
+ adapter: postgresql
11
+ username: postgres
12
+ password: postgres
13
+ database: has_draft_plugin_test
14
+ min_messages: ERROR
15
+
16
+ mysql:
17
+ adapter: mysql
18
+ host: localhost
19
+ username: root
20
+ password:
21
+ database: has_draft_plugin_test
data/spec/db/schema.rb ADDED
@@ -0,0 +1,14 @@
1
+ ActiveRecord::Schema.define(:version => 0) do
2
+
3
+ [:articles, :article_drafts].each do |table_name|
4
+ create_table table_name, :force => true do |t|
5
+ t.references :article if table_name == :article_drafts
6
+
7
+ t.string :title
8
+ t.text :summary
9
+ t.text :body
10
+ t.date :post_date
11
+ end
12
+ end
13
+
14
+ end
Binary file
@@ -0,0 +1,20 @@
1
+ require "factory_girl"
2
+
3
+ Factory.define(:article) do |f|
4
+ f.title { Faker::Lorem.sentence }
5
+ f.summary { Faker::Lorem.paragraphs.first }
6
+ f.body { Faker::Lorem.paragraphs.join("\n\n") }
7
+ f.post_date { 1.day.ago.to_date }
8
+ end
9
+
10
+ Factory.define(:article_draft, :class => Article::Draft) do |f|
11
+ f.association(:article)
12
+ f.title { Faker::Lorem.sentence }
13
+ f.summary { Faker::Lorem.paragraphs.first }
14
+ f.body { Faker::Lorem.paragraphs.join("\n\n") }
15
+ f.post_date { 1.day.ago.to_date }
16
+ end
17
+
18
+ Factory.define(:article_with_draft, :parent => :article) do |f|
19
+ f.association(:draft, :factory => :article_draft)
20
+ end
@@ -0,0 +1,79 @@
1
+ require "spec_helper"
2
+
3
+ describe HasDraft do
4
+ context "Model with has_draft" do
5
+ it "should expose #draft_class_name as Draft" do
6
+ Article.draft_class_name.should == "Draft"
7
+ end
8
+
9
+ it "should expose #draft_class as Article::Draft" do
10
+ Article.draft_class.should == Article::Draft
11
+ end
12
+
13
+ it "should default #draft_foreign_key as article_id" do
14
+ Article.draft_foreign_key.should == "article_id"
15
+ end
16
+
17
+ it "should default #draft_table_name as article_drafts" do
18
+ Article.draft_table_name.should == "article_drafts"
19
+ end
20
+
21
+ describe "Draft Model" do
22
+ it "should be defined under the Article namespace" do
23
+ Article.constants.should include('Draft')
24
+ end
25
+
26
+ it "should expose #original_class as Article" do
27
+ Article::Draft.original_class.should == Article
28
+ end
29
+ end
30
+ end
31
+
32
+ context "an article" do
33
+ before { @article = Factory.create(:article) }
34
+
35
+ context "when instantiating a new draft" do
36
+ before { @article.instantiate_draft! }
37
+
38
+ it "should create draft" do
39
+ @article.draft.should_not be_nil
40
+ @article.draft.should_not be_new_record
41
+ end
42
+
43
+ it "should copy draft fields" do
44
+ Article.draft_columns.each do |column|
45
+ @article.send(column).should == @article.draft.send(column)
46
+ end
47
+ end
48
+ end
49
+
50
+ context "when destroying an existing draft" do
51
+ before do
52
+ @article = Factory.create(:article_with_draft)
53
+ @article.destroy_draft!
54
+ end
55
+
56
+ it "should associated draft" do
57
+ @article.draft.should be_nil
58
+ end
59
+ end
60
+
61
+ context "when replacing with draft" do
62
+ before do
63
+ @article = Factory.create(:article_with_draft)
64
+ @article.replace_with_draft!
65
+ end
66
+
67
+ it "should still have draft" do
68
+ @article.draft.should_not be_nil
69
+ end
70
+
71
+ it "should now have the same field values as draft" do
72
+ Article.draft_columns.each do |column|
73
+ @article.send(column).should == @article.draft.send(column)
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ end
@@ -0,0 +1,25 @@
1
+ require "rubygems"
2
+ require "rspec"
3
+ require "factory_girl"
4
+ require "faker"
5
+ require "rails"
6
+ require "active_record"
7
+ require "active_support"
8
+
9
+ # Establish DB Connection
10
+ config = YAML::load(IO.read(File.join(File.dirname(__FILE__), 'db', 'database.yml')))
11
+ ActiveRecord::Base.configurations = {'test' => config[ENV['DB'] || 'sqlite3']}
12
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
13
+
14
+ # Load Test Schema into the Database
15
+ load(File.dirname(__FILE__) + "/db/schema.rb")
16
+
17
+ require File.dirname(__FILE__) + '/../init'
18
+
19
+ # Example has_draft Model:
20
+ class Article < ActiveRecord::Base
21
+ has_draft
22
+ end
23
+
24
+ # Load Factories:
25
+ Dir[File.join(File.dirname(__FILE__), "factories/**/*.rb")].each {|f| require f}
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: has_draft
3
+ version: !ruby/object:Gem::Version
4
+ hash: 21
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 1
10
+ version: 1.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Ben Hughes
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-10-15 00:00:00 +08:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: activesupport
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 7
30
+ segments:
31
+ - 3
32
+ - 0
33
+ - 0
34
+ version: 3.0.0
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: activerecord
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 7
46
+ segments:
47
+ - 3
48
+ - 0
49
+ - 0
50
+ version: 3.0.0
51
+ type: :runtime
52
+ version_requirements: *id002
53
+ description: Allows for your ActiveRecord models to have drafts which are stored in a separate duplicate table.
54
+ email: ben@railsgarden.com
55
+ executables: []
56
+
57
+ extensions: []
58
+
59
+ extra_rdoc_files: []
60
+
61
+ files:
62
+ - lib/has_draft/railtie.rb
63
+ - lib/has_draft/version.rb
64
+ - lib/has_draft.rb
65
+ - spec/db/database.yml
66
+ - spec/db/schema.rb
67
+ - spec/db/test.sqlite3
68
+ - spec/factories/article.rb
69
+ - spec/has_draft_spec.rb
70
+ - spec/spec_helper.rb
71
+ - CHANGELOG.rdoc
72
+ - LICENSE
73
+ - Rakefile
74
+ - README.rdoc
75
+ - init.rb
76
+ has_rdoc: true
77
+ homepage: http://github.com/rubiety/has_draft
78
+ licenses: []
79
+
80
+ post_install_message:
81
+ rdoc_options: []
82
+
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ hash: 3
91
+ segments:
92
+ - 0
93
+ version: "0"
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ hash: 19
100
+ segments:
101
+ - 1
102
+ - 3
103
+ - 4
104
+ version: 1.3.4
105
+ requirements: []
106
+
107
+ rubyforge_project: has_draft
108
+ rubygems_version: 1.3.7
109
+ signing_key:
110
+ specification_version: 3
111
+ summary: Attached draft model to your ActiveRecord models.
112
+ test_files: []
113
+