secretary-rails 1.0.0.beta1

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 (38) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +186 -0
  3. data/Rakefile +9 -0
  4. data/app/models/secretary/version.rb +93 -0
  5. data/lib/generators/secretary/install_generator.rb +23 -0
  6. data/lib/generators/secretary/templates/versions_migration.rb +18 -0
  7. data/lib/secretary/config.rb +19 -0
  8. data/lib/secretary/engine.rb +4 -0
  9. data/lib/secretary/errors.rb +10 -0
  10. data/lib/secretary/gem_version.rb +3 -0
  11. data/lib/secretary/has_secretary.rb +75 -0
  12. data/lib/secretary/tracks_association.rb +146 -0
  13. data/lib/secretary/versioned_attributes.rb +58 -0
  14. data/lib/secretary-rails.rb +3 -0
  15. data/lib/secretary.rb +40 -0
  16. data/lib/tasks/secretary_tasks.rake +6 -0
  17. data/spec/factories.rb +32 -0
  18. data/spec/internal/app/models/animal.rb +7 -0
  19. data/spec/internal/app/models/hobby.rb +3 -0
  20. data/spec/internal/app/models/location.rb +6 -0
  21. data/spec/internal/app/models/person.rb +14 -0
  22. data/spec/internal/app/models/story.rb +3 -0
  23. data/spec/internal/app/models/user.rb +3 -0
  24. data/spec/internal/config/database.yml +3 -0
  25. data/spec/internal/config/routes.rb +2 -0
  26. data/spec/internal/db/combustion_test.sqlite +0 -0
  27. data/spec/internal/db/schema.rb +50 -0
  28. data/spec/internal/log/test.log +23177 -0
  29. data/spec/lib/generators/secretary/install_generator_spec.rb +17 -0
  30. data/spec/lib/secretary/config_spec.rb +9 -0
  31. data/spec/lib/secretary/has_secretary_spec.rb +116 -0
  32. data/spec/lib/secretary/tracks_association_spec.rb +214 -0
  33. data/spec/lib/secretary/versioned_attributes_spec.rb +63 -0
  34. data/spec/lib/secretary_spec.rb +44 -0
  35. data/spec/models/secretary/version_spec.rb +68 -0
  36. data/spec/spec_helper.rb +20 -0
  37. data/spec/tmp/db/migrate/20131105082639_create_versions.rb +18 -0
  38. metadata +181 -0
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2013 Bryan Ricker, SCPR
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # Secretary
2
+
3
+ #### A note about the Gem name
4
+ There is another gem called [`secretary`](http://rubygems.org/gems/secretary),
5
+ which hasn't been updated since 2008 and is obsolete. This gem is called
6
+ `secretary-rails` since `secretary` is already taken on RubyGems. However, the
7
+ module name is the same, so using them together would be difficult.
8
+
9
+
10
+ ### What is it?
11
+ Light-weight model versioning for ActiveRecord 3.2+.
12
+
13
+ ### How does it work?
14
+ Whenever you save your model, a new version is saved. The changes are
15
+ serialized and stored in the database, along with a version description,
16
+ foreign keys to the object, and a foreign key to the user who saved the object.
17
+
18
+ ### Why is it better than [other versioning gem]?
19
+ * It tracks associations.
20
+ * It provides diffs (using the [`diffy`](http://rubygems.org/gems/diffy) gem).
21
+ * It only stores the changes, not the whole object.
22
+ * It is simple.
23
+
24
+ ### Compatibility
25
+ * Rails 3.2+
26
+ * SQLite
27
+ * MySQL? (untested)
28
+ * Postgres? (untested)
29
+
30
+ ### Dependencies
31
+ * [`activerecord`](http://rubygems.org/gems/activerecord) >= 3.2.0
32
+ * [`railties`](http://rubygems.org/gems/railties) >= 3.2.0
33
+ * [`diffy`](http://rubygems.org/gems/diffy) ~> 3.0.1
34
+
35
+
36
+ ## Installation
37
+ Add to your gemfile:
38
+
39
+ ```ruby
40
+ gem 'secretary-rails'
41
+ ```
42
+
43
+ Run the install command, which will create a migration to add the `versions`
44
+ table, and then run it:
45
+
46
+ ```
47
+ bundle exec rails generate secretary:install
48
+ bundle exec rake db:migrate
49
+ ```
50
+
51
+
52
+ ## Usage
53
+ Add the `has_secretary` macro to your model:
54
+
55
+ ```ruby
56
+ class Article < ActiveRecord::Base
57
+ has_secretary
58
+ end
59
+ ```
60
+
61
+ Congratulations, now your records are being versioned.
62
+
63
+ ### Tracking associations
64
+ This gem is built with the end-user in mind, so it doesn't track hidden
65
+ associations (i.e. join models). However, you can tell it to track associated
66
+ objects WITHIN their parent object's version by using the `tracks_association`
67
+ macro. For example:
68
+
69
+ ```ruby
70
+ class Author < ActiveRecord::Base
71
+ has_many :article_authors
72
+ has_many :articles, through: :article_authors
73
+ end
74
+
75
+ class ArticleAuthor < ActiveRecord::Base
76
+ belongs_to :article
77
+ belongs_to :author
78
+ end
79
+
80
+ class Article < ActiveRecord::Base
81
+ has_secretary
82
+
83
+ has_many :article_authors
84
+ has_many :authors, through: :article_authors
85
+ tracks_association :authors
86
+ end
87
+ ```
88
+
89
+ Now, when you save an `Article`, a new version won't be created for the
90
+ new `ArticleAuthor` object(s). Instead, an array will be added to the `Article`'s
91
+ changes, which will include the information about the author(s).
92
+
93
+ You can also pass in multiple association names into `tracks_association`.
94
+
95
+ ### Tracking Users
96
+ A version has an association to a user object, which tells you who created that
97
+ version. The logged user is an attribute on the object being changed, so you
98
+ can add it in via the controller:
99
+
100
+ ```ruby
101
+ class ArticlesController < ApplicationControler
102
+ before_filter :get_object, only: [:show, :edit, :update, :destroy]
103
+ before_filter :inject_logged_user, only: [:update]
104
+
105
+ def create
106
+ @article = Article.new(article_params)
107
+ inject_logged_user
108
+ # ...
109
+ end
110
+
111
+ # ...
112
+
113
+ private
114
+
115
+ def get_object
116
+ @article = Article.find(params[:id])
117
+ end
118
+
119
+ def inject_logged_user
120
+ @article.logged_user_id = @current_user.id
121
+ end
122
+ end
123
+ ```
124
+
125
+ ### Configuration
126
+ In an initializer (may we suggest `secretary.rb`?), add:
127
+
128
+ ```ruby
129
+ # This is a list of all the possible configurations and their defaults.
130
+ Secretary.configure do |config|
131
+ config.user_class = "::User"
132
+ config.ignored_attributes = ["id", "created_at", "updated_at"]
133
+ end
134
+ ```
135
+
136
+ * **user_class** - The class for your user model.
137
+ * **ignored_attributes** - The attributes which should always be ignored
138
+ when generating a version, for every model, as an array of Strings.
139
+
140
+ ### Specifying which attributes to keep track of
141
+ Sometimes you have an attribute on your model that either isn't public
142
+ (not in the form), or you just don't want to version. You can tell Secretary
143
+ to ignore these attributes globally by setting
144
+ `Secretary.config.ignore_attributes`. You can also ignore attributes on a
145
+ per-model basis by using one of two methods:
146
+
147
+ ```ruby
148
+ class Article < ActiveRecord::Base
149
+ has_secretary
150
+
151
+ # Included
152
+ self.versioned_attributes = ["headline", "body"]
153
+ end
154
+ ```
155
+
156
+ ```ruby
157
+ class Article < ActiveRecord::Base
158
+ has_secretary
159
+
160
+ # Excluded
161
+ self.unversioned_attributes = ["published_at", "is_editable"]
162
+ end
163
+ ```
164
+
165
+ By default, `versioned_attributes` is the model's column names, minus the
166
+ globally configured `ignored_attributes`, minus any `unversioned_attributes`
167
+ you have set. `tracks_association` adds those associations to the
168
+ `versioned_attributes` array.
169
+
170
+
171
+ ## Contributing
172
+ Fork it and send a pull request!
173
+
174
+ ### TODO
175
+ * Rails 4.1+ support.
176
+ * Test (officially) with MySQL and SQLite.
177
+ * Associations are only tracked one-level deep, It would be nice to also
178
+ track the changes of the association (i.e. recognize when an associated
179
+ object was changed and show its changed, instead of just showing a whole
180
+ new object).
181
+ * Support for Rails 3.0 and 3.1.
182
+
183
+ ### Running Tests
184
+ This library uses [appraisal](https://github.com/thoughtbot/appraisal) to test
185
+ against different Rails versions. To run the test suite on all versions, use
186
+ `appraisal rspec`.
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/setup'
3
+ require 'appraisal'
4
+ require 'rspec/core/rake_task'
5
+
6
+ Bundler::GemHelper.install_tasks
7
+
8
+ RSpec::Core::RakeTask.new(:test)
9
+ task :default => :test
@@ -0,0 +1,93 @@
1
+ module Secretary
2
+ class Version < ActiveRecord::Base
3
+ serialize :object_changes
4
+
5
+ belongs_to :versioned, polymorphic: true
6
+ belongs_to :user, class_name: Secretary.config.user_class
7
+
8
+ validates_presence_of :versioned
9
+
10
+ before_create :increment_version_number
11
+
12
+
13
+ class << self
14
+ # Builds a new version for the passed-in object
15
+ # Passed-in object is a dirty object.
16
+ # Version will be saved when the object is saved.
17
+ #
18
+ # If you must generate a version manually, this
19
+ # method should be used instead of `Version.create`.
20
+ # I didn't want to override the public ActiveRecord
21
+ # API.
22
+ def generate(object)
23
+ object.versions.create({
24
+ :user_id => object.logged_user_id,
25
+ :description => generate_description(object),
26
+ :object_changes => object.versioned_changes
27
+ })
28
+ end
29
+
30
+
31
+ private
32
+
33
+ def generate_description(object)
34
+ if was_created?(object)
35
+ "Created #{object.class.name.titleize} ##{object.id}"
36
+
37
+ elsif was_updated?(object)
38
+ attributes = object.versioned_changes.keys
39
+ "Changed #{attributes.to_sentence}"
40
+
41
+ else
42
+ "Generated Version"
43
+ end
44
+ end
45
+
46
+
47
+ def was_created?(object)
48
+ object.persisted? && object.id_changed?
49
+ end
50
+
51
+ def was_updated?(object)
52
+ object.persisted? && !object.id_changed?
53
+ end
54
+ end
55
+
56
+
57
+ # The attribute diffs for this version
58
+ def attribute_diffs
59
+ @attribute_diffs ||= begin
60
+ changes = self.object_changes.dup
61
+ attribute_diffs = {}
62
+
63
+ # Compare each of object_b's attributes to object_a's attributes
64
+ # And if there is a difference, add it to the Diff
65
+ changes.each do |attribute, values|
66
+ # values is [previous_value, new_value]
67
+ diff = Diffy::Diff.new(values[0].to_s, values[1].to_s)
68
+
69
+ if diff.string1 != diff.string2
70
+ attribute_diffs[attribute] = diff
71
+ end
72
+ end
73
+
74
+ attribute_diffs
75
+ end
76
+ end
77
+
78
+ # A simple title for this version.
79
+ # Example: "Article #125 v6"
80
+ def title
81
+ "#{self.versioned.class.name.titleize} " \
82
+ "##{self.versioned.id} v#{self.version_number}"
83
+ end
84
+
85
+
86
+ private
87
+
88
+ def increment_version_number
89
+ latest_version = self.versioned.versions.order("version_number").last
90
+ self.version_number = latest_version.try(:version_number).to_i + 1
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,23 @@
1
+ require 'rails/generators'
2
+
3
+ module Secretary
4
+ class InstallGenerator < Rails::Generators::Base
5
+ include Rails::Generators::Migration
6
+
7
+ if ActiveRecord::VERSION::MAJOR < 4
8
+ require 'rails/generators/active_record/migration'
9
+ extend ActiveRecord::Generators::Migration
10
+ else
11
+ require 'rails/generators/active_record'
12
+ def self.next_migration_number(*args)
13
+ ActiveRecord::Generators::Base.next_migration_number(*args)
14
+ end
15
+ end
16
+
17
+ source_root File.expand_path("../templates", __FILE__)
18
+
19
+ def copy_migration
20
+ migration_template "versions_migration.rb", "db/migrate/create_versions"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ class SecretaryCreateVersions < ActiveRecord::Migration
2
+ def change
3
+ create_table "versions" do |t|
4
+ t.integer "version_number"
5
+ t.string "versioned_type"
6
+ t.integer "versioned_id"
7
+ t.string "user_id"
8
+ t.text "description"
9
+ t.text "object_changes"
10
+ t.datetime "created_at"
11
+ end
12
+
13
+ add_index "versions", ["created_at"]
14
+ add_index "versions", ["user_id"]
15
+ add_index "versions", ["version_number"]
16
+ add_index "versions", ["versioned_type", "versioned_id"]
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ module Secretary
2
+ class Config
3
+ DEFAULTS = {
4
+ :user_class => "::User",
5
+ :ignored_attributes => ['id', 'created_at', 'updated_at']
6
+ }
7
+
8
+
9
+ attr_writer :user_class
10
+ def user_class
11
+ @user_class || DEFAULTS[:user_class]
12
+ end
13
+
14
+ attr_writer :ignored_attributes
15
+ def ignored_attributes
16
+ @ignored_attributes || DEFAULTS[:ignored_attributes]
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,4 @@
1
+ module Secretary
2
+ class Engine < ::Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,10 @@
1
+ class Secretary::NotVersionedError < StandardError
2
+ def initialize(klasses=nil)
3
+ @klasses = Array(klasses)
4
+ end
5
+
6
+ def message
7
+ "Can't track an association on an unversioned model " \
8
+ "(#{@klasses.join(", ")}) Did you declare `has_secretary` first?"
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module Secretary
2
+ GEM_VERSION = "1.0.0.beta1"
3
+ end
@@ -0,0 +1,75 @@
1
+ module Secretary
2
+ module HasSecretary
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ # Check if a class is versioned
7
+ def has_secretary?
8
+ !!@_has_secretary
9
+ end
10
+
11
+ # Apply to any class that should be versioned
12
+ def has_secretary
13
+ @_has_secretary = true
14
+ Secretary.versioned_models.push self.name
15
+
16
+ has_many :versions,
17
+ :class_name => "Secretary::Version",
18
+ :as => :versioned,
19
+ :dependent => :destroy
20
+
21
+ attr_accessor :logged_user_id
22
+
23
+ after_save :generate_version, if: -> { self.changed? }
24
+ after_commit :clear_custom_changes
25
+
26
+ send :include, InstanceMethodsOnActivation
27
+ end
28
+ end
29
+
30
+
31
+ module InstanceMethodsOnActivation
32
+ # Generate a version for this object.
33
+ def generate_version
34
+ Version.generate(self)
35
+ end
36
+
37
+ # Use Rails built-in Dirty attributions to get
38
+ # the easy ones. By the time we're generating
39
+ # this version, this hash could already
40
+ # exist with some custom changes.
41
+ def changes
42
+ self.custom_changes.reverse_merge super
43
+ end
44
+
45
+ # Use Rails' `changed?`, plus check our own custom changes
46
+ # to see if an object has been modified.
47
+ def changed?
48
+ super || custom_changes.present?
49
+ end
50
+
51
+ # Similar to ActiveModel::Dirty#changes, but lets us
52
+ # pass in some custom changes (such as associations)
53
+ # which wouldn't be picked up by the built-in method.
54
+ #
55
+ # This method should only be used for adding custom changes
56
+ # to the changes hash. For storing and comparing and whatnot,
57
+ # use #changes as usual.
58
+ #
59
+ # This method basically exists just to get around the behavior
60
+ # of #changes (since it sends the attribute message to the
61
+ # object, which we don't always want, for associations for
62
+ # example).
63
+ def custom_changes
64
+ @custom_changes ||= HashWithIndifferentAccess.new
65
+ end
66
+
67
+
68
+ private
69
+
70
+ def clear_custom_changes
71
+ self.custom_changes.clear
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,146 @@
1
+ module Secretary
2
+ module TracksAssociation
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ # Track the associations passed-in
7
+ # This will make sure that when you change the association,
8
+ # the saved record will get a new version, with association
9
+ # diffs and everything.
10
+ #
11
+ # Example:
12
+ #
13
+ # has_secretary
14
+ #
15
+ # has_many :bylines,
16
+ # :as => :content,
17
+ # :class_name => "ContentByline",
18
+ # :dependent => :destroy
19
+ #
20
+ # tracks_association :bylines
21
+ #
22
+ # Forcing the changes into the custom_changes allows us
23
+ # to keep track of dirty associations, so that checking stuff
24
+ # like `changed?` will work.
25
+ #
26
+ # If you want to control when an association should be left out of the
27
+ # version, define an instance method named `should_reject_#{name}?`.
28
+ # This method takes a hash of the model's attributes (so you can pass
29
+ # in, for example, form params). This also lets you easily share this
30
+ # method with `accepts_nested_attributes_for`.
31
+ #
32
+ # Example:
33
+ #
34
+ # class Person < ActiveRecord::Base
35
+ # has_secretary
36
+ # has_many :animals
37
+ # tracks_association :animals
38
+ #
39
+ # accepts_nested_attributes_for :animals,
40
+ # :reject_if => :should_reject_animals?
41
+ #
42
+ # private
43
+ #
44
+ # def should_reject_animals?(attributes)
45
+ # attributes['name'].blank?
46
+ # end
47
+ # end
48
+ def tracks_association(*associations)
49
+ if !self.has_secretary?
50
+ raise NotVersionedError, self.name
51
+ end
52
+
53
+ self.versioned_attributes += associations.map(&:to_s)
54
+
55
+ include InstanceMethodsOnActivation
56
+
57
+ associations.each do |name|
58
+ module_eval <<-EOE, __FILE__, __LINE__ + 1
59
+ def #{name}_were
60
+ @#{name}_were ||= association_was("#{name}")
61
+ end
62
+
63
+ def #{name}_changed?
64
+ association_changed?("#{name}")
65
+ end
66
+
67
+
68
+ private
69
+
70
+ def preload_#{name}_were(object)
71
+ #{name}_were
72
+ end
73
+
74
+ def check_for_#{name}_changes
75
+ check_for_association_changes("#{name}")
76
+ end
77
+
78
+ def clear_dirty_#{name}
79
+ @#{name}_were = nil
80
+ end
81
+ EOE
82
+
83
+ before_save :"check_for_#{name}_changes"
84
+ after_commit :"clear_dirty_#{name}"
85
+
86
+ add_callback_methods("before_add_for_#{name}", [
87
+ :"preload_#{name}_were"
88
+ ])
89
+
90
+ add_callback_methods("before_remove_for_#{name}", [
91
+ :"preload_#{name}_were"
92
+ ])
93
+
94
+ add_callback_methods("after_add_for_#{name}", [])
95
+ add_callback_methods("after_remove_for_#{name}", [])
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def add_callback_methods(method_name, new_methods)
102
+ original = send(method_name)
103
+ methods = original + new_methods
104
+ send("#{method_name}=", methods)
105
+ end
106
+ end
107
+
108
+
109
+ module InstanceMethodsOnActivation
110
+ private
111
+
112
+ # This has to be run in a before_save callback,
113
+ # because we can't rely on the after_add, etc. callbacks
114
+ # to fill in our custom changes. For example, setting
115
+ # `self.animals_attributes=` doesn't run these callbacks.
116
+ def check_for_association_changes(name)
117
+ persisted = self.send("#{name}_were")
118
+ current = self.send(name).to_a.reject(&:marked_for_destruction?)
119
+
120
+ persisted_attributes = persisted.map(&:versioned_attributes)
121
+ current_attributes = current.map(&:versioned_attributes)
122
+
123
+ if persisted_attributes != current_attributes
124
+ ensure_custom_changes_for_association(name, persisted)
125
+ self.custom_changes[name][1] = current_attributes
126
+ end
127
+ end
128
+
129
+ def association_was(name)
130
+ self.persisted? ? self.class.find(self.id).send(name).to_a : []
131
+ end
132
+
133
+ def association_changed?(name)
134
+ check_for_association_changes(name)
135
+ self.custom_changes[name].present?
136
+ end
137
+
138
+ def ensure_custom_changes_for_association(name, persisted=nil)
139
+ self.custom_changes[name] ||= [
140
+ (persisted || self.send("#{name}_were")).map(&:versioned_attributes),
141
+ Array.new
142
+ ]
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,58 @@
1
+ module Secretary
2
+ module VersionedAttributes
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class << self
7
+ # Set the attributes which Secretary should keep track of.
8
+ #
9
+ # Example:
10
+ #
11
+ # class Article < ActiveRecord::Base
12
+ # self.versioned_attributes = [:id, :created_at]
13
+ # end
14
+ #
15
+ # Instead of setting `versioned_attributes` explicitly,
16
+ # you can set `unversioned_attributes` to tell Secretary
17
+ # which attributes to ignore.
18
+ #
19
+ # Note: These should be set before any `tracks_association`
20
+ # macros are called.
21
+ #
22
+ # Each takes an array of column names *as strings*.
23
+ attr_writer :versioned_attributes
24
+
25
+ def versioned_attributes
26
+ @versioned_attributes ||=
27
+ self.column_names -
28
+ Secretary.config.ignored_attributes -
29
+ unversioned_attributes
30
+ end
31
+
32
+ def unversioned_attributes=(array)
33
+ self.versioned_attributes -= array
34
+ end
35
+
36
+ private
37
+
38
+ def unversioned_attributes
39
+ @unversioned_attributes ||= []
40
+ end
41
+ end
42
+ end
43
+
44
+
45
+ # The hash that gets serialized into the `object_changes` column.
46
+ def versioned_changes
47
+ self.changes.select { |k,_| versioned_attribute?(k) }.to_hash
48
+ end
49
+
50
+ def versioned_attributes
51
+ self.as_json(root: false).select { |k,_| versioned_attribute?(k) }.to_hash
52
+ end
53
+
54
+ def versioned_attribute?(key)
55
+ self.class.versioned_attributes.include?(key.to_s)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,3 @@
1
+ # This file is nothing.
2
+ # It's here to trick rubygems. >:D
3
+ require 'secretary'