Misc cleanup

This commit is contained in:
jeremystretch 2023-03-28 21:02:35 -04:00
parent 8f3590fdd5
commit bee08050b6
24 changed files with 247 additions and 288 deletions

View File

@ -5,8 +5,8 @@ NetBox is ideal for managing your network's transit and peering providers and ci
```mermaid ```mermaid
flowchart TD flowchart TD
ASN --> Provider ASN --> Provider
Provider --> ProviderAccount --> Circuit Provider --> ProviderNetwork & ProviderAccount & Circuit
Provider --> ProviderNetwork & Circuit ProviderAccount --> Circuit
CircuitType --> Circuit CircuitType --> Circuit
click ASN "../../models/circuits/asn/" click ASN "../../models/circuits/asn/"
@ -27,7 +27,7 @@ Sometimes you'll need to model provider networks into which you don't have full
A circuit is a physical connection between two points, which is installed and maintained by an external provider. For example, an Internet connection delivered as a fiber optic cable would be modeled as a circuit in NetBox. A circuit is a physical connection between two points, which is installed and maintained by an external provider. For example, an Internet connection delivered as a fiber optic cable would be modeled as a circuit in NetBox.
Each circuit is associated with a provider account and assigned a circuit ID, which must be unique to that provider. A circuit is also assigned a user-defined type, operational status, and various other operating characteristics. Each circuit is associated with a provider and assigned a circuit ID, which must be unique to that provider. A circuit is also assigned a user-defined type, operational status, and various other operating characteristics. Provider accounts can also be employed to further categorize circuits belonging to a common provider: These may represent different business units or technologies.
Each circuit may have up to two terminations (A and Z) defined. Each termination can be associated with a particular site or provider network. In the case of the former, a cable can be connected between the circuit termination and a device component to map its physical connectivity. Each circuit may have up to two terminations (A and Z) defined. Each termination can be associated with a particular site or provider network. In the case of the former, a cable can be connected between the circuit termination and a device component to map its physical connectivity.

View File

@ -56,7 +56,7 @@ Below is the (rough) recommended order in which NetBox objects should be created
4. Manufacturers, device types, and module types 4. Manufacturers, device types, and module types
5. Platforms and device roles 5. Platforms and device roles
6. Devices and modules 6. Devices and modules
7. Providers, provider accounts and provider networks 7. Providers, provider accounts, and provider networks
8. Circuit types and circuits 8. Circuit types and circuits
9. Wireless LAN groups and wireless LANs 9. Wireless LAN groups and wireless LANs
10. Route targets and VRFs 10. Route targets and VRFs

View File

@ -4,9 +4,13 @@ A circuit represents a physical point-to-point data connection, typically used t
## Fields ## Fields
### Provider
The [provider](./provider.md) to which this circuit belongs.
### Provider Account ### Provider Account
The [provider account](./provideraccount.md) to which this circuit belongs. Circuits may optionally be assigned to a specific [provider account](./provideraccount.md).
### Circuit ID ### Circuit ID

View File

@ -7,15 +7,13 @@ router.APIRootView = views.CircuitsRootView
# Providers # Providers
router.register('providers', views.ProviderViewSet) router.register('providers', views.ProviderViewSet)
router.register('provider-accounts', views.ProviderAccountViewSet)
router.register('provider-networks', views.ProviderNetworkViewSet)
# Circuits # Circuits
router.register('circuit-types', views.CircuitTypeViewSet) router.register('circuit-types', views.CircuitTypeViewSet)
router.register('circuits', views.CircuitViewSet) router.register('circuits', views.CircuitViewSet)
router.register('circuit-terminations', views.CircuitTerminationViewSet) router.register('circuit-terminations', views.CircuitTerminationViewSet)
# Provider networks
router.register('provider-accounts', views.ProviderAccountViewSet)
router.register('provider-networks', views.ProviderNetworkViewSet)
app_name = 'circuits-api' app_name = 'circuits-api'
urlpatterns = router.urls urlpatterns = router.urls

View File

@ -22,7 +22,7 @@ class CircuitsRootView(APIRootView):
class ProviderViewSet(NetBoxModelViewSet): class ProviderViewSet(NetBoxModelViewSet):
queryset = Provider.objects.prefetch_related('asns', 'tags').annotate( queryset = Provider.objects.prefetch_related('asns', 'tags').annotate(
circuit_count=count_related(Circuit, 'provider_account__provider') circuit_count=count_related(Circuit, 'provider')
) )
serializer_class = serializers.ProviderSerializer serializer_class = serializers.ProviderSerializer
filterset_class = filtersets.ProviderFilterSet filterset_class = filtersets.ProviderFilterSet
@ -46,7 +46,7 @@ class CircuitTypeViewSet(NetBoxModelViewSet):
class CircuitViewSet(NetBoxModelViewSet): class CircuitViewSet(NetBoxModelViewSet):
queryset = Circuit.objects.prefetch_related( queryset = Circuit.objects.prefetch_related(
'type', 'tenant', 'provider_account', 'provider_account__provider', 'termination_a', 'termination_z' 'type', 'tenant', 'provider', 'provider_account', 'termination_a', 'termination_z'
).prefetch_related('tags') ).prefetch_related('tags')
serializer_class = serializers.CircuitSerializer serializer_class = serializers.CircuitSerializer
filterset_class = filtersets.CircuitFilterSet filterset_class = filtersets.CircuitFilterSet
@ -66,11 +66,11 @@ class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
# #
# Provider networks # Provider accounts
# #
class ProviderAccountViewSet(NetBoxModelViewSet): class ProviderAccountViewSet(NetBoxModelViewSet):
queryset = ProviderAccount.objects.prefetch_related('tags') queryset = ProviderAccount.objects.prefetch_related('provider', 'tags')
serializer_class = serializers.ProviderAccountSerializer serializer_class = serializers.ProviderAccountSerializer
filterset_class = filtersets.ProviderAccountFilterSet filterset_class = filtersets.ProviderAccountFilterSet

View File

@ -24,37 +24,37 @@ __all__ = (
class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
region_id = TreeNodeMultipleChoiceFilter( region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='accounts__circuits__terminations__site__region', field_name='circuits__terminations__site__region',
lookup_expr='in', lookup_expr='in',
label=_('Region (ID)'), label=_('Region (ID)'),
) )
region = TreeNodeMultipleChoiceFilter( region = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(), queryset=Region.objects.all(),
field_name='accounts__circuits__terminations__site__region', field_name='circuits__terminations__site__region',
lookup_expr='in', lookup_expr='in',
to_field_name='slug', to_field_name='slug',
label=_('Region (slug)'), label=_('Region (slug)'),
) )
site_group_id = TreeNodeMultipleChoiceFilter( site_group_id = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
field_name='accounts__circuits__terminations__site__group', field_name='circuits__terminations__site__group',
lookup_expr='in', lookup_expr='in',
label=_('Site group (ID)'), label=_('Site group (ID)'),
) )
site_group = TreeNodeMultipleChoiceFilter( site_group = TreeNodeMultipleChoiceFilter(
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
field_name='accounts__circuits__terminations__site__group', field_name='circuits__terminations__site__group',
lookup_expr='in', lookup_expr='in',
to_field_name='slug', to_field_name='slug',
label=_('Site group (slug)'), label=_('Site group (slug)'),
) )
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
field_name='accounts__circuits__terminations__site', field_name='circuits__terminations__site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
label=_('Site'), label=_('Site'),
) )
site = django_filters.ModelMultipleChoiceFilter( site = django_filters.ModelMultipleChoiceFilter(
field_name='accounts__circuits__terminations__site__slug', field_name='circuits__terminations__site__slug',
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
label=_('Site (slug)'), label=_('Site (slug)'),
@ -67,7 +67,7 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
class Meta: class Meta:
model = Provider model = Provider
fields = ['id', 'name', 'slug', ] fields = ['id', 'name', 'slug']
def search(self, queryset, name, value): def search(self, queryset, name, value):
if not value.strip(): if not value.strip():

View File

@ -35,7 +35,7 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
model = Provider model = Provider
fieldsets = ( fieldsets = (
(None, ('asns', )), (None, ('asns', 'description')),
) )
nullable_fields = ( nullable_fields = (
'asns', 'description', 'comments', 'asns', 'description', 'comments',
@ -60,8 +60,7 @@ class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm):
(None, ('provider', 'description')), (None, ('provider', 'description')),
) )
nullable_fields = ( nullable_fields = (
'description', 'description', 'comments',
'comments',
) )

View File

@ -38,7 +38,7 @@ class ProviderAccountImportForm(NetBoxModelImportForm):
class Meta: class Meta:
model = ProviderAccount model = ProviderAccount
fields = ( fields = (
'provider', 'name', 'account', 'comments', 'tags', 'provider', 'name', 'account', 'description', 'comments', 'tags',
) )

View File

@ -69,7 +69,6 @@ class ProviderAccountFilterForm(NetBoxModelFilterSetForm):
label=_('Provider') label=_('Provider')
) )
account = forms.CharField( account = forms.CharField(
max_length=100,
required=False required=False
) )
tag = TagFilterField(model) tag = TagFilterField(model)

View File

@ -41,8 +41,7 @@ class ProviderForm(NetBoxModelForm):
class ProviderAccountForm(NetBoxModelForm): class ProviderAccountForm(NetBoxModelForm):
provider = DynamicModelChoiceField( provider = DynamicModelChoiceField(
queryset=Provider.objects.all(), queryset=Provider.objects.all()
selector=True
) )
comments = CommentField() comments = CommentField()
@ -93,14 +92,10 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
) )
provider_account = DynamicModelChoiceField( provider_account = DynamicModelChoiceField(
queryset=ProviderAccount.objects.all(), queryset=ProviderAccount.objects.all(),
initial_params={ required=False,
'circuits': '$circuit'
},
query_params={ query_params={
'provider_id': '$provider', 'provider_id': '$provider',
}, }
selector=True,
required=False
) )
type = DynamicModelChoiceField( type = DynamicModelChoiceField(
queryset=CircuitType.objects.all() queryset=CircuitType.objects.all()
@ -129,9 +124,6 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
class CircuitTerminationForm(NetBoxModelForm): class CircuitTerminationForm(NetBoxModelForm):
circuit = DynamicModelChoiceField( circuit = DynamicModelChoiceField(
queryset=Circuit.objects.all(), queryset=Circuit.objects.all(),
query_params={
'provider_id': '$provider',
},
selector=True selector=True
) )
site = DynamicModelChoiceField( site = DynamicModelChoiceField(

View File

@ -1,35 +1,34 @@
# Generated by Django 4.1.4 on 2023-03-14 16:02
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import taggit.managers import taggit.managers
import utilities.json import utilities.json
#
# Migrate Account in Provider model to separate account model
#
def create_provideraccounts_from_providers(apps, schema_editor): def create_provideraccounts_from_providers(apps, schema_editor):
"""
Migrate Account in Provider model to separate account model
"""
Provider = apps.get_model('circuits', 'Provider') Provider = apps.get_model('circuits', 'Provider')
ProviderAccount = apps.get_model('circuits', 'ProviderAccount') ProviderAccount = apps.get_model('circuits', 'ProviderAccount')
provider_accounts = []
for provider in Provider.objects.all(): for provider in Provider.objects.all():
if provider.account: if provider.account:
provideraccount = ProviderAccount.objects.create( provider_accounts.append(ProviderAccount(
name=f'{provider.name} {provider.account}',
account=provider.account,
provider=provider, provider=provider,
) account=provider.account
))
ProviderAccount.objects.bulk_create(provider_accounts, batch_size=100)
# def restore_providers_from_provideraccounts(apps, schema_editor):
# Unmigrate ProviderAccount to Provider model """
# Restore Provider account values from auto-generated ProviderAccounts
def revert_provideraccounts_from_providers(apps, schema_editor): """
ProviderAccount = apps.get_model('circuits', 'ProviderAccount') ProviderAccount = apps.get_model('circuits', 'ProviderAccount')
provideraccounts = ProviderAccount.objects.all().order_by('pk') provider_accounts = ProviderAccount.objects.order_by('pk')
for provideraccount in provideraccounts: for provideraccount in provider_accounts:
if provideraccounts.filter(provider=provideraccount.provider)[0] == provideraccount: if provider_accounts.filter(provider=provideraccount.provider)[0] == provideraccount:
provideraccount.provider.account = provideraccount.account provideraccount.provider.account = provideraccount.account
provideraccount.provider.save() provideraccount.provider.save()
@ -51,7 +50,7 @@ class Migration(migrations.Migration):
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
('description', models.CharField(blank=True, max_length=200)), ('description', models.CharField(blank=True, max_length=200)),
('comments', models.TextField(blank=True)), ('comments', models.TextField(blank=True)),
('account', models.CharField(max_length=30)), ('account', models.CharField(max_length=100)),
('name', models.CharField(blank=True, max_length=100)), ('name', models.CharField(blank=True, max_length=100)),
('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='accounts', to='circuits.provider')), ('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='accounts', to='circuits.provider')),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
@ -62,14 +61,14 @@ class Migration(migrations.Migration):
), ),
migrations.AddConstraint( migrations.AddConstraint(
model_name='provideraccount', model_name='provideraccount',
constraint=models.UniqueConstraint(condition=models.Q(('account', ''), _negated=True), fields=('provider', 'name'), name='circuits_provideraccount_unique_provider_name'), constraint=models.UniqueConstraint(condition=models.Q(('name', ''), _negated=True), fields=('provider', 'name'), name='circuits_provideraccount_unique_provider_name'),
), ),
migrations.AddConstraint( migrations.AddConstraint(
model_name='provideraccount', model_name='provideraccount',
constraint=models.UniqueConstraint(fields=('provider', 'account'), name='circuits_provideraccount_unique_provider_account'), constraint=models.UniqueConstraint(fields=('provider', 'account'), name='circuits_provideraccount_unique_provider_account'),
), ),
migrations.RunPython( migrations.RunPython(
create_provideraccounts_from_providers, revert_provideraccounts_from_providers create_provideraccounts_from_providers, restore_providers_from_provideraccounts
), ),
migrations.RemoveField( migrations.RemoveField(
model_name='provider', model_name='provider',

View File

@ -28,9 +28,9 @@ class CircuitType(OrganizationalModel):
class Circuit(PrimaryModel): class Circuit(PrimaryModel):
""" """
A communications circuit connects two points. Each Circuit belongs to a Provider Account; ProviderAccounts may have A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
multiple circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are circuits. Each circuit is also assigned a CircuitType and a Site, and may optionally be assigned to a particular
measured in Kbps. ProviderAccount. Circuit port speed and commit rate are measured in Kbps.
""" """
cid = models.CharField( cid = models.CharField(
max_length=100, max_length=100,
@ -116,7 +116,6 @@ class Circuit(PrimaryModel):
prerequisite_models = ( prerequisite_models = (
'circuits.CircuitType', 'circuits.CircuitType',
'circuits.Provider', 'circuits.Provider',
'circuits.ProviderAccount',
) )
class Meta: class Meta:
@ -143,8 +142,9 @@ class Circuit(PrimaryModel):
def clean(self): def clean(self):
super().clean() super().clean()
if self.provider_account and self.provider != self.provider_account.provider: if self.provider_account and self.provider != self.provider_account.provider:
raise ValidationError("Provider must match ProviderAccount's provider") raise ValidationError({'provider_account': "The assigned account must belong to the assigned provider."})
class CircuitTermination( class CircuitTermination(

View File

@ -15,8 +15,8 @@ __all__ = (
class Provider(PrimaryModel): class Provider(PrimaryModel):
""" """
This is usually a telecommunications company or similar organization. This model stores information pertinent to Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
the user's relationship with the Provider. stores information pertinent to the user's relationship with the Provider.
""" """
name = models.CharField( name = models.CharField(
max_length=100, max_length=100,
@ -54,19 +54,19 @@ class ProviderAccount(PrimaryModel):
""" """
This is a discrete account within a provider. Each Circuit belongs to a Provider Account. This is a discrete account within a provider. Each Circuit belongs to a Provider Account.
""" """
account = models.CharField(
max_length=30,
verbose_name='Account number'
)
name = models.CharField(
max_length=100,
blank=True
)
provider = models.ForeignKey( provider = models.ForeignKey(
to='circuits.Provider', to='circuits.Provider',
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='accounts' related_name='accounts'
) )
account = models.CharField(
max_length=100,
verbose_name='Account ID'
)
name = models.CharField(
max_length=100,
blank=True
)
# Generic relations # Generic relations
contacts = GenericRelation( contacts = GenericRelation(
@ -85,7 +85,7 @@ class ProviderAccount(PrimaryModel):
models.UniqueConstraint( models.UniqueConstraint(
fields=('provider', 'name'), fields=('provider', 'name'),
name='%(app_label)s_%(class)s_unique_provider_name', name='%(app_label)s_%(class)s_unique_provider_name',
condition=~Q(account="") condition=~Q(name="")
), ),
) )

View File

@ -1,5 +1,4 @@
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import Accessor
from circuits.models import * from circuits.models import *
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
@ -53,7 +52,8 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
linkify=True linkify=True
) )
provider_account = tables.Column( provider_account = tables.Column(
linkify=True linkify=True,
verbose_name='Account'
) )
status = columns.ChoiceFieldColumn() status = columns.ChoiceFieldColumn()
termination_a = tables.TemplateColumn( termination_a = tables.TemplateColumn(

View File

@ -50,8 +50,8 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Provider model = Provider
fields = ( fields = (
'pk', 'id', 'name', 'accounts', 'account_count', 'asns', 'asn_count', 'circuit_count', 'description', 'comments', 'contacts', 'pk', 'id', 'name', 'accounts', 'account_count', 'asns', 'asn_count', 'circuit_count', 'description',
'tags', 'created', 'last_updated', 'comments', 'contacts', 'tags', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'account_count', 'circuit_count') default_columns = ('pk', 'name', 'account_count', 'circuit_count')
@ -64,6 +64,12 @@ class ProviderAccountTable(ContactsColumnMixin, NetBoxTable):
provider = tables.Column( provider = tables.Column(
linkify=True linkify=True
) )
circuit_count = columns.LinkedCountColumn(
accessor=Accessor('count_circuits'),
viewname='circuits:circuit_list',
url_params={'provider_account_id': 'pk'},
verbose_name='Circuits'
)
comments = columns.MarkdownColumn() comments = columns.MarkdownColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='circuits:provideraccount_list' url_name='circuits:provideraccount_list'
@ -72,7 +78,8 @@ class ProviderAccountTable(ContactsColumnMixin, NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = ProviderAccount model = ProviderAccount
fields = ( fields = (
'pk', 'id', 'account', 'name', 'provider', 'circuit_count', 'comments', 'contacts', 'tags', 'created', 'last_updated', 'pk', 'id', 'account', 'name', 'provider', 'circuit_count', 'comments', 'contacts', 'tags', 'created',
'last_updated',
) )
default_columns = ('pk', 'account', 'name', 'provider', 'circuit_count') default_columns = ('pk', 'account', 'name', 'provider', 'circuit_count')

View File

@ -158,11 +158,6 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
provider = Provider.objects.create(name='Provider 1', slug='provider-1') provider = Provider.objects.create(name='Provider 1', slug='provider-1')
circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1') circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
provider_account = ProviderAccount.objects.create(
name='Provider Account 2',
provider=provider,
account='2345'
)
sites = ( sites = (
Site(name='Site 1', slug='site-1'), Site(name='Site 1', slug='site-1'),
@ -177,9 +172,9 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
ProviderNetwork.objects.bulk_create(provider_networks) ProviderNetwork.objects.bulk_create(provider_networks)
circuits = ( circuits = (
Circuit(cid='Circuit 1', provider=provider, provider_account=provider_account, type=circuit_type), Circuit(cid='Circuit 1', provider=provider, type=circuit_type),
Circuit(cid='Circuit 2', provider=provider, provider_account=provider_account, type=circuit_type), Circuit(cid='Circuit 2', provider=provider, type=circuit_type),
Circuit(cid='Circuit 3', provider=provider, provider_account=provider_account, type=circuit_type), Circuit(cid='Circuit 3', provider=provider, type=circuit_type),
) )
Circuit.objects.bulk_create(circuits) Circuit.objects.bulk_create(circuits)

View File

@ -36,15 +36,6 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
providers[1].asns.set([asns[1]]) providers[1].asns.set([asns[1]])
providers[2].asns.set([asns[2]]) providers[2].asns.set([asns[2]])
provider_accounts = (
ProviderAccount(name='Account A', account='AAAA', provider=providers[0]),
ProviderAccount(name='Account B', account='BBBB', provider=providers[1]),
ProviderAccount(name='Account C', account='CCCC', provider=providers[2]),
ProviderAccount(name='Account D', account='DDDD', provider=providers[3]),
ProviderAccount(name='Account E', account='EEEE', provider=providers[4]),
)
ProviderAccount.objects.bulk_create(provider_accounts)
regions = ( regions = (
Region(name='Test Region 1', slug='test-region-1'), Region(name='Test Region 1', slug='test-region-1'),
Region(name='Test Region 2', slug='test-region-2'), Region(name='Test Region 2', slug='test-region-2'),
@ -73,8 +64,8 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
CircuitType.objects.bulk_create(circuit_types) CircuitType.objects.bulk_create(circuit_types)
circuits = ( circuits = (
Circuit(provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0], cid='Test Circuit 1'), Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 1'),
Circuit(provider=providers[1], provider_account=provider_accounts[1], type=circuit_types[1], cid='Test Circuit 1'), Circuit(provider=providers[1], type=circuit_types[1], cid='Circuit 2'),
) )
Circuit.objects.bulk_create(circuits) Circuit.objects.bulk_create(circuits)
@ -202,8 +193,9 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
Provider.objects.bulk_create(providers) Provider.objects.bulk_create(providers)
provider_accounts = ( provider_accounts = (
ProviderAccount(name='Provider Account 1', provider=providers[0], account='1234'), ProviderAccount(name='Provider Account 1', provider=providers[0], account='A'),
ProviderAccount(name='Provider Account 2', provider=providers[1], account='2345'), ProviderAccount(name='Provider Account 2', provider=providers[1], account='B'),
ProviderAccount(name='Provider Account 3', provider=providers[2], account='C'),
) )
ProviderAccount.objects.bulk_create(provider_accounts) ProviderAccount.objects.bulk_create(provider_accounts)
@ -217,10 +209,10 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
circuits = ( circuits = (
Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', termination_date='2021-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1'), Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 1', install_date='2020-01-01', termination_date='2021-01-01', commit_rate=1000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar1'),
Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', termination_date='2021-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2'), Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[0], type=circuit_types[0], cid='Test Circuit 2', install_date='2020-01-02', termination_date='2021-01-02', commit_rate=2000, status=CircuitStatusChoices.STATUS_ACTIVE, description='foobar2'),
Circuit(provider=providers[0], provider_account=provider_accounts[0], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', termination_date='2021-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED), Circuit(provider=providers[0], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[0], cid='Test Circuit 3', install_date='2020-01-03', termination_date='2021-01-03', commit_rate=3000, status=CircuitStatusChoices.STATUS_PLANNED),
Circuit(provider=providers[1], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', termination_date='2021-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED), Circuit(provider=providers[1], provider_account=provider_accounts[1], tenant=tenants[1], type=circuit_types[1], cid='Test Circuit 4', install_date='2020-01-04', termination_date='2021-01-04', commit_rate=4000, status=CircuitStatusChoices.STATUS_PLANNED),
Circuit(provider=providers[1], provider_account=provider_accounts[1], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', termination_date='2021-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE), Circuit(provider=providers[1], provider_account=provider_accounts[2], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 5', install_date='2020-01-05', termination_date='2021-01-05', commit_rate=5000, status=CircuitStatusChoices.STATUS_OFFLINE),
Circuit(provider=providers[1], provider_account=provider_accounts[1], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 6', install_date='2020-01-06', termination_date='2021-01-06', commit_rate=6000, status=CircuitStatusChoices.STATUS_OFFLINE), Circuit(provider=providers[1], provider_account=provider_accounts[2], tenant=tenants[2], type=circuit_types[1], cid='Test Circuit 6', install_date='2020-01-06', termination_date='2021-01-06', commit_rate=6000, status=CircuitStatusChoices.STATUS_OFFLINE),
) )
Circuit.objects.bulk_create(circuits) Circuit.objects.bulk_create(circuits)
@ -258,9 +250,9 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
def test_provider_account(self): def test_provider_account(self):
provider_account = ProviderAccount.objects.first() provider_accounts = ProviderAccount.objects.all()[:2]
params = {'provider_account_id': [provider_account.pk]} params = {'provider_account_id': [provider_accounts[0].pk, provider_accounts[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_provider_network(self): def test_provider_network(self):
provider_networks = ProviderNetwork.objects.all()[:2] provider_networks = ProviderNetwork.objects.all()[:2]
@ -342,11 +334,6 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
) )
Provider.objects.bulk_create(providers) Provider.objects.bulk_create(providers)
provider_accounts = (
ProviderAccount(name='Provider Account 1', provider=providers[0], account='1234'),
)
ProviderAccount.objects.bulk_create(provider_accounts)
provider_networks = ( provider_networks = (
ProviderNetwork(name='Provider Network 1', provider=providers[0]), ProviderNetwork(name='Provider Network 1', provider=providers[0]),
ProviderNetwork(name='Provider Network 2', provider=providers[0]), ProviderNetwork(name='Provider Network 2', provider=providers[0]),
@ -355,13 +342,13 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
ProviderNetwork.objects.bulk_create(provider_networks) ProviderNetwork.objects.bulk_create(provider_networks)
circuits = ( circuits = (
Circuit(provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 1'), Circuit(provider=providers[0], circuitstype=circuit_types[0], cid='Circuit 1'),
Circuit(provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 2'), Circuit(provider=providers[0], circuitstype=circuit_types[0], cid='Circuit 2'),
Circuit(provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 3'), Circuit(provider=providers[0], circuitstype=circuit_types[0], cid='Circuit 3'),
Circuit(provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 4'), Circuit(provider=providers[0], circuitstype=circuit_types[0], cid='Circuit 4'),
Circuit(provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 5'), Circuit(provider=providers[0], circuitstype=circuit_types[0], cid='Circuit 5'),
Circuit(provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 6'), Circuit(provider=providers[0], circuitstype=circuit_types[0], cid='Circuit 6'),
Circuit(provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0], cid='Circuit 7'), Circuit(provider=providers[0], circuitstype=circuit_types[0], cid='Circuit 7'),
) )
Circuit.objects.bulk_create(circuits) Circuit.objects.bulk_create(circuits)

View File

@ -202,7 +202,6 @@ class ProviderAccountTestCase(ViewTestCases.PrimaryObjectViewTestCase):
ProviderAccount(name='Provider Account 2', provider=providers[0], account='2345'), ProviderAccount(name='Provider Account 2', provider=providers[0], account='2345'),
ProviderAccount(name='Provider Account 3', provider=providers[0], account='3456'), ProviderAccount(name='Provider Account 3', provider=providers[0], account='3456'),
) )
ProviderAccount.objects.bulk_create(provider_accounts) ProviderAccount.objects.bulk_create(provider_accounts)
tags = create_tags('Alpha', 'Bravo', 'Charlie') tags = create_tags('Alpha', 'Bravo', 'Charlie')
@ -305,12 +304,11 @@ class CircuitTerminationTestCase(
Site.objects.bulk_create(sites) Site.objects.bulk_create(sites)
provider = Provider.objects.create(name='Provider 1', slug='provider-1') provider = Provider.objects.create(name='Provider 1', slug='provider-1')
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1') circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
account = ProviderAccount.objects.create(name='Provider Account 1', provider=provider, account='1234')
circuits = ( circuits = (
Circuit(cid='Circuit 1', provider=provider, provider_account=account, type=circuittype), Circuit(cid='Circuit 1', provider=provider, type=circuittype),
Circuit(cid='Circuit 2', provider=provider, provider_account=account, type=circuittype), Circuit(cid='Circuit 2', provider=provider, type=circuittype),
Circuit(cid='Circuit 3', provider=provider, provider_account=account, type=circuittype), Circuit(cid='Circuit 3', provider=provider, type=circuittype),
) )
Circuit.objects.bulk_create(circuits) Circuit.objects.bulk_create(circuits)

View File

@ -14,7 +14,7 @@ urlpatterns = [
path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'), path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
path('providers/<int:pk>/', include(get_model_urls('circuits', 'provider'))), path('providers/<int:pk>/', include(get_model_urls('circuits', 'provider'))),
# Provider networks # Provider accounts
path('provider-accounts/', views.ProviderAccountListView.as_view(), name='provideraccount_list'), path('provider-accounts/', views.ProviderAccountListView.as_view(), name='provideraccount_list'),
path('provider-accounts/add/', views.ProviderAccountEditView.as_view(), name='provideraccount_add'), path('provider-accounts/add/', views.ProviderAccountEditView.as_view(), name='provideraccount_add'),
path('provider-accounts/import/', views.ProviderAccountBulkImportView.as_view(), name='provideraccount_import'), path('provider-accounts/import/', views.ProviderAccountBulkImportView.as_view(), name='provideraccount_import'),

View File

@ -30,9 +30,8 @@ class CablePathTestCase(TestCase):
cls.powerpanel = PowerPanel.objects.create(site=cls.site, name='Power Panel') cls.powerpanel = PowerPanel.objects.create(site=cls.site, name='Power Panel')
provider = Provider.objects.create(name='Provider', slug='provider') provider = Provider.objects.create(name='Provider', slug='provider')
provider_account = ProviderAccount.objects.create(name='Account', account='AAAA1111', provider=provider)
circuit_type = CircuitType.objects.create(name='Circuit Type', slug='circuit-type') circuit_type = CircuitType.objects.create(name='Circuit Type', slug='circuit-type')
cls.circuit = Circuit.objects.create(provider=provider, provider_account=provider_account, type=circuit_type, cid='Circuit 1') cls.circuit = Circuit.objects.create(provider=provider, type=circuit_type, cid='Circuit 1')
def assertPathExists(self, nodes, **kwargs): def assertPathExists(self, nodes, **kwargs):
""" """
@ -1309,7 +1308,7 @@ class CablePathTestCase(TestCase):
[IF1] --C1-- [CT1] [CT2] --> [PN1] [IF1] --C1-- [CT1] [CT2] --> [PN1]
""" """
interface1 = Interface.objects.create(device=self.device, name='Interface 1') interface1 = Interface.objects.create(device=self.device, name='Interface 1')
providernetwork = ProviderNetwork.objects.create(name='Provider Network 1', provider=self.circuit.provider_account.provider) 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') 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') circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, provider_network=providernetwork, term_side='Z')
@ -1437,7 +1436,7 @@ class CablePathTestCase(TestCase):
""" """
interface1 = Interface.objects.create(device=self.device, name='Interface 1') interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2') interface2 = Interface.objects.create(device=self.device, name='Interface 2')
circuit2 = Circuit.objects.create(provider=self.circuit.provider, provider_account=self.circuit.provider_account, type=self.circuit.type, cid='Circuit 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') 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') 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') circuittermination3 = CircuitTermination.objects.create(circuit=circuit2, site=self.site, term_side='A')

View File

@ -504,11 +504,10 @@ class CableTestCase(TestCase):
device=patch_pannel, name='FP4', type='8p8c', rear_port=rear_port4, rear_port_position=1 device=patch_pannel, name='FP4', type='8p8c', rear_port=rear_port4, rear_port_position=1
) )
provider = Provider.objects.create(name='Provider 1', slug='provider-1') provider = Provider.objects.create(name='Provider 1', slug='provider-1')
provider_account = ProviderAccount.objects.create(name='Provider Account 1', account='A1', provider=provider)
provider_network = ProviderNetwork.objects.create(name='Provider Network 1', provider=provider) provider_network = ProviderNetwork.objects.create(name='Provider Network 1', provider=provider)
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1') circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
circuit1 = Circuit.objects.create(provider=provider, provider_account=provider_account, type=circuittype, cid='1') circuit1 = Circuit.objects.create(provider=provider, type=circuittype, cid='1')
circuit2 = Circuit.objects.create(provider=provider, provider_account=provider_account, type=circuittype, cid='2') circuit2 = Circuit.objects.create(provider=provider, type=circuittype, cid='2')
circuittermination1 = CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='A') circuittermination1 = CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='A')
circuittermination2 = CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='Z') circuittermination2 = CircuitTermination.objects.create(circuit=circuit1, site=site, term_side='Z')
circuittermination3 = CircuitTermination.objects.create(circuit=circuit2, provider_network=provider_network, term_side='A') circuittermination3 = CircuitTermination.objects.create(circuit=circuit2, provider_network=provider_network, term_side='A')

View File

@ -1,6 +1,6 @@
from django.test import TransactionTestCase from django.test import TransactionTestCase
from circuits.models import Provider, Circuit, CircuitType, ProviderAccount from circuits.models import Provider, Circuit, CircuitType
from extras.choices import ChangeActionChoices from extras.choices import ChangeActionChoices
from extras.models import Branch, StagedChange, Tag from extras.models import Branch, StagedChange, Tag
from ipam.models import ASN, RIR from ipam.models import ASN, RIR
@ -28,25 +28,18 @@ class StagingTestCase(TransactionTestCase):
) )
Provider.objects.bulk_create(providers) Provider.objects.bulk_create(providers)
provider_accounts = (
ProviderAccount(name='Account A', provider=providers[0], account='AAAA'),
ProviderAccount(name='Account B', provider=providers[1], account='BBBB'),
ProviderAccount(name='Account C', provider=providers[2], account='CCCC'),
)
ProviderAccount.objects.bulk_create(provider_accounts)
circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1') circuit_type = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
Circuit.objects.bulk_create(( Circuit.objects.bulk_create((
Circuit(provider=providers[0], provider_account=provider_accounts[0], cid='Circuit A1', type=circuit_type), Circuit(provider=providers[0], cid='Circuit A1', type=circuit_type),
Circuit(provider=providers[0], provider_account=provider_accounts[0], cid='Circuit A2', type=circuit_type), Circuit(provider=providers[0], cid='Circuit A2', type=circuit_type),
Circuit(provider=providers[0], provider_account=provider_accounts[0], cid='Circuit A3', type=circuit_type), Circuit(provider=providers[0], cid='Circuit A3', type=circuit_type),
Circuit(provider=providers[1], provider_account=provider_accounts[1], cid='Circuit B1', type=circuit_type), Circuit(provider=providers[1], cid='Circuit B1', type=circuit_type),
Circuit(provider=providers[1], provider_account=provider_accounts[1], cid='Circuit B2', type=circuit_type), Circuit(provider=providers[1], cid='Circuit B2', type=circuit_type),
Circuit(provider=providers[1], provider_account=provider_accounts[1], cid='Circuit B3', type=circuit_type), Circuit(provider=providers[1], cid='Circuit B3', type=circuit_type),
Circuit(provider=providers[2], provider_account=provider_accounts[2], cid='Circuit C1', type=circuit_type), Circuit(provider=providers[2], cid='Circuit C1', type=circuit_type),
Circuit(provider=providers[2], provider_account=provider_accounts[2], cid='Circuit C2', type=circuit_type), Circuit(provider=providers[2], cid='Circuit C2', type=circuit_type),
Circuit(provider=providers[2], provider_account=provider_accounts[2], cid='Circuit C3', type=circuit_type), Circuit(provider=providers[2], cid='Circuit C3', type=circuit_type),
)) ))
def test_object_creation(self): def test_object_creation(self):
@ -57,8 +50,7 @@ class StagingTestCase(TransactionTestCase):
with checkout(branch): with checkout(branch):
provider = Provider.objects.create(name='Provider D', slug='provider-d') provider = Provider.objects.create(name='Provider D', slug='provider-d')
provider.asns.set(asns) provider.asns.set(asns)
provider_account = ProviderAccount.objects.create(name='Account D', provider=provider, account='DDDD') circuit = Circuit.objects.create(provider=provider, cid='Circuit D1', type=CircuitType.objects.first())
circuit = Circuit.objects.create(provider=provider, provider_account=provider_account, cid='Circuit D1', type=CircuitType.objects.first())
circuit.tags.set(tags) circuit.tags.set(tags)
# Sanity-checking # Sanity-checking
@ -70,7 +62,7 @@ class StagingTestCase(TransactionTestCase):
# Verify that changes have been rolled back after exiting the context # Verify that changes have been rolled back after exiting the context
self.assertEqual(Provider.objects.count(), 3) self.assertEqual(Provider.objects.count(), 3)
self.assertEqual(Circuit.objects.count(), 9) self.assertEqual(Circuit.objects.count(), 9)
self.assertEqual(StagedChange.objects.count(), 6) self.assertEqual(StagedChange.objects.count(), 5)
# Verify that changes are replayed upon entering the context # Verify that changes are replayed upon entering the context
with checkout(branch): with checkout(branch):
@ -153,31 +145,25 @@ class StagingTestCase(TransactionTestCase):
with checkout(branch): with checkout(branch):
provider = Provider.objects.get(name='Provider A') provider = Provider.objects.get(name='Provider A')
Circuit.objects.filter(provider_account__provider=provider).delete()
provider.accounts.all().delete()
provider.delete() provider.delete()
# Sanity-checking # Sanity-checking
self.assertEqual(Provider.objects.count(), 2) self.assertEqual(Provider.objects.count(), 2)
self.assertEqual(ProviderAccount.objects.count(), 2)
self.assertEqual(Circuit.objects.count(), 6) self.assertEqual(Circuit.objects.count(), 6)
# Verify that changes have been rolled back after exiting the context # Verify that changes have been rolled back after exiting the context
self.assertEqual(Provider.objects.count(), 3) self.assertEqual(Provider.objects.count(), 3)
self.assertEqual(ProviderAccount.objects.count(), 3)
self.assertEqual(Circuit.objects.count(), 9) self.assertEqual(Circuit.objects.count(), 9)
self.assertEqual(StagedChange.objects.count(), 5) self.assertEqual(StagedChange.objects.count(), 4)
# Verify that changes are replayed upon entering the context # Verify that changes are replayed upon entering the context
with checkout(branch): with checkout(branch):
self.assertEqual(Provider.objects.count(), 2) self.assertEqual(Provider.objects.count(), 2)
self.assertEqual(ProviderAccount.objects.count(), 2)
self.assertEqual(Circuit.objects.count(), 6) self.assertEqual(Circuit.objects.count(), 6)
# Verify that changes are applied and deleted upon branch merge # Verify that changes are applied and deleted upon branch merge
branch.merge() branch.merge()
self.assertEqual(Provider.objects.count(), 2) self.assertEqual(Provider.objects.count(), 2)
self.assertEqual(ProviderAccount.objects.count(), 2)
self.assertEqual(Circuit.objects.count(), 6) self.assertEqual(Circuit.objects.count(), 6)
self.assertEqual(StagedChange.objects.count(), 0) self.assertEqual(StagedChange.objects.count(), 0)

View File

@ -19,8 +19,8 @@
<td>{{ object.provider|linkify }}</td> <td>{{ object.provider|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Provider Account</th> <th scope="row">Account</th>
<td>{{ object.provider_account|linkify }}</td> <td>{{ object.provider_account|linkify|placeholder }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Circuit ID</th> <th scope="row">Circuit ID</th>

View File

@ -13,9 +13,7 @@
<div class="row mb-3"> <div class="row mb-3">
<div class="col col-md-6"> <div class="col col-md-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">Provider Account</h5>
Provider Account
</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
@ -23,12 +21,12 @@
<td>{{ object.provider|linkify }}</td> <td>{{ object.provider|linkify }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Name</th> <th scope="row">Account</th>
<td>{{ object.name }}</td> <td>{{ object.account }}</td>
</tr> </tr>
<tr> <tr>
<th scope="row">Account</th> <th scope="row">Name</th>
<td>{{ object.account|placeholder }}</td> <td>{{ object.name|placeholder }}</td>
</tr> </tr>
</table> </table>
</div> </div>
@ -43,12 +41,11 @@
{% include 'inc/panels/contacts.html' %} {% include 'inc/panels/contacts.html' %}
{% plugin_right_page object %} {% plugin_right_page object %}
</div> </div>
<div class="col col-md-12"> <div class="col col-md-12">
<div class="card"> <div class="card">
<h5 class="card-header">Circuits</h5> <h5 class="card-header">Circuits</h5>
<div class="card-body htmx-container table-responsive" <div class="card-body htmx-container table-responsive"
hx-get="{% url 'circuits:circuit_list' %}?provider_id={{ object.pk }}" hx-get="{% url 'circuits:circuit_list' %}?provider_account_id={{ object.pk }}"
hx-trigger="load" hx-trigger="load"
></div> ></div>
</div> </div>