sidestate 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENCE +20 -0
- data/README.textile +32 -0
- data/generators/sidestate/sidestate_generator.rb +49 -0
- data/lib/sidestate/hook.rb +41 -0
- data/lib/sidestate.rb +8 -0
- data/spec/generators/sidestate/sidestate_generator_spec.rb +42 -0
- data/spec/sidestate/hook_spec.rb +39 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/support/database.rb +7 -0
- data/spec/support/generators.rb +7 -0
- data/spec/support/migrations.rb +12 -0
- data/spec/support/models.rb +31 -0
- metadata +125 -0
data/LICENCE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Envato
|
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.textile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
h1. SideState
|
2
|
+
|
3
|
+
*SideState* is an extention for "AASM":http://github.com/rubyist/aasm, which stores the state in a separate object (and thus providing historical information on the state changes for a given record).
|
4
|
+
|
5
|
+
h2. Installation
|
6
|
+
|
7
|
+
<pre><code>gem install sidestate</code></pre>
|
8
|
+
|
9
|
+
h2. Usage
|
10
|
+
|
11
|
+
You'll want to set up AASM as normal, with states, events and transitions. To get SideState working nicely, you'll want to include the module of the same name into your model:
|
12
|
+
|
13
|
+
<pre><code>class Article < ActiveRecord::Base
|
14
|
+
include AASM
|
15
|
+
include Sidestate
|
16
|
+
|
17
|
+
# ...
|
18
|
+
end</code></pre>
|
19
|
+
|
20
|
+
Then, you'll want the model and migration generated for the companion state object:
|
21
|
+
|
22
|
+
<pre><code>script/generate sidestate article</code></pre>
|
23
|
+
|
24
|
+
You can add whatever you like to the new model (in this example, it would be called ArticleState), but make sure you don't delete the foreign key and name columns. It's also a good idea to add an index to the foreign key column (the generator isn't smart enough to do this yet).
|
25
|
+
|
26
|
+
h2. Warning
|
27
|
+
|
28
|
+
Very much a beta, and might change dramatically. Seems to work in basic situations, at least. Your mileage may vary.
|
29
|
+
|
30
|
+
h2. Credits
|
31
|
+
|
32
|
+
Copyright (c) 2010 "Envato":http://envato.com/. Initially developed by "Pat Allan":http://freelancing-gods.com. Released under an open licence.
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rails_generator/generators/components/model/model_generator'
|
2
|
+
|
3
|
+
class SidestateGenerator < ModelGenerator
|
4
|
+
def manifest
|
5
|
+
record do |m|
|
6
|
+
m.class_collisions class_path, class_name
|
7
|
+
|
8
|
+
m.directory File.join('app/models', class_path)
|
9
|
+
m.template 'model:model.rb',
|
10
|
+
File.join('app/models', class_path, "#{file_name}.rb")
|
11
|
+
m.migration_template 'model:migration.rb', 'db/migrate',
|
12
|
+
:assigns => {
|
13
|
+
:migration_name => "Create#{class_name.pluralize.gsub(/::/, '')}",
|
14
|
+
:table_name => file_path.gsub(/\//, '_').pluralize,
|
15
|
+
:attributes => [
|
16
|
+
Rails::Generator::GeneratedAttribute.new(:name, :string),
|
17
|
+
Rails::Generator::GeneratedAttribute.new(foreign_key, :integer)
|
18
|
+
]
|
19
|
+
},
|
20
|
+
:migration_file_name => "create_#{file_path.gsub(/\//, '_').pluralize}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def banner
|
27
|
+
"Usage: #{$0} #{spec.name} ModelName"
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_options!(opt)
|
31
|
+
# Do nothing - which removes the normal model generator options
|
32
|
+
end
|
33
|
+
|
34
|
+
def class_name
|
35
|
+
super + "State"
|
36
|
+
end
|
37
|
+
|
38
|
+
def file_name
|
39
|
+
super + "_state"
|
40
|
+
end
|
41
|
+
|
42
|
+
def file_path
|
43
|
+
super + "_state"
|
44
|
+
end
|
45
|
+
|
46
|
+
def foreign_key
|
47
|
+
file_name.gsub(/_state$/, '_id').to_sym
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Sidestate::Hook
|
2
|
+
def self.included(base)
|
3
|
+
base.instance_eval do
|
4
|
+
extend Sidestate::Hook::ClassMethods
|
5
|
+
has_many aasm_state_collection
|
6
|
+
|
7
|
+
after_save :save_aasm_state_object
|
8
|
+
|
9
|
+
define_method(aasm_state_object) do
|
10
|
+
aasm_state_collection.last
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
def aasm_state_collection
|
17
|
+
"#{name}State".pluralize.underscore.to_sym
|
18
|
+
end
|
19
|
+
|
20
|
+
def aasm_state_object
|
21
|
+
"#{name}State".underscore.to_sym
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def save_aasm_state_object
|
28
|
+
return unless send("#{aasm_column}_changed?")
|
29
|
+
|
30
|
+
old_state, new_state = send("#{aasm_column}_change")
|
31
|
+
aasm_state_collection.create :name => new_state.to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
def aasm_state_collection
|
35
|
+
send(self.class.aasm_state_collection)
|
36
|
+
end
|
37
|
+
|
38
|
+
def aasm_column
|
39
|
+
self.class.aasm_column
|
40
|
+
end
|
41
|
+
end
|
data/lib/sidestate.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec/spec_helper'
|
2
|
+
|
3
|
+
describe SidestateGenerator do
|
4
|
+
before :each do
|
5
|
+
Rails::Generator::Scripts::Generate.new.run(
|
6
|
+
['sidestate', 'item'],
|
7
|
+
:destination => 'tmp',
|
8
|
+
:quiet => true
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
after :each do
|
13
|
+
FileUtils.rm_r 'tmp/app'
|
14
|
+
FileUtils.rm_r 'tmp/db'
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should generate a migration for the given model's state class" do
|
18
|
+
Dir['tmp/db/migrate/*_create_item_states*'].should_not be_empty
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should generate the given model's state class" do
|
22
|
+
File.exist?('tmp/app/models/item_state.rb').should be_true
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'migration' do
|
26
|
+
before :each do
|
27
|
+
@file = open(Dir['tmp/db/migrate/*_create_item_states*'].first).read
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should set the table name" do
|
31
|
+
@file.should match(/create_table :item_states/)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should include the name" do
|
35
|
+
@file.should match(/t\.string :name/)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should include the foreign key" do
|
39
|
+
@file.should match(/t\.integer :item_id/)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec/spec_helper'
|
2
|
+
|
3
|
+
describe Sidestate::Hook do
|
4
|
+
context '#article_state' do
|
5
|
+
it "should create a new state when a model object is created" do
|
6
|
+
article = Article.create
|
7
|
+
article.article_state.should_not be_nil
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should set the state accordingly" do
|
11
|
+
Article.create.article_state.name.should == 'draft'
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should track state changes" do
|
15
|
+
Timecop.travel 1.day.ago
|
16
|
+
article = Article.create
|
17
|
+
Timecop.return
|
18
|
+
|
19
|
+
article.publish!
|
20
|
+
article.article_state.name.should == 'published'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context '#article_states' do
|
25
|
+
it "should store all states for a given object" do
|
26
|
+
Timecop.travel 2.days.ago
|
27
|
+
article = Article.create
|
28
|
+
Timecop.return
|
29
|
+
Timecop.travel 1.day.ago
|
30
|
+
article.publish!
|
31
|
+
Timecop.return
|
32
|
+
article.flag!
|
33
|
+
|
34
|
+
article.article_states.collect { |state|
|
35
|
+
state.name
|
36
|
+
}.should == ['draft', 'published', 'flagged']
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'active_record'
|
7
|
+
|
8
|
+
require 'aasm'
|
9
|
+
require 'sidestate'
|
10
|
+
|
11
|
+
require 'spec'
|
12
|
+
require 'spec/autorun'
|
13
|
+
require 'timecop'
|
14
|
+
|
15
|
+
Spec::Runner.configure do |config|
|
16
|
+
Dir['spec/support/*.rb'].sort.each do |file|
|
17
|
+
require file.gsub(/\.rb/, '')
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
ActiveRecord::Base.connection.create_table :articles, :force => true do |t|
|
2
|
+
t.string :subject
|
3
|
+
t.text :content
|
4
|
+
t.string :aasm_state
|
5
|
+
t.timestamps
|
6
|
+
end
|
7
|
+
|
8
|
+
ActiveRecord::Base.connection.create_table :article_states, :force => true do |t|
|
9
|
+
t.integer :article_id
|
10
|
+
t.string :name
|
11
|
+
t.timestamps
|
12
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class Article < ActiveRecord::Base
|
2
|
+
include AASM
|
3
|
+
include Sidestate
|
4
|
+
|
5
|
+
aasm_state :draft
|
6
|
+
aasm_state :published
|
7
|
+
aasm_state :flagged
|
8
|
+
aasm_state :deleted
|
9
|
+
|
10
|
+
aasm_initial_state :draft
|
11
|
+
|
12
|
+
aasm_event :publish do
|
13
|
+
transitions :from => :draft, :to => :published
|
14
|
+
end
|
15
|
+
|
16
|
+
aasm_event :flag do
|
17
|
+
transitions :from => :published, :to => :flagged
|
18
|
+
end
|
19
|
+
|
20
|
+
aasm_event :unflag do
|
21
|
+
transitions :from => :flagged, :to => :published
|
22
|
+
end
|
23
|
+
|
24
|
+
aasm_event :delete do
|
25
|
+
transitions :from => [:draft, :published, :flagged], :to => :deleted
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class ArticleState < ActiveRecord::Base
|
30
|
+
#
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sidestate
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Pat Allan
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-03-11 00:00:00 +11:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: aasm
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 2
|
29
|
+
- 1
|
30
|
+
- 5
|
31
|
+
version: 2.1.5
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: rspec
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
segments:
|
42
|
+
- 1
|
43
|
+
- 2
|
44
|
+
- 9
|
45
|
+
version: 1.2.9
|
46
|
+
type: :development
|
47
|
+
version_requirements: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: yard
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
segments:
|
56
|
+
- 0
|
57
|
+
version: "0"
|
58
|
+
type: :development
|
59
|
+
version_requirements: *id003
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: timecop
|
62
|
+
prerelease: false
|
63
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
segments:
|
68
|
+
- 0
|
69
|
+
- 3
|
70
|
+
- 4
|
71
|
+
version: 0.3.4
|
72
|
+
type: :development
|
73
|
+
version_requirements: *id004
|
74
|
+
description: Track state history using AASM
|
75
|
+
email: pat@freelancing-gods.com
|
76
|
+
executables: []
|
77
|
+
|
78
|
+
extensions: []
|
79
|
+
|
80
|
+
extra_rdoc_files:
|
81
|
+
- README.textile
|
82
|
+
files:
|
83
|
+
- LICENCE
|
84
|
+
- README.textile
|
85
|
+
- generators/sidestate/sidestate_generator.rb
|
86
|
+
- lib/sidestate.rb
|
87
|
+
- lib/sidestate/hook.rb
|
88
|
+
has_rdoc: true
|
89
|
+
homepage: http://github.com/envato/sidestate
|
90
|
+
licenses: []
|
91
|
+
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options:
|
94
|
+
- --charset=UTF-8
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
segments:
|
102
|
+
- 0
|
103
|
+
version: "0"
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
segments:
|
109
|
+
- 0
|
110
|
+
version: "0"
|
111
|
+
requirements: []
|
112
|
+
|
113
|
+
rubyforge_project:
|
114
|
+
rubygems_version: 1.3.6
|
115
|
+
signing_key:
|
116
|
+
specification_version: 3
|
117
|
+
summary: AASM Extention for storing state in a separate model
|
118
|
+
test_files:
|
119
|
+
- spec/generators/sidestate/sidestate_generator_spec.rb
|
120
|
+
- spec/sidestate/hook_spec.rb
|
121
|
+
- spec/spec_helper.rb
|
122
|
+
- spec/support/database.rb
|
123
|
+
- spec/support/generators.rb
|
124
|
+
- spec/support/migrations.rb
|
125
|
+
- spec/support/models.rb
|