acts_as_archive 0.3.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/.gitignore +9 -0
  2. data/LICENSE +18 -0
  3. data/README.md +112 -0
  4. data/Rakefile +90 -0
  5. data/acts_as_archive.gemspec +32 -0
  6. data/bin/acts_as_archive +2 -0
  7. data/config/externals.yml +12 -0
  8. data/config/gemsets.yml +10 -0
  9. data/config/gemspec.yml +17 -0
  10. data/init.rb +1 -0
  11. data/lib/acts_as_archive.rb +192 -0
  12. data/lib/acts_as_archive/gems.rb +154 -0
  13. data/rails/init.rb +1 -0
  14. data/spec/Rakefile +9 -0
  15. data/spec/acts_as_archive/gems_spec.rb +249 -0
  16. data/spec/acts_as_archive_spec.rb +113 -0
  17. data/spec/fixtures/config/database.yml.example +6 -0
  18. data/spec/fixtures/db/migrate/001_belongs_tos.rb +14 -0
  19. data/spec/fixtures/db/migrate/002_records.rb +15 -0
  20. data/spec/fixtures/db/migrate/003_has_ones.rb +15 -0
  21. data/spec/fixtures/db/migrate/004_has_manies.rb +15 -0
  22. data/spec/fixtures/db/migrate/005_has_many_through_throughs.rb +16 -0
  23. data/spec/fixtures/db/migrate/006_has_many_throughs.rb +14 -0
  24. data/spec/fixtures/db/migrate/007_has_one_through_throughs.rb +15 -0
  25. data/spec/fixtures/db/migrate/008_has_one_throughs.rb +15 -0
  26. data/spec/fixtures/frameworks.yml +36 -0
  27. data/spec/fixtures/frameworks/rails2/application_controller.rb +58 -0
  28. data/spec/fixtures/frameworks/rails2/database.yml +12 -0
  29. data/spec/fixtures/frameworks/rails2/init.rb +1 -0
  30. data/spec/fixtures/frameworks/rails2/routes.rb +46 -0
  31. data/spec/fixtures/frameworks/rails3/Gemfile +34 -0
  32. data/spec/fixtures/frameworks/rails3/application_controller.rb +51 -0
  33. data/spec/fixtures/frameworks/rails3/database.yml +12 -0
  34. data/spec/fixtures/frameworks/rails3/routes.rb +60 -0
  35. data/spec/fixtures/frameworks/sinatra/application.rb +58 -0
  36. data/spec/fixtures/gemsets.yml +9 -0
  37. data/spec/fixtures/gemspec.yml +15 -0
  38. data/spec/fixtures/helpers/spec_helper.rb +210 -0
  39. data/spec/fixtures/models/belongs_to.rb +6 -0
  40. data/spec/fixtures/models/has_many.rb +6 -0
  41. data/spec/fixtures/models/has_many_through.rb +7 -0
  42. data/spec/fixtures/models/has_many_through_through.rb +7 -0
  43. data/spec/fixtures/models/has_one.rb +6 -0
  44. data/spec/fixtures/models/has_one_through.rb +6 -0
  45. data/spec/fixtures/models/has_one_through_through.rb +7 -0
  46. data/spec/fixtures/models/record.rb +16 -0
  47. data/spec/run +27 -0
  48. data/spec/spec.opts +1 -0
  49. data/spec/spec_helper.rb +73 -0
  50. metadata +96 -13
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ .DS_Store
2
+ *.gem
3
+ coverage
4
+ pkg
5
+ spec/fixtures/config/database.yml
6
+ spec/fixtures/builds
7
+ spec/fixtures/log
8
+ tmp
9
+ vendor
data/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2010
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.md ADDED
@@ -0,0 +1,112 @@
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
+ gem install acts_as_archive
13
+ </pre>
14
+
15
+ ### Rails 2
16
+
17
+ #### config/environment.rb
18
+
19
+ <pre>
20
+ config.gem 'acts_as_archive'
21
+ </pre>
22
+
23
+ ### Rails 3
24
+
25
+ #### Gemfile
26
+
27
+ <pre>
28
+ gem 'acts_as_archive'
29
+ </pre>
30
+
31
+ ### Sinatra
32
+
33
+ <pre>
34
+ require 'acts_as_archive'
35
+ </pre>
36
+
37
+ Add to models
38
+ -------------
39
+
40
+ Add <code>acts\_as\_archive</code> to your models:
41
+
42
+ <pre>
43
+ class Article &lt; ActiveRecord::Base
44
+ acts_as_archive
45
+ end
46
+ </pre>
47
+
48
+ Migrate
49
+ -------
50
+
51
+ Next time you run <code>rake db:migrate</code>, your archive tables will be created automatically.
52
+
53
+ That's it!
54
+ ----------
55
+
56
+ Use <code>destroy</code>, <code>destroy\_all</code>, <code>delete</code>, and <code>delete_all</code> like you normally would.
57
+
58
+ Records move into the archive table instead of being destroyed.
59
+
60
+ What if my schema changes?
61
+ --------------------------
62
+
63
+ New migrations are automatically applied to the archive table.
64
+
65
+ No action is necessary on your part.
66
+
67
+ Query the archive
68
+ -----------------
69
+
70
+ Add <code>::Archive</code> to your ActiveRecord class:
71
+
72
+ <pre>
73
+ Article::Archive.first
74
+ </pre>
75
+
76
+ Delete records without archiving
77
+ --------------------------------
78
+
79
+ Use any of the destroy methods, but add a bang (!):
80
+
81
+ <pre>
82
+ Article::Archive.find(:first).destroy!
83
+ Article.delete_all!([ "id in (?)", [ 1, 2, 3 ] ])
84
+ </pre>
85
+
86
+ Restore from the archive
87
+ ------------------------
88
+
89
+ Use any of the destroy/delete methods on the archived record to move it back to its original table:
90
+
91
+ <pre>
92
+ Article::Archive.first.destroy
93
+ Article::Archive.delete_all([ "id in (?)", [ 1, 2, 3 ] ])
94
+ </pre>
95
+
96
+ Magic columns
97
+ -------------
98
+
99
+ You will find an extra <code>deleted_at</code> datetime column on the archive table.
100
+
101
+ You may manually add a <code>restored_at</code> datetime column to the origin table if you wish to store restoration time as well.
102
+
103
+ Migrate from acts\_as\_paranoid
104
+ -------------------------------
105
+
106
+ Add this line to a migration, or run it via <code>script/console</code>:
107
+
108
+ <pre>
109
+ Article.migrate_from_acts_as_paranoid
110
+ </pre>
111
+
112
+ This copies all records with non-null <code>deleted_at</code> values to the archive.
data/Rakefile ADDED
@@ -0,0 +1,90 @@
1
+ require File.dirname(__FILE__) + '/lib/acts_as_archive/gems'
2
+
3
+ ActsAsArchive::Gems.activate %w(rake rspec)
4
+
5
+ require 'rake'
6
+ require 'spec/rake/spectask'
7
+
8
+ def gemspec
9
+ @gemspec ||= begin
10
+ file = File.expand_path('../acts_as_archive.gemspec', __FILE__)
11
+ eval(File.read(file), binding, file)
12
+ end
13
+ end
14
+
15
+ if defined?(Spec::Rake::SpecTask)
16
+ desc "Run specs"
17
+ Spec::Rake::SpecTask.new do |t|
18
+ t.spec_files = FileList['spec/**/*_spec.rb']
19
+ t.spec_opts = %w(-fs --color)
20
+ t.warning = true
21
+ end
22
+ task :spec
23
+ task :default => :spec
24
+ end
25
+
26
+ desc "Build gem(s)"
27
+ task :gem do
28
+ old_gemset = ENV['GEMSET']
29
+ root = File.expand_path('../', __FILE__)
30
+ pkg = "#{root}/pkg"
31
+ system "rm -Rf #{pkg}"
32
+ ActsAsArchive::Gems.gemset_names.each do |gemset|
33
+ ENV['GEMSET'] = gemset.to_s
34
+ system "cd #{root} && gem build acts_as_archive.gemspec"
35
+ system "mkdir -p #{pkg} && mv *.gem pkg"
36
+ end
37
+ ENV['GEMSET'] = old_gemset
38
+ end
39
+
40
+ namespace :gem do
41
+ desc "Install gem(s)"
42
+ task :install do
43
+ Rake::Task['gem'].invoke
44
+ Dir["#{File.dirname(__FILE__)}/pkg/*.gem"].each do |pkg|
45
+ system "gem install #{pkg} --no-ri --no-rdoc"
46
+ end
47
+ end
48
+
49
+ desc "Push gem(s)"
50
+ task :push do
51
+ Rake::Task['gem'].invoke
52
+ Dir["#{File.dirname(__FILE__)}/pkg/*.gem"].each do |pkg|
53
+ system "gem push #{pkg}"
54
+ end
55
+ end
56
+ end
57
+
58
+ namespace :gems do
59
+ desc "Install gem dependencies (DEV=0 DOCS=0 GEMSPEC=default SUDO=0)"
60
+ task :install do
61
+ dev = ENV['DEV'] == '1'
62
+ docs = ENV['DOCS'] == '1' ? '' : '--no-ri --no-rdoc'
63
+ gemset = ENV['GEMSET']
64
+ sudo = ENV['SUDO'] == '1' ? 'sudo' : ''
65
+
66
+ ActsAsArchive::Gems.gemset = gemset if gemset
67
+
68
+ if dev
69
+ gems = ActsAsArchive::Gems.gemspec.development_dependencies
70
+ else
71
+ gems = ActsAsArchive::Gems.gemspec.dependencies
72
+ end
73
+
74
+ gems.each do |name|
75
+ name = name.to_s
76
+ version = ActsAsArchive::Gems.versions[name]
77
+ if Gem.source_index.find_name(name, version).empty?
78
+ version = version ? "-v #{version}" : ''
79
+ system "#{sudo} gem install #{name} #{version} #{docs}"
80
+ else
81
+ puts "already installed: #{name} #{version}"
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ desc "Validate the gemspec"
88
+ task :gemspec do
89
+ gemspec.validate
90
+ end
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ root = File.expand_path('../', __FILE__)
3
+ lib = "#{root}/lib"
4
+ $:.unshift lib unless $:.include?(lib)
5
+
6
+ require 'acts_as_archive/gems'
7
+ ActsAsArchive::Gems.gemset ||= ENV['GEMSET'] || :default
8
+
9
+ Gem::Specification.new do |s|
10
+ ActsAsArchive::Gems.gemspec.hash.each do |key, value|
11
+ if key == 'name' && ActsAsArchive::Gems.gemset != :default
12
+ s.name = "#{value}-#{ActsAsArchive::Gems.gemset}"
13
+ elsif key == 'summary' && ActsAsArchive::Gems.gemset == :solo
14
+ s.summary = value + " (no dependencies)"
15
+ elsif !%w(dependencies development_dependencies).include?(key)
16
+ s.send "#{key}=", value
17
+ end
18
+ end
19
+
20
+ ActsAsArchive::Gems.dependencies.each do |g|
21
+ s.add_dependency g.to_s, ActsAsArchive::Gems.versions[g]
22
+ end
23
+
24
+ ActsAsArchive::Gems.development_dependencies.each do |g|
25
+ s.add_development_dependency g.to_s, ActsAsArchive::Gems.versions[g]
26
+ end
27
+
28
+ s.executables = `cd #{root} && git ls-files -- {bin}/*`.split("\n").collect { |f| File.basename(f) }
29
+ s.files = `cd #{root} && git ls-files`.split("\n")
30
+ s.require_paths = %w(lib)
31
+ s.test_files = `cd #{root} && git ls-files -- {features,test,spec}/*`.split("\n")
32
+ end
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env ruby
2
+ puts `script/runner "ActsAsArchive.update #{ARGV.join ', '}"`
@@ -0,0 +1,12 @@
1
+ active_wrapper-solo:
2
+ repo: git@github.com:winton/active_wrapper.git
3
+ path: vendor
4
+ also_migrate:
5
+ repo: git@github.com:winton/also_migrate.git
6
+ path: vendor
7
+ framework_fixture:
8
+ repo: git@github.com:winton/framework_fixture.git
9
+ path: vendor
10
+ mover:
11
+ repo: git@github.com:winton/mover.git
12
+ path: vendor
@@ -0,0 +1,10 @@
1
+ acts_as_archive:
2
+ rake: >=0.8.7
3
+ rspec: ~>1.0
4
+ default:
5
+ active_wrapper-solo: =0.4.4
6
+ also_migrate: =0.3.2
7
+ externals: =1.0.2
8
+ framework_fixture: =0.1.3
9
+ mover: =0.3.6
10
+ rack-test: =0.5.6
@@ -0,0 +1,17 @@
1
+ name: acts_as_archive
2
+ version: 0.3.2
3
+ authors:
4
+ - Winton Welsh
5
+ email: mail@wintoni.us
6
+ homepage: http://github.com/winton/acts_as_archive
7
+ summary: Don't delete your records, move them to a different table
8
+ description: Don't delete your records, move them to a different table. Like acts_as_paranoid, but doesn't mess with your SQL queries.
9
+ dependencies:
10
+ - also_migrate
11
+ - mover
12
+ development_dependencies:
13
+ - active_wrapper-solo
14
+ - externals
15
+ - framework_fixture
16
+ - rake
17
+ - rspec
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/rails/init"
@@ -0,0 +1,192 @@
1
+ require File.dirname(__FILE__) + '/acts_as_archive/gems'
2
+
3
+ ActsAsArchive::Gems.activate %w(also_migrate mover)
4
+
5
+ require 'also_migrate'
6
+ require 'mover'
7
+
8
+ $:.unshift File.dirname(__FILE__)
9
+
10
+ class ActsAsArchive
11
+ class <<self
12
+
13
+ attr_accessor :configuration, :disabled
14
+
15
+ def deprecate(msg)
16
+ if defined?(::ActiveSupport::Deprecation)
17
+ ::ActiveSupport::Deprecation.warn msg
18
+ else
19
+ $stdout.puts msg
20
+ end
21
+ end
22
+
23
+ def disable(&block)
24
+ @mutex ||= Mutex.new
25
+ @mutex.synchronize do
26
+ self.disabled = true
27
+ block.call
28
+ end
29
+ ensure
30
+ self.disabled = false
31
+ end
32
+
33
+ def find(from)
34
+ from = [ from ] unless from.is_a?(::Array)
35
+ (@configuration || []).select do |hash|
36
+ if from[0].is_a?(::String)
37
+ from.include?(hash[:from].table_name)
38
+ else
39
+ from.include?(hash[:from])
40
+ end
41
+ end
42
+ end
43
+
44
+ def move(config, where, merge_options={})
45
+ config[:to].each do |to|
46
+ options = config[:options].dup.merge(merge_options)
47
+ if options[:conditions]
48
+ options[:conditions] += " AND #{where}"
49
+ elsif where
50
+ options[:conditions] = where
51
+ end
52
+ config[:from].move_to(to, options)
53
+ end
54
+ end
55
+
56
+ def update(*args)
57
+ deprecate "ActsAsArchive.update is deprecated and no longer necessary."
58
+ end
59
+ end
60
+
61
+ module Base
62
+ def self.included(base)
63
+ unless base.included_modules.include?(InstanceMethods)
64
+ base.extend ClassMethods
65
+ base.send :include, InstanceMethods
66
+ end
67
+ end
68
+
69
+ module ClassMethods
70
+ def acts_as_archive(*args)
71
+ return unless ActsAsArchive.find(self).empty?
72
+
73
+ ActsAsArchive.configuration ||= []
74
+ ActsAsArchive.configuration << (config = { :from => self })
75
+
76
+ options = args.last.is_a?(::Hash) ? args.pop : {}
77
+ options[:copy] = true
78
+
79
+ if options[:archive]
80
+ options[:magic] = 'restored_at'
81
+ else
82
+ options[:magic] = 'deleted_at' if options[:magic].nil?
83
+ options[:add] = [[ options[:magic], :datetime ]]
84
+ options[:ignore] = options[:magic]
85
+ options[:subtract] = 'restored_at'
86
+ options[:timestamps] = false if options[:timestamps].nil?
87
+
88
+ if args.empty?
89
+ class_eval <<-EVAL
90
+ class Archive < ActiveRecord::Base
91
+ set_table_name "archived_#{self.table_name}"
92
+ end
93
+ EVAL
94
+ args << self::Archive
95
+ end
96
+
97
+ args.each do |klass|
98
+ klass.class_eval <<-EVAL
99
+ record_timestamps = #{options[:timestamps].inspect}
100
+ acts_as_archive(#{self}, :archive => true)
101
+ EVAL
102
+ self.reflect_on_all_associations.each do |association|
103
+ if !ActsAsArchive.find(association.klass).empty? && association.options[:dependent]
104
+ opts = association.options.dup
105
+ opts[:class_name] = "::#{association.class_name}::Archive"
106
+ opts[:foreign_key] = association.primary_key_name
107
+ klass.send association.macro, association.name, opts
108
+ end
109
+ end
110
+ unless options[:migrate] == false
111
+ self.also_migrate klass.table_name, options
112
+ end
113
+ end
114
+ end
115
+
116
+ config[:to] = args
117
+ config[:options] = options
118
+ end
119
+
120
+ def delete_all!(*args)
121
+ ActsAsArchive.disable { self.delete_all(*args) }
122
+ end
123
+
124
+ def destroy_all!(*args)
125
+ ActsAsArchive.disable { self.destroy_all(*args) }
126
+ end
127
+
128
+ def migrate_from_acts_as_paranoid
129
+ time = Benchmark.measure do
130
+ ActsAsArchive.find(self).each do |config|
131
+ config = config.dup
132
+ config[:options][:copy] = false
133
+ ActsAsArchive.move(
134
+ config,
135
+ "`#{config[:options][:magic]}` IS NOT NULL",
136
+ :migrate => true
137
+ )
138
+ end
139
+ end
140
+ $stdout.puts "-- #{self}.migrate_from_acts_as_paranoid"
141
+ $stdout.puts " -> #{"%.4fs" % time.real}"
142
+ end
143
+
144
+ def restore_all(*args)
145
+ ActsAsArchive.deprecate "#{self}.restore_all is deprecated, please use #{self}.delete_all."
146
+ self.delete_all *args
147
+ end
148
+ end
149
+
150
+ module InstanceMethods
151
+ def delete!(*args)
152
+ ActsAsArchive.disable { self.delete(*args) }
153
+ end
154
+
155
+ def destroy!(*args)
156
+ ActsAsArchive.disable { self.destroy(*args) }
157
+ end
158
+ end
159
+ end
160
+
161
+ module DatabaseStatements
162
+ def self.included(base)
163
+ unless base.included_modules.include?(InstanceMethods)
164
+ base.send :include, InstanceMethods
165
+ base.class_eval do
166
+ unless method_defined?(:delete_sql_without_archive)
167
+ alias_method :delete_sql_without_archive, :delete_sql
168
+ alias_method :delete_sql, :delete_sql_with_archive
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ module InstanceMethods
175
+ def delete_sql_with_archive(sql, name = nil)
176
+ unless ActsAsArchive.disabled
177
+ from, where = /DELETE FROM (.+)/i.match(sql)[1].split(/\s+WHERE\s+/i, 2)
178
+ from = from.strip.gsub(/`/, '').split(/\s*,\s*/)
179
+
180
+ ActsAsArchive.find(from).each do |config|
181
+ ActsAsArchive.move(config, where)
182
+ end
183
+ end
184
+
185
+ delete_sql_without_archive(sql, name)
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ ::ActiveRecord::Base.send(:include, ::ActsAsArchive::Base)
192
+ ::ActiveRecord::ConnectionAdapters::DatabaseStatements.send(:include, ::ActsAsArchive::DatabaseStatements)