custom_change_messages 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 [Jeremy Olliver]
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 ADDED
@@ -0,0 +1,105 @@
1
+ CustomChangeMessages
2
+ ====================
3
+
4
+ CustomChangeMessages is a rails plugin for providing a nicely formatted log message recording any edits made to an active record object.
5
+ This is based off of the ActiveRecord::Dirty module, making this plugin compatible with rails versions 2.1 and up. There is little to no configuration required, with all columns included by default (except for id, and created_at, updated_at timestamps), and belongs_to associations handled nicely with the default options configurable.
6
+
7
+ Usage
8
+ =====
9
+
10
+ # In a controllers #update action
11
+
12
+ def update
13
+ @post = Post.find(params[:id])
14
+ @post.attributes = params[:post]
15
+ @changes = @post.change_messages
16
+ # Use this array to either log history, display in a flash message, or in a mailer.
17
+ # => ["Title has changed from 'Ruby on Rails plugin' to '[Update] Ruby on Rails plugin'", "Category has changed from 'Ruby' to 'Ruby on Rails'"]
18
+ @post.save!
19
+ end
20
+
21
+ API
22
+ ===
23
+
24
+ ActiveRecord extensions:
25
+
26
+ change_message_for(attribute) # Returns a string message representation of the attribute that has changed
27
+ change_messages # Returns an array of the messages for each changed attribute
28
+
29
+ Installation
30
+ ============
31
+
32
+ gem install custom_change_messages
33
+
34
+ Rails 2.X
35
+ # config/environment.rb
36
+ config.gem "custom_change_messages"
37
+
38
+ Rails 3.X
39
+ # Gemfile
40
+ gem "custom_change_messages"
41
+
42
+
43
+ Requirements: active record, version 2.1 or greater, if you use an earlier version you can try using http://code.bitsweat.net/svn/dirty which backports the ActiveRecord::Dirty code that this gem depends on to earlier rails versions.
44
+
45
+
46
+ Detailed Example
47
+ ================
48
+
49
+ The main use of this is to help clean up controller actions, such as:
50
+
51
+ class ItemsController < ApplicationController
52
+
53
+ def update
54
+ @item.attributes = params[:item]
55
+ Mailer.deliver_item_update(@item, @item.change_messages.to_sentence)
56
+ @item.save!
57
+
58
+ rescue ActiveRecord::RecordInvalid => e
59
+ flash[:error] = e.reord.error_messages
60
+ redirect_to item_url(@item)
61
+ end
62
+ #...
63
+ end
64
+
65
+
66
+ @item.change_messages.to_sentence will return human readable mesages such as:
67
+ => "Description has changed from 'Nice and easy' to 'This task is now rather long and arduous', User has changed from 'Jeremy' to 'Guy', and Due Date has been rescheduled from '09/11/2008' to '10/11/2008'"
68
+
69
+ The messages for each attribute are also customizable, which is especially handy for dealing with belongs_to
70
+ assocations. Here's a more complicated example:
71
+
72
+ Use the custom_message_for method to customize the message for the attribute, specifying :display => :name
73
+ will use the method/attribute :name for displaying the record that the item belongs_to
74
+
75
+ The skip_message_for method can be used to prevent stop any changes to a particular attribute showing up
76
+
77
+ class Item < ActiveRecord::Base
78
+ belongs_to :person
79
+
80
+ custom_message_for :person, :display => :username # display the person's username instead of the id
81
+ custom_message_for :due_on, :as => "Due Date", :message => "has been rescheduled", :format => :pretty_print_date
82
+ # change the syntax of the message for working with dates, because it makes more sense that way
83
+
84
+ # this method is used for formatting the due_on field when it changes
85
+ def pretty_print_date(value = self.due_on)
86
+ value.strftime("%d/%m/%Y")
87
+ end
88
+ end
89
+
90
+ class Person < ActiveRecord::Base
91
+ custom_message_for :username, :as => "Name"
92
+ skip_message_for :internal_calculation
93
+ end
94
+
95
+ p = Person.create!(:username => "Jeremy")
96
+ p2 = Person.create!(:username => "Optimus Prime")
97
+ i = Item.create!(:name => "My Task", :description => nil, :person => p, :due_on => Date.today)
98
+ i.attributes = {:person => p2, :description => "This task is difficult, might need some help"}
99
+
100
+ i.change_messages
101
+ => ["Due Date has been rescheduled from '4/12/2008' to '5/12/2008'", "Person has changed from 'Jeremy' to 'Optimus Prime'", "Description has changed from '' to 'This task is difficult, might need some help'"]
102
+
103
+
104
+
105
+ Copyright (c) 2008 Jeremy Olliver, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+
6
+ desc 'Default: run unit tests.'
7
+ task :default => :test
8
+
9
+ desc 'Run tests for custom_change_messages'
10
+ Rake::TestTask.new(:test) do |t|
11
+ t.libs << 'lib'
12
+ t.pattern = 'test/**/*_test.rb'
13
+ t.verbose = true
14
+ end
15
+
16
+ desc 'Generate documentation for the custom_change_messages gem.'
17
+ Rake::RDocTask.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = 'Custom Change Messages'
20
+ rdoc.options << '--line-numbers' << '--inline-source'
21
+ rdoc.rdoc_files.include('README')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ end
@@ -0,0 +1 @@
1
+ require 'custom_change_messages/active_record'
@@ -0,0 +1,184 @@
1
+ ActiveRecord::Base.class_eval do
2
+
3
+ # hash and array to keep track of the customised messages, belongs_to associations, and any skipped attributes
4
+ class_inheritable_hash :custom_dirty_messages # The watched attributes with configuration options {:field_name => options, ...}
5
+ class_inheritable_hash :belongs_to_key_mapping
6
+
7
+ CUSTOM_CHANGE_MESSAGE_DEFAULTS = {:message => "has changed", :prefix => "from", :suffix => "to"}
8
+ DEFAULT_SKIPPED_COLUMNS = [:updated_at, :created_at, :id]
9
+
10
+ DEFAULT_BELONGS_TO_DISPLAY_OPTIONS = [:name, :title, :display, :description]
11
+
12
+ class << self
13
+
14
+ def initialise_default_change_messages
15
+ unless self.custom_dirty_messages
16
+ self.custom_dirty_messages = {}
17
+ self.belongs_to_key_mapping = {}
18
+
19
+ model_columns = self.column_names.collect(&:to_sym)
20
+
21
+ # Don't include foreign keys for belongs_to associations by default, they must be added manually
22
+ self.reflect_on_all_associations(:belongs_to).each do |association|
23
+ # model_columns -= [association.primary_key_name.to_sym] # Remove the key name from the attributes that will be watched by default
24
+ self.belongs_to_key_mapping.merge!(association.primary_key_name.to_sym => association.name)
25
+ end
26
+
27
+ # Register each column with default options
28
+ model_columns.each do |column_name|
29
+ key = key_name_for(column_name)
30
+ next if DEFAULT_SKIPPED_COLUMNS.include?(key)
31
+ # custom_dirty_messages[key] = CUSTOM_CHANGE_MESSAGE_DEFAULTS.clone
32
+ custom_message_for(key)
33
+ end
34
+ end
35
+ end
36
+
37
+ def custom_message_for(*attr_names)
38
+ initialise_default_change_messages
39
+
40
+ options = attr_names.extract_options!
41
+ options.symbolize_keys!
42
+
43
+ attr_names.each do |attribute|
44
+ key = key_name_for(attribute)
45
+
46
+ if is_association?(key)
47
+ association = self.reflect_on_association(key)
48
+ display_method = options[:display]
49
+ raise "Incorrect :display option. #{display_method} is undefined for #{association.class_name}" if display_method && !method_or_attribute_exists(association, display_method)
50
+ display_method ||= find_default_display_method(association)
51
+ puts "***Warning*** couldn't detect a display method for #{key.to_s}, please set a display option e.g. custom_message_for :#{key.to_s}, :display => :my_display_method (where #{association.class_name}#my_display_method) is defined otherwise #to_s will be used as the default" unless display_method
52
+ display_method ||= :to_s
53
+
54
+ defaults = CUSTOM_CHANGE_MESSAGE_DEFAULTS.merge({:as => association.name.to_s.humanize.titleize, :display => :to_s})
55
+ options = defaults.merge(options).merge({:type => :belongs_to})
56
+ end
57
+
58
+ if self.custom_dirty_messages[key]
59
+ # override defaults
60
+ self.custom_dirty_messages[key].merge!(options)
61
+ else
62
+ # Set values for any not already being watched
63
+ self.custom_dirty_messages.merge!({key => options})
64
+ end
65
+ end
66
+ end
67
+
68
+ def skip_message_for(*attr_names)
69
+ initialise_default_change_messages
70
+
71
+ attr_names.extract_options!
72
+ attr_names.each do |column_name|
73
+ key = key_name_for(column_name)
74
+ self.custom_dirty_messages.delete(key)
75
+ end
76
+ end
77
+
78
+ def is_association?(attribute)
79
+ belongs_to_key_mapping.keys.include?(attribute.to_sym) || belongs_to_key_mapping.values.include?(attribute.to_sym)
80
+ end
81
+
82
+ def key_name_for(attribute)
83
+ attribute = attribute.to_sym
84
+ if is_association?(attribute)
85
+ # Use the association name for belongs_to (could be already passed in)
86
+ belongs_to_key_mapping[attribute] || attribute
87
+ else
88
+ attribute
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def method_or_attribute_exists(association, method)
95
+ klass = association.class_name.constantize
96
+ (klass.column_names + klass.instance_methods).include?(method.to_s)
97
+ end
98
+
99
+ def find_default_display_method(association)
100
+ DEFAULT_BELONGS_TO_DISPLAY_OPTIONS.each do |meth_name|
101
+ return meth_name if method_or_attribute_exists(association, meth_name)
102
+ end
103
+ nil
104
+ end
105
+
106
+ end
107
+
108
+ def change_messages
109
+ self.class.initialise_default_change_messages
110
+
111
+ messages = []
112
+ changes.each do |attribute, diff|
113
+ key = self.class.key_name_for(attribute) # belongs_to association name, or column_name
114
+
115
+ if self.class.custom_dirty_messages.keys.include?(key)
116
+ messages << change_message_for(key, diff)
117
+ end
118
+ end
119
+ messages
120
+ end
121
+
122
+ def change_message_for(attribute, changes = nil)
123
+ self.class.initialise_default_change_messages
124
+
125
+ column_name = column_name_for(attribute)
126
+ changes ||= self.send((column_name.to_s + "_change").to_sym)
127
+
128
+ key = self.class.key_name_for(attribute)
129
+
130
+ val = "#{attr_name(key)} #{message_option_value(key, :message)}"
131
+ val += " #{message_option_value(key, :prefix)} \'#{attr_display(key, changes.first)}\'" unless message_option_value(key, :no_prefix)
132
+ val += " #{message_option_value(key, :suffix)} \'#{attr_display(key, changes.last)}\'" unless message_option_value(key, :no_suffix)
133
+ val
134
+ end
135
+
136
+ private
137
+
138
+ def column_name_for(attribute)
139
+ attribute = attribute.to_sym
140
+ if self.class.belongs_to_key_mapping.values.include?(attribute)
141
+ self.class.belongs_to_key_mapping.to_a.select {|col, assoc_name| assoc_name == attribute }.first.first
142
+ else
143
+ attribute
144
+ end
145
+ end
146
+
147
+ # check if it's an association name, or if the attribute is being watched
148
+ def attr_name(key)
149
+ value = if self.class.custom_dirty_messages[key]
150
+ if (name = self.class.custom_dirty_messages[key][:as])
151
+ name
152
+ else
153
+ key
154
+ end
155
+ else
156
+ key
157
+ end
158
+ value.to_s.humanize.titleize
159
+ end
160
+
161
+ def attr_display(key, value)
162
+ if self.class.custom_dirty_messages[key]
163
+ if (meth = self.class.custom_dirty_messages[key][:format])
164
+ return self.send(meth, value)
165
+ elsif (meth = self.class.custom_dirty_messages[key][:display]) && self.class.is_association?(key)
166
+ assoc = self.class.reflect_on_association(key)
167
+ raise "must set the :display option for belongs_to associations e.g. :display => :name where name is a method on the parent object" unless meth
168
+ finder = ("find_by_" + assoc.klass.primary_key).to_sym
169
+ return assoc.klass.send(finder, value).send(meth.to_sym)
170
+ end
171
+ end
172
+ return value.to_s
173
+ end
174
+
175
+ def message_option_value(key, option)
176
+ if self.class.custom_dirty_messages[key]
177
+ self.class.custom_dirty_messages[key][option] || CUSTOM_CHANGE_MESSAGE_DEFAULTS[option]
178
+ else
179
+ CUSTOM_CHANGE_MESSAGE_DEFAULTS[option]
180
+ end
181
+ end
182
+
183
+
184
+ end
@@ -0,0 +1,115 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ load_schema
4
+
5
+ class Person < ActiveRecord::Base
6
+ custom_message_for :username, :as => "Name"
7
+ skip_message_for :internal_calculation
8
+ end
9
+
10
+ class Category < ActiveRecord::Base
11
+ def to_s
12
+ name
13
+ end
14
+ end
15
+
16
+ class Item < ActiveRecord::Base
17
+ DATEFORMAT = "%d/%m/%Y"
18
+ belongs_to :person
19
+ belongs_to :category
20
+
21
+ custom_message_for :person, :display => :username
22
+ custom_message_for :due_on, :as => "Due Date", :message => "has been rescheduled", :format => :pretty_print_date
23
+
24
+ def pretty_print_date(value = self.due_on)
25
+ value.strftime(DATEFORMAT)
26
+ end
27
+ end
28
+
29
+
30
+ class ActiveRecordTest < Test::Unit::TestCase
31
+
32
+ def test_active_record_extension
33
+ i = Item.create!
34
+ assert i.respond_to?(:change_messages)
35
+ assert i.respond_to?(:change_message_for)
36
+ end
37
+
38
+ def test_ignores_timestamps
39
+ i = Item.create!
40
+ i.attributes = {:created_at => Date.tomorrow, :updated_at => Date.tomorrow}
41
+
42
+ assert i.change_messages.empty?
43
+ end
44
+
45
+ def test_belongs_to_and_keys_ignored_by_default
46
+ c1 = Category.create(:name => "Updates")
47
+ c2 = Category.create(:name => "Posts")
48
+ i = Item.create!(:category => c1)
49
+
50
+ i.category = c2
51
+
52
+ assert_equal ["Category has changed from 'Updates' to 'Posts'"], i.change_messages # The belongs_to association (and it's foreign key) should be ignored by default
53
+ end
54
+
55
+ def test_unwatching
56
+ p = Person.create!(:username => "Robot", :internal_calculation => 1)
57
+ p.internal_calculation = 42
58
+ assert p.change_messages.empty?
59
+ end
60
+
61
+ def test_labeling_attributes
62
+ # ensure associations are given the correct name. In this case username has been renamed to 'Name'
63
+ u = Person.create!(:username => "Jeremy")
64
+ u.username = "Jeremy O"
65
+
66
+ assert_equal "Name has changed from \'Jeremy\' to \'Jeremy O\'", u.change_message_for(:username)
67
+ end
68
+
69
+ def test_associations_loaded
70
+ i = Item.create!(:name => "My Cool Task")
71
+ assert i.class.custom_dirty_messages[:person]
72
+ assert_equal :belongs_to, i.class.custom_dirty_messages[:person][:type]
73
+ end
74
+
75
+ def test_display_of_associations
76
+ u = Person.create!(:username => "Jeremy")
77
+ u2 = Person.create!(:username => "Guy")
78
+ i = Item.create!(:name => "My Task", :description => "super", :person => u)
79
+ i.person = u2
80
+
81
+ assert_equal "Person has changed from \'Jeremy\' to \'Guy\'", i.change_message_for(:person)
82
+ end
83
+
84
+ def test_handling_of_nil_attrs
85
+ i = Item.create!(:name => "Namae wa", :description => nil)
86
+ i.description = "Japanese sentence"
87
+
88
+ assert_nothing_raised do
89
+ i.change_messages
90
+ end
91
+ end
92
+
93
+ def test_formatting_attributes
94
+ i = Item.create!(:name => "Task", :due_on => Date.today)
95
+ i.due_on = Date.tomorrow
96
+
97
+ today = Date.today.strftime(Item::DATEFORMAT)
98
+ tomorrow = Date.tomorrow.strftime(Item::DATEFORMAT)
99
+ assert_equal "Due Date has been rescheduled from '#{today}' to '#{tomorrow}'", i.change_message_for(:due_on)
100
+ end
101
+
102
+ def test_full_sentence_changes
103
+ p = Person.create!(:username => "Jeremy")
104
+ p2 = Person.create!(:username => "Optimus")
105
+ i = Item.create!(:name => "My Task", :description => "Nice and easy", :person => p, :due_on => Date.today)
106
+ i.attributes = {:person => p2, :description => "This task is difficult, might need some help", :due_on => Date.tomorrow }
107
+
108
+ today = Date.today.strftime(Item::DATEFORMAT)
109
+ tomorrow = Date.tomorrow.strftime(Item::DATEFORMAT)
110
+
111
+ assert_equal "Due Date has been rescheduled from '#{today}' to '#{tomorrow}', Person has changed from 'Jeremy' to 'Optimus', and Description has changed from 'Nice and easy' to 'This task is difficult, might need some help'", \
112
+ i.change_messages.to_sentence
113
+ end
114
+
115
+ end
@@ -0,0 +1,25 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class CustomChangeMessagesTest < Test::Unit::TestCase
4
+
5
+ class Item < ActiveRecord::Base
6
+ end
7
+
8
+ class Person < ActiveRecord::Base
9
+ end
10
+
11
+ def setup
12
+ # schema needs to be loaded in the other test, so don't load it here a second time, unless this is run first or isolated
13
+ unless Item.connected? && Person.connected?
14
+ load_schema
15
+ end
16
+ end
17
+
18
+ def test_schema_has_loaded_correctly
19
+ assert_nothing_raised do
20
+ Item.first
21
+ Person.first
22
+ end
23
+ end
24
+
25
+ end
data/test/database.yml ADDED
@@ -0,0 +1,3 @@
1
+ sqlite3:
2
+ adapter: sqlite3
3
+ database: test/custom_change_messages.sqlite3.db
data/test/schema.rb ADDED
@@ -0,0 +1,20 @@
1
+ ActiveRecord::Schema.define(:version => 0) do
2
+ create_table :items, :force => true do |t|
3
+ t.string :name
4
+ t.string :description
5
+ t.integer :person_id
6
+ t.integer :category_id
7
+ t.date :due_on
8
+ t.timestamps
9
+ end
10
+ create_table :people, :force => true do |t|
11
+ t.string :username
12
+ t.string :role
13
+ t.string :internal_calculation
14
+ t.timestamps
15
+ end
16
+ create_table :categories, :force => true do |t|
17
+ t.string :name
18
+ t.timestamps
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ require 'test/unit'
2
+
3
+ require 'rubygems'
4
+ require 'active_record'
5
+
6
+
7
+ def load_schema
8
+ config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
9
+ # ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
10
+
11
+ db_adapter = ENV['DB']
12
+
13
+ # no db passed, try one of these fine config-free DBs before bombing.
14
+ db_adapter ||=
15
+ begin
16
+ require 'rubygems'
17
+ require 'sqlite'
18
+ 'sqlite'
19
+ rescue LoadError
20
+ begin
21
+ require 'sqlite3'
22
+ 'sqlite3'
23
+ rescue LoadError
24
+ puts "could not load sqlite for running the tests"
25
+ end
26
+ end
27
+
28
+ if db_adapter.nil?
29
+ raise "No DB Adapter selected. Pass the DB= option to pick one, or install Sqlite or Sqlite3."
30
+ end
31
+
32
+ ActiveRecord::Base.establish_connection(config[db_adapter])
33
+ load(File.dirname(__FILE__) + "/schema.rb")
34
+ require File.dirname(__FILE__) + '/../rails/init.rb'
35
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: custom_change_messages
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 0
10
+ version: 0.2.0
11
+ platform: ruby
12
+ authors:
13
+ - Jeremy Olliver
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-02-26 00:00:00 +13:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: activerecord
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 11
30
+ segments:
31
+ - 2
32
+ - 1
33
+ - 0
34
+ version: 2.1.0
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: sqlite3-ruby
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 21
46
+ segments:
47
+ - 1
48
+ - 2
49
+ - 5
50
+ version: 1.2.5
51
+ type: :development
52
+ version_requirements: *id002
53
+ description: Change history for ActiveRecord models in a customisable format, making user friendly logging for change history simple
54
+ email: jeremy.olliver@gmail.com
55
+ executables: []
56
+
57
+ extensions: []
58
+
59
+ extra_rdoc_files: []
60
+
61
+ files:
62
+ - README
63
+ - Rakefile
64
+ - MIT-LICENSE
65
+ - lib/custom_change_messages.rb
66
+ - lib/custom_change_messages/active_record.rb
67
+ - test/active_record_test.rb
68
+ - test/custom_change_messages_test.rb
69
+ - test/database.yml
70
+ - test/schema.rb
71
+ - test/test_helper.rb
72
+ has_rdoc: true
73
+ homepage: http://github.com/jeremyolliver/custom_change_messages
74
+ licenses: []
75
+
76
+ post_install_message:
77
+ rdoc_options: []
78
+
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ hash: 3
87
+ segments:
88
+ - 0
89
+ version: "0"
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ hash: 3
96
+ segments:
97
+ - 0
98
+ version: "0"
99
+ requirements: []
100
+
101
+ rubyforge_project:
102
+ rubygems_version: 1.5.2
103
+ signing_key:
104
+ specification_version: 3
105
+ summary: Change history for ActiveRecord models
106
+ test_files:
107
+ - test/active_record_test.rb
108
+ - test/custom_change_messages_test.rb
109
+ - test/database.yml
110
+ - test/schema.rb
111
+ - test/test_helper.rb