bkwld-paper_trail 2.3.2

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 (78) hide show
  1. data/.gitignore +10 -0
  2. data/.travis.yml +5 -0
  3. data/Gemfile +2 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +663 -0
  6. data/Rakefile +15 -0
  7. data/lib/generators/paper_trail/USAGE +2 -0
  8. data/lib/generators/paper_trail/install_generator.rb +20 -0
  9. data/lib/generators/paper_trail/templates/add_object_changes_column_to_versions.rb +9 -0
  10. data/lib/generators/paper_trail/templates/create_versions.rb +19 -0
  11. data/lib/paper_trail.rb +87 -0
  12. data/lib/paper_trail/config.rb +11 -0
  13. data/lib/paper_trail/controller.rb +76 -0
  14. data/lib/paper_trail/has_paper_trail.rb +228 -0
  15. data/lib/paper_trail/version.rb +159 -0
  16. data/lib/paper_trail/version_number.rb +3 -0
  17. data/paper_trail.gemspec +24 -0
  18. data/test/dummy/Rakefile +7 -0
  19. data/test/dummy/app/controllers/application_controller.rb +17 -0
  20. data/test/dummy/app/controllers/test_controller.rb +5 -0
  21. data/test/dummy/app/controllers/widgets_controller.rb +23 -0
  22. data/test/dummy/app/helpers/application_helper.rb +2 -0
  23. data/test/dummy/app/models/animal.rb +4 -0
  24. data/test/dummy/app/models/article.rb +12 -0
  25. data/test/dummy/app/models/authorship.rb +5 -0
  26. data/test/dummy/app/models/book.rb +5 -0
  27. data/test/dummy/app/models/cat.rb +2 -0
  28. data/test/dummy/app/models/document.rb +4 -0
  29. data/test/dummy/app/models/dog.rb +2 -0
  30. data/test/dummy/app/models/elephant.rb +3 -0
  31. data/test/dummy/app/models/fluxor.rb +3 -0
  32. data/test/dummy/app/models/foo_widget.rb +2 -0
  33. data/test/dummy/app/models/person.rb +5 -0
  34. data/test/dummy/app/models/post.rb +4 -0
  35. data/test/dummy/app/models/song.rb +12 -0
  36. data/test/dummy/app/models/widget.rb +5 -0
  37. data/test/dummy/app/models/wotsit.rb +4 -0
  38. data/test/dummy/app/versions/post_version.rb +3 -0
  39. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  40. data/test/dummy/config.ru +4 -0
  41. data/test/dummy/config/application.rb +45 -0
  42. data/test/dummy/config/boot.rb +10 -0
  43. data/test/dummy/config/database.yml +22 -0
  44. data/test/dummy/config/environment.rb +5 -0
  45. data/test/dummy/config/environments/development.rb +26 -0
  46. data/test/dummy/config/environments/production.rb +49 -0
  47. data/test/dummy/config/environments/test.rb +35 -0
  48. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  49. data/test/dummy/config/initializers/inflections.rb +10 -0
  50. data/test/dummy/config/initializers/mime_types.rb +5 -0
  51. data/test/dummy/config/initializers/secret_token.rb +7 -0
  52. data/test/dummy/config/initializers/session_store.rb +8 -0
  53. data/test/dummy/config/locales/en.yml +5 -0
  54. data/test/dummy/config/routes.rb +3 -0
  55. data/test/dummy/db/migrate/20110208155312_set_up_test_tables.rb +120 -0
  56. data/test/dummy/db/schema.rb +103 -0
  57. data/test/dummy/db/test.sqlite3 +0 -0
  58. data/test/dummy/public/404.html +26 -0
  59. data/test/dummy/public/422.html +26 -0
  60. data/test/dummy/public/500.html +26 -0
  61. data/test/dummy/public/favicon.ico +0 -0
  62. data/test/dummy/public/javascripts/application.js +2 -0
  63. data/test/dummy/public/javascripts/controls.js +965 -0
  64. data/test/dummy/public/javascripts/dragdrop.js +974 -0
  65. data/test/dummy/public/javascripts/effects.js +1123 -0
  66. data/test/dummy/public/javascripts/prototype.js +6001 -0
  67. data/test/dummy/public/javascripts/rails.js +175 -0
  68. data/test/dummy/public/stylesheets/.gitkeep +0 -0
  69. data/test/dummy/script/rails +6 -0
  70. data/test/functional/controller_test.rb +71 -0
  71. data/test/functional/thread_safety_test.rb +26 -0
  72. data/test/integration/navigation_test.rb +7 -0
  73. data/test/paper_trail_test.rb +27 -0
  74. data/test/support/integration_case.rb +5 -0
  75. data/test/test_helper.rb +49 -0
  76. data/test/unit/inheritance_column_test.rb +43 -0
  77. data/test/unit/model_test.rb +925 -0
  78. metadata +236 -0
@@ -0,0 +1,159 @@
1
+ class Version < ActiveRecord::Base
2
+ belongs_to :item, :polymorphic => true
3
+ validates_presence_of :event
4
+ serialize :columns
5
+
6
+ def self.with_item_keys(item_type, item_id)
7
+ scoped(:conditions => { :item_type => item_type, :item_id => item_id })
8
+ end
9
+
10
+ scope :subsequent, lambda { |version|
11
+ where(["#{self.primary_key} > ?", version.is_a?(self) ? version.id : version]).order("#{self.primary_key} ASC")
12
+ }
13
+
14
+ scope :preceding, lambda { |version|
15
+ where(["#{self.primary_key} < ?", version.is_a?(self) ? version.id : version]).order("#{self.primary_key} DESC")
16
+ }
17
+
18
+ scope :after, lambda { |timestamp|
19
+ # TODO: is this :order necessary, considering its presence on the has_many :versions association?
20
+ where(['created_at > ?', timestamp]).order("created_at ASC, #{self.primary_key} ASC")
21
+ }
22
+
23
+ # Restore the item from this version.
24
+ #
25
+ # This will automatically restore all :has_one associations as they were "at the time",
26
+ # if they are also being versioned by PaperTrail. NOTE: this isn't always guaranteed
27
+ # to work so you can either change the lookback period (from the default 3 seconds) or
28
+ # opt out.
29
+ #
30
+ # Options:
31
+ # +:has_one+ set to `false` to opt out of has_one reification.
32
+ # set to a float to change the lookback time (check whether your db supports
33
+ # sub-second datetimes if you want them).
34
+ def reify(options = {})
35
+ without_identity_map do
36
+ options[:has_one] = 3 if options[:has_one] == true
37
+ options.reverse_merge! :has_one => false
38
+
39
+ unless object.nil?
40
+ attrs = YAML::load object
41
+
42
+ # Normally a polymorphic belongs_to relationship allows us
43
+ # to get the object we belong to by calling, in this case,
44
+ # +item+. However this returns nil if +item+ has been
45
+ # destroyed, and we need to be able to retrieve destroyed
46
+ # objects.
47
+ #
48
+ # In this situation we constantize the +item_type+ to get hold of
49
+ # the class...except when the stored object's attributes
50
+ # include a +type+ key. If this is the case, the object
51
+ # we belong to is using single table inheritance and the
52
+ # +item_type+ will be the base class, not the actual subclass.
53
+ # If +type+ is present but empty, the class is the base class.
54
+
55
+ if item
56
+ model = item
57
+ else
58
+ inheritance_column_name = item_type.constantize.inheritance_column
59
+ class_name = attrs[inheritance_column_name].blank? ? item_type : attrs[inheritance_column_name]
60
+ klass = class_name.constantize
61
+ model = klass.new
62
+ end
63
+
64
+ attrs.each do |k, v|
65
+ begin
66
+ model.send :write_attribute, k.to_sym , v
67
+ rescue NoMethodError
68
+ logger.warn "Attribute #{k} does not exist on #{item_type} (Version id: #{id})."
69
+ end
70
+ end
71
+
72
+ model.version = self
73
+
74
+ unless options[:has_one] == false
75
+ reify_has_ones model, options[:has_one]
76
+ end
77
+
78
+ model
79
+ end
80
+ end
81
+ end
82
+
83
+ # Returns what changed in this version of the item. Cf. `ActiveModel::Dirty#changes`.
84
+ # Returns nil if your `versions` table does not have an `object_changes` text column.
85
+ def changeset
86
+ if self.class.column_names.include? 'object_changes'
87
+ if changes = object_changes
88
+ HashWithIndifferentAccess[YAML::load(changes)]
89
+ else
90
+ {}
91
+ end
92
+ end
93
+ end
94
+
95
+ # Returns who put the item into the state stored in this version.
96
+ def originator
97
+ previous.try :whodunnit
98
+ end
99
+
100
+ # Returns who changed the item from the state it had in this version.
101
+ # This is an alias for `whodunnit`.
102
+ def terminator
103
+ whodunnit
104
+ end
105
+
106
+ def sibling_versions
107
+ self.class.with_item_keys(item_type, item_id)
108
+ end
109
+
110
+ def next
111
+ sibling_versions.subsequent(self).first
112
+ end
113
+
114
+ def previous
115
+ sibling_versions.preceding(self).first
116
+ end
117
+
118
+ def index
119
+ sibling_versions.select(:id).order("id ASC").map(&:id).index(self.id)
120
+ end
121
+
122
+ private
123
+
124
+ # In Rails 3.1+, calling reify on a previous version confuses the
125
+ # IdentityMap, if enabled. This prevents insertion into the map.
126
+ def without_identity_map(&block)
127
+ if defined?(ActiveRecord::IdentityMap) && ActiveRecord::IdentityMap.respond_to?(:without)
128
+ ActiveRecord::IdentityMap.without(&block)
129
+ else
130
+ block.call
131
+ end
132
+ end
133
+
134
+ # Restore the `model`'s has_one associations as they were when this version was
135
+ # superseded by the next (because that's what the user was looking at when they
136
+ # made the change).
137
+ #
138
+ # The `lookback` sets how many seconds before the model's change we go.
139
+ def reify_has_ones(model, lookback)
140
+ model.class.reflect_on_all_associations(:has_one).each do |assoc|
141
+ child = model.send assoc.name
142
+ if child.respond_to? :version_at
143
+ # N.B. we use version of the child as it was `lookback` seconds before the parent was updated.
144
+ # Ideally we want the version of the child as it was just before the parent was updated...
145
+ # but until PaperTrail knows which updates are "together" (e.g. parent and child being
146
+ # updated on the same form), it's impossible to tell when the overall update started;
147
+ # and therefore impossible to know when "just before" was.
148
+ if (child_as_it_was = child.version_at(created_at - lookback.seconds))
149
+ child_as_it_was.attributes.each do |k,v|
150
+ model.send(assoc.name).send :write_attribute, k.to_sym, v rescue nil
151
+ end
152
+ else
153
+ model.send "#{assoc.name}=", nil
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ end
@@ -0,0 +1,3 @@
1
+ module PaperTrail
2
+ VERSION = '2.3.2'
3
+ end
@@ -0,0 +1,24 @@
1
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
2
+ require 'paper_trail/version_number'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'bkwld-paper_trail'
6
+ s.version = PaperTrail::VERSION
7
+ s.summary = "Track changes to your models' data. Good for auditing or versioning."
8
+ s.description = s.summary
9
+ s.homepage = 'http://github.com/airblade/paper_trail'
10
+ s.authors = ['Andy Stewart']
11
+ s.email = 'boss@airbladesoftware.com'
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
+ s.require_paths = ['lib']
17
+
18
+ s.add_dependency 'rails', '~> 3'
19
+
20
+ s.add_development_dependency 'shoulda', '2.10.3'
21
+ s.add_development_dependency 'sqlite3-ruby', '~> 1.2'
22
+ s.add_development_dependency 'capybara', '>= 0.4.0'
23
+ s.add_development_dependency 'turn'
24
+ end
@@ -0,0 +1,7 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+ require 'rake'
6
+
7
+ Dummy::Application.load_tasks
@@ -0,0 +1,17 @@
1
+ class ApplicationController < ActionController::Base
2
+ protect_from_forgery
3
+
4
+ def rescue_action(e)
5
+ raise e
6
+ end
7
+
8
+ # Returns id of hypothetical current user
9
+ def current_user
10
+ 153
11
+ end
12
+
13
+ def info_for_paper_trail
14
+ {:ip => request.remote_ip, :user_agent => request.user_agent}
15
+ end
16
+
17
+ end
@@ -0,0 +1,5 @@
1
+ class TestController < ActionController::Base
2
+ def current_user
3
+ Thread.current.object_id
4
+ end
5
+ end
@@ -0,0 +1,23 @@
1
+ class WidgetsController < ApplicationController
2
+
3
+ def paper_trail_enabled_for_controller
4
+ request.user_agent != 'Disable User-Agent'
5
+ end
6
+
7
+ def create
8
+ @widget = Widget.create params[:widget]
9
+ head :ok
10
+ end
11
+
12
+ def update
13
+ @widget = Widget.find params[:id]
14
+ @widget.update_attributes params[:widget]
15
+ head :ok
16
+ end
17
+
18
+ def destroy
19
+ @widget = Widget.find params[:id]
20
+ @widget.destroy
21
+ head :ok
22
+ end
23
+ end
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end
@@ -0,0 +1,4 @@
1
+ class Animal < ActiveRecord::Base
2
+ has_paper_trail
3
+ set_inheritance_column 'species'
4
+ end
@@ -0,0 +1,12 @@
1
+ class Article < ActiveRecord::Base
2
+ has_paper_trail :ignore => :title,
3
+ :only => [:content],
4
+ :meta => {:answer => 42,
5
+ :action => :action_data_provider_method,
6
+ :question => Proc.new { "31 + 11 = #{31 + 11}" },
7
+ :article_id => Proc.new { |article| article.id } }
8
+
9
+ def action_data_provider_method
10
+ self.object_id.to_s
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ class Authorship < ActiveRecord::Base
2
+ belongs_to :book
3
+ belongs_to :person
4
+ has_paper_trail
5
+ end
@@ -0,0 +1,5 @@
1
+ class Book < ActiveRecord::Base
2
+ has_many :authorships, :dependent => :destroy
3
+ has_many :authors, :through => :authorships, :source => :person
4
+ has_paper_trail
5
+ end
@@ -0,0 +1,2 @@
1
+ class Cat < Animal
2
+ end
@@ -0,0 +1,4 @@
1
+ class Document < ActiveRecord::Base
2
+ has_paper_trail :versions => :paper_trail_versions,
3
+ :on => [:create, :update]
4
+ end
@@ -0,0 +1,2 @@
1
+ class Dog < Animal
2
+ end
@@ -0,0 +1,3 @@
1
+ class Elephant < Animal
2
+ paper_trail_off
3
+ end
@@ -0,0 +1,3 @@
1
+ class Fluxor < ActiveRecord::Base
2
+ belongs_to :widget
3
+ end
@@ -0,0 +1,2 @@
1
+ class FooWidget < Widget
2
+ end
@@ -0,0 +1,5 @@
1
+ class Person < ActiveRecord::Base
2
+ has_many :authorships, :dependent => :destroy
3
+ has_many :books, :through => :authorships
4
+ has_paper_trail
5
+ end
@@ -0,0 +1,4 @@
1
+ class Post < ActiveRecord::Base
2
+ has_paper_trail :class_name => "PostVersion"
3
+
4
+ end
@@ -0,0 +1,12 @@
1
+ # Example from 'Overwriting default accessors' in ActiveRecord::Base.
2
+ class Song < ActiveRecord::Base
3
+ has_paper_trail
4
+
5
+ # Uses an integer of seconds to hold the length of the song
6
+ def length=(minutes)
7
+ write_attribute(:length, minutes.to_i * 60)
8
+ end
9
+ def length
10
+ read_attribute(:length) / 60
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ class Widget < ActiveRecord::Base
2
+ has_paper_trail
3
+ has_one :wotsit
4
+ has_many :fluxors, :order => :name
5
+ end
@@ -0,0 +1,4 @@
1
+ class Wotsit < ActiveRecord::Base
2
+ has_paper_trail
3
+ belongs_to :widget
4
+ end
@@ -0,0 +1,3 @@
1
+ class PostVersion < Version
2
+ set_table_name :post_versions
3
+ end
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Dummy</title>
5
+ <%= stylesheet_link_tag :all %>
6
+ <%= javascript_include_tag :defaults %>
7
+ <%= csrf_meta_tag %>
8
+ </head>
9
+ <body>
10
+
11
+ <%= yield %>
12
+
13
+ </body>
14
+ </html>
@@ -0,0 +1,4 @@
1
+ # This file is used by Rack-based servers to start the application.
2
+
3
+ require ::File.expand_path('../config/environment', __FILE__)
4
+ run Dummy::Application
@@ -0,0 +1,45 @@
1
+ require File.expand_path('../boot', __FILE__)
2
+
3
+ require "active_model/railtie"
4
+ require "active_record/railtie"
5
+ require "action_controller/railtie"
6
+ require "action_view/railtie"
7
+ require "action_mailer/railtie"
8
+
9
+ Bundler.require
10
+ require 'paper_trail'
11
+
12
+ module Dummy
13
+ class Application < Rails::Application
14
+ # Settings in config/environments/* take precedence over those specified here.
15
+ # Application configuration should go into files in config/initializers
16
+ # -- all .rb files in that directory are automatically loaded.
17
+
18
+ # Custom directories with classes and modules you want to be autoloadable.
19
+ # config.autoload_paths += %W(#{config.root}/extras)
20
+
21
+ # Only load the plugins named here, in the order given (default is alphabetical).
22
+ # :all can be used as a placeholder for all plugins not explicitly named.
23
+ # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
24
+
25
+ # Activate observers that should always be running.
26
+ # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
27
+
28
+ # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
29
+ # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
30
+ # config.time_zone = 'Central Time (US & Canada)'
31
+
32
+ # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
33
+ # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
34
+ # config.i18n.default_locale = :de
35
+
36
+ # JavaScript files you want as :defaults (application.js is always included).
37
+ # config.action_view.javascript_expansions[:defaults] = %w(jquery rails)
38
+
39
+ # Configure the default encoding used in templates for Ruby 1.9.
40
+ config.encoding = "utf-8"
41
+
42
+ # Configure sensitive parameters which will be filtered from the log file.
43
+ config.filter_parameters += [:password]
44
+ end
45
+ end
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ gemfile = File.expand_path('../../../../Gemfile', __FILE__)
3
+
4
+ if File.exist?(gemfile)
5
+ ENV['BUNDLE_GEMFILE'] = gemfile
6
+ require 'bundler'
7
+ Bundler.setup
8
+ end
9
+
10
+ $:.unshift File.expand_path('../../../../lib', __FILE__)