bkwld-paper_trail 2.3.2

Sign up to get free protection for your applications and to get access to all the features.
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__)