From 0df67dbc12783b1322a8bd48f3d888d6411e39f4 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 3 Aug 2021 09:54:06 -0400 Subject: [PATCH 1/8] Add ip_addresses relation on InterfaceType, VMInterfaceType --- netbox/dcim/graphql/types.py | 3 ++- netbox/ipam/graphql/mixins.py | 12 ++++++++++++ netbox/utilities/testing/api.py | 8 ++++++-- netbox/virtualization/graphql/types.py | 3 ++- 4 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 netbox/ipam/graphql/mixins.py diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index de091ec64..a4eb4ed9a 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -1,4 +1,5 @@ from dcim import filtersets, models +from ipam.graphql.mixins import IPAddressesMixin from netbox.graphql.types import BaseObjectType, ObjectType, TaggedObjectType __all__ = ( @@ -158,7 +159,7 @@ class FrontPortTemplateType(BaseObjectType): filterset_class = filtersets.FrontPortTemplateFilterSet -class InterfaceType(TaggedObjectType): +class InterfaceType(IPAddressesMixin, TaggedObjectType): class Meta: model = models.Interface diff --git a/netbox/ipam/graphql/mixins.py b/netbox/ipam/graphql/mixins.py new file mode 100644 index 000000000..ba1eaf463 --- /dev/null +++ b/netbox/ipam/graphql/mixins.py @@ -0,0 +1,12 @@ +import graphene + +__all__ = ( + 'IPAddressesMixin', +) + + +class IPAddressesMixin: + ip_addresses = graphene.List('ipam.graphql.types.IPAddressType') + + def resolve_ip_addresses(self, info): + return self.ip_addresses.restrict(info.context.user, 'view') diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 3bb54b529..fe652843a 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -5,7 +5,7 @@ from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.urls import reverse from django.test import override_settings -from graphene.types.dynamic import Dynamic +from graphene.types import Dynamic as GQLDynamic, List as GQLList from rest_framework import status from rest_framework.test import APIClient @@ -446,9 +446,13 @@ class APIViewTestCases: # Compile list of fields to include fields_string = '' for field_name, field in type_class._meta.fields.items(): - if type(field) is Dynamic: + if type(field) is GQLDynamic: # Dynamic fields must specify a subselection fields_string += f'{field_name} {{ id }}\n' + elif type(field.type) is GQLList and field_name not in ('tags', 'choices'): + # TODO: Come up with something more elegant + # Temporary hack to support automated testing of reverse generic relations + fields_string += f'{field_name} {{ id }}\n' else: fields_string += f'{field_name}\n' diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index f4e757ecf..d12775436 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -1,3 +1,4 @@ +from ipam.graphql.mixins import IPAddressesMixin from virtualization import filtersets, models from netbox.graphql.types import ObjectType, TaggedObjectType @@ -42,7 +43,7 @@ class VirtualMachineType(TaggedObjectType): filterset_class = filtersets.VirtualMachineFilterSet -class VMInterfaceType(TaggedObjectType): +class VMInterfaceType(IPAddressesMixin, TaggedObjectType): class Meta: model = models.VMInterface From 8ad958708f266c5f10e54596fc0bd9502da7d450 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 3 Aug 2021 11:38:18 -0400 Subject: [PATCH 2/8] Add image_attachments to Device, Location, Rack, Site --- netbox/dcim/graphql/types.py | 9 +++++---- netbox/extras/graphql/mixins.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 netbox/extras/graphql/mixins.py diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index a4eb4ed9a..fb9a7cd47 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -1,4 +1,5 @@ from dcim import filtersets, models +from extras.graphql.mixins import ImageAttachmentsMixin from ipam.graphql.mixins import IPAddressesMixin from netbox.graphql.types import BaseObjectType, ObjectType, TaggedObjectType @@ -97,7 +98,7 @@ class ConsoleServerPortTemplateType(BaseObjectType): return self.type or None -class DeviceType(TaggedObjectType): +class DeviceType(ImageAttachmentsMixin, TaggedObjectType): class Meta: model = models.Device @@ -186,7 +187,7 @@ class InventoryItemType(TaggedObjectType): filterset_class = filtersets.InventoryItemFilterSet -class LocationType(ObjectType): +class LocationType(ImageAttachmentsMixin, ObjectType): class Meta: model = models.Location @@ -276,7 +277,7 @@ class PowerPortTemplateType(BaseObjectType): return self.type or None -class RackType(TaggedObjectType): +class RackType(ImageAttachmentsMixin, TaggedObjectType): class Meta: model = models.Rack @@ -330,7 +331,7 @@ class RegionType(ObjectType): filterset_class = filtersets.RegionFilterSet -class SiteType(TaggedObjectType): +class SiteType(ImageAttachmentsMixin, TaggedObjectType): class Meta: model = models.Site diff --git a/netbox/extras/graphql/mixins.py b/netbox/extras/graphql/mixins.py new file mode 100644 index 000000000..5c857db8c --- /dev/null +++ b/netbox/extras/graphql/mixins.py @@ -0,0 +1,12 @@ +import graphene + +__all__ = ( + 'ImageAttachmentsMixin', +) + + +class ImageAttachmentsMixin: + image_attachments = graphene.List('extras.graphql.types.ImageAttachmentType') + + def resolve_image_attachments(self, info): + return self.images.restrict(info.context.user, 'view') From 735286d3b0c6fa772dd151c8935509a1f82a5f8e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 3 Aug 2021 11:49:22 -0400 Subject: [PATCH 3/8] Add vlan_groups to Region, SiteGroup, Site, Location, Rack, ClusterGroup, Cluster --- netbox/dcim/graphql/types.py | 12 ++++++------ netbox/dcim/models/racks.py | 6 ++++++ netbox/dcim/models/sites.py | 24 ++++++++++++++++++++++++ netbox/ipam/graphql/mixins.py | 8 ++++++++ netbox/virtualization/graphql/types.py | 6 +++--- netbox/virtualization/models.py | 12 ++++++++++++ 6 files changed, 59 insertions(+), 9 deletions(-) diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index fb9a7cd47..1e19a1b36 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -1,6 +1,6 @@ from dcim import filtersets, models from extras.graphql.mixins import ImageAttachmentsMixin -from ipam.graphql.mixins import IPAddressesMixin +from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from netbox.graphql.types import BaseObjectType, ObjectType, TaggedObjectType __all__ = ( @@ -187,7 +187,7 @@ class InventoryItemType(TaggedObjectType): filterset_class = filtersets.InventoryItemFilterSet -class LocationType(ImageAttachmentsMixin, ObjectType): +class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ObjectType): class Meta: model = models.Location @@ -277,7 +277,7 @@ class PowerPortTemplateType(BaseObjectType): return self.type or None -class RackType(ImageAttachmentsMixin, TaggedObjectType): +class RackType(VLANGroupsMixin, ImageAttachmentsMixin, TaggedObjectType): class Meta: model = models.Rack @@ -323,7 +323,7 @@ class RearPortTemplateType(BaseObjectType): filterset_class = filtersets.RearPortTemplateFilterSet -class RegionType(ObjectType): +class RegionType(VLANGroupsMixin, ObjectType): class Meta: model = models.Region @@ -331,7 +331,7 @@ class RegionType(ObjectType): filterset_class = filtersets.RegionFilterSet -class SiteType(ImageAttachmentsMixin, TaggedObjectType): +class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, TaggedObjectType): class Meta: model = models.Site @@ -339,7 +339,7 @@ class SiteType(ImageAttachmentsMixin, TaggedObjectType): filterset_class = filtersets.SiteFilterSet -class SiteGroupType(ObjectType): +class SiteGroupType(VLANGroupsMixin, ObjectType): class Meta: model = models.SiteGroup diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 3b74a3d3d..c287d7d6c 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -175,6 +175,12 @@ class Rack(PrimaryModel): comments = models.TextField( blank=True ) + vlan_groups = GenericRelation( + to='ipam.VLANGroup', + content_type_field='scope_type', + object_id_field='scope_id', + related_query_name='rack' + ) images = GenericRelation( to='extras.ImageAttachment' ) diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index 943e98106..56946642b 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -53,6 +53,12 @@ class Region(NestedGroupModel): max_length=200, blank=True ) + vlan_groups = GenericRelation( + to='ipam.VLANGroup', + content_type_field='scope_type', + object_id_field='scope_id', + related_query_name='region' + ) def get_absolute_url(self): return reverse('dcim:region', args=[self.pk]) @@ -95,6 +101,12 @@ class SiteGroup(NestedGroupModel): max_length=200, blank=True ) + vlan_groups = GenericRelation( + to='ipam.VLANGroup', + content_type_field='scope_type', + object_id_field='scope_id', + related_query_name='site_group' + ) def get_absolute_url(self): return reverse('dcim:sitegroup', args=[self.pk]) @@ -210,6 +222,12 @@ class Site(PrimaryModel): comments = models.TextField( blank=True ) + vlan_groups = GenericRelation( + to='ipam.VLANGroup', + content_type_field='scope_type', + object_id_field='scope_id', + related_query_name='site' + ) images = GenericRelation( to='extras.ImageAttachment' ) @@ -267,6 +285,12 @@ class Location(NestedGroupModel): max_length=200, blank=True ) + vlan_groups = GenericRelation( + to='ipam.VLANGroup', + content_type_field='scope_type', + object_id_field='scope_id', + related_query_name='location' + ) images = GenericRelation( to='extras.ImageAttachment' ) diff --git a/netbox/ipam/graphql/mixins.py b/netbox/ipam/graphql/mixins.py index ba1eaf463..283414df3 100644 --- a/netbox/ipam/graphql/mixins.py +++ b/netbox/ipam/graphql/mixins.py @@ -2,6 +2,7 @@ import graphene __all__ = ( 'IPAddressesMixin', + 'VLANGroupsMixin', ) @@ -10,3 +11,10 @@ class IPAddressesMixin: def resolve_ip_addresses(self, info): return self.ip_addresses.restrict(info.context.user, 'view') + + +class VLANGroupsMixin: + vlan_groups = graphene.List('ipam.graphql.types.VLANGroupType') + + def resolve_vlan_groups(self, info): + return self.vlan_groups.restrict(info.context.user, 'view') diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index d12775436..83e5fbc73 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -1,4 +1,4 @@ -from ipam.graphql.mixins import IPAddressesMixin +from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from virtualization import filtersets, models from netbox.graphql.types import ObjectType, TaggedObjectType @@ -11,7 +11,7 @@ __all__ = ( ) -class ClusterType(TaggedObjectType): +class ClusterType(VLANGroupsMixin, TaggedObjectType): class Meta: model = models.Cluster @@ -19,7 +19,7 @@ class ClusterType(TaggedObjectType): filterset_class = filtersets.ClusterFilterSet -class ClusterGroupType(ObjectType): +class ClusterGroupType(VLANGroupsMixin, ObjectType): class Meta: model = models.ClusterGroup diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 4f8683114..3408cedbc 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -81,6 +81,12 @@ class ClusterGroup(OrganizationalModel): max_length=200, blank=True ) + vlan_groups = GenericRelation( + to='ipam.VLANGroup', + content_type_field='scope_type', + object_id_field='scope_id', + related_query_name='cluster_group' + ) objects = RestrictedQuerySet.as_manager() @@ -136,6 +142,12 @@ class Cluster(PrimaryModel): comments = models.TextField( blank=True ) + vlan_groups = GenericRelation( + to='ipam.VLANGroup', + content_type_field='scope_type', + object_id_field='scope_id', + related_query_name='cluster' + ) objects = RestrictedQuerySet.as_manager() From c416fce400e6f8fd3d06f18e61a483b9b5f0b0ee Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 3 Aug 2021 13:49:12 -0400 Subject: [PATCH 4/8] Refactor base Graphene object types --- netbox/extras/graphql/mixins.py | 17 +++++++++++++++++ netbox/netbox/graphql/types.py | 22 ++++++---------------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/netbox/extras/graphql/mixins.py b/netbox/extras/graphql/mixins.py index 5c857db8c..5004ab841 100644 --- a/netbox/extras/graphql/mixins.py +++ b/netbox/extras/graphql/mixins.py @@ -1,12 +1,29 @@ import graphene +from graphene.types.generic import GenericScalar __all__ = ( + 'CustomFieldsMixin', 'ImageAttachmentsMixin', + 'TagsMixin', ) +class CustomFieldsMixin: + custom_fields = GenericScalar() + + def resolve_custom_fields(self, info): + return self.custom_field_data + + class ImageAttachmentsMixin: image_attachments = graphene.List('extras.graphql.types.ImageAttachmentType') def resolve_image_attachments(self, info): return self.images.restrict(info.context.user, 'view') + + +class TagsMixin: + tags = graphene.List(graphene.String) + + def resolve_tags(self, info): + return self.tags.all() diff --git a/netbox/netbox/graphql/types.py b/netbox/netbox/graphql/types.py index 2958247b7..5e9d53338 100644 --- a/netbox/netbox/graphql/types.py +++ b/netbox/netbox/graphql/types.py @@ -1,8 +1,8 @@ -import graphene from django.contrib.contenttypes.models import ContentType -from graphene.types.generic import GenericScalar from graphene_django import DjangoObjectType +from extras.graphql.mixins import CustomFieldsMixin, TagsMixin + __all__ = ( 'BaseObjectType', 'ObjectType', @@ -27,31 +27,21 @@ class BaseObjectType(DjangoObjectType): return queryset.restrict(info.context.user, 'view') -class ObjectType(BaseObjectType): +class ObjectType(CustomFieldsMixin, BaseObjectType): """ - Extends BaseObjectType with support for custom field data. + Extends BaseObjectType with support for custom fields. """ - custom_fields = GenericScalar() - class Meta: abstract = True - def resolve_custom_fields(self, info): - return self.custom_field_data - -class TaggedObjectType(ObjectType): +class TaggedObjectType(CustomFieldsMixin, TagsMixin, BaseObjectType): """ - Extends ObjectType with support for Tags + Extends BaseObjectType with support for custom fields and tags """ - tags = graphene.List(graphene.String) - class Meta: abstract = True - def resolve_tags(self, info): - return self.tags.all() - # # Miscellaneous types From ea86321da80a7e1df8846c1f552abbe9f9a219de Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 3 Aug 2021 13:58:08 -0400 Subject: [PATCH 5/8] Add journal_entries to Graphene object types for all primary models --- netbox/circuits/graphql/types.py | 8 ++++---- netbox/dcim/graphql/types.py | 20 ++++++++++---------- netbox/extras/graphql/mixins.py | 8 ++++++++ netbox/ipam/graphql/types.py | 18 +++++++++--------- netbox/netbox/graphql/types.py | 10 +++++++++- netbox/tenancy/graphql/types.py | 4 ++-- netbox/virtualization/graphql/types.py | 6 +++--- 7 files changed, 45 insertions(+), 29 deletions(-) diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index bde25a09f..ede59f855 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -1,5 +1,5 @@ from circuits import filtersets, models -from netbox.graphql.types import BaseObjectType, ObjectType, TaggedObjectType +from netbox.graphql.types import BaseObjectType, ObjectType, PrimaryObjectType __all__ = ( 'CircuitTerminationType', @@ -18,7 +18,7 @@ class CircuitTerminationType(BaseObjectType): filterset_class = filtersets.CircuitTerminationFilterSet -class CircuitType(TaggedObjectType): +class CircuitType(PrimaryObjectType): class Meta: model = models.Circuit @@ -34,7 +34,7 @@ class CircuitTypeType(ObjectType): filterset_class = filtersets.CircuitTypeFilterSet -class ProviderType(TaggedObjectType): +class ProviderType(PrimaryObjectType): class Meta: model = models.Provider @@ -42,7 +42,7 @@ class ProviderType(TaggedObjectType): filterset_class = filtersets.ProviderFilterSet -class ProviderNetworkType(TaggedObjectType): +class ProviderNetworkType(PrimaryObjectType): class Meta: model = models.ProviderNetwork diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 1e19a1b36..be2de1621 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -1,7 +1,7 @@ from dcim import filtersets, models from extras.graphql.mixins import ImageAttachmentsMixin from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin -from netbox.graphql.types import BaseObjectType, ObjectType, TaggedObjectType +from netbox.graphql.types import BaseObjectType, ObjectType, PrimaryObjectType, TaggedObjectType __all__ = ( 'CableType', @@ -40,7 +40,7 @@ __all__ = ( ) -class CableType(TaggedObjectType): +class CableType(PrimaryObjectType): class Meta: model = models.Cable @@ -98,7 +98,7 @@ class ConsoleServerPortTemplateType(BaseObjectType): return self.type or None -class DeviceType(ImageAttachmentsMixin, TaggedObjectType): +class DeviceType(ImageAttachmentsMixin, PrimaryObjectType): class Meta: model = models.Device @@ -133,7 +133,7 @@ class DeviceRoleType(ObjectType): filterset_class = filtersets.DeviceRoleFilterSet -class DeviceTypeType(TaggedObjectType): +class DeviceTypeType(PrimaryObjectType): class Meta: model = models.DeviceType @@ -211,7 +211,7 @@ class PlatformType(ObjectType): filterset_class = filtersets.PlatformFilterSet -class PowerFeedType(TaggedObjectType): +class PowerFeedType(PrimaryObjectType): class Meta: model = models.PowerFeed @@ -247,7 +247,7 @@ class PowerOutletTemplateType(BaseObjectType): return self.type or None -class PowerPanelType(TaggedObjectType): +class PowerPanelType(PrimaryObjectType): class Meta: model = models.PowerPanel @@ -277,7 +277,7 @@ class PowerPortTemplateType(BaseObjectType): return self.type or None -class RackType(VLANGroupsMixin, ImageAttachmentsMixin, TaggedObjectType): +class RackType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType): class Meta: model = models.Rack @@ -291,7 +291,7 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, TaggedObjectType): return self.outer_unit or None -class RackReservationType(TaggedObjectType): +class RackReservationType(PrimaryObjectType): class Meta: model = models.RackReservation @@ -331,7 +331,7 @@ class RegionType(VLANGroupsMixin, ObjectType): filterset_class = filtersets.RegionFilterSet -class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, TaggedObjectType): +class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType): class Meta: model = models.Site @@ -347,7 +347,7 @@ class SiteGroupType(VLANGroupsMixin, ObjectType): filterset_class = filtersets.SiteGroupFilterSet -class VirtualChassisType(TaggedObjectType): +class VirtualChassisType(PrimaryObjectType): class Meta: model = models.VirtualChassis diff --git a/netbox/extras/graphql/mixins.py b/netbox/extras/graphql/mixins.py index 5004ab841..0a6792025 100644 --- a/netbox/extras/graphql/mixins.py +++ b/netbox/extras/graphql/mixins.py @@ -4,6 +4,7 @@ from graphene.types.generic import GenericScalar __all__ = ( 'CustomFieldsMixin', 'ImageAttachmentsMixin', + 'JournalEntriesMixin', 'TagsMixin', ) @@ -22,6 +23,13 @@ class ImageAttachmentsMixin: return self.images.restrict(info.context.user, 'view') +class JournalEntriesMixin: + journal_entries = graphene.List('extras.graphql.types.JournalEntryType') + + def resolve_journal_entries(self, info): + return self.journal_entries.restrict(info.context.user, 'view') + + class TagsMixin: tags = graphene.List(graphene.String) diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index 2172d63fa..03f9e2d1f 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -1,5 +1,5 @@ from ipam import filtersets, models -from netbox.graphql.types import ObjectType, TaggedObjectType +from netbox.graphql.types import ObjectType, PrimaryObjectType __all__ = ( 'AggregateType', @@ -16,7 +16,7 @@ __all__ = ( ) -class AggregateType(TaggedObjectType): +class AggregateType(PrimaryObjectType): class Meta: model = models.Aggregate @@ -24,7 +24,7 @@ class AggregateType(TaggedObjectType): filterset_class = filtersets.AggregateFilterSet -class IPAddressType(TaggedObjectType): +class IPAddressType(PrimaryObjectType): class Meta: model = models.IPAddress @@ -35,7 +35,7 @@ class IPAddressType(TaggedObjectType): return self.role or None -class IPRangeType(TaggedObjectType): +class IPRangeType(PrimaryObjectType): class Meta: model = models.IPRange @@ -46,7 +46,7 @@ class IPRangeType(TaggedObjectType): return self.role or None -class PrefixType(TaggedObjectType): +class PrefixType(PrimaryObjectType): class Meta: model = models.Prefix @@ -70,7 +70,7 @@ class RoleType(ObjectType): filterset_class = filtersets.RoleFilterSet -class RouteTargetType(TaggedObjectType): +class RouteTargetType(PrimaryObjectType): class Meta: model = models.RouteTarget @@ -78,7 +78,7 @@ class RouteTargetType(TaggedObjectType): filterset_class = filtersets.RouteTargetFilterSet -class ServiceType(TaggedObjectType): +class ServiceType(PrimaryObjectType): class Meta: model = models.Service @@ -86,7 +86,7 @@ class ServiceType(TaggedObjectType): filterset_class = filtersets.ServiceFilterSet -class VLANType(TaggedObjectType): +class VLANType(PrimaryObjectType): class Meta: model = models.VLAN @@ -102,7 +102,7 @@ class VLANGroupType(ObjectType): filterset_class = filtersets.VLANGroupFilterSet -class VRFType(TaggedObjectType): +class VRFType(PrimaryObjectType): class Meta: model = models.VRF diff --git a/netbox/netbox/graphql/types.py b/netbox/netbox/graphql/types.py index 5e9d53338..a7eb0e669 100644 --- a/netbox/netbox/graphql/types.py +++ b/netbox/netbox/graphql/types.py @@ -1,7 +1,7 @@ from django.contrib.contenttypes.models import ContentType from graphene_django import DjangoObjectType -from extras.graphql.mixins import CustomFieldsMixin, TagsMixin +from extras.graphql.mixins import CustomFieldsMixin, JournalEntriesMixin, TagsMixin __all__ = ( 'BaseObjectType', @@ -43,6 +43,14 @@ class TaggedObjectType(CustomFieldsMixin, TagsMixin, BaseObjectType): abstract = True +class PrimaryObjectType(CustomFieldsMixin, JournalEntriesMixin, TagsMixin, BaseObjectType): + """ + Extends BaseObjectType with support for custom fields, tags, and journal entries. + """ + class Meta: + abstract = True + + # # Miscellaneous types # diff --git a/netbox/tenancy/graphql/types.py b/netbox/tenancy/graphql/types.py index 8f9469c10..2d6b388ae 100644 --- a/netbox/tenancy/graphql/types.py +++ b/netbox/tenancy/graphql/types.py @@ -1,5 +1,5 @@ from tenancy import filtersets, models -from netbox.graphql.types import ObjectType, TaggedObjectType +from netbox.graphql.types import ObjectType, PrimaryObjectType __all__ = ( 'TenantType', @@ -7,7 +7,7 @@ __all__ = ( ) -class TenantType(TaggedObjectType): +class TenantType(PrimaryObjectType): class Meta: model = models.Tenant diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index 83e5fbc73..676f8d82c 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -1,6 +1,6 @@ from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from virtualization import filtersets, models -from netbox.graphql.types import ObjectType, TaggedObjectType +from netbox.graphql.types import ObjectType, PrimaryObjectType, TaggedObjectType __all__ = ( 'ClusterType', @@ -11,7 +11,7 @@ __all__ = ( ) -class ClusterType(VLANGroupsMixin, TaggedObjectType): +class ClusterType(VLANGroupsMixin, PrimaryObjectType): class Meta: model = models.Cluster @@ -35,7 +35,7 @@ class ClusterTypeType(ObjectType): filterset_class = filtersets.ClusterTypeFilterSet -class VirtualMachineType(TaggedObjectType): +class VirtualMachineType(PrimaryObjectType): class Meta: model = models.VirtualMachine From 1518a460d58a5af23eb5ed5ab5dd08ad7186c822 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 3 Aug 2021 14:14:36 -0400 Subject: [PATCH 6/8] Rename base Graphene types to match base models --- netbox/circuits/graphql/types.py | 4 +- netbox/dcim/graphql/types.py | 84 ++++++++++++++++++-------- netbox/ipam/graphql/types.py | 8 +-- netbox/netbox/graphql/types.py | 28 ++++----- netbox/tenancy/graphql/types.py | 4 +- netbox/virtualization/graphql/types.py | 9 +-- 6 files changed, 85 insertions(+), 52 deletions(-) diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index ede59f855..c359acf49 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -1,5 +1,5 @@ from circuits import filtersets, models -from netbox.graphql.types import BaseObjectType, ObjectType, PrimaryObjectType +from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, PrimaryObjectType __all__ = ( 'CircuitTerminationType', @@ -26,7 +26,7 @@ class CircuitType(PrimaryObjectType): filterset_class = filtersets.CircuitFilterSet -class CircuitTypeType(ObjectType): +class CircuitTypeType(OrganizationalObjectType): class Meta: model = models.CircuitType diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index be2de1621..c9e538e6e 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -1,10 +1,11 @@ from dcim import filtersets, models -from extras.graphql.mixins import ImageAttachmentsMixin +from extras.graphql.mixins import CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin -from netbox.graphql.types import BaseObjectType, ObjectType, PrimaryObjectType, TaggedObjectType +from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, PrimaryObjectType __all__ = ( 'CableType', + 'ComponentObjectType', 'ConsolePortType', 'ConsolePortTemplateType', 'ConsoleServerPortType', @@ -40,6 +41,37 @@ __all__ = ( ) +# +# Base types +# + + +class ComponentObjectType( + CustomFieldsMixin, + TagsMixin, + BaseObjectType +): + """ + Base type for device/VM components + """ + class Meta: + abstract = True + + +class ComponentTemplateObjectType( + BaseObjectType +): + """ + Base type for device/VM components + """ + class Meta: + abstract = True + + +# +# Model types +# + class CableType(PrimaryObjectType): class Meta: @@ -54,7 +86,7 @@ class CableType(PrimaryObjectType): return self.length_unit or None -class ConsolePortType(TaggedObjectType): +class ConsolePortType(ComponentObjectType): class Meta: model = models.ConsolePort @@ -65,7 +97,7 @@ class ConsolePortType(TaggedObjectType): return self.type or None -class ConsolePortTemplateType(BaseObjectType): +class ConsolePortTemplateType(ComponentTemplateObjectType): class Meta: model = models.ConsolePortTemplate @@ -76,7 +108,7 @@ class ConsolePortTemplateType(BaseObjectType): return self.type or None -class ConsoleServerPortType(TaggedObjectType): +class ConsoleServerPortType(ComponentObjectType): class Meta: model = models.ConsoleServerPort @@ -87,7 +119,7 @@ class ConsoleServerPortType(TaggedObjectType): return self.type or None -class ConsoleServerPortTemplateType(BaseObjectType): +class ConsoleServerPortTemplateType(ComponentTemplateObjectType): class Meta: model = models.ConsoleServerPortTemplate @@ -109,7 +141,7 @@ class DeviceType(ImageAttachmentsMixin, PrimaryObjectType): return self.face or None -class DeviceBayType(TaggedObjectType): +class DeviceBayType(ComponentObjectType): class Meta: model = models.DeviceBay @@ -117,7 +149,7 @@ class DeviceBayType(TaggedObjectType): filterset_class = filtersets.DeviceBayFilterSet -class DeviceBayTemplateType(BaseObjectType): +class DeviceBayTemplateType(ComponentTemplateObjectType): class Meta: model = models.DeviceBayTemplate @@ -125,7 +157,7 @@ class DeviceBayTemplateType(BaseObjectType): filterset_class = filtersets.DeviceBayTemplateFilterSet -class DeviceRoleType(ObjectType): +class DeviceRoleType(OrganizationalObjectType): class Meta: model = models.DeviceRole @@ -144,7 +176,7 @@ class DeviceTypeType(PrimaryObjectType): return self.subdevice_role or None -class FrontPortType(TaggedObjectType): +class FrontPortType(ComponentObjectType): class Meta: model = models.FrontPort @@ -152,7 +184,7 @@ class FrontPortType(TaggedObjectType): filterset_class = filtersets.FrontPortFilterSet -class FrontPortTemplateType(BaseObjectType): +class FrontPortTemplateType(ComponentTemplateObjectType): class Meta: model = models.FrontPortTemplate @@ -160,7 +192,7 @@ class FrontPortTemplateType(BaseObjectType): filterset_class = filtersets.FrontPortTemplateFilterSet -class InterfaceType(IPAddressesMixin, TaggedObjectType): +class InterfaceType(IPAddressesMixin, ComponentObjectType): class Meta: model = models.Interface @@ -171,7 +203,7 @@ class InterfaceType(IPAddressesMixin, TaggedObjectType): return self.mode or None -class InterfaceTemplateType(BaseObjectType): +class InterfaceTemplateType(ComponentTemplateObjectType): class Meta: model = models.InterfaceTemplate @@ -179,7 +211,7 @@ class InterfaceTemplateType(BaseObjectType): filterset_class = filtersets.InterfaceTemplateFilterSet -class InventoryItemType(TaggedObjectType): +class InventoryItemType(ComponentObjectType): class Meta: model = models.InventoryItem @@ -187,7 +219,7 @@ class InventoryItemType(TaggedObjectType): filterset_class = filtersets.InventoryItemFilterSet -class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ObjectType): +class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, OrganizationalObjectType): class Meta: model = models.Location @@ -195,7 +227,7 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ObjectType): filterset_class = filtersets.LocationFilterSet -class ManufacturerType(ObjectType): +class ManufacturerType(OrganizationalObjectType): class Meta: model = models.Manufacturer @@ -203,7 +235,7 @@ class ManufacturerType(ObjectType): filterset_class = filtersets.ManufacturerFilterSet -class PlatformType(ObjectType): +class PlatformType(OrganizationalObjectType): class Meta: model = models.Platform @@ -219,7 +251,7 @@ class PowerFeedType(PrimaryObjectType): filterset_class = filtersets.PowerFeedFilterSet -class PowerOutletType(TaggedObjectType): +class PowerOutletType(ComponentObjectType): class Meta: model = models.PowerOutlet @@ -233,7 +265,7 @@ class PowerOutletType(TaggedObjectType): return self.type or None -class PowerOutletTemplateType(BaseObjectType): +class PowerOutletTemplateType(ComponentTemplateObjectType): class Meta: model = models.PowerOutletTemplate @@ -255,7 +287,7 @@ class PowerPanelType(PrimaryObjectType): filterset_class = filtersets.PowerPanelFilterSet -class PowerPortType(TaggedObjectType): +class PowerPortType(ComponentObjectType): class Meta: model = models.PowerPort @@ -266,7 +298,7 @@ class PowerPortType(TaggedObjectType): return self.type or None -class PowerPortTemplateType(BaseObjectType): +class PowerPortTemplateType(ComponentTemplateObjectType): class Meta: model = models.PowerPortTemplate @@ -299,7 +331,7 @@ class RackReservationType(PrimaryObjectType): filterset_class = filtersets.RackReservationFilterSet -class RackRoleType(ObjectType): +class RackRoleType(OrganizationalObjectType): class Meta: model = models.RackRole @@ -307,7 +339,7 @@ class RackRoleType(ObjectType): filterset_class = filtersets.RackRoleFilterSet -class RearPortType(TaggedObjectType): +class RearPortType(ComponentObjectType): class Meta: model = models.RearPort @@ -315,7 +347,7 @@ class RearPortType(TaggedObjectType): filterset_class = filtersets.RearPortFilterSet -class RearPortTemplateType(BaseObjectType): +class RearPortTemplateType(ComponentTemplateObjectType): class Meta: model = models.RearPortTemplate @@ -323,7 +355,7 @@ class RearPortTemplateType(BaseObjectType): filterset_class = filtersets.RearPortTemplateFilterSet -class RegionType(VLANGroupsMixin, ObjectType): +class RegionType(VLANGroupsMixin, OrganizationalObjectType): class Meta: model = models.Region @@ -339,7 +371,7 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType): filterset_class = filtersets.SiteFilterSet -class SiteGroupType(VLANGroupsMixin, ObjectType): +class SiteGroupType(VLANGroupsMixin, OrganizationalObjectType): class Meta: model = models.SiteGroup diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index 03f9e2d1f..c822dab6b 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -1,5 +1,5 @@ from ipam import filtersets, models -from netbox.graphql.types import ObjectType, PrimaryObjectType +from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType __all__ = ( 'AggregateType', @@ -54,7 +54,7 @@ class PrefixType(PrimaryObjectType): filterset_class = filtersets.PrefixFilterSet -class RIRType(ObjectType): +class RIRType(OrganizationalObjectType): class Meta: model = models.RIR @@ -62,7 +62,7 @@ class RIRType(ObjectType): filterset_class = filtersets.RIRFilterSet -class RoleType(ObjectType): +class RoleType(OrganizationalObjectType): class Meta: model = models.Role @@ -94,7 +94,7 @@ class VLANType(PrimaryObjectType): filterset_class = filtersets.VLANFilterSet -class VLANGroupType(ObjectType): +class VLANGroupType(OrganizationalObjectType): class Meta: model = models.VLANGroup diff --git a/netbox/netbox/graphql/types.py b/netbox/netbox/graphql/types.py index a7eb0e669..275ed0235 100644 --- a/netbox/netbox/graphql/types.py +++ b/netbox/netbox/graphql/types.py @@ -5,8 +5,8 @@ from extras.graphql.mixins import CustomFieldsMixin, JournalEntriesMixin, TagsMi __all__ = ( 'BaseObjectType', - 'ObjectType', - 'TaggedObjectType', + 'OrganizationalObjectType', + 'PrimaryObjectType', ) @@ -27,25 +27,25 @@ class BaseObjectType(DjangoObjectType): return queryset.restrict(info.context.user, 'view') -class ObjectType(CustomFieldsMixin, BaseObjectType): +class OrganizationalObjectType( + CustomFieldsMixin, + BaseObjectType +): """ - Extends BaseObjectType with support for custom fields. + Base type for organizational models """ class Meta: abstract = True -class TaggedObjectType(CustomFieldsMixin, TagsMixin, BaseObjectType): +class PrimaryObjectType( + CustomFieldsMixin, + JournalEntriesMixin, + TagsMixin, + BaseObjectType +): """ - Extends BaseObjectType with support for custom fields and tags - """ - class Meta: - abstract = True - - -class PrimaryObjectType(CustomFieldsMixin, JournalEntriesMixin, TagsMixin, BaseObjectType): - """ - Extends BaseObjectType with support for custom fields, tags, and journal entries. + Base type for primary models """ class Meta: abstract = True diff --git a/netbox/tenancy/graphql/types.py b/netbox/tenancy/graphql/types.py index 2d6b388ae..6f1e27274 100644 --- a/netbox/tenancy/graphql/types.py +++ b/netbox/tenancy/graphql/types.py @@ -1,5 +1,5 @@ from tenancy import filtersets, models -from netbox.graphql.types import ObjectType, PrimaryObjectType +from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType __all__ = ( 'TenantType', @@ -15,7 +15,7 @@ class TenantType(PrimaryObjectType): filterset_class = filtersets.TenantFilterSet -class TenantGroupType(ObjectType): +class TenantGroupType(OrganizationalObjectType): class Meta: model = models.TenantGroup diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index 676f8d82c..3f1304a61 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -1,6 +1,7 @@ +from dcim.graphql.types import ComponentObjectType from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from virtualization import filtersets, models -from netbox.graphql.types import ObjectType, PrimaryObjectType, TaggedObjectType +from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType __all__ = ( 'ClusterType', @@ -19,7 +20,7 @@ class ClusterType(VLANGroupsMixin, PrimaryObjectType): filterset_class = filtersets.ClusterFilterSet -class ClusterGroupType(VLANGroupsMixin, ObjectType): +class ClusterGroupType(VLANGroupsMixin, OrganizationalObjectType): class Meta: model = models.ClusterGroup @@ -27,7 +28,7 @@ class ClusterGroupType(VLANGroupsMixin, ObjectType): filterset_class = filtersets.ClusterGroupFilterSet -class ClusterTypeType(ObjectType): +class ClusterTypeType(OrganizationalObjectType): class Meta: model = models.ClusterType @@ -43,7 +44,7 @@ class VirtualMachineType(PrimaryObjectType): filterset_class = filtersets.VirtualMachineFilterSet -class VMInterfaceType(IPAddressesMixin, TaggedObjectType): +class VMInterfaceType(IPAddressesMixin, ComponentObjectType): class Meta: model = models.VMInterface From 88d2441ab377ddac17ecf59028fa07623f97f2ab Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 3 Aug 2021 14:51:56 -0400 Subject: [PATCH 7/8] Add changelog GraphQL relation for changelogged models --- netbox/circuits/graphql/types.py | 4 ++-- netbox/dcim/graphql/types.py | 4 +++- netbox/extras/graphql/mixins.py | 8 ++++++++ netbox/extras/graphql/types.py | 25 +++++++++++++++++-------- netbox/netbox/graphql/types.py | 15 ++++++++++++++- netbox/netbox/models.py | 5 +++++ 6 files changed, 49 insertions(+), 12 deletions(-) diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index c359acf49..a6c28c4cd 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -1,5 +1,5 @@ from circuits import filtersets, models -from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, PrimaryObjectType +from netbox.graphql.types import ObjectType, OrganizationalObjectType, PrimaryObjectType __all__ = ( 'CircuitTerminationType', @@ -10,7 +10,7 @@ __all__ = ( ) -class CircuitTerminationType(BaseObjectType): +class CircuitTerminationType(ObjectType): class Meta: model = models.CircuitTermination diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index c9e538e6e..45552ae8c 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -1,5 +1,5 @@ from dcim import filtersets, models -from extras.graphql.mixins import CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin +from extras.graphql.mixins import ChangelogMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, PrimaryObjectType @@ -47,6 +47,7 @@ __all__ = ( class ComponentObjectType( + ChangelogMixin, CustomFieldsMixin, TagsMixin, BaseObjectType @@ -59,6 +60,7 @@ class ComponentObjectType( class ComponentTemplateObjectType( + ChangelogMixin, BaseObjectType ): """ diff --git a/netbox/extras/graphql/mixins.py b/netbox/extras/graphql/mixins.py index 0a6792025..0afa6732e 100644 --- a/netbox/extras/graphql/mixins.py +++ b/netbox/extras/graphql/mixins.py @@ -2,6 +2,7 @@ import graphene from graphene.types.generic import GenericScalar __all__ = ( + 'ChangelogMixin', 'CustomFieldsMixin', 'ImageAttachmentsMixin', 'JournalEntriesMixin', @@ -9,6 +10,13 @@ __all__ = ( ) +class ChangelogMixin: + changelog = graphene.List('extras.graphql.types.ObjectChangeType') + + def resolve_changelog(self, info): + return self.object_changes.restrict(info.context.user, 'view') + + class CustomFieldsMixin: custom_fields = GenericScalar() diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index 96651351d..c5f34a11e 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -1,5 +1,5 @@ from extras import filtersets, models -from netbox.graphql.types import BaseObjectType +from netbox.graphql.types import BaseObjectType, ObjectType __all__ = ( 'ConfigContextType', @@ -8,12 +8,13 @@ __all__ = ( 'ExportTemplateType', 'ImageAttachmentType', 'JournalEntryType', + 'ObjectChangeType', 'TagType', 'WebhookType', ) -class ConfigContextType(BaseObjectType): +class ConfigContextType(ObjectType): class Meta: model = models.ConfigContext @@ -21,7 +22,7 @@ class ConfigContextType(BaseObjectType): filterset_class = filtersets.ConfigContextFilterSet -class CustomFieldType(BaseObjectType): +class CustomFieldType(ObjectType): class Meta: model = models.CustomField @@ -29,7 +30,7 @@ class CustomFieldType(BaseObjectType): filterset_class = filtersets.CustomFieldFilterSet -class CustomLinkType(BaseObjectType): +class CustomLinkType(ObjectType): class Meta: model = models.CustomLink @@ -37,7 +38,7 @@ class CustomLinkType(BaseObjectType): filterset_class = filtersets.CustomLinkFilterSet -class ExportTemplateType(BaseObjectType): +class ExportTemplateType(ObjectType): class Meta: model = models.ExportTemplate @@ -53,7 +54,7 @@ class ImageAttachmentType(BaseObjectType): filterset_class = filtersets.ImageAttachmentFilterSet -class JournalEntryType(BaseObjectType): +class JournalEntryType(ObjectType): class Meta: model = models.JournalEntry @@ -61,7 +62,15 @@ class JournalEntryType(BaseObjectType): filterset_class = filtersets.JournalEntryFilterSet -class TagType(BaseObjectType): +class ObjectChangeType(BaseObjectType): + + class Meta: + model = models.ObjectChange + fields = '__all__' + filterset_class = filtersets.ObjectChangeFilterSet + + +class TagType(ObjectType): class Meta: model = models.Tag @@ -69,7 +78,7 @@ class TagType(BaseObjectType): filterset_class = filtersets.TagFilterSet -class WebhookType(BaseObjectType): +class WebhookType(ObjectType): class Meta: model = models.Webhook diff --git a/netbox/netbox/graphql/types.py b/netbox/netbox/graphql/types.py index 275ed0235..181b9a0c6 100644 --- a/netbox/netbox/graphql/types.py +++ b/netbox/netbox/graphql/types.py @@ -1,7 +1,7 @@ from django.contrib.contenttypes.models import ContentType from graphene_django import DjangoObjectType -from extras.graphql.mixins import CustomFieldsMixin, JournalEntriesMixin, TagsMixin +from extras.graphql.mixins import ChangelogMixin, CustomFieldsMixin, JournalEntriesMixin, TagsMixin __all__ = ( 'BaseObjectType', @@ -27,7 +27,19 @@ class BaseObjectType(DjangoObjectType): return queryset.restrict(info.context.user, 'view') +class ObjectType( + ChangelogMixin, + BaseObjectType +): + """ + Base GraphQL object type for unclassified models which support change logging + """ + class Meta: + abstract = True + + class OrganizationalObjectType( + ChangelogMixin, CustomFieldsMixin, BaseObjectType ): @@ -39,6 +51,7 @@ class OrganizationalObjectType( class PrimaryObjectType( + ChangelogMixin, CustomFieldsMixin, JournalEntriesMixin, TagsMixin, diff --git a/netbox/netbox/models.py b/netbox/netbox/models.py index 3045e1fce..317548921 100644 --- a/netbox/netbox/models.py +++ b/netbox/netbox/models.py @@ -40,6 +40,11 @@ class ChangeLoggingMixin(models.Model): blank=True, null=True ) + object_changes = GenericRelation( + to='extras.ObjectChange', + content_type_field='changed_object_type', + object_id_field='changed_object_id' + ) class Meta: abstract = True From cc26bc485840f4456fdd099ee1f7fedb923afbd7 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 3 Aug 2021 14:56:22 -0400 Subject: [PATCH 8/8] Changelog for #6829 --- docs/release-notes/version-3.0.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 9b3f10589..dd2819cb6 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -2,6 +2,10 @@ ## v3.0-beta2 (FUTURE) +### Enhancements + +* [#6829](https://github.com/netbox-community/netbox/issues/6829) - Extend GraphQL API to support reverse generic relationships + ### Bug Fixes * [#6811](https://github.com/netbox-community/netbox/issues/6811) - Fix exception when editing users