mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 12:12:53 -06:00
Merge pull request #6873 from netbox-community/6829-graphql-reverse-relations
Closes #6829: GraphQL reverse generic relations
This commit is contained in:
commit
c411d2a9f1
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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'
|
||||||
)
|
)
|
||||||
|
@ -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'
|
||||||
)
|
)
|
||||||
|
45
netbox/extras/graphql/mixins.py
Normal file
45
netbox/extras/graphql/mixins.py
Normal 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()
|
@ -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
|
||||||
|
20
netbox/ipam/graphql/mixins.py
Normal file
20
netbox/ipam/graphql/mixins.py
Normal 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')
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user