Skip to content

Commit

Permalink
Bug/187 track changes from embedded documents (NOT deeply nested) (#232)
Browse files Browse the repository at this point in the history
  • Loading branch information
Startouf authored and dblock committed Jun 11, 2019
1 parent 8ed7ba1 commit f2eb8e2
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 41 deletions.
47 changes: 24 additions & 23 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2018-08-12 23:34:44 -0700 using RuboCop version 0.48.1.
# on 2019-06-09 13:41:12 +0200 using RuboCop version 0.60.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.

# Offense count: 7
# Configuration parameters: Include.
# Include: **/Gemfile, **/gems.rb
# Include: **/*.gemfile, **/Gemfile, **/gems.rb
Bundler/DuplicatedGem:
Exclude:
- 'Gemfile'

# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyleAlignWith, SupportedStylesAlignWith, AutoCorrect.
# Configuration parameters: EnforcedStyleAlignWith, AutoCorrect, Severity.
# SupportedStylesAlignWith: keyword, variable, start_of_line
Lint/EndAlignment:
Layout/EndAlignment:
Exclude:
- 'lib/mongoid/history/options.rb'

Expand All @@ -32,12 +32,13 @@ Lint/ParenthesesAsGroupedExpression:
- 'spec/integration/integration_spec.rb'
- 'spec/integration/nested_embedded_polymorphic_documents_spec.rb'

# Offense count: 21
# Offense count: 22
Metrics/AbcSize:
Max: 52

# Offense count: 118
# Offense count: 122
# Configuration parameters: CountComments, ExcludedMethods.
# ExcludedMethods: refine
Metrics/BlockLength:
Max: 837

Expand All @@ -50,26 +51,28 @@ Metrics/ClassLength:
Metrics/CyclomaticComplexity:
Max: 13

# Offense count: 392
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Metrics/LineLength:
Max: 688

# Offense count: 16
# Configuration parameters: CountComments.
# Offense count: 17
# Configuration parameters: CountComments, ExcludedMethods.
Metrics/MethodLength:
Max: 23

# Offense count: 2
# Configuration parameters: CountComments.
Metrics/ModuleLength:
Max: 182
Max: 191

# Offense count: 6
Metrics/PerceivedComplexity:
Max: 15

# Offense count: 1
# Configuration parameters: ExpectMatchingDefinition, Regex, IgnoreExecutableScripts, AllowedAcronyms.
# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS
Naming/FileName:
Exclude:
- 'Dangerfile'
- 'lib/mongoid-history.rb'

# Offense count: 12
Style/Documentation:
Exclude:
Expand All @@ -91,15 +94,13 @@ Style/EachWithObject:
- 'lib/mongoid/history/trackable.rb'
- 'lib/mongoid/history/tracker.rb'

# Offense count: 2
# Configuration parameters: ExpectMatchingDefinition, Regex, IgnoreExecutableScripts, AllowedAcronyms.
# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS
Style/FileName:
Exclude:
- 'Dangerfile'
- 'lib/mongoid-history.rb'

# Offense count: 1
Style/MultilineBlockChain:
Exclude:
- 'lib/mongoid/history/tracker.rb'

# Offense count: 404
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Metrics/LineLength:
Max: 688
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
### 0.8.2 (Next)

* [#232](https://github.com/mongoid/mongoid-history/pull/232): Bug/187 track changes from embedded documents (not deeply nested) - [@Startouf](https://github.com/Startouf).
* [#227](https://github.com/mongoid/mongoid-history/pull/227): Store options in inheritable class attributes - [@jnfeinstein](https://github.com/jnfeinstein).
* [#229](https://github.com/mongoid/mongoid-history/pull/229), [#225](https://github.com/mongoid/mongoid-history/pull/225): Fixed inheritance of `history_trackable_options` - [@jnfeinstein](https://github.com/jnfeinstein).
* Your contribution here.
Expand Down
85 changes: 72 additions & 13 deletions lib/mongoid/history/attributes/update.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,91 @@ module Mongoid
module History
module Attributes
class Update < ::Mongoid::History::Attributes::Base
# @example when both an attribute `foo` and a child's attribute `nested_bar.baz` are changed
#
# {
# 'foo' => ['foo_before_changes', 'foo_after_changes']
# 'nested_bar.baz' => ['nested_bar_baz_before_changes', 'nested_bar_baz_after_changes']
# }
# }
#
# @return [Hash<String, Array<(?,?)>>] Hash of changes
def attributes
@attributes = {}
changes_from_parent.deep_merge(changes_from_children)
end

private

def changes_from_parent
parent_changes = {}
changes.each do |k, v|
if trackable_class.tracked_embeds_one?(k)
insert_embeds_one_changes(k, v)
elsif trackable_class.tracked_embeds_many?(k)
insert_embeds_many_changes(k, v)
elsif trackable_class.tracked?(k, :update)
@attributes[k] = format_field(k, v) unless v.all?(&:blank?)
change_value = begin
if trackable_class.tracked_embeds_one?(k)
embeds_one_changes_from_parent(k, v)
elsif trackable_class.tracked_embeds_many?(k)
embeds_many_changes_from_parent(k, v)
elsif trackable_class.tracked?(k, :update)
{ k => format_field(k, v) } unless v.all?(&:blank?)
end
end
parent_changes.merge!(change_value) if change_value.present?
end
@attributes
parent_changes
end

private
def changes_from_children
embeds_one_changes_from_embedded_documents
end

# Retrieve the list of changes applied directly to the nested documents
#
# @example when a child's name is changed from "todd" to "mario"
#
# child = Child.new(name: 'todd')
# Parent.create(child: child)
# child.name = "Mario"
#
# embeds_one_changes_from_embedded_documents # when called from "Parent"
# # => { "child.name"=>["todd", "mario"] }
#
# @return [Hash<String, Array<(?,?)>] changes of embeds_ones from embedded documents
def embeds_one_changes_from_embedded_documents
embedded_doc_changes = {}
trackable_class.tracked_embeds_one.each do |rel|
rel_class = trackable_class.relation_class_of(rel)
paranoia_field = Mongoid::History.trackable_class_settings(rel_class)[:paranoia_field]
paranoia_field = rel_class.aliased_fields.key(paranoia_field) || paranoia_field
rel = aliased_fields.key(rel) || rel
obj = trackable.send(rel)
next if !obj || (obj.respond_to?(paranoia_field) && obj.public_send(paranoia_field).present?)

def insert_embeds_one_changes(relation, value)
obj.changes.each do |k, v|
embedded_doc_changes["#{rel}.#{k}"] = [v.first, v.last]
end
end
embedded_doc_changes
end

# @param [String] relation
# @param [String] value
#
# @return [Hash<String, Array<(?,?)>>]
def embeds_one_changes_from_parent(relation, value)
relation = trackable_class.database_field_name(relation)
relation_class = trackable_class.relation_class_of(relation)
paranoia_field = Mongoid::History.trackable_class_settings(relation_class)[:paranoia_field]
original_value = value[0][paranoia_field].present? ? {} : format_embeds_one_relation(relation, value[0])
modified_value = value[1][paranoia_field].present? ? {} : format_embeds_one_relation(relation, value[1])
return if original_value == modified_value
@attributes[relation] = [original_value, modified_value]

{ relation => [original_value, modified_value] }
end

def insert_embeds_many_changes(relation, value)
# @param [String] relation
# @param [String] value
#
# @return [Hash<Array<(?,?)>>]
def embeds_many_changes_from_parent(relation, value)
relation = trackable_class.database_field_name(relation)
relation_class = trackable_class.relation_class_of(relation)
paranoia_field = Mongoid::History.trackable_class_settings(relation_class)[:paranoia_field]
Expand All @@ -37,7 +95,8 @@ def insert_embeds_many_changes(relation, value)
modified_value = value[1].reject { |rel| rel[paranoia_field].present? }
.map { |v_attrs| format_embeds_many_relation(relation, v_attrs) }
return if original_value == modified_value
@attributes[relation] = [original_value, modified_value]

{ relation => [original_value, modified_value] }
end
end
end
Expand Down
38 changes: 33 additions & 5 deletions lib/mongoid/history/trackable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -263,18 +263,43 @@ def clear_trackable_memoization
@history_tracks = nil
end

# Transform hash of pair of changes into an `original` and `modified` hash
# Nested document keys (key name with dots) are expanded
#
# @param [Hash<Array>] changes
#
# @return [Array<Hash<?>,Hash<?>>] <description>
def transform_changes(changes)
original = {}
modified = {}
changes.each_pair do |k, v|
o, m = v
original[k] = o unless o.nil?
modified[k] = m unless m.nil?
changes.each_pair do |k, modification_pair|
o, m = modification_pair
original.deep_merge!(expand_nested_document_key_value(k, o)) unless o.nil?
modified.deep_merge!(expand_nested_document_key_value(k, m)) unless m.nil?
end

[original, modified]
end

# Handle nested document tracking of changes
#
# @example
#
# expand_nested_document_key('embedded.document.changed_field', 'old'])
# #=> { 'embedded' => {'document' => { 'changed_field' => 'old' }}}
#
# @param [String] document_key key with dots
# @param [?] value
#
# @return [Hash<String, ?>]
def expand_nested_document_key_value(document_key, value)
expanded_key = value
document_key.to_s.split('.').reverse.each do |key|
expanded_key = { key => expanded_key }
end
expanded_key
end

def increment_current_version
current_version = (send(history_trackable_options[:version_field]) || 0) + 1
send("#{history_trackable_options[:version_field]}=", current_version)
Expand All @@ -290,7 +315,10 @@ def track_history_for_action?(action)
def track_history_for_action(action)
if track_history_for_action?(action)
current_version = increment_current_version
last_track = self.class.tracker_class.create!(history_tracker_attributes(action.to_sym).merge(version: current_version, action: action.to_s, trackable: self))
last_track = self.class.tracker_class.create!(
history_tracker_attributes(action.to_sym)
.merge(version: current_version, action: action.to_s, trackable: self)
)
end

clear_trackable_memoization
Expand Down
Loading

0 comments on commit f2eb8e2

Please sign in to comment.