attributes_history 0.0.2

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