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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bfa353667777cba682df7ab603e3b4b21054439c6f6d91e4c0a0a719a244de33
4
- data.tar.gz: 1155b70851fddaa85d6c10e7c0e9541649117ed8a9cf6dd38550788559269093
3
+ metadata.gz: 0c9bb5bdbc7a137501b16be9525858c58cd8665a673f0d81616e7ea08bda7c5b
4
+ data.tar.gz: 6b1b61340da3ca8e3af00f43cb8b4ed8fb634a03a5111301e9d30501258bccad
5
5
  SHA512:
6
- metadata.gz: 0e9f8c86da3f47175fd32bdefc8b070f2526dea41fcc279f5a4e4392dd464eec3d4ca68b2f2b9fd9d785c87c47e110d50463f382875cfd360e9ee3a222c603e7
7
- data.tar.gz: 2cb9c5348ac4a7d6f24c1c3bbd0f1eed42098d5d361009d8ec2d5a820863dba06ea2f2333db2cb61a015f7c90d593cf821dae14cd477ac03eaf6ffcbc926153f
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.reflections.values.detect {|reflection| reflection.foreign_key == field.to_s }
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
- from = reflection.klass.find_by_id(from) || content_tag(:span, 'DELETED', :class => 'deleted', :title => "This #{field_name} has been deleted") if from.present?
37
- to = reflection.klass.find_by_id(to) || content_tag(:span, 'DELETED', :class => 'deleted', :title => "This #{field_name} has been deleted") if to.present?
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) }.to_sentence.html_safe
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 :from, Hash
8
- serialize :to, Hash
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
@@ -1,3 +1,4 @@
1
+ require 'track_changes/configuration'
1
2
  require 'track_changes/model'
2
3
  require 'track_changes/controller'
3
4
  require 'track_changes/attribution'
@@ -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
- has_one :snapshot, :as => :record, :class_name => 'TrackChanges::Snapshot' # A representation of this record as it was last saved
15
- has_many :diffs, lambda { reorder('id DESC') }, :as => :record, :class_name => 'TrackChanges::Diff' # A representation of changes made between saves through this record's lifetime
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 || self.attribute_names
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 = id_was.blank?
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
@@ -1,3 +1,3 @@
1
1
  module TrackChanges
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  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.2.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: 2020-05-20 00:00:00.000000000 Z
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
- rubyforge_project:
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: []