Repository: netbox-community/netbox

Issue:
Provide a diff showing the changes between two logged changes
### Environment
NetBox version:  2.4.6

### Proposed Functionality
As a netbox user I would like to have the ability to compare the changes in object data between logged changes. This feature would be available within the changelog context of an object in the UI and have the ability to select two or more log entries and compare them against each other.

### Use Case
This feature would be beneficial to users that want to quickly identify changes that have been made to an object rather than pulling the full object data and using it in an external tool to do a comparison.

As an example I created a screenshot that shows the differences between two versions of a device object. In this instance updating a serial number after it has been swapped out because it hypothetically had some sort of unrecoverable issue.

![image](https://user-images.githubusercontent.com/10358044/46728971-59d9de80-cc84-11e8-8003-1ac587cf2a01.png)



Discussion:
@jeremystretch Thanks for adding this issue to the ever-growing backlog. I work for a small serviceprovider and we're in the process of evaluating Netbox. So I might raise some more issues in the near future but I'll try to keep it to a minimum.

My programming skills are near non-existent but you can expect my full commitment to test features as they are released. Thanks in advance and a big thumbs up for the awesome work you've done creating this amazing tool. :thumbsup:
Prior to the changelog functionality, I implemented something similar using [dictdiffer](https://dictdiffer.readthedocs.io/en/latest/). Works great, and has been quite useful.
In addition to that, maybe it would be possible to only create a changelog-entry (or save something at all) if settings changed. We had the case where a script updated various entries using the PATCH-method using the API and writing values to existing entries. But it didn't first download the current values and check if there was a difference. Maybe Netbox could detect that the diff was actually empty (all values already current) and then simply not creating a diff / an entry in the changelog.
Imho that works together with diffing old/new values, in a way.
Would like to see this functionality in the changelog UI as well.  diff2html is a pretty popular npm module to accomplish the UI - https://github.com/rtfpessoa/diff2html | https://diff2html.xyz/

This might fit in easily with the recent changes from https://github.com/netbox-community/netbox/pull/3632

> we now make a copy of the object being deleted and defer processing to until after the request has been completed

Resolving patch:
diff --git a/netbox/extras/views.py b/netbox/extras/views.py
--- a/netbox/extras/views.py
+++ b/netbox/extras/views.py
@@ -12,6 +12,7 @@
 
 from utilities.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator
+from utilities.utils import shallow_compare_dict
 from utilities.views import BulkDeleteView, BulkEditView, ObjectDeleteView, ObjectEditView, ObjectListView
 from . import filters, forms
 from .models import ConfigContext, ImageAttachment, ObjectChange, ReportResult, Tag, TaggedItem
@@ -207,8 +208,31 @@ def get(self, request, pk):
             orderable=False
         )
 
+        objectchanges = ObjectChange.objects.filter(
+            changed_object_type=objectchange.changed_object_type,
+            changed_object_id=objectchange.changed_object_id,
+        )
+
+        next_change = objectchanges.filter(time__gt=objectchange.time).order_by('time').first()
+        prev_change = objectchanges.filter(time__lt=objectchange.time).order_by('-time').first()
+
+        if prev_change:
+            diff_added = shallow_compare_dict(
+                prev_change.object_data,
+                objectchange.object_data,
+                exclude=['last_updated'],
+            )
+            diff_removed = {x: prev_change.object_data.get(x) for x in diff_added}
+        else:
+            # No previous change; this is the initial change that added the object
+            diff_added = diff_removed = objectchange.object_data
+
         return render(request, 'extras/objectchange.html', {
             'objectchange': objectchange,
+            'diff_added': diff_added,
+            'diff_removed': diff_removed,
+            'next_change': next_change,
+            'prev_change': prev_change,
             'related_changes_table': related_changes_table,
             'related_changes_count': related_changes.count()
         })
diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py
--- a/netbox/utilities/utils.py
+++ b/netbox/utilities/utils.py
@@ -222,3 +222,19 @@ def querydict_to_dict(querydict):
         key: querydict.get(key) if len(value) == 1 and key != 'pk' else querydict.getlist(key)
         for key, value in querydict.lists()
     }
+
+
+def shallow_compare_dict(source_dict, destination_dict, exclude=None):
+    """
+    Return a new dictionary of the different keys. The values of `destination_dict` are returned. Only the equality of
+    the first layer of keys/values is checked. `exclude` is a list or tuple of keys to be ignored.
+    """
+    difference = {}
+
+    for key in destination_dict:
+        if source_dict.get(key) != destination_dict[key]:
+            if isinstance(exclude, (list, tuple)) and key in exclude:
+                continue
+            difference[key] = destination_dict[key]
+
+    return difference


Test patch: