Merge branch 'feature' into 14736-htmx

This commit is contained in:
Jeremy Stretch 2024-03-18 16:00:34 -04:00
commit e735be7167
260 changed files with 7007 additions and 4995 deletions

View File

@ -17,15 +17,16 @@ body:
How are you running NetBox? (For issues with the Docker image, please go to the
[netbox-docker](https://github.com/netbox-community/netbox-docker) repo.)
options:
- Self-hosted
- NetBox Cloud
- NetBox Enterprise
- Self-hosted
validations:
required: true
- type: input
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
placeholder: v3.7.3
placeholder: v3.7.4
validations:
required: true
- type: dropdown

View File

@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.7.3
placeholder: v3.7.4
validations:
required: true
- type: dropdown

View File

@ -84,4 +84,4 @@ jobs:
run: coverage run --source="netbox/" netbox/manage.py test netbox/ --parallel
- name: Show coverage report
run: coverage report --skip-covered --omit *migrations*
run: coverage report --skip-covered --omit '*/migrations/*,*/tests/*'

View File

@ -101,7 +101,7 @@ markdown-include
mkdocs-material
# Introspection for embedded code
# https://github.com/mkdocstrings/mkdocstrings/blob/master/CHANGELOG.md
# https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md
mkdocstrings[python-legacy]
# Library for manipulating IP prefixes and addresses

View File

@ -384,7 +384,10 @@
"8gfc-sfpp",
"16gfc-sfpp",
"32gfc-sfp28",
"32gfc-sfpp",
"64gfc-qsfpp",
"64gfc-sfpdd",
"64gfc-sfpp",
"128gfc-qsfp28",
"infiniband-sdr",
"infiniband-ddr",

View File

@ -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()

View File

@ -31,8 +31,7 @@ This section entails the installation and configuration of a local PostgreSQL da
Once PostgreSQL has been installed, start the service and enable it to run at boot:
```no-highlight
sudo systemctl start postgresql
sudo systemctl enable postgresql
sudo systemctl enable --now postgresql
```
Before continuing, verify that you have installed PostgreSQL 12 or later:

View File

@ -14,8 +14,7 @@
```no-highlight
sudo yum install -y redis
sudo systemctl start redis
sudo systemctl enable redis
sudo systemctl enable --now redis
```
Before continuing, verify that your installed version of Redis is at least v4.0:

View File

@ -27,8 +27,7 @@ sudo systemctl daemon-reload
Then, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time:
```no-highlight
sudo systemctl start netbox netbox-rq
sudo systemctl enable netbox netbox-rq
sudo systemctl enable --now netbox netbox-rq
```
You can use the command `systemctl status netbox` to verify that the WSGI service is running:

View File

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

View File

@ -38,7 +38,7 @@ The type of data this field holds. This must be one of the following:
| Object | A single NetBox object of the type defined by `object_type` |
| 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.

View File

@ -1,6 +1,31 @@
# NetBox v3.7
## v3.7.4 (FUTURE)
## v3.7.5 (FUTURE)
---
## v3.7.4 (2024-03-13)
### Enhancements
* [#14206](https://github.com/netbox-community/netbox/issues/14206) - Add additional FibreChannel SFP+ interface types
* [#14366](https://github.com/netbox-community/netbox/issues/14366) - Enable custom links for config contexts & templates
* [#15291](https://github.com/netbox-community/netbox/issues/15291) - Add tunnel termination buttons to VM interfaces table
* [#15297](https://github.com/netbox-community/netbox/issues/15297) - Linkify platform column in device & virtual machine tables
### Bug Fixes
* [#13722](https://github.com/netbox-community/netbox/issues/13722) - Fix range expansion for comma-separated numerical values
* [#14832](https://github.com/netbox-community/netbox/issues/14832) - Enable querying IP addresses for an FHRP group via GraphQL
* [#15220](https://github.com/netbox-community/netbox/issues/15220) - Fix validation check when bulk editing the mask length of IP addresses
* [#15232](https://github.com/netbox-community/netbox/issues/15232) - Permit user with sufficient permissions to assign an inventory item to a device type
* [#15241](https://github.com/netbox-community/netbox/issues/15241) - Restore missing `display` field on VirtualDisk serialization in REST API
* [#15243](https://github.com/netbox-community/netbox/issues/15243) - Correct representation of installed module when listing module bays using REST API brief mode
* [#15316](https://github.com/netbox-community/netbox/issues/15316) - Fix selection of 3DES encryption for IKE & IPSec proposals
* [#15322](https://github.com/netbox-community/netbox/issues/15322) - Add description field to YAML export for device & module types
* [#15336](https://github.com/netbox-community/netbox/issues/15336) - Correct label for recurring scheduled jobs
* [#15347](https://github.com/netbox-community/netbox/issues/15347) - Fix querying virtual machine contacts via GraphQL
* [#15356](https://github.com/netbox-community/netbox/issues/15356) - Fix assignment of front & rear images to device types via REST API
---

View File

@ -34,7 +34,7 @@ The REST API now supports specifying which fields to include in the response dat
* [#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
@ -44,3 +44,37 @@ The REST API now supports specifying which fields to include in the response dat
* [#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`

View File

@ -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')

View File

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

View File

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

View File

@ -67,7 +67,7 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
class Meta:
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():

View File

@ -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):

View File

@ -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
)

View File

@ -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')

View File

View File

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

View File

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

View File

@ -28,7 +28,7 @@ class DataSourceFilterSet(NetBoxModelFilterSet):
class Meta:
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():

View File

@ -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(

View File

@ -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

View File

@ -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()),
],
),
]

View File

@ -1,15 +1,15 @@
from django.contrib.contenttypes.models import ContentType as ContentType_, ContentTypeManager as ContentTypeManager_
from django.contrib.contenttypes.models import ContentType, ContentTypeManager
from django.db.models import Q
from 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

View File

@ -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

View File

@ -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):

View File

@ -6,8 +6,6 @@ from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import WritableNestedSerializer
__all__ = [
'ComponentNestedModuleSerializer',
'ModuleBayNestedModuleSerializer',
'NestedCableSerializer',
'NestedConsolePortSerializer',
'NestedConsolePortTemplateSerializer',
@ -319,18 +317,6 @@ class ModuleBayNestedModuleSerializer(WritableNestedSerializer):
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)
@ -414,11 +400,11 @@ class NestedFrontPortSerializer(WritableNestedSerializer):
class NestedModuleBaySerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
module = NestedModuleSerializer(required=False, read_only=True, allow_null=True)
installed_module = ModuleBayNestedModuleSerializer(required=False, allow_null=True)
class Meta:
model = models.ModuleBay
fields = ['id', 'url', 'display', 'module', 'name']
fields = ['id', 'url', 'display', 'installed_module', 'name']
class NestedDeviceBaySerializer(WritableNestedSerializer):

File diff suppressed because it is too large Load Diff

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,6 @@ from rest_framework.response import Response
from rest_framework.routers import APIRootView
from rest_framework.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

View File

@ -889,7 +889,10 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_8GFC_SFP_PLUS = '8gfc-sfpp'
TYPE_16GFC_SFP_PLUS = '16gfc-sfpp'
TYPE_32GFC_SFP28 = '32gfc-sfp28'
TYPE_32GFC_SFP_PLUS = '32gfc-sfpp'
TYPE_64GFC_QSFP_PLUS = '64gfc-qsfpp'
TYPE_64GFC_SFP_DD = '64gfc-sfpdd'
TYPE_64GFC_SFP_PLUS = '64gfc-sfpp'
TYPE_128GFC_QSFP28 = '128gfc-qsfp28'
# InfiniBand
@ -1058,7 +1061,10 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'),
(TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'),
(TYPE_32GFC_SFP28, 'SFP28 (32GFC)'),
(TYPE_32GFC_SFP_PLUS, 'SFP+ (32GFC)'),
(TYPE_64GFC_QSFP_PLUS, 'QSFP+ (64GFC)'),
(TYPE_64GFC_SFP_DD, 'SFP-DD (64GFC)'),
(TYPE_64GFC_SFP_PLUS, 'SFP+ (64GFC)'),
(TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'),
)
),

View File

@ -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()

View File

@ -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
)

View File

@ -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:

View File

@ -229,15 +229,16 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
'manufacturer': self.manufacturer.name,
'model': self.model,
'slug': self.slug,
'description': self.description,
'default_platform': self.default_platform.name if self.default_platform else None,
'part_number': self.part_number,
'u_height': float(self.u_height),
'is_full_depth': self.is_full_depth,
'subdevice_role': self.subdevice_role,
'airflow': self.airflow,
'comments': self.comments,
'weight': float(self.weight) if self.weight is not None else None,
'weight_unit': self.weight_unit,
'comments': self.comments,
}
# Component templates
@ -415,9 +416,10 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin):
'manufacturer': self.manufacturer.name,
'model': self.model,
'part_number': self.part_number,
'comments': self.comments,
'description': self.description,
'weight': float(self.weight) if self.weight is not None else None,
'weight_unit': self.weight_unit,
'comments': self.comments,
}
# Component templates
@ -815,20 +817,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()

View File

@ -210,6 +210,10 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
linkify=True,
verbose_name=_('Type')
)
platform = tables.Column(
linkify=True,
verbose_name=_('Platform')
)
primary_ip = tables.Column(
linkify=True,
order_by=('primary_ip4', 'primary_ip6'),
@ -294,7 +298,7 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
model = models.Device
fields = (
'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'role', 'manufacturer', 'device_type',
'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'parent_device',
'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'parent_device',
'device_bay_position', 'position', 'face', 'latitude', 'longitude', 'airflow', 'primary_ip', 'primary_ip4',
'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description',
'config_template', 'comments', 'contacts', 'tags', 'created', 'last_updated',

View File

@ -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')

View File

@ -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}

View File

@ -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):

View File

@ -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

View File

@ -1079,7 +1079,7 @@ class DeviceTypeInventoryItemsView(DeviceTypeComponentsView):
tab = ViewTab(
label=_('Inventory Items'),
badge=lambda obj: obj.inventory_item_template_count,
permission='dcim.view_invenotryitemtemplate',
permission='dcim.view_inventoryitemtemplate',
weight=590,
hide_if_empty=True
)

View File

@ -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:

View File

@ -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,

View File

@ -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')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@ router.register('config-contexts', views.ConfigContextViewSet)
router.register('config-templates', views.ConfigTemplateViewSet)
router.register('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 = [

View File

@ -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
#

View File

@ -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]

View File

@ -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]

View File

@ -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():

View File

@ -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(

View File

@ -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(

View File

@ -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
)

View File

@ -7,6 +7,7 @@ from extras.models import ObjectChange
__all__ = (
'ChangelogMixin',
'ConfigContextMixin',
'ContactsMixin',
'CustomFieldsMixin',
'ImageAttachmentsMixin',
'JournalEntriesMixin',

View File

@ -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

View File

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

View File

@ -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):

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ from django.db import models
from django.urls import reverse
from django.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

View File

@ -11,7 +11,7 @@ from extras.querysets import ConfigContextQuerySet
from netbox.config import get_config
from netbox.registry import registry
from netbox.models import ChangeLoggedModel
from netbox.models.features import CloningMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin
from netbox.models.features import CloningMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin
from utilities.jinja2 import ConfigTemplateLoader
from utilities.utils import deepmerge
@ -26,7 +26,7 @@ __all__ = (
# Config contexts
#
class ConfigContext(SyncedDataMixin, CloningMixin, ChangeLoggedModel):
class ConfigContext(SyncedDataMixin, CloningMixin, CustomLinksMixin, ChangeLoggedModel):
"""
A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned
qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B
@ -210,7 +210,7 @@ class ConfigContextModel(models.Model):
# Config templates
#
class ConfigTemplate(SyncedDataMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
class ConfigTemplate(SyncedDataMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
name = models.CharField(
verbose_name=_('name'),
max_length=100

View File

@ -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(),

View File

@ -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)
)

View File

@ -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.")

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