revisions 0.0.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/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Rajkumar
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,103 @@
1
+ = revisions
2
+
3
+ Revisions provides a simple, wordpress-like, interface for tracking revisions to a model within a single table. It's a bit rudimentary now, but I hope to add to it as necessary.
4
+
5
+ The basics:
6
+ * Stores drafts, published items, and revisions
7
+ *
8
+
9
+ == INSTALLATION
10
+
11
+ sudo gem install revisions --source http://gemcutter.org
12
+
13
+ then add to environment.rb
14
+
15
+ config.gem 'revisions', :source => 'http://gemcutter.org'
16
+
17
+
18
+ == USAGE
19
+
20
+ The gem is a bit rigid at the moment, so you'll need to setup two columns for it to use:
21
+
22
+ t.string :status # draft, published, revision
23
+ t.integer :revision_of # Foreign Key that tracks what a model is revising.
24
+
25
+ You may also want to index these fields.
26
+
27
+ Once your models are setup, need to declare revisions within any model you intend to track. ie.
28
+
29
+ class Response < ActiveRecord::Base
30
+ has_revisions
31
+ end
32
+
33
+ You can also declare variables that you DON'T want to track (like a slug that has to be unique on the table, publication date, etc.)
34
+
35
+ class Response < ActiveRecord::Base
36
+ has_revisions :ignore => ['published_at', 'slug']
37
+ end
38
+
39
+ In addition to the variables you declare, revisions will ignore the status, id, revision_of, and created_at fields.
40
+
41
+ Now you're all set to save and apply revisions. Revisions exposes three methods: latest_revision, pending_revisions?, save_revision and apply_revision.
42
+
43
+ latest_revision:
44
+ * returns the latest revision
45
+
46
+ pending_revisions:
47
+ * returns true if there are revisions with a more recent updated_at than the model.
48
+
49
+ save_revision:
50
+ * If your model is a draft, it simply saves the model.
51
+ * If your model is published, it saves a revision (does not modify the core model)
52
+ * OG model doesn't change, so all associations are intact
53
+ * Behaves like standard ActiveRecord save (ie. returns true/false if it saves succesfully and creates an errors array if not)
54
+ * can be called with bang (save_revision!) to throw an exception if it doesn't save.
55
+
56
+ apply_revision
57
+ * If called without params, maps the latest revision onto the model, but DOES NOT SAVE.
58
+ * If called with a revision, maps that revision, but doesn't save
59
+ * if called with bang (apply_revision!), maps a revision AND saves
60
+
61
+ How about some examples.
62
+
63
+ Saving a Revision:
64
+
65
+ response = Response.create({:title => 'donkey', :status => 'published'})
66
+ response.title = 'monkey'
67
+ response.save_revision # => true
68
+ response.revisions.size # => 1
69
+ response.pending_revision? # => true
70
+
71
+ save_revision doesn't affect the original response
72
+
73
+ response.reload
74
+ revision = response.latest_revision # => new response object.
75
+ response.title # => 'donkey'
76
+ revision.title # => 'monkey'
77
+
78
+ apply_revision will affect the original response
79
+
80
+ response.apply_revision # => true
81
+ response.title # => 'monkey'
82
+
83
+
84
+
85
+ ==Final Suggestion
86
+
87
+ Since revisions all live within the same table, you probably want to setup a default scope on your model so you don't access revisions directly.
88
+
89
+ default_scope :conditions => "status <> 'revision'"
90
+
91
+
92
+ == LIMITATIONS
93
+
94
+ There are a few things on the to-do list, among them:
95
+
96
+ * There's no way to prune revisions yet. But that's coming ASAP.
97
+ * Revisions assumes your model uses a standard rails Table Name (ie Response => Responses)
98
+ * Revisions requires the status and revision_of columns (for now)
99
+
100
+
101
+ ==Author
102
+
103
+ Brian Hamman, hamman+github [ @ ] gmail.com
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "revisions"
8
+ gem.summary = %Q{A lightweight way to handle Wordpress-like revisions}
9
+ gem.description = %Q{Save and apply revisions to a model, while keeping track of old revisiions}
10
+ gem.email = "hamman@gmail.com"
11
+ gem.homepage = "http://github.com/hamman/revisions"
12
+ gem.authors = ["Brian Hamman"]
13
+ gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19
+ end
20
+
21
+ require 'rake/testtask'
22
+ Rake::TestTask.new(:test) do |test|
23
+ test.libs << 'lib' << 'test'
24
+ test.pattern = 'test/**/test_*.rb'
25
+ test.verbose = true
26
+ end
27
+
28
+ begin
29
+ require 'rcov/rcovtask'
30
+ Rcov::RcovTask.new do |test|
31
+ test.libs << 'test'
32
+ test.pattern = 'test/**/test_*.rb'
33
+ test.verbose = true
34
+ end
35
+ rescue LoadError
36
+ task :rcov do
37
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
38
+ end
39
+ end
40
+
41
+ task :test => :check_dependencies
42
+
43
+ task :default => :test
44
+
45
+ require 'rake/rdoctask'
46
+ Rake::RDocTask.new do |rdoc|
47
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "revisions #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('lib/**/*.rb')
53
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0
data/lib/revisions.rb ADDED
@@ -0,0 +1,96 @@
1
+ module Revisions
2
+ module ClassMethods
3
+
4
+ STATUSES = ['draft', 'published', 'revision']
5
+
6
+ def has_revisions opts={}
7
+ class_inheritable_accessor :unrevised_attributes
8
+
9
+ has_many :revisions,
10
+ :class_name => self.name,
11
+ :conditions => "status='revision'",
12
+ :foreign_key => 'revision_of'
13
+
14
+ include InstanceMethods
15
+
16
+ self.unrevised_attributes = opts[:ignore] || []
17
+ self.unrevised_attributes.concat ['revision_of', 'status', 'created_at']
18
+ end
19
+
20
+ end
21
+ module InstanceMethods
22
+
23
+ def self.included(klass)
24
+ klass.extend(ClassMethods)
25
+ end
26
+
27
+ def published?
28
+ status == 'published'
29
+ end
30
+
31
+ def revision?
32
+ status == 'revision'
33
+ end
34
+
35
+ def draft?
36
+ status == 'draft'
37
+ end
38
+
39
+ def save_revision
40
+ if self.published?
41
+ new_copy = self.clone
42
+ attributes_to_nil = {}
43
+ self.unrevised_attributes.each {|a| attributes_to_nil[a] = nil }
44
+ new_copy.attributes=attributes_to_nil
45
+ new_copy.created_at = new_copy.updated_at = Time.zone.now
46
+ new_copy.status = 'revision'
47
+ new_copy.revision_of = self.id
48
+ if new_copy.save
49
+ true
50
+ else
51
+ new_copy.errors.each {|attribute,message| self.errors.add(attribute,message)}
52
+ false
53
+ end
54
+ else
55
+ save
56
+ end
57
+ end
58
+
59
+ def save_revision!
60
+ save_revision || raise(ActiveRecord::RecordNotSaved)
61
+ end
62
+
63
+ def latest_revision
64
+ revisions.last
65
+ end
66
+
67
+ def pending_revisions?
68
+ !latest_revision.nil? && latest_revision.updated_at > self.updated_at
69
+ end
70
+
71
+ # maps a revision's changes onto the main object
72
+ def apply_revision(revision=nil)
73
+ revision = latest_revision if revision.nil?
74
+ unless revision.nil?
75
+ revised_attributes = revision.attributes.reject {|k,v| self.unrevised_attributes.include?(k) || k == 'id'}
76
+ self.attributes=revised_attributes
77
+ return true
78
+ end
79
+ false
80
+ end
81
+
82
+ def apply_revision!(revision=nil)
83
+ if apply_revision(revision)
84
+ save!
85
+ else
86
+ raise RuntimeError.new("No revision to apply!")
87
+ end
88
+ end
89
+
90
+ end
91
+ end
92
+
93
+
94
+ if defined?(ActiveRecord)
95
+ ActiveRecord::Base.instance_eval { extend Revisions::ClassMethods }
96
+ end
data/revisions.gemspec ADDED
@@ -0,0 +1,58 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{revisions}
8
+ s.version = "0.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Brian Hamman"]
12
+ s.date = %q{2010-04-29}
13
+ s.description = %q{Save and apply revisions to a model, while keeping track of old revisiions}
14
+ s.email = %q{hamman@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "lib/revisions.rb",
27
+ "revisions.gemspec",
28
+ "test/helper.rb",
29
+ "test/models.rb",
30
+ "test/schema.rb",
31
+ "test/test_revisions.rb"
32
+ ]
33
+ s.homepage = %q{http://github.com/hamman/revisions}
34
+ s.rdoc_options = ["--charset=UTF-8"]
35
+ s.require_paths = ["lib"]
36
+ s.rubygems_version = %q{1.3.6}
37
+ s.summary = %q{A lightweight way to handle Wordpress-like revisions}
38
+ s.test_files = [
39
+ "test/helper.rb",
40
+ "test/models.rb",
41
+ "test/schema.rb",
42
+ "test/test_revisions.rb"
43
+ ]
44
+
45
+ if s.respond_to? :specification_version then
46
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
47
+ s.specification_version = 3
48
+
49
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
50
+ s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
51
+ else
52
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
53
+ end
54
+ else
55
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
56
+ end
57
+ end
58
+
data/test/helper.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ class Test::Unit::TestCase
6
+ end
7
+
8
+
9
+ # You can use "rake test AR_VERSION=2.0.5" to test against 2.0.5, for example.
10
+ # The default is to use the latest installed ActiveRecord.
11
+ if ENV["AR_VERSION"]
12
+ gem 'activerecord', "#{ENV["AR_VERSION"]}"
13
+ gem 'activesupport', "#{ENV["AR_VERSION"]}"
14
+ gem 'iridesco-time-warp', :lib => 'time_warp', :source => "http://gems.github.com"
15
+ end
16
+
17
+ require 'active_record'
18
+ require 'active_support'
19
+ require 'time_warp'
20
+
21
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
22
+ require 'revisions'
23
+
24
+ ActiveRecord::Base.establish_connection :adapter => "sqlite3", :database => ":memory:"
25
+ silence_stream(STDOUT) do
26
+ load(File.dirname(__FILE__) + "/schema.rb")
27
+ end
28
+
29
+ require 'models'
data/test/models.rb ADDED
@@ -0,0 +1,4 @@
1
+ class Response < ActiveRecord::Base
2
+ validates_presence_of :title
3
+ has_revisions :ignore => ['slug']
4
+ end
data/test/schema.rb ADDED
@@ -0,0 +1,13 @@
1
+ ActiveRecord::Schema.define(:version => 1) do
2
+ create_table "responses", :force => true do |t|
3
+ t.text "body"
4
+ t.string "title"
5
+ t.string "slug"
6
+ t.string "intro"
7
+ t.datetime "published_at"
8
+ t.datetime "created_at"
9
+ t.datetime "updated_at"
10
+ t.string "status"
11
+ t.integer "revision_of"
12
+ end
13
+ end
@@ -0,0 +1,203 @@
1
+ require 'helper'
2
+
3
+ class TestRevisions < Test::Unit::TestCase
4
+
5
+ Time.zone = 'Eastern Time (US & Canada)'
6
+
7
+
8
+ context "a response" do
9
+
10
+ setup do
11
+ @response = Response.create({
12
+ :title => "This is a post about Donkey",
13
+ :slug => "this-is-a-post-about-donkey",
14
+ :body => "This is the body of Donkey",
15
+ })
16
+ end
17
+
18
+ context "When saving with revisions" do
19
+
20
+ setup do
21
+ @response.status = 'published'
22
+ @response.intro = "donkey intro"
23
+ pretend_now_is(5.minutes.from_now) do
24
+ assert @response.save_revision
25
+ @revision = @response.latest_revision
26
+ end
27
+ end
28
+
29
+ should "just save to the same record when in draft mode" do
30
+ @response.revisions.delete_all
31
+ @response.status = 'draft'
32
+ assert @response.save_revision
33
+ assert_equal 0, @response.revisions.size
34
+ end
35
+
36
+ should "save a revision that copies all fields" do
37
+ assert_equal @revision.title, @response.title
38
+ assert_equal @revision.body, @response.body
39
+ end
40
+
41
+ should "set more updated timestamps" do
42
+ assert @revision.created_at > @response.created_at
43
+ assert @revision.updated_at > @response.updated_at
44
+ end
45
+
46
+ should "say it's a revision of the first guy" do
47
+ assert @revision.revision_of = @response.id
48
+ end
49
+
50
+ should "set the status to revision" do
51
+ assert_equal 'revision', @revision.status
52
+ end
53
+
54
+ should "copy attributes not yet saved to the DB" do
55
+ @response.reload
56
+ assert_equal "donkey intro", @revision.intro
57
+ assert_not_equal @revision.intro, @response.intro
58
+ end
59
+
60
+ should "not copy ignored attributes" do
61
+ assert_not_equal @revision.slug, @response.slug
62
+ end
63
+
64
+ should "return true when it saves" do
65
+ assert @response.save_revision
66
+ end
67
+
68
+ should "return false when it doesn't save" do
69
+ @response.title = nil
70
+ assert_equal false, @response.save_revision
71
+ end
72
+
73
+ should "attach errors to the base object if a revision save fails" do
74
+ @response.title = nil
75
+ @response.save_revision
76
+ assert_equal "can't be blank", @response.errors.on(:title)
77
+ end
78
+
79
+ should "return true when it saves!" do
80
+ assert @response.save_revision!
81
+ end
82
+
83
+ should "raise an exception when it saves! and has bad data" do
84
+ assert_raises ActiveRecord::RecordNotSaved do
85
+ @response.title = nil
86
+ @response.save_revision!
87
+ end
88
+ end
89
+
90
+ end
91
+
92
+ context "when accessing revisions" do
93
+
94
+ should "return null if there aren't revisions" do
95
+ assert_equal nil, @response.latest_revision
96
+ end
97
+
98
+ should "show if there are no pending revisions if none exist" do
99
+ assert_equal false, @response.pending_revisions?
100
+ end
101
+
102
+ context "with a revision" do
103
+ setup do
104
+ @response.status = 'published'
105
+ pretend_now_is(5.minutes.from_now) do
106
+ @response.save_revision!
107
+ @response.save_revision!
108
+ end
109
+ end
110
+
111
+ should "return the revision if there is one" do
112
+ latest = Response.find(:first, :conditions => 'status=\'revision\'', :order => 'id DESC')
113
+ assert_equal latest.id, @response.latest_revision.id
114
+ end
115
+
116
+ should "say there are pending revisions if there are some" do
117
+ assert @response.pending_revisions?
118
+ end
119
+
120
+ should "say there aren't pending revisions if they are old" do
121
+ @response.updated_at = 2.days.from_now
122
+ assert_equal false, @response.pending_revisions?
123
+ end
124
+
125
+
126
+ end
127
+
128
+
129
+
130
+ end
131
+
132
+ context "when applying a revision" do
133
+
134
+ setup do
135
+ @response.update_attribute(:status, 'published')
136
+ assert @response.save_revision
137
+ @revision = @response.latest_revision
138
+ @revision.title = "new revision title"
139
+ @revision.body = "new revision body"
140
+ @revision.slug = "new-donkey-slug"
141
+ @revision.save
142
+ end
143
+
144
+ should "copy over parameters we want (title, body, etc.)" do
145
+ assert @response.apply_revision
146
+ assert_equal @revision.title, @response.title
147
+ assert_equal @revision.body, @response.body
148
+ end
149
+
150
+ should "do nothing (ie not raise an exception)" do
151
+ @response.revisions.delete_all
152
+ assert_equal false, @response.apply_revision
153
+ end
154
+
155
+ should "not copy over parameters we want to keep (slug, status, revision_of, etc.)" do
156
+ @response.apply_revision
157
+ assert_equal 'published', @response.status
158
+ assert_equal nil, @response.revision_of
159
+ assert_not_equal @response.slug, @revision.slug
160
+ end
161
+
162
+ should "not save the response" do
163
+ @response.apply_revision
164
+ @response.reload
165
+ assert_not_equal @revision.title, @response.title
166
+ end
167
+
168
+ should "save the response when using bang" do
169
+ @response.apply_revision!
170
+ @response.reload
171
+ assert_equal @revision.title, @response.title
172
+ assert_equal 'published', @response.status
173
+ assert_equal "this-is-a-post-about-donkey", @response.slug #still keep og slug
174
+ end
175
+
176
+ should "throw an exception on bang when no response" do
177
+ @response.revisions.delete_all
178
+ assert_raises RuntimeError do
179
+ @response.apply_revision!
180
+ end
181
+ end
182
+
183
+ context "with multiple revisions" do
184
+ setup do
185
+ @response.save_revision
186
+ @newer_revision = @response.latest_revision
187
+ @newer_revision.update_attribute(:title, 'even newer title')
188
+ end
189
+
190
+ should "copy over the latest revision if nothing passed" do
191
+ @response.apply_revision
192
+ assert_equal @newer_revision.title, @response.title
193
+ end
194
+
195
+ should "use a selected revision if passed" do
196
+ @response.apply_revision(@revision)
197
+ assert_equal @revision.title, @response.title
198
+ end
199
+ end
200
+
201
+ end
202
+ end
203
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: revisions
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 0
9
+ version: 0.0.0
10
+ platform: ruby
11
+ authors:
12
+ - Brian Hamman
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-04-29 00:00:00 -04:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: thoughtbot-shoulda
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :development
31
+ version_requirements: *id001
32
+ description: Save and apply revisions to a model, while keeping track of old revisiions
33
+ email: hamman@gmail.com
34
+ executables: []
35
+
36
+ extensions: []
37
+
38
+ extra_rdoc_files:
39
+ - LICENSE
40
+ - README.rdoc
41
+ files:
42
+ - .document
43
+ - .gitignore
44
+ - LICENSE
45
+ - README.rdoc
46
+ - Rakefile
47
+ - VERSION
48
+ - lib/revisions.rb
49
+ - revisions.gemspec
50
+ - test/helper.rb
51
+ - test/models.rb
52
+ - test/schema.rb
53
+ - test/test_revisions.rb
54
+ has_rdoc: true
55
+ homepage: http://github.com/hamman/revisions
56
+ licenses: []
57
+
58
+ post_install_message:
59
+ rdoc_options:
60
+ - --charset=UTF-8
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ segments:
68
+ - 0
69
+ version: "0"
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ segments:
75
+ - 0
76
+ version: "0"
77
+ requirements: []
78
+
79
+ rubyforge_project:
80
+ rubygems_version: 1.3.6
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: A lightweight way to handle Wordpress-like revisions
84
+ test_files:
85
+ - test/helper.rb
86
+ - test/models.rb
87
+ - test/schema.rb
88
+ - test/test_revisions.rb