rylwin-acts_as_archive 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/.gitignore +9 -0
  2. data/LICENSE +18 -0
  3. data/README.md +130 -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 +12 -0
  10. data/init.rb +1 -0
  11. data/lib/acts_as_archive.rb +230 -0
  12. data/lib/acts_as_archive/adapters/rails2.rb +1 -0
  13. data/lib/acts_as_archive/adapters/rails3.rb +9 -0
  14. data/lib/acts_as_archive/adapters/sinatra.rb +12 -0
  15. data/lib/acts_as_archive/gems.rb +154 -0
  16. data/rails/init.rb +1 -0
  17. data/spec/Rakefile +9 -0
  18. data/spec/acts_as_archive/gems_spec.rb +249 -0
  19. data/spec/acts_as_archive_spec.rb +113 -0
  20. data/spec/fixtures/config/acts_as_archive.yml +3 -0
  21. data/spec/fixtures/config/database.yml.example +6 -0
  22. data/spec/fixtures/db/migrate/001_belongs_tos.rb +14 -0
  23. data/spec/fixtures/db/migrate/002_records.rb +15 -0
  24. data/spec/fixtures/db/migrate/003_has_ones.rb +15 -0
  25. data/spec/fixtures/db/migrate/004_has_manies.rb +15 -0
  26. data/spec/fixtures/db/migrate/005_has_many_through_throughs.rb +16 -0
  27. data/spec/fixtures/db/migrate/006_has_many_throughs.rb +14 -0
  28. data/spec/fixtures/db/migrate/007_has_one_through_throughs.rb +15 -0
  29. data/spec/fixtures/db/migrate/008_has_one_throughs.rb +15 -0
  30. data/spec/fixtures/frameworks.yml +41 -0
  31. data/spec/fixtures/frameworks/rails2/application_controller.rb +58 -0
  32. data/spec/fixtures/frameworks/rails2/database.yml +12 -0
  33. data/spec/fixtures/frameworks/rails2/init.rb +1 -0
  34. data/spec/fixtures/frameworks/rails2/routes.rb +46 -0
  35. data/spec/fixtures/frameworks/rails3/Gemfile +37 -0
  36. data/spec/fixtures/frameworks/rails3/application_controller.rb +51 -0
  37. data/spec/fixtures/frameworks/rails3/database.yml +12 -0
  38. data/spec/fixtures/frameworks/rails3/routes.rb +60 -0
  39. data/spec/fixtures/frameworks/sinatra/application.rb +59 -0
  40. data/spec/fixtures/gemsets.yml +9 -0
  41. data/spec/fixtures/gemspec.yml +15 -0
  42. data/spec/fixtures/helpers/spec_helper.rb +210 -0
  43. data/spec/fixtures/models/belongs_to.rb +6 -0
  44. data/spec/fixtures/models/has_many.rb +6 -0
  45. data/spec/fixtures/models/has_many_through.rb +7 -0
  46. data/spec/fixtures/models/has_many_through_through.rb +7 -0
  47. data/spec/fixtures/models/has_one.rb +6 -0
  48. data/spec/fixtures/models/has_one_through.rb +6 -0
  49. data/spec/fixtures/models/has_one_through_through.rb +7 -0
  50. data/spec/fixtures/models/record.rb +14 -0
  51. data/spec/run +27 -0
  52. data/spec/spec.opts +1 -0
  53. data/spec/spec_helper.rb +77 -0
  54. metadata +146 -0
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,130 @@
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
+
36
+ class Application &lt; Sinatra::Base
37
+ include ActsAsArchive::Adapters::Sinatra
38
+ end
39
+ </pre>
40
+
41
+ config/acts\_as\_archive.yml
42
+ ----------------------------
43
+
44
+ Create <code>config/acts\_as\_archive.yml</code> to define the archive class and archive table for each of your models:
45
+
46
+ <pre>
47
+ Article:
48
+ - class: Article::Archive
49
+ table: archived_articles
50
+ </pre>
51
+
52
+ It is expected that neither the archive class or archive table exist yet. <code>ActsAsArchive</code> will create these automatically.
53
+
54
+ Migrate
55
+ -------
56
+
57
+ Run <code>rake db:migrate</code>. Your archive table is created automatically.
58
+
59
+ That's it!
60
+ ----------
61
+
62
+ Use <code>destroy</code>, <code>destroy\_all</code>, <code>delete</code>, and <code>delete_all</code> like you normally would.
63
+
64
+ Records move into the archive table instead of being destroyed.
65
+
66
+ Automatically archive relationships
67
+ -----------------------------------
68
+
69
+ If your model's relationship has the <code>:dependent</code> option, and the relationship also uses <code>acts\_as\_archive</code>, that relationship will archive automatically.
70
+
71
+ What if my schema changes?
72
+ --------------------------
73
+
74
+ New migrations are automatically applied to the archive table.
75
+
76
+ No action is necessary on your part.
77
+
78
+ Query the archive
79
+ -----------------
80
+
81
+ Use the archive class you specified in the configuration:
82
+
83
+ <pre>
84
+ Article::Archive.first
85
+ </pre>
86
+
87
+ Delete records without archiving
88
+ --------------------------------
89
+
90
+ Use any of the destroy methods, but add a bang (!):
91
+
92
+ <pre>
93
+ Article::Archive.first.destroy!
94
+ Article.delete_all!([ "id in (?)", [ 1, 2, 3 ] ])
95
+ </pre>
96
+
97
+ Restore from the archive
98
+ ------------------------
99
+
100
+ Use any of the destroy/delete methods on the archived record to move it back to its original table:
101
+
102
+ <pre>
103
+ Article::Archive.first.destroy
104
+ Article::Archive.delete_all([ "id in (?)", [ 1, 2, 3 ] ])
105
+ </pre>
106
+
107
+ Any relationships that were automatically archived will be restored as well.
108
+
109
+ Magic columns
110
+ -------------
111
+
112
+ You will find an extra <code>deleted_at</code> datetime column on the archive table.
113
+
114
+ You may manually add a <code>restored_at</code> datetime column to the origin table if you wish to store restoration time as well.
115
+
116
+ Migrate from acts\_as\_paranoid
117
+ -------------------------------
118
+
119
+ Add this line to a migration, or run it via <code>script/console</code>:
120
+
121
+ <pre>
122
+ Article.migrate_from_acts_as_paranoid
123
+ </pre>
124
+
125
+ This copies all records with non-null <code>deleted_at</code> values to the archive.
126
+
127
+ Running specs
128
+ -------------
129
+
130
+ There is a [wiki entry](https://github.com/winton/acts_as_archive/wiki/Running-Specs) that describes the development setup in-depth.
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.5
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,12 @@
1
+ name: rylwin-acts_as_archive
2
+ version: 0.4.0
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: null
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/rails/init"
@@ -0,0 +1,230 @@
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
+ require 'yaml'
8
+
9
+ $:.unshift File.dirname(__FILE__)
10
+
11
+ class ActsAsArchive
12
+ class << self
13
+
14
+ attr_accessor :configuration, :disabled
15
+
16
+ def deprecate(msg)
17
+ if defined?(::ActiveSupport::Deprecation)
18
+ ::ActiveSupport::Deprecation.warn msg
19
+ else
20
+ $stdout.puts msg
21
+ end
22
+ end
23
+
24
+ def disable(&block)
25
+ @mutex ||= Mutex.new
26
+ @mutex.synchronize do
27
+ self.disabled = true
28
+ block.call
29
+ end
30
+ ensure
31
+ self.disabled = false
32
+ end
33
+
34
+ def find(from)
35
+ from = [ from ] unless from.is_a?(::Array)
36
+ (@configuration || []).select do |hash|
37
+ if from[0].is_a?(::String)
38
+ from.include?(hash[:from].table_name)
39
+ else
40
+ from.include?(hash[:from])
41
+ end
42
+ end
43
+ end
44
+
45
+ def load_from_yaml(root)
46
+ if File.exists?(yaml = "#{root}/config/acts_as_archive.yml")
47
+ YAML.load(File.read(yaml)).each do |klass, config|
48
+ klass = eval(klass) rescue nil
49
+ if klass
50
+ if (%w(class table) - config.last.keys).empty?
51
+ options = {}
52
+ else
53
+ options = config.pop
54
+ end
55
+ config.each do |c|
56
+ klass.acts_as_archive options.merge(c)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ def move(config, where, merge_options={})
64
+ options = config[:options].dup.merge(merge_options)
65
+ if options[:conditions]
66
+ options[:conditions] += " AND #{where}"
67
+ elsif where
68
+ options[:conditions] = where
69
+ end
70
+ config[:from].move_to(config[:to], options)
71
+ end
72
+
73
+ def update(*args)
74
+ deprecate "ActsAsArchive.update is deprecated and no longer necessary."
75
+ end
76
+ end
77
+
78
+ module Base
79
+ def self.included(base)
80
+ unless base.included_modules.include?(InstanceMethods)
81
+ base.extend ClassMethods
82
+ base.send :include, InstanceMethods
83
+ end
84
+ end
85
+
86
+ module ClassMethods
87
+ def acts_as_archive(options={})
88
+ return unless ActsAsArchive.find(self).empty?
89
+
90
+ ActsAsArchive.configuration ||= []
91
+ ActsAsArchive.configuration << (config = { :from => self })
92
+
93
+ options[:copy] = true
94
+
95
+ if options[:archive]
96
+ options[:magic] = 'restored_at'
97
+ klass = options[:class]
98
+ else
99
+ options[:magic] = 'deleted_at' if options[:magic].nil?
100
+ options[:add] = [[ options[:magic], :datetime ]]
101
+ options[:ignore] = options[:magic]
102
+ options[:subtract] = 'restored_at'
103
+ options[:timestamps] = false if options[:timestamps].nil?
104
+
105
+ unless options[:class]
106
+ options[:class] = "#{self}::Archive"
107
+ end
108
+
109
+ unless options[:table]
110
+ options[:table] = "archived_#{self.table_name}"
111
+ end
112
+
113
+ klass = eval(options[:class]) rescue nil
114
+
115
+ if klass
116
+ klass.send :set_table_name, options[:table]
117
+ else
118
+ eval <<-EVAL
119
+ class ::#{options[:class]} < ActiveRecord::Base
120
+ set_table_name "#{options[:table]}"
121
+ end
122
+ EVAL
123
+ klass = eval("::#{options[:class]}")
124
+ end
125
+
126
+ klass.record_timestamps = options[:timestamps].inspect
127
+ klass.acts_as_archive(:class => self, :archive => true)
128
+
129
+ self.reflect_on_all_associations.each do |association|
130
+ if association.options[:dependent] && !association.options[:polymorphic] && !ActsAsArchive.find(association.klass).empty?
131
+ opts = association.options.dup
132
+ opts[:class_name] = "::#{association.class_name}::Archive"
133
+ opts[:foreign_key] = association.primary_key_name
134
+ klass.send association.macro, association.name, opts
135
+ end
136
+ end
137
+
138
+ unless options[:migrate] == false
139
+ AlsoMigrate.configuration ||= []
140
+ AlsoMigrate.configuration << options.merge(
141
+ :source => self.table_name,
142
+ :destination => klass.table_name
143
+ )
144
+ end
145
+ end
146
+
147
+ config[:to] = klass
148
+ config[:options] = options
149
+ end
150
+
151
+ def delete_all!(*args)
152
+ ActsAsArchive.disable { self.delete_all(*args) }
153
+ end
154
+
155
+ def destroy_all!(*args)
156
+ ActsAsArchive.disable { self.destroy_all(*args) }
157
+ end
158
+
159
+ def migrate_from_acts_as_paranoid
160
+ time = Benchmark.measure do
161
+ ActsAsArchive.find(self).each do |config|
162
+ config = config.dup
163
+ config[:options][:copy] = false
164
+ ActsAsArchive.move(
165
+ config,
166
+ "`#{config[:options][:magic]}` IS NOT NULL",
167
+ :migrate => true
168
+ )
169
+ end
170
+ end
171
+ $stdout.puts "-- #{self}.migrate_from_acts_as_paranoid"
172
+ $stdout.puts " -> #{"%.4fs" % time.real}"
173
+ end
174
+
175
+ def restore_all(*args)
176
+ ActsAsArchive.deprecate "#{self}.restore_all is deprecated, please use #{self}.delete_all."
177
+ self.delete_all *args
178
+ end
179
+ end
180
+
181
+ module InstanceMethods
182
+ def delete!(*args)
183
+ ActsAsArchive.disable { self.delete(*args) }
184
+ end
185
+
186
+ def destroy!(*args)
187
+ ActsAsArchive.disable { self.destroy(*args) }
188
+ end
189
+ end
190
+ end
191
+
192
+ module DatabaseStatements
193
+ def self.included(base)
194
+ unless base.included_modules.include?(InstanceMethods)
195
+ base.send :include, InstanceMethods
196
+ base.class_eval do
197
+ unless method_defined?(:delete_sql_without_archive)
198
+ alias_method :delete_sql_without_archive, :delete_sql
199
+ alias_method :delete_sql, :delete_sql_with_archive
200
+ end
201
+ end
202
+ end
203
+ end
204
+
205
+ module InstanceMethods
206
+ def delete_sql_with_archive(sql, name = nil)
207
+ @mutex ||= Mutex.new
208
+ @mutex.synchronize do
209
+ unless ActsAsArchive.disabled
210
+ from, where = /DELETE FROM (.+)/i.match(sql)[1].split(/\s+WHERE\s+/i, 2)
211
+ from = from.strip.gsub(/`/, '').split(/\s*,\s*/)
212
+
213
+ ActsAsArchive.find(from).each do |config|
214
+ ActsAsArchive.move(config, where)
215
+ end
216
+ end
217
+ end
218
+
219
+ delete_sql_without_archive(sql, name)
220
+ end
221
+ end
222
+ end
223
+ end
224
+
225
+ ::ActiveRecord::Base.send(:include, ::ActsAsArchive::Base)
226
+ ::ActiveRecord::ConnectionAdapters::DatabaseStatements.send(:include, ::ActsAsArchive::DatabaseStatements)
227
+
228
+ require "acts_as_archive/adapters/rails#{Rails.version[0..0]}" if defined?(Rails)
229
+ require "acts_as_archive/adapters/sinatra" if defined?(Sinatra)
230
+