culturecode-track_changes 0.2.0 → 0.3.0
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 +4 -4
- data/README.md +14 -0
- data/app/helpers/track_changes/diff_helper.rb +7 -5
- data/app/models/track_changes/diff.rb +4 -2
- data/app/models/track_changes/snapshot.rb +16 -3
- data/lib/track_changes/configuration.rb +6 -0
- data/lib/track_changes/engine.rb +1 -0
- data/lib/track_changes/model.rb +53 -6
- data/lib/track_changes/version.rb +1 -1
- metadata +4 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0c9bb5bdbc7a137501b16be9525858c58cd8665a673f0d81616e7ea08bda7c5b
|
4
|
+
data.tar.gz: 6b1b61340da3ca8e3af00f43cb8b4ed8fb634a03a5111301e9d30501258bccad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 458e536d0b57c406cc66b150b5be511a086aaf5b1fc6aad7e282d0aeb1d8bd4cfb1ce1296820ccfaf03098c25b6d38d6d8ddd4dbfce3195160c803666cd4b3da
|
7
|
+
data.tar.gz: da6901abda1bd01c87b01b0f7cc00be43226f0f7e29a1d480790542930d6a95e86d8a7d76b5a7c0d07839e79217d29a1eb9660318de29fbaf7f840775ec0d55f
|
data/README.md
CHANGED
@@ -24,6 +24,13 @@ class CreateTrackChangesTables < ActiveRecord::Migration
|
|
24
24
|
end
|
25
25
|
```
|
26
26
|
|
27
|
+
## Configuration
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
TrackChanges::Configuration.cascade_destroy = false # Controls whether tracked changes are deleted when the record is deleted. Can be set to false if an audit trail of destroyed records is desired. Default: true
|
31
|
+
TrackChanges::Configuration.serialize = false # Controls whether tracked changes are serialized as YAML before being written to the database. Can be set to false if the `state`, `from`, and `to` columns are JSON datatype instead of text. Default: true
|
32
|
+
```
|
33
|
+
|
27
34
|
## Usage
|
28
35
|
|
29
36
|
```ruby
|
@@ -32,6 +39,11 @@ class Person < ActiveRecord::Base
|
|
32
39
|
end
|
33
40
|
```
|
34
41
|
|
42
|
+
```ruby
|
43
|
+
Person.snapshot_all # Initialize snapshots for existing records so the next time the record is saved a diff can be generated.
|
44
|
+
Person.tracked_change(status: "approved", assigned_to: "user_123") # Returns a list of diffs where specific attributes changed to the specified values.
|
45
|
+
```
|
46
|
+
|
35
47
|
### Options
|
36
48
|
By default all model attributes are tracked, except the primary_key, usually ```id```, ```created_at```, and ```updated_at```.
|
37
49
|
|
@@ -40,6 +52,8 @@ By default all model attributes are tracked, except the primary_key, usually ```
|
|
40
52
|
- ```:methods``` accepts a field name or array of field names to track in addition to the default fields
|
41
53
|
- ```:track_timestamps``` accepts a boolean, enabling or disabling tracking of ```created_at``` and ```updated_at```
|
42
54
|
- ```:track_primary_key``` accepts a boolean, enabling or disabling tracking of the model's primary key
|
55
|
+
- ```:track_locking_column``` accepts a boolean, enabling or disabling tracking of the model's locking column
|
56
|
+
|
43
57
|
|
44
58
|
### Attribution
|
45
59
|
Changes can be attributed to a particular source. The source is saved as a string
|
@@ -29,12 +29,14 @@ module TrackChanges
|
|
29
29
|
|
30
30
|
if record = diff.record
|
31
31
|
field_name = diff.record.class.human_attribute_name(field)
|
32
|
-
reflection = diff.record.class.
|
32
|
+
reflection = diff.record.class.reflect_on_association(field) # Look up association by name, e.g. users for has_many :users
|
33
|
+
reflection ||= diff.record.class.reflections.values.detect {|ref| ref.foreign_key == field.to_s } # Detect association by foreign key, e.g. user_id for belongs_to :user
|
33
34
|
end
|
34
35
|
|
35
36
|
if reflection
|
36
|
-
|
37
|
-
|
37
|
+
primary_key = reflection.options.fetch(:primary_key, :id)
|
38
|
+
from = reflection.klass.where(primary_key => from) || content_tag(:span, 'DELETED', :class => 'deleted', :title => "This #{field_name} has been deleted") if from.present?
|
39
|
+
to = reflection.klass.where(primary_key => to) || content_tag(:span, 'DELETED', :class => 'deleted', :title => "This #{field_name} has been deleted") if to.present?
|
38
40
|
end
|
39
41
|
|
40
42
|
if from.blank?
|
@@ -48,8 +50,8 @@ module TrackChanges
|
|
48
50
|
|
49
51
|
def link_diff_field_value(value, link_models = [])
|
50
52
|
case value
|
51
|
-
when Array
|
52
|
-
value.collect{|v| link_diff_field_value(v, link_models) }
|
53
|
+
when Array, ActiveRecord::Relation
|
54
|
+
safe_join(value.collect {|v| link_diff_field_value(v, link_models) }, ', ')
|
53
55
|
when *link_models
|
54
56
|
link_to(value.to_s, value)
|
55
57
|
else
|
@@ -4,8 +4,10 @@ module TrackChanges
|
|
4
4
|
|
5
5
|
belongs_to :record, :polymorphic => true
|
6
6
|
|
7
|
-
serialize
|
8
|
-
|
7
|
+
if Configuration.serialize
|
8
|
+
serialize :from, Hash
|
9
|
+
serialize :to, Hash
|
10
|
+
end
|
9
11
|
|
10
12
|
# Returns changes but only those where the string representation of the value has changed
|
11
13
|
def visible_changes
|
@@ -4,13 +4,26 @@ module TrackChanges
|
|
4
4
|
|
5
5
|
belongs_to :record, :polymorphic => true
|
6
6
|
|
7
|
-
serialize :state, Hash
|
7
|
+
serialize :state, Hash if Configuration.serialize
|
8
8
|
|
9
9
|
before_save :capture_record_state
|
10
10
|
|
11
11
|
# Returns a hash of the current values for all tracked fields on the record
|
12
12
|
def self.record_state(record)
|
13
|
-
Hash[record.class.track_changes_fields.collect {|method_name| [method_name, record.send(method_name)]
|
13
|
+
Hash[record.class.track_changes_fields.collect {|method_name| [method_name, dump_attribute_value(record.send(method_name))] }]
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.dump_attribute_value(value)
|
17
|
+
case value
|
18
|
+
when ActiveRecord::Relation
|
19
|
+
dump_attribute_value(value.to_a).sort
|
20
|
+
when Array
|
21
|
+
value.map {|member| dump_attribute_value(member) }
|
22
|
+
when ActiveRecord::Base
|
23
|
+
value.send(value.class.primary_key)
|
24
|
+
else
|
25
|
+
value
|
26
|
+
end
|
14
27
|
end
|
15
28
|
|
16
29
|
# Creates a diff object that shows the changes between this snapshot and the record's state
|
@@ -21,7 +34,7 @@ module TrackChanges
|
|
21
34
|
to = {}
|
22
35
|
|
23
36
|
record.class.track_changes_fields.each do |key|
|
24
|
-
if snapshot_state.key?(key) && snapshot_state[key] != record_state[key]
|
37
|
+
if snapshot_state.key?(key) && snapshot_state[key] != record_state[key] # We check for the existence of the key in the snapshot before recording so we don't declare newly tracked attributes as "changed" on their first time recorded
|
25
38
|
from[key] = snapshot_state[key]
|
26
39
|
to[key] = record_state[key]
|
27
40
|
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
module TrackChanges
|
2
|
+
module Configuration
|
3
|
+
mattr_accessor :cascade_destroy, :default => true # Destroy tracked changes when record is destroyed?
|
4
|
+
mattr_accessor :serialize, :default => true # Serialize data in ruby before writing to the database. Not necessary if the diff and snapshot data columns are JSON.
|
5
|
+
end
|
6
|
+
end
|
data/lib/track_changes/engine.rb
CHANGED
data/lib/track_changes/model.rb
CHANGED
@@ -11,8 +11,10 @@ module TrackChanges
|
|
11
11
|
attr_accessor :track_changes # Faux attribute to allow disabling of change tracking on this record
|
12
12
|
attr_writer :track_changes_by # Faux attribute to store who made the changes so we can save it in the diff
|
13
13
|
|
14
|
-
|
15
|
-
|
14
|
+
# A representation of this record as it was last saved
|
15
|
+
has_one :snapshot, :as => :record, :class_name => 'TrackChanges::Snapshot', :dependent => Configuration.cascade_destroy ? :destroy : nil
|
16
|
+
# A representation of changes made between saves through this record's lifetime
|
17
|
+
has_many :diffs, lambda { reorder('id DESC') }, :as => :record, :class_name => 'TrackChanges::Diff', :dependent => Configuration.cascade_destroy ? :delete_all : nil
|
16
18
|
|
17
19
|
after_save :persist_tracked_changes
|
18
20
|
end
|
@@ -21,11 +23,18 @@ module TrackChanges
|
|
21
23
|
module ClassMethods
|
22
24
|
# Returns the method names to call to fetch the fields tracked for changes
|
23
25
|
def track_changes_fields
|
24
|
-
fields = Array(track_changes_options[:only]).collect(&:to_s).presence ||
|
26
|
+
fields = Array(track_changes_options[:only]).collect(&:to_s).presence || default_track_changes_fields
|
25
27
|
fields -= Array(track_changes_options[:except]).collect(&:to_s)
|
26
28
|
fields += Array(track_changes_options[:methods]).collect(&:to_s)
|
27
29
|
fields -= ['created_at', 'updated_at'] unless track_changes_options[:track_timestamps]
|
28
30
|
fields -= [primary_key] unless track_changes_options[:track_primary_key]
|
31
|
+
fields -= [locking_column] unless track_changes_options[:track_locking_column]
|
32
|
+
|
33
|
+
return fields.uniq
|
34
|
+
end
|
35
|
+
|
36
|
+
def default_track_changes_fields
|
37
|
+
attribute_names - stored_attributes.keys.map(&:to_s) + stored_attributes.values.flatten.map(&:to_s)
|
29
38
|
end
|
30
39
|
|
31
40
|
# Create snapshots for all records so that the next changes made are captured
|
@@ -36,11 +45,18 @@ module TrackChanges
|
|
36
45
|
# Create new snapshots
|
37
46
|
where.not(:id => joins(:snapshot)).find_each(&:create_snapshot)
|
38
47
|
end
|
48
|
+
|
49
|
+
# Record a diff and update the snapshot for all records in the scope
|
50
|
+
# This can be used to record a diff after an `update_all`.
|
51
|
+
def persist_tracked_changes(track_changes_by: nil)
|
52
|
+
find_each do |record|
|
53
|
+
record.track_changes_by = track_changes_by
|
54
|
+
record.persist_tracked_changes
|
55
|
+
end
|
56
|
+
end
|
39
57
|
end
|
40
58
|
|
41
59
|
module InstanceMethods
|
42
|
-
private
|
43
|
-
|
44
60
|
def track_changes_by
|
45
61
|
@track_changes_by || TrackChanges.default_attribution
|
46
62
|
end
|
@@ -49,7 +65,7 @@ module TrackChanges
|
|
49
65
|
def persist_tracked_changes
|
50
66
|
return if track_changes == false
|
51
67
|
|
52
|
-
new_record =
|
68
|
+
new_record = was_new_record_before_save?
|
53
69
|
action = new_record ? 'create' : 'update'
|
54
70
|
changes_by = track_changes_by.is_a?(ActiveRecord::Base) ? track_changes_by.id : track_changes_by
|
55
71
|
|
@@ -64,6 +80,37 @@ module TrackChanges
|
|
64
80
|
snapshot.create_diff(:action => action, :changes_by => changes_by, :from => {})
|
65
81
|
end
|
66
82
|
end
|
83
|
+
|
84
|
+
def was_new_record_before_save?
|
85
|
+
if !respond_to?(:attribute_before_last_save) # Rails < 6
|
86
|
+
id_was.blank?
|
87
|
+
elsif saved_change_to_attribute?(:id) # Rails <=5
|
88
|
+
attribute_before_last_save(:id).blank?
|
89
|
+
else # Allow this method to be used outside of a transaction, e.g. after a bulk update
|
90
|
+
new_record?
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Filters tracked change diffs to only those where the given attributes changed *to* the specified values.
|
95
|
+
#
|
96
|
+
# It builds a scope that matches records where each given attribute in `to` has the specified value.
|
97
|
+
#
|
98
|
+
# Example:
|
99
|
+
# model.tracked_change(status: "approved", priority: "high")
|
100
|
+
# # => returns diffs where status became "approved" and priority became "high"
|
101
|
+
#
|
102
|
+
# Params:
|
103
|
+
# - changes (keyword arguments): One or more attribute-value pairs to match against the `to` column.
|
104
|
+
#
|
105
|
+
# Returns:
|
106
|
+
# - ActiveRecord::Relation: A filtered scope on the diffs table.
|
107
|
+
def tracked_change(**changes)
|
108
|
+
table_name = diffs.quoted_table_name
|
109
|
+
column_name = diffs.connection.quote_column_name(:to)
|
110
|
+
changes.reduce(diffs) do |scope, (attribute, value)|
|
111
|
+
scope.where("#{table_name}.#{column_name} ->> ? = ?", attribute, value)
|
112
|
+
end
|
113
|
+
end
|
67
114
|
end
|
68
115
|
end
|
69
116
|
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: culturecode-track_changes
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nicholas Jakobsen, Ryan Wallace
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: rails
|
@@ -56,6 +55,7 @@ files:
|
|
56
55
|
- lib/tasks/track_changes_tasks.rake
|
57
56
|
- lib/track_changes.rb
|
58
57
|
- lib/track_changes/attribution.rb
|
58
|
+
- lib/track_changes/configuration.rb
|
59
59
|
- lib/track_changes/controller.rb
|
60
60
|
- lib/track_changes/engine.rb
|
61
61
|
- lib/track_changes/model.rb
|
@@ -64,7 +64,6 @@ homepage: https://github.com/culturecode/track_changes
|
|
64
64
|
licenses:
|
65
65
|
- MIT
|
66
66
|
metadata: {}
|
67
|
-
post_install_message:
|
68
67
|
rdoc_options: []
|
69
68
|
require_paths:
|
70
69
|
- lib
|
@@ -79,9 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
79
78
|
- !ruby/object:Gem::Version
|
80
79
|
version: '0'
|
81
80
|
requirements: []
|
82
|
-
|
83
|
-
rubygems_version: 2.7.9
|
84
|
-
signing_key:
|
81
|
+
rubygems_version: 3.6.9
|
85
82
|
specification_version: 4
|
86
83
|
summary: Easily track changes to various ActiveRecord models
|
87
84
|
test_files: []
|