mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
9604 Add Termination to CircuitTermination (#17821)
* 9604 add scope type to CircuitTermination * 9604 add scope type to CircuitTermination * 9604 add scope type to CircuitTermination * 9604 model_forms * 9604 form filtersets * 9604 form bulk_import * 9604 form bulk_edit * 9604 serializers * 9604 graphql * 9604 tests and detail template * 9604 fix migration merge * 9604 fix tests * 9604 fix tests * 9604 fix table * 9604 updates * fix tests * fix tests * fix tests * fix tests * fix tests * fix tests * fix tests * 9604 remove provider_network * 9604 fix tests * 9604 fix tests * 9604 fix forms * 9604 review changes * 9604 scope->termination * 9604 fix _circuit_terminations * 9604 fix _circuit_terminations * 9604 sitegroup -> site_group * 9604 update docs * 9604 fix form termination side reference * Misc cleanup * Fix terminations in circuits table * Fix missing imports * Clean up termination attrs display * Add termination & type to CircuitTerminationTable * Update cable tracing logic --------- Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
This commit is contained in:
parent
f74a9a1c76
commit
a8eb455f3e
@ -21,13 +21,9 @@ Designates the termination as forming either the A or Z end of the circuit.
|
||||
|
||||
If selected, the circuit termination will be considered "connected" even if no cable has been connected to it in NetBox.
|
||||
|
||||
### Site
|
||||
### Termination
|
||||
|
||||
The [site](../dcim/site.md) with which this circuit termination is associated. Once created, a cable can be connected between the circuit termination and a device interface (or similar component).
|
||||
|
||||
### Provider Network
|
||||
|
||||
Circuits which do not connect to a site modeled by NetBox can instead be terminated to a [provider network](./providernetwork.md) representing an unknown network operated by a [provider](./provider.md).
|
||||
The [region](../dcim/region.md), [site group](../dcim/sitegroup.md), [site](../dcim/site.md), [location](../dcim/location.md) or [provider network](./providernetwork.md) with which this circuit termination is associated. Once created, a cable can be connected between the circuit termination and a device interface (or similar component).
|
||||
|
||||
### Port Speed
|
||||
|
||||
|
@ -1,11 +1,16 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices
|
||||
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
from circuits.models import Circuit, CircuitGroup, CircuitGroupAssignment, 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.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
|
||||
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||
from utilities.api import get_serializer_for_model
|
||||
|
||||
from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
|
||||
|
||||
@ -33,16 +38,33 @@ class CircuitTypeSerializer(NetBoxModelSerializer):
|
||||
|
||||
|
||||
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
site = SiteSerializer(nested=True, allow_null=True)
|
||||
termination_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(
|
||||
model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
),
|
||||
allow_null=True,
|
||||
required=False,
|
||||
default=None
|
||||
)
|
||||
termination_id = serializers.IntegerField(allow_null=True, required=False, default=None)
|
||||
termination = serializers.SerializerMethodField(read_only=True)
|
||||
provider_network = ProviderNetworkSerializer(nested=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
||||
'id', 'url', 'display_url', 'display', 'termination_type', 'termination_id', 'termination', 'provider_network', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'description',
|
||||
]
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_termination(self, obj):
|
||||
if obj.termination_id is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.termination)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.termination, nested=True, context=context).data
|
||||
|
||||
|
||||
class CircuitGroupSerializer(NetBoxModelSerializer):
|
||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||
@ -95,18 +117,35 @@ class CircuitSerializer(NetBoxModelSerializer):
|
||||
|
||||
class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||
circuit = CircuitSerializer(nested=True)
|
||||
site = SiteSerializer(nested=True, required=False, allow_null=True)
|
||||
termination_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(
|
||||
model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
),
|
||||
allow_null=True,
|
||||
required=False,
|
||||
default=None
|
||||
)
|
||||
termination_id = serializers.IntegerField(allow_null=True, required=False, default=None)
|
||||
termination = serializers.SerializerMethodField(read_only=True)
|
||||
provider_network = ProviderNetworkSerializer(nested=True, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed',
|
||||
'id', 'url', 'display_url', 'display', 'circuit', 'term_side', 'termination_type', 'termination_id', 'termination', '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')
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_termination(self, obj):
|
||||
if obj.termination_id is None:
|
||||
return None
|
||||
serializer = get_serializer_for_model(obj.termination)
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.termination, nested=True, context=context).data
|
||||
|
||||
|
||||
class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
|
||||
circuit = CircuitSerializer(nested=True)
|
||||
|
4
netbox/circuits/constants.py
Normal file
4
netbox/circuits/constants.py
Normal file
@ -0,0 +1,4 @@
|
||||
# models values for ContentTypes which may be CircuitTermination termination types
|
||||
CIRCUIT_TERMINATION_TERMINATION_TYPES = (
|
||||
'region', 'sitegroup', 'site', 'location', 'providernetwork',
|
||||
)
|
@ -3,11 +3,11 @@ from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from dcim.filtersets import CabledObjectFilterSet
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from dcim.models import Location, Region, Site, SiteGroup
|
||||
from ipam.models import ASN
|
||||
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet
|
||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||
from utilities.filters import TreeNodeMultipleChoiceFilter
|
||||
from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
@ -26,37 +26,37 @@ __all__ = (
|
||||
class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='circuits__terminations__site__region',
|
||||
field_name='circuits__terminations___region',
|
||||
lookup_expr='in',
|
||||
label=_('Region (ID)'),
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='circuits__terminations__site__region',
|
||||
field_name='circuits__terminations___region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Region (slug)'),
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='circuits__terminations__site__group',
|
||||
field_name='circuits__terminations___site_group',
|
||||
lookup_expr='in',
|
||||
label=_('Site group (ID)'),
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='circuits__terminations__site__group',
|
||||
field_name='circuits__terminations___site_group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Site group (slug)'),
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuits__terminations__site',
|
||||
field_name='circuits__terminations___site',
|
||||
queryset=Site.objects.all(),
|
||||
label=_('Site'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='circuits__terminations__site__slug',
|
||||
field_name='circuits__terminations___site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
@ -173,7 +173,7 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
label=_('Provider account (account)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__provider_network',
|
||||
field_name='terminations___provider_network',
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
label=_('Provider network (ID)'),
|
||||
)
|
||||
@ -193,37 +193,37 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
||||
)
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='terminations__site__region',
|
||||
field_name='terminations___region',
|
||||
lookup_expr='in',
|
||||
label=_('Region (ID)'),
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='terminations__site__region',
|
||||
field_name='terminations___region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Region (slug)'),
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='terminations__site__group',
|
||||
field_name='terminations___site_group',
|
||||
lookup_expr='in',
|
||||
label=_('Site group (ID)'),
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='terminations__site__group',
|
||||
field_name='terminations___site_group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Site group (slug)'),
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__site',
|
||||
field_name='terminations___site',
|
||||
queryset=Site.objects.all(),
|
||||
label=_('Site (ID)'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__site__slug',
|
||||
field_name='terminations___site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
@ -263,18 +263,60 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
queryset=Circuit.objects.all(),
|
||||
label=_('Circuit'),
|
||||
)
|
||||
termination_type = ContentTypeFilter()
|
||||
region_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='_region',
|
||||
lookup_expr='in',
|
||||
label=_('Region (ID)'),
|
||||
)
|
||||
region = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Region.objects.all(),
|
||||
field_name='_region',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Region (slug)'),
|
||||
)
|
||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='_site_group',
|
||||
lookup_expr='in',
|
||||
label=_('Site group (ID)'),
|
||||
)
|
||||
site_group = TreeNodeMultipleChoiceFilter(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
field_name='_site_group',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Site group (slug)'),
|
||||
)
|
||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Site.objects.all(),
|
||||
field_name='_site',
|
||||
label=_('Site (ID)'),
|
||||
)
|
||||
site = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='site__slug',
|
||||
field_name='_site__slug',
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Site (slug)'),
|
||||
)
|
||||
location_id = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='_location',
|
||||
lookup_expr='in',
|
||||
label=_('Location (ID)'),
|
||||
)
|
||||
location = TreeNodeMultipleChoiceFilter(
|
||||
queryset=Location.objects.all(),
|
||||
field_name='_location',
|
||||
lookup_expr='in',
|
||||
to_field_name='slug',
|
||||
label=_('Location (slug)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
field_name='_provider_network',
|
||||
label=_('ProviderNetwork (ID)'),
|
||||
)
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
@ -292,7 +334,7 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = (
|
||||
'id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description', 'mark_connected',
|
||||
'id', 'termination_id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description', 'mark_connected',
|
||||
'pp_info', 'cable_end',
|
||||
)
|
||||
|
||||
|
@ -1,17 +1,23 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices
|
||||
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from ipam.models import ASN
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from netbox.forms import NetBoxModelBulkEditForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms import add_blank_choice
|
||||
from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||
from utilities.forms.rendering import FieldSet, TabbedGroups
|
||||
from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, NumberWithOptions
|
||||
from utilities.forms import add_blank_choice, get_field_value
|
||||
from utilities.forms.fields import (
|
||||
ColorField, CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import BulkEditNullBooleanSelect, DatePicker, HTMXSelect, NumberWithOptions
|
||||
from utilities.templatetags.builtins.filters import bettertitle
|
||||
|
||||
__all__ = (
|
||||
'CircuitBulkEditForm',
|
||||
@ -197,15 +203,18 @@ class CircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
label=_('Site'),
|
||||
queryset=Site.objects.all(),
|
||||
required=False
|
||||
termination_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.filter(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
|
||||
widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}),
|
||||
required=False,
|
||||
label=_('Termination type')
|
||||
)
|
||||
provider_network = DynamicModelChoiceField(
|
||||
label=_('Provider Network'),
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
required=False
|
||||
termination = DynamicModelChoiceField(
|
||||
label=_('Termination'),
|
||||
queryset=Site.objects.none(), # Initial queryset
|
||||
required=False,
|
||||
disabled=True,
|
||||
selector=True
|
||||
)
|
||||
port_speed = forms.IntegerField(
|
||||
required=False,
|
||||
@ -225,15 +234,26 @@ class CircuitTerminationBulkEditForm(NetBoxModelBulkEditForm):
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'description',
|
||||
TabbedGroups(
|
||||
FieldSet('site', name=_('Site')),
|
||||
FieldSet('provider_network', name=_('Provider Network')),
|
||||
),
|
||||
'termination_type', 'termination',
|
||||
'mark_connected', name=_('Circuit Termination')
|
||||
),
|
||||
FieldSet('port_speed', 'upstream_speed', name=_('Termination Details')),
|
||||
)
|
||||
nullable_fields = ('description')
|
||||
nullable_fields = ('description', 'termination')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if termination_type_id := get_field_value(self, 'termination_type'):
|
||||
try:
|
||||
termination_type = ContentType.objects.get(pk=termination_type_id)
|
||||
model = termination_type.model_class()
|
||||
self.fields['termination'].queryset = model.objects.all()
|
||||
self.fields['termination'].widget.attrs['selector'] = model._meta.label_lower
|
||||
self.fields['termination'].disabled = False
|
||||
self.fields['termination'].label = _(bettertitle(model._meta.verbose_name))
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
class CircuitGroupBulkEditForm(NetBoxModelBulkEditForm):
|
||||
|
@ -1,13 +1,14 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import *
|
||||
from circuits.constants import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from netbox.forms import NetBoxModelImportForm
|
||||
from tenancy.models import Tenant
|
||||
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
|
||||
from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField
|
||||
|
||||
__all__ = (
|
||||
'CircuitImportForm',
|
||||
@ -127,17 +128,10 @@ class BaseCircuitTerminationImportForm(forms.ModelForm):
|
||||
label=_('Termination'),
|
||||
choices=CircuitTerminationSideChoices,
|
||||
)
|
||||
site = CSVModelChoiceField(
|
||||
label=_('Site'),
|
||||
queryset=Site.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
)
|
||||
provider_network = CSVModelChoiceField(
|
||||
label=_('Provider network'),
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
to_field_name='name',
|
||||
required=False
|
||||
termination_type = CSVContentTypeField(
|
||||
queryset=ContentType.objects.filter(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
|
||||
required=False,
|
||||
label=_('Termination type (app & model)')
|
||||
)
|
||||
|
||||
|
||||
@ -145,9 +139,12 @@ class CircuitTerminationImportRelatedForm(BaseCircuitTerminationImportForm):
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'circuit', 'term_side', 'termination_type', 'termination_id', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'pp_info', 'description'
|
||||
]
|
||||
labels = {
|
||||
'termination_id': _('Termination ID'),
|
||||
}
|
||||
|
||||
|
||||
class CircuitTerminationImportForm(NetBoxModelImportForm, BaseCircuitTerminationImportForm):
|
||||
@ -155,9 +152,12 @@ class CircuitTerminationImportForm(NetBoxModelImportForm, BaseCircuitTermination
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'circuit', 'term_side', 'termination_type', 'termination_id', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||
'pp_info', 'description', 'tags'
|
||||
]
|
||||
labels = {
|
||||
'termination_id': _('Termination ID'),
|
||||
}
|
||||
|
||||
|
||||
class CircuitGroupImportForm(NetBoxModelImportForm):
|
||||
|
@ -3,7 +3,7 @@ from django.utils.translation import gettext as _
|
||||
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitPriorityChoices, CircuitStatusChoices, CircuitTerminationSideChoices
|
||||
from circuits.models import *
|
||||
from dcim.models import Region, Site, SiteGroup
|
||||
from dcim.models import Location, Region, Site, SiteGroup
|
||||
from ipam.models import ASN
|
||||
from netbox.choices import DistanceUnitChoices
|
||||
from netbox.forms import NetBoxModelFilterSetForm
|
||||
@ -207,18 +207,29 @@ class CircuitTerminationFilterForm(NetBoxModelFilterSetForm):
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('circuit_id', 'term_side', name=_('Circuit')),
|
||||
FieldSet('provider_id', 'provider_network_id', name=_('Provider')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', name=_('Location')),
|
||||
FieldSet('provider_id', name=_('Provider')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Termination')),
|
||||
)
|
||||
region_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
label=_('Region')
|
||||
)
|
||||
site_group_id = DynamicModelMultipleChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
label=_('Site group')
|
||||
)
|
||||
site_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'region_id': '$region_id',
|
||||
'site_group_id': '$site_group_id',
|
||||
},
|
||||
label=_('Site')
|
||||
)
|
||||
location_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
label=_('Location')
|
||||
)
|
||||
circuit_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
required=False,
|
||||
|
@ -1,14 +1,19 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices
|
||||
from circuits.constants import *
|
||||
from circuits.models import *
|
||||
from dcim.models import Site
|
||||
from ipam.models import ASN
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
|
||||
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
|
||||
from utilities.forms.widgets import DatePicker, NumberWithOptions
|
||||
from utilities.forms import get_field_value
|
||||
from utilities.forms.fields import CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
|
||||
from utilities.forms.rendering import FieldSet, InlineFields
|
||||
from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions
|
||||
from utilities.templatetags.builtins.filters import bettertitle
|
||||
|
||||
__all__ = (
|
||||
'CircuitForm',
|
||||
@ -144,26 +149,24 @@ class CircuitTerminationForm(NetBoxModelForm):
|
||||
queryset=Circuit.objects.all(),
|
||||
selector=True
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
label=_('Site'),
|
||||
queryset=Site.objects.all(),
|
||||
termination_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.filter(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
|
||||
widget=HTMXSelect(),
|
||||
required=False,
|
||||
selector=True
|
||||
label=_('Termination type')
|
||||
)
|
||||
provider_network = DynamicModelChoiceField(
|
||||
label=_('Provider network'),
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
termination = DynamicModelChoiceField(
|
||||
label=_('Termination'),
|
||||
queryset=Site.objects.none(), # Initial queryset
|
||||
required=False,
|
||||
disabled=True,
|
||||
selector=True
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'circuit', 'term_side', 'description', 'tags',
|
||||
TabbedGroups(
|
||||
FieldSet('site', name=_('Site')),
|
||||
FieldSet('provider_network', name=_('Provider Network')),
|
||||
),
|
||||
'termination_type', 'termination',
|
||||
'mark_connected', name=_('Circuit Termination')
|
||||
),
|
||||
FieldSet('port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', name=_('Termination Details')),
|
||||
@ -172,7 +175,7 @@ class CircuitTerminationForm(NetBoxModelForm):
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'circuit', 'term_side', 'site', 'provider_network', 'mark_connected', 'port_speed', 'upstream_speed',
|
||||
'circuit', 'term_side', 'termination_type', 'mark_connected', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'pp_info', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
@ -184,6 +187,36 @@ class CircuitTerminationForm(NetBoxModelForm):
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
instance = kwargs.get('instance')
|
||||
initial = kwargs.get('initial', {})
|
||||
|
||||
if instance is not None and instance.termination:
|
||||
initial['termination'] = instance.termination
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if termination_type_id := get_field_value(self, 'termination_type'):
|
||||
try:
|
||||
termination_type = ContentType.objects.get(pk=termination_type_id)
|
||||
model = termination_type.model_class()
|
||||
self.fields['termination'].queryset = model.objects.all()
|
||||
self.fields['termination'].widget.attrs['selector'] = model._meta.label_lower
|
||||
self.fields['termination'].disabled = False
|
||||
self.fields['termination'].label = _(bettertitle(model._meta.verbose_name))
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
if self.instance and termination_type_id != self.instance.termination_type_id:
|
||||
self.initial['termination'] = None
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Assign the selected termination (if any)
|
||||
self.instance.termination = self.cleaned_data.get('termination')
|
||||
|
||||
|
||||
class CircuitGroupForm(TenancyForm, NetBoxModelForm):
|
||||
slug = SlugField()
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import Annotated, List
|
||||
from typing import Annotated, List, Union
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
@ -59,13 +59,21 @@ class ProviderNetworkType(NetBoxObjectType):
|
||||
|
||||
@strawberry_django.type(
|
||||
models.CircuitTermination,
|
||||
fields='__all__',
|
||||
exclude=('termination_type', 'termination_id', '_location', '_region', '_site', '_site_group', '_provider_network'),
|
||||
filters=CircuitTerminationFilter
|
||||
)
|
||||
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType):
|
||||
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
|
||||
provider_network: Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')] | None
|
||||
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')] | None
|
||||
|
||||
@strawberry_django.field
|
||||
def termination(self) -> Annotated[Union[
|
||||
Annotated["LocationType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["RegionType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["SiteType", strawberry.lazy('dcim.graphql.types')],
|
||||
Annotated["ProviderNetworkType", strawberry.lazy('circuits.graphql.types')],
|
||||
], strawberry.union("CircuitTerminationTerminationType")] | None:
|
||||
return self.termination
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
|
@ -0,0 +1,56 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def copy_site_assignments(apps, schema_editor):
|
||||
"""
|
||||
Copy site ForeignKey values to the Termination GFK.
|
||||
"""
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||
Site = apps.get_model('dcim', 'Site')
|
||||
|
||||
CircuitTermination.objects.filter(site__isnull=False).update(
|
||||
termination_type=ContentType.objects.get_for_model(Site),
|
||||
termination_id=models.F('site_id')
|
||||
)
|
||||
|
||||
ProviderNetwork = apps.get_model('circuits', 'ProviderNetwork')
|
||||
CircuitTermination.objects.filter(provider_network__isnull=False).update(
|
||||
termination_type=ContentType.objects.get_for_model(ProviderNetwork),
|
||||
termination_id=models.F('provider_network_id')
|
||||
)
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0046_charfield_null_choices'),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('dcim', '0193_poweroutlet_color'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='termination_id',
|
||||
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='termination_type',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
limit_choices_to=models.Q(('model__in', ('region', 'sitegroup', 'site', 'location', 'providernetwork'))),
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='+',
|
||||
to='contenttypes.contenttype',
|
||||
),
|
||||
),
|
||||
|
||||
# Copy over existing site assignments
|
||||
migrations.RunPython(
|
||||
code=copy_site_assignments,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
@ -0,0 +1,90 @@
|
||||
# Generated by Django 5.0.9 on 2024-10-21 17:34
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def populate_denormalized_fields(apps, schema_editor):
|
||||
"""
|
||||
Copy site ForeignKey values to the Termination GFK.
|
||||
"""
|
||||
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||
|
||||
terminations = CircuitTermination.objects.filter(site__isnull=False).prefetch_related('site')
|
||||
for termination in terminations:
|
||||
termination._region_id = termination.site.region_id
|
||||
termination._site_group_id = termination.site.group_id
|
||||
termination._site_id = termination.site_id
|
||||
# Note: Location cannot be set prior to migration
|
||||
|
||||
CircuitTermination.objects.bulk_update(terminations, ['_region', '_site_group', '_site'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0047_circuittermination__termination'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='_location',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
to='dcim.location',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='_region',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
to='dcim.region',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='_site',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
to='dcim.site',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuittermination',
|
||||
name='_site_group',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
to='dcim.sitegroup',
|
||||
),
|
||||
),
|
||||
|
||||
# Populate denormalized FK values
|
||||
migrations.RunPython(
|
||||
code=populate_denormalized_fields,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
|
||||
# Delete the site ForeignKey
|
||||
migrations.RemoveField(
|
||||
model_name='circuittermination',
|
||||
name='site',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='circuittermination',
|
||||
old_name='provider_network',
|
||||
new_name='_provider_network',
|
||||
),
|
||||
]
|
@ -1,9 +1,13 @@
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from circuits.choices import *
|
||||
from circuits.constants import *
|
||||
from dcim.models import CabledObjectModel
|
||||
from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
|
||||
from netbox.models.mixins import DistanceMixin
|
||||
@ -230,22 +234,24 @@ class CircuitTermination(
|
||||
term_side = models.CharField(
|
||||
max_length=1,
|
||||
choices=CircuitTerminationSideChoices,
|
||||
verbose_name=_('termination')
|
||||
verbose_name=_('termination side')
|
||||
)
|
||||
site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
termination_type = models.ForeignKey(
|
||||
to='contenttypes.ContentType',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuit_terminations',
|
||||
limit_choices_to=Q(model__in=CIRCUIT_TERMINATION_TERMINATION_TYPES),
|
||||
related_name='+',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
provider_network = models.ForeignKey(
|
||||
to='circuits.ProviderNetwork',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuit_terminations',
|
||||
termination_id = models.PositiveBigIntegerField(
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
termination = GenericForeignKey(
|
||||
ct_field='termination_type',
|
||||
fk_field='termination_id'
|
||||
)
|
||||
port_speed = models.PositiveIntegerField(
|
||||
verbose_name=_('port speed (Kbps)'),
|
||||
blank=True,
|
||||
@ -276,6 +282,43 @@ class CircuitTermination(
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Cached associations to enable efficient filtering
|
||||
_provider_network = models.ForeignKey(
|
||||
to='circuits.ProviderNetwork',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
_location = models.ForeignKey(
|
||||
to='dcim.Location',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
_site = models.ForeignKey(
|
||||
to='dcim.Site',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
_region = models.ForeignKey(
|
||||
to='dcim.Region',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
_site_group = models.ForeignKey(
|
||||
to='dcim.SiteGroup',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='circuit_terminations',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['circuit', 'term_side']
|
||||
constraints = (
|
||||
@ -297,10 +340,35 @@ class CircuitTermination(
|
||||
super().clean()
|
||||
|
||||
# Must define either site *or* provider network
|
||||
if self.site is None and self.provider_network is None:
|
||||
raise ValidationError(_("A circuit termination must attach to either a site or a provider network."))
|
||||
if self.site and self.provider_network:
|
||||
raise ValidationError(_("A circuit termination cannot attach to both a site and a provider network."))
|
||||
if self.termination is None:
|
||||
raise ValidationError(_("A circuit termination must attach to termination."))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Cache objects associated with the terminating object (for filtering)
|
||||
self.cache_related_objects()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def cache_related_objects(self):
|
||||
self._provider_network = self._region = self._site_group = self._site = self._location = None
|
||||
if self.termination_type:
|
||||
termination_type = self.termination_type.model_class()
|
||||
if termination_type == apps.get_model('dcim', 'region'):
|
||||
self._region = self.termination
|
||||
elif termination_type == apps.get_model('dcim', 'sitegroup'):
|
||||
self._site_group = self.termination
|
||||
elif termination_type == apps.get_model('dcim', 'site'):
|
||||
self._region = self.termination.region
|
||||
self._site_group = self.termination.group
|
||||
self._site = self.termination
|
||||
elif termination_type == apps.get_model('dcim', 'location'):
|
||||
self._region = self.termination.site.region
|
||||
self._site_group = self.termination.site.group
|
||||
self._site = self.termination.site
|
||||
self._location = self.termination
|
||||
elif termination_type == apps.get_model('circuits', 'providernetwork'):
|
||||
self._provider_network = self.termination
|
||||
cache_related_objects.alters_data = True
|
||||
|
||||
def to_objectchange(self, action):
|
||||
objectchange = super().to_objectchange(action)
|
||||
@ -314,7 +382,7 @@ class CircuitTermination(
|
||||
def get_peer_termination(self):
|
||||
peer_side = 'Z' if self.term_side == 'A' else 'A'
|
||||
try:
|
||||
return CircuitTermination.objects.prefetch_related('site').get(
|
||||
return CircuitTermination.objects.prefetch_related('termination').get(
|
||||
circuit=self.circuit,
|
||||
term_side=peer_side
|
||||
)
|
||||
|
@ -18,10 +18,8 @@ __all__ = (
|
||||
|
||||
|
||||
CIRCUITTERMINATION_LINK = """
|
||||
{% if value.site %}
|
||||
<a href="{{ value.site.get_absolute_url }}">{{ value.site }}</a>
|
||||
{% elif value.provider_network %}
|
||||
<a href="{{ value.provider_network.get_absolute_url }}">{{ value.provider_network }}</a>
|
||||
{% if value.termination %}
|
||||
<a href="{{ value.termination.get_absolute_url }}">{{ value.termination }}</a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@ -63,12 +61,12 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
verbose_name=_('Account')
|
||||
)
|
||||
status = columns.ChoiceFieldColumn()
|
||||
termination_a = tables.TemplateColumn(
|
||||
termination_a = columns.TemplateColumn(
|
||||
template_code=CIRCUITTERMINATION_LINK,
|
||||
orderable=False,
|
||||
verbose_name=_('Side A')
|
||||
)
|
||||
termination_z = tables.TemplateColumn(
|
||||
termination_z = columns.TemplateColumn(
|
||||
template_code=CIRCUITTERMINATION_LINK,
|
||||
orderable=False,
|
||||
verbose_name=_('Side Z')
|
||||
@ -110,22 +108,54 @@ class CircuitTerminationTable(NetBoxTable):
|
||||
linkify=True,
|
||||
accessor='circuit.provider'
|
||||
)
|
||||
term_side = tables.Column(
|
||||
verbose_name=_('Side')
|
||||
)
|
||||
termination_type = columns.ContentTypeColumn(
|
||||
verbose_name=_('Termination Type'),
|
||||
)
|
||||
termination = tables.Column(
|
||||
verbose_name=_('Termination Point'),
|
||||
linkify=True
|
||||
)
|
||||
|
||||
# Termination types
|
||||
site = tables.Column(
|
||||
verbose_name=_('Site'),
|
||||
linkify=True
|
||||
linkify=True,
|
||||
accessor='_site'
|
||||
)
|
||||
site_group = tables.Column(
|
||||
verbose_name=_('Site Group'),
|
||||
linkify=True,
|
||||
accessor='_sitegroup'
|
||||
)
|
||||
region = tables.Column(
|
||||
verbose_name=_('Region'),
|
||||
linkify=True,
|
||||
accessor='_region'
|
||||
)
|
||||
location = tables.Column(
|
||||
verbose_name=_('Location'),
|
||||
linkify=True,
|
||||
accessor='_location'
|
||||
)
|
||||
provider_network = tables.Column(
|
||||
verbose_name=_('Provider Network'),
|
||||
linkify=True
|
||||
linkify=True,
|
||||
accessor='_provider_network'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = CircuitTermination
|
||||
fields = (
|
||||
'pk', 'id', 'circuit', 'provider', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'pp_info', 'description', 'created', 'last_updated', 'actions',
|
||||
'pk', 'id', 'circuit', 'provider', 'term_side', 'termination_type', 'termination', 'site_group', 'region',
|
||||
'site', 'location', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
|
||||
'description', 'created', 'last_updated', 'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'id', 'circuit', 'provider', 'term_side', 'termination_type', 'termination', 'description',
|
||||
)
|
||||
default_columns = ('pk', 'id', 'circuit', 'provider', 'term_side', 'description')
|
||||
|
||||
|
||||
class CircuitGroupTable(NetBoxTable):
|
||||
|
@ -181,10 +181,10 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
circuit_terminations = (
|
||||
CircuitTermination(circuit=circuits[0], term_side=SIDE_A, site=sites[0]),
|
||||
CircuitTermination(circuit=circuits[0], term_side=SIDE_Z, provider_network=provider_networks[0]),
|
||||
CircuitTermination(circuit=circuits[1], term_side=SIDE_A, site=sites[1]),
|
||||
CircuitTermination(circuit=circuits[1], term_side=SIDE_Z, provider_network=provider_networks[1]),
|
||||
CircuitTermination(circuit=circuits[0], term_side=SIDE_A, termination=sites[0]),
|
||||
CircuitTermination(circuit=circuits[0], term_side=SIDE_Z, termination=provider_networks[0]),
|
||||
CircuitTermination(circuit=circuits[1], term_side=SIDE_A, termination=sites[1]),
|
||||
CircuitTermination(circuit=circuits[1], term_side=SIDE_Z, termination=provider_networks[1]),
|
||||
)
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
|
||||
@ -192,13 +192,15 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
||||
{
|
||||
'circuit': circuits[2].pk,
|
||||
'term_side': SIDE_A,
|
||||
'site': sites[0].pk,
|
||||
'termination_type': 'dcim.site',
|
||||
'termination_id': sites[0].pk,
|
||||
'port_speed': 200000,
|
||||
},
|
||||
{
|
||||
'circuit': circuits[2].pk,
|
||||
'term_side': SIDE_Z,
|
||||
'provider_network': provider_networks[0].pk,
|
||||
'termination_type': 'circuits.providernetwork',
|
||||
'termination_id': provider_networks[0].pk,
|
||||
'port_speed': 200000,
|
||||
},
|
||||
]
|
||||
|
@ -70,10 +70,12 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
CircuitTermination.objects.bulk_create((
|
||||
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[0], term_side='A'),
|
||||
))
|
||||
circuit_terminations = (
|
||||
CircuitTermination(circuit=circuits[0], termination=sites[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[1], termination=sites[0], term_side='A'),
|
||||
)
|
||||
for ct in circuit_terminations:
|
||||
ct.save()
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
@ -233,14 +235,15 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
circuit_terminations = ((
|
||||
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[3], provider_network=provider_networks[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[4], provider_network=provider_networks[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[5], provider_network=provider_networks[2], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[0], termination=sites[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[1], termination=sites[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[2], termination=sites[2], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[3], termination=provider_networks[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[4], termination=provider_networks[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[5], termination=provider_networks[2], term_side='A'),
|
||||
))
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
for ct in circuit_terminations:
|
||||
ct.save()
|
||||
|
||||
def test_q(self):
|
||||
params = {'q': 'foobar1'}
|
||||
@ -384,18 +387,19 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
circuit_terminations = ((
|
||||
CircuitTermination(circuit=circuits[0], site=sites[0], term_side='A', port_speed=1000, upstream_speed=1000, xconnect_id='ABC', description='foobar1'),
|
||||
CircuitTermination(circuit=circuits[0], site=sites[1], term_side='Z', port_speed=1000, upstream_speed=1000, xconnect_id='DEF', description='foobar2'),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[1], term_side='A', port_speed=2000, upstream_speed=2000, xconnect_id='GHI'),
|
||||
CircuitTermination(circuit=circuits[1], site=sites[2], term_side='Z', port_speed=2000, upstream_speed=2000, xconnect_id='JKL'),
|
||||
CircuitTermination(circuit=circuits[2], site=sites[2], term_side='A', port_speed=3000, upstream_speed=3000, xconnect_id='MNO'),
|
||||
CircuitTermination(circuit=circuits[2], site=sites[0], term_side='Z', port_speed=3000, upstream_speed=3000, xconnect_id='PQR'),
|
||||
CircuitTermination(circuit=circuits[3], provider_network=provider_networks[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[4], provider_network=provider_networks[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[5], provider_network=provider_networks[2], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[6], provider_network=provider_networks[0], term_side='A', mark_connected=True),
|
||||
CircuitTermination(circuit=circuits[0], termination=sites[0], term_side='A', port_speed=1000, upstream_speed=1000, xconnect_id='ABC', description='foobar1'),
|
||||
CircuitTermination(circuit=circuits[0], termination=sites[1], term_side='Z', port_speed=1000, upstream_speed=1000, xconnect_id='DEF', description='foobar2'),
|
||||
CircuitTermination(circuit=circuits[1], termination=sites[1], term_side='A', port_speed=2000, upstream_speed=2000, xconnect_id='GHI'),
|
||||
CircuitTermination(circuit=circuits[1], termination=sites[2], term_side='Z', port_speed=2000, upstream_speed=2000, xconnect_id='JKL'),
|
||||
CircuitTermination(circuit=circuits[2], termination=sites[2], term_side='A', port_speed=3000, upstream_speed=3000, xconnect_id='MNO'),
|
||||
CircuitTermination(circuit=circuits[2], termination=sites[0], term_side='Z', port_speed=3000, upstream_speed=3000, xconnect_id='PQR'),
|
||||
CircuitTermination(circuit=circuits[3], termination=provider_networks[0], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[4], termination=provider_networks[1], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[5], termination=provider_networks[2], term_side='A'),
|
||||
CircuitTermination(circuit=circuits[6], termination=provider_networks[0], term_side='A', mark_connected=True),
|
||||
))
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
for ct in circuit_terminations:
|
||||
ct.save()
|
||||
|
||||
Cable(a_terminations=[circuit_terminations[0]], b_terminations=[circuit_terminations[1]]).save()
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import datetime
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
@ -190,27 +191,31 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
|
||||
def test_bulk_import_objects_with_terminations(self):
|
||||
json_data = """
|
||||
site = Site.objects.first()
|
||||
json_data = f"""
|
||||
[
|
||||
{
|
||||
{{
|
||||
"cid": "Circuit 7",
|
||||
"provider": "Provider 1",
|
||||
"type": "Circuit Type 1",
|
||||
"status": "active",
|
||||
"description": "Testing Import",
|
||||
"terminations": [
|
||||
{
|
||||
{{
|
||||
"term_side": "A",
|
||||
"site": "Site 1"
|
||||
},
|
||||
{
|
||||
"termination_type": "dcim.site",
|
||||
"termination_id": "{site.pk}"
|
||||
}},
|
||||
{{
|
||||
"term_side": "Z",
|
||||
"site": "Site 1"
|
||||
}
|
||||
"termination_type": "dcim.site",
|
||||
"termination_id": "{site.pk}"
|
||||
}}
|
||||
]
|
||||
}
|
||||
}}
|
||||
]
|
||||
"""
|
||||
|
||||
initial_count = self._get_queryset().count()
|
||||
data = {
|
||||
'data': json_data,
|
||||
@ -336,7 +341,7 @@ class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
|
||||
class CircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
class TestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = CircuitTermination
|
||||
|
||||
@classmethod
|
||||
@ -359,24 +364,27 @@ class CircuitTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
circuit_terminations = (
|
||||
CircuitTermination(circuit=circuits[0], term_side='A', site=sites[0]),
|
||||
CircuitTermination(circuit=circuits[0], term_side='Z', site=sites[1]),
|
||||
CircuitTermination(circuit=circuits[1], term_side='A', site=sites[0]),
|
||||
CircuitTermination(circuit=circuits[1], term_side='Z', site=sites[1]),
|
||||
CircuitTermination(circuit=circuits[0], term_side='A', termination=sites[0]),
|
||||
CircuitTermination(circuit=circuits[0], term_side='Z', termination=sites[1]),
|
||||
CircuitTermination(circuit=circuits[1], term_side='A', termination=sites[0]),
|
||||
CircuitTermination(circuit=circuits[1], term_side='Z', termination=sites[1]),
|
||||
)
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
for ct in circuit_terminations:
|
||||
ct.save()
|
||||
|
||||
cls.form_data = {
|
||||
'circuit': circuits[2].pk,
|
||||
'term_side': 'A',
|
||||
'site': sites[2].pk,
|
||||
'termination_type': ContentType.objects.get_for_model(Site).pk,
|
||||
'termination': sites[2].pk,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
site = sites[0].pk
|
||||
cls.csv_data = (
|
||||
"circuit,term_side,site,description",
|
||||
"Circuit 3,A,Site 1,Foo",
|
||||
"Circuit 3,Z,Site 1,Bar",
|
||||
"circuit,term_side,termination_type,termination_id,description",
|
||||
f"Circuit 3,A,dcim.site,{site},Foo",
|
||||
f"Circuit 3,Z,dcim.site,{site},Bar",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
|
@ -158,7 +158,7 @@ class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
instance,
|
||||
extra=(
|
||||
(
|
||||
Circuit.objects.restrict(request.user, 'view').filter(terminations__provider_network=instance),
|
||||
Circuit.objects.restrict(request.user, 'view').filter(terminations___provider_network=instance),
|
||||
'provider_network_id',
|
||||
),
|
||||
),
|
||||
@ -257,8 +257,7 @@ class CircuitTypeBulkDeleteView(generic.BulkDeleteView):
|
||||
|
||||
class CircuitListView(generic.ObjectListView):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'tenant__group', 'termination_a__site', 'termination_z__site',
|
||||
'termination_a__provider_network', 'termination_z__provider_network',
|
||||
'tenant__group', 'termination_a__termination', 'termination_z__termination',
|
||||
)
|
||||
filterset = filtersets.CircuitFilterSet
|
||||
filterset_form = forms.CircuitFilterForm
|
||||
@ -298,8 +297,7 @@ class CircuitBulkImportView(generic.BulkImportView):
|
||||
|
||||
class CircuitBulkEditView(generic.BulkEditView):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'termination_a__site', 'termination_z__site',
|
||||
'termination_a__provider_network', 'termination_z__provider_network',
|
||||
'tenant__group', 'termination_a__termination', 'termination_z__termination',
|
||||
)
|
||||
filterset = filtersets.CircuitFilterSet
|
||||
table = tables.CircuitTable
|
||||
@ -308,8 +306,7 @@ class CircuitBulkEditView(generic.BulkEditView):
|
||||
|
||||
class CircuitBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'termination_a__site', 'termination_z__site',
|
||||
'termination_a__provider_network', 'termination_z__provider_network',
|
||||
'tenant__group', 'termination_a__termination', 'termination_z__termination',
|
||||
)
|
||||
filterset = filtersets.CircuitFilterSet
|
||||
table = tables.CircuitTable
|
||||
|
@ -462,6 +462,10 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, Organi
|
||||
devices: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
|
||||
children: List[Annotated["LocationType", strawberry.lazy('dcim.graphql.types')]]
|
||||
|
||||
@strawberry_django.field
|
||||
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
|
||||
return self.circuit_terminations.all()
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.Manufacturer,
|
||||
@ -705,6 +709,10 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
|
||||
def parent(self) -> Annotated["RegionType", strawberry.lazy('dcim.graphql.types')] | None:
|
||||
return self.parent
|
||||
|
||||
@strawberry_django.field
|
||||
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
|
||||
return self.circuit_terminations.all()
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.Site,
|
||||
@ -726,10 +734,13 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje
|
||||
devices: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]]
|
||||
locations: List[Annotated["LocationType", strawberry.lazy('dcim.graphql.types')]]
|
||||
asns: List[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]]
|
||||
circuit_terminations: List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]
|
||||
clusters: List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]
|
||||
vlans: List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]]
|
||||
|
||||
@strawberry_django.field
|
||||
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
|
||||
return self.circuit_terminations.all()
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.SiteGroup,
|
||||
@ -746,6 +757,10 @@ class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
|
||||
def parent(self) -> Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')] | None:
|
||||
return self.parent
|
||||
|
||||
@strawberry_django.field
|
||||
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
|
||||
return self.circuit_terminations.all()
|
||||
|
||||
|
||||
@strawberry_django.type(
|
||||
models.VirtualChassis,
|
||||
|
@ -344,7 +344,7 @@ class CableTermination(ChangeLoggedModel):
|
||||
)
|
||||
|
||||
# A CircuitTermination attached to a ProviderNetwork cannot have a Cable
|
||||
if self.termination_type.model == 'circuittermination' and self.termination.provider_network is not None:
|
||||
if self.termination_type.model == 'circuittermination' and self.termination._provider_network is not None:
|
||||
raise ValidationError(_("Circuit terminations attached to a provider network may not be cabled."))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
@ -690,19 +690,19 @@ class CablePath(models.Model):
|
||||
).first()
|
||||
if circuit_termination is None:
|
||||
break
|
||||
elif circuit_termination.provider_network:
|
||||
elif circuit_termination._provider_network:
|
||||
# Circuit terminates to a ProviderNetwork
|
||||
path.extend([
|
||||
[object_to_path_node(circuit_termination)],
|
||||
[object_to_path_node(circuit_termination.provider_network)],
|
||||
[object_to_path_node(circuit_termination._provider_network)],
|
||||
])
|
||||
is_complete = True
|
||||
break
|
||||
elif circuit_termination.site and not circuit_termination.cable:
|
||||
# Circuit terminates to a Site
|
||||
elif circuit_termination.termination and not circuit_termination.cable:
|
||||
# Circuit terminates to a Region/Site/etc.
|
||||
path.extend([
|
||||
[object_to_path_node(circuit_termination)],
|
||||
[object_to_path_node(circuit_termination.site)],
|
||||
[object_to_path_node(circuit_termination.termination)],
|
||||
])
|
||||
break
|
||||
|
||||
|
@ -1167,7 +1167,7 @@ class CablePathTestCase(TestCase):
|
||||
[IF1] --C1-- [CT1]
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
|
||||
|
||||
# Create cable 1
|
||||
cable1 = Cable(
|
||||
@ -1198,7 +1198,7 @@ class CablePathTestCase(TestCase):
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
|
||||
|
||||
# Create cable 1
|
||||
cable1 = Cable(
|
||||
@ -1214,7 +1214,7 @@ class CablePathTestCase(TestCase):
|
||||
)
|
||||
|
||||
# Create CT2
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='Z')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='Z')
|
||||
|
||||
# Check for partial path to site
|
||||
self.assertPathExists(
|
||||
@ -1266,7 +1266,7 @@ class CablePathTestCase(TestCase):
|
||||
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
|
||||
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
|
||||
interface4 = Interface.objects.create(device=self.device, name='Interface 4')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
|
||||
|
||||
# Create cable 1
|
||||
cable1 = Cable(
|
||||
@ -1282,7 +1282,7 @@ class CablePathTestCase(TestCase):
|
||||
)
|
||||
|
||||
# Create CT2
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='Z')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='Z')
|
||||
|
||||
# Check for partial path to site
|
||||
self.assertPathExists(
|
||||
@ -1335,8 +1335,8 @@ class CablePathTestCase(TestCase):
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
site2 = Site.objects.create(name='Site 2', slug='site-2')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=site2, term_side='Z')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, termination=site2, term_side='Z')
|
||||
|
||||
# Create cable 1
|
||||
cable1 = Cable(
|
||||
@ -1365,8 +1365,8 @@ class CablePathTestCase(TestCase):
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
providernetwork = ProviderNetwork.objects.create(name='Provider Network 1', provider=self.circuit.provider)
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, provider_network=providernetwork, term_side='Z')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, termination=providernetwork, term_side='Z')
|
||||
|
||||
# Create cable 1
|
||||
cable1 = Cable(
|
||||
@ -1413,8 +1413,8 @@ class CablePathTestCase(TestCase):
|
||||
frontport2_2 = FrontPort.objects.create(
|
||||
device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2
|
||||
)
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='Z')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='Z')
|
||||
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
@ -1499,10 +1499,10 @@ class CablePathTestCase(TestCase):
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
|
||||
circuit2 = Circuit.objects.create(provider=self.circuit.provider, type=self.circuit.type, cid='Circuit 2')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='Z')
|
||||
circuittermination3 = CircuitTermination.objects.create(circuit=circuit2, site=self.site, term_side='A')
|
||||
circuittermination4 = CircuitTermination.objects.create(circuit=circuit2, site=self.site, term_side='Z')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='A')
|
||||
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, termination=self.site, term_side='Z')
|
||||
circuittermination3 = CircuitTermination.objects.create(circuit=circuit2, termination=self.site, term_side='A')
|
||||
circuittermination4 = CircuitTermination.objects.create(circuit=circuit2, termination=self.site, term_side='Z')
|
||||
|
||||
# Create cables
|
||||
cable1 = Cable(
|
||||
|
@ -5135,7 +5135,7 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
circuit = Circuit.objects.create(cid='Circuit 1', provider=provider, type=circuit_type)
|
||||
circuit_termination = CircuitTermination.objects.create(circuit=circuit, term_side='A', site=sites[0])
|
||||
circuit_termination = CircuitTermination.objects.create(circuit=circuit, term_side='A', termination=sites[0])
|
||||
|
||||
# Cables
|
||||
cables = (
|
||||
@ -5308,9 +5308,9 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
def test_site(self):
|
||||
site = Site.objects.all()[:2]
|
||||
params = {'site_id': [site[0].pk, site[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 12)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 11)
|
||||
params = {'site': [site[0].slug, site[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 12)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 11)
|
||||
|
||||
def test_tenant(self):
|
||||
tenant = Tenant.objects.all()[:2]
|
||||
|
@ -762,9 +762,9 @@ class CableTestCase(TestCase):
|
||||
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||
circuit1 = Circuit.objects.create(provider=provider, type=circuittype, cid='1')
|
||||
circuit2 = Circuit.objects.create(provider=provider, type=circuittype, cid='2')
|
||||
CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='A')
|
||||
CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='Z')
|
||||
CircuitTermination.objects.create(circuit=circuit2, provider_network=provider_network, term_side='A')
|
||||
CircuitTermination.objects.create(circuit=circuit1, termination=site, term_side='A')
|
||||
CircuitTermination.objects.create(circuit=circuit1, termination=site, term_side='Z')
|
||||
CircuitTermination.objects.create(circuit=circuit2, termination=provider_network, term_side='A')
|
||||
|
||||
def test_cable_creation(self):
|
||||
"""
|
||||
|
@ -242,6 +242,10 @@ class RegionView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
extra=(
|
||||
(Location.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
|
||||
(Rack.objects.restrict(request.user, 'view').filter(site__region__in=regions), 'region_id'),
|
||||
(
|
||||
Circuit.objects.restrict(request.user, 'view').filter(terminations___region=instance).distinct(),
|
||||
'region_id'
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
@ -324,6 +328,10 @@ class SiteGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
extra=(
|
||||
(Location.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
|
||||
(Rack.objects.restrict(request.user, 'view').filter(site__group__in=groups), 'site_group_id'),
|
||||
(
|
||||
Circuit.objects.restrict(request.user, 'view').filter(terminations___site_group=instance).distinct(),
|
||||
'site_group_id'
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
@ -404,8 +412,10 @@ class SiteView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
scope_id=instance.pk
|
||||
), 'site'),
|
||||
(ASN.objects.restrict(request.user, 'view').filter(sites=instance), 'site_id'),
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(terminations__site=instance).distinct(),
|
||||
'site_id'),
|
||||
(
|
||||
Circuit.objects.restrict(request.user, 'view').filter(terminations___site=instance).distinct(),
|
||||
'site_id'
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
@ -475,7 +485,17 @@ class LocationView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
def get_extra_context(self, request, instance):
|
||||
locations = instance.get_descendants(include_self=True)
|
||||
return {
|
||||
'related_models': self.get_related_models(request, locations, [CableTermination]),
|
||||
'related_models': self.get_related_models(
|
||||
request,
|
||||
locations,
|
||||
[CableTermination],
|
||||
(
|
||||
(
|
||||
Circuit.objects.restrict(request.user, 'view').filter(terminations___location=instance).distinct(),
|
||||
'location_id'
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,18 +1,19 @@
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% if termination.site %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Site" %}</th>
|
||||
<th scope="row">{% trans "Termination point" %}</th>
|
||||
{% if termination.termination %}
|
||||
<td>
|
||||
{% if termination.site.region %}
|
||||
{{ termination.site.region|linkify }} /
|
||||
{% endif %}
|
||||
{{ termination.site|linkify }}
|
||||
{{ termination.termination|linkify }}
|
||||
<div class="fs-5 text-muted">{% trans termination.termination_type.name|bettertitle %}</div>
|
||||
</td>
|
||||
{% else %}
|
||||
<td>{{ ''|placeholder }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Termination" %}</th>
|
||||
<th scope="row">{% trans "Connection" %}</th>
|
||||
<td>
|
||||
{% if termination.mark_connected %}
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
|
||||
@ -57,12 +58,6 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider Network" %}</th>
|
||||
<td>{{ termination.provider_network.provider|linkify }} / {{ termination.provider_network|linkify }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Speed" %}</th>
|
||||
<td>
|
||||
|
Loading…
Reference in New Issue
Block a user