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 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 compatibilty for some time. Thanks to
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
- More Later
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 &lt; AR::Base
86
+ has_archive :allow_multiple_archives=&gt; 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 &lt; AR::Base
99
+ has_archive :associate_with_original=&gt;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 &lt; AR:Base
118
+ has\_archive :included\_modules=&gt;BigBadModule
119
+ end
120
+ </pre>
121
+ or if multiple modules are needed
122
+ <pre>
123
+ class MyModel &lt; AR:Base
124
+ has\_archive :included\_modules=&gt;[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 &lt; 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
- v1.0
17
-
18
- * <del>License</del>
19
- * <del>Base Module</del>
20
- * <del> Inserting Subclass (SomeModel::Archive) </del>
21
- * <del> Archive method </del>
22
- * <del> Intercept destroy/deletes </del>
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
+
@@ -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 base.connection.class.to_s.include?("Mysql")
10
+ if connection_class.include?("mysql")
10
11
  base.send(:extend, MySQL)
11
- elsif base.connection.class.to_s.include?("Postgresql")
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.content_columns
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
@@ -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.class.copy_to_archive({:id=>self.id}) unless new_record?
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 untill after all aliases are done
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
- self.transaction do # I would hate for something to happen in the middle of all of this
78
- attrs = m.attributes.merge(:deleted_at=>DateTime.now)
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
- attrs = m.attributes.reject{|k,v| k=="deleted_at"}
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
@@ -1,4 +1,4 @@
1
- #require dependancies eventhough starting a rails app will already have them in place
1
+ #require dependencies even though starting a rails app will already have them in place
2
2
  require 'rubygems'
3
3
  gem 'activerecord','~>3.0.1' #enforce rails 3+
4
4
  require 'active_record'
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: 19
5
- prerelease: false
4
+ hash: 29
5
+ prerelease:
6
6
  segments:
7
7
  - 1
8
8
  - 0
9
- - 2
10
- version: 1.0.2
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: 2010-11-11 00:00:00 -06:00
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.3.7
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