Support group assignment for virtual circuits

This commit is contained in:
Jeremy Stretch 2025-01-02 14:34:02 -05:00
parent 73fa32c01a
commit 19b87e05b1
16 changed files with 349 additions and 88 deletions

View File

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

View File

@ -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_):
member = 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', 'member', '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', 'member', '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):

View File

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

View File

@ -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,57 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
method='search', method='search',
label=_('Search'), label=_('Search'),
) )
provider_id = django_filters.ModelMultipleChoiceFilter( # provider_id = django_filters.ModelMultipleChoiceFilter(
field_name='member__provider', # field_name='member__provider',
queryset=Provider.objects.all(), # queryset=Provider.objects.all(),
label=_('Provider (ID)'), # label=_('Provider (ID)'),
# )
# provider = django_filters.ModelMultipleChoiceFilter(
# field_name='member__provider__slug',
# queryset=Provider.objects.all(),
# to_field_name='slug',
# label=_('Provider (slug)'),
# )
# member_id = django_filters.ModelMultipleChoiceFilter(
# queryset=Circuit.objects.all(),
# label=_('Circuit (ID)'),
# )
# member = django_filters.ModelMultipleChoiceFilter(
# field_name='member__cid',
# queryset=Circuit.objects.all(),
# to_field_name='cid',
# label=_('Circuit (CID)'),
# )
member_type = ContentTypeFilter()
circuit = MultiValueCharFilter(
method='filter_circuit',
field_name='cid',
label=_('Circuit (CID)'),
) )
provider = django_filters.ModelMultipleChoiceFilter( circuit_id = MultiValueNumberFilter(
field_name='member__provider__slug', method='filter_circuit',
queryset=Provider.objects.all(), field_name='pk',
to_field_name='slug',
label=_('Provider (slug)'),
)
member_id = django_filters.ModelMultipleChoiceFilter(
queryset=Circuit.objects.all(),
label=_('Circuit (ID)'), label=_('Circuit (ID)'),
) )
member = django_filters.ModelMultipleChoiceFilter( virtual_circuit = MultiValueCharFilter(
field_name='member__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,7 +433,7 @@ 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():
@ -409,6 +443,45 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
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(

View File

@ -182,7 +182,7 @@ class CircuitGroupAssignmentImportForm(NetBoxModelImportForm):
class Meta: class Meta:
model = CircuitGroupAssignment model = CircuitGroupAssignment
fields = ('member', 'group', 'priority') fields = ('member_type', 'member_id', 'group', 'priority')
class VirtualCircuitImportForm(NetBoxModelImportForm): class VirtualCircuitImportForm(NetBoxModelImportForm):

View File

@ -251,18 +251,59 @@ class CircuitGroupAssignmentForm(NetBoxModelForm):
label=_('Group'), label=_('Group'),
queryset=CircuitGroup.objects.all(), queryset=CircuitGroup.objects.all(),
) )
member_type = ContentTypeChoiceField(
queryset=ContentType.objects.filter(CIRCUIT_GROUP_ASSIGNMENT_MEMBER_MODELS),
widget=HTMXSelect(),
required=False,
label=_('Circuit type')
)
member = DynamicModelChoiceField( 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', 'member', '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(

View File

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

View File

@ -1,32 +0,0 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('circuits', '0050_virtual_circuits'),
('extras', '0122_charfield_null_choices'),
]
operations = [
migrations.AlterModelOptions(
name='circuitgroupassignment',
options={'ordering': ('group', 'member', 'priority', 'pk')},
),
migrations.RemoveConstraint(
model_name='circuitgroupassignment',
name='circuits_circuitgroupassignment_unique_circuit_group',
),
migrations.RenameField(
model_name='circuitgroupassignment',
old_name='circuit',
new_name='member',
),
migrations.AddConstraint(
model_name='circuitgroupassignment',
constraint=models.UniqueConstraint(
fields=('member', 'group'),
name='circuits_circuitgroupassignment_unique_member_group'
),
),
]

View File

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

View File

@ -179,10 +179,16 @@ class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin,
""" """
Assignment of a physical or virtual circuit to a CircuitGroup with an optional priority. Assignment of a physical or virtual circuit to a CircuitGroup with an optional priority.
""" """
member = 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()
member = GenericForeignKey(
ct_field='member_type',
fk_field='member_id'
) )
group = models.ForeignKey( group = models.ForeignKey(
to='circuits.CircuitGroup', to='circuits.CircuitGroup',
@ -201,10 +207,10 @@ class CircuitGroupAssignment(CustomFieldsMixin, ExportTemplatesMixin, TagsMixin,
) )
class Meta: class Meta:
ordering = ('group', 'member', 'priority', 'pk') ordering = ('group', 'member_type', 'member_id', 'priority', 'pk')
constraints = ( constraints = (
models.UniqueConstraint( models.UniqueConstraint(
fields=('member', 'group'), fields=('member_type', 'member_id', 'group'),
name='%(app_label)s_%(class)s_unique_member_group' name='%(app_label)s_%(class)s_unique_member_group'
), ),
) )

View File

@ -192,6 +192,9 @@ class CircuitGroupAssignmentTable(NetBoxTable):
verbose_name=_('Provider'), verbose_name=_('Provider'),
linkify=True linkify=True
) )
member_type = columns.ContentTypeColumn(
verbose_name=_('Type')
)
member = tables.Column( 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', 'member', 'priority', 'created', 'last_updated', 'actions', 'tags', 'pk', 'id', 'group', 'provider', 'member_type', 'member', 'priority', 'created', 'last_updated', 'actions',
'tags',
) )
default_columns = ('pk', 'group', 'provider', 'member', 'priority') default_columns = ('pk', 'group', 'provider', 'member_type', 'member', 'priority')

View File

@ -295,7 +295,7 @@ class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase): class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
model = CircuitGroupAssignment model = CircuitGroupAssignment
brief_fields = ['display', 'group', 'id', 'member', '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,
} }
@ -349,17 +349,20 @@ class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
cls.create_data = [ cls.create_data = [
{ {
'group': circuit_groups[3].pk, 'group': circuit_groups[3].pk,
'member': 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,
'member': 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,
'member': circuits[5].pk, 'member_type': 'circuits.circuit',
'member_id': circuits[5].pk,
'priority': CircuitPriorityChoices.PRIORITY_TERTIARY, 'priority': CircuitPriorityChoices.PRIORITY_TERTIARY,
}, },
] ]

View File

@ -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,10 +662,32 @@ 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],
@ -684,29 +704,51 @@ class CircuitGroupAssignmentTestCase(TestCase, ChangeLoggedFilterSetTests):
member=circuits[2], member=circuits[2],
priority=CircuitPriorityChoices.PRIORITY_TERTIARY 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
),
) )
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(), 4)
def test_circuit(self):
circuits = Circuit.objects.all()[:2]
params = {'circuit_id': [circuits[0].pk, circuits[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
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_member(self): def test_virtual_circuit(self):
circuits = Circuit.objects.all()[:2] virtual_circuits = VirtualCircuit.objects.all()[:2]
params = {'member_id': [circuits[0].pk, circuits[1].pk]} params = {'virtual_circuit_id': [virtual_circuits[0].pk, virtual_circuits[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'member': [circuits[0].cid, circuits[1].cid]} params = {'virtual_circuit': [virtual_circuits[0].cid, virtual_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_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):

View File

@ -517,6 +517,7 @@ class CircuitGroupAssignmentTestCase(
cls.form_data = { cls.form_data = {
'group': circuit_groups[3].pk, 'group': circuit_groups[3].pk,
'member_type': ContentType.objects.get_for_model(Circuit).pk,
'member': circuits[3].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],

View File

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

View File

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