mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-16 04:28:17 -06:00
Merge branch 'feature' into 14799-script-data-source
This commit is contained in:
commit
daf69d4ee8
@ -476,7 +476,7 @@ class NewBranchScript(Script):
|
||||
name=f'{site.slug}-switch{i}',
|
||||
site=site,
|
||||
status=DeviceStatusChoices.STATUS_PLANNED,
|
||||
device_role=switch_role
|
||||
role=switch_role
|
||||
)
|
||||
switch.full_clean()
|
||||
switch.save()
|
||||
|
@ -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.
|
||||
|
||||
### Device Role
|
||||
### Role
|
||||
|
||||
The functional [role](./devicerole.md) assigned to this device.
|
||||
The functional [device role](./devicerole.md) assigned to this device.
|
||||
|
||||
### Device Type
|
||||
|
||||
|
@ -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` |
|
||||
| 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.
|
||||
|
||||
|
@ -13,6 +13,10 @@
|
||||
|
||||
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
|
||||
|
||||
* [#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
|
||||
* [#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
|
||||
* [#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
|
||||
|
||||
* [#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
|
||||
* [#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
|
||||
* [#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
|
||||
* [#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()`)
|
||||
* [#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
|
||||
* [#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`
|
||||
|
@ -1,145 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
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 .serializers_.providers import *
|
||||
from .serializers_.circuits 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')
|
||||
|
0
netbox/circuits/api/serializers_/__init__.py
Normal file
0
netbox/circuits/api/serializers_/__init__.py
Normal file
81
netbox/circuits/api/serializers_/circuits.py
Normal file
81
netbox/circuits/api/serializers_/circuits.py
Normal 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')
|
68
netbox/circuits/api/serializers_/providers.py
Normal file
68
netbox/circuits/api/serializers_/providers.py
Normal 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')
|
@ -67,7 +67,7 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -95,7 +95,7 @@ class ProviderAccountFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = ['id', 'name', 'account', 'description']
|
||||
fields = ('id', 'name', 'account', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -122,7 +122,7 @@ class ProviderNetworkFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ProviderNetwork
|
||||
fields = ['id', 'name', 'service_id', 'description']
|
||||
fields = ('id', 'name', 'service_id', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -139,7 +139,7 @@ class CircuitTypeFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CircuitType
|
||||
fields = ['id', 'name', 'slug', 'color', 'description']
|
||||
fields = ('id', 'name', 'slug', 'color', 'description')
|
||||
|
||||
|
||||
class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
@ -158,6 +158,12 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
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(
|
||||
field_name='terminations__provider_network',
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
@ -214,10 +220,18 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
to_field_name='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:
|
||||
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):
|
||||
if not value.strip():
|
||||
@ -258,7 +272,10 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
if not value.strip():
|
||||
|
@ -330,6 +330,7 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
filterset = CircuitTerminationFilterSet
|
||||
ignore_fields = ('cable',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
@ -4,7 +4,7 @@ from core.choices import JobStatusChoices
|
||||
from core.models import *
|
||||
from netbox.api.fields import ChoiceField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
from users.api.nested_serializers import NestedUserSerializer
|
||||
from users.api.serializers import UserSerializer
|
||||
|
||||
__all__ = (
|
||||
'NestedDataFileSerializer',
|
||||
@ -32,7 +32,8 @@ class NestedDataFileSerializer(WritableNestedSerializer):
|
||||
class NestedJobSerializer(serializers.ModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail')
|
||||
status = ChoiceField(choices=JobStatusChoices)
|
||||
user = NestedUserSerializer(
|
||||
user = UserSerializer(
|
||||
nested=True,
|
||||
read_only=True
|
||||
)
|
||||
|
||||
|
@ -1,74 +1,3 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
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 .serializers_.data import *
|
||||
from .serializers_.jobs 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')
|
||||
|
0
netbox/core/api/serializers_/__init__.py
Normal file
0
netbox/core/api/serializers_/__init__.py
Normal file
53
netbox/core/api/serializers_/data.py
Normal file
53
netbox/core/api/serializers_/data.py
Normal 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')
|
31
netbox/core/api/serializers_/jobs.py
Normal file
31
netbox/core/api/serializers_/jobs.py
Normal 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')
|
@ -28,7 +28,7 @@ class DataSourceFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DataSource
|
||||
fields = ('id', 'name', 'enabled', 'description')
|
||||
fields = ('id', 'name', 'enabled', 'description', 'source_url', 'last_synced')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -115,7 +115,7 @@ class JobFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
if not value.strip():
|
||||
@ -134,9 +134,7 @@ class ConfigRevisionFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConfigRevision
|
||||
fields = [
|
||||
'id',
|
||||
]
|
||||
fields = ('id', 'created', 'comment')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
@ -68,7 +68,7 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
|
||||
)
|
||||
object_type = ContentTypeChoiceField(
|
||||
label=_('Object Type'),
|
||||
queryset=ContentType.objects.with_feature('jobs'),
|
||||
queryset=ObjectType.objects.with_feature('jobs'),
|
||||
required=False,
|
||||
)
|
||||
status = forms.MultipleChoiceField(
|
||||
|
@ -8,7 +8,7 @@ from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
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')
|
||||
|
||||
@ -60,7 +60,7 @@ class Command(BaseCommand):
|
||||
pass
|
||||
|
||||
# Additional objects to include
|
||||
namespace['ContentType'] = ContentType
|
||||
namespace['ObjectType'] = ObjectType
|
||||
namespace['User'] = get_user_model()
|
||||
|
||||
# Load convenience commands
|
||||
|
@ -1,5 +1,3 @@
|
||||
# Generated by Django 4.2.6 on 2023-10-31 19:38
|
||||
|
||||
import core.models.contenttypes
|
||||
from django.db import migrations
|
||||
|
||||
@ -13,7 +11,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ContentType',
|
||||
name='ObjectType',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
@ -23,7 +21,7 @@ class Migration(migrations.Migration):
|
||||
},
|
||||
bases=('contenttypes.contenttype',),
|
||||
managers=[
|
||||
('objects', core.models.contenttypes.ContentTypeManager()),
|
||||
('objects', core.models.contenttypes.ObjectTypeManager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
@ -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 netbox.registry import registry
|
||||
|
||||
__all__ = (
|
||||
'ContentType',
|
||||
'ContentTypeManager',
|
||||
'ObjectType',
|
||||
'ObjectTypeManager',
|
||||
)
|
||||
|
||||
|
||||
class ContentTypeManager(ContentTypeManager_):
|
||||
class ObjectTypeManager(ContentTypeManager):
|
||||
|
||||
def public(self):
|
||||
"""
|
||||
@ -40,11 +40,11 @@ class ContentTypeManager(ContentTypeManager_):
|
||||
return self.get_queryset().filter(q)
|
||||
|
||||
|
||||
class ContentType(ContentType_):
|
||||
class ObjectType(ContentType):
|
||||
"""
|
||||
Wrap Django's native ContentType model to use our custom manager.
|
||||
"""
|
||||
objects = ContentTypeManager()
|
||||
objects = ObjectTypeManager()
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
@ -11,7 +11,7 @@ from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from core.choices import JobStatusChoices
|
||||
from core.models import ContentType
|
||||
from core.models import ObjectType
|
||||
from core.signals import job_end, job_start
|
||||
from extras.constants import EVENT_JOB_END, EVENT_JOB_START
|
||||
from netbox.config import get_config
|
||||
@ -130,7 +130,7 @@ class Job(models.Model):
|
||||
super().clean()
|
||||
|
||||
# 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(
|
||||
_("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
|
||||
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)
|
||||
queue = django_rq.get_queue(rq_queue_name)
|
||||
status = JobStatusChoices.STATUS_SCHEDULED if schedule_at else JobStatusChoices.STATUS_PENDING
|
||||
|
@ -10,6 +10,7 @@ from ..models import *
|
||||
class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = DataSource.objects.all()
|
||||
filterset = DataSourceFilterSet
|
||||
ignore_fields = ('ignore_rules', 'parameters')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -70,6 +71,7 @@ class DataSourceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
class DataFileTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = DataFile.objects.all()
|
||||
filterset = DataFileFilterSet
|
||||
ignore_fields = ('data',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
@ -6,8 +6,6 @@ from netbox.api.fields import RelatedObjectCountField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
|
||||
__all__ = [
|
||||
'ComponentNestedModuleSerializer',
|
||||
'ModuleBayNestedModuleSerializer',
|
||||
'NestedCableSerializer',
|
||||
'NestedConsolePortSerializer',
|
||||
'NestedConsolePortTemplateSerializer',
|
||||
@ -311,26 +309,6 @@ class ModuleNestedModuleBaySerializer(WritableNestedSerializer):
|
||||
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):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
|
File diff suppressed because it is too large
Load Diff
0
netbox/dcim/api/serializers_/__init__.py
Normal file
0
netbox/dcim/api/serializers_/__init__.py
Normal file
37
netbox/dcim/api/serializers_/base.py
Normal file
37
netbox/dcim/api/serializers_/base.py
Normal 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
|
126
netbox/dcim/api/serializers_/cables.py
Normal file
126
netbox/dcim/api/serializers_/cables.py
Normal 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
|
368
netbox/dcim/api/serializers_/device_components.py
Normal file
368
netbox/dcim/api/serializers_/device_components.py
Normal 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
|
157
netbox/dcim/api/serializers_/devices.py
Normal file
157
netbox/dcim/api/serializers_/devices.py
Normal 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')
|
327
netbox/dcim/api/serializers_/devicetype_components.py
Normal file
327
netbox/dcim/api/serializers_/devicetype_components.py
Normal 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
|
74
netbox/dcim/api/serializers_/devicetypes.py
Normal file
74
netbox/dcim/api/serializers_/devicetypes.py
Normal 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')
|
26
netbox/dcim/api/serializers_/manufacturers.py
Normal file
26
netbox/dcim/api/serializers_/manufacturers.py
Normal 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')
|
29
netbox/dcim/api/serializers_/platforms.py
Normal file
29
netbox/dcim/api/serializers_/platforms.py
Normal 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')
|
80
netbox/dcim/api/serializers_/power.py
Normal file
80
netbox/dcim/api/serializers_/power.py
Normal 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')
|
117
netbox/dcim/api/serializers_/racks.py
Normal file
117
netbox/dcim/api/serializers_/racks.py
Normal 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
|
||||
)
|
31
netbox/dcim/api/serializers_/rackunits.py
Normal file
31
netbox/dcim/api/serializers_/rackunits.py
Normal 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']
|
43
netbox/dcim/api/serializers_/roles.py
Normal file
43
netbox/dcim/api/serializers_/roles.py
Normal 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')
|
98
netbox/dcim/api/serializers_/sites.py
Normal file
98
netbox/dcim/api/serializers_/sites.py
Normal 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')
|
26
netbox/dcim/api/serializers_/virtualchassis.py
Normal file
26
netbox/dcim/api/serializers_/virtualchassis.py
Normal 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')
|
@ -7,7 +7,6 @@ from rest_framework.response import Response
|
||||
from rest_framework.routers import APIRootView
|
||||
from rest_framework.viewsets import ViewSet
|
||||
|
||||
from circuits.models import Circuit
|
||||
from dcim import filtersets
|
||||
from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
|
||||
from dcim.models import *
|
||||
@ -18,10 +17,8 @@ from netbox.api.metadata import ContentTypeMetadata
|
||||
from netbox.api.pagination import StripCountAnnotationsPaginator
|
||||
from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin
|
||||
from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin
|
||||
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||
from utilities.api import get_serializer_for_model
|
||||
from utilities.query_functions import CollateAsChar
|
||||
from utilities.utils import count_related
|
||||
from . import serializers
|
||||
from .exceptions import MissingFilterException
|
||||
|
||||
@ -60,16 +57,16 @@ class PathEndpointMixin(object):
|
||||
# Serialize path objects, iterating over each three-tuple in the path
|
||||
for near_ends, cable, far_ends in obj.trace():
|
||||
if near_ends:
|
||||
serializer_a = get_serializer_for_model(near_ends[0], prefix=NESTED_SERIALIZER_PREFIX)
|
||||
near_ends = serializer_a(near_ends, many=True, context={'request': request}).data
|
||||
serializer_a = get_serializer_for_model(near_ends[0])
|
||||
near_ends = serializer_a(near_ends, nested=True, many=True, context={'request': request}).data
|
||||
else:
|
||||
# Path is split; stop here
|
||||
break
|
||||
if cable:
|
||||
cable = serializers.TracedCableSerializer(cable[0], context={'request': request}).data
|
||||
if far_ends:
|
||||
serializer_b = get_serializer_for_model(far_ends[0], prefix=NESTED_SERIALIZER_PREFIX)
|
||||
far_ends = serializer_b(far_ends, many=True, context={'request': request}).data
|
||||
serializer_b = get_serializer_for_model(far_ends[0])
|
||||
far_ends = serializer_b(far_ends, nested=True, many=True, context={'request': request}).data
|
||||
|
||||
path.append((near_ends, cable, far_ends))
|
||||
|
||||
@ -514,7 +511,10 @@ class CableTerminationViewSet(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
|
||||
filterset_class = filtersets.VirtualChassisFilterSet
|
||||
|
||||
|
@ -18,11 +18,12 @@ from tenancy.models import *
|
||||
from utilities.choices import ColorChoices
|
||||
from utilities.filters import (
|
||||
ContentTypeFilter, MultiValueCharFilter, MultiValueMACAddressFilter, MultiValueNumberFilter, MultiValueWWNFilter,
|
||||
TreeNodeMultipleChoiceFilter,
|
||||
NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
||||
)
|
||||
from virtualization.models import Cluster
|
||||
from vpn.models import L2VPN
|
||||
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
|
||||
from wireless.models import WirelessLAN, WirelessLink
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .models import *
|
||||
@ -89,10 +90,23 @@ class RegionFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
||||
to_field_name='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:
|
||||
model = Region
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
||||
@ -106,10 +120,23 @@ class SiteGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet):
|
||||
to_field_name='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:
|
||||
model = SiteGroup
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
@ -152,12 +179,11 @@ class SiteFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
|
||||
queryset=ASN.objects.all(),
|
||||
label=_('AS (ID)'),
|
||||
)
|
||||
time_zone = MultiValueCharFilter()
|
||||
|
||||
class Meta:
|
||||
model = Site
|
||||
fields = (
|
||||
'id', 'name', 'slug', 'facility', 'latitude', 'longitude', 'description'
|
||||
)
|
||||
fields = ('id', 'name', 'slug', 'facility', 'latitude', 'longitude', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -214,13 +240,23 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM
|
||||
to_field_name='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(),
|
||||
field_name='parent',
|
||||
lookup_expr='in',
|
||||
label=_('Location (ID)'),
|
||||
)
|
||||
parent = TreeNodeMultipleChoiceFilter(
|
||||
ancestor = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='parent',
|
||||
lookup_expr='in',
|
||||
@ -234,7 +270,7 @@ class LocationFilterSet(TenancyFilterSet, ContactModelFilterSet, OrganizationalM
|
||||
|
||||
class Meta:
|
||||
model = Location
|
||||
fields = ['id', 'name', 'slug', 'status', 'description']
|
||||
fields = ('id', 'name', 'slug', 'status', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -249,7 +285,7 @@ class RackRoleFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RackRole
|
||||
fields = ['id', 'name', 'slug', 'color', 'description']
|
||||
fields = ('id', 'name', 'slug', 'color', 'description')
|
||||
|
||||
|
||||
class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||
@ -328,10 +364,10 @@ class RackFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSe
|
||||
|
||||
class Meta:
|
||||
model = Rack
|
||||
fields = [
|
||||
fields = (
|
||||
'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',
|
||||
]
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -411,10 +447,14 @@ class RackReservationFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
to_field_name='username',
|
||||
label=_('User (name)'),
|
||||
)
|
||||
unit = NumericArrayFilter(
|
||||
field_name='units',
|
||||
lookup_expr='contains'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RackReservation
|
||||
fields = ['id', 'created', 'description']
|
||||
fields = ('id', 'created', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -431,7 +471,7 @@ class ManufacturerFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet)
|
||||
|
||||
class Meta:
|
||||
model = Manufacturer
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
|
||||
class DeviceTypeFilterSet(NetBoxModelFilterSet):
|
||||
@ -502,10 +542,22 @@ class DeviceTypeFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceType
|
||||
fields = [
|
||||
fields = (
|
||||
'id', 'model', 'slug', 'part_number', 'u_height', 'exclude_from_utilization', 'is_full_depth',
|
||||
'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):
|
||||
if not value.strip():
|
||||
@ -599,7 +651,7 @@ class ModuleTypeFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
if not value.strip():
|
||||
@ -639,12 +691,15 @@ class DeviceTypeComponentFilterSet(django_filters.FilterSet):
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
devicetype_id = django_filters.ModelMultipleChoiceFilter(
|
||||
device_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DeviceType.objects.all(),
|
||||
field_name='device_type_id',
|
||||
label=_('Device type (ID)'),
|
||||
)
|
||||
|
||||
# TODO: Remove in v4.1
|
||||
devicetype_id = device_type_id
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
@ -655,32 +710,35 @@ class DeviceTypeComponentFilterSet(django_filters.FilterSet):
|
||||
|
||||
|
||||
class ModularDeviceTypeComponentFilterSet(DeviceTypeComponentFilterSet):
|
||||
moduletype_id = django_filters.ModelMultipleChoiceFilter(
|
||||
module_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ModuleType.objects.all(),
|
||||
field_name='module_type_id',
|
||||
label=_('Module type (ID)'),
|
||||
)
|
||||
|
||||
# TODO: Remove in v4.1
|
||||
moduletype_id = module_type_id
|
||||
|
||||
|
||||
class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePortTemplate
|
||||
fields = ['id', 'name', 'type', 'description']
|
||||
fields = ('id', 'name', 'label', 'type', 'description')
|
||||
|
||||
|
||||
class ConsoleServerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPortTemplate
|
||||
fields = ['id', 'name', 'type', 'description']
|
||||
fields = ('id', 'name', 'label', 'type', 'description')
|
||||
|
||||
|
||||
class PowerPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
@ -688,10 +746,14 @@ class PowerOutletTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceType
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
null_value=None
|
||||
)
|
||||
power_port_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=PowerPortTemplate.objects.all(),
|
||||
label=_('Power port (ID)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PowerOutletTemplate
|
||||
fields = ['id', 'name', 'type', 'feed_leg', 'description']
|
||||
fields = ('id', 'name', 'label', 'type', 'feed_leg', 'description')
|
||||
|
||||
|
||||
class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
@ -715,7 +777,7 @@ class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
|
||||
|
||||
class Meta:
|
||||
model = InterfaceTemplate
|
||||
fields = ['id', 'name', 'type', 'enabled', 'mgmt_only', 'description']
|
||||
fields = ('id', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description')
|
||||
|
||||
|
||||
class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
@ -723,10 +785,13 @@ class FrontPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
|
||||
choices=PortTypeChoices,
|
||||
null_value=None
|
||||
)
|
||||
rear_port_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=RearPort.objects.all()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = FrontPortTemplate
|
||||
fields = ['id', 'name', 'type', 'color', 'description']
|
||||
fields = ('id', 'name', 'label', 'type', 'color', 'rear_port_position', 'description')
|
||||
|
||||
|
||||
class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet):
|
||||
@ -737,21 +802,21 @@ class RearPortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCom
|
||||
|
||||
class Meta:
|
||||
model = RearPortTemplate
|
||||
fields = ['id', 'name', 'type', 'color', 'positions', 'description']
|
||||
fields = ('id', 'name', 'label', 'type', 'color', 'positions', 'description')
|
||||
|
||||
|
||||
class ModuleBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ModuleBayTemplate
|
||||
fields = ['id', 'name', 'description']
|
||||
fields = ('id', 'name', 'label', 'position', 'description')
|
||||
|
||||
|
||||
class DeviceBayTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceBayTemplate
|
||||
fields = ['id', 'name', 'description']
|
||||
fields = ('id', 'name', 'label', 'description')
|
||||
|
||||
|
||||
class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeComponentFilterSet):
|
||||
@ -784,7 +849,7 @@ class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeCompo
|
||||
|
||||
class Meta:
|
||||
model = InventoryItemTemplate
|
||||
fields = ['id', 'name', 'label', 'part_id', 'description']
|
||||
fields = ('id', 'name', 'label', 'part_id', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -805,7 +870,7 @@ class DeviceRoleFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = DeviceRole
|
||||
fields = ['id', 'name', 'slug', 'color', 'vm_role', 'description']
|
||||
fields = ('id', 'name', 'slug', 'color', 'vm_role', 'description')
|
||||
|
||||
|
||||
class PlatformFilterSet(OrganizationalModelFilterSet):
|
||||
@ -831,7 +896,7 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Platform
|
||||
fields = ['id', 'name', 'slug', 'description']
|
||||
fields = ('id', 'name', 'slug', 'description')
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_for_device_type(self, queryset, name, value):
|
||||
@ -943,6 +1008,11 @@ class DeviceFilterSet(
|
||||
queryset=Rack.objects.all(),
|
||||
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(
|
||||
queryset=Cluster.objects.all(),
|
||||
label=_('VM cluster (ID)'),
|
||||
@ -1032,10 +1102,22 @@ class DeviceFilterSet(
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = [
|
||||
fields = (
|
||||
'id', 'asset_tag', 'face', 'position', 'latitude', 'longitude', 'airflow', 'vc_position', 'vc_priority',
|
||||
'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):
|
||||
if not value.strip():
|
||||
@ -1098,24 +1180,29 @@ class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet, Prim
|
||||
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device',
|
||||
queryset=Device.objects.all(),
|
||||
label='VDC (ID)',
|
||||
label=_('VDC (ID)')
|
||||
)
|
||||
device = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='device',
|
||||
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(
|
||||
choices=VirtualDeviceContextStatusChoices
|
||||
)
|
||||
has_primary_ip = django_filters.BooleanFilter(
|
||||
method='_has_primary_ip',
|
||||
label='Has a primary IP',
|
||||
label=_('Has a primary IP')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VirtualDeviceContext
|
||||
fields = ['id', 'device', 'name', 'description']
|
||||
fields = ('id', 'device', 'name', 'identifier', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -1181,7 +1268,7 @@ class ModuleFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Module
|
||||
fields = ['id', 'status', 'asset_tag', 'description']
|
||||
fields = ('id', 'status', 'asset_tag', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -1325,6 +1412,10 @@ class ModularDeviceComponentFilterSet(DeviceComponentFilterSet):
|
||||
|
||||
|
||||
class CabledObjectFilterSet(django_filters.FilterSet):
|
||||
cable_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Cable.objects.all(),
|
||||
label=_('Cable (ID)'),
|
||||
)
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
@ -1366,7 +1457,7 @@ class ConsolePortFilterSet(
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['id', 'name', 'label', 'description', 'cable_end']
|
||||
fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end')
|
||||
|
||||
|
||||
class ConsoleServerPortFilterSet(
|
||||
@ -1382,7 +1473,7 @@ class ConsoleServerPortFilterSet(
|
||||
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = ['id', 'name', 'label', 'description', 'cable_end']
|
||||
fields = ('id', 'name', 'label', 'speed', 'description', 'mark_connected', 'cable_end')
|
||||
|
||||
|
||||
class PowerPortFilterSet(
|
||||
@ -1398,7 +1489,9 @@ class PowerPortFilterSet(
|
||||
|
||||
class Meta:
|
||||
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(
|
||||
@ -1415,10 +1508,16 @@ class PowerOutletFilterSet(
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
null_value=None
|
||||
)
|
||||
power_port_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=PowerPort.objects.all(),
|
||||
label=_('Power port (ID)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
@ -1533,27 +1632,37 @@ class InterfaceFilterSet(
|
||||
vdc_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vdcs',
|
||||
queryset=VirtualDeviceContext.objects.all(),
|
||||
label='Virtual Device Context',
|
||||
label=_('Virtual Device Context')
|
||||
)
|
||||
vdc_identifier = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vdcs__identifier',
|
||||
queryset=VirtualDeviceContext.objects.all(),
|
||||
to_field_name='identifier',
|
||||
label='Virtual Device Context (Identifier)',
|
||||
label=_('Virtual Device Context (Identifier)')
|
||||
)
|
||||
vdc = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='vdcs__name',
|
||||
queryset=VirtualDeviceContext.objects.all(),
|
||||
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:
|
||||
model = Interface
|
||||
fields = [
|
||||
fields = (
|
||||
'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):
|
||||
try:
|
||||
@ -1582,10 +1691,15 @@ class FrontPortFilterSet(
|
||||
choices=PortTypeChoices,
|
||||
null_value=None
|
||||
)
|
||||
rear_port_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=RearPort.objects.all()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
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(
|
||||
@ -1600,21 +1714,38 @@ class RearPortFilterSet(
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
installed_module_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='installed_module',
|
||||
queryset=ModuleBay.objects.all(),
|
||||
label=_('Installed module (ID)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ModuleBay
|
||||
fields = ['id', 'name', 'label', 'description']
|
||||
fields = ('id', 'name', 'label', 'position', 'description')
|
||||
|
||||
|
||||
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:
|
||||
model = DeviceBay
|
||||
fields = ['id', 'name', 'label', 'description']
|
||||
fields = ('id', 'name', 'label', 'description')
|
||||
|
||||
|
||||
class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
|
||||
@ -1650,7 +1781,7 @@ class InventoryItemFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
if not value.strip():
|
||||
@ -1669,7 +1800,7 @@ class InventoryItemRoleFilterSet(OrganizationalModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = InventoryItemRole
|
||||
fields = ['id', 'name', 'slug', 'color', 'description']
|
||||
fields = ('id', 'name', 'slug', 'color', 'description')
|
||||
|
||||
|
||||
class VirtualChassisFilterSet(NetBoxModelFilterSet):
|
||||
@ -1734,7 +1865,7 @@ class VirtualChassisFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = VirtualChassis
|
||||
fields = ['id', 'domain', 'name', 'description']
|
||||
fields = ('id', 'domain', 'name', 'description', 'member_count')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -1839,7 +1970,7 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
fields = ['id', 'label', 'length', 'length_unit', 'description']
|
||||
fields = ('id', 'label', 'length', 'length_unit', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -1917,12 +2048,12 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||
return self.filter_by_termination_object(queryset, CircuitTermination, value)
|
||||
|
||||
|
||||
class CableTerminationFilterSet(BaseFilterSet):
|
||||
class CableTerminationFilterSet(ChangeLoggedModelFilterSet):
|
||||
termination_type = ContentTypeFilter()
|
||||
|
||||
class Meta:
|
||||
model = CableTermination
|
||||
fields = ['id', 'cable', 'cable_end', 'termination_type', 'termination_id']
|
||||
fields = ('id', 'cable', 'cable_end', 'termination_type', 'termination_id')
|
||||
|
||||
|
||||
class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
@ -1971,7 +2102,7 @@ class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerPanel
|
||||
fields = ['id', 'name', 'description']
|
||||
fields = ('id', 'name', 'description')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -2037,10 +2168,10 @@ class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpoi
|
||||
|
||||
class Meta:
|
||||
model = PowerFeed
|
||||
fields = [
|
||||
'id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'cable_end',
|
||||
'description',
|
||||
]
|
||||
fields = (
|
||||
'id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization',
|
||||
'available_power', 'mark_connected', 'cable_end', 'description',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -2099,18 +2230,18 @@ class ConsoleConnectionFilterSet(ConnectionFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['name']
|
||||
fields = ('name',)
|
||||
|
||||
|
||||
class PowerConnectionFilterSet(ConnectionFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['name']
|
||||
fields = ('name',)
|
||||
|
||||
|
||||
class InterfaceConnectionFilterSet(ConnectionFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = []
|
||||
fields = tuple()
|
||||
|
@ -754,7 +754,7 @@ class DeviceFilterForm(
|
||||
)
|
||||
has_oob_ip = forms.NullBooleanField(
|
||||
required=False,
|
||||
label='Has an OOB IP',
|
||||
label=_('Has an OOB IP'),
|
||||
widget=forms.Select(
|
||||
choices=BOOLEAN_WITH_BLANK_CHOICES
|
||||
)
|
||||
|
@ -9,7 +9,7 @@ from django.dispatch import Signal
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import ContentType
|
||||
from core.models import ObjectType
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.fields import PathField
|
||||
@ -481,13 +481,13 @@ class CablePath(models.Model):
|
||||
def origin_type(self):
|
||||
if self.path:
|
||||
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
|
||||
def destination_type(self):
|
||||
if self.is_complete:
|
||||
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
|
||||
def path_objects(self):
|
||||
@ -594,7 +594,7 @@ class CablePath(models.Model):
|
||||
|
||||
# Step 6: Determine the far-end terminations
|
||||
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(
|
||||
termination_type=termination_type,
|
||||
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.
|
||||
prefetched = {}
|
||||
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)
|
||||
if hasattr(model_class, 'device'):
|
||||
queryset = queryset.prefetch_related('device')
|
||||
@ -774,7 +774,7 @@ class CablePath(models.Model):
|
||||
"""
|
||||
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 = []
|
||||
|
||||
for node in self._nodes:
|
||||
|
@ -815,20 +815,6 @@ class Device(
|
||||
def get_absolute_url(self):
|
||||
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):
|
||||
super().clean()
|
||||
|
||||
|
@ -2156,7 +2156,7 @@ class CablePathTestCase(TestCase):
|
||||
device = Device.objects.create(
|
||||
site=self.site,
|
||||
device_type=self.device.device_type,
|
||||
device_role=self.device.device_role,
|
||||
role=self.device.role,
|
||||
name='Test mid-span Device'
|
||||
)
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
|
@ -64,21 +64,32 @@ class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
regions = (
|
||||
parent_regions = (
|
||||
Region(name='Region 1', slug='region-1', description='foobar1'),
|
||||
Region(name='Region 2', slug='region-2', description='foobar2'),
|
||||
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:
|
||||
region.save()
|
||||
|
||||
child_regions = (
|
||||
Region(name='Region 1A', slug='region-1a', parent=regions[0]),
|
||||
Region(name='Region 1B', slug='region-1b', parent=regions[0]),
|
||||
Region(name='Region 2A', slug='region-2a', parent=regions[1]),
|
||||
Region(name='Region 2B', slug='region-2b', parent=regions[1]),
|
||||
Region(name='Region 3A', slug='region-3a', parent=regions[2]),
|
||||
Region(name='Region 3B', slug='region-3b', parent=regions[2]),
|
||||
Region(name='Region 1A1', slug='region-1a1', parent=regions[0]),
|
||||
Region(name='Region 1B1', slug='region-1b1', parent=regions[1]),
|
||||
Region(name='Region 2A1', slug='region-2a1', parent=regions[2]),
|
||||
Region(name='Region 2B1', slug='region-2b1', parent=regions[3]),
|
||||
Region(name='Region 3A1', slug='region-3a1', parent=regions[4]),
|
||||
Region(name='Region 3B1', slug='region-3b1', parent=regions[5]),
|
||||
)
|
||||
for region in child_regions:
|
||||
region.save()
|
||||
@ -100,12 +111,19 @@ class RegionTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_parent(self):
|
||||
parent_regions = Region.objects.filter(parent__isnull=True)[:2]
|
||||
params = {'parent_id': [parent_regions[0].pk, parent_regions[1].pk]}
|
||||
regions = Region.objects.filter(parent__isnull=True)[:2]
|
||||
params = {'parent_id': [regions[0].pk, regions[1].pk]}
|
||||
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)
|
||||
|
||||
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):
|
||||
queryset = SiteGroup.objects.all()
|
||||
@ -114,24 +132,35 @@ class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
sitegroups = (
|
||||
parent_groups = (
|
||||
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 3', slug='site-group-3', description='foobar3'),
|
||||
)
|
||||
for sitegroup in sitegroups:
|
||||
sitegroup.save()
|
||||
for site_group in parent_groups:
|
||||
site_group.save()
|
||||
|
||||
child_sitegroups = (
|
||||
SiteGroup(name='Site Group 1A', slug='site-group-1a', parent=sitegroups[0]),
|
||||
SiteGroup(name='Site Group 1B', slug='site-group-1b', parent=sitegroups[0]),
|
||||
SiteGroup(name='Site Group 2A', slug='site-group-2a', parent=sitegroups[1]),
|
||||
SiteGroup(name='Site Group 2B', slug='site-group-2b', parent=sitegroups[1]),
|
||||
SiteGroup(name='Site Group 3A', slug='site-group-3a', parent=sitegroups[2]),
|
||||
SiteGroup(name='Site Group 3B', slug='site-group-3b', parent=sitegroups[2]),
|
||||
groups = (
|
||||
SiteGroup(name='Site Group 1A', slug='site-group-1a', parent=parent_groups[0]),
|
||||
SiteGroup(name='Site Group 1B', slug='site-group-1b', parent=parent_groups[0]),
|
||||
SiteGroup(name='Site Group 2A', slug='site-group-2a', parent=parent_groups[1]),
|
||||
SiteGroup(name='Site Group 2B', slug='site-group-2b', parent=parent_groups[1]),
|
||||
SiteGroup(name='Site Group 3A', slug='site-group-3a', parent=parent_groups[2]),
|
||||
SiteGroup(name='Site Group 3B', slug='site-group-3b', parent=parent_groups[2]),
|
||||
)
|
||||
for sitegroup in child_sitegroups:
|
||||
sitegroup.save()
|
||||
for site_group in groups:
|
||||
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):
|
||||
params = {'q': 'foobar1'}
|
||||
@ -150,16 +179,24 @@ class SiteGroupTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_parent(self):
|
||||
parent_sitegroups = SiteGroup.objects.filter(parent__isnull=True)[:2]
|
||||
params = {'parent_id': [parent_sitegroups[0].pk, parent_sitegroups[1].pk]}
|
||||
site_groups = SiteGroup.objects.filter(parent__isnull=True)[:2]
|
||||
params = {'parent_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||
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)
|
||||
|
||||
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):
|
||||
queryset = Site.objects.all()
|
||||
filterset = SiteFilterSet
|
||||
ignore_fields = ('physical_address', 'shipping_address')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -314,21 +351,29 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
parent_locations = (
|
||||
Location(name='Parent Location 1', slug='parent-location-1', site=sites[0]),
|
||||
Location(name='Parent Location 2', slug='parent-location-2', site=sites[1]),
|
||||
Location(name='Parent Location 3', slug='parent-location-3', site=sites[2]),
|
||||
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||
Location(name='Location 2', slug='location-2', site=sites[1]),
|
||||
Location(name='Location 3', slug='location-3', site=sites[2]),
|
||||
)
|
||||
for location in parent_locations:
|
||||
location.save()
|
||||
|
||||
locations = (
|
||||
Location(name='Location 1', slug='location-1', 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 3', slug='location-3', site=sites[2], parent=parent_locations[2], status=LocationStatusChoices.STATUS_DECOMMISSIONING, description='foobar3'),
|
||||
Location(name='Location 1A', slug='location-1a', site=sites[0], parent=parent_locations[0], status=LocationStatusChoices.STATUS_PLANNED, description='foobar1'),
|
||||
Location(name='Location 2A', slug='location-2a', site=sites[1], parent=parent_locations[1], status=LocationStatusChoices.STATUS_STAGING, description='foobar2'),
|
||||
Location(name='Location 3A', slug='location-3a', site=sites[2], parent=parent_locations[2], status=LocationStatusChoices.STATUS_DECOMMISSIONING, description='foobar3'),
|
||||
)
|
||||
for location in locations:
|
||||
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):
|
||||
params = {'q': 'foobar1'}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
@ -352,31 +397,38 @@ class LocationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
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]}
|
||||
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):
|
||||
site_groups = SiteGroup.objects.all()[:2]
|
||||
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]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
|
||||
def test_site(self):
|
||||
sites = Site.objects.all()[:2]
|
||||
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]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
|
||||
def test_parent(self):
|
||||
parent_groups = Location.objects.filter(name__startswith='Parent')[:2]
|
||||
params = {'parent_id': [parent_groups[0].pk, parent_groups[1].pk]}
|
||||
locations = Location.objects.filter(parent__isnull=True)[:2]
|
||||
params = {'parent_id': [locations[0].pk, locations[1].pk]}
|
||||
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)
|
||||
|
||||
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):
|
||||
queryset = RackRole.objects.all()
|
||||
@ -416,6 +468,7 @@ class RackRoleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Rack.objects.all()
|
||||
filterset = RackFilterSet
|
||||
ignore_fields = ('units',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -675,6 +728,7 @@ class RackTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
class RackReservationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = RackReservation.objects.all()
|
||||
filterset = RackReservationFilterSet
|
||||
ignore_fields = ('units',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -838,6 +892,7 @@ class ManufacturerTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
class DeviceTypeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = DeviceType.objects.all()
|
||||
filterset = DeviceTypeFilterSet
|
||||
ignore_fields = ('front_image', 'rear_image')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -1829,6 +1884,7 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Device.objects.all()
|
||||
filterset = DeviceFilterSet
|
||||
ignore_fields = ('local_context_data', 'oob_ip', 'primary_ip4', 'primary_ip6', 'vc_master_for')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -2281,6 +2337,7 @@ class DeviceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
class ModuleTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Module.objects.all()
|
||||
filterset = ModuleFilterSet
|
||||
ignore_fields = ('local_context_data',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -3178,6 +3235,7 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
||||
class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilterSetTests):
|
||||
queryset = Interface.objects.all()
|
||||
filterset = InterfaceFilterSet
|
||||
ignore_fields = ('tagged_vlans', 'untagged_vlan', 'vdcs')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -5281,6 +5339,7 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = VirtualDeviceContext.objects.all()
|
||||
filterset = VirtualDeviceContextFilterSet
|
||||
ignore_fields = ('primary_ip4', 'primary_ip6')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -5350,15 +5409,22 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
VirtualDeviceContext.objects.bulk_create(vdcs)
|
||||
|
||||
interfaces = (
|
||||
Interface(device=devices[0], name='Interface 1', type='virtual'),
|
||||
Interface(device=devices[0], name='Interface 2', type='virtual'),
|
||||
Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.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)
|
||||
|
||||
interfaces[0].vdcs.set([vdcs[0]])
|
||||
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[1], address='10.1.1.2/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=None, address='2001:db8::3/64'),
|
||||
)
|
||||
IPAddress.objects.bulk_create(addresses)
|
||||
|
||||
vdcs[0].primary_ip4 = addresses[0]
|
||||
vdcs[0].primary_ip6 = addresses[3]
|
||||
IPAddress.objects.bulk_create(ip_addresses)
|
||||
vdcs[0].primary_ip4 = ip_addresses[0]
|
||||
vdcs[0].primary_ip6 = ip_addresses[3]
|
||||
vdcs[0].save()
|
||||
vdcs[1].primary_ip4 = addresses[1]
|
||||
vdcs[1].primary_ip6 = addresses[4]
|
||||
vdcs[1].primary_ip4 = ip_addresses[1]
|
||||
vdcs[1].primary_ip6 = ip_addresses[4]
|
||||
vdcs[1].save()
|
||||
|
||||
def test_q(self):
|
||||
@ -5380,8 +5445,11 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
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)
|
||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_status(self):
|
||||
params = {'status': ['active']}
|
||||
@ -5391,10 +5459,10 @@ class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_device_id(self):
|
||||
devices = Device.objects.filter(name__in=['Device 1', 'Device 2'])
|
||||
params = {'device_id': [devices[0].pk, devices[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
def test_interface(self):
|
||||
interfaces = Interface.objects.filter(name__in=['Interface 1', 'Interface 3'])
|
||||
params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_has_primary_ip(self):
|
||||
params = {'has_primary_ip': True}
|
||||
|
@ -1,8 +1,8 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase
|
||||
|
||||
from circuits.models import *
|
||||
from core.models import ObjectType
|
||||
from dcim.choices import *
|
||||
from dcim.models import *
|
||||
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
|
||||
cf1 = CustomField.objects.create(name='cf1', default='foo')
|
||||
cf1.content_types.set(
|
||||
ContentType.objects.filter(app_label='dcim', model__in=[
|
||||
cf1.object_types.set(
|
||||
ObjectType.objects.filter(app_label='dcim', model__in=[
|
||||
'consoleport',
|
||||
'consoleserverport',
|
||||
'powerport',
|
||||
@ -533,30 +533,6 @@ class DeviceTestCase(TestCase):
|
||||
device2.full_clean()
|
||||
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):
|
||||
|
||||
|
@ -3,7 +3,6 @@ from zoneinfo import ZoneInfo
|
||||
|
||||
import yaml
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from netaddr import EUI
|
||||
@ -2982,7 +2981,6 @@ class CableTestCase(
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
interface_ct = ContentType.objects.get_for_model(Interface)
|
||||
cls.form_data = {
|
||||
# TODO: Revisit this limitation
|
||||
# Changing terminations not supported when editing an existing Cable
|
||||
|
@ -1,13 +1,12 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext as _
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework.fields import Field
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from core.models import ObjectType
|
||||
from extras.choices import CustomFieldTypeChoices
|
||||
from extras.models import CustomField
|
||||
from netbox.constants import NESTED_SERIALIZER_PREFIX
|
||||
from utilities.api import get_serializer_for_model
|
||||
|
||||
|
||||
@ -25,8 +24,8 @@ class CustomFieldDefaultValues:
|
||||
self.model = serializer_field.parent.Meta.model
|
||||
|
||||
# Retrieve the CustomFields for the parent model
|
||||
content_type = ContentType.objects.get_for_model(self.model)
|
||||
fields = CustomField.objects.filter(content_types=content_type)
|
||||
object_type = ObjectType.objects.get_for_model(self.model)
|
||||
fields = CustomField.objects.filter(object_types=object_type)
|
||||
|
||||
# Populate the default value for each CustomField
|
||||
value = {}
|
||||
@ -47,8 +46,8 @@ class CustomFieldsDataField(Field):
|
||||
Cache CustomFields assigned to this model to avoid redundant database queries
|
||||
"""
|
||||
if not hasattr(self, '_custom_fields'):
|
||||
content_type = ContentType.objects.get_for_model(self.parent.Meta.model)
|
||||
self._custom_fields = CustomField.objects.filter(content_types=content_type)
|
||||
object_type = ObjectType.objects.get_for_model(self.parent.Meta.model)
|
||||
self._custom_fields = CustomField.objects.filter(object_types=object_type)
|
||||
return self._custom_fields
|
||||
|
||||
def to_representation(self, obj):
|
||||
@ -58,11 +57,11 @@ class CustomFieldsDataField(Field):
|
||||
for cf in self._get_custom_fields():
|
||||
value = cf.deserialize(obj.get(cf.name))
|
||||
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)
|
||||
value = serializer(value, context=self.parent.context).data
|
||||
serializer = get_serializer_for_model(cf.related_object_type.model_class())
|
||||
value = serializer(value, nested=True, context=self.parent.context).data
|
||||
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)
|
||||
value = serializer(value, many=True, context=self.parent.context).data
|
||||
serializer = get_serializer_for_model(cf.related_object_type.model_class())
|
||||
value = serializer(value, nested=True, many=True, context=self.parent.context).data
|
||||
data[cf.name] = value
|
||||
|
||||
return data
|
||||
@ -80,12 +79,9 @@ class CustomFieldsDataField(Field):
|
||||
CustomFieldTypeChoices.TYPE_OBJECT,
|
||||
CustomFieldTypeChoices.TYPE_MULTIOBJECT
|
||||
):
|
||||
serializer_class = get_serializer_for_model(
|
||||
model=cf.object_type.model_class(),
|
||||
prefix=NESTED_SERIALIZER_PREFIX
|
||||
)
|
||||
serializer_class = get_serializer_for_model(cf.related_object_type.model_class())
|
||||
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():
|
||||
data[cf.name] = [obj['id'] for obj in serializer.data] if many else serializer.data['id']
|
||||
else:
|
||||
|
@ -5,7 +5,7 @@ from rest_framework.response import Response
|
||||
from rest_framework.status import HTTP_400_BAD_REQUEST
|
||||
|
||||
from netbox.api.renderers import TextRenderer
|
||||
from .nested_serializers import NestedConfigTemplateSerializer
|
||||
from .serializers import ConfigTemplateSerializer
|
||||
|
||||
__all__ = (
|
||||
'ConfigContextQuerySetMixin',
|
||||
@ -52,7 +52,7 @@ class ConfigTemplateRenderMixin:
|
||||
if request.accepted_renderer.format == 'txt':
|
||||
return Response(output)
|
||||
|
||||
template_serializer = NestedConfigTemplateSerializer(configtemplate, context={'request': request})
|
||||
template_serializer = ConfigTemplateSerializer(configtemplate, nested=True, context={'request': request})
|
||||
|
||||
return Response({
|
||||
'configtemplate': template_serializer.data,
|
||||
|
@ -1,659 +1,16 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
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.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer
|
||||
from core.api.serializers import JobSerializer
|
||||
from core.models import ContentType
|
||||
from dcim.api.nested_serializers import (
|
||||
NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer,
|
||||
NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
|
||||
)
|
||||
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
||||
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 .serializers_.objecttypes import *
|
||||
from .serializers_.attachments import *
|
||||
from .serializers_.bookmarks import *
|
||||
from .serializers_.change_logging import *
|
||||
from .serializers_.customfields import *
|
||||
from .serializers_.customlinks import *
|
||||
from .serializers_.dashboard import *
|
||||
from .serializers_.events import *
|
||||
from .serializers_.exporttemplates import *
|
||||
from .serializers_.journaling import *
|
||||
from .serializers_.configcontexts import *
|
||||
from .serializers_.configtemplates import *
|
||||
from .serializers_.savedfilters import *
|
||||
from .serializers_.scripts import *
|
||||
from .serializers_.tags 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')
|
||||
|
0
netbox/extras/api/serializers_/__init__.py
Normal file
0
netbox/extras/api/serializers_/__init__.py
Normal file
50
netbox/extras/api/serializers_/attachments.py
Normal file
50
netbox/extras/api/serializers_/attachments.py
Normal 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
|
35
netbox/extras/api/serializers_/bookmarks.py
Normal file
35
netbox/extras/api/serializers_/bookmarks.py
Normal 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
|
55
netbox/extras/api/serializers_/change_logging.py
Normal file
55
netbox/extras/api/serializers_/change_logging.py
Normal 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
|
131
netbox/extras/api/serializers_/configcontexts.py
Normal file
131
netbox/extras/api/serializers_/configcontexts.py
Normal 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')
|
30
netbox/extras/api/serializers_/configtemplates.py
Normal file
30
netbox/extras/api/serializers_/configtemplates.py
Normal 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')
|
91
netbox/extras/api/serializers_/customfields.py
Normal file
91
netbox/extras/api/serializers_/customfields.py
Normal 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'
|
26
netbox/extras/api/serializers_/customlinks.py
Normal file
26
netbox/extras/api/serializers_/customlinks.py
Normal 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')
|
13
netbox/extras/api/serializers_/dashboard.py
Normal file
13
netbox/extras/api/serializers_/dashboard.py
Normal 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')
|
71
netbox/extras/api/serializers_/events.py
Normal file
71
netbox/extras/api/serializers_/events.py
Normal 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')
|
36
netbox/extras/api/serializers_/exporttemplates.py
Normal file
36
netbox/extras/api/serializers_/exporttemplates.py
Normal 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')
|
63
netbox/extras/api/serializers_/journaling.py
Normal file
63
netbox/extras/api/serializers_/journaling.py
Normal 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
|
16
netbox/extras/api/serializers_/objecttypes.py
Normal file
16
netbox/extras/api/serializers_/objecttypes.py
Normal 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']
|
26
netbox/extras/api/serializers_/savedfilters.py
Normal file
26
netbox/extras/api/serializers_/savedfilters.py
Normal 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')
|
77
netbox/extras/api/serializers_/scripts.py
Normal file
77
netbox/extras/api/serializers_/scripts.py
Normal 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
|
30
netbox/extras/api/serializers_/tags.py
Normal file
30
netbox/extras/api/serializers_/tags.py
Normal 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')
|
@ -22,7 +22,7 @@ router.register('config-contexts', views.ConfigContextViewSet)
|
||||
router.register('config-templates', views.ConfigTemplateViewSet)
|
||||
router.register('scripts', views.ScriptViewSet, basename='script')
|
||||
router.register('object-changes', views.ObjectChangeViewSet)
|
||||
router.register('content-types', views.ContentTypeViewSet)
|
||||
router.register('object-types', views.ObjectTypeViewSet)
|
||||
|
||||
app_name = 'extras-api'
|
||||
urlpatterns = [
|
||||
|
@ -1,4 +1,3 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django_rq.queues import get_connection
|
||||
from rest_framework import status
|
||||
@ -11,7 +10,7 @@ from rest_framework.routers import APIRootView
|
||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||
from rq import Worker
|
||||
|
||||
from core.models import Job
|
||||
from core.models import Job, ObjectType
|
||||
from extras import filtersets
|
||||
from extras.models import *
|
||||
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]
|
||||
queryset = ContentType.objects.order_by('app_label', 'model')
|
||||
serializer_class = serializers.ContentTypeSerializer
|
||||
filterset_class = filtersets.ContentTypeFilterSet
|
||||
queryset = ObjectType.objects.order_by('app_label', 'model')
|
||||
serializer_class = serializers.ObjectTypeSerializer
|
||||
filterset_class = filtersets.ObjectTypeFilterSet
|
||||
|
||||
|
||||
#
|
||||
|
@ -12,7 +12,7 @@ from django.template.loader import render_to_string
|
||||
from django.urls import NoReverseMatch, resolve, reverse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from core.models import ContentType
|
||||
from core.models import ObjectType
|
||||
from extras.choices import BookmarkOrderingChoices
|
||||
from utilities.choices import ButtonColorChoices
|
||||
from utilities.permissions import get_permission_for_model
|
||||
@ -34,14 +34,14 @@ __all__ = (
|
||||
def get_object_type_choices():
|
||||
return [
|
||||
(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():
|
||||
return [
|
||||
(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 = []
|
||||
for content_type_id in content_types:
|
||||
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())
|
||||
return models
|
||||
|
||||
@ -238,7 +238,7 @@ class ObjectListWidget(DashboardWidget):
|
||||
|
||||
def render(self, request):
|
||||
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')
|
||||
|
||||
# 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'])
|
||||
if object_types := self.config.get('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)
|
||||
if max_items := self.config.get('max_items'):
|
||||
bookmarks = bookmarks[:max_items]
|
||||
|
@ -155,7 +155,7 @@ def process_event_queue(events):
|
||||
if content_type not in events_cache[action_flag]:
|
||||
events_cache[action_flag][content_type] = EventRule.objects.filter(
|
||||
**{action_flag: True},
|
||||
content_types=content_type,
|
||||
object_types=content_type,
|
||||
enabled=True
|
||||
)
|
||||
event_rules = events_cache[action_flag][content_type]
|
||||
|
@ -4,7 +4,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
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 netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
@ -18,7 +18,6 @@ __all__ = (
|
||||
'BookmarkFilterSet',
|
||||
'ConfigContextFilterSet',
|
||||
'ConfigTemplateFilterSet',
|
||||
'ContentTypeFilterSet',
|
||||
'CustomFieldChoiceSetFilterSet',
|
||||
'CustomFieldFilterSet',
|
||||
'CustomLinkFilterSet',
|
||||
@ -28,6 +27,7 @@ __all__ = (
|
||||
'JournalEntryFilterSet',
|
||||
'LocalConfigContextFilterSet',
|
||||
'ObjectChangeFilterSet',
|
||||
'ObjectTypeFilterSet',
|
||||
'SavedFilterFilterSet',
|
||||
'ScriptFilterSet',
|
||||
'TagFilterSet',
|
||||
@ -40,12 +40,14 @@ class ScriptFilterSet(BaseFilterSet):
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
module_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ScriptModule.objects.all(),
|
||||
label=_('Script module (ID)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Script
|
||||
fields = [
|
||||
'id', 'name',
|
||||
]
|
||||
fields = ('id', 'name', 'is_executable')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -69,10 +71,10 @@ class WebhookFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Webhook
|
||||
fields = [
|
||||
fields = (
|
||||
'id', 'name', 'payload_url', 'http_method', 'http_content_type', 'secret', 'ssl_verification',
|
||||
'ca_file_path', 'description',
|
||||
]
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -89,10 +91,13 @@ class EventRuleFilterSet(NetBoxModelFilterSet):
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
content_type_id = MultiValueNumberFilter(
|
||||
field_name='content_types__id'
|
||||
object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ObjectType.objects.all(),
|
||||
field_name='object_types'
|
||||
)
|
||||
object_type = ContentTypeFilter(
|
||||
field_name='object_types'
|
||||
)
|
||||
content_types = ContentTypeFilter()
|
||||
action_type = django_filters.MultipleChoiceFilter(
|
||||
choices=EventRuleActionChoices
|
||||
)
|
||||
@ -101,10 +106,10 @@ class EventRuleFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = EventRule
|
||||
fields = [
|
||||
fields = (
|
||||
'id', 'name', 'type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end', 'enabled',
|
||||
'action_type', 'description',
|
||||
]
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -116,7 +121,7 @@ class EventRuleFilterSet(NetBoxModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class CustomFieldFilterSet(BaseFilterSet):
|
||||
class CustomFieldFilterSet(ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
@ -124,10 +129,18 @@ class CustomFieldFilterSet(BaseFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=CustomFieldTypeChoices
|
||||
)
|
||||
content_type_id = MultiValueNumberFilter(
|
||||
field_name='content_types__id'
|
||||
object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
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(
|
||||
queryset=CustomFieldChoiceSet.objects.all()
|
||||
)
|
||||
@ -139,10 +152,11 @@ class CustomFieldFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CustomField
|
||||
fields = [
|
||||
'id', 'content_types', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible',
|
||||
'ui_editable', 'weight', 'is_cloneable', 'description',
|
||||
]
|
||||
fields = (
|
||||
'id', 'name', 'label', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visible',
|
||||
'ui_editable', 'weight', 'is_cloneable', 'description', 'validation_minimum', 'validation_maximum',
|
||||
'validation_regex',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -155,7 +169,7 @@ class CustomFieldFilterSet(BaseFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class CustomFieldChoiceSetFilterSet(BaseFilterSet):
|
||||
class CustomFieldChoiceSetFilterSet(ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
@ -166,9 +180,9 @@ class CustomFieldChoiceSetFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = CustomFieldChoiceSet
|
||||
fields = [
|
||||
fields = (
|
||||
'id', 'name', 'description', 'base_choices', 'order_alphabetically',
|
||||
]
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -183,21 +197,24 @@ class CustomFieldChoiceSetFilterSet(BaseFilterSet):
|
||||
return queryset.filter(extra_choices__overlap=value)
|
||||
|
||||
|
||||
class CustomLinkFilterSet(BaseFilterSet):
|
||||
class CustomLinkFilterSet(ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
content_type_id = MultiValueNumberFilter(
|
||||
field_name='content_types__id'
|
||||
object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ObjectType.objects.all(),
|
||||
field_name='object_types'
|
||||
)
|
||||
object_type = ContentTypeFilter(
|
||||
field_name='object_types'
|
||||
)
|
||||
content_types = ContentTypeFilter()
|
||||
|
||||
class Meta:
|
||||
model = CustomLink
|
||||
fields = [
|
||||
'id', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'new_window',
|
||||
]
|
||||
fields = (
|
||||
'id', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', 'new_window', 'button_class',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -210,15 +227,18 @@ class CustomLinkFilterSet(BaseFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class ExportTemplateFilterSet(BaseFilterSet):
|
||||
class ExportTemplateFilterSet(ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
content_type_id = MultiValueNumberFilter(
|
||||
field_name='content_types__id'
|
||||
object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ObjectType.objects.all(),
|
||||
field_name='object_types'
|
||||
)
|
||||
object_type = ContentTypeFilter(
|
||||
field_name='object_types'
|
||||
)
|
||||
content_types = ContentTypeFilter()
|
||||
data_source_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=DataSource.objects.all(),
|
||||
label=_('Data source (ID)'),
|
||||
@ -230,7 +250,10 @@ class ExportTemplateFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
if not value.strip():
|
||||
@ -241,15 +264,18 @@ class ExportTemplateFilterSet(BaseFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class SavedFilterFilterSet(BaseFilterSet):
|
||||
class SavedFilterFilterSet(ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
content_type_id = MultiValueNumberFilter(
|
||||
field_name='content_types__id'
|
||||
object_type_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ObjectType.objects.all(),
|
||||
field_name='object_types'
|
||||
)
|
||||
object_type = ContentTypeFilter(
|
||||
field_name='object_types'
|
||||
)
|
||||
content_types = ContentTypeFilter()
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=get_user_model().objects.all(),
|
||||
label=_('User (ID)'),
|
||||
@ -266,7 +292,7 @@ class SavedFilterFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
if not value.strip():
|
||||
@ -307,20 +333,19 @@ class BookmarkFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Bookmark
|
||||
fields = ['id', 'object_id']
|
||||
fields = ('id', 'object_id')
|
||||
|
||||
|
||||
class ImageAttachmentFilterSet(BaseFilterSet):
|
||||
class ImageAttachmentFilterSet(ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
created = django_filters.DateTimeFilter()
|
||||
content_type = ContentTypeFilter()
|
||||
object_type = ContentTypeFilter()
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
if not value.strip():
|
||||
@ -350,7 +375,7 @@ class JournalEntryFilterSet(NetBoxModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
if not value.strip():
|
||||
@ -375,7 +400,7 @@ class TagFilterSet(ChangeLoggedModelFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = ['id', 'name', 'slug', 'color', 'description', 'object_types']
|
||||
fields = ('id', 'name', 'slug', 'color', 'description', 'object_types')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -472,12 +497,12 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
|
||||
queryset=DeviceType.objects.all(),
|
||||
label=_('Device type'),
|
||||
)
|
||||
role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
device_role_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='roles',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
label=_('Role'),
|
||||
)
|
||||
role = django_filters.ModelMultipleChoiceFilter(
|
||||
device_role = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='roles__slug',
|
||||
queryset=DeviceRole.objects.all(),
|
||||
to_field_name='slug',
|
||||
@ -563,9 +588,13 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
|
||||
label=_('Data file (ID)'),
|
||||
)
|
||||
|
||||
# TODO: Remove in v4.1
|
||||
role = device_role
|
||||
role_id = device_role_id
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
if not value.strip():
|
||||
@ -577,7 +606,7 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
|
||||
)
|
||||
|
||||
|
||||
class ConfigTemplateFilterSet(BaseFilterSet):
|
||||
class ConfigTemplateFilterSet(ChangeLoggedModelFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
@ -594,7 +623,7 @@ class ConfigTemplateFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConfigTemplate
|
||||
fields = ['id', 'name', 'description', 'data_synced']
|
||||
fields = ('id', 'name', 'description', 'auto_sync_enabled', 'data_synced')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
@ -642,10 +671,10 @@ class ObjectChangeFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ObjectChange
|
||||
fields = [
|
||||
fields = (
|
||||
'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):
|
||||
if not value.strip():
|
||||
@ -660,15 +689,15 @@ class ObjectChangeFilterSet(BaseFilterSet):
|
||||
# ContentTypes
|
||||
#
|
||||
|
||||
class ContentTypeFilterSet(django_filters.FilterSet):
|
||||
class ObjectTypeFilterSet(django_filters.FilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label=_('Search'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ContentType
|
||||
fields = ['id', 'app_label', 'model']
|
||||
model = ObjectType
|
||||
fields = ('id', 'app_label', 'model')
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
@ -6,7 +6,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import ContentType
|
||||
from core.models import ObjectType
|
||||
from extras.choices import *
|
||||
from extras.models import *
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
@ -30,9 +30,9 @@ __all__ = (
|
||||
|
||||
|
||||
class CustomFieldImportForm(CSVModelForm):
|
||||
content_types = CSVMultipleContentTypeField(
|
||||
label=_('Content types'),
|
||||
queryset=ContentType.objects.with_feature('custom_fields'),
|
||||
object_types = CSVMultipleContentTypeField(
|
||||
label=_('Object types'),
|
||||
queryset=ObjectType.objects.with_feature('custom_fields'),
|
||||
help_text=_("One or more assigned object types")
|
||||
)
|
||||
type = CSVChoiceField(
|
||||
@ -40,9 +40,9 @@ class CustomFieldImportForm(CSVModelForm):
|
||||
choices=CustomFieldTypeChoices,
|
||||
help_text=_('Field data type (e.g. text, integer, etc.)')
|
||||
)
|
||||
object_type = CSVContentTypeField(
|
||||
related_object_type = CSVContentTypeField(
|
||||
label=_('Object type'),
|
||||
queryset=ContentType.objects.public(),
|
||||
queryset=ObjectType.objects.public(),
|
||||
required=False,
|
||||
help_text=_("Object type (for object or multi-object fields)")
|
||||
)
|
||||
@ -69,7 +69,7 @@ class CustomFieldImportForm(CSVModelForm):
|
||||
class Meta:
|
||||
model = CustomField
|
||||
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',
|
||||
'validation_maximum', 'validation_regex', 'ui_visible', 'ui_editable', 'is_cloneable',
|
||||
)
|
||||
@ -111,31 +111,31 @@ class CustomFieldChoiceSetImportForm(CSVModelForm):
|
||||
|
||||
|
||||
class CustomLinkImportForm(CSVModelForm):
|
||||
content_types = CSVMultipleContentTypeField(
|
||||
label=_('Content types'),
|
||||
queryset=ContentType.objects.with_feature('custom_links'),
|
||||
object_types = CSVMultipleContentTypeField(
|
||||
label=_('Object types'),
|
||||
queryset=ObjectType.objects.with_feature('custom_links'),
|
||||
help_text=_("One or more assigned object types")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = CustomLink
|
||||
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',
|
||||
)
|
||||
|
||||
|
||||
class ExportTemplateImportForm(CSVModelForm):
|
||||
content_types = CSVMultipleContentTypeField(
|
||||
label=_('Content types'),
|
||||
queryset=ContentType.objects.with_feature('export_templates'),
|
||||
object_types = CSVMultipleContentTypeField(
|
||||
label=_('Object types'),
|
||||
queryset=ObjectType.objects.with_feature('export_templates'),
|
||||
help_text=_("One or more assigned object types")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ExportTemplate
|
||||
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):
|
||||
content_types = CSVMultipleContentTypeField(
|
||||
label=_('Content types'),
|
||||
queryset=ContentType.objects.all(),
|
||||
object_types = CSVMultipleContentTypeField(
|
||||
label=_('Object types'),
|
||||
queryset=ObjectType.objects.all(),
|
||||
help_text=_("One or more assigned object types")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SavedFilter
|
||||
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):
|
||||
content_types = CSVMultipleContentTypeField(
|
||||
label=_('Content types'),
|
||||
queryset=ContentType.objects.with_feature('event_rules'),
|
||||
object_types = CSVMultipleContentTypeField(
|
||||
label=_('Object types'),
|
||||
queryset=ObjectType.objects.with_feature('event_rules'),
|
||||
help_text=_("One or more assigned object types")
|
||||
)
|
||||
action_object = forms.CharField(
|
||||
@ -187,7 +187,7 @@ class EventRuleImportForm(NetBoxModelImportForm):
|
||||
class Meta:
|
||||
model = EventRule
|
||||
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'
|
||||
)
|
||||
|
||||
@ -213,7 +213,7 @@ class EventRuleImportForm(NetBoxModelImportForm):
|
||||
except ObjectDoesNotExist:
|
||||
raise forms.ValidationError(_("Script {name} not found").format(name=action_object))
|
||||
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):
|
||||
@ -229,7 +229,7 @@ class TagImportForm(CSVModelForm):
|
||||
|
||||
class JournalEntryImportForm(NetBoxModelImportForm):
|
||||
assigned_object_type = CSVContentTypeField(
|
||||
queryset=ContentType.objects.all(),
|
||||
queryset=ObjectType.objects.all(),
|
||||
label=_('Assigned object type'),
|
||||
)
|
||||
kind = CSVChoiceField(
|
||||
|
@ -2,7 +2,7 @@ from django import forms
|
||||
from django.contrib.auth import get_user_model
|
||||
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 extras.choices import *
|
||||
from extras.models import *
|
||||
@ -38,14 +38,14 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id')),
|
||||
(_('Attributes'), (
|
||||
'type', 'content_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible', 'ui_editable',
|
||||
'is_cloneable',
|
||||
'type', 'related_object_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visible',
|
||||
'ui_editable', 'is_cloneable',
|
||||
)),
|
||||
)
|
||||
content_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.with_feature('custom_fields'),
|
||||
related_object_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ObjectType.objects.with_feature('custom_fields'),
|
||||
required=False,
|
||||
label=_('Object type')
|
||||
label=_('Related object type')
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
choices=CustomFieldTypeChoices,
|
||||
@ -108,11 +108,11 @@ class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
|
||||
class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id')),
|
||||
(_('Attributes'), ('content_types', 'enabled', 'new_window', 'weight')),
|
||||
(_('Attributes'), ('object_type', 'enabled', 'new_window', 'weight')),
|
||||
)
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Content types'),
|
||||
queryset=ContentType.objects.with_feature('custom_links'),
|
||||
object_type = ContentTypeMultipleChoiceField(
|
||||
label=_('Object types'),
|
||||
queryset=ObjectType.objects.with_feature('custom_links'),
|
||||
required=False
|
||||
)
|
||||
enabled = forms.NullBooleanField(
|
||||
@ -139,7 +139,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_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(
|
||||
queryset=DataSource.objects.all(),
|
||||
@ -154,8 +154,8 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
||||
'source_id': '$data_source_id'
|
||||
}
|
||||
)
|
||||
content_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.with_feature('export_templates'),
|
||||
object_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ObjectType.objects.with_feature('export_templates'),
|
||||
required=False,
|
||||
label=_('Content types')
|
||||
)
|
||||
@ -179,11 +179,11 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
|
||||
class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id')),
|
||||
(_('Attributes'), ('content_type_id', 'name',)),
|
||||
(_('Attributes'), ('object_type_id', 'name',)),
|
||||
)
|
||||
content_type_id = ContentTypeChoiceField(
|
||||
label=_('Content type'),
|
||||
queryset=ContentType.objects.with_feature('image_attachments'),
|
||||
object_type_id = ContentTypeChoiceField(
|
||||
label=_('Object type'),
|
||||
queryset=ObjectType.objects.with_feature('image_attachments'),
|
||||
required=False
|
||||
)
|
||||
name = forms.CharField(
|
||||
@ -195,11 +195,11 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
|
||||
class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id')),
|
||||
(_('Attributes'), ('content_types', 'enabled', 'shared', 'weight')),
|
||||
(_('Attributes'), ('object_type', 'enabled', 'shared', 'weight')),
|
||||
)
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Content types'),
|
||||
queryset=ContentType.objects.public(),
|
||||
object_type = ContentTypeMultipleChoiceField(
|
||||
label=_('Object types'),
|
||||
queryset=ObjectType.objects.public(),
|
||||
required=False
|
||||
)
|
||||
enabled = forms.NullBooleanField(
|
||||
@ -250,11 +250,11 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm):
|
||||
|
||||
fieldsets = (
|
||||
(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')),
|
||||
)
|
||||
content_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.with_feature('event_rules'),
|
||||
object_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ObjectType.objects.with_feature('event_rules'),
|
||||
required=False,
|
||||
label=_('Object type')
|
||||
)
|
||||
@ -310,12 +310,12 @@ class EventRuleFilterForm(NetBoxModelFilterSetForm):
|
||||
class TagFilterForm(SavedFiltersMixin, FilterForm):
|
||||
model = Tag
|
||||
content_type_id = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.with_feature('tags'),
|
||||
queryset=ObjectType.objects.with_feature('tags'),
|
||||
required=False,
|
||||
label=_('Tagged object type')
|
||||
)
|
||||
for_object_type_id = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.with_feature('tags'),
|
||||
queryset=ObjectType.objects.with_feature('tags'),
|
||||
required=False,
|
||||
label=_('Allowed object type')
|
||||
)
|
||||
@ -464,7 +464,7 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
|
||||
label=_('User')
|
||||
)
|
||||
assigned_object_type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
queryset=ObjectType.objects.all(),
|
||||
required=False,
|
||||
label=_('Object Type'),
|
||||
widget=APISelectMultiple(
|
||||
@ -507,7 +507,7 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
|
||||
label=_('User')
|
||||
)
|
||||
changed_object_type_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ContentType.objects.all(),
|
||||
queryset=ObjectType.objects.all(),
|
||||
required=False,
|
||||
label=_('Object Type'),
|
||||
widget=APISelectMultiple(
|
||||
|
@ -2,12 +2,11 @@ import json
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
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 extras.choices import *
|
||||
from extras.models import *
|
||||
@ -39,13 +38,13 @@ __all__ = (
|
||||
|
||||
|
||||
class CustomFieldForm(forms.ModelForm):
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Content types'),
|
||||
queryset=ContentType.objects.with_feature('custom_fields')
|
||||
object_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Object types'),
|
||||
queryset=ObjectType.objects.with_feature('custom_fields')
|
||||
)
|
||||
object_type = ContentTypeChoiceField(
|
||||
label=_('Object type'),
|
||||
queryset=ContentType.objects.public(),
|
||||
related_object_type = ContentTypeChoiceField(
|
||||
label=_('Related object type'),
|
||||
queryset=ObjectType.objects.public(),
|
||||
required=False,
|
||||
help_text=_("Type of the related object (for object/multi-object fields only)")
|
||||
)
|
||||
@ -56,7 +55,7 @@ class CustomFieldForm(forms.ModelForm):
|
||||
|
||||
fieldsets = (
|
||||
(_('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')),
|
||||
(_('Values'), ('default', 'choice_set')),
|
||||
@ -123,13 +122,13 @@ class CustomFieldChoiceSetForm(forms.ModelForm):
|
||||
|
||||
|
||||
class CustomLinkForm(forms.ModelForm):
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Content types'),
|
||||
queryset=ContentType.objects.with_feature('custom_links')
|
||||
object_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Object types'),
|
||||
queryset=ObjectType.objects.with_feature('custom_links')
|
||||
)
|
||||
|
||||
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')),
|
||||
)
|
||||
|
||||
@ -152,9 +151,9 @@ class CustomLinkForm(forms.ModelForm):
|
||||
|
||||
|
||||
class ExportTemplateForm(SyncedDataMixin, forms.ModelForm):
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Content types'),
|
||||
queryset=ContentType.objects.with_feature('export_templates')
|
||||
object_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Object types'),
|
||||
queryset=ObjectType.objects.with_feature('export_templates')
|
||||
)
|
||||
template_code = forms.CharField(
|
||||
label=_('Template code'),
|
||||
@ -163,7 +162,7 @@ class ExportTemplateForm(SyncedDataMixin, forms.ModelForm):
|
||||
)
|
||||
|
||||
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')),
|
||||
(_('Rendering'), ('mime_type', 'file_extension', 'as_attachment')),
|
||||
)
|
||||
@ -193,14 +192,14 @@ class ExportTemplateForm(SyncedDataMixin, forms.ModelForm):
|
||||
|
||||
class SavedFilterForm(forms.ModelForm):
|
||||
slug = SlugField()
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Content types'),
|
||||
queryset=ContentType.objects.all()
|
||||
object_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Object types'),
|
||||
queryset=ObjectType.objects.all()
|
||||
)
|
||||
parameters = JSONField()
|
||||
|
||||
fieldsets = (
|
||||
(_('Saved Filter'), ('name', 'slug', 'content_types', 'description', 'weight', 'enabled', 'shared')),
|
||||
(_('Saved Filter'), ('name', 'slug', 'object_types', 'description', 'weight', 'enabled', 'shared')),
|
||||
(_('Parameters'), ('parameters',)),
|
||||
)
|
||||
|
||||
@ -221,7 +220,7 @@ class SavedFilterForm(forms.ModelForm):
|
||||
class BookmarkForm(forms.ModelForm):
|
||||
object_type = ContentTypeChoiceField(
|
||||
label=_('Object type'),
|
||||
queryset=ContentType.objects.with_feature('bookmarks')
|
||||
queryset=ObjectType.objects.with_feature('bookmarks')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -249,9 +248,9 @@ class WebhookForm(NetBoxModelForm):
|
||||
|
||||
|
||||
class EventRuleForm(NetBoxModelForm):
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Content types'),
|
||||
queryset=ContentType.objects.with_feature('event_rules'),
|
||||
object_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Object types'),
|
||||
queryset=ObjectType.objects.with_feature('event_rules'),
|
||||
)
|
||||
action_choice = forms.ChoiceField(
|
||||
label=_('Action choice'),
|
||||
@ -267,7 +266,7 @@ class EventRuleForm(NetBoxModelForm):
|
||||
)
|
||||
|
||||
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')),
|
||||
(_('Conditions'), ('conditions',)),
|
||||
(_('Action'), (
|
||||
@ -278,7 +277,7 @@ class EventRuleForm(NetBoxModelForm):
|
||||
class Meta:
|
||||
model = EventRule
|
||||
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',
|
||||
'action_data', 'comments', 'tags'
|
||||
)
|
||||
@ -339,11 +338,11 @@ class EventRuleForm(NetBoxModelForm):
|
||||
action_choice = self.cleaned_data.get('action_choice')
|
||||
# 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
|
||||
# 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,
|
||||
for_concrete_model=False
|
||||
)
|
||||
@ -356,7 +355,7 @@ class TagForm(forms.ModelForm):
|
||||
slug = SlugField()
|
||||
object_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Object types'),
|
||||
queryset=ContentType.objects.with_feature('tags'),
|
||||
queryset=ObjectType.objects.with_feature('tags'),
|
||||
required=False
|
||||
)
|
||||
|
||||
|
@ -39,7 +39,7 @@ class CustomFieldType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.CustomField
|
||||
exclude = ('content_types', )
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CustomFieldFilterSet
|
||||
|
||||
|
||||
@ -55,15 +55,23 @@ class CustomLinkType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.CustomLink
|
||||
exclude = ('content_types', )
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.CustomLinkFilterSet
|
||||
|
||||
|
||||
class EventRuleType(OrganizationalObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.EventRule
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.EventRuleFilterSet
|
||||
|
||||
|
||||
class ExportTemplateType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ExportTemplate
|
||||
exclude = ('content_types', )
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ExportTemplateFilterSet
|
||||
|
||||
|
||||
@ -95,7 +103,7 @@ class SavedFilterType(ObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.SavedFilter
|
||||
exclude = ('content_types', )
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.SavedFilterFilterSet
|
||||
|
||||
|
||||
@ -112,11 +120,3 @@ class WebhookType(OrganizationalObjectType):
|
||||
class Meta:
|
||||
model = models.Webhook
|
||||
filterset_class = filtersets.WebhookFilterSet
|
||||
|
||||
|
||||
class EventRuleType(OrganizationalObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.EventRule
|
||||
exclude = ('content_types', )
|
||||
filterset_class = filtersets.EventRuleFilterSet
|
||||
|
@ -25,7 +25,4 @@ class Migration(migrations.Migration):
|
||||
migrations.DeleteModel(
|
||||
name='Report',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='ReportModule',
|
||||
),
|
||||
]
|
||||
|
@ -82,10 +82,12 @@ def update_scripts(apps, schema_editor):
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
Script = apps.get_model('extras', 'Script')
|
||||
ScriptModule = apps.get_model('extras', 'ScriptModule')
|
||||
ReportModule = apps.get_model('extras', 'ReportModule')
|
||||
Job = apps.get_model('core', 'Job')
|
||||
|
||||
script_ct = ContentType.objects.get_for_model(Script)
|
||||
scriptmodule_ct = ContentType.objects.get_for_model(ScriptModule)
|
||||
script_ct = ContentType.objects.get_for_model(Script, for_concrete_model=False)
|
||||
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 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
|
||||
Job.objects.filter(
|
||||
object_type=scriptmodule_ct,
|
||||
object_type_id=scriptmodule_ct.id,
|
||||
object_id=module.pk,
|
||||
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):
|
||||
|
@ -12,4 +12,7 @@ class Migration(migrations.Migration):
|
||||
model_name='eventrule',
|
||||
name='action_parameters',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='ReportModule',
|
||||
),
|
||||
]
|
||||
|
107
netbox/extras/migrations/0111_rename_content_types.py
Normal file
107
netbox/extras/migrations/0111_rename_content_types.py
Normal 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'),
|
||||
),
|
||||
]
|
17
netbox/extras/migrations/0112_tag_update_object_types.py
Normal file
17
netbox/extras/migrations/0112_tag_update_object_types.py
Normal 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'),
|
||||
),
|
||||
]
|
@ -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',
|
||||
),
|
||||
]
|
@ -5,7 +5,7 @@ from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import ContentType
|
||||
from core.models import ObjectType
|
||||
from extras.choices import *
|
||||
from ..querysets import ObjectChangeQuerySet
|
||||
|
||||
@ -113,7 +113,7 @@ class ObjectChange(models.Model):
|
||||
super().clean()
|
||||
|
||||
# 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(
|
||||
_("Change logging is not supported for this object type ({type}).").format(
|
||||
type=self.changed_object_type
|
||||
|
@ -12,7 +12,7 @@ from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import ContentType
|
||||
from core.models import ObjectType
|
||||
from extras.choices import *
|
||||
from extras.data import CHOICE_SETS
|
||||
from netbox.models import ChangeLoggedModel
|
||||
@ -52,8 +52,8 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
|
||||
"""
|
||||
Return all CustomFields assigned to the given model.
|
||||
"""
|
||||
content_type = ContentType.objects.get_for_model(model._meta.concrete_model)
|
||||
return self.get_queryset().filter(content_types=content_type)
|
||||
content_type = ObjectType.objects.get_for_model(model._meta.concrete_model)
|
||||
return self.get_queryset().filter(object_types=content_type)
|
||||
|
||||
def get_defaults_for_model(self, model):
|
||||
"""
|
||||
@ -66,8 +66,8 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
|
||||
|
||||
|
||||
class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
content_types = models.ManyToManyField(
|
||||
to='contenttypes.ContentType',
|
||||
object_types = models.ManyToManyField(
|
||||
to='core.ObjectType',
|
||||
related_name='custom_fields',
|
||||
help_text=_('The object(s) to which this field applies.')
|
||||
)
|
||||
@ -78,8 +78,8 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
default=CustomFieldTypeChoices.TYPE_TEXT,
|
||||
help_text=_('The type of data this custom field holds')
|
||||
)
|
||||
object_type = models.ForeignKey(
|
||||
to='contenttypes.ContentType',
|
||||
related_object_type = models.ForeignKey(
|
||||
to='core.ObjectType',
|
||||
on_delete=models.PROTECT,
|
||||
blank=True,
|
||||
null=True,
|
||||
@ -209,7 +209,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
objects = CustomFieldManager()
|
||||
|
||||
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',
|
||||
'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.
|
||||
"""
|
||||
for ct in self.content_types.all():
|
||||
for ct in self.object_types.all():
|
||||
model = ct.model_class()
|
||||
params = {f'custom_field_data__{old_name}__isnull': False}
|
||||
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
|
||||
if self.type in (CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT):
|
||||
if not self.object_type:
|
||||
if not self.related_object_type:
|
||||
raise ValidationError({
|
||||
'object_type': _("Object fields must define an object type.")
|
||||
})
|
||||
elif self.object_type:
|
||||
elif self.related_object_type:
|
||||
raise ValidationError({
|
||||
'object_type': _(
|
||||
"{type} fields may not define an object type.")
|
||||
@ -388,10 +388,10 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
except ValueError:
|
||||
return value
|
||||
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()
|
||||
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 value
|
||||
|
||||
@ -488,7 +488,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
|
||||
# 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 = field_class(
|
||||
queryset=model.objects.all(),
|
||||
@ -498,7 +498,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
|
||||
# Multiple objects
|
||||
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 = field_class(
|
||||
queryset=model.objects.all(),
|
||||
|
@ -12,7 +12,7 @@ from django.utils.formats import date_format
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.utils.encoders import JSONEncoder
|
||||
|
||||
from core.models import ContentType
|
||||
from core.models import ObjectType
|
||||
from extras.choices import *
|
||||
from extras.conditions import ConditionSet
|
||||
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
|
||||
webhook or executing a custom script.
|
||||
"""
|
||||
content_types = models.ManyToManyField(
|
||||
to='contenttypes.ContentType',
|
||||
related_name='eventrules',
|
||||
object_types = models.ManyToManyField(
|
||||
to='core.ObjectType',
|
||||
related_name='event_rules',
|
||||
verbose_name=_('object types'),
|
||||
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
|
||||
code to be rendered with an object as context.
|
||||
"""
|
||||
content_types = models.ManyToManyField(
|
||||
to='contenttypes.ContentType',
|
||||
object_types = models.ManyToManyField(
|
||||
to='core.ObjectType',
|
||||
related_name='custom_links',
|
||||
help_text=_('The object type(s) to which this link applies.')
|
||||
)
|
||||
@ -359,7 +359,7 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'content_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window',
|
||||
'object_types', 'enabled', 'weight', 'group_name', 'button_class', 'new_window',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -409,8 +409,8 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
|
||||
|
||||
class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
content_types = models.ManyToManyField(
|
||||
to='contenttypes.ContentType',
|
||||
object_types = models.ManyToManyField(
|
||||
to='core.ObjectType',
|
||||
related_name='export_templates',
|
||||
help_text=_('The object type(s) to which this template applies.')
|
||||
)
|
||||
@ -448,7 +448,7 @@ class ExportTemplate(SyncedDataMixin, CloningMixin, ExportTemplatesMixin, Change
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'content_types', 'template_code', 'mime_type', 'file_extension', 'as_attachment',
|
||||
'object_types', 'template_code', 'mime_type', 'file_extension', 'as_attachment',
|
||||
)
|
||||
|
||||
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.
|
||||
"""
|
||||
content_types = models.ManyToManyField(
|
||||
to='contenttypes.ContentType',
|
||||
object_types = models.ManyToManyField(
|
||||
to='core.ObjectType',
|
||||
related_name='saved_filters',
|
||||
help_text=_('The object type(s) to which this filter applies.')
|
||||
)
|
||||
@ -561,7 +561,7 @@ class SavedFilter(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'content_types', 'weight', 'enabled', 'parameters',
|
||||
'object_types', 'weight', 'enabled', 'parameters',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -598,13 +598,13 @@ class ImageAttachment(ChangeLoggedModel):
|
||||
"""
|
||||
An uploaded image which is associated with an object.
|
||||
"""
|
||||
content_type = models.ForeignKey(
|
||||
object_type = models.ForeignKey(
|
||||
to='contenttypes.ContentType',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
object_id = models.PositiveBigIntegerField()
|
||||
parent = GenericForeignKey(
|
||||
ct_field='content_type',
|
||||
ct_field='object_type',
|
||||
fk_field='object_id'
|
||||
)
|
||||
image = models.ImageField(
|
||||
@ -626,12 +626,12 @@ class ImageAttachment(ChangeLoggedModel):
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
|
||||
clone_fields = ('content_type', 'object_id')
|
||||
clone_fields = ('object_type', 'object_id')
|
||||
|
||||
class Meta:
|
||||
ordering = ('name', 'pk') # name may be non-unique
|
||||
indexes = (
|
||||
models.Index(fields=('content_type', 'object_id')),
|
||||
models.Index(fields=('object_type', 'object_id')),
|
||||
)
|
||||
verbose_name = _('image attachment')
|
||||
verbose_name_plural = _('image attachments')
|
||||
@ -646,9 +646,9 @@ class ImageAttachment(ChangeLoggedModel):
|
||||
super().clean()
|
||||
|
||||
# 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(
|
||||
_("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):
|
||||
@ -739,7 +739,7 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
|
||||
super().clean()
|
||||
|
||||
# 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(
|
||||
_("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()
|
||||
|
||||
# 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(
|
||||
_("Bookmarks cannot be assigned to this object type ({type}).").format(type=self.object_type)
|
||||
)
|
||||
|
@ -34,7 +34,7 @@ class Tag(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel, TagBase):
|
||||
blank=True,
|
||||
)
|
||||
object_types = models.ManyToManyField(
|
||||
to='contenttypes.ContentType',
|
||||
to='core.ObjectType',
|
||||
related_name='+',
|
||||
blank=True,
|
||||
help_text=_("The object type(s) to which this this tag can be applied.")
|
||||
|
@ -8,6 +8,7 @@ from django.dispatch import receiver, Signal
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
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 extras.constants import EVENT_JOB_END, EVENT_JOB_START
|
||||
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.
|
||||
"""
|
||||
instance.remove_stale_data(instance.content_types.all())
|
||||
instance.remove_stale_data(instance.object_types.all())
|
||||
|
||||
|
||||
post_save.connect(handle_cf_renamed, 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_removed_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.object_types.through)
|
||||
|
||||
|
||||
#
|
||||
@ -240,8 +241,8 @@ def validate_assigned_tags(sender, instance, action, model, pk_set, **kwargs):
|
||||
"""
|
||||
if action != 'pre_add':
|
||||
return
|
||||
ct = ContentType.objects.get_for_model(instance)
|
||||
# Retrieve any applied Tags that are restricted to certain object_types
|
||||
ct = ObjectType.objects.get_for_model(instance)
|
||||
# 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'):
|
||||
if ct not in tag.object_types.all():
|
||||
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.
|
||||
"""
|
||||
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
|
||||
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.
|
||||
"""
|
||||
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
|
||||
process_event_rules(event_rules, sender.object_type.model, EVENT_JOB_END, sender.data, username)
|
||||
|
@ -5,7 +5,7 @@ from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from extras.models import *
|
||||
from netbox.tables import NetBoxTable, columns
|
||||
from netbox.tables import BaseTable, NetBoxTable, columns
|
||||
from .template_code import *
|
||||
|
||||
__all__ = (
|
||||
@ -21,6 +21,8 @@ __all__ = (
|
||||
'JournalEntryTable',
|
||||
'ObjectChangeTable',
|
||||
'SavedFilterTable',
|
||||
'ReportResultsTable',
|
||||
'ScriptResultsTable',
|
||||
'TaggedItemTable',
|
||||
'TagTable',
|
||||
'WebhookTable',
|
||||
@ -40,8 +42,8 @@ class CustomFieldTable(NetBoxTable):
|
||||
verbose_name=_('Name'),
|
||||
linkify=True
|
||||
)
|
||||
content_types = columns.ContentTypesColumn(
|
||||
verbose_name=_('Content Types')
|
||||
object_types = columns.ContentTypesColumn(
|
||||
verbose_name=_('Object Types')
|
||||
)
|
||||
required = columns.BooleanColumn(
|
||||
verbose_name=_('Required')
|
||||
@ -55,6 +57,9 @@ class CustomFieldTable(NetBoxTable):
|
||||
description = columns.MarkdownColumn(
|
||||
verbose_name=_('Description')
|
||||
)
|
||||
related_object_type = columns.ContentTypeColumn(
|
||||
verbose_name=_('Related Object Type')
|
||||
)
|
||||
choice_set = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('Choice Set')
|
||||
@ -71,11 +76,11 @@ class CustomFieldTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = CustomField
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'default', 'description',
|
||||
'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', 'weight', 'choice_set',
|
||||
'choices', 'created', 'last_updated',
|
||||
'pk', 'id', 'name', 'object_types', 'label', 'type', 'related_object_type', 'group_name', 'required',
|
||||
'default', 'description', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable',
|
||||
'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):
|
||||
@ -115,8 +120,8 @@ class CustomLinkTable(NetBoxTable):
|
||||
verbose_name=_('Name'),
|
||||
linkify=True
|
||||
)
|
||||
content_types = columns.ContentTypesColumn(
|
||||
verbose_name=_('Content Types'),
|
||||
object_types = columns.ContentTypesColumn(
|
||||
verbose_name=_('Object Types'),
|
||||
)
|
||||
enabled = columns.BooleanColumn(
|
||||
verbose_name=_('Enabled'),
|
||||
@ -128,10 +133,10 @@ class CustomLinkTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = CustomLink
|
||||
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',
|
||||
)
|
||||
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):
|
||||
@ -139,8 +144,8 @@ class ExportTemplateTable(NetBoxTable):
|
||||
verbose_name=_('Name'),
|
||||
linkify=True
|
||||
)
|
||||
content_types = columns.ContentTypesColumn(
|
||||
verbose_name=_('Content Types'),
|
||||
object_types = columns.ContentTypesColumn(
|
||||
verbose_name=_('Object Types'),
|
||||
)
|
||||
as_attachment = columns.BooleanColumn(
|
||||
verbose_name=_('As Attachment'),
|
||||
@ -161,11 +166,11 @@ class ExportTemplateTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = ExportTemplate
|
||||
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',
|
||||
)
|
||||
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'),
|
||||
linkify=False
|
||||
)
|
||||
content_type = columns.ContentTypeColumn(
|
||||
verbose_name=_('Content Type'),
|
||||
object_type = columns.ContentTypeColumn(
|
||||
verbose_name=_('Object Type'),
|
||||
)
|
||||
parent = tables.Column(
|
||||
verbose_name=_('Parent'),
|
||||
@ -193,10 +198,10 @@ class ImageAttachmentTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = ImageAttachment
|
||||
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',
|
||||
)
|
||||
default_columns = ('content_type', 'parent', 'image', 'name', 'size', 'created')
|
||||
default_columns = ('object_type', 'parent', 'image', 'name', 'size', 'created')
|
||||
|
||||
|
||||
class SavedFilterTable(NetBoxTable):
|
||||
@ -204,8 +209,8 @@ class SavedFilterTable(NetBoxTable):
|
||||
verbose_name=_('Name'),
|
||||
linkify=True
|
||||
)
|
||||
content_types = columns.ContentTypesColumn(
|
||||
verbose_name=_('Content Types'),
|
||||
object_types = columns.ContentTypesColumn(
|
||||
verbose_name=_('Object Types'),
|
||||
)
|
||||
enabled = columns.BooleanColumn(
|
||||
verbose_name=_('Enabled'),
|
||||
@ -220,11 +225,11 @@ class SavedFilterTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = SavedFilter
|
||||
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'
|
||||
)
|
||||
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,
|
||||
verbose_name=_('Object'),
|
||||
)
|
||||
content_types = columns.ContentTypesColumn(
|
||||
verbose_name=_('Content Types'),
|
||||
object_types = columns.ContentTypesColumn(
|
||||
verbose_name=_('Object Types'),
|
||||
)
|
||||
enabled = columns.BooleanColumn(
|
||||
verbose_name=_('Enabled'),
|
||||
@ -309,12 +314,12 @@ class EventRuleTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = EventRule
|
||||
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',
|
||||
'last_updated',
|
||||
)
|
||||
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',
|
||||
)
|
||||
|
||||
@ -507,3 +512,61 @@ class JournalEntryTable(NetBoxTable):
|
||||
default_columns = (
|
||||
'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',
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
from django import template
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from core.models import ObjectType
|
||||
from extras.models import CustomLink
|
||||
|
||||
|
||||
@ -32,8 +32,8 @@ def custom_links(context, obj):
|
||||
"""
|
||||
Render all applicable links for the given object.
|
||||
"""
|
||||
content_type = ContentType.objects.get_for_model(obj)
|
||||
custom_links = CustomLink.objects.filter(content_types=content_type, enabled=True)
|
||||
object_type = ObjectType.objects.get_for_model(obj)
|
||||
custom_links = CustomLink.objects.filter(object_types=object_type, enabled=True)
|
||||
if not custom_links:
|
||||
return ''
|
||||
|
||||
|
@ -7,10 +7,10 @@ from django.utils.timezone import make_aware
|
||||
from rest_framework import status
|
||||
|
||||
from core.choices import ManagedFileRootPathChoices
|
||||
from core.models import ObjectType
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
|
||||
from extras.choices import *
|
||||
from extras.models import *
|
||||
from extras.reports import Report
|
||||
from extras.scripts import BooleanVar, IntegerVar, Script as PythonClass, StringVar
|
||||
from utilities.testing import APITestCase, APIViewTestCases
|
||||
|
||||
@ -122,7 +122,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase):
|
||||
cls.create_data = [
|
||||
{
|
||||
'name': 'EventRule 4',
|
||||
'content_types': ['dcim.device', 'dcim.devicetype'],
|
||||
'object_types': ['dcim.device', 'dcim.devicetype'],
|
||||
'type_create': True,
|
||||
'action_type': EventRuleActionChoices.WEBHOOK,
|
||||
'action_object_type': 'extras.webhook',
|
||||
@ -130,7 +130,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
{
|
||||
'name': 'EventRule 5',
|
||||
'content_types': ['dcim.device', 'dcim.devicetype'],
|
||||
'object_types': ['dcim.device', 'dcim.devicetype'],
|
||||
'type_create': True,
|
||||
'action_type': EventRuleActionChoices.WEBHOOK,
|
||||
'action_object_type': 'extras.webhook',
|
||||
@ -138,7 +138,7 @@ class EventRuleTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
{
|
||||
'name': 'EventRule 6',
|
||||
'content_types': ['dcim.device', 'dcim.devicetype'],
|
||||
'object_types': ['dcim.device', 'dcim.devicetype'],
|
||||
'type_create': True,
|
||||
'action_type': EventRuleActionChoices.WEBHOOK,
|
||||
'action_object_type': 'extras.webhook',
|
||||
@ -152,17 +152,17 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase):
|
||||
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'content_types': ['dcim.site'],
|
||||
'object_types': ['dcim.site'],
|
||||
'name': 'cf4',
|
||||
'type': 'date',
|
||||
},
|
||||
{
|
||||
'content_types': ['dcim.site'],
|
||||
'object_types': ['dcim.site'],
|
||||
'name': 'cf5',
|
||||
'type': 'url',
|
||||
},
|
||||
{
|
||||
'content_types': ['dcim.site'],
|
||||
'object_types': ['dcim.site'],
|
||||
'name': 'cf6',
|
||||
'type': 'text',
|
||||
},
|
||||
@ -171,14 +171,14 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase):
|
||||
'description': 'New description',
|
||||
}
|
||||
update_data = {
|
||||
'content_types': ['dcim.device'],
|
||||
'object_types': ['dcim.device'],
|
||||
'name': 'New_Name',
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
site_ct = ContentType.objects.get_for_model(Site)
|
||||
site_ct = ObjectType.objects.get_for_model(Site)
|
||||
|
||||
custom_fields = (
|
||||
CustomField(
|
||||
@ -196,7 +196,7 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase):
|
||||
)
|
||||
CustomField.objects.bulk_create(custom_fields)
|
||||
for cf in custom_fields:
|
||||
cf.content_types.add(site_ct)
|
||||
cf.object_types.add(site_ct)
|
||||
|
||||
|
||||
class CustomFieldChoiceSetTest(APIViewTestCases.APIViewTestCase):
|
||||
@ -273,21 +273,21 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase):
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'content_types': ['dcim.site'],
|
||||
'object_types': ['dcim.site'],
|
||||
'name': 'Custom Link 4',
|
||||
'enabled': True,
|
||||
'link_text': 'Link 4',
|
||||
'link_url': 'http://example.com/?4',
|
||||
},
|
||||
{
|
||||
'content_types': ['dcim.site'],
|
||||
'object_types': ['dcim.site'],
|
||||
'name': 'Custom Link 5',
|
||||
'enabled': True,
|
||||
'link_text': 'Link 5',
|
||||
'link_url': 'http://example.com/?5',
|
||||
},
|
||||
{
|
||||
'content_types': ['dcim.site'],
|
||||
'object_types': ['dcim.site'],
|
||||
'name': 'Custom Link 6',
|
||||
'enabled': False,
|
||||
'link_text': 'Link 6',
|
||||
@ -301,7 +301,7 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
site_ct = ContentType.objects.get_for_model(Site)
|
||||
site_type = ObjectType.objects.get_for_model(Site)
|
||||
|
||||
custom_links = (
|
||||
CustomLink(
|
||||
@ -325,7 +325,7 @@ class CustomLinkTest(APIViewTestCases.APIViewTestCase):
|
||||
)
|
||||
CustomLink.objects.bulk_create(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):
|
||||
@ -333,7 +333,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase):
|
||||
brief_fields = ['description', 'display', 'id', 'name', 'slug', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'content_types': ['dcim.site'],
|
||||
'object_types': ['dcim.site'],
|
||||
'name': 'Saved Filter 4',
|
||||
'slug': 'saved-filter-4',
|
||||
'weight': 100,
|
||||
@ -342,7 +342,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase):
|
||||
'parameters': {'status': ['active']},
|
||||
},
|
||||
{
|
||||
'content_types': ['dcim.site'],
|
||||
'object_types': ['dcim.site'],
|
||||
'name': 'Saved Filter 5',
|
||||
'slug': 'saved-filter-5',
|
||||
'weight': 200,
|
||||
@ -351,7 +351,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase):
|
||||
'parameters': {'status': ['planned']},
|
||||
},
|
||||
{
|
||||
'content_types': ['dcim.site'],
|
||||
'object_types': ['dcim.site'],
|
||||
'name': 'Saved Filter 6',
|
||||
'slug': 'saved-filter-6',
|
||||
'weight': 300,
|
||||
@ -368,7 +368,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
site_ct = ContentType.objects.get_for_model(Site)
|
||||
site_type = ObjectType.objects.get_for_model(Site)
|
||||
|
||||
saved_filters = (
|
||||
SavedFilter(
|
||||
@ -398,7 +398,7 @@ class SavedFilterTest(APIViewTestCases.APIViewTestCase):
|
||||
)
|
||||
SavedFilter.objects.bulk_create(saved_filters)
|
||||
for i, savedfilter in enumerate(saved_filters):
|
||||
savedfilter.content_types.set([site_ct])
|
||||
savedfilter.object_types.set([site_type])
|
||||
|
||||
|
||||
class BookmarkTest(
|
||||
@ -458,17 +458,17 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
brief_fields = ['description', 'display', 'id', 'name', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'content_types': ['dcim.device'],
|
||||
'object_types': ['dcim.device'],
|
||||
'name': 'Test Export Template 4',
|
||||
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
|
||||
},
|
||||
{
|
||||
'content_types': ['dcim.device'],
|
||||
'object_types': ['dcim.device'],
|
||||
'name': 'Test Export Template 5',
|
||||
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
|
||||
},
|
||||
{
|
||||
'content_types': ['dcim.device'],
|
||||
'object_types': ['dcim.device'],
|
||||
'name': 'Test Export Template 6',
|
||||
'template_code': '{% for obj in queryset %}{{ obj.name }}\n{% endfor %}',
|
||||
},
|
||||
@ -495,7 +495,7 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
||||
)
|
||||
ExportTemplate.objects.bulk_create(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):
|
||||
@ -548,7 +548,7 @@ class ImageAttachmentTest(
|
||||
|
||||
image_attachments = (
|
||||
ImageAttachment(
|
||||
content_type=ct,
|
||||
object_type=ct,
|
||||
object_id=site.pk,
|
||||
name='Image Attachment 1',
|
||||
image='http://example.com/image1.png',
|
||||
@ -556,7 +556,7 @@ class ImageAttachmentTest(
|
||||
image_width=100
|
||||
),
|
||||
ImageAttachment(
|
||||
content_type=ct,
|
||||
object_type=ct,
|
||||
object_id=site.pk,
|
||||
name='Image Attachment 2',
|
||||
image='http://example.com/image2.png',
|
||||
@ -564,7 +564,7 @@ class ImageAttachmentTest(
|
||||
image_width=100
|
||||
),
|
||||
ImageAttachment(
|
||||
content_type=ct,
|
||||
object_type=ct,
|
||||
object_id=site.pk,
|
||||
name='Image Attachment 3',
|
||||
image='http://example.com/image3.png',
|
||||
@ -876,17 +876,17 @@ class CreatedUpdatedFilterTest(APITestCase):
|
||||
self.assertEqual(response.data['results'][0]['id'], rack2.pk)
|
||||
|
||||
|
||||
class ContentTypeTest(APITestCase):
|
||||
class ObjectTypeTest(APITestCase):
|
||||
|
||||
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.assertEqual(response.data['count'], contenttype_count)
|
||||
self.assertEqual(response.data['count'], object_type_count)
|
||||
|
||||
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)
|
||||
|
@ -3,6 +3,7 @@ from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
from core.models import ObjectType
|
||||
from dcim.choices import SiteStatusChoices
|
||||
from dcim.models import Site
|
||||
from extras.choices import *
|
||||
@ -23,14 +24,14 @@ class ChangeLogViewTest(ModelViewTestCase):
|
||||
)
|
||||
|
||||
# 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(
|
||||
type=CustomFieldTypeChoices.TYPE_TEXT,
|
||||
name='cf1',
|
||||
required=False
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([ct])
|
||||
cf.object_types.set([site_type])
|
||||
|
||||
# Create a select custom field on the Site model
|
||||
cf_select = CustomField(
|
||||
@ -40,7 +41,7 @@ class ChangeLogViewTest(ModelViewTestCase):
|
||||
choice_set=choice_set
|
||||
)
|
||||
cf_select.save()
|
||||
cf_select.content_types.set([ct])
|
||||
cf_select.object_types.set([site_type])
|
||||
|
||||
def test_create_object(self):
|
||||
tags = create_tags('Tag 1', 'Tag 2')
|
||||
@ -275,14 +276,14 @@ class ChangeLogAPITest(APITestCase):
|
||||
def setUpTestData(cls):
|
||||
|
||||
# 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(
|
||||
type=CustomFieldTypeChoices.TYPE_TEXT,
|
||||
name='cf1',
|
||||
required=False
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([ct])
|
||||
cf.object_types.set([site_type])
|
||||
|
||||
# Create a select custom field on the Site model
|
||||
choice_set = CustomFieldChoiceSet.objects.create(
|
||||
@ -296,7 +297,7 @@ class ChangeLogAPITest(APITestCase):
|
||||
choice_set=choice_set
|
||||
)
|
||||
cf_select.save()
|
||||
cf_select.content_types.set([ct])
|
||||
cf_select.object_types.set([site_type])
|
||||
|
||||
# Create some tags
|
||||
tags = (
|
||||
|
@ -1,11 +1,11 @@
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
from core.models import ObjectType
|
||||
from dcim.filtersets import SiteFilterSet
|
||||
from dcim.forms import SiteImportForm
|
||||
from dcim.models import Manufacturer, Rack, Site
|
||||
@ -28,7 +28,7 @@ class CustomFieldTest(TestCase):
|
||||
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):
|
||||
"""
|
||||
@ -50,7 +50,7 @@ class CustomFieldTest(TestCase):
|
||||
type=CustomFieldTypeChoices.TYPE_TEXT,
|
||||
required=False
|
||||
)
|
||||
cf.content_types.set([self.object_type])
|
||||
cf.object_types.set([self.object_type])
|
||||
instance = Site.objects.first()
|
||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||
|
||||
@ -75,7 +75,7 @@ class CustomFieldTest(TestCase):
|
||||
type=CustomFieldTypeChoices.TYPE_LONGTEXT,
|
||||
required=False
|
||||
)
|
||||
cf.content_types.set([self.object_type])
|
||||
cf.object_types.set([self.object_type])
|
||||
instance = Site.objects.first()
|
||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||
|
||||
@ -99,7 +99,7 @@ class CustomFieldTest(TestCase):
|
||||
type=CustomFieldTypeChoices.TYPE_INTEGER,
|
||||
required=False
|
||||
)
|
||||
cf.content_types.set([self.object_type])
|
||||
cf.object_types.set([self.object_type])
|
||||
instance = Site.objects.first()
|
||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||
|
||||
@ -125,7 +125,7 @@ class CustomFieldTest(TestCase):
|
||||
type=CustomFieldTypeChoices.TYPE_DECIMAL,
|
||||
required=False
|
||||
)
|
||||
cf.content_types.set([self.object_type])
|
||||
cf.object_types.set([self.object_type])
|
||||
instance = Site.objects.first()
|
||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||
|
||||
@ -151,7 +151,7 @@ class CustomFieldTest(TestCase):
|
||||
type=CustomFieldTypeChoices.TYPE_INTEGER,
|
||||
required=False
|
||||
)
|
||||
cf.content_types.set([self.object_type])
|
||||
cf.object_types.set([self.object_type])
|
||||
instance = Site.objects.first()
|
||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||
|
||||
@ -178,7 +178,7 @@ class CustomFieldTest(TestCase):
|
||||
type=CustomFieldTypeChoices.TYPE_DATE,
|
||||
required=False
|
||||
)
|
||||
cf.content_types.set([self.object_type])
|
||||
cf.object_types.set([self.object_type])
|
||||
instance = Site.objects.first()
|
||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||
|
||||
@ -203,7 +203,7 @@ class CustomFieldTest(TestCase):
|
||||
type=CustomFieldTypeChoices.TYPE_DATETIME,
|
||||
required=False
|
||||
)
|
||||
cf.content_types.set([self.object_type])
|
||||
cf.object_types.set([self.object_type])
|
||||
instance = Site.objects.first()
|
||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||
|
||||
@ -228,7 +228,7 @@ class CustomFieldTest(TestCase):
|
||||
type=CustomFieldTypeChoices.TYPE_URL,
|
||||
required=False
|
||||
)
|
||||
cf.content_types.set([self.object_type])
|
||||
cf.object_types.set([self.object_type])
|
||||
instance = Site.objects.first()
|
||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||
|
||||
@ -253,7 +253,7 @@ class CustomFieldTest(TestCase):
|
||||
type=CustomFieldTypeChoices.TYPE_JSON,
|
||||
required=False
|
||||
)
|
||||
cf.content_types.set([self.object_type])
|
||||
cf.object_types.set([self.object_type])
|
||||
instance = Site.objects.first()
|
||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||
|
||||
@ -290,7 +290,7 @@ class CustomFieldTest(TestCase):
|
||||
required=False,
|
||||
choice_set=choice_set
|
||||
)
|
||||
cf.content_types.set([self.object_type])
|
||||
cf.object_types.set([self.object_type])
|
||||
instance = Site.objects.first()
|
||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||
|
||||
@ -327,7 +327,7 @@ class CustomFieldTest(TestCase):
|
||||
required=False,
|
||||
choice_set=choice_set
|
||||
)
|
||||
cf.content_types.set([self.object_type])
|
||||
cf.object_types.set([self.object_type])
|
||||
instance = Site.objects.first()
|
||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||
|
||||
@ -350,10 +350,10 @@ class CustomFieldTest(TestCase):
|
||||
cf = CustomField.objects.create(
|
||||
name='object_field',
|
||||
type=CustomFieldTypeChoices.TYPE_OBJECT,
|
||||
object_type=ContentType.objects.get_for_model(VLAN),
|
||||
related_object_type=ObjectType.objects.get_for_model(VLAN),
|
||||
required=False
|
||||
)
|
||||
cf.content_types.set([self.object_type])
|
||||
cf.object_types.set([self.object_type])
|
||||
instance = Site.objects.first()
|
||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||
|
||||
@ -382,10 +382,10 @@ class CustomFieldTest(TestCase):
|
||||
cf = CustomField.objects.create(
|
||||
name='object_field',
|
||||
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
|
||||
object_type=ContentType.objects.get_for_model(VLAN),
|
||||
related_object_type=ObjectType.objects.get_for_model(VLAN),
|
||||
required=False
|
||||
)
|
||||
cf.content_types.set([self.object_type])
|
||||
cf.object_types.set([self.object_type])
|
||||
instance = Site.objects.first()
|
||||
self.assertIsNone(instance.custom_field_data[cf.name])
|
||||
|
||||
@ -402,13 +402,13 @@ class CustomFieldTest(TestCase):
|
||||
self.assertIsNone(instance.custom_field_data.get(cf.name))
|
||||
|
||||
def test_rename_customfield(self):
|
||||
obj_type = ContentType.objects.get_for_model(Site)
|
||||
obj_type = ObjectType.objects.get_for_model(Site)
|
||||
FIELD_DATA = 'abc'
|
||||
|
||||
# Create a custom field
|
||||
cf = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='field1')
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
cf.object_types.set([obj_type])
|
||||
|
||||
# Assign custom field data to an object
|
||||
site = Site.objects.create(
|
||||
@ -437,7 +437,7 @@ class CustomFieldTest(TestCase):
|
||||
)
|
||||
)
|
||||
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
|
||||
CustomField(name='test', type='text', required=True, default="Default text").full_clean()
|
||||
@ -498,16 +498,28 @@ class CustomFieldTest(TestCase):
|
||||
).full_clean()
|
||||
|
||||
# Object
|
||||
CustomField(name='test', type='object', required=True, object_type=object_type, default=site.pk).full_clean()
|
||||
with self.assertRaises(ValidationError):
|
||||
CustomField(name='test', type='object', required=True, object_type=object_type, default="xxx").full_clean()
|
||||
CustomField(
|
||||
name='test',
|
||||
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
|
||||
CustomField(
|
||||
name='test',
|
||||
type='multiobject',
|
||||
required=True,
|
||||
object_type=object_type,
|
||||
related_object_type=object_type,
|
||||
default=[site.pk]
|
||||
).full_clean()
|
||||
with self.assertRaises(ValidationError):
|
||||
@ -515,7 +527,7 @@ class CustomFieldTest(TestCase):
|
||||
name='test',
|
||||
type='multiobject',
|
||||
required=True,
|
||||
object_type=object_type,
|
||||
related_object_type=object_type,
|
||||
default=["xxx"]
|
||||
).full_clean()
|
||||
|
||||
@ -524,10 +536,10 @@ class CustomFieldManagerTest(TestCase):
|
||||
|
||||
@classmethod
|
||||
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.save()
|
||||
custom_field.content_types.set([content_type])
|
||||
custom_field.object_types.set([object_type])
|
||||
|
||||
def test_get_for_model(self):
|
||||
self.assertEqual(CustomField.objects.get_for_model(Site).count(), 1)
|
||||
@ -538,7 +550,7 @@ class CustomFieldAPITest(APITestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
content_type = ContentType.objects.get_for_model(Site)
|
||||
object_type = ObjectType.objects.get_for_model(Site)
|
||||
|
||||
# Create some VLANs
|
||||
vlans = (
|
||||
@ -581,19 +593,19 @@ class CustomFieldAPITest(APITestCase):
|
||||
CustomField(
|
||||
type=CustomFieldTypeChoices.TYPE_OBJECT,
|
||||
name='object_field',
|
||||
object_type=ContentType.objects.get_for_model(VLAN),
|
||||
related_object_type=ObjectType.objects.get_for_model(VLAN),
|
||||
default=vlans[0].pk,
|
||||
),
|
||||
CustomField(
|
||||
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
|
||||
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],
|
||||
),
|
||||
)
|
||||
for cf in custom_fields:
|
||||
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
|
||||
# default values are not set for the assigned objects.
|
||||
@ -1163,7 +1175,7 @@ class CustomFieldImportTest(TestCase):
|
||||
)
|
||||
for cf in custom_fields:
|
||||
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):
|
||||
"""
|
||||
@ -1256,11 +1268,11 @@ class CustomFieldModelTest(TestCase):
|
||||
def setUpTestData(cls):
|
||||
cf1 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='foo')
|
||||
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.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):
|
||||
"""
|
||||
@ -1299,7 +1311,7 @@ class CustomFieldModelTest(TestCase):
|
||||
"""
|
||||
cf3 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='baz', required=True)
|
||||
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')
|
||||
|
||||
@ -1318,7 +1330,7 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
obj_type = ContentType.objects.get_for_model(Site)
|
||||
object_type = ObjectType.objects.get_for_model(Site)
|
||||
|
||||
manufacturers = Manufacturer.objects.bulk_create((
|
||||
Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
|
||||
@ -1335,17 +1347,17 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
# Integer filtering
|
||||
cf = CustomField(name='cf1', type=CustomFieldTypeChoices.TYPE_INTEGER)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
cf.object_types.set([object_type])
|
||||
|
||||
# Decimal filtering
|
||||
cf = CustomField(name='cf2', type=CustomFieldTypeChoices.TYPE_DECIMAL)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
cf.object_types.set([object_type])
|
||||
|
||||
# Boolean filtering
|
||||
cf = CustomField(name='cf3', type=CustomFieldTypeChoices.TYPE_BOOLEAN)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
cf.object_types.set([object_type])
|
||||
|
||||
# Exact text filtering
|
||||
cf = CustomField(
|
||||
@ -1354,7 +1366,7 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
cf.object_types.set([object_type])
|
||||
|
||||
# Loose text filtering
|
||||
cf = CustomField(
|
||||
@ -1363,12 +1375,12 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
cf.object_types.set([object_type])
|
||||
|
||||
# Date filtering
|
||||
cf = CustomField(name='cf6', type=CustomFieldTypeChoices.TYPE_DATE)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
cf.object_types.set([object_type])
|
||||
|
||||
# Exact URL filtering
|
||||
cf = CustomField(
|
||||
@ -1377,7 +1389,7 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
cf.object_types.set([object_type])
|
||||
|
||||
# Loose URL filtering
|
||||
cf = CustomField(
|
||||
@ -1386,7 +1398,7 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
cf.object_types.set([object_type])
|
||||
|
||||
# Selection filtering
|
||||
cf = CustomField(
|
||||
@ -1395,7 +1407,7 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
choice_set=choice_set
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
cf.object_types.set([object_type])
|
||||
|
||||
# Multiselect filtering
|
||||
cf = CustomField(
|
||||
@ -1404,25 +1416,25 @@ class CustomFieldModelFilterTest(TestCase):
|
||||
choice_set=choice_set
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
cf.object_types.set([object_type])
|
||||
|
||||
# Object filtering
|
||||
cf = CustomField(
|
||||
name='cf11',
|
||||
type=CustomFieldTypeChoices.TYPE_OBJECT,
|
||||
object_type=ContentType.objects.get_for_model(Manufacturer)
|
||||
related_object_type=ObjectType.objects.get_for_model(Manufacturer)
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
cf.object_types.set([object_type])
|
||||
|
||||
# Multi-object filtering
|
||||
cf = CustomField(
|
||||
name='cf12',
|
||||
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
|
||||
object_type=ContentType.objects.get_for_model(Manufacturer)
|
||||
related_object_type=ObjectType.objects.get_for_model(Manufacturer)
|
||||
)
|
||||
cf.save()
|
||||
cf.content_types.set([obj_type])
|
||||
cf.object_types.set([object_type])
|
||||
|
||||
Site.objects.bulk_create([
|
||||
Site(name='Site 1', slug='site-1', custom_field_data={
|
||||
|
@ -3,17 +3,18 @@ import uuid
|
||||
from unittest.mock import patch
|
||||
|
||||
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.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.events import enqueue_object, flush_events, serialize_for_event
|
||||
from extras.models import EventRule, Tag, Webhook
|
||||
from extras.webhooks import generate_signature, send_webhook
|
||||
from requests import Session
|
||||
from rest_framework import status
|
||||
from utilities.testing import APITestCase
|
||||
|
||||
|
||||
@ -29,7 +30,7 @@ class EventRuleTest(APITestCase):
|
||||
@classmethod
|
||||
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_SECRET = 'LOOKATMEIMASECRETSTRING'
|
||||
|
||||
@ -39,32 +40,32 @@ class EventRuleTest(APITestCase):
|
||||
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((
|
||||
EventRule(
|
||||
name='Webhook Event 1',
|
||||
type_create=True,
|
||||
action_type=EventRuleActionChoices.WEBHOOK,
|
||||
action_object_type=ct,
|
||||
action_object_type=webhook_type,
|
||||
action_object_id=webhooks[0].id
|
||||
),
|
||||
EventRule(
|
||||
name='Webhook Event 2',
|
||||
type_update=True,
|
||||
action_type=EventRuleActionChoices.WEBHOOK,
|
||||
action_object_type=ct,
|
||||
action_object_type=webhook_type,
|
||||
action_object_id=webhooks[0].id
|
||||
),
|
||||
EventRule(
|
||||
name='Webhook Event 3',
|
||||
type_delete=True,
|
||||
action_type=EventRuleActionChoices.WEBHOOK,
|
||||
action_object_type=ct,
|
||||
action_object_type=webhook_type,
|
||||
action_object_id=webhooks[0].id
|
||||
),
|
||||
))
|
||||
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(name='Foo', slug='foo'),
|
||||
|
@ -7,6 +7,7 @@ from django.test import TestCase
|
||||
|
||||
from circuits.models import Provider
|
||||
from core.choices import ManagedFileRootPathChoices
|
||||
from core.models import ObjectType
|
||||
from dcim.filtersets import SiteFilterSet
|
||||
from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
|
||||
from dcim.models import Location
|
||||
@ -22,9 +23,10 @@ from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class CustomFieldTestCase(TestCase, BaseFilterSetTests):
|
||||
class CustomFieldTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = CustomField.objects.all()
|
||||
filterset = CustomFieldFilterSet
|
||||
ignore_fields = ('default',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -85,13 +87,23 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
|
||||
ui_editable=CustomFieldUIEditableChoices.HIDDEN,
|
||||
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)
|
||||
custom_fields[0].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'site'))
|
||||
custom_fields[1].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'rack'))
|
||||
custom_fields[2].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device'))
|
||||
custom_fields[3].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device'))
|
||||
custom_fields[4].content_types.add(ContentType.objects.get_by_natural_key('dcim', 'device'))
|
||||
custom_fields[0].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'site'))
|
||||
custom_fields[1].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'rack'))
|
||||
custom_fields[2].object_types.add(ObjectType.objects.get_by_natural_key('dcim', 'device'))
|
||||
custom_fields[3].object_types.add(ObjectType.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):
|
||||
params = {'q': 'foobar1'}
|
||||
@ -101,10 +113,16 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
|
||||
params = {'name': ['Custom Field 1', 'Custom Field 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_content_types(self):
|
||||
params = {'content_types': 'dcim.site'}
|
||||
def test_object_type(self):
|
||||
params = {'object_type': 'dcim.site'}
|
||||
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)
|
||||
|
||||
def test_required(self):
|
||||
@ -138,9 +156,10 @@ class CustomFieldTestCase(TestCase, BaseFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class CustomFieldChoiceSetTestCase(TestCase, BaseFilterSetTests):
|
||||
class CustomFieldChoiceSetTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = CustomFieldChoiceSet.objects.all()
|
||||
filterset = CustomFieldChoiceSetFilterSet
|
||||
ignore_fields = ('extra_choices',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -171,11 +190,10 @@ class CustomFieldChoiceSetTestCase(TestCase, BaseFilterSetTests):
|
||||
class WebhookTestCase(TestCase, BaseFilterSetTests):
|
||||
queryset = Webhook.objects.all()
|
||||
filterset = WebhookFilterSet
|
||||
ignore_fields = ('additional_headers', 'body_template')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
content_types = ContentType.objects.filter(model__in=['region', 'site', 'rack', 'location', 'device'])
|
||||
|
||||
webhooks = (
|
||||
Webhook(
|
||||
name='Webhook 1',
|
||||
@ -237,10 +255,11 @@ class WebhookTestCase(TestCase, BaseFilterSetTests):
|
||||
class EventRuleTestCase(TestCase, BaseFilterSetTests):
|
||||
queryset = EventRule.objects.all()
|
||||
filterset = EventRuleFilterSet
|
||||
ignore_fields = ('action_data', 'conditions')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
content_types = ContentType.objects.filter(
|
||||
object_types = ObjectType.objects.filter(
|
||||
model__in=['region', 'site', 'rack', 'location', 'device']
|
||||
)
|
||||
|
||||
@ -333,11 +352,11 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
|
||||
),
|
||||
)
|
||||
EventRule.objects.bulk_create(event_rules)
|
||||
event_rules[0].content_types.add(content_types[0])
|
||||
event_rules[1].content_types.add(content_types[1])
|
||||
event_rules[2].content_types.add(content_types[2])
|
||||
event_rules[3].content_types.add(content_types[3])
|
||||
event_rules[4].content_types.add(content_types[4])
|
||||
event_rules[0].object_types.add(object_types[0])
|
||||
event_rules[1].object_types.add(object_types[1])
|
||||
event_rules[2].object_types.add(object_types[2])
|
||||
event_rules[3].object_types.add(object_types[3])
|
||||
event_rules[4].object_types.add(object_types[4])
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
@ -351,10 +370,10 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_content_types(self):
|
||||
params = {'content_types': 'dcim.region'}
|
||||
def test_object_type(self):
|
||||
params = {'object_type': 'dcim.region'}
|
||||
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)
|
||||
|
||||
def test_action_type(self):
|
||||
@ -390,13 +409,13 @@ class EventRuleTestCase(TestCase, BaseFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
||||
class CustomLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = CustomLink.objects.all()
|
||||
filterset = CustomLinkFilterSet
|
||||
|
||||
@classmethod
|
||||
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 = (
|
||||
CustomLink(
|
||||
@ -426,7 +445,7 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
||||
)
|
||||
CustomLink.objects.bulk_create(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):
|
||||
params = {'q': 'Custom Link 1'}
|
||||
@ -436,10 +455,10 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
||||
params = {'name': ['Custom Link 1', 'Custom Link 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_content_types(self):
|
||||
params = {'content_types': 'dcim.site'}
|
||||
def test_object_type(self):
|
||||
params = {'object_type': 'dcim.site'}
|
||||
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)
|
||||
|
||||
def test_weight(self):
|
||||
@ -459,13 +478,14 @@ class CustomLinkTestCase(TestCase, BaseFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class SavedFilterTestCase(TestCase, BaseFilterSetTests):
|
||||
class SavedFilterTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = SavedFilter.objects.all()
|
||||
filterset = SavedFilterFilterSet
|
||||
ignore_fields = ('parameters',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
content_types = ContentType.objects.filter(model__in=['site', 'rack', 'device'])
|
||||
object_types = ObjectType.objects.filter(model__in=['site', 'rack', 'device'])
|
||||
|
||||
users = (
|
||||
User(username='User 1'),
|
||||
@ -508,7 +528,7 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests):
|
||||
)
|
||||
SavedFilter.objects.bulk_create(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):
|
||||
params = {'q': 'foobar1'}
|
||||
@ -526,10 +546,10 @@ class SavedFilterTestCase(TestCase, BaseFilterSetTests):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_content_types(self):
|
||||
params = {'content_types': 'dcim.site'}
|
||||
def test_object_type(self):
|
||||
params = {'object_type': 'dcim.site'}
|
||||
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)
|
||||
|
||||
def test_user(self):
|
||||
@ -632,13 +652,14 @@ class BookmarkTestCase(TestCase, BaseFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
|
||||
class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
|
||||
class ExportTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ExportTemplate.objects.all()
|
||||
filterset = ExportTemplateFilterSet
|
||||
ignore_fields = ('template_code', 'data_path')
|
||||
|
||||
@classmethod
|
||||
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 = (
|
||||
ExportTemplate(name='Export Template 1', template_code='TESTING', description='foobar1'),
|
||||
@ -647,7 +668,7 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
|
||||
)
|
||||
ExportTemplate.objects.bulk_create(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):
|
||||
params = {'q': 'foobar1'}
|
||||
@ -657,10 +678,10 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
|
||||
params = {'name': ['Export Template 1', 'Export Template 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_content_types(self):
|
||||
params = {'content_types': 'dcim.site'}
|
||||
def test_object_type(self):
|
||||
params = {'object_type': 'dcim.site'}
|
||||
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)
|
||||
|
||||
def test_description(self):
|
||||
@ -668,9 +689,10 @@ class ExportTemplateTestCase(TestCase, BaseFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
|
||||
class ImageAttachmentTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ImageAttachment.objects.all()
|
||||
filterset = ImageAttachmentFilterSet
|
||||
ignore_fields = ('image',)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -692,7 +714,7 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
|
||||
|
||||
image_attachments = (
|
||||
ImageAttachment(
|
||||
content_type=site_ct,
|
||||
object_type=site_ct,
|
||||
object_id=sites[0].pk,
|
||||
name='Image Attachment 1',
|
||||
image='http://example.com/image1.png',
|
||||
@ -700,7 +722,7 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
|
||||
image_width=100
|
||||
),
|
||||
ImageAttachment(
|
||||
content_type=site_ct,
|
||||
object_type=site_ct,
|
||||
object_id=sites[1].pk,
|
||||
name='Image Attachment 2',
|
||||
image='http://example.com/image2.png',
|
||||
@ -708,7 +730,7 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
|
||||
image_width=100
|
||||
),
|
||||
ImageAttachment(
|
||||
content_type=rack_ct,
|
||||
object_type=rack_ct,
|
||||
object_id=racks[0].pk,
|
||||
name='Image Attachment 3',
|
||||
image='http://example.com/image3.png',
|
||||
@ -716,7 +738,7 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
|
||||
image_width=100
|
||||
),
|
||||
ImageAttachment(
|
||||
content_type=rack_ct,
|
||||
object_type=rack_ct,
|
||||
object_id=racks[1].pk,
|
||||
name='Image Attachment 4',
|
||||
image='http://example.com/image4.png',
|
||||
@ -734,23 +756,17 @@ class ImageAttachmentTestCase(TestCase, BaseFilterSetTests):
|
||||
params = {'name': ['Image Attachment 1', 'Image Attachment 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_content_type(self):
|
||||
params = {'content_type': 'dcim.site'}
|
||||
def test_object_type(self):
|
||||
params = {'object_type': 'dcim.site'}
|
||||
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 = {
|
||||
'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],
|
||||
}
|
||||
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):
|
||||
queryset = JournalEntry.objects.all()
|
||||
@ -858,6 +874,7 @@ class JournalEntryTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ConfigContext.objects.all()
|
||||
filterset = ConfigContextFilterSet
|
||||
ignore_fields = ('data', 'data_path')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -1026,11 +1043,11 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'device_type_id': [device_types[0].pk, device_types[1].pk]}
|
||||
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]
|
||||
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)
|
||||
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)
|
||||
|
||||
def test_platform(self):
|
||||
@ -1081,9 +1098,10 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ConfigTemplateTestCase(TestCase, BaseFilterSetTests):
|
||||
class ConfigTemplateTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ConfigTemplate.objects.all()
|
||||
filterset = ConfigTemplateFilterSet
|
||||
ignore_fields = ('template_code', 'environment_params', 'data_path')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@ -1110,12 +1128,99 @@ class ConfigTemplateTestCase(TestCase, BaseFilterSetTests):
|
||||
class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = Tag.objects.all()
|
||||
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
|
||||
def setUpTestData(cls):
|
||||
content_types = {
|
||||
'site': ContentType.objects.get_by_natural_key('dcim', 'site'),
|
||||
'provider': ContentType.objects.get_by_natural_key('circuits', 'provider'),
|
||||
object_types = {
|
||||
'site': ObjectType.objects.get_by_natural_key('dcim', 'site'),
|
||||
'provider': ObjectType.objects.get_by_natural_key('circuits', 'provider'),
|
||||
}
|
||||
|
||||
tags = (
|
||||
@ -1124,8 +1229,8 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Tag(name='Tag 3', slug='tag-3', color='0000ff'),
|
||||
)
|
||||
Tag.objects.bulk_create(tags)
|
||||
tags[0].object_types.add(content_types['site'])
|
||||
tags[1].object_types.add(content_types['provider'])
|
||||
tags[0].object_types.add(object_types['site'])
|
||||
tags[1].object_types.add(object_types['provider'])
|
||||
|
||||
# Apply some tags so we can filter by content type
|
||||
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)
|
||||
|
||||
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(
|
||||
list(self.filterset(params, self.queryset).qs.values_list('name', flat=True)),
|
||||
['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(
|
||||
list(self.filterset(params, self.queryset).qs.values_list('name', flat=True)),
|
||||
['Tag 2', 'Tag 3']
|
||||
@ -1178,6 +1283,7 @@ class TagTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
class ObjectChangeTestCase(TestCase, BaseFilterSetTests):
|
||||
queryset = ObjectChange.objects.all()
|
||||
filterset = ObjectChangeFilterSet
|
||||
ignore_fields = ('prechange_data', 'postchange_data')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from core.models import ObjectType
|
||||
from dcim.forms import SiteForm
|
||||
from dcim.models import Site
|
||||
from extras.choices import CustomFieldTypeChoices
|
||||
@ -12,66 +12,66 @@ class CustomFieldModelFormTest(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
obj_type = ContentType.objects.get_for_model(Site)
|
||||
object_type = ObjectType.objects.get_for_model(Site)
|
||||
choice_set = CustomFieldChoiceSet.objects.create(
|
||||
name='Choice Set 1',
|
||||
extra_choices=(('a', 'A'), ('b', 'B'), ('c', 'C'))
|
||||
)
|
||||
|
||||
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.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.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.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.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.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.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.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.content_types.set([obj_type])
|
||||
cf_json.object_types.set([object_type])
|
||||
|
||||
cf_select = CustomField.objects.create(
|
||||
name='select',
|
||||
type=CustomFieldTypeChoices.TYPE_SELECT,
|
||||
choice_set=choice_set
|
||||
)
|
||||
cf_select.content_types.set([obj_type])
|
||||
cf_select.object_types.set([object_type])
|
||||
|
||||
cf_multiselect = CustomField.objects.create(
|
||||
name='multiselect',
|
||||
type=CustomFieldTypeChoices.TYPE_MULTISELECT,
|
||||
choice_set=choice_set
|
||||
)
|
||||
cf_multiselect.content_types.set([obj_type])
|
||||
cf_multiselect.object_types.set([object_type])
|
||||
|
||||
cf_object = CustomField.objects.create(
|
||||
name='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(
|
||||
name='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):
|
||||
"""
|
||||
@ -99,7 +99,7 @@ class SavedFilterFormTest(TestCase):
|
||||
form = SavedFilterForm({
|
||||
'name': '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,
|
||||
'parameters': {
|
||||
"status": [
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from core.models import ObjectType
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup
|
||||
from extras.models import ConfigContext, Tag
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
@ -22,7 +22,7 @@ class TagTest(TestCase):
|
||||
|
||||
# Create a Tag that can only be applied to Regions
|
||||
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
|
||||
region.tags.add(tag)
|
||||
|
@ -5,6 +5,7 @@ from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
|
||||
from core.models import ObjectType
|
||||
from dcim.models import DeviceType, Manufacturer, Site
|
||||
from extras.choices import *
|
||||
from extras.models import *
|
||||
@ -19,7 +20,7 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
site_ct = ContentType.objects.get_for_model(Site)
|
||||
site_type = ObjectType.objects.get_for_model(Site)
|
||||
CustomFieldChoiceSet.objects.create(
|
||||
name='Choice Set 1',
|
||||
extra_choices=(
|
||||
@ -36,13 +37,13 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
)
|
||||
for customfield in custom_fields:
|
||||
customfield.save()
|
||||
customfield.content_types.add(site_ct)
|
||||
customfield.object_types.add(site_type)
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'field_x',
|
||||
'label': 'Field X',
|
||||
'type': 'text',
|
||||
'content_types': [site_ct.pk],
|
||||
'object_types': [site_type.pk],
|
||||
'search_weight': 2000,
|
||||
'filter_logic': CustomFieldFilterLogicChoices.FILTER_EXACT,
|
||||
'default': None,
|
||||
@ -53,7 +54,7 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
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',
|
||||
'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',
|
||||
@ -137,7 +138,7 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
site_ct = ContentType.objects.get_for_model(Site)
|
||||
site_type = ObjectType.objects.get_for_model(Site)
|
||||
custom_links = (
|
||||
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'),
|
||||
@ -145,11 +146,11 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
)
|
||||
CustomLink.objects.bulk_create(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 = {
|
||||
'name': 'Custom Link X',
|
||||
'content_types': [site_ct.pk],
|
||||
'object_types': [site_type.pk],
|
||||
'enabled': False,
|
||||
'weight': 100,
|
||||
'button_class': CustomLinkButtonClassChoices.DEFAULT,
|
||||
@ -158,7 +159,7 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
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 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",
|
||||
@ -183,7 +184,7 @@ class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
site_ct = ContentType.objects.get_for_model(Site)
|
||||
site_type = ObjectType.objects.get_for_model(Site)
|
||||
|
||||
users = (
|
||||
User(username='User 1'),
|
||||
@ -217,12 +218,12 @@ class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
)
|
||||
SavedFilter.objects.bulk_create(saved_filters)
|
||||
for i, savedfilter in enumerate(saved_filters):
|
||||
savedfilter.content_types.set([site_ct])
|
||||
savedfilter.object_types.set([site_type])
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Saved Filter X',
|
||||
'slug': 'saved-filter-x',
|
||||
'content_types': [site_ct.pk],
|
||||
'object_types': [site_type.pk],
|
||||
'description': 'Foo',
|
||||
'weight': 1000,
|
||||
'enabled': True,
|
||||
@ -231,7 +232,7 @@ class SavedFilterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
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 5,saved-filter-5,dcim.device,500,True,True,{"foo": "b"}',
|
||||
'Saved Filter 6,saved-filter-6,dcim.device,600,True,True,{"foo": "c"}',
|
||||
@ -302,7 +303,7 @@ class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
|
||||
@classmethod
|
||||
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 %}"""
|
||||
|
||||
export_templates = (
|
||||
@ -312,16 +313,16 @@ class ExportTemplateTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
)
|
||||
ExportTemplate.objects.bulk_create(export_templates)
|
||||
for et in export_templates:
|
||||
et.content_types.set([site_ct])
|
||||
et.object_types.set([site_type])
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Export Template X',
|
||||
'content_types': [site_ct.pk],
|
||||
'object_types': [site_type.pk],
|
||||
'template_code': TEMPLATE_CODE,
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,content_types,template_code",
|
||||
"name,object_types,template_code",
|
||||
f"Export Template 4,dcim.site,{TEMPLATE_CODE}",
|
||||
f"Export Template 5,dcim.site,{TEMPLATE_CODE}",
|
||||
f"Export Template 6,dcim.site,{TEMPLATE_CODE}",
|
||||
@ -396,7 +397,7 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
for webhook in webhooks:
|
||||
webhook.save()
|
||||
|
||||
site_ct = ContentType.objects.get_for_model(Site)
|
||||
site_type = ObjectType.objects.get_for_model(Site)
|
||||
event_rules = (
|
||||
EventRule(name='EventRule 1', type_create=True, action_object=webhooks[0]),
|
||||
EventRule(name='EventRule 2', type_create=True, action_object=webhooks[1]),
|
||||
@ -404,12 +405,12 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
)
|
||||
for event in event_rules:
|
||||
event.save()
|
||||
event.content_types.add(site_ct)
|
||||
event.object_types.add(site_type)
|
||||
|
||||
webhook_ct = ContentType.objects.get_for_model(Webhook)
|
||||
cls.form_data = {
|
||||
'name': 'Event X',
|
||||
'content_types': [site_ct.pk],
|
||||
'object_types': [site_type.pk],
|
||||
'type_create': False,
|
||||
'type_update': True,
|
||||
'type_delete': True,
|
||||
@ -422,7 +423,7 @@ class EventRulesTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
@ -651,7 +652,7 @@ class CustomLinkTest(TestCase):
|
||||
new_window=False
|
||||
)
|
||||
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.save()
|
||||
|
@ -24,7 +24,7 @@ def image_upload(instance, filename):
|
||||
elif 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):
|
||||
|
@ -17,6 +17,7 @@ from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm
|
||||
from extras.dashboard.utils import get_widget_class
|
||||
from netbox.constants import DEFAULT_ACTION_PERMISSIONS
|
||||
from netbox.views import generic
|
||||
from netbox.views.generic.mixins import TableMixin
|
||||
from utilities.forms import ConfirmationForm, get_field_value
|
||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||
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 .models import *
|
||||
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):
|
||||
related_models = ()
|
||||
|
||||
for content_type in instance.content_types.all():
|
||||
for object_type in instance.object_types.all():
|
||||
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}': None})
|
||||
),
|
||||
@ -762,8 +764,8 @@ class ImageAttachmentEditView(generic.ObjectEditView):
|
||||
def alter_object(self, instance, request, args, kwargs):
|
||||
if not instance.pk:
|
||||
# Assign the parent object based on URL kwargs
|
||||
content_type = get_object_or_404(ContentType, pk=request.GET.get('content_type'))
|
||||
instance.parent = get_object_or_404(content_type.model_class(), pk=request.GET.get('object_id'))
|
||||
object_type = get_object_or_404(ContentType, pk=request.GET.get('object_type'))
|
||||
instance.parent = get_object_or_404(object_type.model_class(), pk=request.GET.get('object_id'))
|
||||
return instance
|
||||
|
||||
def get_return_url(self, request, obj=None):
|
||||
@ -771,7 +773,7 @@ class ImageAttachmentEditView(generic.ObjectEditView):
|
||||
|
||||
def get_extra_addanother_params(self, request):
|
||||
return {
|
||||
'content_type': request.GET.get('content_type'),
|
||||
'object_type': request.GET.get('object_type'),
|
||||
'object_id': request.GET.get('object_id'),
|
||||
}
|
||||
|
||||
@ -1143,19 +1145,72 @@ class LegacyScriptRedirectView(ContentTypePermissionRequiredMixin, View):
|
||||
return redirect(f'{url}{path}')
|
||||
|
||||
|
||||
class ScriptResultView(generic.ObjectView):
|
||||
class ScriptResultView(TableMixin, generic.ObjectView):
|
||||
queryset = Job.objects.all()
|
||||
|
||||
def get_required_permission(self):
|
||||
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):
|
||||
table = None
|
||||
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 = {
|
||||
'script': job.object,
|
||||
'job': job,
|
||||
'table': table,
|
||||
}
|
||||
|
||||
if job.data and 'log' in job.data:
|
||||
# Script
|
||||
context['tests'] = job.data.get('tests', {})
|
||||
|
@ -1,510 +1,8 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
|
||||
from ipam.choices import *
|
||||
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
|
||||
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 .serializers_.asns import *
|
||||
from .serializers_.vrfs import *
|
||||
from .serializers_.roles import *
|
||||
from .serializers_.vlans import *
|
||||
from .serializers_.ip import *
|
||||
from .serializers_.fhrpgroups import *
|
||||
from .serializers_.services 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
Loading…
Reference in New Issue
Block a user