Merge pull request #6873 from netbox-community/6829-graphql-reverse-relations

Closes #6829: GraphQL reverse generic relations
This commit is contained in:
Jeremy Stretch 2021-08-03 16:22:45 -04:00 committed by GitHub
commit c411d2a9f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 266 additions and 88 deletions

View File

@ -2,6 +2,10 @@
## v3.0-beta2 (FUTURE) ## v3.0-beta2 (FUTURE)
### Enhancements
* [#6829](https://github.com/netbox-community/netbox/issues/6829) - Extend GraphQL API to support reverse generic relationships
### Bug Fixes ### Bug Fixes
* [#6811](https://github.com/netbox-community/netbox/issues/6811) - Fix exception when editing users * [#6811](https://github.com/netbox-community/netbox/issues/6811) - Fix exception when editing users

View File

@ -1,5 +1,5 @@
from circuits import filtersets, models from circuits import filtersets, models
from netbox.graphql.types import BaseObjectType, ObjectType, TaggedObjectType from netbox.graphql.types import ObjectType, OrganizationalObjectType, PrimaryObjectType
__all__ = ( __all__ = (
'CircuitTerminationType', 'CircuitTerminationType',
@ -10,7 +10,7 @@ __all__ = (
) )
class CircuitTerminationType(BaseObjectType): class CircuitTerminationType(ObjectType):
class Meta: class Meta:
model = models.CircuitTermination model = models.CircuitTermination
@ -18,7 +18,7 @@ class CircuitTerminationType(BaseObjectType):
filterset_class = filtersets.CircuitTerminationFilterSet filterset_class = filtersets.CircuitTerminationFilterSet
class CircuitType(TaggedObjectType): class CircuitType(PrimaryObjectType):
class Meta: class Meta:
model = models.Circuit model = models.Circuit
@ -26,7 +26,7 @@ class CircuitType(TaggedObjectType):
filterset_class = filtersets.CircuitFilterSet filterset_class = filtersets.CircuitFilterSet
class CircuitTypeType(ObjectType): class CircuitTypeType(OrganizationalObjectType):
class Meta: class Meta:
model = models.CircuitType model = models.CircuitType
@ -34,7 +34,7 @@ class CircuitTypeType(ObjectType):
filterset_class = filtersets.CircuitTypeFilterSet filterset_class = filtersets.CircuitTypeFilterSet
class ProviderType(TaggedObjectType): class ProviderType(PrimaryObjectType):
class Meta: class Meta:
model = models.Provider model = models.Provider
@ -42,7 +42,7 @@ class ProviderType(TaggedObjectType):
filterset_class = filtersets.ProviderFilterSet filterset_class = filtersets.ProviderFilterSet
class ProviderNetworkType(TaggedObjectType): class ProviderNetworkType(PrimaryObjectType):
class Meta: class Meta:
model = models.ProviderNetwork model = models.ProviderNetwork

View File

@ -1,8 +1,11 @@
from dcim import filtersets, models from dcim import filtersets, models
from netbox.graphql.types import BaseObjectType, ObjectType, TaggedObjectType from extras.graphql.mixins import ChangelogMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, PrimaryObjectType
__all__ = ( __all__ = (
'CableType', 'CableType',
'ComponentObjectType',
'ConsolePortType', 'ConsolePortType',
'ConsolePortTemplateType', 'ConsolePortTemplateType',
'ConsoleServerPortType', 'ConsoleServerPortType',
@ -38,7 +41,40 @@ __all__ = (
) )
class CableType(TaggedObjectType): #
# Base types
#
class ComponentObjectType(
ChangelogMixin,
CustomFieldsMixin,
TagsMixin,
BaseObjectType
):
"""
Base type for device/VM components
"""
class Meta:
abstract = True
class ComponentTemplateObjectType(
ChangelogMixin,
BaseObjectType
):
"""
Base type for device/VM components
"""
class Meta:
abstract = True
#
# Model types
#
class CableType(PrimaryObjectType):
class Meta: class Meta:
model = models.Cable model = models.Cable
@ -52,7 +88,7 @@ class CableType(TaggedObjectType):
return self.length_unit or None return self.length_unit or None
class ConsolePortType(TaggedObjectType): class ConsolePortType(ComponentObjectType):
class Meta: class Meta:
model = models.ConsolePort model = models.ConsolePort
@ -63,7 +99,7 @@ class ConsolePortType(TaggedObjectType):
return self.type or None return self.type or None
class ConsolePortTemplateType(BaseObjectType): class ConsolePortTemplateType(ComponentTemplateObjectType):
class Meta: class Meta:
model = models.ConsolePortTemplate model = models.ConsolePortTemplate
@ -74,7 +110,7 @@ class ConsolePortTemplateType(BaseObjectType):
return self.type or None return self.type or None
class ConsoleServerPortType(TaggedObjectType): class ConsoleServerPortType(ComponentObjectType):
class Meta: class Meta:
model = models.ConsoleServerPort model = models.ConsoleServerPort
@ -85,7 +121,7 @@ class ConsoleServerPortType(TaggedObjectType):
return self.type or None return self.type or None
class ConsoleServerPortTemplateType(BaseObjectType): class ConsoleServerPortTemplateType(ComponentTemplateObjectType):
class Meta: class Meta:
model = models.ConsoleServerPortTemplate model = models.ConsoleServerPortTemplate
@ -96,7 +132,7 @@ class ConsoleServerPortTemplateType(BaseObjectType):
return self.type or None return self.type or None
class DeviceType(TaggedObjectType): class DeviceType(ImageAttachmentsMixin, PrimaryObjectType):
class Meta: class Meta:
model = models.Device model = models.Device
@ -107,7 +143,7 @@ class DeviceType(TaggedObjectType):
return self.face or None return self.face or None
class DeviceBayType(TaggedObjectType): class DeviceBayType(ComponentObjectType):
class Meta: class Meta:
model = models.DeviceBay model = models.DeviceBay
@ -115,7 +151,7 @@ class DeviceBayType(TaggedObjectType):
filterset_class = filtersets.DeviceBayFilterSet filterset_class = filtersets.DeviceBayFilterSet
class DeviceBayTemplateType(BaseObjectType): class DeviceBayTemplateType(ComponentTemplateObjectType):
class Meta: class Meta:
model = models.DeviceBayTemplate model = models.DeviceBayTemplate
@ -123,7 +159,7 @@ class DeviceBayTemplateType(BaseObjectType):
filterset_class = filtersets.DeviceBayTemplateFilterSet filterset_class = filtersets.DeviceBayTemplateFilterSet
class DeviceRoleType(ObjectType): class DeviceRoleType(OrganizationalObjectType):
class Meta: class Meta:
model = models.DeviceRole model = models.DeviceRole
@ -131,7 +167,7 @@ class DeviceRoleType(ObjectType):
filterset_class = filtersets.DeviceRoleFilterSet filterset_class = filtersets.DeviceRoleFilterSet
class DeviceTypeType(TaggedObjectType): class DeviceTypeType(PrimaryObjectType):
class Meta: class Meta:
model = models.DeviceType model = models.DeviceType
@ -142,7 +178,7 @@ class DeviceTypeType(TaggedObjectType):
return self.subdevice_role or None return self.subdevice_role or None
class FrontPortType(TaggedObjectType): class FrontPortType(ComponentObjectType):
class Meta: class Meta:
model = models.FrontPort model = models.FrontPort
@ -150,7 +186,7 @@ class FrontPortType(TaggedObjectType):
filterset_class = filtersets.FrontPortFilterSet filterset_class = filtersets.FrontPortFilterSet
class FrontPortTemplateType(BaseObjectType): class FrontPortTemplateType(ComponentTemplateObjectType):
class Meta: class Meta:
model = models.FrontPortTemplate model = models.FrontPortTemplate
@ -158,7 +194,7 @@ class FrontPortTemplateType(BaseObjectType):
filterset_class = filtersets.FrontPortTemplateFilterSet filterset_class = filtersets.FrontPortTemplateFilterSet
class InterfaceType(TaggedObjectType): class InterfaceType(IPAddressesMixin, ComponentObjectType):
class Meta: class Meta:
model = models.Interface model = models.Interface
@ -169,7 +205,7 @@ class InterfaceType(TaggedObjectType):
return self.mode or None return self.mode or None
class InterfaceTemplateType(BaseObjectType): class InterfaceTemplateType(ComponentTemplateObjectType):
class Meta: class Meta:
model = models.InterfaceTemplate model = models.InterfaceTemplate
@ -177,7 +213,7 @@ class InterfaceTemplateType(BaseObjectType):
filterset_class = filtersets.InterfaceTemplateFilterSet filterset_class = filtersets.InterfaceTemplateFilterSet
class InventoryItemType(TaggedObjectType): class InventoryItemType(ComponentObjectType):
class Meta: class Meta:
model = models.InventoryItem model = models.InventoryItem
@ -185,7 +221,7 @@ class InventoryItemType(TaggedObjectType):
filterset_class = filtersets.InventoryItemFilterSet filterset_class = filtersets.InventoryItemFilterSet
class LocationType(ObjectType): class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, OrganizationalObjectType):
class Meta: class Meta:
model = models.Location model = models.Location
@ -193,7 +229,7 @@ class LocationType(ObjectType):
filterset_class = filtersets.LocationFilterSet filterset_class = filtersets.LocationFilterSet
class ManufacturerType(ObjectType): class ManufacturerType(OrganizationalObjectType):
class Meta: class Meta:
model = models.Manufacturer model = models.Manufacturer
@ -201,7 +237,7 @@ class ManufacturerType(ObjectType):
filterset_class = filtersets.ManufacturerFilterSet filterset_class = filtersets.ManufacturerFilterSet
class PlatformType(ObjectType): class PlatformType(OrganizationalObjectType):
class Meta: class Meta:
model = models.Platform model = models.Platform
@ -209,7 +245,7 @@ class PlatformType(ObjectType):
filterset_class = filtersets.PlatformFilterSet filterset_class = filtersets.PlatformFilterSet
class PowerFeedType(TaggedObjectType): class PowerFeedType(PrimaryObjectType):
class Meta: class Meta:
model = models.PowerFeed model = models.PowerFeed
@ -217,7 +253,7 @@ class PowerFeedType(TaggedObjectType):
filterset_class = filtersets.PowerFeedFilterSet filterset_class = filtersets.PowerFeedFilterSet
class PowerOutletType(TaggedObjectType): class PowerOutletType(ComponentObjectType):
class Meta: class Meta:
model = models.PowerOutlet model = models.PowerOutlet
@ -231,7 +267,7 @@ class PowerOutletType(TaggedObjectType):
return self.type or None return self.type or None
class PowerOutletTemplateType(BaseObjectType): class PowerOutletTemplateType(ComponentTemplateObjectType):
class Meta: class Meta:
model = models.PowerOutletTemplate model = models.PowerOutletTemplate
@ -245,7 +281,7 @@ class PowerOutletTemplateType(BaseObjectType):
return self.type or None return self.type or None
class PowerPanelType(TaggedObjectType): class PowerPanelType(PrimaryObjectType):
class Meta: class Meta:
model = models.PowerPanel model = models.PowerPanel
@ -253,7 +289,7 @@ class PowerPanelType(TaggedObjectType):
filterset_class = filtersets.PowerPanelFilterSet filterset_class = filtersets.PowerPanelFilterSet
class PowerPortType(TaggedObjectType): class PowerPortType(ComponentObjectType):
class Meta: class Meta:
model = models.PowerPort model = models.PowerPort
@ -264,7 +300,7 @@ class PowerPortType(TaggedObjectType):
return self.type or None return self.type or None
class PowerPortTemplateType(BaseObjectType): class PowerPortTemplateType(ComponentTemplateObjectType):
class Meta: class Meta:
model = models.PowerPortTemplate model = models.PowerPortTemplate
@ -275,7 +311,7 @@ class PowerPortTemplateType(BaseObjectType):
return self.type or None return self.type or None
class RackType(TaggedObjectType): class RackType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType):
class Meta: class Meta:
model = models.Rack model = models.Rack
@ -289,7 +325,7 @@ class RackType(TaggedObjectType):
return self.outer_unit or None return self.outer_unit or None
class RackReservationType(TaggedObjectType): class RackReservationType(PrimaryObjectType):
class Meta: class Meta:
model = models.RackReservation model = models.RackReservation
@ -297,7 +333,7 @@ class RackReservationType(TaggedObjectType):
filterset_class = filtersets.RackReservationFilterSet filterset_class = filtersets.RackReservationFilterSet
class RackRoleType(ObjectType): class RackRoleType(OrganizationalObjectType):
class Meta: class Meta:
model = models.RackRole model = models.RackRole
@ -305,7 +341,7 @@ class RackRoleType(ObjectType):
filterset_class = filtersets.RackRoleFilterSet filterset_class = filtersets.RackRoleFilterSet
class RearPortType(TaggedObjectType): class RearPortType(ComponentObjectType):
class Meta: class Meta:
model = models.RearPort model = models.RearPort
@ -313,7 +349,7 @@ class RearPortType(TaggedObjectType):
filterset_class = filtersets.RearPortFilterSet filterset_class = filtersets.RearPortFilterSet
class RearPortTemplateType(BaseObjectType): class RearPortTemplateType(ComponentTemplateObjectType):
class Meta: class Meta:
model = models.RearPortTemplate model = models.RearPortTemplate
@ -321,7 +357,7 @@ class RearPortTemplateType(BaseObjectType):
filterset_class = filtersets.RearPortTemplateFilterSet filterset_class = filtersets.RearPortTemplateFilterSet
class RegionType(ObjectType): class RegionType(VLANGroupsMixin, OrganizationalObjectType):
class Meta: class Meta:
model = models.Region model = models.Region
@ -329,7 +365,7 @@ class RegionType(ObjectType):
filterset_class = filtersets.RegionFilterSet filterset_class = filtersets.RegionFilterSet
class SiteType(TaggedObjectType): class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType):
class Meta: class Meta:
model = models.Site model = models.Site
@ -337,7 +373,7 @@ class SiteType(TaggedObjectType):
filterset_class = filtersets.SiteFilterSet filterset_class = filtersets.SiteFilterSet
class SiteGroupType(ObjectType): class SiteGroupType(VLANGroupsMixin, OrganizationalObjectType):
class Meta: class Meta:
model = models.SiteGroup model = models.SiteGroup
@ -345,7 +381,7 @@ class SiteGroupType(ObjectType):
filterset_class = filtersets.SiteGroupFilterSet filterset_class = filtersets.SiteGroupFilterSet
class VirtualChassisType(TaggedObjectType): class VirtualChassisType(PrimaryObjectType):
class Meta: class Meta:
model = models.VirtualChassis model = models.VirtualChassis

View File

@ -175,6 +175,12 @@ class Rack(PrimaryModel):
comments = models.TextField( comments = models.TextField(
blank=True blank=True
) )
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='rack'
)
images = GenericRelation( images = GenericRelation(
to='extras.ImageAttachment' to='extras.ImageAttachment'
) )

View File

@ -53,6 +53,12 @@ class Region(NestedGroupModel):
max_length=200, max_length=200,
blank=True 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): def get_absolute_url(self):
return reverse('dcim:region', args=[self.pk]) return reverse('dcim:region', args=[self.pk])
@ -95,6 +101,12 @@ class SiteGroup(NestedGroupModel):
max_length=200, max_length=200,
blank=True 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): def get_absolute_url(self):
return reverse('dcim:sitegroup', args=[self.pk]) return reverse('dcim:sitegroup', args=[self.pk])
@ -210,6 +222,12 @@ class Site(PrimaryModel):
comments = models.TextField( comments = models.TextField(
blank=True blank=True
) )
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='site'
)
images = GenericRelation( images = GenericRelation(
to='extras.ImageAttachment' to='extras.ImageAttachment'
) )
@ -267,6 +285,12 @@ class Location(NestedGroupModel):
max_length=200, max_length=200,
blank=True blank=True
) )
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='location'
)
images = GenericRelation( images = GenericRelation(
to='extras.ImageAttachment' to='extras.ImageAttachment'
) )

View File

@ -0,0 +1,45 @@
import graphene
from graphene.types.generic import GenericScalar
__all__ = (
'ChangelogMixin',
'CustomFieldsMixin',
'ImageAttachmentsMixin',
'JournalEntriesMixin',
'TagsMixin',
)
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()
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 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)
def resolve_tags(self, info):
return self.tags.all()

View File

@ -1,5 +1,5 @@
from extras import filtersets, models from extras import filtersets, models
from netbox.graphql.types import BaseObjectType from netbox.graphql.types import BaseObjectType, ObjectType
__all__ = ( __all__ = (
'ConfigContextType', 'ConfigContextType',
@ -8,12 +8,13 @@ __all__ = (
'ExportTemplateType', 'ExportTemplateType',
'ImageAttachmentType', 'ImageAttachmentType',
'JournalEntryType', 'JournalEntryType',
'ObjectChangeType',
'TagType', 'TagType',
'WebhookType', 'WebhookType',
) )
class ConfigContextType(BaseObjectType): class ConfigContextType(ObjectType):
class Meta: class Meta:
model = models.ConfigContext model = models.ConfigContext
@ -21,7 +22,7 @@ class ConfigContextType(BaseObjectType):
filterset_class = filtersets.ConfigContextFilterSet filterset_class = filtersets.ConfigContextFilterSet
class CustomFieldType(BaseObjectType): class CustomFieldType(ObjectType):
class Meta: class Meta:
model = models.CustomField model = models.CustomField
@ -29,7 +30,7 @@ class CustomFieldType(BaseObjectType):
filterset_class = filtersets.CustomFieldFilterSet filterset_class = filtersets.CustomFieldFilterSet
class CustomLinkType(BaseObjectType): class CustomLinkType(ObjectType):
class Meta: class Meta:
model = models.CustomLink model = models.CustomLink
@ -37,7 +38,7 @@ class CustomLinkType(BaseObjectType):
filterset_class = filtersets.CustomLinkFilterSet filterset_class = filtersets.CustomLinkFilterSet
class ExportTemplateType(BaseObjectType): class ExportTemplateType(ObjectType):
class Meta: class Meta:
model = models.ExportTemplate model = models.ExportTemplate
@ -53,7 +54,7 @@ class ImageAttachmentType(BaseObjectType):
filterset_class = filtersets.ImageAttachmentFilterSet filterset_class = filtersets.ImageAttachmentFilterSet
class JournalEntryType(BaseObjectType): class JournalEntryType(ObjectType):
class Meta: class Meta:
model = models.JournalEntry model = models.JournalEntry
@ -61,7 +62,15 @@ class JournalEntryType(BaseObjectType):
filterset_class = filtersets.JournalEntryFilterSet 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: class Meta:
model = models.Tag model = models.Tag
@ -69,7 +78,7 @@ class TagType(BaseObjectType):
filterset_class = filtersets.TagFilterSet filterset_class = filtersets.TagFilterSet
class WebhookType(BaseObjectType): class WebhookType(ObjectType):
class Meta: class Meta:
model = models.Webhook model = models.Webhook

View File

@ -0,0 +1,20 @@
import graphene
__all__ = (
'IPAddressesMixin',
'VLANGroupsMixin',
)
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')
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')

View File

@ -1,5 +1,5 @@
from ipam import filtersets, models from ipam import filtersets, models
from netbox.graphql.types import ObjectType, TaggedObjectType from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType
__all__ = ( __all__ = (
'AggregateType', 'AggregateType',
@ -16,7 +16,7 @@ __all__ = (
) )
class AggregateType(TaggedObjectType): class AggregateType(PrimaryObjectType):
class Meta: class Meta:
model = models.Aggregate model = models.Aggregate
@ -24,7 +24,7 @@ class AggregateType(TaggedObjectType):
filterset_class = filtersets.AggregateFilterSet filterset_class = filtersets.AggregateFilterSet
class IPAddressType(TaggedObjectType): class IPAddressType(PrimaryObjectType):
class Meta: class Meta:
model = models.IPAddress model = models.IPAddress
@ -35,7 +35,7 @@ class IPAddressType(TaggedObjectType):
return self.role or None return self.role or None
class IPRangeType(TaggedObjectType): class IPRangeType(PrimaryObjectType):
class Meta: class Meta:
model = models.IPRange model = models.IPRange
@ -46,7 +46,7 @@ class IPRangeType(TaggedObjectType):
return self.role or None return self.role or None
class PrefixType(TaggedObjectType): class PrefixType(PrimaryObjectType):
class Meta: class Meta:
model = models.Prefix model = models.Prefix
@ -54,7 +54,7 @@ class PrefixType(TaggedObjectType):
filterset_class = filtersets.PrefixFilterSet filterset_class = filtersets.PrefixFilterSet
class RIRType(ObjectType): class RIRType(OrganizationalObjectType):
class Meta: class Meta:
model = models.RIR model = models.RIR
@ -62,7 +62,7 @@ class RIRType(ObjectType):
filterset_class = filtersets.RIRFilterSet filterset_class = filtersets.RIRFilterSet
class RoleType(ObjectType): class RoleType(OrganizationalObjectType):
class Meta: class Meta:
model = models.Role model = models.Role
@ -70,7 +70,7 @@ class RoleType(ObjectType):
filterset_class = filtersets.RoleFilterSet filterset_class = filtersets.RoleFilterSet
class RouteTargetType(TaggedObjectType): class RouteTargetType(PrimaryObjectType):
class Meta: class Meta:
model = models.RouteTarget model = models.RouteTarget
@ -78,7 +78,7 @@ class RouteTargetType(TaggedObjectType):
filterset_class = filtersets.RouteTargetFilterSet filterset_class = filtersets.RouteTargetFilterSet
class ServiceType(TaggedObjectType): class ServiceType(PrimaryObjectType):
class Meta: class Meta:
model = models.Service model = models.Service
@ -86,7 +86,7 @@ class ServiceType(TaggedObjectType):
filterset_class = filtersets.ServiceFilterSet filterset_class = filtersets.ServiceFilterSet
class VLANType(TaggedObjectType): class VLANType(PrimaryObjectType):
class Meta: class Meta:
model = models.VLAN model = models.VLAN
@ -94,7 +94,7 @@ class VLANType(TaggedObjectType):
filterset_class = filtersets.VLANFilterSet filterset_class = filtersets.VLANFilterSet
class VLANGroupType(ObjectType): class VLANGroupType(OrganizationalObjectType):
class Meta: class Meta:
model = models.VLANGroup model = models.VLANGroup
@ -102,7 +102,7 @@ class VLANGroupType(ObjectType):
filterset_class = filtersets.VLANGroupFilterSet filterset_class = filtersets.VLANGroupFilterSet
class VRFType(TaggedObjectType): class VRFType(PrimaryObjectType):
class Meta: class Meta:
model = models.VRF model = models.VRF

View File

@ -1,12 +1,12 @@
import graphene
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from graphene.types.generic import GenericScalar
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from extras.graphql.mixins import ChangelogMixin, CustomFieldsMixin, JournalEntriesMixin, TagsMixin
__all__ = ( __all__ = (
'BaseObjectType', 'BaseObjectType',
'ObjectType', 'OrganizationalObjectType',
'TaggedObjectType', 'PrimaryObjectType',
) )
@ -27,30 +27,41 @@ class BaseObjectType(DjangoObjectType):
return queryset.restrict(info.context.user, 'view') return queryset.restrict(info.context.user, 'view')
class ObjectType(BaseObjectType): class ObjectType(
ChangelogMixin,
BaseObjectType
):
""" """
Extends BaseObjectType with support for custom field data. Base GraphQL object type for unclassified models which support change logging
""" """
custom_fields = GenericScalar()
class Meta: class Meta:
abstract = True abstract = True
def resolve_custom_fields(self, info):
return self.custom_field_data
class OrganizationalObjectType(
class TaggedObjectType(ObjectType): ChangelogMixin,
CustomFieldsMixin,
BaseObjectType
):
""" """
Extends ObjectType with support for Tags Base type for organizational models
""" """
tags = graphene.List(graphene.String)
class Meta: class Meta:
abstract = True abstract = True
def resolve_tags(self, info):
return self.tags.all() class PrimaryObjectType(
ChangelogMixin,
CustomFieldsMixin,
JournalEntriesMixin,
TagsMixin,
BaseObjectType
):
"""
Base type for primary models
"""
class Meta:
abstract = True
# #

View File

@ -40,6 +40,11 @@ class ChangeLoggingMixin(models.Model):
blank=True, blank=True,
null=True null=True
) )
object_changes = GenericRelation(
to='extras.ObjectChange',
content_type_field='changed_object_type',
object_id_field='changed_object_id'
)
class Meta: class Meta:
abstract = True abstract = True

View File

@ -1,5 +1,5 @@
from tenancy import filtersets, models from tenancy import filtersets, models
from netbox.graphql.types import ObjectType, TaggedObjectType from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType
__all__ = ( __all__ = (
'TenantType', 'TenantType',
@ -7,7 +7,7 @@ __all__ = (
) )
class TenantType(TaggedObjectType): class TenantType(PrimaryObjectType):
class Meta: class Meta:
model = models.Tenant model = models.Tenant
@ -15,7 +15,7 @@ class TenantType(TaggedObjectType):
filterset_class = filtersets.TenantFilterSet filterset_class = filtersets.TenantFilterSet
class TenantGroupType(ObjectType): class TenantGroupType(OrganizationalObjectType):
class Meta: class Meta:
model = models.TenantGroup model = models.TenantGroup

View File

@ -5,7 +5,7 @@ from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.urls import reverse from django.urls import reverse
from django.test import override_settings 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 import status
from rest_framework.test import APIClient from rest_framework.test import APIClient
@ -446,9 +446,13 @@ class APIViewTestCases:
# Compile list of fields to include # Compile list of fields to include
fields_string = '' fields_string = ''
for field_name, field in type_class._meta.fields.items(): 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 # Dynamic fields must specify a subselection
fields_string += f'{field_name} {{ id }}\n' 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: else:
fields_string += f'{field_name}\n' fields_string += f'{field_name}\n'

View File

@ -1,5 +1,7 @@
from dcim.graphql.types import ComponentObjectType
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
from virtualization import filtersets, models from virtualization import filtersets, models
from netbox.graphql.types import ObjectType, TaggedObjectType from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType
__all__ = ( __all__ = (
'ClusterType', 'ClusterType',
@ -10,7 +12,7 @@ __all__ = (
) )
class ClusterType(TaggedObjectType): class ClusterType(VLANGroupsMixin, PrimaryObjectType):
class Meta: class Meta:
model = models.Cluster model = models.Cluster
@ -18,7 +20,7 @@ class ClusterType(TaggedObjectType):
filterset_class = filtersets.ClusterFilterSet filterset_class = filtersets.ClusterFilterSet
class ClusterGroupType(ObjectType): class ClusterGroupType(VLANGroupsMixin, OrganizationalObjectType):
class Meta: class Meta:
model = models.ClusterGroup model = models.ClusterGroup
@ -26,7 +28,7 @@ class ClusterGroupType(ObjectType):
filterset_class = filtersets.ClusterGroupFilterSet filterset_class = filtersets.ClusterGroupFilterSet
class ClusterTypeType(ObjectType): class ClusterTypeType(OrganizationalObjectType):
class Meta: class Meta:
model = models.ClusterType model = models.ClusterType
@ -34,7 +36,7 @@ class ClusterTypeType(ObjectType):
filterset_class = filtersets.ClusterTypeFilterSet filterset_class = filtersets.ClusterTypeFilterSet
class VirtualMachineType(TaggedObjectType): class VirtualMachineType(PrimaryObjectType):
class Meta: class Meta:
model = models.VirtualMachine model = models.VirtualMachine
@ -42,7 +44,7 @@ class VirtualMachineType(TaggedObjectType):
filterset_class = filtersets.VirtualMachineFilterSet filterset_class = filtersets.VirtualMachineFilterSet
class VMInterfaceType(TaggedObjectType): class VMInterfaceType(IPAddressesMixin, ComponentObjectType):
class Meta: class Meta:
model = models.VMInterface model = models.VMInterface

View File

@ -81,6 +81,12 @@ class ClusterGroup(OrganizationalModel):
max_length=200, max_length=200,
blank=True 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() objects = RestrictedQuerySet.as_manager()
@ -136,6 +142,12 @@ class Cluster(PrimaryModel):
comments = models.TextField( comments = models.TextField(
blank=True 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() objects = RestrictedQuerySet.as_manager()