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 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,3 @@
1
+ module AttributesHistory
2
+ VERSION = '0.0.2'
3
+ end
@@ -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,11 @@
1
+ require 'rspec/core'
2
+
3
+ RSpec.configure do |config|
4
+ config.before(:each) do
5
+ ::AttributesHistory.enabled = false
6
+ end
7
+
8
+ config.before(:each, versioning: true) do
9
+ ::AttributesHistory.enabled = true
10
+ end
11
+ 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: