mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
* Rename circuit to member on CircuitGroupAssignment * Support group assignment for virtual circuits * Update release notes * Introduce separate nav menu heading for circuit groups * Add generic relations for group assignments * Remove obsolete code * Clean up bulk import & extend tests
This commit is contained in:
parent
6f4bec7644
commit
c3b0de3ebd
@ -8,9 +8,9 @@ Circuits can be assigned to [circuit groups](./circuitgroup.md) for correlation
|
|||||||
|
|
||||||
The [circuit group](./circuitgroup.md) being assigned.
|
The [circuit group](./circuitgroup.md) being assigned.
|
||||||
|
|
||||||
### Circuit
|
### Member
|
||||||
|
|
||||||
The [circuit](./circuit.md) that is being assigned to the group.
|
The [circuit](./circuit.md) or [virtual circuit](./virtualcircuit.md) assigned to the group.
|
||||||
|
|
||||||
### Priority
|
### Priority
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
* The `site` and `provider_network` foreign key fields on `circuits.CircuitTermination` have been replaced by the `termination` generic foreign key.
|
* The `site` and `provider_network` foreign key fields on `circuits.CircuitTermination` have been replaced by the `termination` generic foreign key.
|
||||||
* The `site` foreign key field on `ipam.Prefix` has been replaced by the `scope` generic foreign key.
|
* The `site` foreign key field on `ipam.Prefix` has been replaced by the `scope` generic foreign key.
|
||||||
* The `site` foreign key field on `virtualization.Cluster` has been replaced by the `scope` generic foreign key.
|
* The `site` foreign key field on `virtualization.Cluster` has been replaced by the `scope` generic foreign key.
|
||||||
|
* The `circuit` foreign key field on `circuits.CircuitGroupAssignment` has been replaced by the `member` generic foreign key.
|
||||||
* Obsolete nested REST API serializers have been removed. These were deprecated in NetBox v4.1 under [#17143](https://github.com/netbox-community/netbox/issues/17143).
|
* Obsolete nested REST API serializers have been removed. These were deprecated in NetBox v4.1 under [#17143](https://github.com/netbox-community/netbox/issues/17143).
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
@ -77,6 +78,8 @@ NetBox now supports the designation of customer VLANs (CVLANs) and service VLANs
|
|||||||
* `/api/ipam/vlan-translation-rules/`
|
* `/api/ipam/vlan-translation-rules/`
|
||||||
* circuits.Circuit
|
* circuits.Circuit
|
||||||
* Added the optional `distance` and `distance_unit` fields
|
* Added the optional `distance` and `distance_unit` fields
|
||||||
|
* circuits.CircuitGroupAssignment
|
||||||
|
* Replaced the `circuit` field with `member_type` and `member_id` to support virtual circuit assignment
|
||||||
* circuits.CircuitTermination
|
* circuits.CircuitTermination
|
||||||
* Removed the `site` & `provider_network` fields
|
* Removed the `site` & `provider_network` fields
|
||||||
* Added the `termination_type` & `termination_id` fields to facilitate termination assignment
|
* Added the `termination_type` & `termination_id` fields to facilitate termination assignment
|
||||||
|
@ -3,7 +3,7 @@ from drf_spectacular.utils import extend_schema_field
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
|
from circuits.choices import CircuitPriorityChoices, CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
|
||||||
from circuits.constants import CIRCUIT_TERMINATION_TERMINATION_TYPES
|
from circuits.constants import CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS, CIRCUIT_TERMINATION_TERMINATION_TYPES
|
||||||
from circuits.models import (
|
from circuits.models import (
|
||||||
Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType, VirtualCircuit,
|
Circuit, CircuitGroup, CircuitGroupAssignment, CircuitTermination, CircuitType, VirtualCircuit,
|
||||||
VirtualCircuitTermination,
|
VirtualCircuitTermination,
|
||||||
@ -15,7 +15,6 @@ from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializ
|
|||||||
from netbox.choices import DistanceUnitChoices
|
from netbox.choices import DistanceUnitChoices
|
||||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
|
|
||||||
from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
|
from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -154,14 +153,26 @@ class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer
|
|||||||
|
|
||||||
|
|
||||||
class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
|
class CircuitGroupAssignmentSerializer(CircuitGroupAssignmentSerializer_):
|
||||||
circuit = CircuitSerializer(nested=True)
|
member_type = ContentTypeField(
|
||||||
|
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS)
|
||||||
|
)
|
||||||
|
member = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitGroupAssignment
|
model = CircuitGroupAssignment
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'group', 'circuit', 'priority', 'tags', 'created', 'last_updated',
|
'id', 'url', 'display_url', 'display', 'group', 'member_type', 'member_id', 'member', 'priority', 'tags',
|
||||||
|
'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'group', 'circuit', 'priority')
|
brief_fields = ('id', 'url', 'display', 'group', 'member_type', 'member_id', 'member', 'priority')
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
|
def get_member(self, obj):
|
||||||
|
if obj.member_id is None:
|
||||||
|
return None
|
||||||
|
serializer = get_serializer_for_model(obj.member)
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
return serializer(obj.member, nested=True, context=context).data
|
||||||
|
|
||||||
|
|
||||||
class VirtualCircuitSerializer(NetBoxModelSerializer):
|
class VirtualCircuitSerializer(NetBoxModelSerializer):
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
|
||||||
# models values for ContentTypes which may be CircuitTermination termination types
|
# models values for ContentTypes which may be CircuitTermination termination types
|
||||||
CIRCUIT_TERMINATION_TERMINATION_TYPES = (
|
CIRCUIT_TERMINATION_TERMINATION_TYPES = (
|
||||||
'region', 'sitegroup', 'site', 'location', 'providernetwork',
|
'region', 'sitegroup', 'site', 'location', 'providernetwork',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS = Q(
|
||||||
|
app_label='circuits',
|
||||||
|
model__in=['circuit', 'virtualcircuit']
|
||||||
|
)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
@ -7,7 +8,9 @@ from dcim.models import Interface, Location, Region, Site, SiteGroup
|
|||||||
from ipam.models import ASN
|
from ipam.models import ASN
|
||||||
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet
|
from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet
|
||||||
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
|
||||||
from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter
|
from utilities.filters import (
|
||||||
|
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, TreeNodeMultipleChoiceFilter,
|
||||||
|
)
|
||||||
from .choices import *
|
from .choices import *
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
@ -365,26 +368,36 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label=_('Search'),
|
label=_('Search'),
|
||||||
)
|
)
|
||||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
member_type = ContentTypeFilter()
|
||||||
field_name='circuit__provider',
|
circuit = MultiValueCharFilter(
|
||||||
queryset=Provider.objects.all(),
|
method='filter_circuit',
|
||||||
label=_('Provider (ID)'),
|
field_name='cid',
|
||||||
|
label=_('Circuit (CID)'),
|
||||||
)
|
)
|
||||||
provider = django_filters.ModelMultipleChoiceFilter(
|
circuit_id = MultiValueNumberFilter(
|
||||||
field_name='circuit__provider__slug',
|
method='filter_circuit',
|
||||||
queryset=Provider.objects.all(),
|
field_name='pk',
|
||||||
to_field_name='slug',
|
|
||||||
label=_('Provider (slug)'),
|
|
||||||
)
|
|
||||||
circuit_id = django_filters.ModelMultipleChoiceFilter(
|
|
||||||
queryset=Circuit.objects.all(),
|
|
||||||
label=_('Circuit (ID)'),
|
label=_('Circuit (ID)'),
|
||||||
)
|
)
|
||||||
circuit = django_filters.ModelMultipleChoiceFilter(
|
virtual_circuit = MultiValueCharFilter(
|
||||||
field_name='circuit__cid',
|
method='filter_virtual_circuit',
|
||||||
queryset=Circuit.objects.all(),
|
field_name='cid',
|
||||||
to_field_name='cid',
|
label=_('Virtual circuit (CID)'),
|
||||||
label=_('Circuit (CID)'),
|
)
|
||||||
|
virtual_circuit_id = MultiValueNumberFilter(
|
||||||
|
method='filter_virtual_circuit',
|
||||||
|
field_name='pk',
|
||||||
|
label=_('Virtual circuit (ID)'),
|
||||||
|
)
|
||||||
|
provider = MultiValueCharFilter(
|
||||||
|
method='filter_provider',
|
||||||
|
field_name='slug',
|
||||||
|
label=_('Provider (name)'),
|
||||||
|
)
|
||||||
|
provider_id = MultiValueNumberFilter(
|
||||||
|
method='filter_provider',
|
||||||
|
field_name='pk',
|
||||||
|
label=_('Provider (ID)'),
|
||||||
)
|
)
|
||||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=CircuitGroup.objects.all(),
|
queryset=CircuitGroup.objects.all(),
|
||||||
@ -399,16 +412,55 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitGroupAssignment
|
model = CircuitGroupAssignment
|
||||||
fields = ('id', 'priority')
|
fields = ('id', 'member_id', 'priority')
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(circuit__cid__icontains=value) |
|
Q(member__cid__icontains=value) |
|
||||||
Q(group__name__icontains=value)
|
Q(group__name__icontains=value)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def filter_circuit(self, queryset, name, value):
|
||||||
|
circuits = Circuit.objects.filter(**{f'{name}__in': value})
|
||||||
|
if not circuits.exists():
|
||||||
|
return queryset.none()
|
||||||
|
return queryset.filter(
|
||||||
|
Q(
|
||||||
|
member_type=ContentType.objects.get_for_model(Circuit),
|
||||||
|
member_id__in=circuits
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def filter_virtual_circuit(self, queryset, name, value):
|
||||||
|
virtual_circuits = VirtualCircuit.objects.filter(**{f'{name}__in': value})
|
||||||
|
if not virtual_circuits.exists():
|
||||||
|
return queryset.none()
|
||||||
|
return queryset.filter(
|
||||||
|
Q(
|
||||||
|
member_type=ContentType.objects.get_for_model(VirtualCircuit),
|
||||||
|
member_id__in=virtual_circuits
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def filter_provider(self, queryset, name, value):
|
||||||
|
providers = Provider.objects.filter(**{f'{name}__in': value})
|
||||||
|
if not providers.exists():
|
||||||
|
return queryset.none()
|
||||||
|
circuits = Circuit.objects.filter(provider__in=providers)
|
||||||
|
virtual_circuits = VirtualCircuit.objects.filter(provider_network__provider__in=providers)
|
||||||
|
return queryset.filter(
|
||||||
|
Q(
|
||||||
|
member_type=ContentType.objects.get_for_model(Circuit),
|
||||||
|
member_id__in=circuits
|
||||||
|
) |
|
||||||
|
Q(
|
||||||
|
member_type=ContentType.objects.get_for_model(VirtualCircuit),
|
||||||
|
member_id__in=virtual_circuits
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class VirtualCircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
class VirtualCircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
@ -279,7 +279,7 @@ class CircuitGroupBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
|
|
||||||
|
|
||||||
class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
|
class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
circuit = DynamicModelChoiceField(
|
member = DynamicModelChoiceField(
|
||||||
label=_('Circuit'),
|
label=_('Circuit'),
|
||||||
queryset=Circuit.objects.all(),
|
queryset=Circuit.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
@ -292,7 +292,7 @@ class CircuitGroupAssignmentBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
|
|
||||||
model = CircuitGroupAssignment
|
model = CircuitGroupAssignment
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('circuit', 'priority'),
|
FieldSet('member', 'priority'),
|
||||||
)
|
)
|
||||||
nullable_fields = ('priority',)
|
nullable_fields = ('priority',)
|
||||||
|
|
||||||
|
@ -179,10 +179,19 @@ class CircuitGroupImportForm(NetBoxModelImportForm):
|
|||||||
|
|
||||||
|
|
||||||
class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
|
class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
|
||||||
|
member_type = CSVContentTypeField(
|
||||||
|
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS),
|
||||||
|
label=_('Circuit type (app & model)')
|
||||||
|
)
|
||||||
|
priority = CSVChoiceField(
|
||||||
|
label=_('Priority'),
|
||||||
|
choices=CircuitPriorityChoices,
|
||||||
|
required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitGroupAssignment
|
model = CircuitGroupAssignment
|
||||||
fields = ('circuit', 'group', 'priority')
|
fields = ('member_type', 'member_id', 'group', 'priority')
|
||||||
|
|
||||||
|
|
||||||
class VirtualCircuitImportForm(NetBoxModelImportForm):
|
class VirtualCircuitImportForm(NetBoxModelImportForm):
|
||||||
|
@ -277,14 +277,14 @@ class CircuitGroupAssignmentFilterForm(NetBoxModelFilterSetForm):
|
|||||||
model = CircuitGroupAssignment
|
model = CircuitGroupAssignment
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('provider_id', 'circuit_id', 'group_id', 'priority', name=_('Assignment')),
|
FieldSet('provider_id', 'member_id', 'group_id', 'priority', name=_('Assignment')),
|
||||||
)
|
)
|
||||||
provider_id = DynamicModelMultipleChoiceField(
|
provider_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Provider.objects.all(),
|
queryset=Provider.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Provider')
|
label=_('Provider')
|
||||||
)
|
)
|
||||||
circuit_id = DynamicModelMultipleChoiceField(
|
member_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Circuit.objects.all(),
|
queryset=Circuit.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Circuit')
|
label=_('Circuit')
|
||||||
|
@ -251,18 +251,59 @@ class CircuitGroupAssignmentForm(NetBoxModelForm):
|
|||||||
label=_('Group'),
|
label=_('Group'),
|
||||||
queryset=CircuitGroup.objects.all(),
|
queryset=CircuitGroup.objects.all(),
|
||||||
)
|
)
|
||||||
circuit = DynamicModelChoiceField(
|
member_type = ContentTypeChoiceField(
|
||||||
|
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS),
|
||||||
|
widget=HTMXSelect(),
|
||||||
|
required=False,
|
||||||
|
label=_('Circuit type')
|
||||||
|
)
|
||||||
|
member = DynamicModelChoiceField(
|
||||||
label=_('Circuit'),
|
label=_('Circuit'),
|
||||||
queryset=Circuit.objects.all(),
|
queryset=Circuit.objects.none(), # Initial queryset
|
||||||
|
required=False,
|
||||||
|
disabled=True,
|
||||||
selector=True
|
selector=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
FieldSet('group', 'member_type', 'member', 'priority', 'tags', name=_('Group Assignment')),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitGroupAssignment
|
model = CircuitGroupAssignment
|
||||||
fields = [
|
fields = [
|
||||||
'group', 'circuit', 'priority', 'tags',
|
'group', 'member_type', 'priority', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
instance = kwargs.get('instance')
|
||||||
|
initial = kwargs.get('initial', {})
|
||||||
|
|
||||||
|
if instance is not None and instance.member:
|
||||||
|
initial['member'] = instance.member
|
||||||
|
kwargs['initial'] = initial
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if member_type_id := get_field_value(self, 'member_type'):
|
||||||
|
try:
|
||||||
|
model = ContentType.objects.get(pk=member_type_id).model_class()
|
||||||
|
self.fields['member'].queryset = model.objects.all()
|
||||||
|
self.fields['member'].widget.attrs['selector'] = model._meta.label_lower
|
||||||
|
self.fields['member'].disabled = False
|
||||||
|
self.fields['member'].label = _(bettertitle(model._meta.verbose_name))
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self.instance.pk and member_type_id != self.instance.member_type_id:
|
||||||
|
self.initial['member'] = None
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Assign the selected circuit (if any)
|
||||||
|
self.instance.member = self.cleaned_data.get('member')
|
||||||
|
|
||||||
|
|
||||||
class VirtualCircuitForm(TenancyForm, NetBoxModelForm):
|
class VirtualCircuitForm(TenancyForm, NetBoxModelForm):
|
||||||
provider_network = DynamicModelChoiceField(
|
provider_network = DynamicModelChoiceField(
|
||||||
|
@ -116,12 +116,18 @@ class CircuitGroupType(OrganizationalObjectType):
|
|||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
models.CircuitGroupAssignment,
|
models.CircuitGroupAssignment,
|
||||||
fields='__all__',
|
exclude=('member_type', 'member_id'),
|
||||||
filters=CircuitGroupAssignmentFilter
|
filters=CircuitGroupAssignmentFilter
|
||||||
)
|
)
|
||||||
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
|
class CircuitGroupAssignmentType(TagsMixin, BaseObjectType):
|
||||||
group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')]
|
group: Annotated["CircuitGroupType", strawberry.lazy('circuits.graphql.types')]
|
||||||
circuit: Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]
|
|
||||||
|
@strawberry_django.field
|
||||||
|
def member(self) -> Annotated[Union[
|
||||||
|
Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')],
|
||||||
|
Annotated["VirtualCircuitType", strawberry.lazy('circuits.graphql.types')],
|
||||||
|
], strawberry.union("CircuitGroupAssignmentMemberType")] | None:
|
||||||
|
return self.member
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def set_member_type(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Set member_type on any existing CircuitGroupAssignments to the content type for Circuit.
|
||||||
|
"""
|
||||||
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
|
Circuit = apps.get_model('circuits', 'Circuit')
|
||||||
|
CircuitGroupAssignment = apps.get_model('circuits', 'CircuitGroupAssignment')
|
||||||
|
|
||||||
|
CircuitGroupAssignment.objects.update(
|
||||||
|
member_type=ContentType.objects.get_for_model(Circuit)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('circuits', '0050_virtual_circuits'),
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('extras', '0122_charfield_null_choices'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveConstraint(
|
||||||
|
model_name='circuitgroupassignment',
|
||||||
|
name='circuits_circuitgroupassignment_unique_circuit_group',
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='circuitgroupassignment',
|
||||||
|
options={'ordering': ('group', 'member_type', 'member_id', 'priority', 'pk')},
|
||||||
|
),
|
||||||
|
|
||||||
|
# Change member_id to an integer field for the member GFK
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='circuitgroupassignment',
|
||||||
|
old_name='circuit',
|
||||||
|
new_name='member_id',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='circuitgroupassignment',
|
||||||
|
name='member_id',
|
||||||
|
field=models.PositiveBigIntegerField(),
|
||||||
|
),
|
||||||
|
|
||||||
|
# Add content type pointer for the member GFK
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='circuitgroupassignment',
|
||||||
|
name='member_type',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
limit_choices_to=models.Q(('app_label', 'circuits'), ('model__in', ['circuit', 'virtualcircuit'])),
|
||||||
|
related_name='+',
|
||||||
|
to='contenttypes.contenttype',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
|
||||||
|
# Populate member_type for any existing assignments
|
||||||
|
migrations.RunPython(code=set_member_type, reverse_code=migrations.RunPython.noop),
|
||||||
|
|
||||||
|
# Disallow null values for member_type
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='circuitgroupassignment',
|
||||||
|
name='member_type',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
limit_choices_to=models.Q(('app_label', 'circuits'), ('model__in', ['circuit', 'virtualcircuit'])),
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name='+',
|
||||||
|
to='contenttypes.contenttype'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='circuitgroupassignment',
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=('member_type', 'member_id', 'group'),
|
||||||
|
name='circuits_circuitgroupassignment_unique_member_group'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -1,8 +1,7 @@
|
|||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
@ -117,6 +116,13 @@ class Circuit(ContactsMixin, ImageAttachmentsMixin, DistanceMixin, PrimaryModel)
|
|||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
group_assignments = GenericRelation(
|
||||||
|
to='circuits.CircuitGroupAssignment',
|
||||||
|
content_type_field='member_type',
|
||||||
|
object_id_field='member_id',
|
||||||
|
related_query_name='circuit'
|
||||||
|
)
|
||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate',
|
'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate',
|
||||||
'description',
|
'description',
|
||||||
@ -177,15 +183,23 @@ class CircuitGroup(OrganizationalModel):
|
|||||||
|
|
||||||
class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
|
class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel):
|
||||||
"""
|
"""
|
||||||
Assignment of a Circuit to a CircuitGroup with an optional priority.
|
Assignment of a physical or virtual circuit to a CircuitGroup with an optional priority.
|
||||||
"""
|
"""
|
||||||
circuit = models.ForeignKey(
|
member_type = models.ForeignKey(
|
||||||
Circuit,
|
to='contenttypes.ContentType',
|
||||||
on_delete=models.CASCADE,
|
limit_choices_to=CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS,
|
||||||
related_name='assignments'
|
on_delete=models.PROTECT,
|
||||||
|
related_name='+'
|
||||||
|
)
|
||||||
|
member_id = models.PositiveBigIntegerField(
|
||||||
|
verbose_name=_('member ID')
|
||||||
|
)
|
||||||
|
member = GenericForeignKey(
|
||||||
|
ct_field='member_type',
|
||||||
|
fk_field='member_id'
|
||||||
)
|
)
|
||||||
group = models.ForeignKey(
|
group = models.ForeignKey(
|
||||||
CircuitGroup,
|
to='circuits.CircuitGroup',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='assignments'
|
related_name='assignments'
|
||||||
)
|
)
|
||||||
@ -197,16 +211,15 @@ class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin,
|
|||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
prerequisite_models = (
|
prerequisite_models = (
|
||||||
'circuits.Circuit',
|
|
||||||
'circuits.CircuitGroup',
|
'circuits.CircuitGroup',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('group', 'circuit', 'priority', 'pk')
|
ordering = ('group', 'member_type', 'member_id', 'priority', 'pk')
|
||||||
constraints = (
|
constraints = (
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(
|
||||||
fields=('circuit', 'group'),
|
fields=('member_type', 'member_id', 'group'),
|
||||||
name='%(app_label)s_%(class)s_unique_circuit_group'
|
name='%(app_label)s_%(class)s_unique_member_group'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
verbose_name = _('Circuit group assignment')
|
verbose_name = _('Circuit group assignment')
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -50,6 +51,13 @@ class VirtualCircuit(PrimaryModel):
|
|||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
group_assignments = GenericRelation(
|
||||||
|
to='circuits.CircuitGroupAssignment',
|
||||||
|
content_type_field='member_type',
|
||||||
|
object_id_field='member_id',
|
||||||
|
related_query_name='virtual_circuit'
|
||||||
|
)
|
||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'provider_network', 'provider_account', 'status', 'tenant', 'description',
|
'provider_network', 'provider_account', 'status', 'tenant', 'description',
|
||||||
)
|
)
|
||||||
|
@ -188,11 +188,14 @@ class CircuitGroupAssignmentTable(NetBoxTable):
|
|||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
provider = tables.Column(
|
provider = tables.Column(
|
||||||
accessor='circuit__provider',
|
accessor='member__provider',
|
||||||
verbose_name=_('Provider'),
|
verbose_name=_('Provider'),
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
circuit = tables.Column(
|
member_type = columns.ContentTypeColumn(
|
||||||
|
verbose_name=_('Type')
|
||||||
|
)
|
||||||
|
member = tables.Column(
|
||||||
verbose_name=_('Circuit'),
|
verbose_name=_('Circuit'),
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -206,6 +209,7 @@ class CircuitGroupAssignmentTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = CircuitGroupAssignment
|
model = CircuitGroupAssignment
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'group', 'provider', 'circuit', 'priority', 'created', 'last_updated', 'actions', 'tags',
|
'pk', 'id', 'group', 'provider', 'member_type', 'member', 'priority', 'created', 'last_updated', 'actions',
|
||||||
|
'tags',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'group', 'provider', 'circuit', 'priority')
|
default_columns = ('pk', 'group', 'provider', 'member_type', 'member', 'priority')
|
||||||
|
@ -295,7 +295,7 @@ class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
|
class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = CircuitGroupAssignment
|
model = CircuitGroupAssignment
|
||||||
brief_fields = ['circuit', 'display', 'group', 'id', 'priority', 'url']
|
brief_fields = ['display', 'group', 'id', 'member', 'member_id', 'member_type', 'priority', 'url']
|
||||||
bulk_update_data = {
|
bulk_update_data = {
|
||||||
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
||||||
}
|
}
|
||||||
@ -330,17 +330,17 @@ class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
|
|||||||
assignments = (
|
assignments = (
|
||||||
CircuitGroupAssignment(
|
CircuitGroupAssignment(
|
||||||
group=circuit_groups[0],
|
group=circuit_groups[0],
|
||||||
circuit=circuits[0],
|
member=circuits[0],
|
||||||
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
||||||
),
|
),
|
||||||
CircuitGroupAssignment(
|
CircuitGroupAssignment(
|
||||||
group=circuit_groups[1],
|
group=circuit_groups[1],
|
||||||
circuit=circuits[1],
|
member=circuits[1],
|
||||||
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
||||||
),
|
),
|
||||||
CircuitGroupAssignment(
|
CircuitGroupAssignment(
|
||||||
group=circuit_groups[2],
|
group=circuit_groups[2],
|
||||||
circuit=circuits[2],
|
member=circuits[2],
|
||||||
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
|
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -349,17 +349,20 @@ class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
|
|||||||
cls.create_data = [
|
cls.create_data = [
|
||||||
{
|
{
|
||||||
'group': circuit_groups[3].pk,
|
'group': circuit_groups[3].pk,
|
||||||
'circuit': circuits[3].pk,
|
'member_type': 'circuits.circuit',
|
||||||
|
'member_id': circuits[3].pk,
|
||||||
'priority': CircuitPriorityChoices.PRIORITY_PRIMARY,
|
'priority': CircuitPriorityChoices.PRIORITY_PRIMARY,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'group': circuit_groups[4].pk,
|
'group': circuit_groups[4].pk,
|
||||||
'circuit': circuits[4].pk,
|
'member_type': 'circuits.circuit',
|
||||||
|
'member_id': circuits[4].pk,
|
||||||
'priority': CircuitPriorityChoices.PRIORITY_SECONDARY,
|
'priority': CircuitPriorityChoices.PRIORITY_SECONDARY,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'group': circuit_groups[5].pk,
|
'group': circuit_groups[5].pk,
|
||||||
'circuit': circuits[5].pk,
|
'member_type': 'circuits.circuit',
|
||||||
|
'member_id': circuits[5].pk,
|
||||||
'priority': CircuitPriorityChoices.PRIORITY_TERTIARY,
|
'priority': CircuitPriorityChoices.PRIORITY_TERTIARY,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -648,7 +648,6 @@ class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
CircuitGroup(name='Circuit Group 1', slug='circuit-group-1'),
|
CircuitGroup(name='Circuit Group 1', slug='circuit-group-1'),
|
||||||
CircuitGroup(name='Circuit Group 2', slug='circuit-group-2'),
|
CircuitGroup(name='Circuit Group 2', slug='circuit-group-2'),
|
||||||
CircuitGroup(name='Circuit Group 3', slug='circuit-group-3'),
|
CircuitGroup(name='Circuit Group 3', slug='circuit-group-3'),
|
||||||
CircuitGroup(name='Circuit Group 4', slug='circuit-group-4'),
|
|
||||||
)
|
)
|
||||||
CircuitGroup.objects.bulk_create(circuit_groups)
|
CircuitGroup.objects.bulk_create(circuit_groups)
|
||||||
|
|
||||||
@ -656,7 +655,6 @@ class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Provider(name='Provider 1', slug='provider-1'),
|
Provider(name='Provider 1', slug='provider-1'),
|
||||||
Provider(name='Provider 2', slug='provider-2'),
|
Provider(name='Provider 2', slug='provider-2'),
|
||||||
Provider(name='Provider 3', slug='provider-3'),
|
Provider(name='Provider 3', slug='provider-3'),
|
||||||
Provider(name='Provider 4', slug='provider-4'),
|
|
||||||
))
|
))
|
||||||
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
|
||||||
|
|
||||||
@ -664,35 +662,72 @@ class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Circuit(cid='Circuit 1', provider=providers[0], type=circuittype),
|
Circuit(cid='Circuit 1', provider=providers[0], type=circuittype),
|
||||||
Circuit(cid='Circuit 2', provider=providers[1], type=circuittype),
|
Circuit(cid='Circuit 2', provider=providers[1], type=circuittype),
|
||||||
Circuit(cid='Circuit 3', provider=providers[2], type=circuittype),
|
Circuit(cid='Circuit 3', provider=providers[2], type=circuittype),
|
||||||
Circuit(cid='Circuit 4', provider=providers[3], type=circuittype),
|
|
||||||
)
|
)
|
||||||
Circuit.objects.bulk_create(circuits)
|
Circuit.objects.bulk_create(circuits)
|
||||||
|
|
||||||
|
provider_networks = (
|
||||||
|
ProviderNetwork(name='Provider Network 1', provider=providers[0]),
|
||||||
|
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
|
||||||
|
ProviderNetwork(name='Provider Network 3', provider=providers[2]),
|
||||||
|
)
|
||||||
|
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||||
|
|
||||||
|
virtual_circuits = (
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_networks[0],
|
||||||
|
cid='Virtual Circuit 1'
|
||||||
|
),
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_networks[1],
|
||||||
|
cid='Virtual Circuit 2'
|
||||||
|
),
|
||||||
|
VirtualCircuit(
|
||||||
|
provider_network=provider_networks[2],
|
||||||
|
cid='Virtual Circuit 3'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
VirtualCircuit.objects.bulk_create(virtual_circuits)
|
||||||
|
|
||||||
assignments = (
|
assignments = (
|
||||||
CircuitGroupAssignment(
|
CircuitGroupAssignment(
|
||||||
group=circuit_groups[0],
|
group=circuit_groups[0],
|
||||||
circuit=circuits[0],
|
member=circuits[0],
|
||||||
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
||||||
),
|
),
|
||||||
CircuitGroupAssignment(
|
CircuitGroupAssignment(
|
||||||
group=circuit_groups[1],
|
group=circuit_groups[1],
|
||||||
circuit=circuits[1],
|
member=circuits[1],
|
||||||
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
||||||
),
|
),
|
||||||
CircuitGroupAssignment(
|
CircuitGroupAssignment(
|
||||||
group=circuit_groups[2],
|
group=circuit_groups[2],
|
||||||
circuit=circuits[2],
|
member=circuits[2],
|
||||||
|
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
|
||||||
|
),
|
||||||
|
CircuitGroupAssignment(
|
||||||
|
group=circuit_groups[0],
|
||||||
|
member=virtual_circuits[0],
|
||||||
|
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
||||||
|
),
|
||||||
|
CircuitGroupAssignment(
|
||||||
|
group=circuit_groups[1],
|
||||||
|
member=virtual_circuits[1],
|
||||||
|
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
||||||
|
),
|
||||||
|
CircuitGroupAssignment(
|
||||||
|
group=circuit_groups[2],
|
||||||
|
member=virtual_circuits[2],
|
||||||
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
|
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
CircuitGroupAssignment.objects.bulk_create(assignments)
|
CircuitGroupAssignment.objects.bulk_create(assignments)
|
||||||
|
|
||||||
def test_group_id(self):
|
def test_group(self):
|
||||||
groups = CircuitGroup.objects.filter(name__in=['Circuit Group 1', 'Circuit Group 2'])
|
groups = CircuitGroup.objects.all()[:2]
|
||||||
params = {'group_id': [groups[0].pk, groups[1].pk]}
|
params = {'group_id': [groups[0].pk, groups[1].pk]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
params = {'group': [groups[0].slug, groups[1].slug]}
|
params = {'group': [groups[0].slug, groups[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
def test_circuit(self):
|
def test_circuit(self):
|
||||||
circuits = Circuit.objects.all()[:2]
|
circuits = Circuit.objects.all()[:2]
|
||||||
@ -701,12 +736,19 @@ class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'circuit': [circuits[0].cid, circuits[1].cid]}
|
params = {'circuit': [circuits[0].cid, circuits[1].cid]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_virtual_circuit(self):
|
||||||
|
virtual_circuits = VirtualCircuit.objects.all()[:2]
|
||||||
|
params = {'virtual_circuit_id': [virtual_circuits[0].pk, virtual_circuits[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'virtual_circuit': [virtual_circuits[0].cid, virtual_circuits[1].cid]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_provider(self):
|
def test_provider(self):
|
||||||
providers = Provider.objects.all()[:2]
|
providers = Provider.objects.all()[:2]
|
||||||
params = {'provider_id': [providers[0].pk, providers[1].pk]}
|
params = {'provider_id': [providers[0].pk, providers[1].pk]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
params = {'provider': [providers[0].slug, providers[1].slug]}
|
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||||
|
|
||||||
|
|
||||||
class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
|
@ -468,6 +468,7 @@ class CircuitGroupAssignmentTestCase(
|
|||||||
ViewTestCases.DeleteObjectViewTestCase,
|
ViewTestCases.DeleteObjectViewTestCase,
|
||||||
ViewTestCases.ListObjectsViewTestCase,
|
ViewTestCases.ListObjectsViewTestCase,
|
||||||
ViewTestCases.BulkEditObjectsViewTestCase,
|
ViewTestCases.BulkEditObjectsViewTestCase,
|
||||||
|
ViewTestCases.BulkImportObjectsViewTestCase,
|
||||||
ViewTestCases.BulkDeleteObjectsViewTestCase
|
ViewTestCases.BulkDeleteObjectsViewTestCase
|
||||||
):
|
):
|
||||||
model = CircuitGroupAssignment
|
model = CircuitGroupAssignment
|
||||||
@ -497,17 +498,17 @@ class CircuitGroupAssignmentTestCase(
|
|||||||
assignments = (
|
assignments = (
|
||||||
CircuitGroupAssignment(
|
CircuitGroupAssignment(
|
||||||
group=circuit_groups[0],
|
group=circuit_groups[0],
|
||||||
circuit=circuits[0],
|
member=circuits[0],
|
||||||
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
priority=CircuitPriorityChoices.PRIORITY_PRIMARY
|
||||||
),
|
),
|
||||||
CircuitGroupAssignment(
|
CircuitGroupAssignment(
|
||||||
group=circuit_groups[1],
|
group=circuit_groups[1],
|
||||||
circuit=circuits[1],
|
member=circuits[1],
|
||||||
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
priority=CircuitPriorityChoices.PRIORITY_SECONDARY
|
||||||
),
|
),
|
||||||
CircuitGroupAssignment(
|
CircuitGroupAssignment(
|
||||||
group=circuit_groups[2],
|
group=circuit_groups[2],
|
||||||
circuit=circuits[2],
|
member=circuits[2],
|
||||||
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
|
priority=CircuitPriorityChoices.PRIORITY_TERTIARY
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -517,11 +518,26 @@ class CircuitGroupAssignmentTestCase(
|
|||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'group': circuit_groups[3].pk,
|
'group': circuit_groups[3].pk,
|
||||||
'circuit': circuits[3].pk,
|
'member_type': ContentType.objects.get_for_model(Circuit).pk,
|
||||||
|
'member': circuits[3].pk,
|
||||||
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
||||||
'tags': [t.pk for t in tags],
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cls.csv_data = (
|
||||||
|
"member_type,member_id,group,priority",
|
||||||
|
f"circuits.circuit,{circuits[0].pk},{circuit_groups[3].pk},primary",
|
||||||
|
f"circuits.circuit,{circuits[1].pk},{circuit_groups[3].pk},secondary",
|
||||||
|
f"circuits.circuit,{circuits[2].pk},{circuit_groups[3].pk},tertiary",
|
||||||
|
)
|
||||||
|
|
||||||
|
cls.csv_update_data = (
|
||||||
|
"id,priority",
|
||||||
|
f"{assignments[0].pk},inactive",
|
||||||
|
f"{assignments[1].pk},inactive",
|
||||||
|
f"{assignments[2].pk},inactive",
|
||||||
|
)
|
||||||
|
|
||||||
cls.bulk_edit_data = {
|
cls.bulk_edit_data = {
|
||||||
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
|
||||||
}
|
}
|
||||||
|
@ -279,8 +279,6 @@ CIRCUITS_MENU = Menu(
|
|||||||
items=(
|
items=(
|
||||||
get_model_item('circuits', 'circuit', _('Circuits')),
|
get_model_item('circuits', 'circuit', _('Circuits')),
|
||||||
get_model_item('circuits', 'circuittype', _('Circuit Types')),
|
get_model_item('circuits', 'circuittype', _('Circuit Types')),
|
||||||
get_model_item('circuits', 'circuitgroup', _('Circuit Groups')),
|
|
||||||
get_model_item('circuits', 'circuitgroupassignment', _('Group Assignments')),
|
|
||||||
get_model_item('circuits', 'circuittermination', _('Circuit Terminations')),
|
get_model_item('circuits', 'circuittermination', _('Circuit Terminations')),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -291,6 +289,13 @@ CIRCUITS_MENU = Menu(
|
|||||||
get_model_item('circuits', 'virtualcircuittermination', _('Virtual Circuit Terminations')),
|
get_model_item('circuits', 'virtualcircuittermination', _('Virtual Circuit Terminations')),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
MenuGroup(
|
||||||
|
label=_('Groups'),
|
||||||
|
items=(
|
||||||
|
get_model_item('circuits', 'circuitgroup', _('Circuit Groups')),
|
||||||
|
get_model_item('circuits', 'circuitgroupassignment', _('Group Assignments')),
|
||||||
|
),
|
||||||
|
),
|
||||||
MenuGroup(
|
MenuGroup(
|
||||||
label=_('Providers'),
|
label=_('Providers'),
|
||||||
items=(
|
items=(
|
||||||
|
@ -76,7 +76,7 @@
|
|||||||
{% trans "Group Assignments" %}
|
{% trans "Group Assignments" %}
|
||||||
{% if perms.circuits.add_circuitgroupassignment %}
|
{% if perms.circuits.add_circuitgroupassignment %}
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<a href="{% url 'circuits:circuitgroupassignment_add' %}?circuit={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
<a href="{% url 'circuits:circuitgroupassignment_add' %}?member_type={{ object|content_type_id }}&member={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Assign Group" %}
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Assign Group" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,9 +22,13 @@
|
|||||||
<th scope="row">{% trans "Group" %}</th>
|
<th scope="row">{% trans "Group" %}</th>
|
||||||
<td>{{ object.group|linkify }}</td>
|
<td>{{ object.group|linkify }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Provider" %}</th>
|
||||||
|
<td>{{ object.member.provider|linkify }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Circuit" %}</th>
|
<th scope="row">{% trans "Circuit" %}</th>
|
||||||
<td>{{ object.circuit|linkify }}</td>
|
<td>{{ object.member|linkify }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Priority" %}</th>
|
<th scope="row">{% trans "Priority" %}</th>
|
||||||
|
@ -60,6 +60,19 @@
|
|||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
{% include 'inc/panels/comments.html' %}
|
{% include 'inc/panels/comments.html' %}
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header">
|
||||||
|
{% trans "Group Assignments" %}
|
||||||
|
{% if perms.circuits.add_circuitgroupassignment %}
|
||||||
|
<div class="card-actions">
|
||||||
|
<a href="{% url 'circuits:circuitgroupassignment_add' %}?member_type={{ object|content_type_id }}&member={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||||
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Assign Group" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</h2>
|
||||||
|
{% htmx_table 'circuits:circuitgroupassignment_list' virtual_circuit_id=object.pk %}
|
||||||
|
</div>
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user