attributes_history 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +138 -0
- data/Rakefile +17 -0
- data/lib/attributes_history/gem_version.rb +3 -0
- data/lib/attributes_history/has_attributes_history.rb +38 -0
- data/lib/attributes_history/history_retriever.rb +20 -0
- data/lib/attributes_history/history_saver.rb +40 -0
- data/lib/attributes_history/rspec.rb +11 -0
- data/lib/attributes_history.rb +17 -0
- metadata +178 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c5ab51f8fcbd9cad95f6e3724546f8f5d57fb848
|
4
|
+
data.tar.gz: ecdc7309e290238fc6b21bf46c259b76f9dff6bc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7c05774dccf3d142db49d91e8a949aa4cb4d80159c9839b36a31f62c5c7b2cc7cc82487d473e9c1ae432d651718fa21a5e38a136c12a1b886041a6ea58f2fd43
|
7
|
+
data.tar.gz: d054f8a660744d412454a4c086bb881accbeb0cbc39db75115218ffa3a7b64abbb4efb50e5a67d603f8885547d0243fdce8a025450d4433f0fe15f863bc90967
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2015
|
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,138 @@
|
|
1
|
+
# AttributesHistory
|
2
|
+
|
3
|
+
Date-granular history for specified model fields. Compact & easy to query.
|
4
|
+
|
5
|
+
[![Build Status](https://travis-ci.org/CruGlobal/attributes_history.svg)](https://travis-ci.org/CruGlobal/attributes_history)
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
Include the gem:
|
10
|
+
|
11
|
+
`gem 'attributes_history'`
|
12
|
+
|
13
|
+
Call `has_attributes_history for: [attributes], with_model: AttributesLog` where
|
14
|
+
`attributes` are the ones you want to track and `AttributesLog` will contain the
|
15
|
+
history entries. Your `AttributesLog` model should have a field `recorded_on`
|
16
|
+
which tracks the date when those attributes changed to the values in the next
|
17
|
+
recorded entry (or to the current attribute values).
|
18
|
+
|
19
|
+
## Example
|
20
|
+
|
21
|
+
Here's an example of how you could track the `status` and `pledge` fields
|
22
|
+
for a ministry donor contact in a `PartnerStatusLog` table. This would then allow
|
23
|
+
you to easily query the ministry partner's status and commitment information
|
24
|
+
over time.
|
25
|
+
|
26
|
+
```
|
27
|
+
class Contact < ActiveRecord::Base
|
28
|
+
has_attributes_history for: [:status, :pledge], with_model: PartnerStatusLog
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
Here would be the `ParterStatusLog` model and relevant migrations:
|
33
|
+
|
34
|
+
```
|
35
|
+
class PartnerStatusLog < ActiveRecord::Base
|
36
|
+
end
|
37
|
+
|
38
|
+
class CreateContacts < ActiveRecord::Migration
|
39
|
+
def change
|
40
|
+
create_table :contacts do |t|
|
41
|
+
t.string :name
|
42
|
+
t.string :status
|
43
|
+
t.decimal :pledge
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class CreatePartnerStatusLogs < ActiveRecord::Migration
|
49
|
+
def change
|
50
|
+
create_table :partner_status_logs do |t|
|
51
|
+
t.integer :contact_id, null: false
|
52
|
+
t.date :recorded_on, null: false
|
53
|
+
t.string :status
|
54
|
+
t.decimal :pledge
|
55
|
+
end
|
56
|
+
|
57
|
+
add_index :partner_status_logs, :contact_id
|
58
|
+
add_index :partner_status_logs, :recorded_on
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
## Retrieving past values with `attribute_on_date` methods
|
64
|
+
|
65
|
+
To make retrieving previous values easy, `attributes_history` defines an
|
66
|
+
`attribute_on_date(attribute, date)` method, as well as specific
|
67
|
+
`#{attribute}_on_date` method for each of your histroy-tracked attributes
|
68
|
+
which returns that value on the specified date based on the log.
|
69
|
+
|
70
|
+
For instance, in the ministry partner history example, there would
|
71
|
+
be methods `status_on_date` and `pledge_on_date` that would return the
|
72
|
+
`status` or `pledge` for a contact for that given date. You could also call
|
73
|
+
`attribute_on_date(:pledge, date)` to get the pledge value for a given date.
|
74
|
+
|
75
|
+
The log is granular by date and so it makes the assumption that a change
|
76
|
+
any time during a date is effective for the whole of that date. The
|
77
|
+
`attribute_on_date` methods will use caching so if you look up multiple fields on
|
78
|
+
the same date only one query will be performed.
|
79
|
+
|
80
|
+
## Querying the log table directly
|
81
|
+
|
82
|
+
You can also query the history log table directly. The `recorded_on` field in the
|
83
|
+
table represents the date that set of attributes was replaced by a new
|
84
|
+
set, either in a subsequent history record, or in the object itself.
|
85
|
+
|
86
|
+
So to look up the version for a particular date, do a query like this:
|
87
|
+
```
|
88
|
+
current_version = contact.partner_status_logs
|
89
|
+
.where('recorded_on > ?', date).order(:recorded_on).first || contact
|
90
|
+
```
|
91
|
+
That will give either a `PartnerStatusLog` instance for the past, or the current
|
92
|
+
`Contact` instance for the present record, both of which will respond to the
|
93
|
+
history-tracked attributes of `status` and `pledge`.
|
94
|
+
|
95
|
+
This is similar to how [paper_trail](https://github.com/airblade/paper_trail)
|
96
|
+
works in that the versions represent past data, and only the current regular
|
97
|
+
model record (contact in this case) has the current state.
|
98
|
+
|
99
|
+
## Enabling and disabling
|
100
|
+
|
101
|
+
By default the history logging is enabled once you set it up, but you can
|
102
|
+
disable it by setting `AttributesHistory.enabled = false` (and reset it back to
|
103
|
+
`true` also).
|
104
|
+
|
105
|
+
## Testing with RSpec
|
106
|
+
|
107
|
+
For testing with RSpec, you can `require 'attributes_history/rspec'` which will
|
108
|
+
disable attribute history by default in your specs unless you specify
|
109
|
+
`versioning: true` in the spec metadata, or you explicitly set
|
110
|
+
`AttributesHistory.enabled = true`.
|
111
|
+
|
112
|
+
## Designed to complement (not replace) a full audit trail
|
113
|
+
|
114
|
+
This is intended to augment a full audit trail solution like
|
115
|
+
[paper_trail](https://github.com/airblade/paper_trail). The advantage of a full
|
116
|
+
audit trail is that you track every change in a consistent way across models.
|
117
|
+
|
118
|
+
But it's possible for the full audit trail to become large and it's often stored in
|
119
|
+
a less easily queryable way (object data stored in a generic `object` field as
|
120
|
+
YAML/JSON).
|
121
|
+
|
122
|
+
If you use auto-saving or make single attribute changes easy, then you may get a
|
123
|
+
lot of updates in the same day which semantically represent a single update.
|
124
|
+
|
125
|
+
This `attributes_history` gem allows you to choose a subset of fields for a
|
126
|
+
particular model that will be tracked with at most one new version per day to
|
127
|
+
limit growth and make time-series displaying of the versions easier. And is
|
128
|
+
designed to store the versions in specialized table(s) per model with fields that
|
129
|
+
parallel those in the model itself so you can more easily query the historical
|
130
|
+
fields.
|
131
|
+
|
132
|
+
## Acknowledgement and License
|
133
|
+
|
134
|
+
Credit to [Spencer Oberstadt](https://github.com/soberstadt) for coming up with
|
135
|
+
the idea of versioning our partner status log with date level granularity to
|
136
|
+
keep it compact and easy to query.
|
137
|
+
|
138
|
+
AttributesHistory is [MIT Licensed](https://github.com/CruGlobal/attributes_history/blob/master/MIT-LICENSE).
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'VersionableByDate'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
Bundler::GemHelper.install_tasks
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require_relative 'history_saver'
|
3
|
+
require_relative 'history_retriever'
|
4
|
+
|
5
|
+
module AttributesHistory
|
6
|
+
module HasAttributesHistory
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
# The options should include the keys :attributes and :version_class
|
11
|
+
def has_attributes_history(options)
|
12
|
+
class_attribute :history_model, :history_attributes, :history_association
|
13
|
+
self.history_model = options[:with_model]
|
14
|
+
self.history_attributes = options[:for].map(&:to_s)
|
15
|
+
self.history_association = history_model.name.underscore.pluralize.to_sym
|
16
|
+
|
17
|
+
has_many history_association
|
18
|
+
after_update { HistorySaver.new(self).save_if_needed }
|
19
|
+
define_verisons_by_date_lookups
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def define_verisons_by_date_lookups
|
25
|
+
define_method :attribute_on_date do |attribute, date|
|
26
|
+
@history_retriever ||= HistoryRetriever.new(self)
|
27
|
+
@history_retriever.attribute_on_date(attribute, date)
|
28
|
+
end
|
29
|
+
|
30
|
+
history_attributes.each do |attribute|
|
31
|
+
define_method "#{attribute}_on_date" do |date|
|
32
|
+
attribute_on_date(attribute, date)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module AttributesHistory
|
2
|
+
class HistoryRetriever
|
3
|
+
def initialize(object)
|
4
|
+
@object = object
|
5
|
+
@cached_history = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def attribute_on_date(attribute, date)
|
9
|
+
history_entry = @cached_history[date] ||= find_entry_on(date)
|
10
|
+
history_entry.public_send(attribute)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def find_entry_on(date)
|
16
|
+
@object.public_send(@object.history_association)
|
17
|
+
.where('recorded_on > ?', date).order(:recorded_on).first || @object
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module AttributesHistory
|
2
|
+
class HistorySaver
|
3
|
+
def initialize(changed_object)
|
4
|
+
@object = changed_object
|
5
|
+
end
|
6
|
+
|
7
|
+
def save_if_needed
|
8
|
+
save_history_entry if ::AttributesHistory.enabled? &&
|
9
|
+
history_attributes_changed?
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def history_attributes_changed?
|
15
|
+
(@object.changed & @object.history_attributes).present?
|
16
|
+
end
|
17
|
+
|
18
|
+
def save_history_entry
|
19
|
+
history_entry = @object.public_send(@object.history_association)
|
20
|
+
.find_or_initialize_by(recorded_on: Date.current)
|
21
|
+
|
22
|
+
# If there is an existing history record for today, just leave it as is,
|
23
|
+
# otherwise, save the newly initialized one.
|
24
|
+
history_entry.update!(history_params) if history_entry.new_record?
|
25
|
+
end
|
26
|
+
|
27
|
+
def history_params
|
28
|
+
Hash[@object.history_attributes.map { |f| [f, history_value_for(f)] }]
|
29
|
+
end
|
30
|
+
|
31
|
+
def history_value_for(attribute)
|
32
|
+
# Use the previous value if it was changed, otherwise the current value
|
33
|
+
if attribute.in?(@object.changed)
|
34
|
+
@object.changes[attribute].first
|
35
|
+
else
|
36
|
+
@object.public_send(attribute)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'attributes_history/gem_version.rb'
|
2
|
+
require 'attributes_history/has_attributes_history.rb'
|
3
|
+
|
4
|
+
module AttributesHistory
|
5
|
+
class << self
|
6
|
+
attr_writer :enabled
|
7
|
+
|
8
|
+
def enabled?
|
9
|
+
# Enabled by default
|
10
|
+
@enabled.nil? ? true : @enabled
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
ActiveSupport.on_load(:active_record) do
|
16
|
+
include AttributesHistory::HasAttributesHistory
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: attributes_history
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- draffensperger
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-12-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.0'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '6.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '3.0'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '6.0'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: activesupport
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '3.0'
|
40
|
+
- - "<"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '6.0'
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '3.0'
|
50
|
+
- - "<"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '6.0'
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: rails
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: 4.2.5
|
60
|
+
type: :development
|
61
|
+
prerelease: false
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - "~>"
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: 4.2.5
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: rspec-rails
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - "~>"
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: 3.4.0
|
74
|
+
type: :development
|
75
|
+
prerelease: false
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - "~>"
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: 3.4.0
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: rubocop
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - "~>"
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 0.35.1
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - "~>"
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: 0.35.1
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: sqlite3
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
- !ruby/object:Gem::Dependency
|
110
|
+
name: pry
|
111
|
+
requirement: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
type: :development
|
117
|
+
prerelease: false
|
118
|
+
version_requirements: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
123
|
+
- !ruby/object:Gem::Dependency
|
124
|
+
name: pry-byebug
|
125
|
+
requirement: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - ">="
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
type: :development
|
131
|
+
prerelease: false
|
132
|
+
version_requirements: !ruby/object:Gem::Requirement
|
133
|
+
requirements:
|
134
|
+
- - ">="
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '0'
|
137
|
+
description: Date-granular history for specified model fields. Compact & easy to query.
|
138
|
+
email:
|
139
|
+
- d.raffensperger@gmail.com
|
140
|
+
executables: []
|
141
|
+
extensions: []
|
142
|
+
extra_rdoc_files: []
|
143
|
+
files:
|
144
|
+
- MIT-LICENSE
|
145
|
+
- README.md
|
146
|
+
- Rakefile
|
147
|
+
- lib/attributes_history.rb
|
148
|
+
- lib/attributes_history/gem_version.rb
|
149
|
+
- lib/attributes_history/has_attributes_history.rb
|
150
|
+
- lib/attributes_history/history_retriever.rb
|
151
|
+
- lib/attributes_history/history_saver.rb
|
152
|
+
- lib/attributes_history/rspec.rb
|
153
|
+
homepage: https://github.com/CruGlobal/attributes_history
|
154
|
+
licenses:
|
155
|
+
- MIT
|
156
|
+
metadata: {}
|
157
|
+
post_install_message:
|
158
|
+
rdoc_options: []
|
159
|
+
require_paths:
|
160
|
+
- lib
|
161
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
167
|
+
requirements:
|
168
|
+
- - ">="
|
169
|
+
- !ruby/object:Gem::Version
|
170
|
+
version: '0'
|
171
|
+
requirements: []
|
172
|
+
rubyforge_project:
|
173
|
+
rubygems_version: 2.4.8
|
174
|
+
signing_key:
|
175
|
+
specification_version: 4
|
176
|
+
summary: Date-granular history for specified model fields. Compact & easy to query.
|
177
|
+
test_files: []
|
178
|
+
has_rdoc:
|