archivist 1.0.2 → 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +151 -22
- data/lib/archivist/archive.rb +53 -0
- data/lib/archivist/base/db.rb +11 -3
- data/lib/archivist/base.rb +92 -17
- data/lib/archivist.rb +1 -1
- metadata +7 -6
data/README.md
CHANGED
@@ -3,31 +3,160 @@ README.md
|
|
3
3
|
|
4
4
|
This gem is intended as a direct replacement for acts\_as\_archive (AAA)
|
5
5
|
in Rails 3 apps with most of the same functionality and wrapping AAA's
|
6
|
-
methods in aliases to maintain
|
6
|
+
methods in aliases to maintain compatibility for some time. Thanks to
|
7
7
|
[Winton Welsh](https://github.com/winton "Winton on github") for his
|
8
8
|
original work on AAA, it is good solution to a problem that makes
|
9
9
|
maintaining audit records a breeze.
|
10
10
|
|
11
|
-
|
11
|
+
TOC
|
12
|
+
---
|
13
|
+
1. <a href="#requirements">Requirements</a>
|
14
|
+
2. <a href="#install">Install</a>
|
15
|
+
2. <a href="#update_models">Update models</a>
|
16
|
+
2. <a href="#add_archive_tables">Add Archive tables</a>
|
17
|
+
1. <a href="#basic_usage">Basic Usage</a>
|
18
|
+
1. <a href="#additional_options">Additional Options</a>
|
19
|
+
1. <a href="#multiple_archives">Allowing multiple archived copies</a>
|
20
|
+
1. <a href="#associating">Associating archive to original</a>
|
21
|
+
1. <a href="#including">Including External modules</a>
|
22
|
+
1. <a href="#customizing">Customizing `copy_self_to_archive`</a>
|
23
|
+
1. <a href="#contributing">Contributing</a>
|
24
|
+
1. <a href="#todo">TODO</a>
|
12
25
|
|
26
|
+
<a name="requirements"></a>
|
27
|
+
Requirements
|
28
|
+
------------
|
29
|
+
This gem is intended to be used with ActiveRecord/ActiveSupport 3.0.1 and later.
|
30
|
+
|
31
|
+
<a name="install"></a>
|
32
|
+
Install
|
33
|
+
-------
|
34
|
+
**Gemfile**:
|
35
|
+
gem 'archivist'
|
36
|
+
|
37
|
+
<a name="update_models"></a>
|
38
|
+
Update models
|
39
|
+
-------------
|
40
|
+
add `has_archive` to your models:
|
41
|
+
class SomeModel < ActiveRecord::Base
|
42
|
+
has_archive
|
43
|
+
end
|
44
|
+
|
45
|
+
N.B. if you have any serialized attributes the has\_archive declaration *MUST* be after the serialization declarations or they will not be preserved and things will break when you try to deserialize the attributes. This is an unfortunate side effect of the declarative nature of both `serialize` and `has_archive` and I couldn't think of any way of getting around it that wouldn't make the entire gem really slow, any suggestions on this are welcome.
|
46
|
+
|
47
|
+
i.e.
|
48
|
+
class AnotherModel < ActiveRecord::Base
|
49
|
+
serialize(:some_array,Array)
|
50
|
+
has_archive
|
51
|
+
end
|
52
|
+
|
53
|
+
*NOT*
|
54
|
+
class ThisModel < ActiveRecord::Base
|
55
|
+
has_archive
|
56
|
+
serialize(:a_hash,Hash)
|
57
|
+
end
|
58
|
+
|
59
|
+
<a name="add_archive_tables"></a>
|
60
|
+
Add Archive tables
|
61
|
+
------------------
|
62
|
+
There are two ways to do this, the first is to use the built in updater like acts as archive.
|
63
|
+
`Archivist.update SomeModel`
|
64
|
+
Currently this doesn't support adding indexes automatically (AAA does) but I'm working on doing multi column indexes (any help is greatly appreciated)
|
65
|
+
|
66
|
+
The second way of adding archive tables is to build a migration yourself, if you're wanting to keep track of who triggered the archive or inject some other information you'll have to add those columns manually and pass a block into `copy_self_to_archive` before calling `delete!` or `destroy!`.
|
67
|
+
|
68
|
+
<a name="basic_usage"></a>
|
69
|
+
Basic Usage
|
70
|
+
-----------
|
71
|
+
Use `destroy`, `delete`, `destroy_all` as usual and the data will get moved to the archive table. If you really want the data to go away you can still do so by simply calling `destroy!` etc. This bypasses the archiving step but leaves your callback chain intact where appropriate.
|
72
|
+
|
73
|
+
Migrations affecting the columns on the original model's table will be applied to the archive table as well.
|
74
|
+
|
75
|
+
<a name="additional_options"></a>
|
76
|
+
Additional Options
|
77
|
+
------------------
|
78
|
+
<a name="multiple_archives"></a>
|
79
|
+
###Allowing multiple archived copies
|
80
|
+
By default `copy_self_to_archive` just keeps updating a single instance of the archived record, this behavior is find if you're just trying to keep your main working table clean but can be problematic if you need a history of changes to a record.
|
81
|
+
This behavior can be changed to allow multiple copies of a archived record to be created by setting the `:allow_multiple_archives` to true in the options hash when calling `has_archive`.
|
82
|
+
|
83
|
+
####Example:
|
84
|
+
<pre>
|
85
|
+
class SpecialModel < AR::Base
|
86
|
+
has_archive :allow_multiple_archives=> true
|
87
|
+
end
|
88
|
+
</pre>
|
89
|
+
|
90
|
+
<a name="associating"></a>
|
91
|
+
### Associating archive to original
|
92
|
+
The default here is to not associate the archived records in any way to the originals. But, if you're keeping a history of changes to a record the archived copies can be associated automatically with the 'original' by setting the `associate_with_original` option to true.
|
93
|
+
|
94
|
+
*N.B.* Using this option automatically sets `allow_multiple_archives` to true
|
95
|
+
|
96
|
+
####Example
|
97
|
+
<pre>
|
98
|
+
class SpecialModel < AR::Base
|
99
|
+
has_archive :associate_with_original=>true
|
100
|
+
end
|
101
|
+
</pre>
|
102
|
+
|
103
|
+
allows for calls like:
|
104
|
+
|
105
|
+
`SpecialModel.first.archived_special_models`
|
106
|
+
|
107
|
+
or
|
108
|
+
|
109
|
+
`SpecialModel::Archive.first.special_model`
|
110
|
+
|
111
|
+
<a name="including"></a>
|
112
|
+
###Including External modules
|
113
|
+
If you want to include additional functionality in the `Archive` class you can pass in an array of module constants, this will allow adding scopes and methods to this class without any monkey patching.
|
114
|
+
|
115
|
+
####Example
|
116
|
+
<pre>
|
117
|
+
class MyModel < AR:Base
|
118
|
+
has\_archive :included\_modules=>BigBadModule
|
119
|
+
end
|
120
|
+
</pre>
|
121
|
+
or if multiple modules are needed
|
122
|
+
<pre>
|
123
|
+
class MyModel < AR:Base
|
124
|
+
has\_archive :included\_modules=>[BigBadModule,MyScopes,MyArchiveMethods]
|
125
|
+
end
|
126
|
+
</pre>
|
127
|
+
|
128
|
+
<a name="customizing"></a>
|
129
|
+
###Customizing copy\_self\_to\_archive
|
130
|
+
A block can be passed into `copy_self_to_archive` which takes a single argument (the new archived record)
|
131
|
+
|
132
|
+
####Example:
|
133
|
+
Supposing we have added an archiver\_id column to our archive table we can pass a block into the `copy_self_to_archive` method setting this value. The block gets called immediately before saving the archived record so all of the attributes have been copied over from the original and are available for use in the block.
|
134
|
+
<pre>
|
135
|
+
class SpecialModel < AR:Base
|
136
|
+
has_archive
|
137
|
+
def archive!(user)
|
138
|
+
self.copy_self_to_archive do |archive|
|
139
|
+
archive.archiver_id = user.id
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
</pre>
|
144
|
+
|
145
|
+
<a name="contributing"></a>
|
146
|
+
Contributing
|
147
|
+
------------
|
148
|
+
If you'd like to help out please feel free to fork and browse the TODO list below or add a feature that you're in need of. Then send a pull request my way and I'll happily merge in well tested changes.
|
149
|
+
|
150
|
+
Also, I use autotest and MySQL but [nertzy (Grant Hutchins)](https://github.com/nertzy "Grant on github") was kind enough to add support for testing against Postgres using the pg gem.
|
151
|
+
|
152
|
+
<a name="todo"></a>
|
13
153
|
TODO
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
*
|
19
|
-
* <del>
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
* <del>Restore archive</del>
|
24
|
-
* <del> Build archive table for existing models </del>
|
25
|
-
* <del>Migrations Module</del>
|
26
|
-
* <del>rewrite method_missing to act on the archived table</del>
|
27
|
-
|
28
|
-
Future
|
29
|
-
|
30
|
-
* give subclass Archive its parent's methods
|
31
|
-
* give Archive relations
|
32
|
-
* give Archive scopes
|
33
|
-
* make archive\_all method chain-able with scopes
|
154
|
+
----
|
155
|
+
|
156
|
+
* <del>Maintain seralized attributes from original model</del>
|
157
|
+
* <del>allow passing of a block into copy\_to\_archive</del>
|
158
|
+
* give Archive scopes from parent (may only work w/ 1.9 since scopes are Procs)
|
159
|
+
* <del>give subclass Archive its parent's methods (method\_missing?)</del>
|
160
|
+
* <del>associate SomeModel::Archive with SomeModel (if archiving more than one copy)</del>
|
161
|
+
* associate Archive with other models (SomeModel.reflect\_on\_all\_associations?)
|
162
|
+
* make archive\_all method chain-able with scopes and other finder type items
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Archivist
|
2
|
+
module ArchiveMethods
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
extend ArchiveClassMethods
|
6
|
+
protected :get_klass,:get_klass_name,:get_klass_instance_methods,:build_proxy_method
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def method_missing(method,*args,&block)
|
11
|
+
if get_klass_instance_methods.include?(method.to_s)
|
12
|
+
build_proxy_method(method.to_s)
|
13
|
+
self.method(method).call(*args,&block)
|
14
|
+
else
|
15
|
+
super(method,*args,&block)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def respond_to?(method,include_private=false)
|
20
|
+
if get_klass_instance_methods.include?(method.to_s)
|
21
|
+
return true
|
22
|
+
else
|
23
|
+
super(method,include_private)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def get_klass
|
28
|
+
@klass ||= Kernel.const_get(get_klass_name)
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_klass_name
|
32
|
+
@klass_name ||= self.class.to_s.split("::").first
|
33
|
+
end
|
34
|
+
|
35
|
+
def get_klass_instance_methods
|
36
|
+
@klass_instance_methods ||= get_klass.instance_methods(false)
|
37
|
+
end
|
38
|
+
|
39
|
+
def build_proxy_method(method_name)
|
40
|
+
class_eval <<-EOF
|
41
|
+
def #{method_name}(*args,&block)
|
42
|
+
instance = #{get_klass_name}.new
|
43
|
+
attrs = self.attributes.select{|k,v| #{get_klass.new.attribute_names.inspect}.include?(k.to_s)}
|
44
|
+
instance.attributes= attrs,false
|
45
|
+
instance.#{method_name}(*args,&block)
|
46
|
+
end
|
47
|
+
EOF
|
48
|
+
end
|
49
|
+
|
50
|
+
module ArchiveClassMethods;end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
data/lib/archivist/base/db.rb
CHANGED
@@ -5,10 +5,11 @@ module Archivist
|
|
5
5
|
def self.included(base)
|
6
6
|
base.send(:extend, ClassMethods)
|
7
7
|
base.send(:include, InstanceMethods)
|
8
|
+
connection_class = base.connection.class.to_s.downcase
|
8
9
|
|
9
|
-
if
|
10
|
+
if connection_class.include?("mysql")
|
10
11
|
base.send(:extend, MySQL)
|
11
|
-
elsif
|
12
|
+
elsif connection_class.include?("postgresql")
|
12
13
|
base.send(:extend, PostgreSQL)
|
13
14
|
else
|
14
15
|
raise "DB type not supported by Archivist!"
|
@@ -22,14 +23,21 @@ module Archivist
|
|
22
23
|
|
23
24
|
def create_archive_table
|
24
25
|
if table_exists? && !archive_table_exists?
|
25
|
-
cols = self.
|
26
|
+
cols = self.columns.reject { |column| column.name == primary_key }
|
26
27
|
connection.create_table("archived_#{table_name}")
|
27
28
|
cols.each do |c|
|
28
29
|
connection.add_column("archived_#{table_name}",c.name,c.type)
|
29
30
|
end
|
30
31
|
connection.add_column("archived_#{table_name}",:deleted_at,:datetime)
|
32
|
+
if archive_options[:associate_with_original]
|
33
|
+
connection.add_column("archived_#{table_name}","#{self.new.class.to_s.underscore}_id",:integer)
|
34
|
+
end
|
31
35
|
end
|
32
36
|
end
|
37
|
+
|
38
|
+
def create_archive_indexes
|
39
|
+
# TODO?
|
40
|
+
end
|
33
41
|
end
|
34
42
|
|
35
43
|
module InstanceMethods
|
data/lib/archivist/base.rb
CHANGED
@@ -6,12 +6,18 @@ end
|
|
6
6
|
|
7
7
|
module Archivist
|
8
8
|
module Base
|
9
|
+
|
10
|
+
DEFAULT_OPTIONS = {:associate_with_original=>false,:allow_multiple_archives=>false}
|
11
|
+
|
9
12
|
def self.included(base)
|
10
13
|
base.extend ClassMethods
|
11
14
|
end
|
12
15
|
|
13
16
|
module ClassMethods
|
14
17
|
def has_archive(options={})
|
18
|
+
options = DEFAULT_OPTIONS.merge(options)
|
19
|
+
options[:allow_multiple_archives] = true if options[:associate_with_original]
|
20
|
+
|
15
21
|
class_eval <<-EOF
|
16
22
|
alias_method :delete!, :delete
|
17
23
|
|
@@ -22,6 +28,10 @@ module Archivist
|
|
22
28
|
def self.archive_indexes
|
23
29
|
#{Array(options[:indexes]).collect{|i| i.to_s}.inspect}
|
24
30
|
end
|
31
|
+
|
32
|
+
def self.archive_options
|
33
|
+
#{options.inspect}
|
34
|
+
end
|
25
35
|
|
26
36
|
def self.has_archive?
|
27
37
|
true
|
@@ -31,11 +41,21 @@ module Archivist
|
|
31
41
|
warn "DEPRECATION WARNING: #acts_as_archive is provided for compatibility with AAA and will be removed soon, please use has_archive?"
|
32
42
|
has_archive?
|
33
43
|
end
|
44
|
+
|
34
45
|
class Archive < ActiveRecord::Base
|
35
46
|
self.record_timestamps = false
|
36
47
|
self.table_name = "archived_#{self.table_name}"
|
48
|
+
#{build_serialization_strings(self.serialized_attributes)}
|
49
|
+
#{build_belongs_to_association(options[:associate_with_original])}
|
50
|
+
#{build_inclusion_strings(options[:included_modules])}
|
51
|
+
include Archivist::ArchiveMethods
|
37
52
|
end
|
53
|
+
|
54
|
+
#{build_has_many_association(options[:associate_with_original])}
|
55
|
+
|
56
|
+
#{build_copy_self_to_archive(options[:allow_multiple_archives])}
|
38
57
|
EOF
|
58
|
+
|
39
59
|
include InstanceMethods
|
40
60
|
extend ClassExtensions
|
41
61
|
include DB
|
@@ -44,11 +64,75 @@ module Archivist
|
|
44
64
|
def acts_as_archive(options={})
|
45
65
|
has_archive(options)
|
46
66
|
end
|
67
|
+
|
68
|
+
def build_inclusion_strings(included_modules)
|
69
|
+
modules = ""
|
70
|
+
included_modules = [included_modules] unless included_modules.is_a?(Array)
|
71
|
+
included_modules.each do |mod|
|
72
|
+
modules << "include #{mod.to_s}\n"
|
73
|
+
end
|
74
|
+
return modules
|
75
|
+
end
|
76
|
+
|
77
|
+
def build_serialization_strings(serializde_attributes)
|
78
|
+
serializations = ""
|
79
|
+
self.serialized_attributes.each do |key,value|
|
80
|
+
serializations << "serialize(:#{key},#{value.to_s})\n"
|
81
|
+
end
|
82
|
+
return serializations
|
83
|
+
end
|
84
|
+
|
85
|
+
def build_has_many_association(associate=false)
|
86
|
+
associate ? "has_many :archived_#{self.table_name},:class_name=>'#{self.new.class.to_s}::Archive'" : ""
|
87
|
+
end
|
88
|
+
|
89
|
+
def build_belongs_to_association(associate=false)
|
90
|
+
associate ? "belongs_to :#{self.table_name},:class_name=>'#{self.new.class.to_s}'" : ""
|
91
|
+
end
|
92
|
+
|
93
|
+
def build_copy_self_to_archive(allow_multiple=false)
|
94
|
+
if allow_multiple #we put the original pk in the fk instead
|
95
|
+
"def copy_self_to_archive
|
96
|
+
self.class.transaction do
|
97
|
+
attrs = self.attributes.merge(:deleted_at=>DateTime.now)
|
98
|
+
archived = #{self.to_s}::Archive.new(attrs.reject{|k,v| k=='id'})
|
99
|
+
archived.#{self.to_s.underscore}_id = attrs['id']
|
100
|
+
#{yield_and_save}
|
101
|
+
end
|
102
|
+
end"
|
103
|
+
else
|
104
|
+
"def copy_self_to_archive
|
105
|
+
self.class.transaction do #it would be really shitty for us to loose data in the middle of this
|
106
|
+
attrs = self.attributes.merge(:deleted_at=>DateTime.now)
|
107
|
+
archived = #{self.to_s}::Archive.new
|
108
|
+
if archived.class.where(:id=>self.id).empty? #create a new one if necessary, else update
|
109
|
+
archived.id = attrs[\"id\"]
|
110
|
+
archived.attributes = attrs.reject{|k,v| k=='id'}
|
111
|
+
else
|
112
|
+
archived = archived.class.where(:id=>attrs[\"id\"]).first
|
113
|
+
archived.update_attributes(attrs)
|
114
|
+
end
|
115
|
+
#{yield_and_save}
|
116
|
+
end
|
117
|
+
end"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def yield_and_save
|
122
|
+
"yield(archived) if block_given?
|
123
|
+
archived.save"
|
124
|
+
end
|
125
|
+
|
126
|
+
private :build_inclusion_strings,:build_serialization_strings,
|
127
|
+
:build_has_many_association,:build_belongs_to_association,
|
128
|
+
:build_copy_self_to_archive,:yield_and_save
|
47
129
|
end
|
48
130
|
|
49
131
|
module InstanceMethods #these defs can't happen untill after we've aliased their respective originals
|
132
|
+
|
50
133
|
def delete
|
51
|
-
self.
|
134
|
+
result = self.copy_self_to_archive unless new_record?
|
135
|
+
self.delete! if result
|
52
136
|
@destroyed = true
|
53
137
|
freeze
|
54
138
|
end
|
@@ -68,25 +152,14 @@ module Archivist
|
|
68
152
|
end
|
69
153
|
end
|
70
154
|
|
71
|
-
module ClassExtensions #these can't get included in the class def
|
72
|
-
def copy_to_archive(conditions,delete=true)
|
155
|
+
module ClassExtensions #these can't get included in the class def until after all aliases are done
|
156
|
+
def copy_to_archive(conditions,delete=true,&block)
|
73
157
|
where = sanitize_sql(conditions)
|
74
158
|
found = self.where(where)
|
75
159
|
|
76
160
|
found.each do |m|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
if self::Archive.where(:id=>m.id).empty? #create a new one if necessary, else update
|
81
|
-
archived = self::Archive.new
|
82
|
-
archived.id = m.id
|
83
|
-
archived.attributes = attrs.reject{|k,v| k==:id}
|
84
|
-
archived.save
|
85
|
-
else
|
86
|
-
self::Archive.where(:id=>m.id).first.update_attributes(attrs)
|
87
|
-
end
|
88
|
-
m.destroy! if delete
|
89
|
-
end
|
161
|
+
result = m.copy_self_to_archive(&block)
|
162
|
+
m.destroy! if delete && result
|
90
163
|
end
|
91
164
|
end
|
92
165
|
|
@@ -101,7 +174,9 @@ module Archivist
|
|
101
174
|
|
102
175
|
found.each do |m|
|
103
176
|
self.transaction do
|
104
|
-
|
177
|
+
my_attribute_names = self.new.attribute_names
|
178
|
+
#this should be Hash.select but 1.8.7 returns an array from select but a hash from reject... dumb
|
179
|
+
attrs = m.attributes.reject{|k,v| !my_attribute_names.include?(k.to_s)}
|
105
180
|
|
106
181
|
if self.where(:id=>m.id).empty?
|
107
182
|
new_m = self.create(attrs)
|
data/lib/archivist.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: archivist
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
5
|
-
prerelease:
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
6
|
segments:
|
7
7
|
- 1
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 1.0.
|
9
|
+
- 5
|
10
|
+
version: 1.0.5
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Tyler Pickett
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date:
|
18
|
+
date: 2011-02-15 00:00:00 -06:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -61,6 +61,7 @@ extensions: []
|
|
61
61
|
extra_rdoc_files: []
|
62
62
|
|
63
63
|
files:
|
64
|
+
- lib/archivist/archive.rb
|
64
65
|
- lib/archivist/base/db.rb
|
65
66
|
- lib/archivist/base.rb
|
66
67
|
- lib/archivist/migration.rb
|
@@ -97,7 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
97
98
|
requirements: []
|
98
99
|
|
99
100
|
rubyforge_project:
|
100
|
-
rubygems_version: 1.
|
101
|
+
rubygems_version: 1.4.1
|
101
102
|
signing_key:
|
102
103
|
specification_version: 3
|
103
104
|
summary: A rails 3 model archiving system based on acts_as_archive
|