brianjlandau-acts_as_archive 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2009 Winton Welsh
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,119 @@
1
+ ActsAsArchive
2
+ =============
3
+
4
+ Don't delete your records, move them to a different table.
5
+
6
+ Like <code>acts\_as\_paranoid</code>, but doesn't mess with your SQL queries.
7
+
8
+ Install
9
+ -------
10
+
11
+ <pre>
12
+ sudo gem install acts_as_archive
13
+ </pre>
14
+
15
+ **environment.rb**:
16
+
17
+ <pre>
18
+ config.gem 'acts_as_archive'
19
+ </pre>
20
+
21
+ Update models
22
+ -------------
23
+
24
+ Add <code>acts\_as\_archive</code> to your models:
25
+
26
+ <pre>
27
+ class Article < ActiveRecord::Base
28
+ acts_as_archive
29
+ end
30
+ </pre>
31
+
32
+ <a name="create_archive_tables"></a>
33
+
34
+ Create archive tables
35
+ ---------------------
36
+
37
+ Add this line to a migration:
38
+
39
+ <pre>
40
+ ActsAsArchive.update Article, Comment
41
+ </pre>
42
+
43
+ Replace <code>Article, Comment</code> with your own models that use <code>acts_as_archive</code>.
44
+
45
+ Archive tables mirror your table's structure, but with an additional <code>deleted_at</code> column.
46
+
47
+ There is an [alternate way to create archive tables](http://wiki.github.com/winton/acts_as_archive/alternatives-to-migrations) if you don't like migrations.
48
+
49
+ That's it!
50
+ ----------
51
+
52
+ Use <code>destroy</code>, <code>delete</code>, and <code>delete_all</code> like you normally would.
53
+
54
+ Records move into the archive table instead of being destroyed.
55
+
56
+ What if my schema changes?
57
+ --------------------------
58
+
59
+ New migrations are automatically applied to the archive table.
60
+
61
+ No action is necessary on your part.
62
+
63
+ Query the archive
64
+ -----------------
65
+
66
+ Add <code>::Archive</code> to your ActiveRecord class:
67
+
68
+ <pre>
69
+ Article::Archive.find(:first)
70
+ </pre>
71
+
72
+ Restore from the archive
73
+ ------------------------
74
+
75
+ Use <code>restore\_all</code> to copy archived records back to your table:
76
+
77
+ <pre>
78
+ Article.restore_all([ 'id = ?', 1 ])
79
+ </pre>
80
+
81
+ Auto-migrate from acts\_as\_paranoid
82
+ ------------------------------------
83
+
84
+ If you previously used <code>acts\_as\_paranoid</code>, the <code>ActsAsArchive.update</code>
85
+ call will automatically move your deleted records to the archive table
86
+ (see <a href="#create_archive_tables">_Create archive tables_</a>).
87
+
88
+ Original <code>deleted_at</code> values are preserved.
89
+
90
+ Add indexes to the archive table
91
+ --------------------------------
92
+
93
+ To keep insertions fast, there are no indexes on your archive table by default.
94
+
95
+ If you are querying your archive a lot, you will want to add indexes:
96
+
97
+ <pre>
98
+ class Article < ActiveRecord::Base
99
+ acts_as_archive :indexes => [ :id, :created_at, :deleted_at ]
100
+ end
101
+ </pre>
102
+
103
+ Call <code>ActsAsArchive.update</code> upon adding new indexes
104
+ (see <a href="#create_archive_tables">_Create archive tables_</a>).
105
+
106
+ Delete records without archiving
107
+ --------------------------------
108
+
109
+ To destroy a record without archiving:
110
+
111
+ <pre>
112
+ article.destroy!
113
+ </pre>
114
+
115
+ To delete multiple records without archiving:
116
+
117
+ <pre>
118
+ Article.delete_all!(["id in (?)", [1,2,3]])
119
+ </pre>
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "#{File.dirname(__FILE__)}/require"
2
+ Require.rakefile!
3
+
4
+ desc "Generate gemspec"
5
+ task :gemspec do
6
+ File.open("#{Rake.original_dir}/acts_as_archive.gemspec", 'w') do |f|
7
+ f.write(Require.gemspec.to_ruby)
8
+ end
9
+ end
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env ruby
2
+ puts `script/runner "ActsAsArchive.update #{ARGV.join ', '}"`
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/rails/init"
@@ -0,0 +1,19 @@
1
+ require File.expand_path("#{File.dirname(__FILE__)}/../require")
2
+ Require.lib!
3
+
4
+ module ActsAsArchive
5
+
6
+ def self.update(*models)
7
+ models.each do |klass|
8
+ if klass.respond_to?(:acts_as_archive?) && klass.acts_as_archive?
9
+ time = Benchmark.measure do
10
+ klass.create_archive_table
11
+ klass.migrate_from_acts_as_paranoid
12
+ klass.create_archive_indexes
13
+ end
14
+ $stdout.puts "-- ActsAsArchive.update(#{models.join(', ')})"
15
+ $stdout.puts " -> #{"%.4fs" % time.real}"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,42 @@
1
+ module ActsAsArchive
2
+
3
+ module Base
4
+ def self.included(base)
5
+ base.extend ActMethods
6
+ end
7
+
8
+ module ActMethods
9
+ def acts_as_archive(options={})
10
+ class_eval <<-end_eval
11
+
12
+ def self.acts_as_archive?
13
+ self.to_s == #{self.to_s.inspect}
14
+ end
15
+
16
+ def self.archive_indexes
17
+ #{Array(options[:indexes]).collect(&:to_s).inspect}
18
+ end
19
+
20
+ if self.descends_from_active_record?
21
+ class Archive < ActiveRecord::Base
22
+ self.record_timestamps = false
23
+ self.table_name = "archived_#{self.table_name}"
24
+ end
25
+ else
26
+ class Archive < self.superclass::Archive
27
+ self.record_timestamps = false
28
+ self.table_name = "archived_#{self.table_name}"
29
+
30
+ def self.sti_name
31
+ "#{self.sti_name}"
32
+ end
33
+ end
34
+ end
35
+ end_eval
36
+ include Destroy
37
+ include Restore
38
+ include Table
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,17 @@
1
+ module ActsAsArchive
2
+ module Base
3
+ module Adapters
4
+ module MySQL
5
+
6
+ private
7
+
8
+ def archive_table_indexed_columns
9
+ index_query = "SHOW INDEX FROM archived_#{table_name}"
10
+ indexes = connection.select_all(index_query).collect do |r|
11
+ r["Column_name"]
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,40 @@
1
+ module ActsAsArchive
2
+ module Base
3
+ module Adapters
4
+ module PostgreSQL
5
+
6
+ private
7
+
8
+ def archive_table_indexed_columns
9
+ # This query comes courtesy of cope360:
10
+ # http://stackoverflow.com/questions/2204058/show-which-columns-an-index-is-on-in-postgresql/2213199#2213199
11
+ index_query = <<-SQL
12
+ select
13
+ t.relname as table_name,
14
+ i.relname as index_name,
15
+ a.attname as column_name
16
+ from
17
+ pg_class t,
18
+ pg_class i,
19
+ pg_index ix,
20
+ pg_attribute a
21
+ where
22
+ t.oid = ix.indrelid
23
+ and i.oid = ix.indexrelid
24
+ and a.attrelid = t.oid
25
+ and a.attnum = ANY(ix.indkey)
26
+ and t.relkind = 'r'
27
+ and t.relname = 'archived_#{table_name}'
28
+ order by
29
+ t.relname,
30
+ i.relname
31
+ SQL
32
+
33
+ indexes = connection.select_all(index_query).collect do |r|
34
+ r["column_name"]
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,71 @@
1
+ module ActsAsArchive
2
+ module Base
3
+ module Destroy
4
+
5
+ def self.included(base)
6
+ unless base.included_modules.include?(InstanceMethods)
7
+ base.class_eval do
8
+ alias_method :destroy_without_callbacks!, :destroy_without_callbacks
9
+ class <<self
10
+ alias_method :delete_all!, :delete_all
11
+ end
12
+ end
13
+ base.send :extend, ClassMethods
14
+ base.send :include, InstanceMethods
15
+ end
16
+ end
17
+
18
+ module ClassMethods
19
+ def copy_to_archive(conditions, import=false)
20
+ add_conditions!(where = '', conditions)
21
+ insert_cols = column_names.clone
22
+ select_cols = column_names.clone
23
+ if insert_cols.include?('deleted_at')
24
+ unless import
25
+ select_cols[select_cols.index('deleted_at')] = "'#{Time.now.utc.to_s(:db)}'"
26
+ end
27
+ else
28
+ insert_cols << 'deleted_at'
29
+ select_cols << "'#{Time.now.utc.to_s(:db)}'"
30
+ end
31
+
32
+ insert_cols.map! { |col| connection.quote_column_name(col) }
33
+ select_cols.map! { |col| col =~ /^\'/ ? col : connection.quote_column_name(col) }
34
+
35
+ connection.execute(%{
36
+ INSERT INTO archived_#{table_name} (#{insert_cols.join(', ')})
37
+ SELECT #{select_cols.join(', ')}
38
+ FROM #{table_name}
39
+ #{where}
40
+ })
41
+ connection.execute("DELETE FROM #{table_name} #{where}")
42
+ end
43
+
44
+ def delete_all(conditions=nil)
45
+ copy_to_archive(conditions)
46
+ end
47
+ end
48
+
49
+ module InstanceMethods
50
+ def destroy_without_callbacks
51
+ unless new_record?
52
+ self.class.copy_to_archive("#{self.class.primary_key} = #{id}")
53
+ end
54
+ @destroyed = true
55
+ freeze
56
+ end
57
+
58
+ def destroy!
59
+ transaction { destroy_with_callbacks! }
60
+ end
61
+
62
+ def destroy_with_callbacks!
63
+ return false if callback(:before_destroy) == false
64
+ result = destroy_without_callbacks!
65
+ callback(:after_destroy)
66
+ result
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,36 @@
1
+ module ActsAsArchive
2
+ module Base
3
+ module Restore
4
+
5
+ def self.included(base)
6
+ unless base.included_modules.include?(InstanceMethods)
7
+ base.send :extend, ClassMethods
8
+ base.send :include, InstanceMethods
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+
14
+ def copy_from_archive(conditions)
15
+ add_conditions!(where = '', conditions)
16
+ col_names = column_names - [ 'deleted_at' ]
17
+ col_names.map! { |col| connection.quote_column_name(col) }
18
+ connection.execute(%{
19
+ INSERT INTO #{table_name} (#{col_names.join(', ')})
20
+ SELECT #{col_names.join(', ')}
21
+ FROM archived_#{table_name}
22
+ #{where}
23
+ })
24
+ connection.execute("DELETE FROM archived_#{table_name} #{where}")
25
+ end
26
+
27
+ def restore_all(conditions=nil)
28
+ copy_from_archive(conditions)
29
+ end
30
+ end
31
+
32
+ module InstanceMethods
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,109 @@
1
+ module ActsAsArchive
2
+ module Base
3
+ module Table
4
+
5
+ def self.included(base)
6
+ unless base.included_modules.include?(InstanceMethods)
7
+ base.send :extend, ClassMethods
8
+ base.send :include, InstanceMethods
9
+
10
+ if base.connection.class.to_s.include?('Mysql')
11
+ base.send :extend, ActsAsArchive::Base::Adapters::MySQL
12
+ elsif base.connection.class.to_s.include?('PostgreSQL')
13
+ base.send :extend, ActsAsArchive::Base::Adapters::PostgreSQL
14
+ else
15
+ raise 'acts_as_archive does not support this database adapter'
16
+ end
17
+ end
18
+ end
19
+
20
+ module ClassMethods
21
+
22
+ def archive_table_exists?
23
+ connection.table_exists?("archived_#{table_name}")
24
+ end
25
+
26
+ def create_archive_table
27
+ if table_exists? && !archive_table_exists?
28
+ connection.execute(%{
29
+ CREATE TABLE archived_#{table_name}
30
+ #{"ENGINE=InnoDB" if connection.class.to_s.include?('Mysql')}
31
+ AS SELECT * from #{table_name}
32
+ WHERE false;
33
+ })
34
+ columns = connection.columns("archived_#{table_name}").collect(&:name)
35
+ unless columns.include?('deleted_at')
36
+ connection.add_column("archived_#{table_name}", :deleted_at, :datetime)
37
+ end
38
+ end
39
+ end
40
+
41
+ def create_archive_indexes
42
+ if archive_table_exists?
43
+ indexes = archive_table_indexed_columns
44
+
45
+ (archive_indexes - indexes).each do |index|
46
+ connection.add_index("archived_#{table_name}", index)
47
+ end
48
+ (indexes - archive_indexes).each do |index|
49
+ connection.remove_index("archived_#{table_name}", index)
50
+ end
51
+ end
52
+ end
53
+
54
+
55
+ def migrate_from_acts_as_paranoid
56
+ if column_names.include?('deleted_at')
57
+ if table_exists? && archive_table_exists?
58
+ condition = "deleted_at IS NOT NULL"
59
+ if self.count_by_sql("SELECT COUNT(*) FROM #{table_name} WHERE #{condition}") > 0
60
+ # Base::Destroy.copy_to_archive
61
+ copy_to_archive(condition, true)
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def archive_table_indexed_columns
70
+ case connection.class.to_s
71
+ when "ActiveRecord::ConnectionAdapters::MysqlAdapter"
72
+ index_query = "SHOW INDEX FROM archived_#{table_name}"
73
+ indexes = connection.select_all(index_query).collect do |r|
74
+ r["Column_name"]
75
+ end
76
+ when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
77
+ #postgresql is...slightly...more complicated
78
+ index_query = <<EOS
79
+ SELECT c2.relname as index_name
80
+ FROM pg_catalog.pg_class c,
81
+ pg_catalog.pg_class c2,
82
+ pg_catalog.pg_index i
83
+ WHERE c.oid = (SELECT c.oid
84
+ FROM pg_catalog.pg_class c
85
+ WHERE c.relname ~ '^(archived_#{table_name})$')
86
+ AND c.oid = i.indrelid
87
+ AND i.indexrelid = c2.oid
88
+ EOS
89
+ indexes = connection.select_all(index_query).collect do |r|
90
+ r["index_name"]
91
+ end
92
+
93
+ # HACK: reverse engineer the column name
94
+ # This sucks, but acts_as_archive only adds indexes on single columns anyway so it should work OK
95
+ # and getting the columns indexed is INCREDIBLY complicated in PostgreSQL.
96
+ indexes.map do |index|
97
+ index.split("_on_").last
98
+ end
99
+ else
100
+ raise "Unsupported Database"
101
+ end
102
+ end
103
+ end
104
+
105
+ module InstanceMethods
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,49 @@
1
+ module ActsAsArchive
2
+ module Migration
3
+
4
+ def self.included(base)
5
+ unless base.included_modules.include?(InstanceMethods)
6
+ base.send :extend, ClassMethods
7
+ base.class_eval do
8
+ class <<self
9
+ unless method_defined?(:method_missing_without_archive)
10
+ alias_method :method_missing_without_archive, :method_missing
11
+ alias_method :method_missing, :method_missing_with_archive
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ module ClassMethods
19
+
20
+ def method_missing_with_archive(method, *arguments, &block)
21
+ args = Marshal.load(Marshal.dump(arguments))
22
+ method_missing_without_archive(method, *arguments, &block)
23
+ supported = [
24
+ :add_column, :add_timestamps, :change_column,
25
+ :change_column_default, :change_table,
26
+ :drop_table, :remove_column, :remove_columns,
27
+ :remove_timestamps, :rename_column, :rename_table
28
+ ]
29
+ if args.include?(:deleted_at) || args.include?('deleted_at')
30
+ # Don't change the archive's deleted_at column
31
+ return
32
+ end
33
+ if !args.empty? && supported.include?(method)
34
+ connection = ActiveRecord::Base.connection
35
+ args[0] = "archived_" + ActiveRecord::Migrator.proper_table_name(args[0])
36
+ if method == :rename_table
37
+ args[1] = "archived_" + args[1].to_s
38
+ end
39
+ if connection.table_exists?(args[0])
40
+ connection.send(method, *args, &block)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ module InstanceMethods
47
+ end
48
+ end
49
+ end
data/rails/init.rb ADDED
@@ -0,0 +1,5 @@
1
+ require File.expand_path("#{File.dirname(__FILE__)}/../require")
2
+ Require.rails_init!
3
+
4
+ ActiveRecord::Base.send(:include, ActsAsArchive::Base)
5
+ ActiveRecord::Migration.send(:include, ActsAsArchive::Migration)
data/require.rb ADDED
@@ -0,0 +1,49 @@
1
+ require 'rubygems'
2
+ gem 'require'
3
+ require 'require'
4
+
5
+ Require do
6
+ gem(:activerecord) { require 'active_record' }
7
+ gem :require, '=0.2.7'
8
+ gem(:rake, '=0.8.7') { require 'rake' }
9
+ gem :rspec, '=1.3.0'
10
+
11
+ gemspec do
12
+ author 'Winton Welsh'
13
+ dependencies do
14
+ gem :require
15
+ end
16
+ email 'mail@wintoni.us'
17
+ name 'brianjlandau-acts_as_archive'
18
+ homepage "http://github.com/winton/#{name}"
19
+ summary "Don't delete your records, move them to a different table"
20
+ version '0.2.6'
21
+ end
22
+
23
+ lib do
24
+ require "lib/acts_as_archive/base"
25
+ require "lib/acts_as_archive/base/adapters/mysql"
26
+ require "lib/acts_as_archive/base/adapters/postgresql"
27
+ require "lib/acts_as_archive/base/destroy"
28
+ require "lib/acts_as_archive/base/restore"
29
+ require "lib/acts_as_archive/base/table"
30
+ require "lib/acts_as_archive/migration"
31
+ end
32
+
33
+ rails_init { require 'lib/acts_as_archive' }
34
+
35
+ rakefile do
36
+ gem(:rake) { require 'rake/gempackagetask' }
37
+ gem(:rspec) { require 'spec/rake/spectask' }
38
+ require 'require/tasks'
39
+ end
40
+
41
+ spec_helper do
42
+ require 'require/spec_helper'
43
+ gem :activerecord
44
+ require 'logger'
45
+ require 'yaml'
46
+ require 'pp'
47
+ require 'rails/init'
48
+ end
49
+ end
@@ -0,0 +1,117 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../../spec_helper")
2
+
3
+ describe ActsAsArchive::Base::Destroy do
4
+
5
+ before(:all) do
6
+ establish_test_db
7
+ Article.create_archive_table
8
+ end
9
+
10
+ describe 'delete_all!' do
11
+
12
+ before(:all) do
13
+ create_records
14
+ end
15
+
16
+ it "should really delete all records" do
17
+ Article.delete_all!
18
+ Article.count.should == 0
19
+ Article::Archive.count.should == 0
20
+ end
21
+
22
+ end
23
+
24
+ describe 'destroy!' do
25
+
26
+ before(:all) do
27
+ create_records
28
+ @article = Article.first
29
+ end
30
+
31
+ it "should really destroy a records" do
32
+ @article.destroy!
33
+ Article::Archive.count.should == 0
34
+ end
35
+
36
+ end
37
+
38
+ describe 'delete_all' do
39
+
40
+ before(:all) do
41
+ @articles = create_records
42
+ end
43
+
44
+ describe 'with conditions' do
45
+
46
+ before(:all) do
47
+ # Mini delete_all parameter test
48
+ Article.delete_all [ 'id = ?', @articles[0].id ]
49
+ Article.delete_all "id = #{@articles[1].id}"
50
+ end
51
+
52
+ it "should move some records to the archive table" do
53
+ Article.count.should == 3
54
+ Article::Archive.count.should == 2
55
+ end
56
+
57
+ it "should preserve record attributes" do
58
+ 2.times do |x|
59
+ original = @articles[x]
60
+ copy = Article::Archive.find(original.id)
61
+ article_match?(original, copy)
62
+ end
63
+ end
64
+ end
65
+
66
+ describe 'without conditions' do
67
+
68
+ before(:all) do
69
+ Article.delete_all
70
+ end
71
+
72
+ it "should move all records to the archive table" do
73
+ Article.count.should == 0
74
+ Article::Archive.count.should == 5
75
+ end
76
+
77
+ it "should preserve record attributes" do
78
+ 5.times do |x|
79
+ original = @articles[x]
80
+ copy = Article::Archive.find(original.id)
81
+ article_match?(original, copy)
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ [ :destroy, :delete ].each do |d|
88
+
89
+ describe d do
90
+
91
+ before(:all) do
92
+ @articles = create_records
93
+ Article.find(@articles[0..1].collect(&:id)).each do |a|
94
+ a.send(d)
95
+ end
96
+ end
97
+
98
+ it "should move some records to the archive table" do
99
+ Article.count.should == 3
100
+ Article::Archive.count.should == 2
101
+ end
102
+
103
+ it "should preserve record attributes" do
104
+ 2.times do |x|
105
+ original = @articles[x]
106
+ copy = Article::Archive.find(original.id)
107
+ article_match?(original, copy)
108
+ end
109
+ end
110
+
111
+ it "should mark the object as destroyed" do
112
+ @articles[3].send(d)
113
+ @articles[3].destroyed?.should == true
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,58 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../../spec_helper")
2
+
3
+ describe ActsAsArchive::Base::Restore do
4
+
5
+ before(:all) do
6
+ establish_test_db
7
+ Article.create_archive_table
8
+ end
9
+
10
+ describe 'restore_all' do
11
+
12
+ before(:all) do
13
+ @articles = create_records(Article::Archive)
14
+ end
15
+
16
+ describe 'with conditions' do
17
+
18
+ before(:all) do
19
+ # Mini restore parameter test
20
+ Article.restore_all [ 'id = ?', @articles[0].id ]
21
+ Article.restore_all "id = #{@articles[1].id}"
22
+ end
23
+
24
+ it "should move some records to the article table" do
25
+ Article::Archive.count.should == 3
26
+ Article.count.should == 2
27
+ end
28
+
29
+ it "should preserve record attributes" do
30
+ 2.times do |x|
31
+ original = @articles[x]
32
+ copy = Article.find(original.id)
33
+ article_match?(original, copy)
34
+ end
35
+ end
36
+ end
37
+
38
+ describe 'without conditions' do
39
+
40
+ before(:all) do
41
+ Article.restore_all
42
+ end
43
+
44
+ it "should move all records to the archive table" do
45
+ Article::Archive.count.should == 0
46
+ Article.count.should == 5
47
+ end
48
+
49
+ it "should preserve record attributes" do
50
+ 5.times do |x|
51
+ original = @articles[x]
52
+ copy = Article.find(original.id)
53
+ article_match?(original, copy)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,74 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../../spec_helper")
2
+
3
+ describe ActsAsArchive::Base::Table do
4
+
5
+ before(:all) do
6
+ establish_test_db
7
+ Article.create_archive_table
8
+ end
9
+
10
+ describe 'create_archive_table' do
11
+
12
+ before(:all) do
13
+ @article_columns = connection.columns("articles").collect(&:name)
14
+ @archive_columns = connection.columns("archived_articles").collect(&:name)
15
+ end
16
+
17
+ it "should create an archive table" do
18
+ connection.table_exists?("archived_articles").should == true
19
+ end
20
+
21
+ it "should create an archive table with the same structure as the original table" do
22
+ @article_columns.each do |col|
23
+ @archive_columns.include?(col).should == true
24
+ end
25
+ end
26
+
27
+ it "should add a deleted_at column to the archive table" do
28
+ (@archive_columns - @article_columns).should == [ 'deleted_at' ]
29
+ end
30
+ end
31
+
32
+ describe 'create_archive_indexes' do
33
+
34
+ before(:all) do
35
+ Article.create_archive_indexes
36
+ end
37
+
38
+ it "should create archive indexes" do
39
+ indexes.to_set.should == [ "id", "deleted_at" ].to_set
40
+ end
41
+
42
+ it "should destroy archive indexes" do
43
+ Article.class_eval { acts_as_archive }
44
+ Article.create_archive_indexes
45
+ indexes.should == []
46
+ end
47
+ end
48
+
49
+ describe 'migrate_from_acts_as_paranoid' do
50
+
51
+ before(:all) do
52
+ connection.add_column(:articles, :deleted_at, :datetime)
53
+ Article.reset_column_information
54
+ end
55
+
56
+ before(:each) do
57
+ connection.execute("DELETE FROM #{Article::Archive.table_name}")
58
+ end
59
+
60
+ it "should move deleted records to the archive" do
61
+ create_records(Article, :deleted_at => Time.now.utc)
62
+ Article.migrate_from_acts_as_paranoid
63
+ Article.count.should == 0
64
+ Article::Archive.count.should == 5
65
+ end
66
+
67
+ it "should not move non-deleted records to the archive" do
68
+ create_records
69
+ Article.migrate_from_acts_as_paranoid
70
+ Article.count.should == 5
71
+ Article::Archive.count.should == 0
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,40 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
2
+
3
+ describe ActsAsArchive::Base do
4
+
5
+ before(:all) do
6
+ establish_test_db
7
+ end
8
+
9
+ describe 'acts_as_archive' do
10
+ it "should add self.acts_as_archive? to the model" do
11
+ Article.respond_to?(:acts_as_archive?).should == true
12
+ end
13
+
14
+ it "should add self.archive_indexes to the model" do
15
+ Article.respond_to?(:archive_indexes).should == true
16
+ Article.archive_indexes.should == [ 'id', 'deleted_at' ]
17
+ end
18
+
19
+ it "should add Archive class to the model" do
20
+ defined?(Article::Archive).should == "constant"
21
+ end
22
+
23
+ describe 'for STI models' do
24
+ before do
25
+ Article.create_archive_table
26
+ @headline = Headline.create(:title => "This is a headline")
27
+ @oped = Opinion.create(:title => "This is an op-ed")
28
+ @headline.destroy
29
+ @oped.destroy
30
+ end
31
+
32
+ it 'should only return a deleted entry for the STI type used' do
33
+ Headline::Archive.all.map(&:id).should include(@headline.id)
34
+ Headline::Archive.all.map(&:id).should_not include(@oped.id)
35
+ Opinion::Archive.all.map(&:id).should_not include(@headline.id)
36
+ Opinion::Archive.all.map(&:id).should include(@oped.id)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,37 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe ActsAsArchive::Migration do
4
+
5
+ before(:each) do
6
+ establish_test_db
7
+ Article.create_archive_table
8
+ end
9
+
10
+ describe 'method_missing_with_archive' do
11
+
12
+ it 'should migrate both tables up' do
13
+ migrate_up
14
+ (@new_article_columns - @old_article_columns).should == [ 'permalink' ]
15
+ (@new_archive_columns - @old_archive_columns).should == [ 'permalink' ]
16
+ end
17
+
18
+ it 'should migrate both tables down' do
19
+ migrate_up
20
+ @old_article_columns = @new_article_columns
21
+ @old_archive_columns = @new_archive_columns
22
+ ActiveRecord::Migrator.migrate("#{SPEC}/db/migrate", 0)
23
+ @new_article_columns = columns("articles")
24
+ @new_archive_columns = columns("archived_articles")
25
+ (@old_article_columns - @new_article_columns).should == [ 'permalink' ]
26
+ (@old_archive_columns - @new_archive_columns).should == [ 'permalink' ]
27
+ end
28
+
29
+ it "should not touch the archive's deleted_at column" do
30
+ connection.add_column(:articles, :deleted_at, :datetime)
31
+ Article.reset_column_information
32
+ migrate_up("migrate_2")
33
+ (@old_article_columns - @new_article_columns).should == [ 'deleted_at' ]
34
+ (@old_archive_columns - @new_archive_columns).should == []
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,6 @@
1
+ test:
2
+ adapter: mysql
3
+ database: acts_as_archive
4
+ username: root
5
+ password:
6
+ host: localhost
@@ -0,0 +1,6 @@
1
+ test:
2
+ adapter: postgresql
3
+ database: acts_as_archive
4
+ username: postgres
5
+ password:
6
+ host: localhost
@@ -0,0 +1,9 @@
1
+ class AddToArticles < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :articles, :permalink, :string
4
+ end
5
+
6
+ def self.down
7
+ remove_column :articles, :permalink
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class AddToArticles < ActiveRecord::Migration
2
+ def self.up
3
+ remove_column :articles, :deleted_at
4
+ end
5
+
6
+ def self.down
7
+ add_column :articles, :deleted_at, :string
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ class Article < ActiveRecord::Base
2
+ acts_as_archive :indexes => [ :id, :deleted_at ]
3
+ end
@@ -0,0 +1,3 @@
1
+ class Headline < Article
2
+ acts_as_archive :indexes => [ :id, :deleted_at ]
3
+ end
@@ -0,0 +1,3 @@
1
+ class Opinion < Article
2
+ acts_as_archive :indexes => [ :id, :deleted_at ]
3
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,89 @@
1
+ require File.expand_path("#{File.dirname(__FILE__)}/../require")
2
+ Require.spec_helper!
3
+
4
+ Spec::Runner.configure do |config|
5
+ end
6
+
7
+ def db_type
8
+ ENV['DB_TYPE'] ? ENV['DB_TYPE'] : 'mysql'
9
+ end
10
+
11
+ def article_match?(original, copy)
12
+ copy.id.should == original.id
13
+ copy.title.should == original.title
14
+ copy.body.should == original.body
15
+ end
16
+
17
+ def columns(table)
18
+ connection.columns(table).collect(&:name)
19
+ end
20
+
21
+ def connection
22
+ ActiveRecord::Base.connection
23
+ end
24
+
25
+ def create_records(klass=Article, values={})
26
+ articles = []
27
+ table = klass.table_name
28
+ cols = columns(table)
29
+ connection.execute("DELETE FROM #{table}")
30
+ (1..5).collect do |x|
31
+ vals = cols.collect do |c|
32
+ if values.keys.include?(c.intern)
33
+ values[c.intern] ? "'#{values[c.intern]}'" : "NULL"
34
+ else
35
+ case c.intern
36
+ when :id; x
37
+ when :deleted_at; 'NULL'
38
+ when :type; klass.sti_name.to_s.inspect
39
+ else "'#{c.capitalize} #{x}'"
40
+ end
41
+ end
42
+ end
43
+ connection.execute(%{
44
+ INSERT INTO #{table} (#{cols.collect { |c| "#{connection.quote_column_name(c)}" }.join(', ')})
45
+ VALUES (#{vals.join(', ')})
46
+ })
47
+ klass.find(x)
48
+ end
49
+ end
50
+
51
+ def establish_test_db
52
+ # Establish connection
53
+ unless ActiveRecord::Base.connected?
54
+ config = YAML::load(File.open("#{SPEC}/db/config/database.#{db_type}.yml"))
55
+ ActiveRecord::Base.configurations = config
56
+ ActiveRecord::Base.establish_connection(config['test'])
57
+ end
58
+ # Establish logger
59
+ logger_file = File.open("#{SPEC}/db/log/test.log", 'a')
60
+ logger_file.sync = true
61
+ @logger = Logger.new(logger_file)
62
+ ActiveRecord::Base.logger = @logger
63
+ # The database should have only a simple articles table
64
+ connection.execute("DROP TABLE IF EXISTS articles")
65
+ connection.execute("DROP TABLE IF EXISTS archived_articles")
66
+ connection.execute("DROP TABLE IF EXISTS schema_migrations")
67
+ connection.create_table(:articles) do |t|
68
+ t.string :title
69
+ t.string :body
70
+ t.string :type
71
+ t.boolean :read # break mysql w/o quotation
72
+ end
73
+ # Load the model
74
+ load "#{SPEC}/db/models/article.rb"
75
+ load "#{SPEC}/db/models/headline.rb"
76
+ load "#{SPEC}/db/models/opinion.rb"
77
+ end
78
+
79
+ def indexes
80
+ Article.send(:archive_table_indexed_columns)
81
+ end
82
+
83
+ def migrate_up(directory='migrate')
84
+ @old_article_columns = columns("articles")
85
+ @old_archive_columns = columns("archived_articles")
86
+ ActiveRecord::Migrator.migrate("#{SPEC}/db/#{directory}")
87
+ @new_article_columns = columns("articles")
88
+ @new_archive_columns = columns("archived_articles")
89
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: brianjlandau-acts_as_archive
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 6
10
+ version: 0.2.6
11
+ platform: ruby
12
+ authors:
13
+ - Winton Welsh
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-12-29 00:00:00 -05:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: require
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - "="
28
+ - !ruby/object:Gem::Version
29
+ hash: 25
30
+ segments:
31
+ - 0
32
+ - 2
33
+ - 7
34
+ version: 0.2.7
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ description:
38
+ email: mail@wintoni.us
39
+ executables:
40
+ - acts_as_archive
41
+ extensions: []
42
+
43
+ extra_rdoc_files:
44
+ - README.markdown
45
+ files:
46
+ - MIT-LICENSE
47
+ - README.markdown
48
+ - Rakefile
49
+ - bin/acts_as_archive
50
+ - init.rb
51
+ - lib/acts_as_archive/base/adapters/mysql.rb
52
+ - lib/acts_as_archive/base/adapters/postgresql.rb
53
+ - lib/acts_as_archive/base/destroy.rb
54
+ - lib/acts_as_archive/base/restore.rb
55
+ - lib/acts_as_archive/base/table.rb
56
+ - lib/acts_as_archive/base.rb
57
+ - lib/acts_as_archive/migration.rb
58
+ - lib/acts_as_archive.rb
59
+ - rails/init.rb
60
+ - require.rb
61
+ - spec/acts_as_archive/base/destroy_spec.rb
62
+ - spec/acts_as_archive/base/restore_spec.rb
63
+ - spec/acts_as_archive/base/table_spec.rb
64
+ - spec/acts_as_archive/base_spec.rb
65
+ - spec/acts_as_archive/migration_spec.rb
66
+ - spec/db/config/database.mysql.yml
67
+ - spec/db/config/database.postgresql.yml
68
+ - spec/db/migrate/001_add_to_articles.rb
69
+ - spec/db/migrate_2/001_add_to_articles.rb
70
+ - spec/db/models/article.rb
71
+ - spec/db/models/headline.rb
72
+ - spec/db/models/opinion.rb
73
+ - spec/spec.opts
74
+ - spec/spec_helper.rb
75
+ has_rdoc: true
76
+ homepage: http://github.com/winton/brianjlandau-acts_as_archive
77
+ licenses: []
78
+
79
+ post_install_message:
80
+ rdoc_options: []
81
+
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ hash: 3
90
+ segments:
91
+ - 0
92
+ version: "0"
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ hash: 3
99
+ segments:
100
+ - 0
101
+ version: "0"
102
+ requirements: []
103
+
104
+ rubyforge_project:
105
+ rubygems_version: 1.3.7
106
+ signing_key:
107
+ specification_version: 3
108
+ summary: Don't delete your records, move them to a different table
109
+ test_files: []
110
+