Skip to content

Commit e5bbf47

Browse files
committed
Fixes #5583: Eliminate redundant change records when adding/removing tags
1 parent 9bda2a4 commit e5bbf47

File tree

5 files changed

+96
-31
lines changed

5 files changed

+96
-31
lines changed

docs/release-notes/version-2.11.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
### Bug Fixes (from Beta)
1515

16+
* [#5583](https://github.com/netbox-community/netbox/issues/5583) - Eliminate redundant change records when adding/removing tags
1617
* [#6100](https://github.com/netbox-community/netbox/issues/6100) - Fix VM interfaces table "add interfaces" link
1718
* [#6104](https://github.com/netbox-community/netbox/issues/6104) - Fix location column on racks table
1819
* [#6105](https://github.com/netbox-community/netbox/issues/6105) - Hide checkboxes for VMs under cluster VMs view

netbox/extras/signals.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,35 @@ def _handle_changed_object(request, sender, instance, **kwargs):
2222
"""
2323
Fires when an object is created or updated.
2424
"""
25-
# Queue the object for processing once the request completes
25+
m2m_changed = False
26+
27+
# Determine the type of change being made
2628
if kwargs.get('created'):
2729
action = ObjectChangeActionChoices.ACTION_CREATE
2830
elif 'created' in kwargs:
2931
action = ObjectChangeActionChoices.ACTION_UPDATE
3032
elif kwargs.get('action') in ['post_add', 'post_remove'] and kwargs['pk_set']:
3133
# m2m_changed with objects added or removed
34+
m2m_changed = True
3235
action = ObjectChangeActionChoices.ACTION_UPDATE
3336
else:
3437
return
3538

3639
# Record an ObjectChange if applicable
3740
if hasattr(instance, 'to_objectchange'):
38-
objectchange = instance.to_objectchange(action)
39-
objectchange.user = request.user
40-
objectchange.request_id = request.id
41-
objectchange.save()
41+
if m2m_changed:
42+
ObjectChange.objects.filter(
43+
changed_object_type=ContentType.objects.get_for_model(instance),
44+
changed_object_id=instance.pk,
45+
request_id=request.id
46+
).update(
47+
postchange_data=instance.to_objectchange(action).postchange_data
48+
)
49+
else:
50+
objectchange = instance.to_objectchange(action)
51+
objectchange.user = request.user
52+
objectchange.request_id = request.id
53+
objectchange.save()
4254

4355
# Enqueue webhooks
4456
enqueue_webhooks(instance, request.user, request.id, action)

netbox/extras/tests/test_changelog.py

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -56,19 +56,18 @@ def test_create_object(self):
5656
response = self.client.post(**request)
5757
self.assertHttpStatus(response, 302)
5858

59+
# Verify the creation of a new ObjectChange record
5960
site = Site.objects.get(name='Site 1')
60-
# First OC is the creation; second is the tags update
61-
oc_list = ObjectChange.objects.filter(
61+
oc = ObjectChange.objects.get(
6262
changed_object_type=ContentType.objects.get_for_model(Site),
6363
changed_object_id=site.pk
64-
).order_by('pk')
65-
self.assertEqual(oc_list[0].changed_object, site)
66-
self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
67-
self.assertEqual(oc_list[0].prechange_data, None)
68-
self.assertEqual(oc_list[0].postchange_data['custom_fields']['my_field'], form_data['cf_my_field'])
69-
self.assertEqual(oc_list[0].postchange_data['custom_fields']['my_field_select'], form_data['cf_my_field_select'])
70-
self.assertEqual(oc_list[1].action, ObjectChangeActionChoices.ACTION_UPDATE)
71-
self.assertEqual(oc_list[1].postchange_data['tags'], ['Tag 1', 'Tag 2'])
64+
)
65+
self.assertEqual(oc.changed_object, site)
66+
self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_CREATE)
67+
self.assertEqual(oc.prechange_data, None)
68+
self.assertEqual(oc.postchange_data['custom_fields']['my_field'], form_data['cf_my_field'])
69+
self.assertEqual(oc.postchange_data['custom_fields']['my_field_select'], form_data['cf_my_field_select'])
70+
self.assertEqual(oc.postchange_data['tags'], ['Tag 1', 'Tag 2'])
7271

7372
def test_update_object(self):
7473
site = Site(name='Site 1', slug='site-1')
@@ -93,8 +92,8 @@ def test_update_object(self):
9392
response = self.client.post(**request)
9493
self.assertHttpStatus(response, 302)
9594

95+
# Verify the creation of a new ObjectChange record
9696
site.refresh_from_db()
97-
# Get only the most recent OC
9897
oc = ObjectChange.objects.filter(
9998
changed_object_type=ContentType.objects.get_for_model(Site),
10099
changed_object_id=site.pk
@@ -259,17 +258,15 @@ def test_create_object(self):
259258
self.assertHttpStatus(response, status.HTTP_201_CREATED)
260259

261260
site = Site.objects.get(pk=response.data['id'])
262-
# First OC is the creation; second is the tags update
263-
oc_list = ObjectChange.objects.filter(
261+
oc = ObjectChange.objects.get(
264262
changed_object_type=ContentType.objects.get_for_model(Site),
265263
changed_object_id=site.pk
266-
).order_by('pk')
267-
self.assertEqual(oc_list[0].changed_object, site)
268-
self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
269-
self.assertEqual(oc_list[0].prechange_data, None)
270-
self.assertEqual(oc_list[0].postchange_data['custom_fields'], data['custom_fields'])
271-
self.assertEqual(oc_list[1].action, ObjectChangeActionChoices.ACTION_UPDATE)
272-
self.assertEqual(oc_list[1].postchange_data['tags'], ['Tag 1', 'Tag 2'])
264+
)
265+
self.assertEqual(oc.changed_object, site)
266+
self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_CREATE)
267+
self.assertEqual(oc.prechange_data, None)
268+
self.assertEqual(oc.postchange_data['custom_fields'], data['custom_fields'])
269+
self.assertEqual(oc.postchange_data['tags'], ['Tag 1', 'Tag 2'])
273270

274271
def test_update_object(self):
275272
site = Site(name='Site 1', slug='site-1')
@@ -294,11 +291,10 @@ def test_update_object(self):
294291
self.assertHttpStatus(response, status.HTTP_200_OK)
295292

296293
site = Site.objects.get(pk=response.data['id'])
297-
# Get only the most recent OC
298-
oc = ObjectChange.objects.filter(
294+
oc = ObjectChange.objects.get(
299295
changed_object_type=ContentType.objects.get_for_model(Site),
300296
changed_object_id=site.pk
301-
).first()
297+
)
302298
self.assertEqual(oc.changed_object, site)
303299
self.assertEqual(oc.action, ObjectChangeActionChoices.ACTION_UPDATE)
304300
self.assertEqual(oc.postchange_data['custom_fields'], data['custom_fields'])

netbox/utilities/testing/api.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from rest_framework import status
77
from rest_framework.test import APIClient
88

9+
from extras.choices import ObjectChangeActionChoices
10+
from extras.models import ObjectChange
911
from users.models import ObjectPermission, Token
1012
from .utils import disable_warnings
1113
from .views import ModelTestCase
@@ -223,13 +225,23 @@ def test_create_object(self):
223225
response = self.client.post(self._get_list_url(), self.create_data[0], format='json', **self.header)
224226
self.assertHttpStatus(response, status.HTTP_201_CREATED)
225227
self.assertEqual(self._get_queryset().count(), initial_count + 1)
228+
instance = self._get_queryset().get(pk=response.data['id'])
226229
self.assertInstanceEqual(
227-
self._get_queryset().get(pk=response.data['id']),
230+
instance,
228231
self.create_data[0],
229232
exclude=self.validation_excluded_fields,
230233
api=True
231234
)
232235

236+
# Verify ObjectChange creation
237+
if hasattr(self.model, 'to_objectchange'):
238+
objectchanges = ObjectChange.objects.filter(
239+
changed_object_type=ContentType.objects.get_for_model(instance),
240+
changed_object_id=instance.pk
241+
)
242+
self.assertEqual(len(objectchanges), 1)
243+
self.assertEqual(objectchanges[0].action, ObjectChangeActionChoices.ACTION_CREATE)
244+
233245
def test_bulk_create_objects(self):
234246
"""
235247
POST a set of objects in a single request.
@@ -304,6 +316,15 @@ def test_update_object(self):
304316
api=True
305317
)
306318

319+
# Verify ObjectChange creation
320+
if hasattr(self.model, 'to_objectchange'):
321+
objectchanges = ObjectChange.objects.filter(
322+
changed_object_type=ContentType.objects.get_for_model(instance),
323+
changed_object_id=instance.pk
324+
)
325+
self.assertEqual(len(objectchanges), 1)
326+
self.assertEqual(objectchanges[0].action, ObjectChangeActionChoices.ACTION_UPDATE)
327+
307328
def test_bulk_update_objects(self):
308329
"""
309330
PATCH a set of objects in a single request.
@@ -367,6 +388,15 @@ def test_delete_object(self):
367388
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
368389
self.assertFalse(self._get_queryset().filter(pk=instance.pk).exists())
369390

391+
# Verify ObjectChange creation
392+
if hasattr(self.model, 'to_objectchange'):
393+
objectchanges = ObjectChange.objects.filter(
394+
changed_object_type=ContentType.objects.get_for_model(instance),
395+
changed_object_id=instance.pk
396+
)
397+
self.assertEqual(len(objectchanges), 1)
398+
self.assertEqual(objectchanges[0].action, ObjectChangeActionChoices.ACTION_DELETE)
399+
370400
def test_bulk_delete_objects(self):
371401
"""
372402
DELETE a set of objects in a single request.

netbox/utilities/testing/views.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
from netaddr import IPNetwork
1111
from taggit.managers import TaggableManager
1212

13-
from extras.models import Tag
13+
from extras.choices import ObjectChangeActionChoices
14+
from extras.models import ObjectChange, Tag
1415
from users.models import ObjectPermission
1516
from utilities.permissions import resolve_permission_ct
1617
from .utils import disable_warnings, extract_form_failures, post_data
@@ -323,7 +324,16 @@ def test_create_object_with_permission(self):
323324
}
324325
self.assertHttpStatus(self.client.post(**request), 302)
325326
self.assertEqual(initial_count + 1, self._get_queryset().count())
326-
self.assertInstanceEqual(self._get_queryset().order_by('pk').last(), self.form_data)
327+
instance = self._get_queryset().order_by('pk').last()
328+
self.assertInstanceEqual(instance, self.form_data)
329+
330+
# Verify ObjectChange creation
331+
objectchanges = ObjectChange.objects.filter(
332+
changed_object_type=ContentType.objects.get_for_model(instance),
333+
changed_object_id=instance.pk
334+
)
335+
self.assertEqual(len(objectchanges), 1)
336+
self.assertEqual(objectchanges[0].action, ObjectChangeActionChoices.ACTION_CREATE)
327337

328338
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
329339
def test_create_object_with_constrained_permission(self):
@@ -410,6 +420,14 @@ def test_edit_object_with_permission(self):
410420
self.assertHttpStatus(self.client.post(**request), 302)
411421
self.assertInstanceEqual(self._get_queryset().get(pk=instance.pk), self.form_data)
412422

423+
# Verify ObjectChange creation
424+
objectchanges = ObjectChange.objects.filter(
425+
changed_object_type=ContentType.objects.get_for_model(instance),
426+
changed_object_id=instance.pk
427+
)
428+
self.assertEqual(len(objectchanges), 1)
429+
self.assertEqual(objectchanges[0].action, ObjectChangeActionChoices.ACTION_UPDATE)
430+
413431
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
414432
def test_edit_object_with_constrained_permission(self):
415433
instance1, instance2 = self._get_queryset().all()[:2]
@@ -489,6 +507,14 @@ def test_delete_object_with_permission(self):
489507
with self.assertRaises(ObjectDoesNotExist):
490508
self._get_queryset().get(pk=instance.pk)
491509

510+
# Verify ObjectChange creation
511+
objectchanges = ObjectChange.objects.filter(
512+
changed_object_type=ContentType.objects.get_for_model(instance),
513+
changed_object_id=instance.pk
514+
)
515+
self.assertEqual(len(objectchanges), 1)
516+
self.assertEqual(objectchanges[0].action, ObjectChangeActionChoices.ACTION_DELETE)
517+
492518
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
493519
def test_delete_object_with_constrained_permission(self):
494520
instance1, instance2 = self._get_queryset().all()[:2]

0 commit comments

Comments
 (0)