From 6e4c4c43428afedb85f08a2409077efddfa47375 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 16 Mar 2023 16:29:43 -0400 Subject: [PATCH] Closes #11494: Enable filtering objects by create/update request IDs --- docs/release-notes/version-3.5.md | 1 + .../0090_objectchange_index_request_id.py | 18 +++++ netbox/extras/models/change_logging.py | 3 +- netbox/extras/tests/test_filtersets.py | 69 +++++++++++++++++++ netbox/netbox/filtersets.py | 24 ++++++- 5 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 netbox/extras/migrations/0090_objectchange_index_request_id.py diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index 792a244e4..7a7335e4b 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -37,6 +37,7 @@ A new ASN range model has been introduced to facilitate the provisioning of new * [#10729](https://github.com/netbox-community/netbox/issues/10729) - Add date & time custom field type * [#11254](https://github.com/netbox-community/netbox/issues/11254) - Introduce the `X-Request-ID` HTTP header to annotate the unique ID of each request for change logging * [#11440](https://github.com/netbox-community/netbox/issues/11440) - Add an `enabled` field for device type interfaces +* [#11494](https://github.com/netbox-community/netbox/issues/11494) - Enable filtering objects by create/update request IDs * [#11517](https://github.com/netbox-community/netbox/issues/11517) - Standardize the inclusion of related objects across the entire UI * [#11584](https://github.com/netbox-community/netbox/issues/11584) - Add a list view for contact assignments * [#11625](https://github.com/netbox-community/netbox/issues/11625) - Add HTMX support to ObjectEditView diff --git a/netbox/extras/migrations/0090_objectchange_index_request_id.py b/netbox/extras/migrations/0090_objectchange_index_request_id.py new file mode 100644 index 000000000..00e8fde42 --- /dev/null +++ b/netbox/extras/migrations/0090_objectchange_index_request_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.7 on 2023-03-16 20:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0089_customfield_is_cloneable'), + ] + + operations = [ + migrations.AlterField( + model_name='objectchange', + name='request_id', + field=models.UUIDField(db_index=True, editable=False), + ), + ] diff --git a/netbox/extras/models/change_logging.py b/netbox/extras/models/change_logging.py index 2c91d97a4..9660930c2 100644 --- a/netbox/extras/models/change_logging.py +++ b/netbox/extras/models/change_logging.py @@ -31,7 +31,8 @@ class ObjectChange(models.Model): editable=False ) request_id = models.UUIDField( - editable=False + editable=False, + db_index=True ) action = models.CharField( max_length=50, diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index f95cf08de..24535152b 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -6,6 +6,7 @@ from django.contrib.contenttypes.models import ContentType from django.test import TestCase from circuits.models import Provider +from dcim.filtersets import SiteFilterSet from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup from dcim.models import Location from extras.choices import * @@ -924,3 +925,71 @@ class ObjectChangeTestCase(TestCase, BaseFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) params = {'changed_object_type_id': [ContentType.objects.get(app_label='dcim', model='site').pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) + + +class ChangeLoggedFilterSetTestCase(TestCase): + """ + Evaluate base ChangeLoggedFilterSet filters using the Site model. + """ + queryset = Site.objects.all() + filterset = SiteFilterSet + + @classmethod + def setUpTestData(cls): + content_type = ContentType.objects.get_for_model(Site) + + # Create three sites + sites = ( + Site(name='Site 1', slug='site-1'), + Site(name='Site 2', slug='site-2'), + Site(name='Site 3', slug='site-3'), + ) + Site.objects.bulk_create(sites) + + # Simulate *creation* changelog records for two of the sites + request_id = uuid.uuid4() + objectchanges = ( + ObjectChange( + changed_object_type=content_type, + changed_object_id=sites[0].pk, + action=ObjectChangeActionChoices.ACTION_CREATE, + request_id=request_id + ), + ObjectChange( + changed_object_type=content_type, + changed_object_id=sites[1].pk, + action=ObjectChangeActionChoices.ACTION_CREATE, + request_id=request_id + ), + ) + ObjectChange.objects.bulk_create(objectchanges) + + # Simulate *update* changelog records for two of the sites + request_id = uuid.uuid4() + objectchanges = ( + ObjectChange( + changed_object_type=content_type, + changed_object_id=sites[0].pk, + action=ObjectChangeActionChoices.ACTION_UPDATE, + request_id=request_id + ), + ObjectChange( + changed_object_type=content_type, + changed_object_id=sites[1].pk, + action=ObjectChangeActionChoices.ACTION_UPDATE, + request_id=request_id + ), + ) + ObjectChange.objects.bulk_create(objectchanges) + + def test_created_by_request(self): + request_id = ObjectChange.objects.filter(action=ObjectChangeActionChoices.ACTION_CREATE).first().request_id + params = {'created_by_request': request_id} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + self.assertEqual(self.queryset.count(), 3) + + def test_updated_by_request(self): + request_id = ObjectChange.objects.filter(action=ObjectChangeActionChoices.ACTION_UPDATE).first().request_id + params = {'updated_by_request': request_id} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + self.assertEqual(self.queryset.count(), 3) diff --git a/netbox/netbox/filtersets.py b/netbox/netbox/filtersets.py index ee0ab330c..3ac9174de 100644 --- a/netbox/netbox/filtersets.py +++ b/netbox/netbox/filtersets.py @@ -7,9 +7,9 @@ from django_filters.exceptions import FieldLookupError from django_filters.utils import get_model_field, resolve_field from django.utils.translation import gettext as _ -from extras.choices import CustomFieldFilterLogicChoices +from extras.choices import CustomFieldFilterLogicChoices, ObjectChangeActionChoices from extras.filters import TagFilter -from extras.models import CustomField, SavedFilter +from extras.models import CustomField, ObjectChange, SavedFilter from utilities.constants import ( FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP, FILTER_NUMERIC_BASED_LOOKUP_MAP @@ -231,6 +231,26 @@ class ChangeLoggedModelFilterSet(BaseFilterSet): """ created = filters.MultiValueDateTimeFilter() last_updated = filters.MultiValueDateTimeFilter() + created_by_request = django_filters.UUIDFilter( + method='filter_by_request' + ) + updated_by_request = django_filters.UUIDFilter( + method='filter_by_request' + ) + + def filter_by_request(self, queryset, name, value): + content_type = ContentType.objects.get_for_model(self.Meta.model) + action = { + 'created_by_request': ObjectChangeActionChoices.ACTION_CREATE, + 'updated_by_request': ObjectChangeActionChoices.ACTION_UPDATE, + }.get(name) + request_id = value + pks = ObjectChange.objects.filter( + changed_object_type=content_type, + action=action, + request_id=request_id + ).values_list('changed_object_id', flat=True) + return queryset.filter(pk__in=pks) class NetBoxModelFilterSet(ChangeLoggedModelFilterSet):