rylwin-acts_as_archive 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+