secretary-rails 1.0.0.beta1

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