Merge branch 'feature' into 14799-script-data-source

This commit is contained in:
Arthur 2024-03-14 13:59:01 -07:00
commit daf69d4ee8
235 changed files with 6836 additions and 4922 deletions

View File

@ -476,7 +476,7 @@ class NewBranchScript(Script):
name=f'{site.slug}-switch{i}', name=f'{site.slug}-switch{i}',
site=site, site=site,
status=DeviceStatusChoices.STATUS_PLANNED, status=DeviceStatusChoices.STATUS_PLANNED,
device_role=switch_role role=switch_role
) )
switch.full_clean() switch.full_clean()
switch.save() switch.save()

View File

@ -18,9 +18,9 @@ When a device has one or more interfaces with IP addresses assigned, a primary I
The device's configured name. This field is optional; devices can be unnamed. However, if set, the name must be unique to the assigned site and tenant. The device's configured name. This field is optional; devices can be unnamed. However, if set, the name must be unique to the assigned site and tenant.
### Device Role ### Role
The functional [role](./devicerole.md) assigned to this device. The functional [device role](./devicerole.md) assigned to this device.
### Device Type ### Device Type

View File

@ -38,7 +38,7 @@ The type of data this field holds. This must be one of the following:
| Object | A single NetBox object of the type defined by `object_type` | | Object | A single NetBox object of the type defined by `object_type` |
| Multiple object | One or more NetBox objects of the type defined by `object_type` | | Multiple object | One or more NetBox objects of the type defined by `object_type` |
### Object Type ### Related Object Type
For object and multiple-object fields only. Designates the type of NetBox object being referenced. For object and multiple-object fields only. Designates the type of NetBox object being referenced.

View File

@ -13,6 +13,10 @@
The NetBox user interface has been completely refreshed and updated. The NetBox user interface has been completely refreshed and updated.
#### Dynamic REST API Fields ([#15087](https://github.com/netbox-community/netbox/issues/15087))
The REST API now supports specifying which fields to include in the response data.
### Enhancements ### Enhancements
* [#12851](https://github.com/netbox-community/netbox/issues/12851) - Replace bleach HTML sanitization library with nh3 * [#12851](https://github.com/netbox-community/netbox/issues/12851) - Replace bleach HTML sanitization library with nh3
@ -22,17 +26,55 @@ The NetBox user interface has been completely refreshed and updated.
* [#14672](https://github.com/netbox-community/netbox/issues/14672) - Add support for Python 3.12 * [#14672](https://github.com/netbox-community/netbox/issues/14672) - Add support for Python 3.12
* [#14728](https://github.com/netbox-community/netbox/issues/14728) - The plugins list view has been moved from the legacy admin UI to the main NetBox UI * [#14728](https://github.com/netbox-community/netbox/issues/14728) - The plugins list view has been moved from the legacy admin UI to the main NetBox UI
* [#14729](https://github.com/netbox-community/netbox/issues/14729) - All background task views have been moved from the legacy admin UI to the main NetBox UI * [#14729](https://github.com/netbox-community/netbox/issues/14729) - All background task views have been moved from the legacy admin UI to the main NetBox UI
* [#14438](https://github.com/netbox-community/netbox/issues/14438) - Track individual custom scripts as database objects
* [#15131](https://github.com/netbox-community/netbox/issues/15131) - Automatically annotate related object counts on REST API querysets
* [#15238](https://github.com/netbox-community/netbox/issues/15238) - Include the `description` field in "brief" REST API serializations
### Other Changes ### Other Changes
* [#12325](https://github.com/netbox-community/netbox/issues/12325) - The Django admin UI is now disabled by default (set `DJANGO_ADMIN_ENABLED` to True to enable it) * [#12325](https://github.com/netbox-community/netbox/issues/12325) - The Django admin UI is now disabled by default (set `DJANGO_ADMIN_ENABLED` to True to enable it)
* [#12510](https://github.com/netbox-community/netbox/issues/12510) - Dropped support for legacy reports * [#12510](https://github.com/netbox-community/netbox/issues/12510) - Dropped support for legacy reports
* [#12795](https://github.com/netbox-community/netbox/issues/12795) - NetBox now uses a custom User model rather than the stock model provided by Django * [#12795](https://github.com/netbox-community/netbox/issues/12795) - NetBox now uses custom User and Group models rather than the stock models provided by Django
* [#13647](https://github.com/netbox-community/netbox/issues/13647) - Squash all database migrations prior to v3.7 * [#13647](https://github.com/netbox-community/netbox/issues/13647) - Squash all database migrations prior to v3.7
* [#14092](https://github.com/netbox-community/netbox/issues/14092) - Remove backward compatibility for importing plugin resources from `extras.plugins` (now `netbox.plugins`) * [#14092](https://github.com/netbox-community/netbox/issues/14092) - Remove backward compatibility for importing plugin resources from `extras.plugins` (now `netbox.plugins`)
* [#14638](https://github.com/netbox-community/netbox/issues/14638) - Drop support for Python 3.8 and 3.9 * [#14638](https://github.com/netbox-community/netbox/issues/14638) - Drop support for Python 3.8 and 3.9
* [#14657](https://github.com/netbox-community/netbox/issues/14657) - Remove backward compatibility for old permissions mapping under `ActionsMixin` * [#14657](https://github.com/netbox-community/netbox/issues/14657) - Remove backward compatibility for old permissions mapping under `ActionsMixin`
* [#14658](https://github.com/netbox-community/netbox/issues/14658) - Remove backward compatibility for importing `process_webhook()` (now `extras.webhooks.send_webhook()`) * [#14658](https://github.com/netbox-community/netbox/issues/14658) - Remove backward compatibility for importing `process_webhook()` (now `extras.webhooks.send_webhook()`)
* [#14740](https://github.com/netbox-community/netbox/issues/14740) - Remove the obsolete `BootstrapMixin` form mixin class * [#14740](https://github.com/netbox-community/netbox/issues/14740) - Remove the obsolete `BootstrapMixin` form mixin class
* [#15042](https://github.com/netbox-community/netbox/issues/15042) - Rearchitect the logic for registering models & model features
* [#15099](https://github.com/netbox-community/netbox/issues/15099) - Remove obsolete `device_role` and `device_role_id` filters for devices * [#15099](https://github.com/netbox-community/netbox/issues/15099) - Remove obsolete `device_role` and `device_role_id` filters for devices
* [#15100](https://github.com/netbox-community/netbox/issues/15100) - Remove obsolete `NullableCharField` class * [#15100](https://github.com/netbox-community/netbox/issues/15100) - Remove obsolete `NullableCharField` class
* [#15277](https://github.com/netbox-community/netbox/issues/15277) - Replace references to ContentType without ObjectType proxy model & standardize field names
* [#15292](https://github.com/netbox-community/netbox/issues/15292) - Remove obsolete `device_role` attribute from Device model (this field was renamed to `role` in v3.6)
### REST API Changes
* The `/api/extras/content-types/` endpoint has moved to `/api/extras/object-types/`
* dcim.Device
* The obsolete read-only attribute `device_role` has been removed (replaced by `role` in v3.6)
* extras.CustomField
* `content_types` has been renamed to `object_types`
* The `content_types` filter is now `object_type`
* The `content_type_id` filter is now `object_type_id`
* extras.CustomLink
* `content_types` has been renamed to `object_types`
* The `content_types` filter is now `object_type`
* The `content_type_id` filter is now `object_type_id`
* extras.EventRule
* `content_types` has been renamed to `object_types`
* The `content_types` filter is now `object_type`
* The `content_type_id` filter is now `object_type_id`
* extras.ExportTemplate
* `content_types` has been renamed to `object_types`
* The `content_types` filter is now `object_type`
* The `content_type_id` filter is now `object_type_id`
* extras.ImageAttachment
* `content_type` has been renamed to `object_type`
* The `content_type` filter is now `object_type`
* extras.SavedFilter
* `content_types` has been renamed to `object_types`
* The `content_types` filter is now `object_type`
* The `content_type_id` filter is now `object_type_id`
* tenancy.ContactAssignment
* `content_type` has been renamed to `object_type`
* The `content_type_id` filter is now `object_type_id`

View File

@ -1,145 +1,3 @@
from rest_framework import serializers from .serializers_.providers import *
from .serializers_.circuits import *
from circuits.choices import CircuitStatusChoices
from circuits.models import *
from dcim.api.nested_serializers import NestedSiteSerializer
from dcim.api.serializers import CabledObjectSerializer
from ipam.api.nested_serializers import NestedASNSerializer
from ipam.models import ASN
from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer
from .nested_serializers import * from .nested_serializers import *
#
# Providers
#
class ProviderSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
accounts = SerializedPKRelatedField(
queryset=ProviderAccount.objects.all(),
serializer=NestedProviderAccountSerializer,
required=False,
many=True
)
asns = SerializedPKRelatedField(
queryset=ASN.objects.all(),
serializer=NestedASNSerializer,
required=False,
many=True
)
# Related object counts
circuit_count = RelatedObjectCountField('circuits')
class Meta:
model = Provider
fields = [
'id', 'url', 'display', 'name', 'slug', 'accounts', 'description', 'comments', 'asns', 'tags',
'custom_fields', 'created', 'last_updated', 'circuit_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
#
# Provider Accounts
#
class ProviderAccountSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
provider = NestedProviderSerializer()
class Meta:
model = ProviderAccount
fields = [
'id', 'url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'account', 'description')
#
# Provider networks
#
class ProviderNetworkSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
provider = NestedProviderSerializer()
class Meta:
model = ProviderNetwork
fields = [
'id', 'url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags',
'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
#
# Circuits
#
class CircuitTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
# Related object counts
circuit_count = RelatedObjectCountField('circuits')
class Meta:
model = CircuitType
fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
'last_updated', 'circuit_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
site = NestedSiteSerializer(allow_null=True)
provider_network = NestedProviderNetworkSerializer(allow_null=True)
class Meta:
model = CircuitTermination
fields = [
'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
'description',
]
class CircuitSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
provider = NestedProviderSerializer()
provider_account = NestedProviderAccountSerializer(required=False, allow_null=True)
status = ChoiceField(choices=CircuitStatusChoices, required=False)
type = NestedCircuitTypeSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True)
termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
class Meta:
model = Circuit
fields = [
'id', 'url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date',
'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z', 'comments', 'tags',
'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'cid', 'description')
class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
circuit = NestedCircuitSerializer()
site = NestedSiteSerializer(required=False, allow_null=True)
provider_network = NestedProviderNetworkSerializer(required=False, allow_null=True)
class Meta:
model = CircuitTermination
fields = [
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
]
brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied')

View File

@ -0,0 +1,81 @@
from rest_framework import serializers
from circuits.choices import CircuitStatusChoices
from circuits.models import Circuit, CircuitTermination, CircuitType
from dcim.api.serializers_.cables import CabledObjectSerializer
from dcim.api.serializers_.sites import SiteSerializer
from netbox.api.fields import ChoiceField, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
from tenancy.api.serializers_.tenants import TenantSerializer
from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
__all__ = (
'CircuitSerializer',
'CircuitTerminationSerializer',
'CircuitTypeSerializer',
)
class CircuitTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
# Related object counts
circuit_count = RelatedObjectCountField('circuits')
class Meta:
model = CircuitType
fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
'last_updated', 'circuit_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
site = SiteSerializer(nested=True, allow_null=True)
provider_network = ProviderNetworkSerializer(nested=True, allow_null=True)
class Meta:
model = CircuitTermination
fields = [
'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
'description',
]
class CircuitSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
provider = ProviderSerializer(nested=True)
provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True)
status = ChoiceField(choices=CircuitStatusChoices, required=False)
type = CircuitTypeSerializer(nested=True)
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
class Meta:
model = Circuit
fields = [
'id', 'url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date',
'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z', 'comments', 'tags',
'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'cid', 'description')
class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
circuit = CircuitSerializer(nested=True)
site = SiteSerializer(nested=True, required=False, allow_null=True)
provider_network = ProviderNetworkSerializer(nested=True, required=False, allow_null=True)
class Meta:
model = CircuitTermination
fields = [
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
]
brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied')

View File

@ -0,0 +1,68 @@
from rest_framework import serializers
from circuits.models import Provider, ProviderAccount, ProviderNetwork
from ipam.api.serializers_.asns import ASNSerializer
from ipam.models import ASN
from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer
from ..nested_serializers import *
__all__ = (
'ProviderAccountSerializer',
'ProviderNetworkSerializer',
'ProviderSerializer',
)
class ProviderSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
accounts = SerializedPKRelatedField(
queryset=ProviderAccount.objects.all(),
serializer=NestedProviderAccountSerializer,
required=False,
many=True
)
asns = SerializedPKRelatedField(
queryset=ASN.objects.all(),
serializer=ASNSerializer,
nested=True,
required=False,
many=True
)
# Related object counts
circuit_count = RelatedObjectCountField('circuits')
class Meta:
model = Provider
fields = [
'id', 'url', 'display', 'name', 'slug', 'accounts', 'description', 'comments', 'asns', 'tags',
'custom_fields', 'created', 'last_updated', 'circuit_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count')
class ProviderAccountSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
provider = ProviderSerializer(nested=True)
class Meta:
model = ProviderAccount
fields = [
'id', 'url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'account', 'description')
class ProviderNetworkSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
provider = ProviderSerializer(nested=True)
class Meta:
model = ProviderNetwork
fields = [
'id', 'url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags',
'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')

View File

@ -67,7 +67,7 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
class Meta: class Meta:
model = Provider model = Provider
fields = ['id', 'name', 'slug', 'description'] fields = ('id', 'name', 'slug', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -95,7 +95,7 @@ class ProviderAccountFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = ProviderAccount model = ProviderAccount
fields = ['id', 'name', 'account', 'description'] fields = ('id', 'name', 'account', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -122,7 +122,7 @@ class ProviderNetworkFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = ProviderNetwork model = ProviderNetwork
fields = ['id', 'name', 'service_id', 'description'] fields = ('id', 'name', 'service_id', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -139,7 +139,7 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
class Meta: class Meta:
model = CircuitType model = CircuitType
fields = ['id', 'name', 'slug', 'color', 'description'] fields = ('id', 'name', 'slug', 'color', 'description')
class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
@ -158,6 +158,12 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
queryset=ProviderAccount.objects.all(), queryset=ProviderAccount.objects.all(),
label=_('Provider account (ID)'), label=_('Provider account (ID)'),
) )
provider_account = django_filters.ModelMultipleChoiceFilter(
field_name='provider_account__account',
queryset=Provider.objects.all(),
to_field_name='account',
label=_('Provider account (account)'),
)
provider_network_id = django_filters.ModelMultipleChoiceFilter( provider_network_id = django_filters.ModelMultipleChoiceFilter(
field_name='terminations__provider_network', field_name='terminations__provider_network',
queryset=ProviderNetwork.objects.all(), queryset=ProviderNetwork.objects.all(),
@ -214,10 +220,18 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
to_field_name='slug', to_field_name='slug',
label=_('Site (slug)'), label=_('Site (slug)'),
) )
termination_a_id = django_filters.ModelMultipleChoiceFilter(
queryset=CircuitTermination.objects.all(),
label=_('Termination A (ID)'),
)
termination_z_id = django_filters.ModelMultipleChoiceFilter(
queryset=CircuitTermination.objects.all(),
label=_('Termination A (ID)'),
)
class Meta: class Meta:
model = Circuit model = Circuit
fields = ['id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate'] fields = ('id', 'cid', 'description', 'install_date', 'termination_date', 'commit_rate')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -258,7 +272,10 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = ['id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description', 'cable_end'] fields = (
'id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description', 'mark_connected',
'pp_info', 'cable_end',
)
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@ -330,6 +330,7 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests): class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = CircuitTermination.objects.all() queryset = CircuitTermination.objects.all()
filterset = CircuitTerminationFilterSet filterset = CircuitTerminationFilterSet
ignore_fields = ('cable',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):

View File

@ -4,7 +4,7 @@ from core.choices import JobStatusChoices
from core.models import * from core.models import *
from netbox.api.fields import ChoiceField from netbox.api.fields import ChoiceField
from netbox.api.serializers import WritableNestedSerializer from netbox.api.serializers import WritableNestedSerializer
from users.api.nested_serializers import NestedUserSerializer from users.api.serializers import UserSerializer
__all__ = ( __all__ = (
'NestedDataFileSerializer', 'NestedDataFileSerializer',
@ -32,7 +32,8 @@ class NestedDataFileSerializer(WritableNestedSerializer):
class NestedJobSerializer(serializers.ModelSerializer): class NestedJobSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail') url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
status = ChoiceField(choices=JobStatusChoices) status = ChoiceField(choices=JobStatusChoices)
user = NestedUserSerializer( user = UserSerializer(
nested=True,
read_only=True read_only=True
) )

View File

@ -1,74 +1,3 @@
from rest_framework import serializers from .serializers_.data import *
from .serializers_.jobs import *
from core.choices import *
from core.models import *
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer
from netbox.utils import get_data_backend_choices
from users.api.nested_serializers import NestedUserSerializer
from .nested_serializers import * from .nested_serializers import *
__all__ = (
'DataFileSerializer',
'DataSourceSerializer',
'JobSerializer',
)
class DataSourceSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='core-api:datasource-detail'
)
type = ChoiceField(
choices=get_data_backend_choices()
)
status = ChoiceField(
choices=DataSourceStatusChoices,
read_only=True
)
# Related object counts
file_count = RelatedObjectCountField('datafiles')
class Meta:
model = DataSource
fields = [
'id', 'url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', 'comments',
'parameters', 'ignore_rules', 'custom_fields', 'created', 'last_updated', 'file_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
class DataFileSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='core-api:datafile-detail'
)
source = NestedDataSourceSerializer(
read_only=True
)
class Meta:
model = DataFile
fields = [
'id', 'url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
]
brief_fields = ('id', 'url', 'display', 'path')
class JobSerializer(BaseModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
user = NestedUserSerializer(
read_only=True
)
status = ChoiceField(choices=JobStatusChoices, read_only=True)
object_type = ContentTypeField(
read_only=True
)
class Meta:
model = Job
fields = [
'id', 'url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'interval',
'started', 'completed', 'user', 'data', 'error', 'job_id',
]
brief_fields = ('url', 'created', 'completed', 'user', 'status')

View File

View File

@ -0,0 +1,53 @@
from rest_framework import serializers
from core.choices import *
from core.models import DataFile, DataSource
from netbox.api.fields import ChoiceField, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer
from netbox.utils import get_data_backend_choices
__all__ = (
'DataFileSerializer',
'DataSourceSerializer',
)
class DataSourceSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='core-api:datasource-detail'
)
type = ChoiceField(
choices=get_data_backend_choices()
)
status = ChoiceField(
choices=DataSourceStatusChoices,
read_only=True
)
# Related object counts
file_count = RelatedObjectCountField('datafiles')
class Meta:
model = DataSource
fields = [
'id', 'url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', 'comments',
'parameters', 'ignore_rules', 'custom_fields', 'created', 'last_updated', 'file_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
class DataFileSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='core-api:datafile-detail'
)
source = DataSourceSerializer(
nested=True,
read_only=True
)
class Meta:
model = DataFile
fields = [
'id', 'url', 'display', 'source', 'path', 'last_updated', 'size', 'hash',
]
brief_fields = ('id', 'url', 'display', 'path')

View File

@ -0,0 +1,31 @@
from rest_framework import serializers
from core.choices import *
from core.models import Job
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.serializers import BaseModelSerializer
from users.api.serializers_.users import UserSerializer
__all__ = (
'JobSerializer',
)
class JobSerializer(BaseModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
user = UserSerializer(
nested=True,
read_only=True
)
status = ChoiceField(choices=JobStatusChoices, read_only=True)
object_type = ContentTypeField(
read_only=True
)
class Meta:
model = Job
fields = [
'id', 'url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'interval',
'started', 'completed', 'user', 'data', 'error', 'job_id',
]
brief_fields = ('url', 'created', 'completed', 'user', 'status')

View File

@ -28,7 +28,7 @@ class DataSourceFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = DataSource model = DataSource
fields = ('id', 'name', 'enabled', 'description') fields = ('id', 'name', 'enabled', 'description', 'source_url', 'last_synced')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -115,7 +115,7 @@ class JobFilterSet(BaseFilterSet):
class Meta: class Meta:
model = Job model = Job
fields = ('id', 'object_type', 'object_id', 'name', 'interval', 'status', 'user') fields = ('id', 'object_type', 'object_id', 'name', 'interval', 'status', 'user', 'job_id')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -134,9 +134,7 @@ class ConfigRevisionFilterSet(BaseFilterSet):
class Meta: class Meta:
model = ConfigRevision model = ConfigRevision
fields = [ fields = ('id', 'created', 'comment')
'id',
]
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@ -68,7 +68,7 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
) )
object_type = ContentTypeChoiceField( object_type = ContentTypeChoiceField(
label=_('Object Type'), label=_('Object Type'),
queryset=ContentType.objects.with_feature('jobs'), queryset=ObjectType.objects.with_feature('jobs'),
required=False, required=False,
) )
status = forms.MultipleChoiceField( status = forms.MultipleChoiceField(

View File

@ -8,7 +8,7 @@ from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from core.models import ContentType from core.models import ObjectType
APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless') APPS = ('circuits', 'core', 'dcim', 'extras', 'ipam', 'tenancy', 'users', 'virtualization', 'vpn', 'wireless')
@ -60,7 +60,7 @@ class Command(BaseCommand):
pass pass
# Additional objects to include # Additional objects to include
namespace['ContentType'] = ContentType namespace['ObjectType'] = ObjectType
namespace['User'] = get_user_model() namespace['User'] = get_user_model()
# Load convenience commands # Load convenience commands

View File

@ -1,5 +1,3 @@
# Generated by Django 4.2.6 on 2023-10-31 19:38
import core.models.contenttypes import core.models.contenttypes
from django.db import migrations from django.db import migrations
@ -13,7 +11,7 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='ContentType', name='ObjectType',
fields=[ fields=[
], ],
options={ options={
@ -23,7 +21,7 @@ class Migration(migrations.Migration):
}, },
bases=('contenttypes.contenttype',), bases=('contenttypes.contenttype',),
managers=[ managers=[
('objects', core.models.contenttypes.ContentTypeManager()), ('objects', core.models.contenttypes.ObjectTypeManager()),
], ],
), ),
] ]

View File

@ -1,15 +1,15 @@
from django.contrib.contenttypes.models import ContentType as ContentType_, ContentTypeManager as ContentTypeManager_ from django.contrib.contenttypes.models import ContentType, ContentTypeManager
from django.db.models import Q from django.db.models import Q
from netbox.registry import registry from netbox.registry import registry
__all__ = ( __all__ = (
'ContentType', 'ObjectType',
'ContentTypeManager', 'ObjectTypeManager',
) )
class ContentTypeManager(ContentTypeManager_): class ObjectTypeManager(ContentTypeManager):
def public(self): def public(self):
""" """
@ -40,11 +40,11 @@ class ContentTypeManager(ContentTypeManager_):
return self.get_queryset().filter(q) return self.get_queryset().filter(q)
class ContentType(ContentType_): class ObjectType(ContentType):
""" """
Wrap Django's native ContentType model to use our custom manager. Wrap Django's native ContentType model to use our custom manager.
""" """
objects = ContentTypeManager() objects = ObjectTypeManager()
class Meta: class Meta:
proxy = True proxy = True

View File

@ -11,7 +11,7 @@ from django.utils import timezone
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from core.choices import JobStatusChoices from core.choices import JobStatusChoices
from core.models import ContentType from core.models import ObjectType
from core.signals import job_end, job_start from core.signals import job_end, job_start
from extras.constants import EVENT_JOB_END, EVENT_JOB_START from extras.constants import EVENT_JOB_END, EVENT_JOB_START
from netbox.config import get_config from netbox.config import get_config
@ -130,7 +130,7 @@ class Job(models.Model):
super().clean() super().clean()
# Validate the assigned object type # Validate the assigned object type
if self.object_type not in ContentType.objects.with_feature('jobs'): if self.object_type not in ObjectType.objects.with_feature('jobs'):
raise ValidationError( raise ValidationError(
_("Jobs cannot be assigned to this object type ({type}).").format(type=self.object_type) _("Jobs cannot be assigned to this object type ({type}).").format(type=self.object_type)
) )
@ -210,7 +210,7 @@ class Job(models.Model):
schedule_at: Schedule the job to be executed at the passed date and time schedule_at: Schedule the job to be executed at the passed date and time
interval: Recurrence interval (in minutes) interval: Recurrence interval (in minutes)
""" """
object_type = ContentType.objects.get_for_model(instance, for_concrete_model=False) object_type = ObjectType.objects.get_for_model(instance, for_concrete_model=False)
rq_queue_name = get_queue_for_model(object_type.model) rq_queue_name = get_queue_for_model(object_type.model)
queue = django_rq.get_queue(rq_queue_name) queue = django_rq.get_queue(rq_queue_name)
status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING

View File

@ -10,6 +10,7 @@ from ..models import *
class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests): class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = DataSource.objects.all() queryset = DataSource.objects.all()
filterset = DataSourceFilterSet filterset = DataSourceFilterSet
ignore_fields = ('ignore_rules', 'parameters')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -70,6 +71,7 @@ class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests):
class DataFileTestCase(TestCase, ChangeLoggedFilterSetTests): class DataFileTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = DataFile.objects.all() queryset = DataFile.objects.all()
filterset = DataFileFilterSet filterset = DataFileFilterSet
ignore_fields = ('data',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):

View File

@ -6,8 +6,6 @@ from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import WritableNestedSerializer from netbox.api.serializers import WritableNestedSerializer
__all__ = [ __all__ = [
'ComponentNestedModuleSerializer',
'ModuleBayNestedModuleSerializer',
'NestedCableSerializer', 'NestedCableSerializer',
'NestedConsolePortSerializer', 'NestedConsolePortSerializer',
'NestedConsolePortTemplateSerializer', 'NestedConsolePortTemplateSerializer',
@ -311,26 +309,6 @@ class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display', 'name']
class ModuleBayNestedModuleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
class Meta:
model = models.Module
fields = ['id', 'url', 'display', 'serial']
class ComponentNestedModuleSerializer(WritableNestedSerializer):
"""
Used by device component serializers.
"""
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
module_bay = ModuleNestedModuleBaySerializer(read_only=True)
class Meta:
model = models.Module
fields = ['id', 'url', 'display', 'device', 'module_bay']
class NestedModuleSerializer(WritableNestedSerializer): class NestedModuleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)

File diff suppressed because it is too large Load Diff

View File

View File

@ -0,0 +1,37 @@
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from utilities.api import get_serializer_for_model
__all__ = (
'ConnectedEndpointsSerializer',
)
class ConnectedEndpointsSerializer(serializers.ModelSerializer):
"""
Legacy serializer for pre-v3.3 connections
"""
connected_endpoints_type = serializers.SerializerMethodField(read_only=True)
connected_endpoints = serializers.SerializerMethodField(read_only=True)
connected_endpoints_reachable = serializers.SerializerMethodField(read_only=True)
@extend_schema_field(OpenApiTypes.STR)
def get_connected_endpoints_type(self, obj):
if endpoints := obj.connected_endpoints:
return f'{endpoints[0]._meta.app_label}.{endpoints[0]._meta.model_name}'
@extend_schema_field(serializers.ListField)
def get_connected_endpoints(self, obj):
"""
Return the appropriate serializer for the type of connected object.
"""
if endpoints := obj.connected_endpoints:
serializer = get_serializer_for_model(endpoints[0])
context = {'request': self.context['request']}
return serializer(endpoints, nested=True, many=True, context=context).data
@extend_schema_field(serializers.BooleanField)
def get_connected_endpoints_reachable(self, obj):
return obj._path and obj._path.is_complete and obj._path.is_active

View File

@ -0,0 +1,126 @@
from django.contrib.contenttypes.models import ContentType
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from dcim.choices import *
from dcim.constants import *
from dcim.models import Cable, CablePath, CableTermination
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.serializers import GenericObjectSerializer, NetBoxModelSerializer
from tenancy.api.serializers_.tenants import TenantSerializer
from utilities.api import get_serializer_for_model
__all__ = (
'CablePathSerializer',
'CableSerializer',
'CableTerminationSerializer',
'CabledObjectSerializer',
'TracedCableSerializer',
)
class CableSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
a_terminations = GenericObjectSerializer(many=True, required=False)
b_terminations = GenericObjectSerializer(many=True, required=False)
status = ChoiceField(choices=LinkStatusChoices, required=False)
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False, allow_null=True)
class Meta:
model = Cable
fields = [
'id', 'url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'tenant', 'label', 'color',
'length', 'length_unit', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'label', 'description')
class TracedCableSerializer(serializers.ModelSerializer):
"""
Used only while tracing a cable path.
"""
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
class Meta:
model = Cable
fields = [
'id', 'url', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'description',
]
class CableTerminationSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cabletermination-detail')
termination_type = ContentTypeField(
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
)
termination = serializers.SerializerMethodField(read_only=True)
class Meta:
model = CableTermination
fields = [
'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id', 'termination',
'created', 'last_updated',
]
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_termination(self, obj):
serializer = get_serializer_for_model(obj.termination)
context = {'request': self.context['request']}
return serializer(obj.termination, nested=True, context=context).data
class CablePathSerializer(serializers.ModelSerializer):
path = serializers.SerializerMethodField(read_only=True)
class Meta:
model = CablePath
fields = ['id', 'path', 'is_active', 'is_complete', 'is_split']
@extend_schema_field(serializers.ListField)
def get_path(self, obj):
ret = []
for nodes in obj.path_objects:
serializer = get_serializer_for_model(nodes[0])
context = {'request': self.context['request']}
ret.append(serializer(nodes, nested=True, many=True, context=context).data)
return ret
class CabledObjectSerializer(serializers.ModelSerializer):
cable = CableSerializer(nested=True, read_only=True, allow_null=True)
cable_end = serializers.CharField(read_only=True)
link_peers_type = serializers.SerializerMethodField(read_only=True)
link_peers = serializers.SerializerMethodField(read_only=True)
_occupied = serializers.SerializerMethodField(read_only=True)
@extend_schema_field(OpenApiTypes.STR)
def get_link_peers_type(self, obj):
"""
Return the type of the peer link terminations, or None.
"""
if not obj.cable:
return None
if obj.link_peers:
return f'{obj.link_peers[0]._meta.app_label}.{obj.link_peers[0]._meta.model_name}'
return None
@extend_schema_field(serializers.ListField)
def get_link_peers(self, obj):
"""
Return the appropriate serializer for the link termination model.
"""
if not obj.link_peers:
return []
# Return serialized peer termination objects
serializer = get_serializer_for_model(obj.link_peers[0])
context = {'request': self.context['request']}
return serializer(obj.link_peers, nested=True, many=True, context=context).data
@extend_schema_field(serializers.BooleanField)
def get__occupied(self, obj):
return obj._occupied

View File

@ -0,0 +1,368 @@
from django.contrib.contenttypes.models import ContentType
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from dcim.choices import *
from dcim.constants import *
from dcim.models import (
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort,
RearPort, VirtualDeviceContext,
)
from ipam.api.serializers_.vlans import VLANSerializer
from ipam.api.serializers_.vrfs import VRFSerializer
from ipam.models import VLAN
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
from utilities.api import get_serializer_for_model
from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
from wireless.api.nested_serializers import NestedWirelessLinkSerializer
from wireless.api.serializers_.wirelesslans import WirelessLANSerializer
from wireless.choices import *
from wireless.models import WirelessLAN
from .base import ConnectedEndpointsSerializer
from .cables import CabledObjectSerializer
from .devices import DeviceSerializer, ModuleSerializer, VirtualDeviceContextSerializer
from .manufacturers import ManufacturerSerializer
from .roles import InventoryItemRoleSerializer
from ..nested_serializers import *
__all__ = (
'ConsolePortSerializer',
'ConsoleServerPortSerializer',
'DeviceBaySerializer',
'FrontPortSerializer',
'InterfaceSerializer',
'InventoryItemSerializer',
'ModuleBaySerializer',
'PowerOutletSerializer',
'PowerPortSerializer',
'RearPortSerializer',
)
class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
device = DeviceSerializer(nested=True)
module = ModuleSerializer(
nested=True,
fields=('id', 'url', 'display', 'device', 'module_bay'),
required=False,
allow_null=True
)
type = ChoiceField(
choices=ConsolePortTypeChoices,
allow_blank=True,
required=False
)
speed = ChoiceField(
choices=ConsolePortSpeedChoices,
allow_null=True,
required=False
)
class Meta:
model = ConsoleServerPort
fields = [
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
'last_updated', '_occupied',
]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
device = DeviceSerializer(nested=True)
module = ModuleSerializer(
nested=True,
fields=('id', 'url', 'display', 'device', 'module_bay'),
required=False,
allow_null=True
)
type = ChoiceField(
choices=ConsolePortTypeChoices,
allow_blank=True,
required=False
)
speed = ChoiceField(
choices=ConsolePortSpeedChoices,
allow_null=True,
required=False
)
class Meta:
model = ConsolePort
fields = [
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
'last_updated', '_occupied',
]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
device = DeviceSerializer(nested=True)
module = ModuleSerializer(
nested=True,
fields=('id', 'url', 'display', 'device', 'module_bay'),
required=False,
allow_null=True
)
type = ChoiceField(
choices=PowerPortTypeChoices,
allow_blank=True,
required=False,
allow_null=True
)
class Meta:
model = PowerPort
fields = [
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
'created', 'last_updated', '_occupied',
]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
device = DeviceSerializer(nested=True)
module = ModuleSerializer(
nested=True,
fields=('id', 'url', 'display', 'device', 'module_bay'),
required=False,
allow_null=True
)
type = ChoiceField(
choices=PowerOutletTypeChoices,
allow_blank=True,
required=False,
allow_null=True
)
power_port = PowerPortSerializer(
nested=True,
required=False,
allow_null=True
)
feed_leg = ChoiceField(
choices=PowerOutletFeedLegChoices,
allow_blank=True,
required=False,
allow_null=True
)
class Meta:
model = PowerOutlet
fields = [
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg',
'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
'created', 'last_updated', '_occupied',
]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
device = DeviceSerializer(nested=True)
vdcs = SerializedPKRelatedField(
queryset=VirtualDeviceContext.objects.all(),
serializer=VirtualDeviceContextSerializer,
nested=True,
required=False,
many=True
)
module = ModuleSerializer(
nested=True,
fields=('id', 'url', 'display', 'device', 'module_bay'),
required=False,
allow_null=True
)
type = ChoiceField(choices=InterfaceTypeChoices)
parent = NestedInterfaceSerializer(required=False, allow_null=True)
bridge = NestedInterfaceSerializer(required=False, allow_null=True)
lag = NestedInterfaceSerializer(required=False, allow_null=True)
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True)
duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True, allow_null=True)
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True)
rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True)
poe_mode = ChoiceField(choices=InterfacePoEModeChoices, required=False, allow_blank=True)
poe_type = ChoiceField(choices=InterfacePoETypeChoices, required=False, allow_blank=True)
untagged_vlan = VLANSerializer(nested=True, required=False, allow_null=True)
tagged_vlans = SerializedPKRelatedField(
queryset=VLAN.objects.all(),
serializer=VLANSerializer,
nested=True,
required=False,
many=True
)
vrf = VRFSerializer(nested=True, required=False, allow_null=True)
l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True)
wireless_link = NestedWirelessLinkSerializer(read_only=True, allow_null=True)
wireless_lans = SerializedPKRelatedField(
queryset=WirelessLAN.objects.all(),
serializer=WirelessLANSerializer,
nested=True,
required=False,
many=True
)
count_ipaddresses = serializers.IntegerField(read_only=True)
count_fhrp_groups = serializers.IntegerField(read_only=True)
mac_address = serializers.CharField(
required=False,
default=None,
allow_blank=True,
allow_null=True
)
wwn = serializers.CharField(required=False, default=None, allow_blank=True, allow_null=True)
class Meta:
model = Interface
fields = [
'id', 'url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge',
'lag', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role',
'rf_channel', 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers',
'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints',
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
def validate(self, data):
# Validate many-to-many VLAN assignments
if not self.nested:
device = self.instance.device if self.instance else data.get('device')
for vlan in data.get('tagged_vlans', []):
if vlan.site not in [device.site, None]:
raise serializers.ValidationError({
'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent device, "
f"or it must be global."
})
return super().validate(data)
class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
device = DeviceSerializer(nested=True)
module = ModuleSerializer(
nested=True,
fields=('id', 'url', 'display', 'device', 'module_bay'),
required=False,
allow_null=True
)
type = ChoiceField(choices=PortTypeChoices)
class Meta:
model = RearPort
fields = [
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'description',
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created',
'last_updated', '_occupied',
]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
class FrontPortRearPortSerializer(WritableNestedSerializer):
"""
NestedRearPortSerializer but with parent device omitted (since front and rear ports must belong to same device)
"""
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
class Meta:
model = RearPort
fields = ['id', 'url', 'display', 'name', 'label', 'description']
class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
device = DeviceSerializer(nested=True)
module = ModuleSerializer(
nested=True,
fields=('id', 'url', 'display', 'device', 'module_bay'),
required=False,
allow_null=True
)
type = ChoiceField(choices=PortTypeChoices)
rear_port = FrontPortRearPortSerializer()
class Meta:
model = FrontPort
fields = [
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port',
'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
class ModuleBaySerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
device = DeviceSerializer(nested=True)
installed_module = ModuleSerializer(
nested=True,
fields=('id', 'url', 'display', 'serial', 'description'),
required=False,
allow_null=True
)
class Meta:
model = ModuleBay
fields = [
'id', 'url', 'display', 'device', 'name', 'installed_module', 'label', 'position', 'description', 'tags',
'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description')
class DeviceBaySerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
device = DeviceSerializer(nested=True)
installed_device = DeviceSerializer(nested=True, required=False, allow_null=True)
class Meta:
model = DeviceBay
fields = [
'id', 'url', 'display', 'device', 'name', 'label', 'description', 'installed_device', 'tags',
'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description')
class InventoryItemSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
device = DeviceSerializer(nested=True)
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True)
manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True, default=None)
component_type = ContentTypeField(
queryset=ContentType.objects.filter(MODULAR_COMPONENT_MODELS),
required=False,
allow_null=True
)
component = serializers.SerializerMethodField(read_only=True)
_depth = serializers.IntegerField(source='level', read_only=True)
class Meta:
model = InventoryItem
fields = [
'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial',
'asset_tag', 'discovered', 'description', 'component_type', 'component_id', 'component', 'tags',
'custom_fields', 'created', 'last_updated', '_depth',
]
brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', '_depth')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_component(self, obj):
if obj.component is None:
return None
serializer = get_serializer_for_model(obj.component)
context = {'request': self.context['request']}
return serializer(obj.component, nested=True, context=context).data

View File

@ -0,0 +1,157 @@
import decimal
from django.utils.translation import gettext as _
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from dcim.choices import *
from dcim.models import Device, DeviceBay, Module, VirtualDeviceContext
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
from ipam.api.serializers_.ip import IPAddressSerializer
from netbox.api.fields import ChoiceField, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer
from tenancy.api.serializers_.tenants import TenantSerializer
from virtualization.api.serializers_.clusters import ClusterSerializer
from .devicetypes import *
from .platforms import PlatformSerializer
from .racks import RackSerializer
from .roles import DeviceRoleSerializer
from .sites import LocationSerializer, SiteSerializer
from .virtualchassis import VirtualChassisSerializer
from ..nested_serializers import *
__all__ = (
'DeviceSerializer',
'DeviceWithConfigContextSerializer',
'ModuleSerializer',
'VirtualDeviceContextSerializer',
)
class DeviceSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
device_type = DeviceTypeSerializer(nested=True)
role = DeviceRoleSerializer(nested=True)
tenant = TenantSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
platform = PlatformSerializer(nested=True, required=False, allow_null=True)
site = SiteSerializer(nested=True)
location = LocationSerializer(nested=True, required=False, allow_null=True, default=None)
rack = RackSerializer(nested=True, required=False, allow_null=True, default=None)
face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, default=lambda: '')
position = serializers.DecimalField(
max_digits=4,
decimal_places=1,
allow_null=True,
label=_('Position (U)'),
min_value=decimal.Decimal(0.5),
default=None
)
status = ChoiceField(choices=DeviceStatusChoices, required=False)
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False)
primary_ip = IPAddressSerializer(nested=True, read_only=True)
primary_ip4 = IPAddressSerializer(nested=True, required=False, allow_null=True)
primary_ip6 = IPAddressSerializer(nested=True, required=False, allow_null=True)
oob_ip = IPAddressSerializer(nested=True, required=False, allow_null=True)
parent_device = serializers.SerializerMethodField()
cluster = ClusterSerializer(nested=True, required=False, allow_null=True)
virtual_chassis = VirtualChassisSerializer(nested=True, required=False, allow_null=True, default=None)
vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None)
config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
# Counter fields
console_port_count = serializers.IntegerField(read_only=True)
console_server_port_count = serializers.IntegerField(read_only=True)
power_port_count = serializers.IntegerField(read_only=True)
power_outlet_count = serializers.IntegerField(read_only=True)
interface_count = serializers.IntegerField(read_only=True)
front_port_count = serializers.IntegerField(read_only=True)
rear_port_count = serializers.IntegerField(read_only=True)
device_bay_count = serializers.IntegerField(read_only=True)
module_bay_count = serializers.IntegerField(read_only=True)
inventory_item_count = serializers.IntegerField(read_only=True)
class Meta:
model = Device
fields = [
'id', 'url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial', 'asset_tag', 'site',
'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow',
'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position',
'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields',
'created', 'last_updated', 'console_port_count', 'console_server_port_count', 'power_port_count',
'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', 'device_bay_count',
'module_bay_count', 'inventory_item_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
@extend_schema_field(NestedDeviceSerializer)
def get_parent_device(self, obj):
try:
device_bay = obj.parent_bay
except DeviceBay.DoesNotExist:
return None
context = {'request': self.context['request']}
data = NestedDeviceSerializer(instance=device_bay.device, context=context).data
data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
return data
class DeviceWithConfigContextSerializer(DeviceSerializer):
config_context = serializers.SerializerMethodField(read_only=True)
class Meta(DeviceSerializer.Meta):
fields = [
'id', 'url', 'display', 'name', 'device_type', 'role', 'tenant', 'platform', 'serial', 'asset_tag', 'site',
'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow',
'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position',
'vc_priority', 'description', 'comments', 'config_template', 'config_context', 'local_context_data', 'tags',
'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count',
'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count',
'device_bay_count', 'module_bay_count', 'inventory_item_count',
]
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_config_context(self, obj):
return obj.get_config_context()
class VirtualDeviceContextSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail')
device = DeviceSerializer(nested=True)
tenant = TenantSerializer(nested=True, required=False, allow_null=True, default=None)
primary_ip = IPAddressSerializer(nested=True, read_only=True, allow_null=True)
primary_ip4 = IPAddressSerializer(nested=True, required=False, allow_null=True)
primary_ip6 = IPAddressSerializer(nested=True, required=False, allow_null=True)
status = ChoiceField(choices=VirtualDeviceContextStatusChoices)
# Related object counts
interface_count = RelatedObjectCountField('interfaces')
class Meta:
model = VirtualDeviceContext
fields = [
'id', 'url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip', 'primary_ip4',
'primary_ip6', 'status', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'interface_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'identifier', 'device', 'description')
class ModuleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
device = DeviceSerializer(nested=True)
module_bay = NestedModuleBaySerializer()
module_type = ModuleTypeSerializer(nested=True)
status = ChoiceField(choices=ModuleStatusChoices, required=False)
class Meta:
model = Module
fields = [
'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag',
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'device', 'module_bay', 'module_type', 'description')

View File

@ -0,0 +1,327 @@
from django.contrib.contenttypes.models import ContentType
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from dcim.choices import *
from dcim.constants import *
from dcim.models import (
ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate,
InventoryItemTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
)
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.serializers import ValidatedModelSerializer
from utilities.api import get_serializer_for_model
from wireless.choices import *
from .devicetypes import DeviceTypeSerializer, ModuleTypeSerializer
from .manufacturers import ManufacturerSerializer
from .roles import InventoryItemRoleSerializer
from ..nested_serializers import *
__all__ = (
'ConsolePortTemplateSerializer',
'ConsoleServerPortTemplateSerializer',
'DeviceBayTemplateSerializer',
'FrontPortTemplateSerializer',
'InterfaceTemplateSerializer',
'InventoryItemTemplateSerializer',
'ModuleBayTemplateSerializer',
'PowerOutletTemplateSerializer',
'PowerPortTemplateSerializer',
'RearPortTemplateSerializer',
)
class ConsolePortTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
device_type = DeviceTypeSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
module_type = ModuleTypeSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
type = ChoiceField(
choices=ConsolePortTypeChoices,
allow_blank=True,
required=False
)
class Meta:
model = ConsolePortTemplate
fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created',
'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverporttemplate-detail')
device_type = DeviceTypeSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
module_type = ModuleTypeSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
type = ChoiceField(
choices=ConsolePortTypeChoices,
allow_blank=True,
required=False
)
class Meta:
model = ConsoleServerPortTemplate
fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created',
'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
class PowerPortTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail')
device_type = DeviceTypeSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
module_type = ModuleTypeSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
type = ChoiceField(
choices=PowerPortTypeChoices,
allow_blank=True,
required=False,
allow_null=True
)
class Meta:
model = PowerPortTemplate
fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw',
'allocated_draw', 'description', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
class PowerOutletTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlettemplate-detail')
device_type = DeviceTypeSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
module_type = ModuleTypeSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
type = ChoiceField(
choices=PowerOutletTypeChoices,
allow_blank=True,
required=False,
allow_null=True
)
power_port = PowerPortTemplateSerializer(
nested=True,
required=False,
allow_null=True
)
feed_leg = ChoiceField(
choices=PowerOutletFeedLegChoices,
allow_blank=True,
required=False,
allow_null=True
)
class Meta:
model = PowerOutletTemplate
fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg',
'description', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
class InterfaceTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfacetemplate-detail')
device_type = DeviceTypeSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
module_type = ModuleTypeSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
type = ChoiceField(choices=InterfaceTypeChoices)
bridge = NestedInterfaceTemplateSerializer(
required=False,
allow_null=True
)
poe_mode = ChoiceField(
choices=InterfacePoEModeChoices,
required=False,
allow_blank=True,
allow_null=True
)
poe_type = ChoiceField(
choices=InterfacePoETypeChoices,
required=False,
allow_blank=True,
allow_null=True
)
rf_role = ChoiceField(
choices=WirelessRoleChoices,
required=False,
allow_blank=True,
allow_null=True
)
class Meta:
model = InterfaceTemplate
fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only',
'description', 'bridge', 'poe_mode', 'poe_type', 'rf_role', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
class RearPortTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
device_type = DeviceTypeSerializer(
required=False,
nested=True,
allow_null=True,
default=None
)
module_type = ModuleTypeSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
type = ChoiceField(choices=PortTypeChoices)
class Meta:
model = RearPortTemplate
fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions',
'description', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
class FrontPortTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail')
device_type = DeviceTypeSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
module_type = ModuleTypeSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
type = ChoiceField(choices=PortTypeChoices)
rear_port = RearPortTemplateSerializer(nested=True)
class Meta:
model = FrontPortTemplate
fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port',
'rear_port_position', 'description', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
class ModuleBayTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail')
device_type = DeviceTypeSerializer(
nested=True
)
class Meta:
model = ModuleBayTemplate
fields = [
'id', 'url', 'display', 'device_type', 'name', 'label', 'position', 'description', 'created',
'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
class DeviceBayTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail')
device_type = DeviceTypeSerializer(
nested=True
)
class Meta:
model = DeviceBayTemplate
fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated']
brief_fields = ('id', 'url', 'display', 'name', 'description')
class InventoryItemTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtemplate-detail')
device_type = DeviceTypeSerializer(
nested=True
)
parent = serializers.PrimaryKeyRelatedField(
queryset=InventoryItemTemplate.objects.all(),
allow_null=True,
default=None
)
role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True)
manufacturer = ManufacturerSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
component_type = ContentTypeField(
queryset=ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS),
required=False,
allow_null=True
)
component = serializers.SerializerMethodField(read_only=True)
_depth = serializers.IntegerField(source='level', read_only=True)
class Meta:
model = InventoryItemTemplate
fields = [
'id', 'url', 'display', 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id',
'description', 'component_type', 'component_id', 'component', 'created', 'last_updated', '_depth',
]
brief_fields = ('id', 'url', 'display', 'name', 'description', '_depth')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_component(self, obj):
if obj.component is None:
return None
serializer = get_serializer_for_model(obj.component)
context = {'request': self.context['request']}
return serializer(obj.component, nested=True, context=context).data

View File

@ -0,0 +1,74 @@
from django.utils.translation import gettext as _
from rest_framework import serializers
from dcim.choices import *
from dcim.models import DeviceType, ModuleType
from netbox.api.fields import ChoiceField, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer
from .manufacturers import ManufacturerSerializer
from .platforms import PlatformSerializer
__all__ = (
'DeviceTypeSerializer',
'ModuleTypeSerializer',
)
class DeviceTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
manufacturer = ManufacturerSerializer(nested=True)
default_platform = PlatformSerializer(nested=True, required=False, allow_null=True)
u_height = serializers.DecimalField(
max_digits=4,
decimal_places=1,
label=_('Position (U)'),
min_value=0,
default=1.0
)
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False, allow_null=True)
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False, allow_null=True)
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
front_image = serializers.URLField(allow_null=True, required=False)
rear_image = serializers.URLField(allow_null=True, required=False)
# Counter fields
console_port_template_count = serializers.IntegerField(read_only=True)
console_server_port_template_count = serializers.IntegerField(read_only=True)
power_port_template_count = serializers.IntegerField(read_only=True)
power_outlet_template_count = serializers.IntegerField(read_only=True)
interface_template_count = serializers.IntegerField(read_only=True)
front_port_template_count = serializers.IntegerField(read_only=True)
rear_port_template_count = serializers.IntegerField(read_only=True)
device_bay_template_count = serializers.IntegerField(read_only=True)
module_bay_template_count = serializers.IntegerField(read_only=True)
inventory_item_template_count = serializers.IntegerField(read_only=True)
# Related object counts
device_count = RelatedObjectCountField('instances')
class Meta:
model = DeviceType
fields = [
'id', 'url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height',
'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
'front_image', 'rear_image', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'device_count', 'console_port_template_count', 'console_server_port_template_count',
'power_port_template_count', 'power_outlet_template_count', 'interface_template_count',
'front_port_template_count', 'rear_port_template_count', 'device_bay_template_count',
'module_bay_template_count', 'inventory_item_template_count',
]
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'device_count')
class ModuleTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
manufacturer = ManufacturerSerializer(nested=True)
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
class Meta:
model = ModuleType
fields = [
'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description',
'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'description')

View File

@ -0,0 +1,26 @@
from rest_framework import serializers
from dcim.models import Manufacturer
from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer
__all__ = (
'ManufacturerSerializer',
)
class ManufacturerSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
# Related object counts
devicetype_count = RelatedObjectCountField('device_types')
inventoryitem_count = RelatedObjectCountField('inventory_items')
platform_count = RelatedObjectCountField('platforms')
class Meta:
model = Manufacturer
fields = [
'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
'devicetype_count', 'inventoryitem_count', 'platform_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count')

View File

@ -0,0 +1,29 @@
from rest_framework import serializers
from dcim.models import Platform
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer
from .manufacturers import ManufacturerSerializer
__all__ = (
'PlatformSerializer',
)
class PlatformSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True)
config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
# Related object counts
device_count = RelatedObjectCountField('devices')
virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta:
model = Platform
fields = [
'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count')

View File

@ -0,0 +1,80 @@
from rest_framework import serializers
from dcim.choices import *
from dcim.models import PowerFeed, PowerPanel
from netbox.api.fields import ChoiceField, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer
from tenancy.api.serializers_.tenants import TenantSerializer
from .base import ConnectedEndpointsSerializer
from .cables import CabledObjectSerializer
from .racks import RackSerializer
from .sites import LocationSerializer, SiteSerializer
__all__ = (
'PowerFeedSerializer',
'PowerPanelSerializer',
)
class PowerPanelSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
site = SiteSerializer(nested=True)
location = LocationSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
# Related object counts
powerfeed_count = RelatedObjectCountField('powerfeeds')
class Meta:
model = PowerPanel
fields = [
'id', 'url', 'display', 'site', 'location', 'name', 'description', 'comments', 'tags', 'custom_fields',
'powerfeed_count', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description', 'powerfeed_count')
class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
power_panel = PowerPanelSerializer(nested=True)
rack = RackSerializer(
nested=True,
required=False,
allow_null=True,
default=None
)
type = ChoiceField(
choices=PowerFeedTypeChoices,
default=lambda: PowerFeedTypeChoices.TYPE_PRIMARY,
)
status = ChoiceField(
choices=PowerFeedStatusChoices,
default=lambda: PowerFeedStatusChoices.STATUS_ACTIVE,
)
supply = ChoiceField(
choices=PowerFeedSupplyChoices,
default=lambda: PowerFeedSupplyChoices.SUPPLY_AC,
)
phase = ChoiceField(
choices=PowerFeedPhaseChoices,
default=lambda: PowerFeedPhaseChoices.PHASE_SINGLE,
)
tenant = TenantSerializer(
nested=True,
required=False,
allow_null=True
)
class Meta:
model = PowerFeed
fields = [
'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'description',
'tenant', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
]
brief_fields = ('id', 'url', 'display', 'name', 'description', 'cable', '_occupied')

View File

@ -0,0 +1,117 @@
from django.utils.translation import gettext as _
from rest_framework import serializers
from dcim.choices import *
from dcim.constants import *
from dcim.models import Rack, RackReservation, RackRole
from netbox.api.fields import ChoiceField, RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer
from netbox.config import ConfigItem
from tenancy.api.serializers_.tenants import TenantSerializer
from users.api.serializers_.users import UserSerializer
from .sites import LocationSerializer, SiteSerializer
__all__ = (
'RackElevationDetailFilterSerializer',
'RackReservationSerializer',
'RackRoleSerializer',
'RackSerializer',
)
class RackRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
# Related object counts
rack_count = RelatedObjectCountField('racks')
class Meta:
model = RackRole
fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
'last_updated', 'rack_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count')
class RackSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
site = SiteSerializer(nested=True)
location = LocationSerializer(nested=True, required=False, allow_null=True, default=None)
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
status = ChoiceField(choices=RackStatusChoices, required=False)
role = RackRoleSerializer(nested=True, required=False, allow_null=True)
type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False, allow_null=True)
facility_id = serializers.CharField(max_length=50, allow_blank=True, allow_null=True, label=_('Facility ID'),
default=None)
width = ChoiceField(choices=RackWidthChoices, required=False)
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False, allow_null=True)
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
# Related object counts
device_count = RelatedObjectCountField('devices')
powerfeed_count = RelatedObjectCountField('powerfeeds')
class Meta:
model = Rack
fields = [
'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial',
'asset_tag', 'type', 'width', 'u_height', 'starting_unit', 'weight', 'max_weight', 'weight_unit',
'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'description', 'comments',
'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count')
class RackReservationSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
rack = RackSerializer(nested=True)
user = UserSerializer(nested=True)
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
class Meta:
model = RackReservation
fields = [
'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description',
'comments', 'tags', 'custom_fields',
]
brief_fields = ('id', 'url', 'display', 'user', 'description', 'units')
class RackElevationDetailFilterSerializer(serializers.Serializer):
q = serializers.CharField(
required=False,
default=None
)
face = serializers.ChoiceField(
choices=DeviceFaceChoices,
default=DeviceFaceChoices.FACE_FRONT
)
render = serializers.ChoiceField(
choices=RackElevationDetailRenderChoices,
default=RackElevationDetailRenderChoices.RENDER_JSON
)
unit_width = serializers.IntegerField(
default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_WIDTH')
)
unit_height = serializers.IntegerField(
default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT')
)
legend_width = serializers.IntegerField(
default=RACK_ELEVATION_DEFAULT_LEGEND_WIDTH
)
margin_width = serializers.IntegerField(
default=RACK_ELEVATION_DEFAULT_MARGIN_WIDTH
)
exclude = serializers.IntegerField(
required=False,
default=None
)
expand_devices = serializers.BooleanField(
required=False,
default=True
)
include_images = serializers.BooleanField(
required=False,
default=True
)

View File

@ -0,0 +1,31 @@
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from dcim.choices import *
from netbox.api.fields import ChoiceField
from .devices import DeviceSerializer
__all__ = (
'RackUnitSerializer',
)
class RackUnitSerializer(serializers.Serializer):
"""
A rack unit is an abstraction formed by the set (rack, position, face); it does not exist as a row in the database.
"""
id = serializers.DecimalField(
max_digits=4,
decimal_places=1,
read_only=True
)
name = serializers.CharField(read_only=True)
face = ChoiceField(choices=DeviceFaceChoices, read_only=True)
device = DeviceSerializer(nested=True, read_only=True)
occupied = serializers.BooleanField(read_only=True)
display = serializers.SerializerMethodField(read_only=True)
@extend_schema_field(OpenApiTypes.STR)
def get_display(self, obj):
return obj['name']

View File

@ -0,0 +1,43 @@
from rest_framework import serializers
from dcim.models import DeviceRole, InventoryItemRole
from extras.api.serializers_.configtemplates import ConfigTemplateSerializer
from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import NetBoxModelSerializer
__all__ = (
'DeviceRoleSerializer',
'InventoryItemRoleSerializer',
)
class DeviceRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
# Related object counts
device_count = RelatedObjectCountField('devices')
virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta:
model = DeviceRole
fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags',
'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count')
class InventoryItemRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
# Related object counts
inventoryitem_count = RelatedObjectCountField('inventory_items')
class Meta:
model = InventoryItemRole
fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
'last_updated', 'inventoryitem_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'inventoryitem_count')

View File

@ -0,0 +1,98 @@
from rest_framework import serializers
from timezone_field.rest_framework import TimeZoneSerializerField
from dcim.choices import *
from dcim.models import Location, Region, Site, SiteGroup
from ipam.api.serializers_.asns import ASNSerializer
from ipam.models import ASN
from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
from tenancy.api.serializers_.tenants import TenantSerializer
from ..nested_serializers import *
__all__ = (
'LocationSerializer',
'RegionSerializer',
'SiteGroupSerializer',
'SiteSerializer',
)
class RegionSerializer(NestedGroupModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
parent = NestedRegionSerializer(required=False, allow_null=True, default=None)
site_count = serializers.IntegerField(read_only=True)
class Meta:
model = Region
fields = [
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
'last_updated', 'site_count', '_depth',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
class SiteGroupSerializer(NestedGroupModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None)
site_count = serializers.IntegerField(read_only=True)
class Meta:
model = SiteGroup
fields = [
'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
'last_updated', 'site_count', '_depth',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
class SiteSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
status = ChoiceField(choices=SiteStatusChoices, required=False)
region = RegionSerializer(nested=True, required=False, allow_null=True)
group = SiteGroupSerializer(nested=True, required=False, allow_null=True)
tenant = TenantSerializer(required=False, allow_null=True)
time_zone = TimeZoneSerializerField(required=False, allow_null=True)
asns = SerializedPKRelatedField(
queryset=ASN.objects.all(),
serializer=ASNSerializer,
nested=True,
required=False,
many=True
)
# Related object counts
circuit_count = RelatedObjectCountField('circuit_terminations')
device_count = RelatedObjectCountField('devices')
prefix_count = RelatedObjectCountField('prefixes')
rack_count = RelatedObjectCountField('racks')
vlan_count = RelatedObjectCountField('vlans')
virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta:
model = Site
fields = [
'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone',
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'asns', 'tags',
'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'rack_count',
'virtualmachine_count', 'vlan_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'description', 'slug')
class LocationSerializer(NestedGroupModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
site = SiteSerializer(nested=True)
parent = NestedLocationSerializer(required=False, allow_null=True)
status = ChoiceField(choices=LocationStatusChoices, required=False)
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
rack_count = serializers.IntegerField(read_only=True)
device_count = serializers.IntegerField(read_only=True)
class Meta:
model = Location
fields = [
'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'description', 'tags',
'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth')

View File

@ -0,0 +1,26 @@
from rest_framework import serializers
from dcim.models import VirtualChassis
from netbox.api.serializers import NetBoxModelSerializer
from ..nested_serializers import *
__all__ = (
'VirtualChassisSerializer',
)
class VirtualChassisSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
master = NestedDeviceSerializer(required=False, allow_null=True, default=None)
members = NestedDeviceSerializer(many=True, read_only=True)
# Counter fields
member_count = serializers.IntegerField(read_only=True)
class Meta:
model = VirtualChassis
fields = [
'id', 'url', 'display', 'name', 'domain', 'master', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated', 'member_count', 'members',
]
brief_fields = ('id', 'url', 'display', 'name', 'master', 'description', 'member_count')

View File

@ -7,7 +7,6 @@ from rest_framework.response import Response
from rest_framework.routers import APIRootView from rest_framework.routers import APIRootView
from rest_framework.viewsets import ViewSet from rest_framework.viewsets import ViewSet
from circuits.models import Circuit
from dcim import filtersets from dcim import filtersets
from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
from dcim.models import * from dcim.models import *
@ -18,10 +17,8 @@ from netbox.api.metadata import ContentTypeMetadata
from netbox.api.pagination import StripCountAnnotationsPaginator from netbox.api.pagination import StripCountAnnotationsPaginator
from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin
from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin
from netbox.constants import NESTED_SERIALIZER_PREFIX
from utilities.api import get_serializer_for_model from utilities.api import get_serializer_for_model
from utilities.query_functions import CollateAsChar from utilities.query_functions import CollateAsChar
from utilities.utils import count_related
from . import serializers from . import serializers
from .exceptions import MissingFilterException from .exceptions import MissingFilterException
@ -60,16 +57,16 @@ class PathEndpointMixin(object):
# Serialize path objects, iterating over each three-tuple in the path # Serialize path objects, iterating over each three-tuple in the path
for near_ends, cable, far_ends in obj.trace(): for near_ends, cable, far_ends in obj.trace():
if near_ends: if near_ends:
serializer_a = get_serializer_for_model(near_ends[0], prefix=NESTED_SERIALIZER_PREFIX) serializer_a = get_serializer_for_model(near_ends[0])
near_ends = serializer_a(near_ends, many=True, context={'request': request}).data near_ends = serializer_a(near_ends, nested=True, many=True, context={'request': request}).data
else: else:
# Path is split; stop here # Path is split; stop here
break break
if cable: if cable:
cable = serializers.TracedCableSerializer(cable[0], context={'request': request}).data cable = serializers.TracedCableSerializer(cable[0], context={'request': request}).data
if far_ends: if far_ends:
serializer_b = get_serializer_for_model(far_ends[0], prefix=NESTED_SERIALIZER_PREFIX) serializer_b = get_serializer_for_model(far_ends[0])
far_ends = serializer_b(far_ends, many=True, context={'request': request}).data far_ends = serializer_b(far_ends, nested=True, many=True, context={'request': request}).data
path.append((near_ends, cable, far_ends)) path.append((near_ends, cable, far_ends))
@ -514,7 +511,10 @@ class CableTerminationViewSet(NetBoxModelViewSet):
# #
class VirtualChassisViewSet(NetBoxModelViewSet): class VirtualChassisViewSet(NetBoxModelViewSet):
queryset = VirtualChassis.objects.all() queryset = VirtualChassis.objects.prefetch_related(
# Prefetch related object for the display of unnamed devices
'master__virtual_chassis',
)
serializer_class = serializers.VirtualChassisSerializer serializer_class = serializers.VirtualChassisSerializer
filterset_class = filtersets.VirtualChassisFilterSet filterset_class = filtersets.VirtualChassisFilterSet

View File

@ -18,11 +18,12 @@ from tenancy.models import *
from utilities.choices import ColorChoices from utilities.choices import ColorChoices
from utilities.filters import ( from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter, ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
TreeNodeMultipleChoiceFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
) )
from virtualization.models import Cluster from virtualization.models import Cluster
from vpn.models import L2VPN from vpn.models import L2VPN
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
from wireless.models import WirelessLAN, WirelessLink
from .choices import * from .choices import *
from .constants import * from .constants import *
from .models import * from .models import *
@ -89,10 +90,23 @@ class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
to_field_name='slug', to_field_name='slug',
label=_('Parent region (slug)'), label=_('Parent region (slug)'),
) )
ancestor_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='parent',
lookup_expr='in',
label=_('Region (ID)'),
)
ancestor = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='parent',
lookup_expr='in',
to_field_name='slug',
label=_('Region (slug)'),
)
class Meta: class Meta:
model = Region model = Region
fields = ['id', 'name', 'slug', 'description'] fields = ('id', 'name', 'slug', 'description')
class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet): class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
@ -106,10 +120,23 @@ class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
to_field_name='slug', to_field_name='slug',
label=_('Parent site group (slug)'), label=_('Parent site group (slug)'),
) )
ancestor_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='parent',
lookup_expr='in',
label=_('Site group (ID)'),
)
ancestor = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(),
field_name='parent',
lookup_expr='in',
to_field_name='slug',
label=_('Site group (slug)'),
)
class Meta: class Meta:
model = SiteGroup model = SiteGroup
fields = ['id', 'name', 'slug', 'description'] fields = ('id', 'name', 'slug', 'description')
class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
@ -152,12 +179,11 @@ class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
queryset=ASN.objects.all(), queryset=ASN.objects.all(),
label=_('AS (ID)'), label=_('AS (ID)'),
) )
time_zone = MultiValueCharFilter()
class Meta: class Meta:
model = Site model = Site
fields = ( fields = ('id', 'name', 'slug', 'facility', 'latitude', 'longitude', 'description')
'id', 'name', 'slug', 'facility', 'latitude', 'longitude', 'description'
)
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -214,13 +240,23 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM
to_field_name='slug', to_field_name='slug',
label=_('Site (slug)'), label=_('Site (slug)'),
) )
parent_id = TreeNodeMultipleChoiceFilter( parent_id = django_filters.ModelMultipleChoiceFilter(
queryset=Location.objects.all(),
label=_('Parent location (ID)'),
)
parent = django_filters.ModelMultipleChoiceFilter(
field_name='parent__slug',
queryset=Location.objects.all(),
to_field_name='slug',
label=_('Parent location (slug)'),
)
ancestor_id = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(), queryset=Location.objects.all(),
field_name='parent', field_name='parent',
lookup_expr='in', lookup_expr='in',
label=_('Location (ID)'), label=_('Location (ID)'),
) )
parent = TreeNodeMultipleChoiceFilter( ancestor = TreeNodeMultipleChoiceFilter(
queryset=Location.objects.all(), queryset=Location.objects.all(),
field_name='parent', field_name='parent',
lookup_expr='in', lookup_expr='in',
@ -234,7 +270,7 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM
class Meta: class Meta:
model = Location model = Location
fields = ['id', 'name', 'slug', 'status', 'description'] fields = ('id', 'name', 'slug', 'status', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -249,7 +285,7 @@ class RackRoleFilterSet(OrganizationalModelFilterSet):
class Meta: class Meta:
model = RackRole model = RackRole
fields = ['id', 'name', 'slug', 'color', 'description'] fields = ('id', 'name', 'slug', 'color', 'description')
class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet): class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
@ -328,10 +364,10 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
class Meta: class Meta:
model = Rack model = Rack
fields = [ fields = (
'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'starting_unit', 'desc_units', 'outer_width', 'id', 'name', 'facility_id', 'asset_tag', 'u_height', 'starting_unit', 'desc_units', 'outer_width',
'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description', 'outer_depth', 'outer_unit', 'mounting_depth', 'weight', 'max_weight', 'weight_unit', 'description',
] )
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -411,10 +447,14 @@ class RackReservationFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
to_field_name='username', to_field_name='username',
label=_('User (name)'), label=_('User (name)'),
) )
unit = NumericArrayFilter(
field_name='units',
lookup_expr='contains'
)
class Meta: class Meta:
model = RackReservation model = RackReservation
fields = ['id', 'created', 'description'] fields = ('id', 'created', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -431,7 +471,7 @@ class ManufacturerFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet)
class Meta: class Meta:
model = Manufacturer model = Manufacturer
fields = ['id', 'name', 'slug', 'description'] fields = ('id', 'name', 'slug', 'description')
class DeviceTypeFilterSet(NetBoxModelFilterSet): class DeviceTypeFilterSet(NetBoxModelFilterSet):
@ -502,10 +542,22 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = DeviceType model = DeviceType
fields = [ fields = (
'id', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth', 'id', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth',
'subdevice_role', 'airflow', 'weight', 'weight_unit', 'description', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'description',
]
# Counters
'console_port_template_count',
'console_server_port_template_count',
'power_port_template_count',
'power_outlet_template_count',
'interface_template_count',
'front_port_template_count',
'rear_port_template_count',
'device_bay_template_count',
'module_bay_template_count',
'inventory_item_template_count',
)
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -599,7 +651,7 @@ class ModuleTypeFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = ModuleType model = ModuleType
fields = ['id', 'model', 'part_number', 'weight', 'weight_unit', 'description'] fields = ('id', 'model', 'part_number', 'weight', 'weight_unit', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -639,12 +691,15 @@ class DeviceTypeComponentFilterSet(django_filters.FilterSet):
method='search', method='search',
label=_('Search'), label=_('Search'),
) )
devicetype_id = django_filters.ModelMultipleChoiceFilter( device_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
field_name='device_type_id', field_name='device_type_id',
label=_('Device type (ID)'), label=_('Device type (ID)'),
) )
# TODO: Remove in v4.1
devicetype_id = device_type_id
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
return queryset return queryset
@ -655,32 +710,35 @@ class DeviceTypeComponentFilterSet(django_filters.FilterSet):
class ModularDeviceTypeComponentFilterSet(DeviceTypeComponentFilterSet): class ModularDeviceTypeComponentFilterSet(DeviceTypeComponentFilterSet):
moduletype_id = django_filters.ModelMultipleChoiceFilter( module_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=ModuleType.objects.all(), queryset=ModuleType.objects.all(),
field_name='module_type_id', field_name='module_type_id',
label=_('Module type (ID)'), label=_('Module type (ID)'),
) )
# TODO: Remove in v4.1
moduletype_id = module_type_id
class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
fields = ['id', 'name', 'type', 'description'] fields = ('id', 'name', 'label', 'type', 'description')
class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
fields = ['id', 'name', 'type', 'description'] fields = ('id', 'name', 'label', 'type', 'description')
class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
fields = ['id', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description'] fields = ('id', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
@ -688,10 +746,14 @@ class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceType
choices=PowerOutletFeedLegChoices, choices=PowerOutletFeedLegChoices,
null_value=None null_value=None
) )
power_port_id = django_filters.ModelMultipleChoiceFilter(
queryset=PowerPortTemplate.objects.all(),
label=_('Power port (ID)'),
)
class Meta: class Meta:
model = PowerOutletTemplate model = PowerOutletTemplate
fields = ['id', 'name', 'type', 'feed_leg', 'description'] fields = ('id', 'name', 'label', 'type', 'feed_leg', 'description')
class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
@ -715,7 +777,7 @@ class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
class Meta: class Meta:
model = InterfaceTemplate model = InterfaceTemplate
fields = ['id', 'name', 'type', 'enabled', 'mgmt_only', 'description'] fields = ('id', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description')
class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
@ -723,10 +785,13 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
choices=PortTypeChoices, choices=PortTypeChoices,
null_value=None null_value=None
) )
rear_port_id = django_filters.ModelMultipleChoiceFilter(
queryset=RearPort.objects.all()
)
class Meta: class Meta:
model = FrontPortTemplate model = FrontPortTemplate
fields = ['id', 'name', 'type', 'color', 'description'] fields = ('id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description')
class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
@ -737,21 +802,21 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCom
class Meta: class Meta:
model = RearPortTemplate model = RearPortTemplate
fields = ['id', 'name', 'type', 'color', 'positions', 'description'] fields = ('id', 'name', 'label', 'type', 'color', 'positions', 'description')
class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = ModuleBayTemplate model = ModuleBayTemplate
fields = ['id', 'name', 'description'] fields = ('id', 'name', 'label', 'position', 'description')
class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
class Meta: class Meta:
model = DeviceBayTemplate model = DeviceBayTemplate
fields = ['id', 'name', 'description'] fields = ('id', 'name', 'label', 'description')
class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet): class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
@ -784,7 +849,7 @@ class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeCompo
class Meta: class Meta:
model = InventoryItemTemplate model = InventoryItemTemplate
fields = ['id', 'name', 'label', 'part_id', 'description'] fields = ('id', 'name', 'label', 'part_id', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -805,7 +870,7 @@ class DeviceRoleFilterSet(OrganizationalModelFilterSet):
class Meta: class Meta:
model = DeviceRole model = DeviceRole
fields = ['id', 'name', 'slug', 'color', 'vm_role', 'description'] fields = ('id', 'name', 'slug', 'color', 'vm_role', 'description')
class PlatformFilterSet(OrganizationalModelFilterSet): class PlatformFilterSet(OrganizationalModelFilterSet):
@ -831,7 +896,7 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
class Meta: class Meta:
model = Platform model = Platform
fields = ['id', 'name', 'slug', 'description'] fields = ('id', 'name', 'slug', 'description')
@extend_schema_field(OpenApiTypes.STR) @extend_schema_field(OpenApiTypes.STR)
def get_for_device_type(self, queryset, name, value): def get_for_device_type(self, queryset, name, value):
@ -943,6 +1008,11 @@ class DeviceFilterSet(
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
label=_('Rack (ID)'), label=_('Rack (ID)'),
) )
parent_bay_id = django_filters.ModelMultipleChoiceFilter(
field_name='parent_bay',
queryset=DeviceBay.objects.all(),
label=_('Parent bay (ID)'),
)
cluster_id = django_filters.ModelMultipleChoiceFilter( cluster_id = django_filters.ModelMultipleChoiceFilter(
queryset=Cluster.objects.all(), queryset=Cluster.objects.all(),
label=_('VM cluster (ID)'), label=_('VM cluster (ID)'),
@ -1032,10 +1102,22 @@ class DeviceFilterSet(
class Meta: class Meta:
model = Device model = Device
fields = [ fields = (
'id', 'asset_tag', 'face', 'position', 'latitude', 'longitude', 'airflow', 'vc_position', 'vc_priority', 'id', 'asset_tag', 'face', 'position', 'latitude', 'longitude', 'airflow', 'vc_position', 'vc_priority',
'description', 'description',
]
# Counters
'console_port_count',
'console_server_port_count',
'power_port_count',
'power_outlet_count',
'interface_count',
'front_port_count',
'rear_port_count',
'device_bay_count',
'module_bay_count',
'inventory_item_count',
)
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -1098,24 +1180,29 @@ class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet, Prim
device_id = django_filters.ModelMultipleChoiceFilter( device_id = django_filters.ModelMultipleChoiceFilter(
field_name='device', field_name='device',
queryset=Device.objects.all(), queryset=Device.objects.all(),
label='VDC (ID)', label=_('VDC (ID)')
) )
device = django_filters.ModelMultipleChoiceFilter( device = django_filters.ModelMultipleChoiceFilter(
field_name='device', field_name='device',
queryset=Device.objects.all(), queryset=Device.objects.all(),
label='Device model', label=_('Device model')
)
interface_id = django_filters.ModelMultipleChoiceFilter(
field_name='interfaces',
queryset=Interface.objects.all(),
label=_('Interface (ID)')
) )
status = django_filters.MultipleChoiceFilter( status = django_filters.MultipleChoiceFilter(
choices=VirtualDeviceContextStatusChoices choices=VirtualDeviceContextStatusChoices
) )
has_primary_ip = django_filters.BooleanFilter( has_primary_ip = django_filters.BooleanFilter(
method='_has_primary_ip', method='_has_primary_ip',
label='Has a primary IP', label=_('Has a primary IP')
) )
class Meta: class Meta:
model = VirtualDeviceContext model = VirtualDeviceContext
fields = ['id', 'device', 'name', 'description'] fields = ('id', 'device', 'name', 'identifier', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -1181,7 +1268,7 @@ class ModuleFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = Module model = Module
fields = ['id', 'status', 'asset_tag', 'description'] fields = ('id', 'status', 'asset_tag', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -1325,6 +1412,10 @@ class ModularDeviceComponentFilterSet(DeviceComponentFilterSet):
class CabledObjectFilterSet(django_filters.FilterSet): class CabledObjectFilterSet(django_filters.FilterSet):
cable_id = django_filters.ModelMultipleChoiceFilter(
queryset=Cable.objects.all(),
label=_('Cable (ID)'),
)
cabled = django_filters.BooleanFilter( cabled = django_filters.BooleanFilter(
field_name='cable', field_name='cable',
lookup_expr='isnull', lookup_expr='isnull',
@ -1366,7 +1457,7 @@ class ConsolePortFilterSet(
class Meta: class Meta:
model = ConsolePort model = ConsolePort
fields = ['id', 'name', 'label', 'description', 'cable_end'] fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end')
class ConsoleServerPortFilterSet( class ConsoleServerPortFilterSet(
@ -1382,7 +1473,7 @@ class ConsoleServerPortFilterSet(
class Meta: class Meta:
model = ConsoleServerPort model = ConsoleServerPort
fields = ['id', 'name', 'label', 'description', 'cable_end'] fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end')
class PowerPortFilterSet( class PowerPortFilterSet(
@ -1398,7 +1489,9 @@ class PowerPortFilterSet(
class Meta: class Meta:
model = PowerPort model = PowerPort
fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description', 'cable_end'] fields = (
'id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description', 'mark_connected', 'cable_end',
)
class PowerOutletFilterSet( class PowerOutletFilterSet(
@ -1415,10 +1508,16 @@ class PowerOutletFilterSet(
choices=PowerOutletFeedLegChoices, choices=PowerOutletFeedLegChoices,
null_value=None null_value=None
) )
power_port_id = django_filters.ModelMultipleChoiceFilter(
queryset=PowerPort.objects.all(),
label=_('Power port (ID)'),
)
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = ['id', 'name', 'label', 'feed_leg', 'description', 'cable_end'] fields = (
'id', 'name', 'label', 'feed_leg', 'description', 'mark_connected', 'cable_end',
)
class CommonInterfaceFilterSet(django_filters.FilterSet): class CommonInterfaceFilterSet(django_filters.FilterSet):
@ -1533,27 +1632,37 @@ class InterfaceFilterSet(
vdc_id = django_filters.ModelMultipleChoiceFilter( vdc_id = django_filters.ModelMultipleChoiceFilter(
field_name='vdcs', field_name='vdcs',
queryset=VirtualDeviceContext.objects.all(), queryset=VirtualDeviceContext.objects.all(),
label='Virtual Device Context', label=_('Virtual Device Context')
) )
vdc_identifier = django_filters.ModelMultipleChoiceFilter( vdc_identifier = django_filters.ModelMultipleChoiceFilter(
field_name='vdcs__identifier', field_name='vdcs__identifier',
queryset=VirtualDeviceContext.objects.all(), queryset=VirtualDeviceContext.objects.all(),
to_field_name='identifier', to_field_name='identifier',
label='Virtual Device Context (Identifier)', label=_('Virtual Device Context (Identifier)')
) )
vdc = django_filters.ModelMultipleChoiceFilter( vdc = django_filters.ModelMultipleChoiceFilter(
field_name='vdcs__name', field_name='vdcs__name',
queryset=VirtualDeviceContext.objects.all(), queryset=VirtualDeviceContext.objects.all(),
to_field_name='name', to_field_name='name',
label='Virtual Device Context', label=_('Virtual Device Context')
)
wireless_lan_id = django_filters.ModelMultipleChoiceFilter(
field_name='wireless_lans',
queryset=WirelessLAN.objects.all(),
label=_('Wireless LAN')
)
wireless_link_id = django_filters.ModelMultipleChoiceFilter(
queryset=WirelessLink.objects.all(),
label=_('Wireless link')
) )
class Meta: class Meta:
model = Interface model = Interface
fields = [ fields = (
'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'poe_mode', 'poe_type', 'mode', 'rf_role', 'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'poe_mode', 'poe_type', 'mode', 'rf_role',
'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'cable_end', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected',
] 'cable_id', 'cable_end',
)
def filter_virtual_chassis_member(self, queryset, name, value): def filter_virtual_chassis_member(self, queryset, name, value):
try: try:
@ -1582,10 +1691,15 @@ class FrontPortFilterSet(
choices=PortTypeChoices, choices=PortTypeChoices,
null_value=None null_value=None
) )
rear_port_id = django_filters.ModelMultipleChoiceFilter(
queryset=RearPort.objects.all()
)
class Meta: class Meta:
model = FrontPort model = FrontPort
fields = ['id', 'name', 'label', 'type', 'color', 'description', 'cable_end'] fields = (
'id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description', 'mark_connected', 'cable_end',
)
class RearPortFilterSet( class RearPortFilterSet(
@ -1600,21 +1714,38 @@ class RearPortFilterSet(
class Meta: class Meta:
model = RearPort model = RearPort
fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description', 'cable_end'] fields = (
'id', 'name', 'label', 'type', 'color', 'positions', 'description', 'mark_connected', 'cable_end',
)
class ModuleBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet): class ModuleBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
installed_module_id = django_filters.ModelMultipleChoiceFilter(
field_name='installed_module',
queryset=ModuleBay.objects.all(),
label=_('Installed module (ID)'),
)
class Meta: class Meta:
model = ModuleBay model = ModuleBay
fields = ['id', 'name', 'label', 'description'] fields = ('id', 'name', 'label', 'position', 'description')
class DeviceBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet): class DeviceBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
installed_device_id = django_filters.ModelMultipleChoiceFilter(
queryset=Device.objects.all(),
label=_('Installed device (ID)'),
)
installed_device = django_filters.ModelMultipleChoiceFilter(
field_name='installed_device__name',
queryset=Device.objects.all(),
to_field_name='name',
label=_('Installed device (name)'),
)
class Meta: class Meta:
model = DeviceBay model = DeviceBay
fields = ['id', 'name', 'label', 'description'] fields = ('id', 'name', 'label', 'description')
class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet): class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
@ -1650,7 +1781,7 @@ class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
class Meta: class Meta:
model = InventoryItem model = InventoryItem
fields = ['id', 'name', 'label', 'part_id', 'asset_tag', 'discovered'] fields = ('id', 'name', 'label', 'part_id', 'asset_tag', 'description', 'discovered')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -1669,7 +1800,7 @@ class InventoryItemRoleFilterSet(OrganizationalModelFilterSet):
class Meta: class Meta:
model = InventoryItemRole model = InventoryItemRole
fields = ['id', 'name', 'slug', 'color', 'description'] fields = ('id', 'name', 'slug', 'color', 'description')
class VirtualChassisFilterSet(NetBoxModelFilterSet): class VirtualChassisFilterSet(NetBoxModelFilterSet):
@ -1734,7 +1865,7 @@ class VirtualChassisFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = VirtualChassis model = VirtualChassis
fields = ['id', 'domain', 'name', 'description'] fields = ('id', 'domain', 'name', 'description', 'member_count')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -1839,7 +1970,7 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
class Meta: class Meta:
model = Cable model = Cable
fields = ['id', 'label', 'length', 'length_unit', 'description'] fields = ('id', 'label', 'length', 'length_unit', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -1917,12 +2048,12 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
return self.filter_by_termination_object(queryset, CircuitTermination, value) return self.filter_by_termination_object(queryset, CircuitTermination, value)
class CableTerminationFilterSet(BaseFilterSet): class CableTerminationFilterSet(ChangeLoggedModelFilterSet):
termination_type = ContentTypeFilter() termination_type = ContentTypeFilter()
class Meta: class Meta:
model = CableTermination model = CableTermination
fields = ['id', 'cable', 'cable_end', 'termination_type', 'termination_id'] fields = ('id', 'cable', 'cable_end', 'termination_type', 'termination_id')
class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
@ -1971,7 +2102,7 @@ class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
class Meta: class Meta:
model = PowerPanel model = PowerPanel
fields = ['id', 'name', 'description'] fields = ('id', 'name', 'description')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -2037,10 +2168,10 @@ class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpoi
class Meta: class Meta:
model = PowerFeed model = PowerFeed
fields = [ fields = (
'id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'cable_end', 'id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization',
'description', 'available_power', 'mark_connected', 'cable_end', 'description',
] )
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -2099,18 +2230,18 @@ class ConsoleConnectionFilterSet(ConnectionFilterSet):
class Meta: class Meta:
model = ConsolePort model = ConsolePort
fields = ['name'] fields = ('name',)
class PowerConnectionFilterSet(ConnectionFilterSet): class PowerConnectionFilterSet(ConnectionFilterSet):
class Meta: class Meta:
model = PowerPort model = PowerPort
fields = ['name'] fields = ('name',)
class InterfaceConnectionFilterSet(ConnectionFilterSet): class InterfaceConnectionFilterSet(ConnectionFilterSet):
class Meta: class Meta:
model = Interface model = Interface
fields = [] fields = tuple()

View File

@ -754,7 +754,7 @@ class DeviceFilterForm(
) )
has_oob_ip = forms.NullBooleanField( has_oob_ip = forms.NullBooleanField(
required=False, required=False,
label='Has an OOB IP', label=_('Has an OOB IP'),
widget=forms.Select( widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES choices=BOOLEAN_WITH_BLANK_CHOICES
) )

View File

@ -9,7 +9,7 @@ from django.dispatch import Signal
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from core.models import ContentType from core.models import ObjectType
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.fields import PathField from dcim.fields import PathField
@ -481,13 +481,13 @@ class CablePath(models.Model):
def origin_type(self): def origin_type(self):
if self.path: if self.path:
ct_id, _ = decompile_path_node(self.path[0][0]) ct_id, _ = decompile_path_node(self.path[0][0])
return ContentType.objects.get_for_id(ct_id) return ObjectType.objects.get_for_id(ct_id)
@property @property
def destination_type(self): def destination_type(self):
if self.is_complete: if self.is_complete:
ct_id, _ = decompile_path_node(self.path[-1][0]) ct_id, _ = decompile_path_node(self.path[-1][0])
return ContentType.objects.get_for_id(ct_id) return ObjectType.objects.get_for_id(ct_id)
@property @property
def path_objects(self): def path_objects(self):
@ -594,7 +594,7 @@ class CablePath(models.Model):
# Step 6: Determine the far-end terminations # Step 6: Determine the far-end terminations
if isinstance(links[0], Cable): if isinstance(links[0], Cable):
termination_type = ContentType.objects.get_for_model(terminations[0]) termination_type = ObjectType.objects.get_for_model(terminations[0])
local_cable_terminations = CableTermination.objects.filter( local_cable_terminations = CableTermination.objects.filter(
termination_type=termination_type, termination_type=termination_type,
termination_id__in=[t.pk for t in terminations] termination_id__in=[t.pk for t in terminations]
@ -747,7 +747,7 @@ class CablePath(models.Model):
# Prefetch path objects using one query per model type. Prefetch related devices where appropriate. # Prefetch path objects using one query per model type. Prefetch related devices where appropriate.
prefetched = {} prefetched = {}
for ct_id, object_ids in to_prefetch.items(): for ct_id, object_ids in to_prefetch.items():
model_class = ContentType.objects.get_for_id(ct_id).model_class() model_class = ObjectType.objects.get_for_id(ct_id).model_class()
queryset = model_class.objects.filter(pk__in=object_ids) queryset = model_class.objects.filter(pk__in=object_ids)
if hasattr(model_class, 'device'): if hasattr(model_class, 'device'):
queryset = queryset.prefetch_related('device') queryset = queryset.prefetch_related('device')
@ -774,7 +774,7 @@ class CablePath(models.Model):
""" """
Return all Cable IDs within the path. Return all Cable IDs within the path.
""" """
cable_ct = ContentType.objects.get_for_model(Cable).pk cable_ct = ObjectType.objects.get_for_model(Cable).pk
cable_ids = [] cable_ids = []
for node in self._nodes: for node in self._nodes:

View File

@ -815,20 +815,6 @@ class Device(
def get_absolute_url(self): def get_absolute_url(self):
return reverse('dcim:device', args=[self.pk]) return reverse('dcim:device', args=[self.pk])
@property
def device_role(self):
"""
For backwards compatibility with pre-v3.6 code expecting a device_role to be present on Device.
"""
return self.role
@device_role.setter
def device_role(self, value):
"""
For backwards compatibility with pre-v3.6 code expecting a device_role to be present on Device.
"""
self.role = value
def clean(self): def clean(self):
super().clean() super().clean()

View File

@ -2156,7 +2156,7 @@ class CablePathTestCase(TestCase):
device = Device.objects.create( device = Device.objects.create(
site=self.site, site=self.site,
device_type=self.device.device_type, device_type=self.device.device_type,
device_role=self.device.device_role, role=self.device.role,
name='Test mid-span Device' name='Test mid-span Device'
) )
interface1 = Interface.objects.create(device=self.device, name='Interface 1') interface1 = Interface.objects.create(device=self.device, name='Interface 1')

View File

@ -64,21 +64,32 @@ class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
regions = ( parent_regions = (
Region(name='Region 1', slug='region-1', description='foobar1'), Region(name='Region 1', slug='region-1', description='foobar1'),
Region(name='Region 2', slug='region-2', description='foobar2'), Region(name='Region 2', slug='region-2', description='foobar2'),
Region(name='Region 3', slug='region-3', description='foobar3'), Region(name='Region 3', slug='region-3', description='foobar3'),
) )
for region in parent_regions:
region.save()
regions = (
Region(name='Region 1A', slug='region-1a', parent=parent_regions[0]),
Region(name='Region 1B', slug='region-1b', parent=parent_regions[0]),
Region(name='Region 2A', slug='region-2a', parent=parent_regions[1]),
Region(name='Region 2B', slug='region-2b', parent=parent_regions[1]),
Region(name='Region 3A', slug='region-3a', parent=parent_regions[2]),
Region(name='Region 3B', slug='region-3b', parent=parent_regions[2]),
)
for region in regions: for region in regions:
region.save() region.save()
child_regions = ( child_regions = (
Region(name='Region 1A', slug='region-1a', parent=regions[0]), Region(name='Region 1A1', slug='region-1a1', parent=regions[0]),
Region(name='Region 1B', slug='region-1b', parent=regions[0]), Region(name='Region 1B1', slug='region-1b1', parent=regions[1]),
Region(name='Region 2A', slug='region-2a', parent=regions[1]), Region(name='Region 2A1', slug='region-2a1', parent=regions[2]),
Region(name='Region 2B', slug='region-2b', parent=regions[1]), Region(name='Region 2B1', slug='region-2b1', parent=regions[3]),
Region(name='Region 3A', slug='region-3a', parent=regions[2]), Region(name='Region 3A1', slug='region-3a1', parent=regions[4]),
Region(name='Region 3B', slug='region-3b', parent=regions[2]), Region(name='Region 3B1', slug='region-3b1', parent=regions[5]),
) )
for region in child_regions: for region in child_regions:
region.save() region.save()
@ -100,12 +111,19 @@ class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_parent(self): def test_parent(self):
parent_regions = Region.objects.filter(parent__isnull=True)[:2] regions = Region.objects.filter(parent__isnull=True)[:2]
params = {'parent_id': [parent_regions[0].pk, parent_regions[1].pk]} params = {'parent_id': [regions[0].pk, regions[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'parent': [parent_regions[0].slug, parent_regions[1].slug]} params = {'parent': [regions[0].slug, regions[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_ancestor(self):
regions = Region.objects.filter(parent__isnull=True)[:2]
params = {'ancestor_id': [regions[0].pk, regions[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
params = {'ancestor': [regions[0].slug, regions[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests): class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = SiteGroup.objects.all() queryset = SiteGroup.objects.all()
@ -114,24 +132,35 @@ class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
sitegroups = ( parent_groups = (
SiteGroup(name='Site Group 1', slug='site-group-1', description='foobar1'), SiteGroup(name='Site Group 1', slug='site-group-1', description='foobar1'),
SiteGroup(name='Site Group 2', slug='site-group-2', description='foobar2'), SiteGroup(name='Site Group 2', slug='site-group-2', description='foobar2'),
SiteGroup(name='Site Group 3', slug='site-group-3', description='foobar3'), SiteGroup(name='Site Group 3', slug='site-group-3', description='foobar3'),
) )
for sitegroup in sitegroups: for site_group in parent_groups:
sitegroup.save() site_group.save()
child_sitegroups = ( groups = (
SiteGroup(name='Site Group 1A', slug='site-group-1a', parent=sitegroups[0]), SiteGroup(name='Site Group 1A', slug='site-group-1a', parent=parent_groups[0]),
SiteGroup(name='Site Group 1B', slug='site-group-1b', parent=sitegroups[0]), SiteGroup(name='Site Group 1B', slug='site-group-1b', parent=parent_groups[0]),
SiteGroup(name='Site Group 2A', slug='site-group-2a', parent=sitegroups[1]), SiteGroup(name='Site Group 2A', slug='site-group-2a', parent=parent_groups[1]),
SiteGroup(name='Site Group 2B', slug='site-group-2b', parent=sitegroups[1]), SiteGroup(name='Site Group 2B', slug='site-group-2b', parent=parent_groups[1]),
SiteGroup(name='Site Group 3A', slug='site-group-3a', parent=sitegroups[2]), SiteGroup(name='Site Group 3A', slug='site-group-3a', parent=parent_groups[2]),
SiteGroup(name='Site Group 3B', slug='site-group-3b', parent=sitegroups[2]), SiteGroup(name='Site Group 3B', slug='site-group-3b', parent=parent_groups[2]),
) )
for sitegroup in child_sitegroups: for site_group in groups:
sitegroup.save() site_group.save()
child_groups = (
SiteGroup(name='Site Group 1A1', slug='site-group-1a1', parent=groups[0]),
SiteGroup(name='Site Group 1B1', slug='site-group-1b1', parent=groups[1]),
SiteGroup(name='Site Group 2A1', slug='site-group-2a1', parent=groups[2]),
SiteGroup(name='Site Group 2B1', slug='site-group-2b1', parent=groups[3]),
SiteGroup(name='Site Group 3A1', slug='site-group-3a1', parent=groups[4]),
SiteGroup(name='Site Group 3B1', slug='site-group-3b1', parent=groups[5]),
)
for site_group in child_groups:
site_group.save()
def test_q(self): def test_q(self):
params = {'q': 'foobar1'} params = {'q': 'foobar1'}
@ -150,16 +179,24 @@ class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_parent(self): def test_parent(self):
parent_sitegroups = SiteGroup.objects.filter(parent__isnull=True)[:2] site_groups = SiteGroup.objects.filter(parent__isnull=True)[:2]
params = {'parent_id': [parent_sitegroups[0].pk, parent_sitegroups[1].pk]} params = {'parent_id': [site_groups[0].pk, site_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'parent': [parent_sitegroups[0].slug, parent_sitegroups[1].slug]} params = {'parent': [site_groups[0].slug, site_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_ancestor(self):
site_groups = SiteGroup.objects.filter(parent__isnull=True)[:2]
params = {'ancestor_id': [site_groups[0].pk, site_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
params = {'ancestor': [site_groups[0].slug, site_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
class SiteTestCase(TestCase, ChangeLoggedFilterSetTests): class SiteTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Site.objects.all() queryset = Site.objects.all()
filterset = SiteFilterSet filterset = SiteFilterSet
ignore_fields = ('physical_address', 'shipping_address')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -314,21 +351,29 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
Site.objects.bulk_create(sites) Site.objects.bulk_create(sites)
parent_locations = ( parent_locations = (
Location(name='Parent Location 1', slug='parent-location-1', site=sites[0]), Location(name='Location 1', slug='location-1', site=sites[0]),
Location(name='Parent Location 2', slug='parent-location-2', site=sites[1]), Location(name='Location 2', slug='location-2', site=sites[1]),
Location(name='Parent Location 3', slug='parent-location-3', site=sites[2]), Location(name='Location 3', slug='location-3', site=sites[2]),
) )
for location in parent_locations: for location in parent_locations:
location.save() location.save()
locations = ( locations = (
Location(name='Location 1', slug='location-1', site=sites[0], parent=parent_locations[0], status=LocationStatusChoices.STATUS_PLANNED, description='foobar1'), Location(name='Location 1A', slug='location-1a', site=sites[0], parent=parent_locations[0], status=LocationStatusChoices.STATUS_PLANNED, description='foobar1'),
Location(name='Location 2', slug='location-2', site=sites[1], parent=parent_locations[1], status=LocationStatusChoices.STATUS_STAGING, description='foobar2'), Location(name='Location 2A', slug='location-2a', site=sites[1], parent=parent_locations[1], status=LocationStatusChoices.STATUS_STAGING, description='foobar2'),
Location(name='Location 3', slug='location-3', site=sites[2], parent=parent_locations[2], status=LocationStatusChoices.STATUS_DECOMMISSIONING, description='foobar3'), Location(name='Location 3A', slug='location-3a', site=sites[2], parent=parent_locations[2], status=LocationStatusChoices.STATUS_DECOMMISSIONING, description='foobar3'),
) )
for location in locations: for location in locations:
location.save() location.save()
child_locations = (
Location(name='Location 1A1', slug='location-1a1', site=sites[0], parent=locations[0]),
Location(name='Location 2A1', slug='location-2a1', site=sites[1], parent=locations[1]),
Location(name='Location 3A1', slug='location-3a1', site=sites[2], parent=locations[2]),
)
for location in child_locations:
location.save()
def test_q(self): def test_q(self):
params = {'q': 'foobar1'} params = {'q': 'foobar1'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
@ -352,31 +397,38 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
def test_region(self): def test_region(self):
regions = Region.objects.all()[:2] regions = Region.objects.all()[:2]
params = {'region_id': [regions[0].pk, regions[1].pk]} params = {'region_id': [regions[0].pk, regions[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'region': [regions[0].slug, regions[1].slug]} params = {'region': [regions[0].slug, regions[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_site_group(self): def test_site_group(self):
site_groups = SiteGroup.objects.all()[:2] site_groups = SiteGroup.objects.all()[:2]
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]} params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]} params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_site(self): def test_site(self):
sites = Site.objects.all()[:2] sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]} params = {'site_id': [sites[0].pk, sites[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'site': [sites[0].slug, sites[1].slug]} params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
def test_parent(self): def test_parent(self):
parent_groups = Location.objects.filter(name__startswith='Parent')[:2] locations = Location.objects.filter(parent__isnull=True)[:2]
params = {'parent_id': [parent_groups[0].pk, parent_groups[1].pk]} params = {'parent_id': [locations[0].pk, locations[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'parent': [parent_groups[0].slug, parent_groups[1].slug]} params = {'parent': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_ancestor(self):
locations = Location.objects.filter(parent__isnull=True)[:2]
params = {'ancestor_id': [locations[0].pk, locations[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'ancestor': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests): class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = RackRole.objects.all() queryset = RackRole.objects.all()
@ -416,6 +468,7 @@ class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
class RackTestCase(TestCase, ChangeLoggedFilterSetTests): class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Rack.objects.all() queryset = Rack.objects.all()
filterset = RackFilterSet filterset = RackFilterSet
ignore_fields = ('units',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -675,6 +728,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests): class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = RackReservation.objects.all() queryset = RackReservation.objects.all()
filterset = RackReservationFilterSet filterset = RackReservationFilterSet
ignore_fields = ('units',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -838,6 +892,7 @@ class ManufacturerTestCase(TestCase, ChangeLoggedFilterSetTests):
class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests): class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = DeviceType.objects.all() queryset = DeviceType.objects.all()
filterset = DeviceTypeFilterSet filterset = DeviceTypeFilterSet
ignore_fields = ('front_image', 'rear_image')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -1829,6 +1884,7 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests): class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Device.objects.all() queryset = Device.objects.all()
filterset = DeviceFilterSet filterset = DeviceFilterSet
ignore_fields = ('local_context_data', 'oob_ip', 'primary_ip4', 'primary_ip6', 'vc_master_for')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -2281,6 +2337,7 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests): class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Module.objects.all() queryset = Module.objects.all()
filterset = ModuleFilterSet filterset = ModuleFilterSet
ignore_fields = ('local_context_data',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -3178,6 +3235,7 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests): class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
queryset = Interface.objects.all() queryset = Interface.objects.all()
filterset = InterfaceFilterSet filterset = InterfaceFilterSet
ignore_fields = ('tagged_vlans', 'untagged_vlan', 'vdcs')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -5281,6 +5339,7 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests): class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VirtualDeviceContext.objects.all() queryset = VirtualDeviceContext.objects.all()
filterset = VirtualDeviceContextFilterSet filterset = VirtualDeviceContextFilterSet
ignore_fields = ('primary_ip4', 'primary_ip6')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -5350,15 +5409,22 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
VirtualDeviceContext.objects.bulk_create(vdcs) VirtualDeviceContext.objects.bulk_create(vdcs)
interfaces = ( interfaces = (
Interface(device=devices[0], name='Interface 1', type='virtual'), Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=devices[0], name='Interface 2', type='virtual'), Interface(device=devices[0], name='Interface 2', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=devices[1], name='Interface 3', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=devices[1], name='Interface 4', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=devices[2], name='Interface 5', type=InterfaceTypeChoices.TYPE_VIRTUAL),
Interface(device=devices[2], name='Interface 6', type=InterfaceTypeChoices.TYPE_VIRTUAL),
) )
Interface.objects.bulk_create(interfaces) Interface.objects.bulk_create(interfaces)
interfaces[0].vdcs.set([vdcs[0]]) interfaces[0].vdcs.set([vdcs[0]])
interfaces[1].vdcs.set([vdcs[1]]) interfaces[1].vdcs.set([vdcs[1]])
interfaces[2].vdcs.set([vdcs[2]])
interfaces[3].vdcs.set([vdcs[3]])
interfaces[4].vdcs.set([vdcs[4]])
interfaces[5].vdcs.set([vdcs[5]])
addresses = ( ip_addresses = (
IPAddress(assigned_object=interfaces[0], address='10.1.1.1/24'), IPAddress(assigned_object=interfaces[0], address='10.1.1.1/24'),
IPAddress(assigned_object=interfaces[1], address='10.1.1.2/24'), IPAddress(assigned_object=interfaces[1], address='10.1.1.2/24'),
IPAddress(assigned_object=None, address='10.1.1.3/24'), IPAddress(assigned_object=None, address='10.1.1.3/24'),
@ -5366,13 +5432,12 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
IPAddress(assigned_object=interfaces[1], address='2001:db8::2/64'), IPAddress(assigned_object=interfaces[1], address='2001:db8::2/64'),
IPAddress(assigned_object=None, address='2001:db8::3/64'), IPAddress(assigned_object=None, address='2001:db8::3/64'),
) )
IPAddress.objects.bulk_create(addresses) IPAddress.objects.bulk_create(ip_addresses)
vdcs[0].primary_ip4 = ip_addresses[0]
vdcs[0].primary_ip4 = addresses[0] vdcs[0].primary_ip6 = ip_addresses[3]
vdcs[0].primary_ip6 = addresses[3]
vdcs[0].save() vdcs[0].save()
vdcs[1].primary_ip4 = addresses[1] vdcs[1].primary_ip4 = ip_addresses[1]
vdcs[1].primary_ip6 = addresses[4] vdcs[1].primary_ip6 = ip_addresses[4]
vdcs[1].save() vdcs[1].save()
def test_q(self): def test_q(self):
@ -5380,8 +5445,11 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_device(self): def test_device(self):
params = {'device': ['Device 1', 'Device 2']} devices = Device.objects.filter(name__in=['Device 1', 'Device 2'])
params = {'device': [devices[0].name, devices[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
params = {'device_id': [devices[0].pk, devices[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_status(self): def test_status(self):
params = {'status': ['active']} params = {'status': ['active']}
@ -5391,10 +5459,10 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'description': ['foobar1', 'foobar2']} params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_device_id(self): def test_interface(self):
devices = Device.objects.filter(name__in=['Device 1', 'Device 2']) interfaces = Interface.objects.filter(name__in=['Interface 1', 'Interface 3'])
params = {'device_id': [devices[0].pk, devices[1].pk]} params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_has_primary_ip(self): def test_has_primary_ip(self):
params = {'has_primary_ip': True} params = {'has_primary_ip': True}

View File

@ -1,8 +1,8 @@
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.test import TestCase from django.test import TestCase
from circuits.models import * from circuits.models import *
from core.models import ObjectType
from dcim.choices import * from dcim.choices import *
from dcim.models import * from dcim.models import *
from extras.models import CustomField from extras.models import CustomField
@ -293,8 +293,8 @@ class DeviceTestCase(TestCase):
# Create a CustomField with a default value & assign it to all component models # Create a CustomField with a default value & assign it to all component models
cf1 = CustomField.objects.create(name='cf1', default='foo') cf1 = CustomField.objects.create(name='cf1', default='foo')
cf1.content_types.set( cf1.object_types.set(
ContentType.objects.filter(app_label='dcim', model__in=[ ObjectType.objects.filter(app_label='dcim', model__in=[
'consoleport', 'consoleport',
'consoleserverport', 'consoleserverport',
'powerport', 'powerport',
@ -533,30 +533,6 @@ class DeviceTestCase(TestCase):
device2.full_clean() device2.full_clean()
device2.save() device2.save()
def test_old_device_role_field(self):
"""
Ensure that the old device role field sets the value in the new role field.
"""
# Test getter method
device = Device(
site=Site.objects.first(),
device_type=DeviceType.objects.first(),
role=DeviceRole.objects.first(),
name='Test Device 1',
device_role=DeviceRole.objects.first()
)
device.full_clean()
device.save()
self.assertEqual(device.role, device.device_role)
# Test setter method
device.device_role = DeviceRole.objects.last()
device.full_clean()
device.save()
self.assertEqual(device.role, device.device_role)
class CableTestCase(TestCase): class CableTestCase(TestCase):

View File

@ -3,7 +3,6 @@ from zoneinfo import ZoneInfo
import yaml import yaml
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.test import override_settings from django.test import override_settings
from django.urls import reverse from django.urls import reverse
from netaddr import EUI from netaddr import EUI
@ -2982,7 +2981,6 @@ class CableTestCase(
tags = create_tags('Alpha', 'Bravo', 'Charlie') tags = create_tags('Alpha', 'Bravo', 'Charlie')
interface_ct = ContentType.objects.get_for_model(Interface)
cls.form_data = { cls.form_data = {
# TODO: Revisit this limitation # TODO: Revisit this limitation
# Changing terminations not supported when editing an existing Cable # Changing terminations not supported when editing an existing Cable

View File

@ -1,13 +1,12 @@
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from drf_spectacular.utils import extend_schema_field
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework.fields import Field from rest_framework.fields import Field
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from core.models import ObjectType
from extras.choices import CustomFieldTypeChoices from extras.choices import CustomFieldTypeChoices
from extras.models import CustomField from extras.models import CustomField
from netbox.constants import NESTED_SERIALIZER_PREFIX
from utilities.api import get_serializer_for_model from utilities.api import get_serializer_for_model
@ -25,8 +24,8 @@ class CustomFieldDefaultValues:
self.model = serializer_field.parent.Meta.model self.model = serializer_field.parent.Meta.model
# Retrieve the CustomFields for the parent model # Retrieve the CustomFields for the parent model
content_type = ContentType.objects.get_for_model(self.model) object_type = ObjectType.objects.get_for_model(self.model)
fields = CustomField.objects.filter(content_types=content_type) fields = CustomField.objects.filter(object_types=object_type)
# Populate the default value for each CustomField # Populate the default value for each CustomField
value = {} value = {}
@ -47,8 +46,8 @@ class CustomFieldsDataField(Field):
Cache CustomFields assigned to this model to avoid redundant database queries Cache CustomFields assigned to this model to avoid redundant database queries
""" """
if not hasattr(self, '_custom_fields'): if not hasattr(self, '_custom_fields'):
content_type = ContentType.objects.get_for_model(self.parent.Meta.model) object_type = ObjectType.objects.get_for_model(self.parent.Meta.model)
self._custom_fields = CustomField.objects.filter(content_types=content_type) self._custom_fields = CustomField.objects.filter(object_types=object_type)
return self._custom_fields return self._custom_fields
def to_representation(self, obj): def to_representation(self, obj):
@ -58,11 +57,11 @@ class CustomFieldsDataField(Field):
for cf in self._get_custom_fields(): for cf in self._get_custom_fields():
value = cf.deserialize(obj.get(cf.name)) value = cf.deserialize(obj.get(cf.name))
if value is not None and cf.type == CustomFieldTypeChoices.TYPE_OBJECT: if value is not None and cf.type == CustomFieldTypeChoices.TYPE_OBJECT:
serializer = get_serializer_for_model(cf.object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX) serializer = get_serializer_for_model(cf.related_object_type.model_class())
value = serializer(value, context=self.parent.context).data value = serializer(value, nested=True, context=self.parent.context).data
elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
serializer = get_serializer_for_model(cf.object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX) serializer = get_serializer_for_model(cf.related_object_type.model_class())
value = serializer(value, many=True, context=self.parent.context).data value = serializer(value, nested=True, many=True, context=self.parent.context).data
data[cf.name] = value data[cf.name] = value
return data return data
@ -80,12 +79,9 @@ class CustomFieldsDataField(Field):
CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_OBJECT,
CustomFieldTypeChoices.TYPE_MULTIOBJECT CustomFieldTypeChoices.TYPE_MULTIOBJECT
): ):
serializer_class = get_serializer_for_model( serializer_class = get_serializer_for_model(cf.related_object_type.model_class())
model=cf.object_type.model_class(),
prefix=NESTED_SERIALIZER_PREFIX
)
many = cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT many = cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT
serializer = serializer_class(data=data[cf.name], many=many, context=self.parent.context) serializer = serializer_class(data=data[cf.name], nested=True, many=many, context=self.parent.context)
if serializer.is_valid(): if serializer.is_valid():
data[cf.name] = [obj['id'] for obj in serializer.data] if many else serializer.data['id'] data[cf.name] = [obj['id'] for obj in serializer.data] if many else serializer.data['id']
else: else:

View File

@ -5,7 +5,7 @@ from rest_framework.response import Response
from rest_framework.status import HTTP_400_BAD_REQUEST from rest_framework.status import HTTP_400_BAD_REQUEST
from netbox.api.renderers import TextRenderer from netbox.api.renderers import TextRenderer
from .nested_serializers import NestedConfigTemplateSerializer from .serializers import ConfigTemplateSerializer
__all__ = ( __all__ = (
'ConfigContextQuerySetMixin', 'ConfigContextQuerySetMixin',
@ -52,7 +52,7 @@ class ConfigTemplateRenderMixin:
if request.accepted_renderer.format == 'txt': if request.accepted_renderer.format == 'txt':
return Response(output) return Response(output)
template_serializer = NestedConfigTemplateSerializer(configtemplate, context={'request': request}) template_serializer = ConfigTemplateSerializer(configtemplate, nested=True, context={'request': request})
return Response({ return Response({
'configtemplate': template_serializer.data, 'configtemplate': template_serializer.data,

View File

@ -1,659 +1,16 @@
from django.contrib.auth import get_user_model from .serializers_.objecttypes import *
from django.core.exceptions import ObjectDoesNotExist from .serializers_.attachments import *
from django.utils.translation import gettext as _ from .serializers_.bookmarks import *
from drf_spectacular.types import OpenApiTypes from .serializers_.change_logging import *
from drf_spectacular.utils import extend_schema_field from .serializers_.customfields import *
from rest_framework import serializers from .serializers_.customlinks import *
from .serializers_.dashboard import *
from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer from .serializers_.events import *
from core.api.serializers import JobSerializer from .serializers_.exporttemplates import *
from core.models import ContentType from .serializers_.journaling import *
from dcim.api.nested_serializers import ( from .serializers_.configcontexts import *
NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer, from .serializers_.configtemplates import *
NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer, from .serializers_.savedfilters import *
) from .serializers_.scripts import *
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from .serializers_.tags import *
from extras.choices import *
from extras.models import *
from netbox.api.exceptions import SerializerNotFound
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer
from netbox.api.serializers.features import TaggableModelSerializer
from netbox.constants import NESTED_SERIALIZER_PREFIX
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
from tenancy.models import Tenant, TenantGroup
from users.api.nested_serializers import NestedUserSerializer
from utilities.api import get_serializer_for_model
from virtualization.api.nested_serializers import (
NestedClusterGroupSerializer, NestedClusterSerializer, NestedClusterTypeSerializer,
)
from virtualization.models import Cluster, ClusterGroup, ClusterType
from .nested_serializers import * from .nested_serializers import *
__all__ = (
'BookmarkSerializer',
'ConfigContextSerializer',
'ConfigTemplateSerializer',
'ContentTypeSerializer',
'CustomFieldChoiceSetSerializer',
'CustomFieldSerializer',
'CustomLinkSerializer',
'DashboardSerializer',
'EventRuleSerializer',
'ExportTemplateSerializer',
'ImageAttachmentSerializer',
'JournalEntrySerializer',
'ObjectChangeSerializer',
'SavedFilterSerializer',
'ScriptDetailSerializer',
'ScriptInputSerializer',
'ScriptSerializer',
'TagSerializer',
'WebhookSerializer',
)
#
# Event Rules
#
class EventRuleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail')
content_types = ContentTypeField(
queryset=ContentType.objects.with_feature('event_rules'),
many=True
)
action_type = ChoiceField(choices=EventRuleActionChoices)
action_object_type = ContentTypeField(
queryset=ContentType.objects.with_feature('event_rules'),
)
action_object = serializers.SerializerMethodField(read_only=True)
class Meta:
model = EventRule
fields = [
'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete',
'type_job_start', 'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type',
'action_object_id', 'action_object', 'description', 'custom_fields', 'tags', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
@extend_schema_field(OpenApiTypes.OBJECT)
def get_action_object(self, instance):
context = {'request': self.context['request']}
# We need to manually instantiate the serializer for scripts
if instance.action_type == EventRuleActionChoices.SCRIPT:
script = instance.action_object
instance = script.python_class() if script.python_class else None
return NestedScriptSerializer(instance, context=context).data
else:
serializer = get_serializer_for_model(
model=instance.action_object_type.model_class(),
prefix=NESTED_SERIALIZER_PREFIX
)
return serializer(instance.action_object, context=context).data
#
# Webhooks
#
class WebhookSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail')
class Meta:
model = Webhook
fields = [
'id', 'url', 'display', 'name', 'description', 'payload_url', 'http_method', 'http_content_type',
'additional_headers', 'body_template', 'secret', 'ssl_verification', 'ca_file_path', 'custom_fields',
'tags', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
#
# Custom fields
#
class CustomFieldSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
content_types = ContentTypeField(
queryset=ContentType.objects.with_feature('custom_fields'),
many=True
)
type = ChoiceField(choices=CustomFieldTypeChoices)
object_type = ContentTypeField(
queryset=ContentType.objects.all(),
required=False,
allow_null=True
)
filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
data_type = serializers.SerializerMethodField()
choice_set = NestedCustomFieldChoiceSetSerializer(
required=False,
allow_null=True
)
ui_visible = ChoiceField(choices=CustomFieldUIVisibleChoices, required=False)
ui_editable = ChoiceField(choices=CustomFieldUIEditableChoices, required=False)
class Meta:
model = CustomField
fields = [
'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name',
'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable',
'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set',
'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
def validate_type(self, value):
if self.instance and self.instance.type != value:
raise serializers.ValidationError(_('Changing the type of custom fields is not supported.'))
return value
@extend_schema_field(OpenApiTypes.STR)
def get_data_type(self, obj):
types = CustomFieldTypeChoices
if obj.type == types.TYPE_INTEGER:
return 'integer'
if obj.type == types.TYPE_DECIMAL:
return 'decimal'
if obj.type == types.TYPE_BOOLEAN:
return 'boolean'
if obj.type in (types.TYPE_JSON, types.TYPE_OBJECT):
return 'object'
if obj.type in (types.TYPE_MULTISELECT, types.TYPE_MULTIOBJECT):
return 'array'
return 'string'
class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
base_choices = ChoiceField(
choices=CustomFieldChoiceSetBaseChoices,
required=False
)
extra_choices = serializers.ListField(
child=serializers.ListField(
min_length=2,
max_length=2
)
)
class Meta:
model = CustomFieldChoiceSet
fields = [
'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically',
'choices_count', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description', 'choices_count')
#
# Custom links
#
class CustomLinkSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
content_types = ContentTypeField(
queryset=ContentType.objects.with_feature('custom_links'),
many=True
)
class Meta:
model = CustomLink
fields = [
'id', 'url', 'display', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name',
'button_class', 'new_window', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name')
#
# Export templates
#
class ExportTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')
content_types = ContentTypeField(
queryset=ContentType.objects.with_feature('export_templates'),
many=True
)
data_source = NestedDataSourceSerializer(
required=False
)
data_file = NestedDataFileSerializer(
read_only=True
)
class Meta:
model = ExportTemplate
fields = [
'id', 'url', 'display', 'content_types', 'name', 'description', 'template_code', 'mime_type',
'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created',
'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
#
# Saved filters
#
class SavedFilterSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
content_types = ContentTypeField(
queryset=ContentType.objects.all(),
many=True
)
class Meta:
model = SavedFilter
fields = [
'id', 'url', 'display', 'content_types', 'name', 'slug', 'description', 'user', 'weight', 'enabled',
'shared', 'parameters', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')
#
# Bookmarks
#
class BookmarkSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail')
object_type = ContentTypeField(
queryset=ContentType.objects.with_feature('bookmarks'),
)
object = serializers.SerializerMethodField(read_only=True)
user = NestedUserSerializer()
class Meta:
model = Bookmark
fields = [
'id', 'url', 'display', 'object_type', 'object_id', 'object', 'user', 'created',
]
brief_fields = ('id', 'url', 'display', 'object_id', 'object_type')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_object(self, instance):
serializer = get_serializer_for_model(instance.object, prefix=NESTED_SERIALIZER_PREFIX)
return serializer(instance.object, context={'request': self.context['request']}).data
#
# Tags
#
class TagSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
object_types = ContentTypeField(
queryset=ContentType.objects.with_feature('tags'),
many=True,
required=False
)
# Related object counts
tagged_items = RelatedObjectCountField('extras_taggeditem_items')
class Meta:
model = Tag
fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'object_types', 'tagged_items', 'created',
'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'color', 'description')
#
# Image attachments
#
class ImageAttachmentSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
content_type = ContentTypeField(
queryset=ContentType.objects.all()
)
parent = serializers.SerializerMethodField(read_only=True)
class Meta:
model = ImageAttachment
fields = [
'id', 'url', 'display', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height',
'image_width', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'image')
def validate(self, data):
# Validate that the parent object exists
try:
data['content_type'].get_object_for_this_type(id=data['object_id'])
except ObjectDoesNotExist:
raise serializers.ValidationError(
"Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
)
# Enforce model validation
super().validate(data)
return data
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_parent(self, obj):
serializer = get_serializer_for_model(obj.parent, prefix=NESTED_SERIALIZER_PREFIX)
return serializer(obj.parent, context={'request': self.context['request']}).data
#
# Journal entries
#
class JournalEntrySerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
assigned_object_type = ContentTypeField(
queryset=ContentType.objects.all()
)
assigned_object = serializers.SerializerMethodField(read_only=True)
created_by = serializers.PrimaryKeyRelatedField(
allow_null=True,
queryset=get_user_model().objects.all(),
required=False,
default=serializers.CurrentUserDefault()
)
kind = ChoiceField(
choices=JournalEntryKindChoices,
required=False
)
class Meta:
model = JournalEntry
fields = [
'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created',
'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'created')
def validate(self, data):
# Validate that the parent object exists
if 'assigned_object_type' in data and 'assigned_object_id' in data:
try:
data['assigned_object_type'].get_object_for_this_type(id=data['assigned_object_id'])
except ObjectDoesNotExist:
raise serializers.ValidationError(
f"Invalid assigned_object: {data['assigned_object_type']} ID {data['assigned_object_id']}"
)
# Enforce model validation
super().validate(data)
return data
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_assigned_object(self, instance):
serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
context = {'request': self.context['request']}
return serializer(instance.assigned_object, context=context).data
#
# Config contexts
#
class ConfigContextSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:configcontext-detail')
regions = SerializedPKRelatedField(
queryset=Region.objects.all(),
serializer=NestedRegionSerializer,
required=False,
many=True
)
site_groups = SerializedPKRelatedField(
queryset=SiteGroup.objects.all(),
serializer=NestedSiteGroupSerializer,
required=False,
many=True
)
sites = SerializedPKRelatedField(
queryset=Site.objects.all(),
serializer=NestedSiteSerializer,
required=False,
many=True
)
locations = SerializedPKRelatedField(
queryset=Location.objects.all(),
serializer=NestedLocationSerializer,
required=False,
many=True
)
device_types = SerializedPKRelatedField(
queryset=DeviceType.objects.all(),
serializer=NestedDeviceTypeSerializer,
required=False,
many=True
)
roles = SerializedPKRelatedField(
queryset=DeviceRole.objects.all(),
serializer=NestedDeviceRoleSerializer,
required=False,
many=True
)
platforms = SerializedPKRelatedField(
queryset=Platform.objects.all(),
serializer=NestedPlatformSerializer,
required=False,
many=True
)
cluster_types = SerializedPKRelatedField(
queryset=ClusterType.objects.all(),
serializer=NestedClusterTypeSerializer,
required=False,
many=True
)
cluster_groups = SerializedPKRelatedField(
queryset=ClusterGroup.objects.all(),
serializer=NestedClusterGroupSerializer,
required=False,
many=True
)
clusters = SerializedPKRelatedField(
queryset=Cluster.objects.all(),
serializer=NestedClusterSerializer,
required=False,
many=True
)
tenant_groups = SerializedPKRelatedField(
queryset=TenantGroup.objects.all(),
serializer=NestedTenantGroupSerializer,
required=False,
many=True
)
tenants = SerializedPKRelatedField(
queryset=Tenant.objects.all(),
serializer=NestedTenantSerializer,
required=False,
many=True
)
tags = serializers.SlugRelatedField(
queryset=Tag.objects.all(),
slug_field='slug',
required=False,
many=True
)
data_source = NestedDataSourceSerializer(
required=False
)
data_file = NestedDataFileSerializer(
read_only=True
)
class Meta:
model = ConfigContext
fields = [
'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites',
'locations', 'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters',
'tenant_groups', 'tenants', 'tags', 'data_source', 'data_path', 'data_file', 'data_synced', 'data',
'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
#
# Config templates
#
class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail')
data_source = NestedDataSourceSerializer(
required=False
)
data_file = NestedDataFileSerializer(
required=False
)
class Meta:
model = ConfigTemplate
fields = [
'id', 'url', 'display', 'name', 'description', 'environment_params', 'template_code', 'data_source',
'data_path', 'data_file', 'data_synced', 'tags', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
#
# Scripts
#
class ScriptSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:script-detail')
description = serializers.SerializerMethodField(read_only=True)
vars = serializers.SerializerMethodField(read_only=True)
result = NestedJobSerializer(read_only=True)
class Meta:
model = Script
fields = [
'id', 'url', 'module', 'name', 'description', 'vars', 'result', 'display', 'is_executable',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_vars(self, obj):
if obj.python_class:
return {
k: v.__class__.__name__ for k, v in obj.python_class()._get_vars().items()
}
else:
return {}
@extend_schema_field(serializers.CharField())
def get_display(self, obj):
return f'{obj.name} ({obj.module})'
@extend_schema_field(serializers.CharField())
def get_description(self, obj):
if obj.python_class:
return obj.python_class().description
else:
return None
class ScriptDetailSerializer(ScriptSerializer):
result = serializers.SerializerMethodField(read_only=True)
@extend_schema_field(JobSerializer())
def get_result(self, obj):
job = obj.jobs.all().order_by('-created').first()
context = {
'request': self.context['request']
}
data = JobSerializer(job, context=context).data
return data
class ScriptInputSerializer(serializers.Serializer):
data = serializers.JSONField()
commit = serializers.BooleanField()
schedule_at = serializers.DateTimeField(required=False, allow_null=True)
interval = serializers.IntegerField(required=False, allow_null=True)
def validate_schedule_at(self, value):
if value and not self.context['script'].scheduling_enabled:
raise serializers.ValidationError(_("Scheduling is not enabled for this script."))
return value
def validate_interval(self, value):
if value and not self.context['script'].scheduling_enabled:
raise serializers.ValidationError(_("Scheduling is not enabled for this script."))
return value
#
# Change logging
#
class ObjectChangeSerializer(BaseModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:objectchange-detail')
user = NestedUserSerializer(
read_only=True
)
action = ChoiceField(
choices=ObjectChangeActionChoices,
read_only=True
)
changed_object_type = ContentTypeField(
read_only=True
)
changed_object = serializers.SerializerMethodField(
read_only=True
)
class Meta:
model = ObjectChange
fields = [
'id', 'url', 'display', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type',
'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
]
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_changed_object(self, obj):
"""
Serialize a nested representation of the changed object.
"""
if obj.changed_object is None:
return None
try:
serializer = get_serializer_for_model(obj.changed_object, prefix=NESTED_SERIALIZER_PREFIX)
except SerializerNotFound:
return obj.object_repr
context = {
'request': self.context['request']
}
data = serializer(obj.changed_object, context=context).data
return data
#
# ContentTypes
#
class ContentTypeSerializer(BaseModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:contenttype-detail')
class Meta:
model = ContentType
fields = ['id', 'url', 'display', 'app_label', 'model']
#
# User dashboard
#
class DashboardSerializer(serializers.ModelSerializer):
class Meta:
model = Dashboard
fields = ('layout', 'config')

View File

@ -0,0 +1,50 @@
from django.core.exceptions import ObjectDoesNotExist
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from core.models import ObjectType
from extras.models import ImageAttachment
from netbox.api.fields import ContentTypeField
from netbox.api.serializers import ValidatedModelSerializer
from utilities.api import get_serializer_for_model
__all__ = (
'ImageAttachmentSerializer',
)
class ImageAttachmentSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
object_type = ContentTypeField(
queryset=ObjectType.objects.all()
)
parent = serializers.SerializerMethodField(read_only=True)
class Meta:
model = ImageAttachment
fields = [
'id', 'url', 'display', 'object_type', 'object_id', 'parent', 'name', 'image', 'image_height',
'image_width', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'image')
def validate(self, data):
# Validate that the parent object exists
try:
data['object_type'].get_object_for_this_type(id=data['object_id'])
except ObjectDoesNotExist:
raise serializers.ValidationError(
"Invalid parent object: {} ID {}".format(data['object_type'], data['object_id'])
)
# Enforce model validation
super().validate(data)
return data
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_parent(self, obj):
serializer = get_serializer_for_model(obj.parent)
context = {'request': self.context['request']}
return serializer(obj.parent, nested=True, context=context).data

View File

@ -0,0 +1,35 @@
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from core.models import ObjectType
from extras.models import Bookmark
from netbox.api.fields import ContentTypeField
from netbox.api.serializers import ValidatedModelSerializer
from users.api.serializers_.users import UserSerializer
from utilities.api import get_serializer_for_model
__all__ = (
'BookmarkSerializer',
)
class BookmarkSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail')
object_type = ContentTypeField(
queryset=ObjectType.objects.with_feature('bookmarks'),
)
object = serializers.SerializerMethodField(read_only=True)
user = UserSerializer(nested=True)
class Meta:
model = Bookmark
fields = [
'id', 'url', 'display', 'object_type', 'object_id', 'object', 'user', 'created',
]
brief_fields = ('id', 'url', 'display', 'object_id', 'object_type')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_object(self, instance):
serializer = get_serializer_for_model(instance.object)
context = {'request': self.context['request']}
return serializer(instance.object, nested=True, context=context).data

View File

@ -0,0 +1,55 @@
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from extras.choices import *
from extras.models import ObjectChange
from netbox.api.exceptions import SerializerNotFound
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.serializers import BaseModelSerializer
from users.api.serializers_.users import UserSerializer
from utilities.api import get_serializer_for_model
__all__ = (
'ObjectChangeSerializer',
)
class ObjectChangeSerializer(BaseModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:objectchange-detail')
user = UserSerializer(
nested=True,
read_only=True
)
action = ChoiceField(
choices=ObjectChangeActionChoices,
read_only=True
)
changed_object_type = ContentTypeField(
read_only=True
)
changed_object = serializers.SerializerMethodField(
read_only=True
)
class Meta:
model = ObjectChange
fields = [
'id', 'url', 'display', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type',
'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
]
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_changed_object(self, obj):
"""
Serialize a nested representation of the changed object.
"""
if obj.changed_object is None:
return None
try:
serializer = get_serializer_for_model(obj.changed_object)
except SerializerNotFound:
return obj.object_repr
data = serializer(obj.changed_object, nested=True, context={'request': self.context['request']}).data
return data

View File

@ -0,0 +1,131 @@
from rest_framework import serializers
from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer
from dcim.api.serializers_.devicetypes import DeviceTypeSerializer
from dcim.api.serializers_.platforms import PlatformSerializer
from dcim.api.serializers_.roles import DeviceRoleSerializer
from dcim.api.serializers_.sites import LocationSerializer, RegionSerializer, SiteSerializer, SiteGroupSerializer
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from extras.models import ConfigContext, Tag
from netbox.api.fields import SerializedPKRelatedField
from netbox.api.serializers import ValidatedModelSerializer
from tenancy.api.serializers_.tenants import TenantSerializer, TenantGroupSerializer
from tenancy.models import Tenant, TenantGroup
from virtualization.api.serializers_.clusters import ClusterSerializer, ClusterGroupSerializer, ClusterTypeSerializer
from virtualization.models import Cluster, ClusterGroup, ClusterType
__all__ = (
'ConfigContextSerializer',
)
class ConfigContextSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:configcontext-detail')
regions = SerializedPKRelatedField(
queryset=Region.objects.all(),
serializer=RegionSerializer,
nested=True,
required=False,
many=True
)
site_groups = SerializedPKRelatedField(
queryset=SiteGroup.objects.all(),
serializer=SiteGroupSerializer,
nested=True,
required=False,
many=True
)
sites = SerializedPKRelatedField(
queryset=Site.objects.all(),
serializer=SiteSerializer,
nested=True,
required=False,
many=True
)
locations = SerializedPKRelatedField(
queryset=Location.objects.all(),
serializer=LocationSerializer,
nested=True,
required=False,
many=True
)
device_types = SerializedPKRelatedField(
queryset=DeviceType.objects.all(),
serializer=DeviceTypeSerializer,
nested=True,
required=False,
many=True
)
roles = SerializedPKRelatedField(
queryset=DeviceRole.objects.all(),
serializer=DeviceRoleSerializer,
nested=True,
required=False,
many=True
)
platforms = SerializedPKRelatedField(
queryset=Platform.objects.all(),
serializer=PlatformSerializer,
nested=True,
required=False,
many=True
)
cluster_types = SerializedPKRelatedField(
queryset=ClusterType.objects.all(),
serializer=ClusterTypeSerializer,
nested=True,
required=False,
many=True
)
cluster_groups = SerializedPKRelatedField(
queryset=ClusterGroup.objects.all(),
serializer=ClusterGroupSerializer,
nested=True,
required=False,
many=True
)
clusters = SerializedPKRelatedField(
queryset=Cluster.objects.all(),
serializer=ClusterSerializer,
nested=True,
required=False,
many=True
)
tenant_groups = SerializedPKRelatedField(
queryset=TenantGroup.objects.all(),
serializer=TenantGroupSerializer,
nested=True,
required=False,
many=True
)
tenants = SerializedPKRelatedField(
queryset=Tenant.objects.all(),
serializer=TenantSerializer,
nested=True,
required=False,
many=True
)
tags = serializers.SlugRelatedField(
queryset=Tag.objects.all(),
slug_field='slug',
required=False,
many=True
)
data_source = DataSourceSerializer(
nested=True,
required=False
)
data_file = DataFileSerializer(
nested=True,
read_only=True
)
class Meta:
model = ConfigContext
fields = [
'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites',
'locations', 'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters',
'tenant_groups', 'tenants', 'tags', 'data_source', 'data_path', 'data_file', 'data_synced', 'data',
'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')

View File

@ -0,0 +1,30 @@
from rest_framework import serializers
from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer
from extras.models import ConfigTemplate
from netbox.api.serializers import ValidatedModelSerializer
from netbox.api.serializers.features import TaggableModelSerializer
__all__ = (
'ConfigTemplateSerializer',
)
class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail')
data_source = DataSourceSerializer(
nested=True,
required=False
)
data_file = DataFileSerializer(
nested=True,
required=False
)
class Meta:
model = ConfigTemplate
fields = [
'id', 'url', 'display', 'name', 'description', 'environment_params', 'template_code', 'data_source',
'data_path', 'data_file', 'data_synced', 'tags', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')

View File

@ -0,0 +1,91 @@
from django.utils.translation import gettext as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from core.models import ObjectType
from extras.choices import *
from extras.models import CustomField, CustomFieldChoiceSet
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.serializers import ValidatedModelSerializer
__all__ = (
'CustomFieldChoiceSetSerializer',
'CustomFieldSerializer',
)
class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
base_choices = ChoiceField(
choices=CustomFieldChoiceSetBaseChoices,
required=False
)
extra_choices = serializers.ListField(
child=serializers.ListField(
min_length=2,
max_length=2
)
)
class Meta:
model = CustomFieldChoiceSet
fields = [
'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically',
'choices_count', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description', 'choices_count')
class CustomFieldSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
object_types = ContentTypeField(
queryset=ObjectType.objects.with_feature('custom_fields'),
many=True
)
type = ChoiceField(choices=CustomFieldTypeChoices)
related_object_type = ContentTypeField(
queryset=ObjectType.objects.all(),
required=False,
allow_null=True
)
filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
data_type = serializers.SerializerMethodField()
choice_set = CustomFieldChoiceSetSerializer(
nested=True,
required=False,
allow_null=True
)
ui_visible = ChoiceField(choices=CustomFieldUIVisibleChoices, required=False)
ui_editable = ChoiceField(choices=CustomFieldUIEditableChoices, required=False)
class Meta:
model = CustomField
fields = [
'id', 'url', 'display', 'object_types', 'type', 'related_object_type', 'data_type', 'name', 'label',
'group_name', 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable',
'is_cloneable', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
'choice_set', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
def validate_type(self, value):
if self.instance and self.instance.type != value:
raise serializers.ValidationError(_('Changing the type of custom fields is not supported.'))
return value
@extend_schema_field(OpenApiTypes.STR)
def get_data_type(self, obj):
types = CustomFieldTypeChoices
if obj.type == types.TYPE_INTEGER:
return 'integer'
if obj.type == types.TYPE_DECIMAL:
return 'decimal'
if obj.type == types.TYPE_BOOLEAN:
return 'boolean'
if obj.type in (types.TYPE_JSON, types.TYPE_OBJECT):
return 'object'
if obj.type in (types.TYPE_MULTISELECT, types.TYPE_MULTIOBJECT):
return 'array'
return 'string'

View File

@ -0,0 +1,26 @@
from rest_framework import serializers
from core.models import ObjectType
from extras.models import CustomLink
from netbox.api.fields import ContentTypeField
from netbox.api.serializers import ValidatedModelSerializer
__all__ = (
'CustomLinkSerializer',
)
class CustomLinkSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
object_types = ContentTypeField(
queryset=ObjectType.objects.with_feature('custom_links'),
many=True
)
class Meta:
model = CustomLink
fields = [
'id', 'url', 'display', 'object_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name',
'button_class', 'new_window', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name')

View File

@ -0,0 +1,13 @@
from rest_framework import serializers
from extras.models import Dashboard
__all__ = (
'DashboardSerializer',
)
class DashboardSerializer(serializers.ModelSerializer):
class Meta:
model = Dashboard
fields = ('layout', 'config')

View File

@ -0,0 +1,71 @@
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from core.models import ObjectType
from extras.choices import *
from extras.models import EventRule, Webhook
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.serializers import NetBoxModelSerializer
from utilities.api import get_serializer_for_model
from .scripts import ScriptSerializer
__all__ = (
'EventRuleSerializer',
'WebhookSerializer',
)
#
# Event Rules
#
class EventRuleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail')
object_types = ContentTypeField(
queryset=ObjectType.objects.with_feature('event_rules'),
many=True
)
action_type = ChoiceField(choices=EventRuleActionChoices)
action_object_type = ContentTypeField(
queryset=ObjectType.objects.with_feature('event_rules'),
)
action_object = serializers.SerializerMethodField(read_only=True)
class Meta:
model = EventRule
fields = [
'id', 'url', 'display', 'object_types', 'name', 'type_create', 'type_update', 'type_delete',
'type_job_start', 'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type',
'action_object_id', 'action_object', 'description', 'custom_fields', 'tags', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
@extend_schema_field(OpenApiTypes.OBJECT)
def get_action_object(self, instance):
context = {'request': self.context['request']}
# We need to manually instantiate the serializer for scripts
if instance.action_type == EventRuleActionChoices.SCRIPT:
script = instance.action_object
instance = script.python_class() if script.python_class else None
return ScriptSerializer(instance, nested=True, context=context).data
else:
serializer = get_serializer_for_model(instance.action_object_type.model_class())
return serializer(instance.action_object, nested=True, context=context).data
#
# Webhooks
#
class WebhookSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail')
class Meta:
model = Webhook
fields = [
'id', 'url', 'display', 'name', 'description', 'payload_url', 'http_method', 'http_content_type',
'additional_headers', 'body_template', 'secret', 'ssl_verification', 'ca_file_path', 'custom_fields',
'tags', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')

View File

@ -0,0 +1,36 @@
from rest_framework import serializers
from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer
from core.models import ObjectType
from extras.models import ExportTemplate
from netbox.api.fields import ContentTypeField
from netbox.api.serializers import ValidatedModelSerializer
__all__ = (
'ExportTemplateSerializer',
)
class ExportTemplateSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')
object_types = ContentTypeField(
queryset=ObjectType.objects.with_feature('export_templates'),
many=True
)
data_source = DataSourceSerializer(
nested=True,
required=False
)
data_file = DataFileSerializer(
nested=True,
read_only=True
)
class Meta:
model = ExportTemplate
fields = [
'id', 'url', 'display', 'object_types', 'name', 'description', 'template_code', 'mime_type',
'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created',
'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')

View File

@ -0,0 +1,63 @@
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from core.models import ObjectType
from extras.choices import *
from extras.models import JournalEntry
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.serializers import NetBoxModelSerializer
from utilities.api import get_serializer_for_model
__all__ = (
'JournalEntrySerializer',
)
class JournalEntrySerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
assigned_object_type = ContentTypeField(
queryset=ObjectType.objects.all()
)
assigned_object = serializers.SerializerMethodField(read_only=True)
created_by = serializers.PrimaryKeyRelatedField(
allow_null=True,
queryset=get_user_model().objects.all(),
required=False,
default=serializers.CurrentUserDefault()
)
kind = ChoiceField(
choices=JournalEntryKindChoices,
required=False
)
class Meta:
model = JournalEntry
fields = [
'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created',
'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'created')
def validate(self, data):
# Validate that the parent object exists
if 'assigned_object_type' in data and 'assigned_object_id' in data:
try:
data['assigned_object_type'].get_object_for_this_type(id=data['assigned_object_id'])
except ObjectDoesNotExist:
raise serializers.ValidationError(
f"Invalid assigned_object: {data['assigned_object_type']} ID {data['assigned_object_id']}"
)
# Enforce model validation
super().validate(data)
return data
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_assigned_object(self, instance):
serializer = get_serializer_for_model(instance.assigned_object_type.model_class())
context = {'request': self.context['request']}
return serializer(instance.assigned_object, nested=True, context=context).data

View File

@ -0,0 +1,16 @@
from rest_framework import serializers
from core.models import ObjectType
from netbox.api.serializers import BaseModelSerializer
__all__ = (
'ObjectTypeSerializer',
)
class ObjectTypeSerializer(BaseModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:objecttype-detail')
class Meta:
model = ObjectType
fields = ['id', 'url', 'display', 'app_label', 'model']

View File

@ -0,0 +1,26 @@
from rest_framework import serializers
from core.models import ObjectType
from extras.models import SavedFilter
from netbox.api.fields import ContentTypeField
from netbox.api.serializers import ValidatedModelSerializer
__all__ = (
'SavedFilterSerializer',
)
class SavedFilterSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
object_types = ContentTypeField(
queryset=ObjectType.objects.all(),
many=True
)
class Meta:
model = SavedFilter
fields = [
'id', 'url', 'display', 'object_types', 'name', 'slug', 'description', 'user', 'weight', 'enabled',
'shared', 'parameters', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')

View File

@ -0,0 +1,77 @@
from django.utils.translation import gettext as _
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from core.api.serializers_.jobs import JobSerializer
from extras.models import Script
from netbox.api.serializers import ValidatedModelSerializer
__all__ = (
'ScriptDetailSerializer',
'ScriptInputSerializer',
'ScriptSerializer',
)
class ScriptSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:script-detail')
description = serializers.SerializerMethodField(read_only=True)
vars = serializers.SerializerMethodField(read_only=True)
result = JobSerializer(nested=True, read_only=True)
class Meta:
model = Script
fields = [
'id', 'url', 'module', 'name', 'description', 'vars', 'result', 'display', 'is_executable',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_vars(self, obj):
if obj.python_class:
return {
k: v.__class__.__name__ for k, v in obj.python_class()._get_vars().items()
}
else:
return {}
@extend_schema_field(serializers.CharField())
def get_display(self, obj):
return f'{obj.name} ({obj.module})'
@extend_schema_field(serializers.CharField())
def get_description(self, obj):
if obj.python_class:
return obj.python_class().description
else:
return None
class ScriptDetailSerializer(ScriptSerializer):
result = serializers.SerializerMethodField(read_only=True)
@extend_schema_field(JobSerializer())
def get_result(self, obj):
job = obj.jobs.all().order_by('-created').first()
context = {
'request': self.context['request']
}
data = JobSerializer(job, context=context).data
return data
class ScriptInputSerializer(serializers.Serializer):
data = serializers.JSONField()
commit = serializers.BooleanField()
schedule_at = serializers.DateTimeField(required=False, allow_null=True)
interval = serializers.IntegerField(required=False, allow_null=True)
def validate_schedule_at(self, value):
if value and not self.context['script'].scheduling_enabled:
raise serializers.ValidationError(_("Scheduling is not enabled for this script."))
return value
def validate_interval(self, value):
if value and not self.context['script'].scheduling_enabled:
raise serializers.ValidationError(_("Scheduling is not enabled for this script."))
return value

View File

@ -0,0 +1,30 @@
from rest_framework import serializers
from core.models import ObjectType
from extras.models import Tag
from netbox.api.fields import ContentTypeField, RelatedObjectCountField
from netbox.api.serializers import ValidatedModelSerializer
__all__ = (
'TagSerializer',
)
class TagSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
object_types = ContentTypeField(
queryset=ObjectType.objects.with_feature('tags'),
many=True,
required=False
)
# Related object counts
tagged_items = RelatedObjectCountField('extras_taggeditem_items')
class Meta:
model = Tag
fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'object_types', 'tagged_items', 'created',
'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'color', 'description')

View File

@ -22,7 +22,7 @@ router.register('config-contexts', views.ConfigContextViewSet)
router.register('config-templates', views.ConfigTemplateViewSet) router.register('config-templates', views.ConfigTemplateViewSet)
router.register('scripts', views.ScriptViewSet, basename='script') router.register('scripts', views.ScriptViewSet, basename='script')
router.register('object-changes', views.ObjectChangeViewSet) router.register('object-changes', views.ObjectChangeViewSet)
router.register('content-types', views.ContentTypeViewSet) router.register('object-types', views.ObjectTypeViewSet)
app_name = 'extras-api' app_name = 'extras-api'
urlpatterns = [ urlpatterns = [

View File

@ -1,4 +1,3 @@
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django_rq.queues import get_connection from django_rq.queues import get_connection
from rest_framework import status from rest_framework import status
@ -11,7 +10,7 @@ from rest_framework.routers import APIRootView
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rq import Worker from rq import Worker
from core.models import Job from core.models import Job, ObjectType
from extras import filtersets from extras import filtersets
from extras.models import * from extras.models import *
from extras.scripts import run_script from extras.scripts import run_script
@ -275,17 +274,17 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet):
# #
# ContentTypes # Object types
# #
class ContentTypeViewSet(ReadOnlyModelViewSet): class ObjectTypeViewSet(ReadOnlyModelViewSet):
""" """
Read-only list of ContentTypes. Limit results to ContentTypes pertinent to NetBox objects. Read-only list of ObjectTypes.
""" """
permission_classes = [IsAuthenticatedOrLoginNotRequired] permission_classes = [IsAuthenticatedOrLoginNotRequired]
queryset = ContentType.objects.order_by('app_label', 'model') queryset = ObjectType.objects.order_by('app_label', 'model')
serializer_class = serializers.ContentTypeSerializer serializer_class = serializers.ObjectTypeSerializer
filterset_class = filtersets.ContentTypeFilterSet filterset_class = filtersets.ObjectTypeFilterSet
# #

View File

@ -12,7 +12,7 @@ from django.template.loader import render_to_string
from django.urls import NoReverseMatch, resolve, reverse from django.urls import NoReverseMatch, resolve, reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from core.models import ContentType from core.models import ObjectType
from extras.choices import BookmarkOrderingChoices from extras.choices import BookmarkOrderingChoices
from utilities.choices import ButtonColorChoices from utilities.choices import ButtonColorChoices
from utilities.permissions import get_permission_for_model from utilities.permissions import get_permission_for_model
@ -34,14 +34,14 @@ __all__ = (
def get_object_type_choices(): def get_object_type_choices():
return [ return [
(content_type_identifier(ct), content_type_name(ct)) (content_type_identifier(ct), content_type_name(ct))
for ct in ContentType.objects.public().order_by('app_label', 'model') for ct in ObjectType.objects.public().order_by('app_label', 'model')
] ]
def get_bookmarks_object_type_choices(): def get_bookmarks_object_type_choices():
return [ return [
(content_type_identifier(ct), content_type_name(ct)) (content_type_identifier(ct), content_type_name(ct))
for ct in ContentType.objects.with_feature('bookmarks').order_by('app_label', 'model') for ct in ObjectType.objects.with_feature('bookmarks').order_by('app_label', 'model')
] ]
@ -52,7 +52,7 @@ def get_models_from_content_types(content_types):
models = [] models = []
for content_type_id in content_types: for content_type_id in content_types:
app_label, model_name = content_type_id.split('.') app_label, model_name = content_type_id.split('.')
content_type = ContentType.objects.get_by_natural_key(app_label, model_name) content_type = ObjectType.objects.get_by_natural_key(app_label, model_name)
models.append(content_type.model_class()) models.append(content_type.model_class())
return models return models
@ -238,7 +238,7 @@ class ObjectListWidget(DashboardWidget):
def render(self, request): def render(self, request):
app_label, model_name = self.config['model'].split('.') app_label, model_name = self.config['model'].split('.')
model = ContentType.objects.get_by_natural_key(app_label, model_name).model_class() model = ObjectType.objects.get_by_natural_key(app_label, model_name).model_class()
viewname = get_viewname(model, action='list') viewname = get_viewname(model, action='list')
# Evaluate user's permission. Note that this controls only whether the HTMX element is # Evaluate user's permission. Note that this controls only whether the HTMX element is
@ -371,7 +371,7 @@ class BookmarksWidget(DashboardWidget):
bookmarks = Bookmark.objects.filter(user=request.user).order_by(self.config['order_by']) bookmarks = Bookmark.objects.filter(user=request.user).order_by(self.config['order_by'])
if object_types := self.config.get('object_types'): if object_types := self.config.get('object_types'):
models = get_models_from_content_types(object_types) models = get_models_from_content_types(object_types)
conent_types = ContentType.objects.get_for_models(*models).values() conent_types = ObjectType.objects.get_for_models(*models).values()
bookmarks = bookmarks.filter(object_type__in=conent_types) bookmarks = bookmarks.filter(object_type__in=conent_types)
if max_items := self.config.get('max_items'): if max_items := self.config.get('max_items'):
bookmarks = bookmarks[:max_items] bookmarks = bookmarks[:max_items]

View File

@ -155,7 +155,7 @@ def process_event_queue(events):
if content_type not in events_cache[action_flag]: if content_type not in events_cache[action_flag]:
events_cache[action_flag][content_type] = EventRule.objects.filter( events_cache[action_flag][content_type] = EventRule.objects.filter(
**{action_flag: True}, **{action_flag: True},
content_types=content_type, object_types=content_type,
enabled=True enabled=True
) )
event_rules = events_cache[action_flag][content_type] event_rules = events_cache[action_flag][content_type]

View File

@ -4,7 +4,7 @@ from django.contrib.contenttypes.models import ContentType
from django.db.models import Q from django.db.models import Q
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from core.models import DataSource from core.models import DataSource, ObjectType
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
@ -18,7 +18,6 @@ __all__ = (
'BookmarkFilterSet', 'BookmarkFilterSet',
'ConfigContextFilterSet', 'ConfigContextFilterSet',
'ConfigTemplateFilterSet', 'ConfigTemplateFilterSet',
'ContentTypeFilterSet',
'CustomFieldChoiceSetFilterSet', 'CustomFieldChoiceSetFilterSet',
'CustomFieldFilterSet', 'CustomFieldFilterSet',
'CustomLinkFilterSet', 'CustomLinkFilterSet',
@ -28,6 +27,7 @@ __all__ = (
'JournalEntryFilterSet', 'JournalEntryFilterSet',
'LocalConfigContextFilterSet', 'LocalConfigContextFilterSet',
'ObjectChangeFilterSet', 'ObjectChangeFilterSet',
'ObjectTypeFilterSet',
'SavedFilterFilterSet', 'SavedFilterFilterSet',
'ScriptFilterSet', 'ScriptFilterSet',
'TagFilterSet', 'TagFilterSet',
@ -40,12 +40,14 @@ class ScriptFilterSet(BaseFilterSet):
method='search', method='search',
label=_('Search'), label=_('Search'),
) )
module_id = django_filters.ModelMultipleChoiceFilter(
queryset=ScriptModule.objects.all(),
label=_('Script module (ID)'),
)
class Meta: class Meta:
model = Script model = Script
fields = [ fields = ('id', 'name', 'is_executable')
'id', 'name',
]
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -69,10 +71,10 @@ class WebhookFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = Webhook model = Webhook
fields = [ fields = (
'id', 'name', 'payload_url', 'http_method', 'http_content_type', 'secret', 'ssl_verification', 'id', 'name', 'payload_url', 'http_method', 'http_content_type', 'secret', 'ssl_verification',
'ca_file_path', 'description', 'ca_file_path', 'description',
] )
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -89,10 +91,13 @@ class EventRuleFilterSet(NetBoxModelFilterSet):
method='search', method='search',
label=_('Search'), label=_('Search'),
) )
content_type_id = MultiValueNumberFilter( object_type_id = django_filters.ModelMultipleChoiceFilter(
field_name='content_types__id' queryset=ObjectType.objects.all(),
field_name='object_types'
)
object_type = ContentTypeFilter(
field_name='object_types'
) )
content_types = ContentTypeFilter()
action_type = django_filters.MultipleChoiceFilter( action_type = django_filters.MultipleChoiceFilter(
choices=EventRuleActionChoices choices=EventRuleActionChoices
) )
@ -101,10 +106,10 @@ class EventRuleFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = EventRule model = EventRule
fields = [ fields = (
'id', 'name', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'enabled', 'id', 'name', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'enabled',
'action_type', 'description', 'action_type', 'description',
] )
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -116,7 +121,7 @@ class EventRuleFilterSet(NetBoxModelFilterSet):
) )
class CustomFieldFilterSet(BaseFilterSet): class CustomFieldFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label=_('Search'), label=_('Search'),
@ -124,10 +129,18 @@ class CustomFieldFilterSet(BaseFilterSet):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=CustomFieldTypeChoices choices=CustomFieldTypeChoices
) )
content_type_id = MultiValueNumberFilter( object_type_id = django_filters.ModelMultipleChoiceFilter(
field_name='content_types__id' queryset=ObjectType.objects.all(),
field_name='object_types'
) )
content_types = ContentTypeFilter() object_type = ContentTypeFilter(
field_name='object_types'
)
related_object_type_id = django_filters.ModelMultipleChoiceFilter(
queryset=ObjectType.objects.all(),
field_name='related_object_type'
)
related_object_type = ContentTypeFilter()
choice_set_id = django_filters.ModelMultipleChoiceFilter( choice_set_id = django_filters.ModelMultipleChoiceFilter(
queryset=CustomFieldChoiceSet.objects.all() queryset=CustomFieldChoiceSet.objects.all()
) )
@ -139,10 +152,11 @@ class CustomFieldFilterSet(BaseFilterSet):
class Meta: class Meta:
model = CustomField model = CustomField
fields = [ fields = (
'id', 'content_types', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'id', 'name', 'label', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible',
'ui_editable', 'weight', 'is_cloneable', 'description', 'ui_editable', 'weight', 'is_cloneable', 'description', 'validation_minimum', 'validation_maximum',
] 'validation_regex',
)
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -155,7 +169,7 @@ class CustomFieldFilterSet(BaseFilterSet):
) )
class CustomFieldChoiceSetFilterSet(BaseFilterSet): class CustomFieldChoiceSetFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label=_('Search'), label=_('Search'),
@ -166,9 +180,9 @@ class CustomFieldChoiceSetFilterSet(BaseFilterSet):
class Meta: class Meta:
model = CustomFieldChoiceSet model = CustomFieldChoiceSet
fields = [ fields = (
'id', 'name', 'description', 'base_choices', 'order_alphabetically', 'id', 'name', 'description', 'base_choices', 'order_alphabetically',
] )
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -183,21 +197,24 @@ class CustomFieldChoiceSetFilterSet(BaseFilterSet):
return queryset.filter(extra_choices__overlap=value) return queryset.filter(extra_choices__overlap=value)
class CustomLinkFilterSet(BaseFilterSet): class CustomLinkFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label=_('Search'), label=_('Search'),
) )
content_type_id = MultiValueNumberFilter( object_type_id = django_filters.ModelMultipleChoiceFilter(
field_name='content_types__id' queryset=ObjectType.objects.all(),
field_name='object_types'
)
object_type = ContentTypeFilter(
field_name='object_types'
) )
content_types = ContentTypeFilter()
class Meta: class Meta:
model = CustomLink model = CustomLink
fields = [ fields = (
'id', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'new_window', 'id', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'new_window', 'button_class',
] )
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -210,15 +227,18 @@ class CustomLinkFilterSet(BaseFilterSet):
) )
class ExportTemplateFilterSet(BaseFilterSet): class ExportTemplateFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label=_('Search'), label=_('Search'),
) )
content_type_id = MultiValueNumberFilter( object_type_id = django_filters.ModelMultipleChoiceFilter(
field_name='content_types__id' queryset=ObjectType.objects.all(),
field_name='object_types'
)
object_type = ContentTypeFilter(
field_name='object_types'
) )
content_types = ContentTypeFilter()
data_source_id = django_filters.ModelMultipleChoiceFilter( data_source_id = django_filters.ModelMultipleChoiceFilter(
queryset=DataSource.objects.all(), queryset=DataSource.objects.all(),
label=_('Data source (ID)'), label=_('Data source (ID)'),
@ -230,7 +250,10 @@ class ExportTemplateFilterSet(BaseFilterSet):
class Meta: class Meta:
model = ExportTemplate model = ExportTemplate
fields = ['id', 'content_types', 'name', 'description', 'data_synced'] fields = (
'id', 'name', 'description', 'mime_type', 'file_extension', 'as_attachment', 'auto_sync_enabled',
'data_synced',
)
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -241,15 +264,18 @@ class ExportTemplateFilterSet(BaseFilterSet):
) )
class SavedFilterFilterSet(BaseFilterSet): class SavedFilterFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label=_('Search'), label=_('Search'),
) )
content_type_id = MultiValueNumberFilter( object_type_id = django_filters.ModelMultipleChoiceFilter(
field_name='content_types__id' queryset=ObjectType.objects.all(),
field_name='object_types'
)
object_type = ContentTypeFilter(
field_name='object_types'
) )
content_types = ContentTypeFilter()
user_id = django_filters.ModelMultipleChoiceFilter( user_id = django_filters.ModelMultipleChoiceFilter(
queryset=get_user_model().objects.all(), queryset=get_user_model().objects.all(),
label=_('User (ID)'), label=_('User (ID)'),
@ -266,7 +292,7 @@ class SavedFilterFilterSet(BaseFilterSet):
class Meta: class Meta:
model = SavedFilter model = SavedFilter
fields = ['id', 'content_types', 'name', 'slug', 'description', 'enabled', 'shared', 'weight'] fields = ('id', 'name', 'slug', 'description', 'enabled', 'shared', 'weight')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -307,20 +333,19 @@ class BookmarkFilterSet(BaseFilterSet):
class Meta: class Meta:
model = Bookmark model = Bookmark
fields = ['id', 'object_id'] fields = ('id', 'object_id')
class ImageAttachmentFilterSet(BaseFilterSet): class ImageAttachmentFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label=_('Search'), label=_('Search'),
) )
created = django_filters.DateTimeFilter() object_type = ContentTypeFilter()
content_type = ContentTypeFilter()
class Meta: class Meta:
model = ImageAttachment model = ImageAttachment
fields = ['id', 'content_type_id', 'object_id', 'name'] fields = ('id', 'object_type_id', 'object_id', 'name', 'image_width', 'image_height')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -350,7 +375,7 @@ class JournalEntryFilterSet(NetBoxModelFilterSet):
class Meta: class Meta:
model = JournalEntry model = JournalEntry
fields = ['id', 'assigned_object_type_id', 'assigned_object_id', 'created', 'kind'] fields = ('id', 'assigned_object_type_id', 'assigned_object_id', 'created', 'kind')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -375,7 +400,7 @@ class TagFilterSet(ChangeLoggedModelFilterSet):
class Meta: class Meta:
model = Tag model = Tag
fields = ['id', 'name', 'slug', 'color', 'description', 'object_types'] fields = ('id', 'name', 'slug', 'color', 'description', 'object_types')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -472,12 +497,12 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
queryset=DeviceType.objects.all(), queryset=DeviceType.objects.all(),
label=_('Device type'), label=_('Device type'),
) )
role_id = django_filters.ModelMultipleChoiceFilter( device_role_id = django_filters.ModelMultipleChoiceFilter(
field_name='roles', field_name='roles',
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
label=_('Role'), label=_('Role'),
) )
role = django_filters.ModelMultipleChoiceFilter( device_role = django_filters.ModelMultipleChoiceFilter(
field_name='roles__slug', field_name='roles__slug',
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),
to_field_name='slug', to_field_name='slug',
@ -563,9 +588,13 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
label=_('Data file (ID)'), label=_('Data file (ID)'),
) )
# TODO: Remove in v4.1
role = device_role
role_id = device_role_id
class Meta: class Meta:
model = ConfigContext model = ConfigContext
fields = ['id', 'name', 'is_active', 'data_synced', 'description'] fields = ('id', 'name', 'is_active', 'description', 'weight', 'auto_sync_enabled', 'data_synced')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -577,7 +606,7 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
) )
class ConfigTemplateFilterSet(BaseFilterSet): class ConfigTemplateFilterSet(ChangeLoggedModelFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label=_('Search'), label=_('Search'),
@ -594,7 +623,7 @@ class ConfigTemplateFilterSet(BaseFilterSet):
class Meta: class Meta:
model = ConfigTemplate model = ConfigTemplate
fields = ['id', 'name', 'description', 'data_synced'] fields = ('id', 'name', 'description', 'auto_sync_enabled', 'data_synced')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -642,10 +671,10 @@ class ObjectChangeFilterSet(BaseFilterSet):
class Meta: class Meta:
model = ObjectChange model = ObjectChange
fields = [ fields = (
'id', 'user', 'user_name', 'request_id', 'action', 'changed_object_type_id', 'changed_object_id', 'id', 'user', 'user_name', 'request_id', 'action', 'changed_object_type_id', 'changed_object_id',
'object_repr', 'related_object_type', 'related_object_id', 'object_repr',
] )
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():
@ -660,15 +689,15 @@ class ObjectChangeFilterSet(BaseFilterSet):
# ContentTypes # ContentTypes
# #
class ContentTypeFilterSet(django_filters.FilterSet): class ObjectTypeFilterSet(django_filters.FilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label=_('Search'), label=_('Search'),
) )
class Meta: class Meta:
model = ContentType model = ObjectType
fields = ['id', 'app_label', 'model'] fields = ('id', 'app_label', 'model')
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@ -6,7 +6,7 @@ from django.core.exceptions import ObjectDoesNotExist
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from core.models import ContentType from core.models import ObjectType
from extras.choices import * from extras.choices import *
from extras.models import * from extras.models import *
from netbox.forms import NetBoxModelImportForm from netbox.forms import NetBoxModelImportForm
@ -30,9 +30,9 @@ __all__ = (
class CustomFieldImportForm(CSVModelForm): class CustomFieldImportForm(CSVModelForm):
content_types = CSVMultipleContentTypeField( object_types = CSVMultipleContentTypeField(
label=_('Content types'), label=_('Object types'),
queryset=ContentType.objects.with_feature('custom_fields'), queryset=ObjectType.objects.with_feature('custom_fields'),
help_text=_("One or more assigned object types") help_text=_("One or more assigned object types")
) )
type = CSVChoiceField( type = CSVChoiceField(
@ -40,9 +40,9 @@ class CustomFieldImportForm(CSVModelForm):
choices=CustomFieldTypeChoices, choices=CustomFieldTypeChoices,
help_text=_('Field data type (e.g. text, integer, etc.)') help_text=_('Field data type (e.g. text, integer, etc.)')
) )
object_type = CSVContentTypeField( related_object_type = CSVContentTypeField(
label=_('Object type'), label=_('Object type'),
queryset=ContentType.objects.public(), queryset=ObjectType.objects.public(),
required=False, required=False,
help_text=_("Object type (for object or multi-object fields)") help_text=_("Object type (for object or multi-object fields)")
) )
@ -69,7 +69,7 @@ class CustomFieldImportForm(CSVModelForm):
class Meta: class Meta:
model = CustomField model = CustomField
fields = ( fields = (
'name', 'label', 'group_name', 'type', 'content_types', 'object_type', 'required', 'description', 'name', 'label', 'group_name', 'type', 'object_types', 'related_object_type', 'required', 'description',
'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum', 'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum',
'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable', 'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable',
) )
@ -111,31 +111,31 @@ class CustomFieldChoiceSetImportForm(CSVModelForm):
class CustomLinkImportForm(CSVModelForm): class CustomLinkImportForm(CSVModelForm):
content_types = CSVMultipleContentTypeField( object_types = CSVMultipleContentTypeField(
label=_('Content types'), label=_('Object types'),
queryset=ContentType.objects.with_feature('custom_links'), queryset=ObjectType.objects.with_feature('custom_links'),
help_text=_("One or more assigned object types") help_text=_("One or more assigned object types")
) )
class Meta: class Meta:
model = CustomLink model = CustomLink
fields = ( fields = (
'name', 'content_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window', 'link_text', 'name', 'object_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window', 'link_text',
'link_url', 'link_url',
) )
class ExportTemplateImportForm(CSVModelForm): class ExportTemplateImportForm(CSVModelForm):
content_types = CSVMultipleContentTypeField( object_types = CSVMultipleContentTypeField(
label=_('Content types'), label=_('Object types'),
queryset=ContentType.objects.with_feature('export_templates'), queryset=ObjectType.objects.with_feature('export_templates'),
help_text=_("One or more assigned object types") help_text=_("One or more assigned object types")
) )
class Meta: class Meta:
model = ExportTemplate model = ExportTemplate
fields = ( fields = (
'name', 'content_types', 'description', 'mime_type', 'file_extension', 'as_attachment', 'template_code', 'name', 'object_types', 'description', 'mime_type', 'file_extension', 'as_attachment', 'template_code',
) )
@ -149,16 +149,16 @@ class ConfigTemplateImportForm(CSVModelForm):
class SavedFilterImportForm(CSVModelForm): class SavedFilterImportForm(CSVModelForm):
content_types = CSVMultipleContentTypeField( object_types = CSVMultipleContentTypeField(
label=_('Content types'), label=_('Object types'),
queryset=ContentType.objects.all(), queryset=ObjectType.objects.all(),
help_text=_("One or more assigned object types") help_text=_("One or more assigned object types")
) )
class Meta: class Meta:
model = SavedFilter model = SavedFilter
fields = ( fields = (
'name', 'slug', 'content_types', 'description', 'weight', 'enabled', 'shared', 'parameters', 'name', 'slug', 'object_types', 'description', 'weight', 'enabled', 'shared', 'parameters',
) )
@ -173,9 +173,9 @@ class WebhookImportForm(NetBoxModelImportForm):
class EventRuleImportForm(NetBoxModelImportForm): class EventRuleImportForm(NetBoxModelImportForm):
content_types = CSVMultipleContentTypeField( object_types = CSVMultipleContentTypeField(
label=_('Content types'), label=_('Object types'),
queryset=ContentType.objects.with_feature('event_rules'), queryset=ObjectType.objects.with_feature('event_rules'),
help_text=_("One or more assigned object types") help_text=_("One or more assigned object types")
) )
action_object = forms.CharField( action_object = forms.CharField(
@ -187,7 +187,7 @@ class EventRuleImportForm(NetBoxModelImportForm):
class Meta: class Meta:
model = EventRule model = EventRule
fields = ( fields = (
'name', 'description', 'enabled', 'conditions', 'content_types', 'type_create', 'type_update', 'name', 'description', 'enabled', 'conditions', 'object_types', 'type_create', 'type_update',
'type_delete', 'type_job_start', 'type_job_end', 'action_type', 'action_object', 'comments', 'tags' 'type_delete', 'type_job_start', 'type_job_end', 'action_type', 'action_object', 'comments', 'tags'
) )
@ -213,7 +213,7 @@ class EventRuleImportForm(NetBoxModelImportForm):
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise forms.ValidationError(_("Script {name} not found").format(name=action_object)) raise forms.ValidationError(_("Script {name} not found").format(name=action_object))
self.instance.action_object = script self.instance.action_object = script
self.instance.action_object_type = ContentType.objects.get_for_model(script, for_concrete_model=False) self.instance.action_object_type = ObjectType.objects.get_for_model(script, for_concrete_model=False)
class TagImportForm(CSVModelForm): class TagImportForm(CSVModelForm):
@ -229,7 +229,7 @@ class TagImportForm(CSVModelForm):
class JournalEntryImportForm(NetBoxModelImportForm): class JournalEntryImportForm(NetBoxModelImportForm):
assigned_object_type = CSVContentTypeField( assigned_object_type = CSVContentTypeField(
queryset=ContentType.objects.all(), queryset=ObjectType.objects.all(),
label=_('Assigned object type'), label=_('Assigned object type'),
) )
kind = CSVChoiceField( kind = CSVChoiceField(

View File

@ -2,7 +2,7 @@ from django import forms
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from core.models import ContentType, DataFile, DataSource from core.models import ObjectType, DataFile, DataSource
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from extras.choices import * from extras.choices import *
from extras.models import * from extras.models import *
@ -38,14 +38,14 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), (None, ('q', 'filter_id')),
(_('Attributes'), ( (_('Attributes'), (
'type', 'content_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', 'ui_editable', 'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible',
'is_cloneable', 'ui_editable', 'is_cloneable',
)), )),
) )
content_type_id = ContentTypeMultipleChoiceField( related_object_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.with_feature('custom_fields'), queryset=ObjectType.objects.with_feature('custom_fields'),
required=False, required=False,
label=_('Object type') label=_('Related object type')
) )
type = forms.MultipleChoiceField( type = forms.MultipleChoiceField(
choices=CustomFieldTypeChoices, choices=CustomFieldTypeChoices,
@ -108,11 +108,11 @@ class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
class CustomLinkFilterForm(SavedFiltersMixin, FilterForm): class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), (None, ('q', 'filter_id')),
(_('Attributes'), ('content_types', 'enabled', 'new_window', 'weight')), (_('Attributes'), ('object_type', 'enabled', 'new_window', 'weight')),
) )
content_types = ContentTypeMultipleChoiceField( object_type = ContentTypeMultipleChoiceField(
label=_('Content types'), label=_('Object types'),
queryset=ContentType.objects.with_feature('custom_links'), queryset=ObjectType.objects.with_feature('custom_links'),
required=False required=False
) )
enabled = forms.NullBooleanField( enabled = forms.NullBooleanField(
@ -139,7 +139,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), (None, ('q', 'filter_id')),
(_('Data'), ('data_source_id', 'data_file_id')), (_('Data'), ('data_source_id', 'data_file_id')),
(_('Attributes'), ('content_type_id', 'mime_type', 'file_extension', 'as_attachment')), (_('Attributes'), ('object_type_id', 'mime_type', 'file_extension', 'as_attachment')),
) )
data_source_id = DynamicModelMultipleChoiceField( data_source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(), queryset=DataSource.objects.all(),
@ -154,8 +154,8 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
'source_id': '$data_source_id' 'source_id': '$data_source_id'
} }
) )
content_type_id = ContentTypeMultipleChoiceField( object_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.with_feature('export_templates'), queryset=ObjectType.objects.with_feature('export_templates'),
required=False, required=False,
label=_('Content types') label=_('Content types')
) )
@ -179,11 +179,11 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm): class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), (None, ('q', 'filter_id')),
(_('Attributes'), ('content_type_id', 'name',)), (_('Attributes'), ('object_type_id', 'name',)),
) )
content_type_id = ContentTypeChoiceField( object_type_id = ContentTypeChoiceField(
label=_('Content type'), label=_('Object type'),
queryset=ContentType.objects.with_feature('image_attachments'), queryset=ObjectType.objects.with_feature('image_attachments'),
required=False required=False
) )
name = forms.CharField( name = forms.CharField(
@ -195,11 +195,11 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
class SavedFilterFilterForm(SavedFiltersMixin, FilterForm): class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id')), (None, ('q', 'filter_id')),
(_('Attributes'), ('content_types', 'enabled', 'shared', 'weight')), (_('Attributes'), ('object_type', 'enabled', 'shared', 'weight')),
) )
content_types = ContentTypeMultipleChoiceField( object_type = ContentTypeMultipleChoiceField(
label=_('Content types'), label=_('Object types'),
queryset=ContentType.objects.public(), queryset=ObjectType.objects.public(),
required=False required=False
) )
enabled = forms.NullBooleanField( enabled = forms.NullBooleanField(
@ -250,11 +250,11 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm):
fieldsets = ( fieldsets = (
(None, ('q', 'filter_id', 'tag')), (None, ('q', 'filter_id', 'tag')),
(_('Attributes'), ('content_type_id', 'action_type', 'enabled')), (_('Attributes'), ('object_type_id', 'action_type', 'enabled')),
(_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')), (_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
) )
content_type_id = ContentTypeMultipleChoiceField( object_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.with_feature('event_rules'), queryset=ObjectType.objects.with_feature('event_rules'),
required=False, required=False,
label=_('Object type') label=_('Object type')
) )
@ -310,12 +310,12 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm):
class TagFilterForm(SavedFiltersMixin, FilterForm): class TagFilterForm(SavedFiltersMixin, FilterForm):
model = Tag model = Tag
content_type_id = ContentTypeMultipleChoiceField( content_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.with_feature('tags'), queryset=ObjectType.objects.with_feature('tags'),
required=False, required=False,
label=_('Tagged object type') label=_('Tagged object type')
) )
for_object_type_id = ContentTypeChoiceField( for_object_type_id = ContentTypeChoiceField(
queryset=ContentType.objects.with_feature('tags'), queryset=ObjectType.objects.with_feature('tags'),
required=False, required=False,
label=_('Allowed object type') label=_('Allowed object type')
) )
@ -464,7 +464,7 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
label=_('User') label=_('User')
) )
assigned_object_type_id = DynamicModelMultipleChoiceField( assigned_object_type_id = DynamicModelMultipleChoiceField(
queryset=ContentType.objects.all(), queryset=ObjectType.objects.all(),
required=False, required=False,
label=_('Object Type'), label=_('Object Type'),
widget=APISelectMultiple( widget=APISelectMultiple(
@ -507,7 +507,7 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
label=_('User') label=_('User')
) )
changed_object_type_id = DynamicModelMultipleChoiceField( changed_object_type_id = DynamicModelMultipleChoiceField(
queryset=ContentType.objects.all(), queryset=ObjectType.objects.all(),
required=False, required=False,
label=_('Object Type'), label=_('Object Type'),
widget=APISelectMultiple( widget=APISelectMultiple(

View File

@ -2,12 +2,11 @@ import json
import re import re
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from core.forms.mixins import SyncedDataMixin from core.forms.mixins import SyncedDataMixin
from core.models import ContentType from core.models import ObjectType
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from extras.choices import * from extras.choices import *
from extras.models import * from extras.models import *
@ -39,13 +38,13 @@ __all__ = (
class CustomFieldForm(forms.ModelForm): class CustomFieldForm(forms.ModelForm):
content_types = ContentTypeMultipleChoiceField( object_types = ContentTypeMultipleChoiceField(
label=_('Content types'), label=_('Object types'),
queryset=ContentType.objects.with_feature('custom_fields') queryset=ObjectType.objects.with_feature('custom_fields')
) )
object_type = ContentTypeChoiceField( related_object_type = ContentTypeChoiceField(
label=_('Object type'), label=_('Related object type'),
queryset=ContentType.objects.public(), queryset=ObjectType.objects.public(),
required=False, required=False,
help_text=_("Type of the related object (for object/multi-object fields only)") help_text=_("Type of the related object (for object/multi-object fields only)")
) )
@ -56,7 +55,7 @@ class CustomFieldForm(forms.ModelForm):
fieldsets = ( fieldsets = (
(_('Custom Field'), ( (_('Custom Field'), (
'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'required', 'description', 'object_types', 'name', 'label', 'group_name', 'type', 'related_object_type', 'required', 'description',
)), )),
(_('Behavior'), ('search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable')), (_('Behavior'), ('search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'weight', 'is_cloneable')),
(_('Values'), ('default', 'choice_set')), (_('Values'), ('default', 'choice_set')),
@ -123,13 +122,13 @@ class CustomFieldChoiceSetForm(forms.ModelForm):
class CustomLinkForm(forms.ModelForm): class CustomLinkForm(forms.ModelForm):
content_types = ContentTypeMultipleChoiceField( object_types = ContentTypeMultipleChoiceField(
label=_('Content types'), label=_('Object types'),
queryset=ContentType.objects.with_feature('custom_links') queryset=ObjectType.objects.with_feature('custom_links')
) )
fieldsets = ( fieldsets = (
(_('Custom Link'), ('name', 'content_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')), (_('Custom Link'), ('name', 'object_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')),
(_('Templates'), ('link_text', 'link_url')), (_('Templates'), ('link_text', 'link_url')),
) )
@ -152,9 +151,9 @@ class CustomLinkForm(forms.ModelForm):
class ExportTemplateForm(SyncedDataMixin, forms.ModelForm): class ExportTemplateForm(SyncedDataMixin, forms.ModelForm):
content_types = ContentTypeMultipleChoiceField( object_types = ContentTypeMultipleChoiceField(
label=_('Content types'), label=_('Object types'),
queryset=ContentType.objects.with_feature('export_templates') queryset=ObjectType.objects.with_feature('export_templates')
) )
template_code = forms.CharField( template_code = forms.CharField(
label=_('Template code'), label=_('Template code'),
@ -163,7 +162,7 @@ class ExportTemplateForm(SyncedDataMixin, forms.ModelForm):
) )
fieldsets = ( fieldsets = (
(_('Export Template'), ('name', 'content_types', 'description', 'template_code')), (_('Export Template'), ('name', 'object_types', 'description', 'template_code')),
(_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')), (_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')),
(_('Rendering'), ('mime_type', 'file_extension', 'as_attachment')), (_('Rendering'), ('mime_type', 'file_extension', 'as_attachment')),
) )
@ -193,14 +192,14 @@ class ExportTemplateForm(SyncedDataMixin, forms.ModelForm):
class SavedFilterForm(forms.ModelForm): class SavedFilterForm(forms.ModelForm):
slug = SlugField() slug = SlugField()
content_types = ContentTypeMultipleChoiceField( object_types = ContentTypeMultipleChoiceField(
label=_('Content types'), label=_('Object types'),
queryset=ContentType.objects.all() queryset=ObjectType.objects.all()
) )
parameters = JSONField() parameters = JSONField()
fieldsets = ( fieldsets = (
(_('Saved Filter'), ('name', 'slug', 'content_types', 'description', 'weight', 'enabled', 'shared')), (_('Saved Filter'), ('name', 'slug', 'object_types', 'description', 'weight', 'enabled', 'shared')),
(_('Parameters'), ('parameters',)), (_('Parameters'), ('parameters',)),
) )
@ -221,7 +220,7 @@ class SavedFilterForm(forms.ModelForm):
class BookmarkForm(forms.ModelForm): class BookmarkForm(forms.ModelForm):
object_type = ContentTypeChoiceField( object_type = ContentTypeChoiceField(
label=_('Object type'), label=_('Object type'),
queryset=ContentType.objects.with_feature('bookmarks') queryset=ObjectType.objects.with_feature('bookmarks')
) )
class Meta: class Meta:
@ -249,9 +248,9 @@ class WebhookForm(NetBoxModelForm):
class EventRuleForm(NetBoxModelForm): class EventRuleForm(NetBoxModelForm):
content_types = ContentTypeMultipleChoiceField( object_types = ContentTypeMultipleChoiceField(
label=_('Content types'), label=_('Object types'),
queryset=ContentType.objects.with_feature('event_rules'), queryset=ObjectType.objects.with_feature('event_rules'),
) )
action_choice = forms.ChoiceField( action_choice = forms.ChoiceField(
label=_('Action choice'), label=_('Action choice'),
@ -267,7 +266,7 @@ class EventRuleForm(NetBoxModelForm):
) )
fieldsets = ( fieldsets = (
(_('Event Rule'), ('name', 'description', 'content_types', 'enabled', 'tags')), (_('Event Rule'), ('name', 'description', 'object_types', 'enabled', 'tags')),
(_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')), (_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
(_('Conditions'), ('conditions',)), (_('Conditions'), ('conditions',)),
(_('Action'), ( (_('Action'), (
@ -278,7 +277,7 @@ class EventRuleForm(NetBoxModelForm):
class Meta: class Meta:
model = EventRule model = EventRule
fields = ( fields = (
'content_types', 'name', 'description', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'object_types', 'name', 'description', 'type_create', 'type_update', 'type_delete', 'type_job_start',
'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type', 'action_object_id', 'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type', 'action_object_id',
'action_data', 'comments', 'tags' 'action_data', 'comments', 'tags'
) )
@ -339,11 +338,11 @@ class EventRuleForm(NetBoxModelForm):
action_choice = self.cleaned_data.get('action_choice') action_choice = self.cleaned_data.get('action_choice')
# Webhook # Webhook
if self.cleaned_data.get('action_type') == EventRuleActionChoices.WEBHOOK: if self.cleaned_data.get('action_type') == EventRuleActionChoices.WEBHOOK:
self.cleaned_data['action_object_type'] = ContentType.objects.get_for_model(action_choice) self.cleaned_data['action_object_type'] = ObjectType.objects.get_for_model(action_choice)
self.cleaned_data['action_object_id'] = action_choice.id self.cleaned_data['action_object_id'] = action_choice.id
# Script # Script
elif self.cleaned_data.get('action_type') == EventRuleActionChoices.SCRIPT: elif self.cleaned_data.get('action_type') == EventRuleActionChoices.SCRIPT:
self.cleaned_data['action_object_type'] = ContentType.objects.get_for_model( self.cleaned_data['action_object_type'] = ObjectType.objects.get_for_model(
Script, Script,
for_concrete_model=False for_concrete_model=False
) )
@ -356,7 +355,7 @@ class TagForm(forms.ModelForm):
slug = SlugField() slug = SlugField()
object_types = ContentTypeMultipleChoiceField( object_types = ContentTypeMultipleChoiceField(
label=_('Object types'), label=_('Object types'),
queryset=ContentType.objects.with_feature('tags'), queryset=ObjectType.objects.with_feature('tags'),
required=False required=False
) )

View File

@ -39,7 +39,7 @@ class CustomFieldType(ObjectType):
class Meta: class Meta:
model = models.CustomField model = models.CustomField
exclude = ('content_types', ) fields = '__all__'
filterset_class = filtersets.CustomFieldFilterSet filterset_class = filtersets.CustomFieldFilterSet
@ -55,15 +55,23 @@ class CustomLinkType(ObjectType):
class Meta: class Meta:
model = models.CustomLink model = models.CustomLink
exclude = ('content_types', ) fields = '__all__'
filterset_class = filtersets.CustomLinkFilterSet filterset_class = filtersets.CustomLinkFilterSet
class EventRuleType(OrganizationalObjectType):
class Meta:
model = models.EventRule
fields = '__all__'
filterset_class = filtersets.EventRuleFilterSet
class ExportTemplateType(ObjectType): class ExportTemplateType(ObjectType):
class Meta: class Meta:
model = models.ExportTemplate model = models.ExportTemplate
exclude = ('content_types', ) fields = '__all__'
filterset_class = filtersets.ExportTemplateFilterSet filterset_class = filtersets.ExportTemplateFilterSet
@ -95,7 +103,7 @@ class SavedFilterType(ObjectType):
class Meta: class Meta:
model = models.SavedFilter model = models.SavedFilter
exclude = ('content_types', ) fields = '__all__'
filterset_class = filtersets.SavedFilterFilterSet filterset_class = filtersets.SavedFilterFilterSet
@ -112,11 +120,3 @@ class WebhookType(OrganizationalObjectType):
class Meta: class Meta:
model = models.Webhook model = models.Webhook
filterset_class = filtersets.WebhookFilterSet filterset_class = filtersets.WebhookFilterSet
class EventRuleType(OrganizationalObjectType):
class Meta:
model = models.EventRule
exclude = ('content_types', )
filterset_class = filtersets.EventRuleFilterSet

View File

@ -25,7 +25,4 @@ class Migration(migrations.Migration):
migrations.DeleteModel( migrations.DeleteModel(
name='Report', name='Report',
), ),
migrations.DeleteModel(
name='ReportModule',
),
] ]

View File

@ -82,10 +82,12 @@ def update_scripts(apps, schema_editor):
ContentType = apps.get_model('contenttypes', 'ContentType') ContentType = apps.get_model('contenttypes', 'ContentType')
Script = apps.get_model('extras', 'Script') Script = apps.get_model('extras', 'Script')
ScriptModule = apps.get_model('extras', 'ScriptModule') ScriptModule = apps.get_model('extras', 'ScriptModule')
ReportModule = apps.get_model('extras', 'ReportModule')
Job = apps.get_model('core', 'Job') Job = apps.get_model('core', 'Job')
script_ct = ContentType.objects.get_for_model(Script) script_ct = ContentType.objects.get_for_model(Script, for_concrete_model=False)
scriptmodule_ct = ContentType.objects.get_for_model(ScriptModule) scriptmodule_ct = ContentType.objects.get_for_model(ScriptModule, for_concrete_model=False)
reportmodule_ct = ContentType.objects.get_for_model(ReportModule, for_concrete_model=False)
for module in ScriptModule.objects.all(): for module in ScriptModule.objects.all():
for script_name in get_module_scripts(module): for script_name in get_module_scripts(module):
@ -96,10 +98,16 @@ def update_scripts(apps, schema_editor):
# Update all Jobs associated with this ScriptModule & script name to point to the new Script object # Update all Jobs associated with this ScriptModule & script name to point to the new Script object
Job.objects.filter( Job.objects.filter(
object_type=scriptmodule_ct, object_type_id=scriptmodule_ct.id,
object_id=module.pk, object_id=module.pk,
name=script_name name=script_name
).update(object_type=script_ct, object_id=script.pk) ).update(object_type_id=script_ct.id, object_id=script.pk)
# Update all Jobs associated with this ScriptModule & script name to point to the new Script object
Job.objects.filter(
object_type_id=reportmodule_ct.id,
object_id=module.pk,
name=script_name
).update(object_type_id=script_ct.id, object_id=script.pk)
def update_event_rules(apps, schema_editor): def update_event_rules(apps, schema_editor):

View File

@ -12,4 +12,7 @@ class Migration(migrations.Migration):
model_name='eventrule', model_name='eventrule',
name='action_parameters', name='action_parameters',
), ),
migrations.DeleteModel(
name='ReportModule',
),
] ]

View File

@ -0,0 +1,107 @@
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0010_gfk_indexes'),
('extras', '0110_remove_eventrule_action_parameters'),
]
operations = [
# Custom fields
migrations.RenameField(
model_name='customfield',
old_name='content_types',
new_name='object_types',
),
migrations.AlterField(
model_name='customfield',
name='object_types',
field=models.ManyToManyField(related_name='custom_fields', to='core.objecttype'),
),
migrations.AlterField(
model_name='customfield',
name='object_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='core.objecttype'),
),
migrations.RunSQL(
"ALTER TABLE extras_customfield_content_types_id_seq RENAME TO extras_customfield_object_types_id_seq"
),
# Custom links
migrations.RenameField(
model_name='customlink',
old_name='content_types',
new_name='object_types',
),
migrations.AlterField(
model_name='customlink',
name='object_types',
field=models.ManyToManyField(related_name='custom_links', to='core.objecttype'),
),
migrations.RunSQL(
"ALTER TABLE extras_customlink_content_types_id_seq RENAME TO extras_customlink_object_types_id_seq"
),
# Event rules
migrations.RenameField(
model_name='eventrule',
old_name='content_types',
new_name='object_types',
),
migrations.AlterField(
model_name='eventrule',
name='object_types',
field=models.ManyToManyField(related_name='event_rules', to='core.objecttype'),
),
migrations.RunSQL(
"ALTER TABLE extras_eventrule_content_types_id_seq RENAME TO extras_eventrule_object_types_id_seq"
),
# Export templates
migrations.RenameField(
model_name='exporttemplate',
old_name='content_types',
new_name='object_types',
),
migrations.AlterField(
model_name='exporttemplate',
name='object_types',
field=models.ManyToManyField(related_name='export_templates', to='core.objecttype'),
),
migrations.RunSQL(
"ALTER TABLE extras_exporttemplate_content_types_id_seq RENAME TO extras_exporttemplate_object_types_id_seq"
),
# Saved filters
migrations.RenameField(
model_name='savedfilter',
old_name='content_types',
new_name='object_types',
),
migrations.AlterField(
model_name='savedfilter',
name='object_types',
field=models.ManyToManyField(related_name='saved_filters', to='core.objecttype'),
),
migrations.RunSQL(
"ALTER TABLE extras_savedfilter_content_types_id_seq RENAME TO extras_savedfilter_object_types_id_seq"
),
# Image attachments
migrations.RemoveIndex(
model_name='imageattachment',
name='extras_imag_content_94728e_idx',
),
migrations.RenameField(
model_name='imageattachment',
old_name='content_type',
new_name='object_type',
),
migrations.AddIndex(
model_name='imageattachment',
index=models.Index(fields=['object_type', 'object_id'], name='extras_imag_object__96bebc_idx'),
),
]

View File

@ -0,0 +1,17 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0010_gfk_indexes'),
('extras', '0111_rename_content_types'),
]
operations = [
migrations.AlterField(
model_name='tag',
name='object_types',
field=models.ManyToManyField(blank=True, related_name='+', to='core.objecttype'),
),
]

View File

@ -0,0 +1,16 @@
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('extras', '0112_tag_update_object_types'),
]
operations = [
migrations.RenameField(
model_name='customfield',
old_name='object_type',
new_name='related_object_type',
),
]

View File

@ -5,7 +5,7 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from core.models import ContentType from core.models import ObjectType
from extras.choices import * from extras.choices import *
from ..querysets import ObjectChangeQuerySet from ..querysets import ObjectChangeQuerySet
@ -113,7 +113,7 @@ class ObjectChange(models.Model):
super().clean() super().clean()
# Validate the assigned object type # Validate the assigned object type
if self.changed_object_type not in ContentType.objects.with_feature('change_logging'): if self.changed_object_type not in ObjectType.objects.with_feature('change_logging'):
raise ValidationError( raise ValidationError(
_("Change logging is not supported for this object type ({type}).").format( _("Change logging is not supported for this object type ({type}).").format(
type=self.changed_object_type type=self.changed_object_type

View File

@ -12,7 +12,7 @@ from django.urls import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from core.models import ContentType from core.models import ObjectType
from extras.choices import * from extras.choices import *
from extras.data import CHOICE_SETS from extras.data import CHOICE_SETS
from netbox.models import ChangeLoggedModel from netbox.models import ChangeLoggedModel
@ -52,8 +52,8 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
""" """
Return all CustomFields assigned to the given model. Return all CustomFields assigned to the given model.
""" """
content_type = ContentType.objects.get_for_model(model._meta.concrete_model) content_type = ObjectType.objects.get_for_model(model._meta.concrete_model)
return self.get_queryset().filter(content_types=content_type) return self.get_queryset().filter(object_types=content_type)
def get_defaults_for_model(self, model): def get_defaults_for_model(self, model):
""" """
@ -66,8 +66,8 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
content_types = models.ManyToManyField( object_types = models.ManyToManyField(
to='contenttypes.ContentType', to='core.ObjectType',
related_name='custom_fields', related_name='custom_fields',
help_text=_('The object(s) to which this field applies.') help_text=_('The object(s) to which this field applies.')
) )
@ -78,8 +78,8 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
default=CustomFieldTypeChoices.TYPE_TEXT, default=CustomFieldTypeChoices.TYPE_TEXT,
help_text=_('The type of data this custom field holds') help_text=_('The type of data this custom field holds')
) )
object_type = models.ForeignKey( related_object_type = models.ForeignKey(
to='contenttypes.ContentType', to='core.ObjectType',
on_delete=models.PROTECT, on_delete=models.PROTECT,
blank=True, blank=True,
null=True, null=True,
@ -209,7 +209,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
objects = CustomFieldManager() objects = CustomFieldManager()
clone_fields = ( clone_fields = (
'content_types', 'type', 'object_type', 'group_name', 'description', 'required', 'search_weight', 'object_types', 'type', 'related_object_type', 'group_name', 'description', 'required', 'search_weight',
'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex',
'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable', 'choice_set', 'ui_visible', 'ui_editable', 'is_cloneable',
) )
@ -284,7 +284,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
""" """
Called when a CustomField has been renamed. Updates all assigned object data. Called when a CustomField has been renamed. Updates all assigned object data.
""" """
for ct in self.content_types.all(): for ct in self.object_types.all():
model = ct.model_class() model = ct.model_class()
params = {f'custom_field_data__{old_name}__isnull': False} params = {f'custom_field_data__{old_name}__isnull': False}
instances = model.objects.filter(**params) instances = model.objects.filter(**params)
@ -344,11 +344,11 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Object fields must define an object_type; other fields must not # Object fields must define an object_type; other fields must not
if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT): if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT):
if not self.object_type: if not self.related_object_type:
raise ValidationError({ raise ValidationError({
'object_type': _("Object fields must define an object type.") 'object_type': _("Object fields must define an object type.")
}) })
elif self.object_type: elif self.related_object_type:
raise ValidationError({ raise ValidationError({
'object_type': _( 'object_type': _(
"{type} fields may not define an object type.") "{type} fields may not define an object type.")
@ -388,10 +388,10 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
except ValueError: except ValueError:
return value return value
if self.type == CustomFieldTypeChoices.TYPE_OBJECT: if self.type == CustomFieldTypeChoices.TYPE_OBJECT:
model = self.object_type.model_class() model = self.related_object_type.model_class()
return model.objects.filter(pk=value).first() return model.objects.filter(pk=value).first()
if self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: if self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
model = self.object_type.model_class() model = self.related_object_type.model_class()
return model.objects.filter(pk__in=value) return model.objects.filter(pk__in=value)
return value return value
@ -488,7 +488,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Object # Object
elif self.type == CustomFieldTypeChoices.TYPE_OBJECT: elif self.type == CustomFieldTypeChoices.TYPE_OBJECT:
model = self.object_type.model_class() model = self.related_object_type.model_class()
field_class = CSVModelChoiceField if for_csv_import else DynamicModelChoiceField field_class = CSVModelChoiceField if for_csv_import else DynamicModelChoiceField
field = field_class( field = field_class(
queryset=model.objects.all(), queryset=model.objects.all(),
@ -498,7 +498,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
# Multiple objects # Multiple objects
elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: elif self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT:
model = self.object_type.model_class() model = self.related_object_type.model_class()
field_class = CSVModelMultipleChoiceField if for_csv_import else DynamicModelMultipleChoiceField field_class = CSVModelMultipleChoiceField if for_csv_import else DynamicModelMultipleChoiceField
field = field_class( field = field_class(
queryset=model.objects.all(), queryset=model.objects.all(),

View File

@ -12,7 +12,7 @@ from django.utils.formats import date_format
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.utils.encoders import JSONEncoder from rest_framework.utils.encoders import JSONEncoder
from core.models import ContentType from core.models import ObjectType
from extras.choices import * from extras.choices import *
from extras.conditions import ConditionSet from extras.conditions import ConditionSet
from extras.constants import * from extras.constants import *
@ -43,9 +43,9 @@ class EventRule(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLogged
specific type of object is created, modified, or deleted. The action to be taken might entail transmitting a specific type of object is created, modified, or deleted. The action to be taken might entail transmitting a
webhook or executing a custom script. webhook or executing a custom script.
""" """
content_types = models.ManyToManyField( object_types = models.ManyToManyField(
to='contenttypes.ContentType', to='core.ObjectType',
related_name='eventrules', related_name='event_rules',
verbose_name=_('object types'), verbose_name=_('object types'),
help_text=_("The object(s) to which this rule applies.") help_text=_("The object(s) to which this rule applies.")
) )
@ -313,8 +313,8 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
code to be rendered with an object as context. code to be rendered with an object as context.
""" """
content_types = models.ManyToManyField( object_types = models.ManyToManyField(
to='contenttypes.ContentType', to='core.ObjectType',
related_name='custom_links', related_name='custom_links',
help_text=_('The object type(s) to which this link applies.') help_text=_('The object type(s) to which this link applies.')
) )
@ -359,7 +359,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
) )
clone_fields = ( clone_fields = (
'content_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window', 'object_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window',
) )
class Meta: class Meta:
@ -409,8 +409,8 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
content_types = models.ManyToManyField( object_types = models.ManyToManyField(
to='contenttypes.ContentType', to='core.ObjectType',
related_name='export_templates', related_name='export_templates',
help_text=_('The object type(s) to which this template applies.') help_text=_('The object type(s) to which this template applies.')
) )
@ -448,7 +448,7 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change
) )
clone_fields = ( clone_fields = (
'content_types', 'template_code', 'mime_type', 'file_extension', 'as_attachment', 'object_types', 'template_code', 'mime_type', 'file_extension', 'as_attachment',
) )
class Meta: class Meta:
@ -518,8 +518,8 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
""" """
A set of predefined keyword parameters that can be reused to filter for specific objects. A set of predefined keyword parameters that can be reused to filter for specific objects.
""" """
content_types = models.ManyToManyField( object_types = models.ManyToManyField(
to='contenttypes.ContentType', to='core.ObjectType',
related_name='saved_filters', related_name='saved_filters',
help_text=_('The object type(s) to which this filter applies.') help_text=_('The object type(s) to which this filter applies.')
) )
@ -561,7 +561,7 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
) )
clone_fields = ( clone_fields = (
'content_types', 'weight', 'enabled', 'parameters', 'object_types', 'weight', 'enabled', 'parameters',
) )
class Meta: class Meta:
@ -598,13 +598,13 @@ class ImageAttachment(ChangeLoggedModel):
""" """
An uploaded image which is associated with an object. An uploaded image which is associated with an object.
""" """
content_type = models.ForeignKey( object_type = models.ForeignKey(
to='contenttypes.ContentType', to='contenttypes.ContentType',
on_delete=models.CASCADE on_delete=models.CASCADE
) )
object_id = models.PositiveBigIntegerField() object_id = models.PositiveBigIntegerField()
parent = GenericForeignKey( parent = GenericForeignKey(
ct_field='content_type', ct_field='object_type',
fk_field='object_id' fk_field='object_id'
) )
image = models.ImageField( image = models.ImageField(
@ -626,12 +626,12 @@ class ImageAttachment(ChangeLoggedModel):
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
clone_fields = ('content_type', 'object_id') clone_fields = ('object_type', 'object_id')
class Meta: class Meta:
ordering = ('name', 'pk') # name may be non-unique ordering = ('name', 'pk') # name may be non-unique
indexes = ( indexes = (
models.Index(fields=('content_type', 'object_id')), models.Index(fields=('object_type', 'object_id')),
) )
verbose_name = _('image attachment') verbose_name = _('image attachment')
verbose_name_plural = _('image attachments') verbose_name_plural = _('image attachments')
@ -646,9 +646,9 @@ class ImageAttachment(ChangeLoggedModel):
super().clean() super().clean()
# Validate the assigned object type # Validate the assigned object type
if self.content_type not in ContentType.objects.with_feature('image_attachments'): if self.object_type not in ObjectType.objects.with_feature('image_attachments'):
raise ValidationError( raise ValidationError(
_("Image attachments cannot be assigned to this object type ({type}).").format(type=self.content_type) _("Image attachments cannot be assigned to this object type ({type}).").format(type=self.object_type)
) )
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
@ -739,7 +739,7 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
super().clean() super().clean()
# Validate the assigned object type # Validate the assigned object type
if self.assigned_object_type not in ContentType.objects.with_feature('journaling'): if self.assigned_object_type not in ObjectType.objects.with_feature('journaling'):
raise ValidationError( raise ValidationError(
_("Journaling is not supported for this object type ({type}).").format(type=self.assigned_object_type) _("Journaling is not supported for this object type ({type}).").format(type=self.assigned_object_type)
) )
@ -795,7 +795,7 @@ class Bookmark(models.Model):
super().clean() super().clean()
# Validate the assigned object type # Validate the assigned object type
if self.object_type not in ContentType.objects.with_feature('bookmarks'): if self.object_type not in ObjectType.objects.with_feature('bookmarks'):
raise ValidationError( raise ValidationError(
_("Bookmarks cannot be assigned to this object type ({type}).").format(type=self.object_type) _("Bookmarks cannot be assigned to this object type ({type}).").format(type=self.object_type)
) )

View File

@ -34,7 +34,7 @@ class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase):
blank=True, blank=True,
) )
object_types = models.ManyToManyField( object_types = models.ManyToManyField(
to='contenttypes.ContentType', to='core.ObjectType',
related_name='+', related_name='+',
blank=True, blank=True,
help_text=_("The object type(s) to which this this tag can be applied.") help_text=_("The object type(s) to which this this tag can be applied.")

View File

@ -8,6 +8,7 @@ from django.dispatch import receiver, Signal
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_prometheus.models import model_deletes, model_inserts, model_updates from django_prometheus.models import model_deletes, model_inserts, model_updates
from core.models import ObjectType
from core.signals import job_end, job_start from core.signals import job_end, job_start
from extras.constants import EVENT_JOB_END, EVENT_JOB_START from extras.constants import EVENT_JOB_END, EVENT_JOB_START
from extras.events import process_event_rules from extras.events import process_event_rules
@ -205,13 +206,13 @@ def handle_cf_deleted(instance, **kwargs):
""" """
Handle the cleanup of old custom field data when a CustomField is deleted. Handle the cleanup of old custom field data when a CustomField is deleted.
""" """
instance.remove_stale_data(instance.content_types.all()) instance.remove_stale_data(instance.object_types.all())
post_save.connect(handle_cf_renamed, sender=CustomField) post_save.connect(handle_cf_renamed, sender=CustomField)
pre_delete.connect(handle_cf_deleted, sender=CustomField) pre_delete.connect(handle_cf_deleted, sender=CustomField)
m2m_changed.connect(handle_cf_added_obj_types, sender=CustomField.content_types.through) m2m_changed.connect(handle_cf_added_obj_types, sender=CustomField.object_types.through)
m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.content_types.through) m2m_changed.connect(handle_cf_removed_obj_types, sender=CustomField.object_types.through)
# #
@ -240,8 +241,8 @@ def validate_assigned_tags(sender, instance, action, model, pk_set, **kwargs):
""" """
if action != 'pre_add': if action != 'pre_add':
return return
ct = ContentType.objects.get_for_model(instance) ct = ObjectType.objects.get_for_model(instance)
# Retrieve any applied Tags that are restricted to certain object_types # Retrieve any applied Tags that are restricted to certain object types
for tag in model.objects.filter(pk__in=pk_set, object_types__isnull=False).prefetch_related('object_types'): for tag in model.objects.filter(pk__in=pk_set, object_types__isnull=False).prefetch_related('object_types'):
if ct not in tag.object_types.all(): if ct not in tag.object_types.all():
raise AbortRequest(f"Tag {tag} cannot be assigned to {ct.model} objects.") raise AbortRequest(f"Tag {tag} cannot be assigned to {ct.model} objects.")
@ -256,7 +257,7 @@ def process_job_start_event_rules(sender, **kwargs):
""" """
Process event rules for jobs starting. Process event rules for jobs starting.
""" """
event_rules = EventRule.objects.filter(type_job_start=True, enabled=True, content_types=sender.object_type) event_rules = EventRule.objects.filter(type_job_start=True, enabled=True, object_types=sender.object_type)
username = sender.user.username if sender.user else None username = sender.user.username if sender.user else None
process_event_rules(event_rules, sender.object_type.model, EVENT_JOB_START, sender.data, username) process_event_rules(event_rules, sender.object_type.model, EVENT_JOB_START, sender.data, username)
@ -266,6 +267,6 @@ def process_job_end_event_rules(sender, **kwargs):
""" """
Process event rules for jobs terminating. Process event rules for jobs terminating.
""" """
event_rules = EventRule.objects.filter(type_job_end=True, enabled=True, content_types=sender.object_type) event_rules = EventRule.objects.filter(type_job_end=True, enabled=True, object_types=sender.object_type)
username = sender.user.username if sender.user else None username = sender.user.username if sender.user else None
process_event_rules(event_rules, sender.object_type.model, EVENT_JOB_END, sender.data, username) process_event_rules(event_rules, sender.object_type.model, EVENT_JOB_END, sender.data, username)

View File

@ -5,7 +5,7 @@ from django.conf import settings
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from extras.models import * from extras.models import *
from netbox.tables import NetBoxTable, columns from netbox.tables import BaseTable, NetBoxTable, columns
from .template_code import * from .template_code import *
__all__ = ( __all__ = (
@ -21,6 +21,8 @@ __all__ = (
'JournalEntryTable', 'JournalEntryTable',
'ObjectChangeTable', 'ObjectChangeTable',
'SavedFilterTable', 'SavedFilterTable',
'ReportResultsTable',
'ScriptResultsTable',
'TaggedItemTable', 'TaggedItemTable',
'TagTable', 'TagTable',
'WebhookTable', 'WebhookTable',
@ -40,8 +42,8 @@ class CustomFieldTable(NetBoxTable):
verbose_name=_('Name'), verbose_name=_('Name'),
linkify=True linkify=True
) )
content_types = columns.ContentTypesColumn( object_types = columns.ContentTypesColumn(
verbose_name=_('Content Types') verbose_name=_('Object Types')
) )
required = columns.BooleanColumn( required = columns.BooleanColumn(
verbose_name=_('Required') verbose_name=_('Required')
@ -55,6 +57,9 @@ class CustomFieldTable(NetBoxTable):
description = columns.MarkdownColumn( description = columns.MarkdownColumn(
verbose_name=_('Description') verbose_name=_('Description')
) )
related_object_type = columns.ContentTypeColumn(
verbose_name=_('Related Object Type')
)
choice_set = tables.Column( choice_set = tables.Column(
linkify=True, linkify=True,
verbose_name=_('Choice Set') verbose_name=_('Choice Set')
@ -71,11 +76,11 @@ class CustomFieldTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = CustomField model = CustomField
fields = ( fields = (
'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'default', 'description', 'pk', 'id', 'name', 'object_types', 'label', 'type', 'related_object_type', 'group_name', 'required',
'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', 'weight', 'choice_set', 'default', 'description', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable',
'choices', 'created', 'last_updated', 'weight', 'choice_set', 'choices', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description') default_columns = ('pk', 'name', 'object_types', 'label', 'group_name', 'type', 'required', 'description')
class CustomFieldChoiceSetTable(NetBoxTable): class CustomFieldChoiceSetTable(NetBoxTable):
@ -115,8 +120,8 @@ class CustomLinkTable(NetBoxTable):
verbose_name=_('Name'), verbose_name=_('Name'),
linkify=True linkify=True
) )
content_types = columns.ContentTypesColumn( object_types = columns.ContentTypesColumn(
verbose_name=_('Content Types'), verbose_name=_('Object Types'),
) )
enabled = columns.BooleanColumn( enabled = columns.BooleanColumn(
verbose_name=_('Enabled'), verbose_name=_('Enabled'),
@ -128,10 +133,10 @@ class CustomLinkTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = CustomLink model = CustomLink
fields = ( fields = (
'pk', 'id', 'name', 'content_types', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'pk', 'id', 'name', 'object_types', 'enabled', 'link_text', 'link_url', 'weight', 'group_name',
'button_class', 'new_window', 'created', 'last_updated', 'button_class', 'new_window', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'content_types', 'enabled', 'group_name', 'button_class', 'new_window') default_columns = ('pk', 'name', 'object_types', 'enabled', 'group_name', 'button_class', 'new_window')
class ExportTemplateTable(NetBoxTable): class ExportTemplateTable(NetBoxTable):
@ -139,8 +144,8 @@ class ExportTemplateTable(NetBoxTable):
verbose_name=_('Name'), verbose_name=_('Name'),
linkify=True linkify=True
) )
content_types = columns.ContentTypesColumn( object_types = columns.ContentTypesColumn(
verbose_name=_('Content Types'), verbose_name=_('Object Types'),
) )
as_attachment = columns.BooleanColumn( as_attachment = columns.BooleanColumn(
verbose_name=_('As Attachment'), verbose_name=_('As Attachment'),
@ -161,11 +166,11 @@ class ExportTemplateTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = ExportTemplate model = ExportTemplate
fields = ( fields = (
'pk', 'id', 'name', 'content_types', 'description', 'mime_type', 'file_extension', 'as_attachment', 'pk', 'id', 'name', 'object_types', 'description', 'mime_type', 'file_extension', 'as_attachment',
'data_source', 'data_file', 'data_synced', 'created', 'last_updated', 'data_source', 'data_file', 'data_synced', 'created', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'name', 'content_types', 'description', 'mime_type', 'file_extension', 'as_attachment', 'is_synced', 'pk', 'name', 'object_types', 'description', 'mime_type', 'file_extension', 'as_attachment', 'is_synced',
) )
@ -174,8 +179,8 @@ class ImageAttachmentTable(NetBoxTable):
verbose_name=_('ID'), verbose_name=_('ID'),
linkify=False linkify=False
) )
content_type = columns.ContentTypeColumn( object_type = columns.ContentTypeColumn(
verbose_name=_('Content Type'), verbose_name=_('Object Type'),
) )
parent = tables.Column( parent = tables.Column(
verbose_name=_('Parent'), verbose_name=_('Parent'),
@ -193,10 +198,10 @@ class ImageAttachmentTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = ImageAttachment model = ImageAttachment
fields = ( fields = (
'pk', 'content_type', 'parent', 'image', 'name', 'image_height', 'image_width', 'size', 'created', 'pk', 'object_type', 'parent', 'image', 'name', 'image_height', 'image_width', 'size', 'created',
'last_updated', 'last_updated',
) )
default_columns = ('content_type', 'parent', 'image', 'name', 'size', 'created') default_columns = ('object_type', 'parent', 'image', 'name', 'size', 'created')
class SavedFilterTable(NetBoxTable): class SavedFilterTable(NetBoxTable):
@ -204,8 +209,8 @@ class SavedFilterTable(NetBoxTable):
verbose_name=_('Name'), verbose_name=_('Name'),
linkify=True linkify=True
) )
content_types = columns.ContentTypesColumn( object_types = columns.ContentTypesColumn(
verbose_name=_('Content Types'), verbose_name=_('Object Types'),
) )
enabled = columns.BooleanColumn( enabled = columns.BooleanColumn(
verbose_name=_('Enabled'), verbose_name=_('Enabled'),
@ -220,11 +225,11 @@ class SavedFilterTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = SavedFilter model = SavedFilter
fields = ( fields = (
'pk', 'id', 'name', 'slug', 'content_types', 'description', 'user', 'weight', 'enabled', 'shared', 'pk', 'id', 'name', 'slug', 'object_types', 'description', 'user', 'weight', 'enabled', 'shared',
'created', 'last_updated', 'parameters' 'created', 'last_updated', 'parameters'
) )
default_columns = ( default_columns = (
'pk', 'name', 'content_types', 'user', 'description', 'enabled', 'shared', 'pk', 'name', 'object_types', 'user', 'description', 'enabled', 'shared',
) )
@ -281,8 +286,8 @@ class EventRuleTable(NetBoxTable):
linkify=True, linkify=True,
verbose_name=_('Object'), verbose_name=_('Object'),
) )
content_types = columns.ContentTypesColumn( object_types = columns.ContentTypesColumn(
verbose_name=_('Content Types'), verbose_name=_('Object Types'),
) )
enabled = columns.BooleanColumn( enabled = columns.BooleanColumn(
verbose_name=_('Enabled'), verbose_name=_('Enabled'),
@ -309,12 +314,12 @@ class EventRuleTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = EventRule model = EventRule
fields = ( fields = (
'pk', 'id', 'name', 'enabled', 'description', 'action_type', 'action_object', 'content_types', 'pk', 'id', 'name', 'enabled', 'description', 'action_type', 'action_object', 'object_types',
'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'tags', 'created', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'tags', 'created',
'last_updated', 'last_updated',
) )
default_columns = ( default_columns = (
'pk', 'name', 'enabled', 'action_type', 'action_object', 'content_types', 'type_create', 'type_update', 'pk', 'name', 'enabled', 'action_type', 'action_object', 'object_types', 'type_create', 'type_update',
'type_delete', 'type_job_start', 'type_job_end', 'type_delete', 'type_job_start', 'type_job_end',
) )
@ -507,3 +512,61 @@ class JournalEntryTable(NetBoxTable):
default_columns = ( default_columns = (
'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'comments' 'pk', 'created', 'created_by', 'assigned_object_type', 'assigned_object', 'kind', 'comments'
) )
class ScriptResultsTable(BaseTable):
index = tables.Column(
verbose_name=_('Line')
)
time = tables.Column(
verbose_name=_('Time')
)
status = tables.TemplateColumn(
template_code="""{% load log_levels %}{% log_level record.status %}""",
verbose_name=_('Level')
)
message = tables.Column(
verbose_name=_('Message')
)
class Meta(BaseTable.Meta):
empty_text = _('No results found')
fields = (
'index', 'time', 'status', 'message',
)
class ReportResultsTable(BaseTable):
index = tables.Column(
verbose_name=_('Line')
)
method = tables.Column(
verbose_name=_('Method')
)
time = tables.Column(
verbose_name=_('Time')
)
status = tables.Column(
empty_values=(),
verbose_name=_('Level')
)
status = tables.TemplateColumn(
template_code="""{% load log_levels %}{% log_level record.status %}""",
verbose_name=_('Level')
)
object = tables.Column(
verbose_name=_('Object')
)
url = tables.Column(
verbose_name=_('URL')
)
message = tables.Column(
verbose_name=_('Message')
)
class Meta(BaseTable.Meta):
empty_text = _('No results found')
fields = (
'index', 'method', 'time', 'status', 'object', 'url', 'message',
)

View File

@ -1,7 +1,7 @@
from django import template from django import template
from django.contrib.contenttypes.models import ContentType
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from core.models import ObjectType
from extras.models import CustomLink from extras.models import CustomLink
@ -32,8 +32,8 @@ def custom_links(context, obj):
""" """
Render all applicable links for the given object. Render all applicable links for the given object.
""" """
content_type = ContentType.objects.get_for_model(obj) object_type = ObjectType.objects.get_for_model(obj)
custom_links = CustomLink.objects.filter(content_types=content_type, enabled=True) custom_links = CustomLink.objects.filter(object_types=object_type, enabled=True)
if not custom_links: if not custom_links:
return '' return ''

View File

@ -7,10 +7,10 @@ from django.utils.timezone import make_aware
from rest_framework import status from rest_framework import status
from core.choices import ManagedFileRootPathChoices from core.choices import ManagedFileRootPathChoices
from core.models import ObjectType
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
from extras.choices import * from extras.choices import *
from extras.models import * from extras.models import *
from extras.reports import Report
from extras.scripts import BooleanVar, IntegerVar, Script as PythonClass, StringVar from extras.scripts import BooleanVar, IntegerVar, Script as PythonClass, StringVar
from utilities.testing import APITestCase, APIViewTestCases from utilities.testing import APITestCase, APIViewTestCases
@ -122,7 +122,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase):
cls.create_data = [ cls.create_data = [
{ {
'name': 'EventRule 4', 'name': 'EventRule 4',
'content_types': ['dcim.device', 'dcim.devicetype'], 'object_types': ['dcim.device', 'dcim.devicetype'],
'type_create': True, 'type_create': True,
'action_type': EventRuleActionChoices.WEBHOOK, 'action_type': EventRuleActionChoices.WEBHOOK,
'action_object_type': 'extras.webhook', 'action_object_type': 'extras.webhook',
@ -130,7 +130,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase):
}, },
{ {
'name': 'EventRule 5', 'name': 'EventRule 5',
'content_types': ['dcim.device', 'dcim.devicetype'], 'object_types': ['dcim.device', 'dcim.devicetype'],
'type_create': True, 'type_create': True,
'action_type': EventRuleActionChoices.WEBHOOK, 'action_type': EventRuleActionChoices.WEBHOOK,
'action_object_type': 'extras.webhook', 'action_object_type': 'extras.webhook',
@ -138,7 +138,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase):
}, },
{ {
'name': 'EventRule 6', 'name': 'EventRule 6',
'content_types': ['dcim.device', 'dcim.devicetype'], 'object_types': ['dcim.device', 'dcim.devicetype'],
'type_create': True, 'type_create': True,
'action_type': EventRuleActionChoices.WEBHOOK, 'action_type': EventRuleActionChoices.WEBHOOK,
'action_object_type': 'extras.webhook', 'action_object_type': 'extras.webhook',
@ -152,17 +152,17 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase):
brief_fields = ['description', 'display', 'id', 'name', 'url'] brief_fields = ['description', 'display', 'id', 'name', 'url']
create_data = [ create_data = [
{ {
'content_types': ['dcim.site'], 'object_types': ['dcim.site'],
'name': 'cf4', 'name': 'cf4',
'type': 'date', 'type': 'date',
}, },
{ {
'content_types': ['dcim.site'], 'object_types': ['dcim.site'],
'name': 'cf5', 'name': 'cf5',
'type': 'url', 'type': 'url',
}, },
{ {
'content_types': ['dcim.site'], 'object_types': ['dcim.site'],
'name': 'cf6', 'name': 'cf6',
'type': 'text', 'type': 'text',
}, },
@ -171,14 +171,14 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase):
'description': 'New description', 'description': 'New description',
} }
update_data = { update_data = {
'content_types': ['dcim.device'], 'object_types': ['dcim.device'],
'name': 'New_Name', 'name': 'New_Name',
'description': 'New description', 'description': 'New description',
} }
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
site_ct = ContentType.objects.get_for_model(Site) site_ct = ObjectType.objects.get_for_model(Site)
custom_fields = ( custom_fields = (
CustomField( CustomField(
@ -196,7 +196,7 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase):
) )
CustomField.objects.bulk_create(custom_fields) CustomField.objects.bulk_create(custom_fields)
for cf in custom_fields: for cf in custom_fields:
cf.content_types.add(site_ct) cf.object_types.add(site_ct)
class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase): class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase):
@ -273,21 +273,21 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase):
brief_fields = ['display', 'id', 'name', 'url'] brief_fields = ['display', 'id', 'name', 'url']
create_data = [ create_data = [
{ {
'content_types': ['dcim.site'], 'object_types': ['dcim.site'],
'name': 'Custom Link 4', 'name': 'Custom Link 4',
'enabled': True, 'enabled': True,
'link_text': 'Link 4', 'link_text': 'Link 4',
'link_url': 'http://example.com/?4', 'link_url': 'http://example.com/?4',
}, },
{ {
'content_types': ['dcim.site'], 'object_types': ['dcim.site'],
'name': 'Custom Link 5', 'name': 'Custom Link 5',
'enabled': True, 'enabled': True,
'link_text': 'Link 5', 'link_text': 'Link 5',
'link_url': 'http://example.com/?5', 'link_url': 'http://example.com/?5',
}, },
{ {
'content_types': ['dcim.site'], 'object_types': ['dcim.site'],
'name': 'Custom Link 6', 'name': 'Custom Link 6',
'enabled': False, 'enabled': False,
'link_text': 'Link 6', 'link_text': 'Link 6',
@ -301,7 +301,7 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
site_ct = ContentType.objects.get_for_model(Site) site_type = ObjectType.objects.get_for_model(Site)
custom_links = ( custom_links = (
CustomLink( CustomLink(
@ -325,7 +325,7 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase):
) )
CustomLink.objects.bulk_create(custom_links) CustomLink.objects.bulk_create(custom_links)
for i, custom_link in enumerate(custom_links): for i, custom_link in enumerate(custom_links):
custom_link.content_types.set([site_ct]) custom_link.object_types.set([site_type])
class SavedFilterTest(APIViewTestCases.APIViewTestCase): class SavedFilterTest(APIViewTestCases.APIViewTestCase):
@ -333,7 +333,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase):
brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url'] brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url']
create_data = [ create_data = [
{ {
'content_types': ['dcim.site'], 'object_types': ['dcim.site'],
'name': 'Saved Filter 4', 'name': 'Saved Filter 4',
'slug': 'saved-filter-4', 'slug': 'saved-filter-4',
'weight': 100, 'weight': 100,
@ -342,7 +342,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase):
'parameters': {'status': ['active']}, 'parameters': {'status': ['active']},
}, },
{ {
'content_types': ['dcim.site'], 'object_types': ['dcim.site'],
'name': 'Saved Filter 5', 'name': 'Saved Filter 5',
'slug': 'saved-filter-5', 'slug': 'saved-filter-5',
'weight': 200, 'weight': 200,
@ -351,7 +351,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase):
'parameters': {'status': ['planned']}, 'parameters': {'status': ['planned']},
}, },
{ {
'content_types': ['dcim.site'], 'object_types': ['dcim.site'],
'name': 'Saved Filter 6', 'name': 'Saved Filter 6',
'slug': 'saved-filter-6', 'slug': 'saved-filter-6',
'weight': 300, 'weight': 300,
@ -368,7 +368,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
site_ct = ContentType.objects.get_for_model(Site) site_type = ObjectType.objects.get_for_model(Site)
saved_filters = ( saved_filters = (
SavedFilter( SavedFilter(
@ -398,7 +398,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase):
) )
SavedFilter.objects.bulk_create(saved_filters) SavedFilter.objects.bulk_create(saved_filters)
for i, savedfilter in enumerate(saved_filters): for i, savedfilter in enumerate(saved_filters):
savedfilter.content_types.set([site_ct]) savedfilter.object_types.set([site_type])
class BookmarkTest( class BookmarkTest(
@ -458,17 +458,17 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
brief_fields = ['description', 'display', 'id', 'name', 'url'] brief_fields = ['description', 'display', 'id', 'name', 'url']
create_data = [ create_data = [
{ {
'content_types': ['dcim.device'], 'object_types': ['dcim.device'],
'name': 'Test Export Template 4', 'name': 'Test Export Template 4',
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}', 'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
}, },
{ {
'content_types': ['dcim.device'], 'object_types': ['dcim.device'],
'name': 'Test Export Template 5', 'name': 'Test Export Template 5',
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}', 'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
}, },
{ {
'content_types': ['dcim.device'], 'object_types': ['dcim.device'],
'name': 'Test Export Template 6', 'name': 'Test Export Template 6',
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}', 'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
}, },
@ -495,7 +495,7 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
) )
ExportTemplate.objects.bulk_create(export_templates) ExportTemplate.objects.bulk_create(export_templates)
for et in export_templates: for et in export_templates:
et.content_types.set([ContentType.objects.get_for_model(Device)]) et.object_types.set([ObjectType.objects.get_for_model(Device)])
class TagTest(APIViewTestCases.APIViewTestCase): class TagTest(APIViewTestCases.APIViewTestCase):
@ -548,7 +548,7 @@ class ImageAttachmentTest(
image_attachments = ( image_attachments = (
ImageAttachment( ImageAttachment(
content_type=ct, object_type=ct,
object_id=site.pk, object_id=site.pk,
name='Image Attachment 1', name='Image Attachment 1',
image='http://example.com/image1.png', image='http://example.com/image1.png',
@ -556,7 +556,7 @@ class ImageAttachmentTest(
image_width=100 image_width=100
), ),
ImageAttachment( ImageAttachment(
content_type=ct, object_type=ct,
object_id=site.pk, object_id=site.pk,
name='Image Attachment 2', name='Image Attachment 2',
image='http://example.com/image2.png', image='http://example.com/image2.png',
@ -564,7 +564,7 @@ class ImageAttachmentTest(
image_width=100 image_width=100
), ),
ImageAttachment( ImageAttachment(
content_type=ct, object_type=ct,
object_id=site.pk, object_id=site.pk,
name='Image Attachment 3', name='Image Attachment 3',
image='http://example.com/image3.png', image='http://example.com/image3.png',
@ -876,17 +876,17 @@ class CreatedUpdatedFilterTest(APITestCase):
self.assertEqual(response.data['results'][0]['id'], rack2.pk) self.assertEqual(response.data['results'][0]['id'], rack2.pk)
class ContentTypeTest(APITestCase): class ObjectTypeTest(APITestCase):
def test_list_objects(self): def test_list_objects(self):
contenttype_count = ContentType.objects.count() object_type_count = ObjectType.objects.count()
response = self.client.get(reverse('extras-api:contenttype-list'), **self.header) response = self.client.get(reverse('extras-api:objecttype-list'), **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK) self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(response.data['count'], contenttype_count) self.assertEqual(response.data['count'], object_type_count)
def test_get_object(self): def test_get_object(self):
contenttype = ContentType.objects.first() object_type = ObjectType.objects.first()
url = reverse('extras-api:contenttype-detail', kwargs={'pk': contenttype.pk}) url = reverse('extras-api:objecttype-detail', kwargs={'pk': object_type.pk})
self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_200_OK) self.assertHttpStatus(self.client.get(url, **self.header), status.HTTP_200_OK)

View File

@ -3,6 +3,7 @@ from django.test import override_settings
from django.urls import reverse from django.urls import reverse
from rest_framework import status from rest_framework import status
from core.models import ObjectType
from dcim.choices import SiteStatusChoices from dcim.choices import SiteStatusChoices
from dcim.models import Site from dcim.models import Site
from extras.choices import * from extras.choices import *
@ -23,14 +24,14 @@ class ChangeLogViewTest(ModelViewTestCase):
) )
# Create a custom field on the Site model # Create a custom field on the Site model
ct = ContentType.objects.get_for_model(Site) site_type = ObjectType.objects.get_for_model(Site)
cf = CustomField( cf = CustomField(
type=CustomFieldTypeChoices.TYPE_TEXT, type=CustomFieldTypeChoices.TYPE_TEXT,
name='cf1', name='cf1',
required=False required=False
) )
cf.save() cf.save()
cf.content_types.set([ct]) cf.object_types.set([site_type])
# Create a select custom field on the Site model # Create a select custom field on the Site model
cf_select = CustomField( cf_select = CustomField(
@ -40,7 +41,7 @@ class ChangeLogViewTest(ModelViewTestCase):
choice_set=choice_set choice_set=choice_set
) )
cf_select.save() cf_select.save()
cf_select.content_types.set([ct]) cf_select.object_types.set([site_type])
def test_create_object(self): def test_create_object(self):
tags = create_tags('Tag 1', 'Tag 2') tags = create_tags('Tag 1', 'Tag 2')
@ -275,14 +276,14 @@ class ChangeLogAPITest(APITestCase):
def setUpTestData(cls): def setUpTestData(cls):
# Create a custom field on the Site model # Create a custom field on the Site model
ct = ContentType.objects.get_for_model(Site) site_type = ObjectType.objects.get_for_model(Site)
cf = CustomField( cf = CustomField(
type=CustomFieldTypeChoices.TYPE_TEXT, type=CustomFieldTypeChoices.TYPE_TEXT,
name='cf1', name='cf1',
required=False required=False
) )
cf.save() cf.save()
cf.content_types.set([ct]) cf.object_types.set([site_type])
# Create a select custom field on the Site model # Create a select custom field on the Site model
choice_set = CustomFieldChoiceSet.objects.create( choice_set = CustomFieldChoiceSet.objects.create(
@ -296,7 +297,7 @@ class ChangeLogAPITest(APITestCase):
choice_set=choice_set choice_set=choice_set
) )
cf_select.save() cf_select.save()
cf_select.content_types.set([ct]) cf_select.object_types.set([site_type])
# Create some tags # Create some tags
tags = ( tags = (

View File

@ -1,11 +1,11 @@
import datetime import datetime
from decimal import Decimal from decimal import Decimal
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.urls import reverse from django.urls import reverse
from rest_framework import status from rest_framework import status
from core.models import ObjectType
from dcim.filtersets import SiteFilterSet from dcim.filtersets import SiteFilterSet
from dcim.forms import SiteImportForm from dcim.forms import SiteImportForm
from dcim.models import Manufacturer, Rack, Site from dcim.models import Manufacturer, Rack, Site
@ -28,7 +28,7 @@ class CustomFieldTest(TestCase):
Site(name='Site C', slug='site-c'), Site(name='Site C', slug='site-c'),
]) ])
cls.object_type = ContentType.objects.get_for_model(Site) cls.object_type = ObjectType.objects.get_for_model(Site)
def test_invalid_name(self): def test_invalid_name(self):
""" """
@ -50,7 +50,7 @@ class CustomFieldTest(TestCase):
type=CustomFieldTypeChoices.TYPE_TEXT, type=CustomFieldTypeChoices.TYPE_TEXT,
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -75,7 +75,7 @@ class CustomFieldTest(TestCase):
type=CustomFieldTypeChoices.TYPE_LONGTEXT, type=CustomFieldTypeChoices.TYPE_LONGTEXT,
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -99,7 +99,7 @@ class CustomFieldTest(TestCase):
type=CustomFieldTypeChoices.TYPE_INTEGER, type=CustomFieldTypeChoices.TYPE_INTEGER,
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -125,7 +125,7 @@ class CustomFieldTest(TestCase):
type=CustomFieldTypeChoices.TYPE_DECIMAL, type=CustomFieldTypeChoices.TYPE_DECIMAL,
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -151,7 +151,7 @@ class CustomFieldTest(TestCase):
type=CustomFieldTypeChoices.TYPE_INTEGER, type=CustomFieldTypeChoices.TYPE_INTEGER,
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -178,7 +178,7 @@ class CustomFieldTest(TestCase):
type=CustomFieldTypeChoices.TYPE_DATE, type=CustomFieldTypeChoices.TYPE_DATE,
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -203,7 +203,7 @@ class CustomFieldTest(TestCase):
type=CustomFieldTypeChoices.TYPE_DATETIME, type=CustomFieldTypeChoices.TYPE_DATETIME,
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -228,7 +228,7 @@ class CustomFieldTest(TestCase):
type=CustomFieldTypeChoices.TYPE_URL, type=CustomFieldTypeChoices.TYPE_URL,
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -253,7 +253,7 @@ class CustomFieldTest(TestCase):
type=CustomFieldTypeChoices.TYPE_JSON, type=CustomFieldTypeChoices.TYPE_JSON,
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -290,7 +290,7 @@ class CustomFieldTest(TestCase):
required=False, required=False,
choice_set=choice_set choice_set=choice_set
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -327,7 +327,7 @@ class CustomFieldTest(TestCase):
required=False, required=False,
choice_set=choice_set choice_set=choice_set
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -350,10 +350,10 @@ class CustomFieldTest(TestCase):
cf = CustomField.objects.create( cf = CustomField.objects.create(
name='object_field', name='object_field',
type=CustomFieldTypeChoices.TYPE_OBJECT, type=CustomFieldTypeChoices.TYPE_OBJECT,
object_type=ContentType.objects.get_for_model(VLAN), related_object_type=ObjectType.objects.get_for_model(VLAN),
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -382,10 +382,10 @@ class CustomFieldTest(TestCase):
cf = CustomField.objects.create( cf = CustomField.objects.create(
name='object_field', name='object_field',
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
object_type=ContentType.objects.get_for_model(VLAN), related_object_type=ObjectType.objects.get_for_model(VLAN),
required=False required=False
) )
cf.content_types.set([self.object_type]) cf.object_types.set([self.object_type])
instance = Site.objects.first() instance = Site.objects.first()
self.assertIsNone(instance.custom_field_data[cf.name]) self.assertIsNone(instance.custom_field_data[cf.name])
@ -402,13 +402,13 @@ class CustomFieldTest(TestCase):
self.assertIsNone(instance.custom_field_data.get(cf.name)) self.assertIsNone(instance.custom_field_data.get(cf.name))
def test_rename_customfield(self): def test_rename_customfield(self):
obj_type = ContentType.objects.get_for_model(Site) obj_type = ObjectType.objects.get_for_model(Site)
FIELD_DATA = 'abc' FIELD_DATA = 'abc'
# Create a custom field # Create a custom field
cf = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='field1') cf = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='field1')
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([obj_type])
# Assign custom field data to an object # Assign custom field data to an object
site = Site.objects.create( site = Site.objects.create(
@ -437,7 +437,7 @@ class CustomFieldTest(TestCase):
) )
) )
site = Site.objects.create(name='Site 1', slug='site-1') site = Site.objects.create(name='Site 1', slug='site-1')
object_type = ContentType.objects.get_for_model(Site) object_type = ObjectType.objects.get_for_model(Site)
# Text # Text
CustomField(name='test', type='text', required=True, default="Default text").full_clean() CustomField(name='test', type='text', required=True, default="Default text").full_clean()
@ -498,16 +498,28 @@ class CustomFieldTest(TestCase):
).full_clean() ).full_clean()
# Object # Object
CustomField(name='test', type='object', required=True, object_type=object_type, default=site.pk).full_clean() CustomField(
with self.assertRaises(ValidationError): name='test',
CustomField(name='test', type='object', required=True, object_type=object_type, default="xxx").full_clean() type='object',
required=True,
related_object_type=object_type,
default=site.pk
).full_clean()
with (self.assertRaises(ValidationError)):
CustomField(
name='test',
type='object',
required=True,
related_object_type=object_type,
default="xxx"
).full_clean()
# Multi-object # Multi-object
CustomField( CustomField(
name='test', name='test',
type='multiobject', type='multiobject',
required=True, required=True,
object_type=object_type, related_object_type=object_type,
default=[site.pk] default=[site.pk]
).full_clean() ).full_clean()
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
@ -515,7 +527,7 @@ class CustomFieldTest(TestCase):
name='test', name='test',
type='multiobject', type='multiobject',
required=True, required=True,
object_type=object_type, related_object_type=object_type,
default=["xxx"] default=["xxx"]
).full_clean() ).full_clean()
@ -524,10 +536,10 @@ class CustomFieldManagerTest(TestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
content_type = ContentType.objects.get_for_model(Site) object_type = ObjectType.objects.get_for_model(Site)
custom_field = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo') custom_field = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo')
custom_field.save() custom_field.save()
custom_field.content_types.set([content_type]) custom_field.object_types.set([object_type])
def test_get_for_model(self): def test_get_for_model(self):
self.assertEqual(CustomField.objects.get_for_model(Site).count(), 1) self.assertEqual(CustomField.objects.get_for_model(Site).count(), 1)
@ -538,7 +550,7 @@ class CustomFieldAPITest(APITestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
content_type = ContentType.objects.get_for_model(Site) object_type = ObjectType.objects.get_for_model(Site)
# Create some VLANs # Create some VLANs
vlans = ( vlans = (
@ -581,19 +593,19 @@ class CustomFieldAPITest(APITestCase):
CustomField( CustomField(
type=CustomFieldTypeChoices.TYPE_OBJECT, type=CustomFieldTypeChoices.TYPE_OBJECT,
name='object_field', name='object_field',
object_type=ContentType.objects.get_for_model(VLAN), related_object_type=ObjectType.objects.get_for_model(VLAN),
default=vlans[0].pk, default=vlans[0].pk,
), ),
CustomField( CustomField(
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
name='multiobject_field', name='multiobject_field',
object_type=ContentType.objects.get_for_model(VLAN), related_object_type=ObjectType.objects.get_for_model(VLAN),
default=[vlans[0].pk, vlans[1].pk], default=[vlans[0].pk, vlans[1].pk],
), ),
) )
for cf in custom_fields: for cf in custom_fields:
cf.save() cf.save()
cf.content_types.set([content_type]) cf.object_types.set([object_type])
# Create some sites *after* creating the custom fields. This ensures that # Create some sites *after* creating the custom fields. This ensures that
# default values are not set for the assigned objects. # default values are not set for the assigned objects.
@ -1163,7 +1175,7 @@ class CustomFieldImportTest(TestCase):
) )
for cf in custom_fields: for cf in custom_fields:
cf.save() cf.save()
cf.content_types.set([ContentType.objects.get_for_model(Site)]) cf.object_types.set([ObjectType.objects.get_for_model(Site)])
def test_import(self): def test_import(self):
""" """
@ -1256,11 +1268,11 @@ class CustomFieldModelTest(TestCase):
def setUpTestData(cls): def setUpTestData(cls):
cf1 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='foo') cf1 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='foo')
cf1.save() cf1.save()
cf1.content_types.set([ContentType.objects.get_for_model(Site)]) cf1.object_types.set([ObjectType.objects.get_for_model(Site)])
cf2 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='bar') cf2 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='bar')
cf2.save() cf2.save()
cf2.content_types.set([ContentType.objects.get_for_model(Rack)]) cf2.object_types.set([ObjectType.objects.get_for_model(Rack)])
def test_cf_data(self): def test_cf_data(self):
""" """
@ -1299,7 +1311,7 @@ class CustomFieldModelTest(TestCase):
""" """
cf3 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='baz', required=True) cf3 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='baz', required=True)
cf3.save() cf3.save()
cf3.content_types.set([ContentType.objects.get_for_model(Site)]) cf3.object_types.set([ObjectType.objects.get_for_model(Site)])
site = Site(name='Test Site', slug='test-site') site = Site(name='Test Site', slug='test-site')
@ -1318,7 +1330,7 @@ class CustomFieldModelFilterTest(TestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
obj_type = ContentType.objects.get_for_model(Site) object_type = ObjectType.objects.get_for_model(Site)
manufacturers = Manufacturer.objects.bulk_create(( manufacturers = Manufacturer.objects.bulk_create((
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
@ -1335,17 +1347,17 @@ class CustomFieldModelFilterTest(TestCase):
# Integer filtering # Integer filtering
cf = CustomField(name='cf1', type=CustomFieldTypeChoices.TYPE_INTEGER) cf = CustomField(name='cf1', type=CustomFieldTypeChoices.TYPE_INTEGER)
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Decimal filtering # Decimal filtering
cf = CustomField(name='cf2', type=CustomFieldTypeChoices.TYPE_DECIMAL) cf = CustomField(name='cf2', type=CustomFieldTypeChoices.TYPE_DECIMAL)
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Boolean filtering # Boolean filtering
cf = CustomField(name='cf3', type=CustomFieldTypeChoices.TYPE_BOOLEAN) cf = CustomField(name='cf3', type=CustomFieldTypeChoices.TYPE_BOOLEAN)
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Exact text filtering # Exact text filtering
cf = CustomField( cf = CustomField(
@ -1354,7 +1366,7 @@ class CustomFieldModelFilterTest(TestCase):
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
) )
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Loose text filtering # Loose text filtering
cf = CustomField( cf = CustomField(
@ -1363,12 +1375,12 @@ class CustomFieldModelFilterTest(TestCase):
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
) )
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Date filtering # Date filtering
cf = CustomField(name='cf6', type=CustomFieldTypeChoices.TYPE_DATE) cf = CustomField(name='cf6', type=CustomFieldTypeChoices.TYPE_DATE)
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Exact URL filtering # Exact URL filtering
cf = CustomField( cf = CustomField(
@ -1377,7 +1389,7 @@ class CustomFieldModelFilterTest(TestCase):
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
) )
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Loose URL filtering # Loose URL filtering
cf = CustomField( cf = CustomField(
@ -1386,7 +1398,7 @@ class CustomFieldModelFilterTest(TestCase):
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
) )
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Selection filtering # Selection filtering
cf = CustomField( cf = CustomField(
@ -1395,7 +1407,7 @@ class CustomFieldModelFilterTest(TestCase):
choice_set=choice_set choice_set=choice_set
) )
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Multiselect filtering # Multiselect filtering
cf = CustomField( cf = CustomField(
@ -1404,25 +1416,25 @@ class CustomFieldModelFilterTest(TestCase):
choice_set=choice_set choice_set=choice_set
) )
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Object filtering # Object filtering
cf = CustomField( cf = CustomField(
name='cf11', name='cf11',
type=CustomFieldTypeChoices.TYPE_OBJECT, type=CustomFieldTypeChoices.TYPE_OBJECT,
object_type=ContentType.objects.get_for_model(Manufacturer) related_object_type=ObjectType.objects.get_for_model(Manufacturer)
) )
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
# Multi-object filtering # Multi-object filtering
cf = CustomField( cf = CustomField(
name='cf12', name='cf12',
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
object_type=ContentType.objects.get_for_model(Manufacturer) related_object_type=ObjectType.objects.get_for_model(Manufacturer)
) )
cf.save() cf.save()
cf.content_types.set([obj_type]) cf.object_types.set([object_type])
Site.objects.bulk_create([ Site.objects.bulk_create([
Site(name='Site 1', slug='site-1', custom_field_data={ Site(name='Site 1', slug='site-1', custom_field_data={

View File

@ -3,17 +3,18 @@ import uuid
from unittest.mock import patch from unittest.mock import patch
import django_rq import django_rq
from dcim.choices import SiteStatusChoices
from dcim.models import Site
from django.contrib.contenttypes.models import ContentType
from django.http import HttpResponse from django.http import HttpResponse
from django.urls import reverse from django.urls import reverse
from requests import Session
from rest_framework import status
from core.models import ObjectType
from dcim.choices import SiteStatusChoices
from dcim.models import Site
from extras.choices import EventRuleActionChoices, ObjectChangeActionChoices from extras.choices import EventRuleActionChoices, ObjectChangeActionChoices
from extras.events import enqueue_object, flush_events, serialize_for_event from extras.events import enqueue_object, flush_events, serialize_for_event
from extras.models import EventRule, Tag, Webhook from extras.models import EventRule, Tag, Webhook
from extras.webhooks import generate_signature, send_webhook from extras.webhooks import generate_signature, send_webhook
from requests import Session
from rest_framework import status
from utilities.testing import APITestCase from utilities.testing import APITestCase
@ -29,7 +30,7 @@ class EventRuleTest(APITestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
site_ct = ContentType.objects.get_for_model(Site) site_type = ObjectType.objects.get_for_model(Site)
DUMMY_URL = 'http://localhost:9000/' DUMMY_URL = 'http://localhost:9000/'
DUMMY_SECRET = 'LOOKATMEIMASECRETSTRING' DUMMY_SECRET = 'LOOKATMEIMASECRETSTRING'
@ -39,32 +40,32 @@ class EventRuleTest(APITestCase):
Webhook(name='Webhook 3', payload_url=DUMMY_URL, secret=DUMMY_SECRET), Webhook(name='Webhook 3', payload_url=DUMMY_URL, secret=DUMMY_SECRET),
)) ))
ct = ContentType.objects.get(app_label='extras', model='webhook') webhook_type = ObjectType.objects.get(app_label='extras', model='webhook')
event_rules = EventRule.objects.bulk_create(( event_rules = EventRule.objects.bulk_create((
EventRule( EventRule(
name='Webhook Event 1', name='Webhook Event 1',
type_create=True, type_create=True,
action_type=EventRuleActionChoices.WEBHOOK, action_type=EventRuleActionChoices.WEBHOOK,
action_object_type=ct, action_object_type=webhook_type,
action_object_id=webhooks[0].id action_object_id=webhooks[0].id
), ),
EventRule( EventRule(
name='Webhook Event 2', name='Webhook Event 2',
type_update=True, type_update=True,
action_type=EventRuleActionChoices.WEBHOOK, action_type=EventRuleActionChoices.WEBHOOK,
action_object_type=ct, action_object_type=webhook_type,
action_object_id=webhooks[0].id action_object_id=webhooks[0].id
), ),
EventRule( EventRule(
name='Webhook Event 3', name='Webhook Event 3',
type_delete=True, type_delete=True,
action_type=EventRuleActionChoices.WEBHOOK, action_type=EventRuleActionChoices.WEBHOOK,
action_object_type=ct, action_object_type=webhook_type,
action_object_id=webhooks[0].id action_object_id=webhooks[0].id
), ),
)) ))
for event_rule in event_rules: for event_rule in event_rules:
event_rule.content_types.set([site_ct]) event_rule.object_types.set([site_type])
Tag.objects.bulk_create(( Tag.objects.bulk_create((
Tag(name='Foo', slug='foo'), Tag(name='Foo', slug='foo'),

View File

@ -7,6 +7,7 @@ from django.test import TestCase
from circuits.models import Provider from circuits.models import Provider
from core.choices import ManagedFileRootPathChoices from core.choices import ManagedFileRootPathChoices
from core.models import ObjectType
from dcim.filtersets import SiteFilterSet from dcim.filtersets import SiteFilterSet
from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
from dcim.models import Location from dcim.models import Location
@ -22,9 +23,10 @@ from virtualization.models import Cluster, ClusterGroup, ClusterType
User = get_user_model() User = get_user_model()
class CustomFieldTestCase(TestCase, BaseFilterSetTests): class CustomFieldTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = CustomField.objects.all() queryset = CustomField.objects.all()
filterset = CustomFieldFilterSet filterset = CustomFieldFilterSet
ignore_fields = ('default',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -85,13 +87,23 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
ui_editable=CustomFieldUIEditableChoices.HIDDEN, ui_editable=CustomFieldUIEditableChoices.HIDDEN,
choice_set=choice_sets[1] choice_set=choice_sets[1]
), ),
CustomField(
name='Custom Field 6',
type=CustomFieldTypeChoices.TYPE_OBJECT,
related_object_type=ObjectType.objects.get_by_natural_key('dcim', 'site'),
required=False,
weight=600,
filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED,
ui_visible=CustomFieldUIVisibleChoices.HIDDEN,
ui_editable=CustomFieldUIEditableChoices.HIDDEN
),
) )
CustomField.objects.bulk_create(custom_fields) CustomField.objects.bulk_create(custom_fields)
custom_fields[0].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'site')) custom_fields[0].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'site'))
custom_fields[1].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'rack')) custom_fields[1].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'rack'))
custom_fields[2].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device')) custom_fields[2].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'device'))
custom_fields[3].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device')) custom_fields[3].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'device'))
custom_fields[4].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device')) custom_fields[4].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'device'))
def test_q(self): def test_q(self):
params = {'q': 'foobar1'} params = {'q': 'foobar1'}
@ -101,10 +113,16 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
params = {'name': ['Custom Field 1', 'Custom Field 2']} params = {'name': ['Custom Field 1', 'Custom Field 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_content_types(self): def test_object_type(self):
params = {'content_types': 'dcim.site'} params = {'object_type': 'dcim.site'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'content_type_id': [ContentType.objects.get_by_natural_key('dcim', 'site').pk]} params = {'object_type_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_related_object_type(self):
params = {'related_object_type': 'dcim.site'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'related_object_type_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_required(self): def test_required(self):
@ -138,9 +156,10 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class CustomFieldChoiceSetTestCase(TestCase, BaseFilterSetTests): class CustomFieldChoiceSetTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = CustomFieldChoiceSet.objects.all() queryset = CustomFieldChoiceSet.objects.all()
filterset = CustomFieldChoiceSetFilterSet filterset = CustomFieldChoiceSetFilterSet
ignore_fields = ('extra_choices',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -171,11 +190,10 @@ class CustomFieldChoiceSetTestCase(TestCase, BaseFilterSetTests):
class WebhookTestCase(TestCase, BaseFilterSetTests): class WebhookTestCase(TestCase, BaseFilterSetTests):
queryset = Webhook.objects.all() queryset = Webhook.objects.all()
filterset = WebhookFilterSet filterset = WebhookFilterSet
ignore_fields = ('additional_headers', 'body_template')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
content_types = ContentType.objects.filter(model__in=['region', 'site', 'rack', 'location', 'device'])
webhooks = ( webhooks = (
Webhook( Webhook(
name='Webhook 1', name='Webhook 1',
@ -237,10 +255,11 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
class EventRuleTestCase(TestCase, BaseFilterSetTests): class EventRuleTestCase(TestCase, BaseFilterSetTests):
queryset = EventRule.objects.all() queryset = EventRule.objects.all()
filterset = EventRuleFilterSet filterset = EventRuleFilterSet
ignore_fields = ('action_data', 'conditions')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
content_types = ContentType.objects.filter( object_types = ObjectType.objects.filter(
model__in=['region', 'site', 'rack', 'location', 'device'] model__in=['region', 'site', 'rack', 'location', 'device']
) )
@ -333,11 +352,11 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
), ),
) )
EventRule.objects.bulk_create(event_rules) EventRule.objects.bulk_create(event_rules)
event_rules[0].content_types.add(content_types[0]) event_rules[0].object_types.add(object_types[0])
event_rules[1].content_types.add(content_types[1]) event_rules[1].object_types.add(object_types[1])
event_rules[2].content_types.add(content_types[2]) event_rules[2].object_types.add(object_types[2])
event_rules[3].content_types.add(content_types[3]) event_rules[3].object_types.add(object_types[3])
event_rules[4].content_types.add(content_types[4]) event_rules[4].object_types.add(object_types[4])
def test_q(self): def test_q(self):
params = {'q': 'foobar1'} params = {'q': 'foobar1'}
@ -351,10 +370,10 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
params = {'description': ['foobar1', 'foobar2']} params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_content_types(self): def test_object_type(self):
params = {'content_types': 'dcim.region'} params = {'object_type': 'dcim.region'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'content_type_id': [ContentType.objects.get_for_model(Region).pk]} params = {'object_type_id': [ObjectType.objects.get_for_model(Region).pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_action_type(self): def test_action_type(self):
@ -390,13 +409,13 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
class CustomLinkTestCase(TestCase, BaseFilterSetTests): class CustomLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = CustomLink.objects.all() queryset = CustomLink.objects.all()
filterset = CustomLinkFilterSet filterset = CustomLinkFilterSet
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device']) object_types = ObjectType.objects.filter(model__in=['site', 'rack', 'device'])
custom_links = ( custom_links = (
CustomLink( CustomLink(
@ -426,7 +445,7 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
) )
CustomLink.objects.bulk_create(custom_links) CustomLink.objects.bulk_create(custom_links)
for i, custom_link in enumerate(custom_links): for i, custom_link in enumerate(custom_links):
custom_link.content_types.set([content_types[i]]) custom_link.object_types.set([object_types[i]])
def test_q(self): def test_q(self):
params = {'q': 'Custom Link 1'} params = {'q': 'Custom Link 1'}
@ -436,10 +455,10 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
params = {'name': ['Custom Link 1', 'Custom Link 2']} params = {'name': ['Custom Link 1', 'Custom Link 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_content_types(self): def test_object_type(self):
params = {'content_types': 'dcim.site'} params = {'object_type': 'dcim.site'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]} params = {'object_type_id': [ObjectType.objects.get_for_model(Site).pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_weight(self): def test_weight(self):
@ -459,13 +478,14 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
class SavedFilterTestCase(TestCase, BaseFilterSetTests): class SavedFilterTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = SavedFilter.objects.all() queryset = SavedFilter.objects.all()
filterset = SavedFilterFilterSet filterset = SavedFilterFilterSet
ignore_fields = ('parameters',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device']) object_types = ObjectType.objects.filter(model__in=['site', 'rack', 'device'])
users = ( users = (
User(username='User 1'), User(username='User 1'),
@ -508,7 +528,7 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests):
) )
SavedFilter.objects.bulk_create(saved_filters) SavedFilter.objects.bulk_create(saved_filters)
for i, savedfilter in enumerate(saved_filters): for i, savedfilter in enumerate(saved_filters):
savedfilter.content_types.set([content_types[i]]) savedfilter.object_types.set([object_types[i]])
def test_q(self): def test_q(self):
params = {'q': 'foobar1'} params = {'q': 'foobar1'}
@ -526,10 +546,10 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests):
params = {'description': ['foobar1', 'foobar2']} params = {'description': ['foobar1', 'foobar2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_content_types(self): def test_object_type(self):
params = {'content_types': 'dcim.site'} params = {'object_type': 'dcim.site'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]} params = {'object_type_id': [ObjectType.objects.get_for_model(Site).pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_user(self): def test_user(self):
@ -632,13 +652,14 @@ class BookmarkTestCase(TestCase, BaseFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
class ExportTemplateTestCase(TestCase, BaseFilterSetTests): class ExportTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ExportTemplate.objects.all() queryset = ExportTemplate.objects.all()
filterset = ExportTemplateFilterSet filterset = ExportTemplateFilterSet
ignore_fields = ('template_code', 'data_path')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device']) object_types = ObjectType.objects.filter(model__in=['site', 'rack', 'device'])
export_templates = ( export_templates = (
ExportTemplate(name='Export Template 1', template_code='TESTING', description='foobar1'), ExportTemplate(name='Export Template 1', template_code='TESTING', description='foobar1'),
@ -647,7 +668,7 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
) )
ExportTemplate.objects.bulk_create(export_templates) ExportTemplate.objects.bulk_create(export_templates)
for i, et in enumerate(export_templates): for i, et in enumerate(export_templates):
et.content_types.set([content_types[i]]) et.object_types.set([object_types[i]])
def test_q(self): def test_q(self):
params = {'q': 'foobar1'} params = {'q': 'foobar1'}
@ -657,10 +678,10 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
params = {'name': ['Export Template 1', 'Export Template 2']} params = {'name': ['Export Template 1', 'Export Template 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_content_types(self): def test_object_type(self):
params = {'content_types': 'dcim.site'} params = {'object_type': 'dcim.site'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
params = {'content_type_id': [ContentType.objects.get_for_model(Site).pk]} params = {'object_type_id': [ObjectType.objects.get_for_model(Site).pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_description(self): def test_description(self):
@ -668,9 +689,10 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class ImageAttachmentTestCase(TestCase, BaseFilterSetTests): class ImageAttachmentTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ImageAttachment.objects.all() queryset = ImageAttachment.objects.all()
filterset = ImageAttachmentFilterSet filterset = ImageAttachmentFilterSet
ignore_fields = ('image',)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -692,7 +714,7 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
image_attachments = ( image_attachments = (
ImageAttachment( ImageAttachment(
content_type=site_ct, object_type=site_ct,
object_id=sites[0].pk, object_id=sites[0].pk,
name='Image Attachment 1', name='Image Attachment 1',
image='http://example.com/image1.png', image='http://example.com/image1.png',
@ -700,7 +722,7 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
image_width=100 image_width=100
), ),
ImageAttachment( ImageAttachment(
content_type=site_ct, object_type=site_ct,
object_id=sites[1].pk, object_id=sites[1].pk,
name='Image Attachment 2', name='Image Attachment 2',
image='http://example.com/image2.png', image='http://example.com/image2.png',
@ -708,7 +730,7 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
image_width=100 image_width=100
), ),
ImageAttachment( ImageAttachment(
content_type=rack_ct, object_type=rack_ct,
object_id=racks[0].pk, object_id=racks[0].pk,
name='Image Attachment 3', name='Image Attachment 3',
image='http://example.com/image3.png', image='http://example.com/image3.png',
@ -716,7 +738,7 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
image_width=100 image_width=100
), ),
ImageAttachment( ImageAttachment(
content_type=rack_ct, object_type=rack_ct,
object_id=racks[1].pk, object_id=racks[1].pk,
name='Image Attachment 4', name='Image Attachment 4',
image='http://example.com/image4.png', image='http://example.com/image4.png',
@ -734,23 +756,17 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
params = {'name': ['Image Attachment 1', 'Image Attachment 2']} params = {'name': ['Image Attachment 1', 'Image Attachment 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_content_type(self): def test_object_type(self):
params = {'content_type': 'dcim.site'} params = {'object_type': 'dcim.site'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_content_type_id_and_object_id(self): def test_object_type_id_and_object_id(self):
params = { params = {
'content_type_id': ContentType.objects.get(app_label='dcim', model='site').pk, 'object_type_id': ContentType.objects.get(app_label='dcim', model='site').pk,
'object_id': [Site.objects.first().pk], 'object_id': [Site.objects.first().pk],
} }
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_created(self):
pk_list = self.queryset.values_list('pk', flat=True)[:2]
self.queryset.filter(pk__in=pk_list).update(created=datetime(2021, 1, 1, 0, 0, 0, tzinfo=timezone.utc))
params = {'created': '2021-01-01T00:00:00'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class JournalEntryTestCase(TestCase, ChangeLoggedFilterSetTests): class JournalEntryTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = JournalEntry.objects.all() queryset = JournalEntry.objects.all()
@ -858,6 +874,7 @@ class JournalEntryTestCase(TestCase, ChangeLoggedFilterSetTests):
class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests): class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ConfigContext.objects.all() queryset = ConfigContext.objects.all()
filterset = ConfigContextFilterSet filterset = ConfigContextFilterSet
ignore_fields = ('data', 'data_path')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -1026,11 +1043,11 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'device_type_id': [device_types[0].pk, device_types[1].pk]} params = {'device_type_id': [device_types[0].pk, device_types[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_role(self): def test_device_role(self):
device_roles = DeviceRole.objects.all()[:2] device_roles = DeviceRole.objects.all()[:2]
params = {'role_id': [device_roles[0].pk, device_roles[1].pk]} params = {'device_role_id': [device_roles[0].pk, device_roles[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'role': [device_roles[0].slug, device_roles[1].slug]} params = {'device_role': [device_roles[0].slug, device_roles[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_platform(self): def test_platform(self):
@ -1081,9 +1098,10 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class ConfigTemplateTestCase(TestCase, BaseFilterSetTests): class ConfigTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ConfigTemplate.objects.all() queryset = ConfigTemplate.objects.all()
filterset = ConfigTemplateFilterSet filterset = ConfigTemplateFilterSet
ignore_fields = ('template_code', 'environment_params', 'data_path')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -1110,12 +1128,99 @@ class ConfigTemplateTestCase(TestCase, BaseFilterSetTests):
class TagTestCase(TestCase, ChangeLoggedFilterSetTests): class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = Tag.objects.all() queryset = Tag.objects.all()
filterset = TagFilterSet filterset = TagFilterSet
ignore_fields = (
'object_types',
# Reverse relationships (to tagged models) we can ignore
'aggregate',
'asn',
'asnrange',
'cable',
'circuit',
'circuittermination',
'circuittype',
'cluster',
'clustergroup',
'clustertype',
'configtemplate',
'consoleport',
'consoleserverport',
'contact',
'contactassignment',
'contactgroup',
'contactrole',
'datasource',
'device',
'devicebay',
'devicerole',
'devicetype',
'dummymodel', # From dummy_plugin
'eventrule',
'fhrpgroup',
'frontport',
'ikepolicy',
'ikeproposal',
'interface',
'inventoryitem',
'inventoryitemrole',
'ipaddress',
'iprange',
'ipsecpolicy',
'ipsecprofile',
'ipsecproposal',
'journalentry',
'l2vpn',
'l2vpntermination',
'location',
'manufacturer',
'module',
'modulebay',
'moduletype',
'platform',
'powerfeed',
'poweroutlet',
'powerpanel',
'powerport',
'prefix',
'provider',
'provideraccount',
'providernetwork',
'rack',
'rackreservation',
'rackrole',
'rearport',
'region',
'rir',
'role',
'routetarget',
'service',
'servicetemplate',
'site',
'sitegroup',
'tenant',
'tenantgroup',
'tunnel',
'tunnelgroup',
'tunneltermination',
'virtualchassis',
'virtualdevicecontext',
'virtualdisk',
'virtualmachine',
'vlan',
'vlangroup',
'vminterface',
'vrf',
'webhook',
'wirelesslan',
'wirelesslangroup',
'wirelesslink',
)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
content_types = { object_types = {
'site': ContentType.objects.get_by_natural_key('dcim', 'site'), 'site': ObjectType.objects.get_by_natural_key('dcim', 'site'),
'provider': ContentType.objects.get_by_natural_key('circuits', 'provider'), 'provider': ObjectType.objects.get_by_natural_key('circuits', 'provider'),
} }
tags = ( tags = (
@ -1124,8 +1229,8 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
Tag(name='Tag 3', slug='tag-3', color='0000ff'), Tag(name='Tag 3', slug='tag-3', color='0000ff'),
) )
Tag.objects.bulk_create(tags) Tag.objects.bulk_create(tags)
tags[0].object_types.add(content_types['site']) tags[0].object_types.add(object_types['site'])
tags[1].object_types.add(content_types['provider']) tags[1].object_types.add(object_types['provider'])
# Apply some tags so we can filter by content type # Apply some tags so we can filter by content type
site = Site.objects.create(name='Site 1', slug='site-1') site = Site.objects.create(name='Site 1', slug='site-1')
@ -1163,12 +1268,12 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_object_types(self): def test_object_types(self):
params = {'for_object_type_id': [ContentType.objects.get_by_natural_key('dcim', 'site').pk]} params = {'for_object_type_id': [ObjectType.objects.get_by_natural_key('dcim', 'site').pk]}
self.assertEqual( self.assertEqual(
list(self.filterset(params, self.queryset).qs.values_list('name', flat=True)), list(self.filterset(params, self.queryset).qs.values_list('name', flat=True)),
['Tag 1', 'Tag 3'] ['Tag 1', 'Tag 3']
) )
params = {'for_object_type_id': [ContentType.objects.get_by_natural_key('circuits', 'provider').pk]} params = {'for_object_type_id': [ObjectType.objects.get_by_natural_key('circuits', 'provider').pk]}
self.assertEqual( self.assertEqual(
list(self.filterset(params, self.queryset).qs.values_list('name', flat=True)), list(self.filterset(params, self.queryset).qs.values_list('name', flat=True)),
['Tag 2', 'Tag 3'] ['Tag 2', 'Tag 3']
@ -1178,6 +1283,7 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
class ObjectChangeTestCase(TestCase, BaseFilterSetTests): class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
queryset = ObjectChange.objects.all() queryset = ObjectChange.objects.all()
filterset = ObjectChangeFilterSet filterset = ObjectChangeFilterSet
ignore_fields = ('prechange_data', 'postchange_data')
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):

View File

@ -1,6 +1,6 @@
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase from django.test import TestCase
from core.models import ObjectType
from dcim.forms import SiteForm from dcim.forms import SiteForm
from dcim.models import Site from dcim.models import Site
from extras.choices import CustomFieldTypeChoices from extras.choices import CustomFieldTypeChoices
@ -12,66 +12,66 @@ class CustomFieldModelFormTest(TestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
obj_type = ContentType.objects.get_for_model(Site) object_type = ObjectType.objects.get_for_model(Site)
choice_set = CustomFieldChoiceSet.objects.create( choice_set = CustomFieldChoiceSet.objects.create(
name='Choice Set 1', name='Choice Set 1',
extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C')) extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C'))
) )
cf_text = CustomField.objects.create(name='text', type=CustomFieldTypeChoices.TYPE_TEXT) cf_text = CustomField.objects.create(name='text', type=CustomFieldTypeChoices.TYPE_TEXT)
cf_text.content_types.set([obj_type]) cf_text.object_types.set([object_type])
cf_longtext = CustomField.objects.create(name='longtext', type=CustomFieldTypeChoices.TYPE_LONGTEXT) cf_longtext = CustomField.objects.create(name='longtext', type=CustomFieldTypeChoices.TYPE_LONGTEXT)
cf_longtext.content_types.set([obj_type]) cf_longtext.object_types.set([object_type])
cf_integer = CustomField.objects.create(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER) cf_integer = CustomField.objects.create(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER)
cf_integer.content_types.set([obj_type]) cf_integer.object_types.set([object_type])
cf_integer = CustomField.objects.create(name='decimal', type=CustomFieldTypeChoices.TYPE_DECIMAL) cf_integer = CustomField.objects.create(name='decimal', type=CustomFieldTypeChoices.TYPE_DECIMAL)
cf_integer.content_types.set([obj_type]) cf_integer.object_types.set([object_type])
cf_boolean = CustomField.objects.create(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN) cf_boolean = CustomField.objects.create(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN)
cf_boolean.content_types.set([obj_type]) cf_boolean.object_types.set([object_type])
cf_date = CustomField.objects.create(name='date', type=CustomFieldTypeChoices.TYPE_DATE) cf_date = CustomField.objects.create(name='date', type=CustomFieldTypeChoices.TYPE_DATE)
cf_date.content_types.set([obj_type]) cf_date.object_types.set([object_type])
cf_datetime = CustomField.objects.create(name='datetime', type=CustomFieldTypeChoices.TYPE_DATETIME) cf_datetime = CustomField.objects.create(name='datetime', type=CustomFieldTypeChoices.TYPE_DATETIME)
cf_datetime.content_types.set([obj_type]) cf_datetime.object_types.set([object_type])
cf_url = CustomField.objects.create(name='url', type=CustomFieldTypeChoices.TYPE_URL) cf_url = CustomField.objects.create(name='url', type=CustomFieldTypeChoices.TYPE_URL)
cf_url.content_types.set([obj_type]) cf_url.object_types.set([object_type])
cf_json = CustomField.objects.create(name='json', type=CustomFieldTypeChoices.TYPE_JSON) cf_json = CustomField.objects.create(name='json', type=CustomFieldTypeChoices.TYPE_JSON)
cf_json.content_types.set([obj_type]) cf_json.object_types.set([object_type])
cf_select = CustomField.objects.create( cf_select = CustomField.objects.create(
name='select', name='select',
type=CustomFieldTypeChoices.TYPE_SELECT, type=CustomFieldTypeChoices.TYPE_SELECT,
choice_set=choice_set choice_set=choice_set
) )
cf_select.content_types.set([obj_type]) cf_select.object_types.set([object_type])
cf_multiselect = CustomField.objects.create( cf_multiselect = CustomField.objects.create(
name='multiselect', name='multiselect',
type=CustomFieldTypeChoices.TYPE_MULTISELECT, type=CustomFieldTypeChoices.TYPE_MULTISELECT,
choice_set=choice_set choice_set=choice_set
) )
cf_multiselect.content_types.set([obj_type]) cf_multiselect.object_types.set([object_type])
cf_object = CustomField.objects.create( cf_object = CustomField.objects.create(
name='object', name='object',
type=CustomFieldTypeChoices.TYPE_OBJECT, type=CustomFieldTypeChoices.TYPE_OBJECT,
object_type=ContentType.objects.get_for_model(Site) related_object_type=ObjectType.objects.get_for_model(Site)
) )
cf_object.content_types.set([obj_type]) cf_object.object_types.set([object_type])
cf_multiobject = CustomField.objects.create( cf_multiobject = CustomField.objects.create(
name='multiobject', name='multiobject',
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT, type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
object_type=ContentType.objects.get_for_model(Site) related_object_type=ObjectType.objects.get_for_model(Site)
) )
cf_multiobject.content_types.set([obj_type]) cf_multiobject.object_types.set([object_type])
def test_empty_values(self): def test_empty_values(self):
""" """
@ -99,7 +99,7 @@ class SavedFilterFormTest(TestCase):
form = SavedFilterForm({ form = SavedFilterForm({
'name': 'test-sf', 'name': 'test-sf',
'slug': 'test-sf', 'slug': 'test-sf',
'content_types': [ContentType.objects.get_for_model(Site).pk], 'object_types': [ObjectType.objects.get_for_model(Site).pk],
'weight': 100, 'weight': 100,
'parameters': { 'parameters': {
"status": [ "status": [

View File

@ -1,6 +1,6 @@
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase from django.test import TestCase
from core.models import ObjectType
from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup
from extras.models import ConfigContext, Tag from extras.models import ConfigContext, Tag
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
@ -22,7 +22,7 @@ class TagTest(TestCase):
# Create a Tag that can only be applied to Regions # Create a Tag that can only be applied to Regions
tag = Tag.objects.create(name='Tag 1', slug='tag-1') tag = Tag.objects.create(name='Tag 1', slug='tag-1')
tag.object_types.add(ContentType.objects.get_by_natural_key('dcim', 'region')) tag.object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'region'))
# Apply the Tag to a Region # Apply the Tag to a Region
region.tags.add(tag) region.tags.add(tag)

View File

@ -5,6 +5,7 @@ from django.contrib.auth import get_user_model
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 core.models import ObjectType
from dcim.models import DeviceType, Manufacturer, Site from dcim.models import DeviceType, Manufacturer, Site
from extras.choices import * from extras.choices import *
from extras.models import * from extras.models import *
@ -19,7 +20,7 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
site_ct = ContentType.objects.get_for_model(Site) site_type = ObjectType.objects.get_for_model(Site)
CustomFieldChoiceSet.objects.create( CustomFieldChoiceSet.objects.create(
name='Choice Set 1', name='Choice Set 1',
extra_choices=( extra_choices=(
@ -36,13 +37,13 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
) )
for customfield in custom_fields: for customfield in custom_fields:
customfield.save() customfield.save()
customfield.content_types.add(site_ct) customfield.object_types.add(site_type)
cls.form_data = { cls.form_data = {
'name': 'field_x', 'name': 'field_x',
'label': 'Field X', 'label': 'Field X',
'type': 'text', 'type': 'text',
'content_types': [site_ct.pk], 'object_types': [site_type.pk],
'search_weight': 2000, 'search_weight': 2000,
'filter_logic': CustomFieldFilterLogicChoices.FILTER_EXACT, 'filter_logic': CustomFieldFilterLogicChoices.FILTER_EXACT,
'default': None, 'default': None,
@ -53,7 +54,7 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
} }
cls.csv_data = ( cls.csv_data = (
'name,label,type,content_types,object_type,weight,search_weight,filter_logic,choice_set,validation_minimum,validation_maximum,validation_regex,ui_visible,ui_editable', 'name,label,type,object_types,related_object_type,weight,search_weight,filter_logic,choice_set,validation_minimum,validation_maximum,validation_regex,ui_visible,ui_editable',
'field4,Field 4,text,dcim.site,,100,1000,exact,,,,[a-z]{3},always,yes', 'field4,Field 4,text,dcim.site,,100,1000,exact,,,,[a-z]{3},always,yes',
'field5,Field 5,integer,dcim.site,,100,2000,exact,,1,100,,always,yes', 'field5,Field 5,integer,dcim.site,,100,2000,exact,,1,100,,always,yes',
'field6,Field 6,select,dcim.site,,100,3000,exact,Choice Set 1,,,,always,yes', 'field6,Field 6,select,dcim.site,,100,3000,exact,Choice Set 1,,,,always,yes',
@ -137,7 +138,7 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
site_ct = ContentType.objects.get_for_model(Site) site_type = ObjectType.objects.get_for_model(Site)
custom_links = ( custom_links = (
CustomLink(name='Custom Link 1', enabled=True, link_text='Link 1', link_url='http://example.com/?1'), CustomLink(name='Custom Link 1', enabled=True, link_text='Link 1', link_url='http://example.com/?1'),
CustomLink(name='Custom Link 2', enabled=True, link_text='Link 2', link_url='http://example.com/?2'), CustomLink(name='Custom Link 2', enabled=True, link_text='Link 2', link_url='http://example.com/?2'),
@ -145,11 +146,11 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
) )
CustomLink.objects.bulk_create(custom_links) CustomLink.objects.bulk_create(custom_links)
for i, custom_link in enumerate(custom_links): for i, custom_link in enumerate(custom_links):
custom_link.content_types.set([site_ct]) custom_link.object_types.set([site_type])
cls.form_data = { cls.form_data = {
'name': 'Custom Link X', 'name': 'Custom Link X',
'content_types': [site_ct.pk], 'object_types': [site_type.pk],
'enabled': False, 'enabled': False,
'weight': 100, 'weight': 100,
'button_class': CustomLinkButtonClassChoices.DEFAULT, 'button_class': CustomLinkButtonClassChoices.DEFAULT,
@ -158,7 +159,7 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
} }
cls.csv_data = ( cls.csv_data = (
"name,content_types,enabled,weight,button_class,link_text,link_url", "name,object_types,enabled,weight,button_class,link_text,link_url",
"Custom Link 4,dcim.site,True,100,blue,Link 4,http://exmaple.com/?4", "Custom Link 4,dcim.site,True,100,blue,Link 4,http://exmaple.com/?4",
"Custom Link 5,dcim.site,True,100,blue,Link 5,http://exmaple.com/?5", "Custom Link 5,dcim.site,True,100,blue,Link 5,http://exmaple.com/?5",
"Custom Link 6,dcim.site,False,100,blue,Link 6,http://exmaple.com/?6", "Custom Link 6,dcim.site,False,100,blue,Link 6,http://exmaple.com/?6",
@ -183,7 +184,7 @@ class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
site_ct = ContentType.objects.get_for_model(Site) site_type = ObjectType.objects.get_for_model(Site)
users = ( users = (
User(username='User 1'), User(username='User 1'),
@ -217,12 +218,12 @@ class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
) )
SavedFilter.objects.bulk_create(saved_filters) SavedFilter.objects.bulk_create(saved_filters)
for i, savedfilter in enumerate(saved_filters): for i, savedfilter in enumerate(saved_filters):
savedfilter.content_types.set([site_ct]) savedfilter.object_types.set([site_type])
cls.form_data = { cls.form_data = {
'name': 'Saved Filter X', 'name': 'Saved Filter X',
'slug': 'saved-filter-x', 'slug': 'saved-filter-x',
'content_types': [site_ct.pk], 'object_types': [site_type.pk],
'description': 'Foo', 'description': 'Foo',
'weight': 1000, 'weight': 1000,
'enabled': True, 'enabled': True,
@ -231,7 +232,7 @@ class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
} }
cls.csv_data = ( cls.csv_data = (
'name,slug,content_types,weight,enabled,shared,parameters', 'name,slug,object_types,weight,enabled,shared,parameters',
'Saved Filter 4,saved-filter-4,dcim.device,400,True,True,{"foo": "a"}', 'Saved Filter 4,saved-filter-4,dcim.device,400,True,True,{"foo": "a"}',
'Saved Filter 5,saved-filter-5,dcim.device,500,True,True,{"foo": "b"}', 'Saved Filter 5,saved-filter-5,dcim.device,500,True,True,{"foo": "b"}',
'Saved Filter 6,saved-filter-6,dcim.device,600,True,True,{"foo": "c"}', 'Saved Filter 6,saved-filter-6,dcim.device,600,True,True,{"foo": "c"}',
@ -302,7 +303,7 @@ class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
site_ct = ContentType.objects.get_for_model(Site) site_type = ObjectType.objects.get_for_model(Site)
TEMPLATE_CODE = """{% for object in queryset %}{{ object }}{% endfor %}""" TEMPLATE_CODE = """{% for object in queryset %}{{ object }}{% endfor %}"""
export_templates = ( export_templates = (
@ -312,16 +313,16 @@ class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
) )
ExportTemplate.objects.bulk_create(export_templates) ExportTemplate.objects.bulk_create(export_templates)
for et in export_templates: for et in export_templates:
et.content_types.set([site_ct]) et.object_types.set([site_type])
cls.form_data = { cls.form_data = {
'name': 'Export Template X', 'name': 'Export Template X',
'content_types': [site_ct.pk], 'object_types': [site_type.pk],
'template_code': TEMPLATE_CODE, 'template_code': TEMPLATE_CODE,
} }
cls.csv_data = ( cls.csv_data = (
"name,content_types,template_code", "name,object_types,template_code",
f"Export Template 4,dcim.site,{TEMPLATE_CODE}", f"Export Template 4,dcim.site,{TEMPLATE_CODE}",
f"Export Template 5,dcim.site,{TEMPLATE_CODE}", f"Export Template 5,dcim.site,{TEMPLATE_CODE}",
f"Export Template 6,dcim.site,{TEMPLATE_CODE}", f"Export Template 6,dcim.site,{TEMPLATE_CODE}",
@ -396,7 +397,7 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase):
for webhook in webhooks: for webhook in webhooks:
webhook.save() webhook.save()
site_ct = ContentType.objects.get_for_model(Site) site_type = ObjectType.objects.get_for_model(Site)
event_rules = ( event_rules = (
EventRule(name='EventRule 1', type_create=True, action_object=webhooks[0]), EventRule(name='EventRule 1', type_create=True, action_object=webhooks[0]),
EventRule(name='EventRule 2', type_create=True, action_object=webhooks[1]), EventRule(name='EventRule 2', type_create=True, action_object=webhooks[1]),
@ -404,12 +405,12 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase):
) )
for event in event_rules: for event in event_rules:
event.save() event.save()
event.content_types.add(site_ct) event.object_types.add(site_type)
webhook_ct = ContentType.objects.get_for_model(Webhook) webhook_ct = ContentType.objects.get_for_model(Webhook)
cls.form_data = { cls.form_data = {
'name': 'Event X', 'name': 'Event X',
'content_types': [site_ct.pk], 'object_types': [site_type.pk],
'type_create': False, 'type_create': False,
'type_update': True, 'type_update': True,
'type_delete': True, 'type_delete': True,
@ -422,7 +423,7 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase):
} }
cls.csv_data = ( cls.csv_data = (
"name,content_types,type_create,action_type,action_object", "name,object_types,type_create,action_type,action_object",
"Webhook 4,dcim.site,True,webhook,Webhook 1", "Webhook 4,dcim.site,True,webhook,Webhook 1",
) )
@ -651,7 +652,7 @@ class CustomLinkTest(TestCase):
new_window=False new_window=False
) )
customlink.save() customlink.save()
customlink.content_types.set([ContentType.objects.get_for_model(Site)]) customlink.object_types.set([ObjectType.objects.get_for_model(Site)])
site = Site(name='Test Site', slug='test-site') site = Site(name='Test Site', slug='test-site')
site.save() site.save()

View File

@ -24,7 +24,7 @@ def image_upload(instance, filename):
elif instance.name: elif instance.name:
filename = instance.name filename = instance.name
return '{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename) return '{}{}_{}_{}'.format(path, instance.object_type.name, instance.object_id, filename)
def is_script(obj): def is_script(obj):

View File

@ -17,6 +17,7 @@ from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
from extras.dashboard.utils import get_widget_class from extras.dashboard.utils import get_widget_class
from netbox.constants import DEFAULT_ACTION_PERMISSIONS from netbox.constants import DEFAULT_ACTION_PERMISSIONS
from netbox.views import generic from netbox.views import generic
from netbox.views.generic.mixins import TableMixin
from utilities.forms import ConfirmationForm, get_field_value from utilities.forms import ConfirmationForm, get_field_value
from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.paginator import EnhancedPaginator, get_paginate_count
from utilities.rqworker import get_workers_for_queue from utilities.rqworker import get_workers_for_queue
@ -26,6 +27,7 @@ from utilities.views import ContentTypePermissionRequiredMixin, register_model_v
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .models import * from .models import *
from .scripts import run_script from .scripts import run_script
from .tables import ReportResultsTable, ScriptResultsTable
# #
@ -46,9 +48,9 @@ class CustomFieldView(generic.ObjectView):
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
related_models = () related_models = ()
for content_type in instance.content_types.all(): for object_type in instance.object_types.all():
related_models += ( related_models += (
content_type.model_class().objects.restrict(request.user, 'view').exclude( object_type.model_class().objects.restrict(request.user, 'view').exclude(
Q(**{f'custom_field_data__{instance.name}': ''}) | Q(**{f'custom_field_data__{instance.name}': ''}) |
Q(**{f'custom_field_data__{instance.name}': None}) Q(**{f'custom_field_data__{instance.name}': None})
), ),
@ -762,8 +764,8 @@ class ImageAttachmentEditView(generic.ObjectEditView):
def alter_object(self, instance, request, args, kwargs): def alter_object(self, instance, request, args, kwargs):
if not instance.pk: if not instance.pk:
# Assign the parent object based on URL kwargs # Assign the parent object based on URL kwargs
content_type = get_object_or_404(ContentType, pk=request.GET.get('content_type')) object_type = get_object_or_404(ContentType, pk=request.GET.get('object_type'))
instance.parent = get_object_or_404(content_type.model_class(), pk=request.GET.get('object_id')) instance.parent = get_object_or_404(object_type.model_class(), pk=request.GET.get('object_id'))
return instance return instance
def get_return_url(self, request, obj=None): def get_return_url(self, request, obj=None):
@ -771,7 +773,7 @@ class ImageAttachmentEditView(generic.ObjectEditView):
def get_extra_addanother_params(self, request): def get_extra_addanother_params(self, request):
return { return {
'content_type': request.GET.get('content_type'), 'object_type': request.GET.get('object_type'),
'object_id': request.GET.get('object_id'), 'object_id': request.GET.get('object_id'),
} }
@ -1143,19 +1145,72 @@ class LegacyScriptRedirectView(ContentTypePermissionRequiredMixin, View):
return redirect(f'{url}{path}') return redirect(f'{url}{path}')
class ScriptResultView(generic.ObjectView): class ScriptResultView(TableMixin, generic.ObjectView):
queryset = Job.objects.all() queryset = Job.objects.all()
def get_required_permission(self): def get_required_permission(self):
return 'extras.view_script' return 'extras.view_script'
def get_table(self, job, request, bulk_actions=True):
data = []
tests = None
table = None
index = 0
if job.data:
if 'log' in job.data:
if 'tests' in job.data:
tests = job.data['tests']
for log in job.data['log']:
index += 1
result = {
'index': index,
'time': log.get('time'),
'status': log.get('status'),
'message': log.get('message'),
}
data.append(result)
table = ScriptResultsTable(data, user=request.user)
table.configure(request)
else:
# for legacy reports
tests = job.data
if tests:
for method, test_data in tests.items():
if 'log' in test_data:
for time, status, obj, url, message in test_data['log']:
index += 1
result = {
'index': index,
'method': method,
'time': time,
'status': status,
'object': obj,
'url': url,
'message': message,
}
data.append(result)
table = ReportResultsTable(data, user=request.user)
table.configure(request)
return table
def get(self, request, **kwargs): def get(self, request, **kwargs):
table = None
job = get_object_or_404(Job.objects.all(), pk=kwargs.get('job_pk')) job = get_object_or_404(Job.objects.all(), pk=kwargs.get('job_pk'))
if job.completed:
table = self.get_table(job, request, bulk_actions=False)
context = { context = {
'script': job.object, 'script': job.object,
'job': job, 'job': job,
'table': table,
} }
if job.data and 'log' in job.data: if job.data and 'log' in job.data:
# Script # Script
context['tests'] = job.data.get('tests', {}) context['tests'] = job.data.get('tests', {})

View File

@ -1,510 +1,8 @@
from django.contrib.contenttypes.models import ContentType from .serializers_.asns import *
from drf_spectacular.utils import extend_schema_field from .serializers_.vrfs import *
from rest_framework import serializers from .serializers_.roles import *
from .serializers_.vlans import *
from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer from .serializers_.ip import *
from ipam.choices import * from .serializers_.fhrpgroups import *
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES from .serializers_.services import *
from ipam.models import *
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer
from netbox.constants import NESTED_SERIALIZER_PREFIX
from tenancy.api.nested_serializers import NestedTenantSerializer
from utilities.api import get_serializer_for_model
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer
from .field_serializers import IPAddressField, IPNetworkField
from .nested_serializers import * from .nested_serializers import *
#
# ASN ranges
#
class ASNRangeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asnrange-detail')
rir = NestedRIRSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True)
asn_count = serializers.IntegerField(read_only=True)
class Meta:
model = ASNRange
fields = [
'id', 'url', 'display', 'name', 'slug', 'rir', 'start', 'end', 'tenant', 'description', 'tags',
'custom_fields', 'created', 'last_updated', 'asn_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
#
# ASNs
#
class ASNSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
rir = NestedRIRSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
# Related object counts
site_count = RelatedObjectCountField('sites')
provider_count = RelatedObjectCountField('providers')
class Meta:
model = ASN
fields = [
'id', 'url', 'display', 'asn', 'rir', 'tenant', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated', 'site_count', 'provider_count',
]
brief_fields = ('id', 'url', 'display', 'asn', 'description')
class AvailableASNSerializer(serializers.Serializer):
"""
Representation of an ASN which does not exist in the database.
"""
asn = serializers.IntegerField(read_only=True)
description = serializers.CharField(required=False)
def to_representation(self, asn):
rir = NestedRIRSerializer(self.context['range'].rir, context={
'request': self.context['request']
}).data
return {
'rir': rir,
'asn': asn,
}
#
# VRFs
#
class VRFSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
tenant = NestedTenantSerializer(required=False, allow_null=True)
import_targets = SerializedPKRelatedField(
queryset=RouteTarget.objects.all(),
serializer=NestedRouteTargetSerializer,
required=False,
many=True
)
export_targets = SerializedPKRelatedField(
queryset=RouteTarget.objects.all(),
serializer=NestedRouteTargetSerializer,
required=False,
many=True
)
# Related object counts
ipaddress_count = RelatedObjectCountField('ip_addresses')
prefix_count = RelatedObjectCountField('prefixes')
class Meta:
model = VRF
fields = [
'id', 'url', 'display', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'comments',
'import_targets', 'export_targets', 'tags', 'custom_fields', 'created', 'last_updated', 'ipaddress_count',
'prefix_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'rd', 'description', 'prefix_count')
#
# Route targets
#
class RouteTargetSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail')
tenant = NestedTenantSerializer(required=False, allow_null=True)
class Meta:
model = RouteTarget
fields = [
'id', 'url', 'display', 'name', 'tenant', 'description', 'comments', 'tags', 'custom_fields', 'created',
'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'description')
#
# RIRs/aggregates
#
class RIRSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
# Related object counts
aggregate_count = RelatedObjectCountField('aggregates')
class Meta:
model = RIR
fields = [
'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'tags', 'custom_fields', 'created',
'last_updated', 'aggregate_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'aggregate_count')
class AggregateSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
rir = NestedRIRSerializer()
tenant = NestedTenantSerializer(required=False, allow_null=True)
prefix = IPNetworkField()
class Meta:
model = Aggregate
fields = [
'id', 'url', 'display', 'family', 'prefix', 'rir', 'tenant', 'date_added', 'description', 'comments',
'tags', 'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description')
#
# FHRP Groups
#
class FHRPGroupSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroup-detail')
ip_addresses = NestedIPAddressSerializer(many=True, read_only=True)
class Meta:
model = FHRPGroup
fields = [
'id', 'name', 'url', 'display', 'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'comments',
'tags', 'custom_fields', 'created', 'last_updated', 'ip_addresses',
]
brief_fields = ('id', 'url', 'display', 'protocol', 'group_id', 'description')
class FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail')
group = NestedFHRPGroupSerializer()
interface_type = ContentTypeField(
queryset=ContentType.objects.all()
)
interface = serializers.SerializerMethodField(read_only=True)
class Meta:
model = FHRPGroupAssignment
fields = [
'id', 'url', 'display', 'group', 'interface_type', 'interface_id', 'interface', 'priority', 'created',
'last_updated',
]
brief_fields = ('id', 'url', 'display', 'group', 'interface_type', 'interface_id', 'priority')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_interface(self, obj):
if obj.interface is None:
return None
serializer = get_serializer_for_model(obj.interface, prefix=NESTED_SERIALIZER_PREFIX)
context = {'request': self.context['request']}
return serializer(obj.interface, context=context).data
#
# VLANs
#
class RoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
# Related object counts
prefix_count = RelatedObjectCountField('prefixes')
vlan_count = RelatedObjectCountField('vlans')
class Meta:
model = Role
fields = [
'id', 'url', 'display', 'name', 'slug', 'weight', 'description', 'tags', 'custom_fields', 'created',
'last_updated', 'prefix_count', 'vlan_count',
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'prefix_count', 'vlan_count')
class VLANGroupSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
scope_type = ContentTypeField(
queryset=ContentType.objects.filter(
model__in=VLANGROUP_SCOPE_TYPES
),
allow_null=True,
required=False,
default=None
)
scope_id = serializers.IntegerField(allow_null=True, required=False, default=None)
scope = serializers.SerializerMethodField(read_only=True)
utilization = serializers.CharField(read_only=True)
# Related object counts
vlan_count = RelatedObjectCountField('vlans')
class Meta:
model = VLANGroup
fields = [
'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'min_vid', 'max_vid',
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count', 'utilization'
]
brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'vlan_count')
validators = []
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_scope(self, obj):
if obj.scope_id is None:
return None
serializer = get_serializer_for_model(obj.scope, prefix=NESTED_SERIALIZER_PREFIX)
context = {'request': self.context['request']}
return serializer(obj.scope, context=context).data
class VLANSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
site = NestedSiteSerializer(required=False, allow_null=True)
group = NestedVLANGroupSerializer(required=False, allow_null=True, default=None)
tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceField(choices=VLANStatusChoices, required=False)
role = NestedRoleSerializer(required=False, allow_null=True)
l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True)
# Related object counts
prefix_count = RelatedObjectCountField('prefixes')
class Meta:
model = VLAN
fields = [
'id', 'url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description',
'comments', 'l2vpn_termination', 'tags', 'custom_fields', 'created', 'last_updated', 'prefix_count',
]
brief_fields = ('id', 'url', 'display', 'vid', 'name', 'description')
class AvailableVLANSerializer(serializers.Serializer):
"""
Representation of a VLAN which does not exist in the database.
"""
vid = serializers.IntegerField(read_only=True)
group = NestedVLANGroupSerializer(read_only=True)
def to_representation(self, instance):
return {
'vid': instance,
'group': NestedVLANGroupSerializer(
self.context['group'],
context={'request': self.context['request']}
).data,
}
class CreateAvailableVLANSerializer(NetBoxModelSerializer):
site = NestedSiteSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceField(choices=VLANStatusChoices, required=False)
role = NestedRoleSerializer(required=False, allow_null=True)
class Meta:
model = VLAN
fields = [
'name', 'site', 'tenant', 'status', 'role', 'description', 'tags', 'custom_fields',
]
def validate(self, data):
# Bypass model validation since we don't have a VID yet
return data
#
# Prefixes
#
class PrefixSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
site = NestedSiteSerializer(required=False, allow_null=True)
vrf = NestedVRFSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
vlan = NestedVLANSerializer(required=False, allow_null=True)
status = ChoiceField(choices=PrefixStatusChoices, required=False)
role = NestedRoleSerializer(required=False, allow_null=True)
children = serializers.IntegerField(read_only=True)
_depth = serializers.IntegerField(read_only=True)
prefix = IPNetworkField()
class Meta:
model = Prefix
fields = [
'id', 'url', 'display', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool',
'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'children',
'_depth',
]
brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description', '_depth')
class PrefixLengthSerializer(serializers.Serializer):
prefix_length = serializers.IntegerField()
def to_internal_value(self, data):
requested_prefix = data.get('prefix_length')
if requested_prefix is None:
raise serializers.ValidationError({
'prefix_length': 'this field can not be missing'
})
if not isinstance(requested_prefix, int):
raise serializers.ValidationError({
'prefix_length': 'this field must be int type'
})
prefix = self.context.get('prefix')
if prefix.family == 4 and requested_prefix > 32:
raise serializers.ValidationError({
'prefix_length': 'Invalid prefix length ({}) for IPv4'.format((requested_prefix))
})
elif prefix.family == 6 and requested_prefix > 128:
raise serializers.ValidationError({
'prefix_length': 'Invalid prefix length ({}) for IPv6'.format((requested_prefix))
})
return data
class AvailablePrefixSerializer(serializers.Serializer):
"""
Representation of a prefix which does not exist in the database.
"""
family = serializers.IntegerField(read_only=True)
prefix = serializers.CharField(read_only=True)
vrf = NestedVRFSerializer(read_only=True)
def to_representation(self, instance):
if self.context.get('vrf'):
vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data
else:
vrf = None
return {
'family': instance.version,
'prefix': str(instance),
'vrf': vrf,
}
#
# IP ranges
#
class IPRangeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:iprange-detail')
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
start_address = IPAddressField()
end_address = IPAddressField()
vrf = NestedVRFSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceField(choices=IPRangeStatusChoices, required=False)
role = NestedRoleSerializer(required=False, allow_null=True)
class Meta:
model = IPRange
fields = [
'id', 'url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant', 'status', 'role',
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'family', 'start_address', 'end_address', 'description')
#
# IP addresses
#
class IPAddressSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
address = IPAddressField()
vrf = NestedVRFSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceField(choices=IPAddressStatusChoices, required=False)
role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False)
assigned_object_type = ContentTypeField(
queryset=ContentType.objects.filter(IPADDRESS_ASSIGNMENT_MODELS),
required=False,
allow_null=True
)
assigned_object = serializers.SerializerMethodField(read_only=True)
nat_inside = NestedIPAddressSerializer(required=False, allow_null=True)
nat_outside = NestedIPAddressSerializer(many=True, read_only=True)
class Meta:
model = IPAddress
fields = [
'id', 'url', 'display', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'assigned_object_type',
'assigned_object_id', 'assigned_object', 'nat_inside', 'nat_outside', 'dns_name', 'description', 'comments',
'tags', 'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'family', 'address', 'description')
@extend_schema_field(serializers.JSONField(allow_null=True))
def get_assigned_object(self, obj):
if obj.assigned_object is None:
return None
serializer = get_serializer_for_model(obj.assigned_object, prefix=NESTED_SERIALIZER_PREFIX)
context = {'request': self.context['request']}
return serializer(obj.assigned_object, context=context).data
class AvailableIPSerializer(serializers.Serializer):
"""
Representation of an IP address which does not exist in the database.
"""
family = serializers.IntegerField(read_only=True)
address = serializers.CharField(read_only=True)
vrf = NestedVRFSerializer(read_only=True)
description = serializers.CharField(required=False)
def to_representation(self, instance):
if self.context.get('vrf'):
vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data
else:
vrf = None
return {
'family': self.context['parent'].family,
'address': f"{instance}/{self.context['parent'].mask_length}",
'vrf': vrf,
}
#
# Services
#
class ServiceTemplateSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:servicetemplate-detail')
protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
class Meta:
model = ServiceTemplate
fields = [
'id', 'url', 'display', 'name', 'protocol', 'ports', 'description', 'comments', 'tags', 'custom_fields',
'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description')
class ServiceSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail')
device = NestedDeviceSerializer(required=False, allow_null=True)
virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
ipaddresses = SerializedPKRelatedField(
queryset=IPAddress.objects.all(),
serializer=NestedIPAddressSerializer,
required=False,
many=True
)
class Meta:
model = Service
fields = [
'id', 'url', 'display', 'device', 'virtual_machine', 'name', 'protocol', 'ports', 'ipaddresses',
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description')

Some files were not shown because too many files have changed in this diff Show More