attributes_history 0.0.2
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.
- 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
|
+
[](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:
|