diff --git a/docs/release-notes/version-2.9.md b/docs/release-notes/version-2.9.md index fb1db7ad5..1f54713f9 100644 --- a/docs/release-notes/version-2.9.md +++ b/docs/release-notes/version-2.9.md @@ -11,6 +11,7 @@ * [#5383](https://github.com/netbox-community/netbox/issues/5383) - Fix setting user password via REST API * [#5396](https://github.com/netbox-community/netbox/issues/5396) - Fix uniqueness constraint for virtual machine names +* [#5387](https://github.com/netbox-community/netbox/issues/5387) - Fix error when rendering config contexts when objects have multiple tags assigned * [#5407](https://github.com/netbox-community/netbox/issues/5407) - Add direct link to secret on secrets list * [#5408](https://github.com/netbox-community/netbox/issues/5408) - Fix updating secrets without setting new plaintext * [#5410](https://github.com/netbox-community/netbox/issues/5410) - Restore tags field on cable connection forms diff --git a/netbox/extras/querysets.py b/netbox/extras/querysets.py index 92402c45f..f75b01cc9 100644 --- a/netbox/extras/querysets.py +++ b/netbox/extras/querysets.py @@ -2,6 +2,7 @@ from collections import OrderedDict from django.db.models import OuterRef, Subquery, Q +from extras.models.tags import TaggedItem from utilities.query_functions import EmptyGroupByJSONBAgg, OrderableJSONBAgg from utilities.querysets import RestrictedQuerySet @@ -99,11 +100,25 @@ class ConfigContextModelQuerySet(RestrictedQuerySet): def _get_config_context_filters(self): # Construct the set of Q objects for the specific object types + tag_query_filters = { + "object_id": OuterRef(OuterRef('pk')), + "content_type__app_label": self.model._meta.app_label, + "content_type__model": self.model._meta.model_name + } base_query = Q( Q(platforms=OuterRef('platform')) | Q(platforms=None), Q(tenant_groups=OuterRef('tenant__group')) | Q(tenant_groups=None), Q(tenants=OuterRef('tenant')) | Q(tenants=None), - Q(tags=OuterRef('tags')) | Q(tags=None), + Q( + tags__pk__in=Subquery( + TaggedItem.objects.filter( + **tag_query_filters + ).values_list( + 'tag_id', + flat=True + ) + ) + ) | Q(tags=None), is_active=True, ) diff --git a/netbox/extras/tests/test_models.py b/netbox/extras/tests/test_models.py index 80beb0296..d55f1ddd3 100644 --- a/netbox/extras/tests/test_models.py +++ b/netbox/extras/tests/test_models.py @@ -363,3 +363,46 @@ class ConfigContextTest(TestCase): annotated_queryset = Device.objects.filter(name=device.name).annotate_config_context_data() self.assertEqual(ConfigContext.objects.get_for_object(device).count(), 1) self.assertEqual(device.get_config_context(), annotated_queryset[0].get_config_context()) + + def test_multiple_tags_return_distinct_objects_with_seperate_config_contexts(self): + """ + Tagged items use a generic relationship, which results in duplicate rows being returned when queried. + This is combatted by by appending distinct() to the config context querysets. This test creates a config + context assigned to two tags and ensures objects related by those same two tags result in only a single + config context record being returned. + + This test case is seperate from the above in that it deals with multiple config context objects in play. + + See https://github.com/netbox-community/netbox/issues/5387 + """ + tag_context_1 = ConfigContext.objects.create( + name="tag-1", + weight=100, + data={ + "tag": 1 + } + ) + tag_context_1.tags.add(self.tag) + tag_context_2 = ConfigContext.objects.create( + name="tag-2", + weight=100, + data={ + "tag": 1 + } + ) + tag_context_2.tags.add(self.tag2) + + device = Device.objects.create( + name="Device 3", + site=self.site, + tenant=self.tenant, + platform=self.platform, + device_role=self.devicerole, + device_type=self.devicetype + ) + device.tags.add(self.tag) + device.tags.add(self.tag2) + + annotated_queryset = Device.objects.filter(name=device.name).annotate_config_context_data() + self.assertEqual(ConfigContext.objects.get_for_object(device).count(), 2) + self.assertEqual(device.get_config_context(), annotated_queryset[0].get_config_context())