* #9047 - ProviderAccount * #9047 - Move to new selector types * #9047 - Re-introduce provider FK to Circuit model * #9047 - Fix broken tests * Misc cleanup * Revert errant change * Fix tests * Update circuit filter form --------- Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
This commit is contained in:
parent
d2a694a878
commit
9d709c84e7
|
@ -32,6 +32,7 @@ These are considered the "core" application models which are used to model netwo
|
|||
|
||||
* [circuits.Circuit](../models/circuits/circuit.md)
|
||||
* [circuits.Provider](../models/circuits/provider.md)
|
||||
* [circuits.ProviderAccount](../models/circuits/provideracount.md)
|
||||
* [circuits.ProviderNetwork](../models/circuits/providernetwork.md)
|
||||
* [core.DataSource](../models/core/datasource.md)
|
||||
* [dcim.Cable](../models/dcim/cable.md)
|
||||
|
|
|
@ -29,7 +29,7 @@ A SearchIndex subclass defines both its model and a list of two-tuples specifyin
|
|||
| 60 | Unique serialized attribute (per related object) | Device.serial |
|
||||
| 100 | Primary human identifier | Device.name, Circuit.cid, Cable.label |
|
||||
| 110 | Slug | Site.slug |
|
||||
| 200 | Secondary identifier | Provider.account, DeviceType.part_number |
|
||||
| 200 | Secondary identifier | ProviderAccount.account, DeviceType.part_number |
|
||||
| 300 | Highly unique descriptive attribute | CircuitTermination.xconnect_id, IPAddress.dns_name |
|
||||
| 500 | Description | Site.description |
|
||||
| 1000 | Custom field default | - |
|
||||
|
|
|
@ -5,13 +5,15 @@ NetBox is ideal for managing your network's transit and peering providers and ci
|
|||
```mermaid
|
||||
flowchart TD
|
||||
ASN --> Provider
|
||||
Provider --> ProviderNetwork & Circuit
|
||||
Provider --> ProviderNetwork & ProviderAccount & Circuit
|
||||
ProviderAccount --> Circuit
|
||||
CircuitType --> Circuit
|
||||
|
||||
click ASN "../../models/circuits/asn/"
|
||||
click Circuit "../../models/circuits/circuit/"
|
||||
click CircuitType "../../models/circuits/circuittype/"
|
||||
click Provider "../../models/circuits/provider/"
|
||||
click ProviderAccount "../../models/circuits/provideraccount/"
|
||||
click ProviderNetwork "../../models/circuits/providernetwork/"
|
||||
```
|
||||
|
||||
|
@ -25,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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ The following models support the assignment of contacts:
|
|||
|
||||
* circuits.Circuit
|
||||
* circuits.Provider
|
||||
* circuits.ProviderAccount
|
||||
* dcim.Device
|
||||
* dcim.Location
|
||||
* dcim.Manufacturer
|
||||
|
|
|
@ -56,7 +56,7 @@ Below is the (rough) recommended order in which NetBox objects should be created
|
|||
4. Manufacturers, device types, and module types
|
||||
5. Platforms and device roles
|
||||
6. Devices and modules
|
||||
7. Providers and provider networks
|
||||
7. Providers, provider accounts, and provider networks
|
||||
8. Circuit types and circuits
|
||||
9. Wireless LAN groups and wireless LANs
|
||||
10. Route targets and VRFs
|
||||
|
|
|
@ -8,6 +8,10 @@ A circuit represents a physical point-to-point data connection, typically used t
|
|||
|
||||
The [provider](./provider.md) to which this circuit belongs.
|
||||
|
||||
### Provider Account
|
||||
|
||||
Circuits may optionally be assigned to a specific [provider account](./provideraccount.md).
|
||||
|
||||
### Circuit ID
|
||||
|
||||
An identifier for this circuit. This must be unique to the assigned provider. (Circuits assigned to different providers may have the same circuit ID.)
|
||||
|
|
|
@ -16,10 +16,6 @@ A unique URL-friendly identifier. (This value can be used for filtering.)
|
|||
|
||||
The [AS numbers](../ipam/asn.md) assigned to this provider (optional).
|
||||
|
||||
### Account Number
|
||||
|
||||
The administrative account identifier tied to this provider for your organization.
|
||||
|
||||
### Portal URL
|
||||
|
||||
The URL for the provider's customer service portal.
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# Provider Accounts
|
||||
|
||||
This model can be used to represent individual accounts associated with a provider.
|
||||
|
||||
## Fields
|
||||
|
||||
### Provider
|
||||
|
||||
The [provider](./provider.md) the account belongs to.
|
||||
|
||||
### Name
|
||||
|
||||
A human-friendly name, unique to the provider.
|
||||
|
||||
### Account Number
|
||||
|
||||
The administrative account identifier tied to this provider for your organization.
|
|
@ -9,6 +9,7 @@ __all__ = [
|
|||
'NestedCircuitTypeSerializer',
|
||||
'NestedProviderNetworkSerializer',
|
||||
'NestedProviderSerializer',
|
||||
'NestedProviderAccountSerializer',
|
||||
]
|
||||
|
||||
|
||||
|
@ -37,6 +38,18 @@ class NestedProviderSerializer(WritableNestedSerializer):
|
|||
fields = ['id', 'url', 'display', 'name', 'slug', 'circuit_count']
|
||||
|
||||
|
||||
#
|
||||
# Provider Accounts
|
||||
#
|
||||
|
||||
class NestedProviderAccountSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = ['id', 'url', 'display', 'name', 'account']
|
||||
|
||||
|
||||
#
|
||||
# Circuits
|
||||
#
|
||||
|
|
|
@ -18,6 +18,12 @@ from .nested_serializers import *
|
|||
|
||||
class ProviderSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||
accounts = SerializedPKRelatedField(
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
serializer=NestedProviderAccountSerializer,
|
||||
required=False,
|
||||
many=True
|
||||
)
|
||||
asns = SerializedPKRelatedField(
|
||||
queryset=ASN.objects.all(),
|
||||
serializer=NestedASNSerializer,
|
||||
|
@ -31,11 +37,27 @@ class ProviderSerializer(NetBoxModelSerializer):
|
|||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'account', 'description', 'comments', 'asns', 'tags',
|
||||
'id', 'url', 'display', 'name', 'slug', 'accounts', 'description', 'comments', 'asns', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'circuit_count',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Provider Accounts
|
||||
#
|
||||
|
||||
class ProviderAccountSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail')
|
||||
provider = NestedProviderSerializer()
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = [
|
||||
'id', 'url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
#
|
||||
|
@ -84,6 +106,7 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
|||
class CircuitSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||
provider = NestedProviderSerializer()
|
||||
provider_account = NestedProviderAccountSerializer()
|
||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||
type = NestedCircuitTypeSerializer()
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
|
@ -93,9 +116,9 @@ class CircuitSerializer(NetBoxModelSerializer):
|
|||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'id', 'url', 'display', 'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date',
|
||||
'commit_rate', 'description', 'termination_a', 'termination_z', 'comments', 'tags', 'custom_fields',
|
||||
'created', 'last_updated',
|
||||
'id', 'url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date',
|
||||
'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -7,14 +7,13 @@ router.APIRootView = views.CircuitsRootView
|
|||
|
||||
# Providers
|
||||
router.register('providers', views.ProviderViewSet)
|
||||
router.register('provider-accounts', views.ProviderAccountViewSet)
|
||||
router.register('provider-networks', views.ProviderNetworkViewSet)
|
||||
|
||||
# Circuits
|
||||
router.register('circuit-types', views.CircuitTypeViewSet)
|
||||
router.register('circuits', views.CircuitViewSet)
|
||||
router.register('circuit-terminations', views.CircuitTerminationViewSet)
|
||||
|
||||
# Provider networks
|
||||
router.register('provider-networks', views.ProviderNetworkViewSet)
|
||||
|
||||
app_name = 'circuits-api'
|
||||
urlpatterns = router.urls
|
||||
|
|
|
@ -46,7 +46,7 @@ class CircuitTypeViewSet(NetBoxModelViewSet):
|
|||
|
||||
class CircuitViewSet(NetBoxModelViewSet):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
'type', 'tenant', 'provider', 'termination_a', 'termination_z'
|
||||
'type', 'tenant', 'provider', 'provider_account', 'termination_a', 'termination_z'
|
||||
).prefetch_related('tags')
|
||||
serializer_class = serializers.CircuitSerializer
|
||||
filterset_class = filtersets.CircuitFilterSet
|
||||
|
@ -65,6 +65,16 @@ class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
|||
brief_prefetch_fields = ['circuit']
|
||||
|
||||
|
||||
#
|
||||
# Provider accounts
|
||||
#
|
||||
|
||||
class ProviderAccountViewSet(NetBoxModelViewSet):
|
||||
queryset = ProviderAccount.objects.prefetch_related('provider', 'tags')
|
||||
serializer_class = serializers.ProviderAccountSerializer
|
||||
filterset_class = filtersets.ProviderAccountFilterSet
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
#
|
||||
|
|
|
@ -16,6 +16,7 @@ __all__ = (
|
|||
'CircuitTerminationFilterSet',
|
||||
'CircuitTypeFilterSet',
|
||||
'ProviderNetworkFilterSet',
|
||||
'ProviderAccountFilterSet',
|
||||
'ProviderFilterSet',
|
||||
)
|
||||
|
||||
|
@ -66,7 +67,34 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
|||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ['id', 'name', 'slug', 'account']
|
||||
fields = ['id', 'name', 'slug']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(accounts__account__icontains=value) |
|
||||
Q(accounts__name__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class ProviderAccountFilterSet(NetBoxModelFilterSet):
|
||||
provider_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Provider.objects.all(),
|
||||
label=_('Provider (ID)'),
|
||||
)
|
||||
provider = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider__slug',
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = ['id', 'name', 'account', 'description']
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
@ -75,7 +103,7 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
|||
Q(name__icontains=value) |
|
||||
Q(account__icontains=value) |
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
).distinct()
|
||||
|
||||
|
||||
class ProviderNetworkFilterSet(NetBoxModelFilterSet):
|
||||
|
@ -123,6 +151,11 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
|||
to_field_name='slug',
|
||||
label=_('Provider (slug)'),
|
||||
)
|
||||
provider_account_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='provider_account',
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
label=_('ProviderAccount (ID)'),
|
||||
)
|
||||
provider_network_id = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='terminations__provider_network',
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
|
|
|
@ -14,6 +14,7 @@ __all__ = (
|
|||
'CircuitBulkEditForm',
|
||||
'CircuitTypeBulkEditForm',
|
||||
'ProviderBulkEditForm',
|
||||
'ProviderAccountBulkEditForm',
|
||||
'ProviderNetworkBulkEditForm',
|
||||
)
|
||||
|
||||
|
@ -24,11 +25,6 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
|
|||
label=_('ASNs'),
|
||||
required=False
|
||||
)
|
||||
account = forms.CharField(
|
||||
max_length=30,
|
||||
required=False,
|
||||
label=_('Account number')
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
|
@ -39,10 +35,32 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
|
|||
|
||||
model = Provider
|
||||
fieldsets = (
|
||||
(None, ('asns', 'account', )),
|
||||
(None, ('asns', 'description')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'asns', 'account', 'description', 'comments',
|
||||
'asns', 'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False
|
||||
)
|
||||
description = forms.CharField(
|
||||
max_length=200,
|
||||
required=False
|
||||
)
|
||||
comments = CommentField(
|
||||
label=_('Comments')
|
||||
)
|
||||
|
||||
model = ProviderAccount
|
||||
fieldsets = (
|
||||
(None, ('provider', 'description')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
|
@ -95,6 +113,13 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
|||
queryset=Provider.objects.all(),
|
||||
required=False
|
||||
)
|
||||
provider_account = DynamicModelChoiceField(
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'provider': '$provider'
|
||||
}
|
||||
)
|
||||
status = forms.ChoiceField(
|
||||
choices=add_blank_choice(CircuitStatusChoices),
|
||||
required=False,
|
||||
|
@ -127,7 +152,7 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
|
|||
model = Circuit
|
||||
fieldsets = (
|
||||
('Circuit', ('provider', 'type', 'status', 'description')),
|
||||
('Service Parameters', ('install_date', 'termination_date', 'commit_rate')),
|
||||
('Service Parameters', ('provider_account', 'install_date', 'termination_date', 'commit_rate')),
|
||||
('Tenancy', ('tenant',)),
|
||||
)
|
||||
nullable_fields = (
|
||||
|
|
|
@ -13,6 +13,7 @@ __all__ = (
|
|||
'CircuitTerminationImportForm',
|
||||
'CircuitTypeImportForm',
|
||||
'ProviderImportForm',
|
||||
'ProviderAccountImportForm',
|
||||
'ProviderNetworkImportForm',
|
||||
)
|
||||
|
||||
|
@ -23,7 +24,21 @@ class ProviderImportForm(NetBoxModelImportForm):
|
|||
class Meta:
|
||||
model = Provider
|
||||
fields = (
|
||||
'name', 'slug', 'account', 'description', 'comments', 'tags',
|
||||
'name', 'slug', 'description', 'comments', 'tags',
|
||||
)
|
||||
|
||||
|
||||
class ProviderAccountImportForm(NetBoxModelImportForm):
|
||||
provider = CSVModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text=_('Assigned provider')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = (
|
||||
'provider', 'name', 'account', 'description', 'comments', 'tags',
|
||||
)
|
||||
|
||||
|
||||
|
@ -55,6 +70,11 @@ class CircuitImportForm(NetBoxModelImportForm):
|
|||
to_field_name='name',
|
||||
help_text=_('Assigned provider')
|
||||
)
|
||||
provider_account = CSVModelChoiceField(
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
to_field_name='name',
|
||||
help_text=_('Assigned provider account')
|
||||
)
|
||||
type = CSVModelChoiceField(
|
||||
queryset=CircuitType.objects.all(),
|
||||
to_field_name='name',
|
||||
|
@ -74,8 +94,8 @@ class CircuitImportForm(NetBoxModelImportForm):
|
|||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'cid', 'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate',
|
||||
'description', 'comments', 'tags'
|
||||
'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date',
|
||||
'commit_rate', 'description', 'comments', 'tags'
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ __all__ = (
|
|||
'CircuitFilterForm',
|
||||
'CircuitTypeFilterForm',
|
||||
'ProviderFilterForm',
|
||||
'ProviderAccountFilterForm',
|
||||
'ProviderNetworkFilterForm',
|
||||
)
|
||||
|
||||
|
@ -56,6 +57,23 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
|||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ProviderAccountFilterForm(NetBoxModelFilterSetForm):
|
||||
model = ProviderAccount
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
('Attributes', ('provider_id', 'account')),
|
||||
)
|
||||
provider_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
label=_('Provider')
|
||||
)
|
||||
account = forms.CharField(
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
|
||||
model = ProviderNetwork
|
||||
fieldsets = (
|
||||
|
@ -83,7 +101,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
|||
model = Circuit
|
||||
fieldsets = (
|
||||
(None, ('q', 'filter_id', 'tag')),
|
||||
('Provider', ('provider_id', 'provider_network_id')),
|
||||
('Provider', ('provider_id', 'provider_account_id', 'provider_network_id')),
|
||||
('Attributes', ('type_id', 'status', 'install_date', 'termination_date', 'commit_rate')),
|
||||
('Location', ('region_id', 'site_group_id', 'site_id')),
|
||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||
|
@ -99,6 +117,14 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
|
|||
required=False,
|
||||
label=_('Provider')
|
||||
)
|
||||
provider_account_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'provider_id': '$provider_id'
|
||||
},
|
||||
label=_('Provider account')
|
||||
)
|
||||
provider_network_id = DynamicModelMultipleChoiceField(
|
||||
queryset=ProviderNetwork.objects.all(),
|
||||
required=False,
|
||||
|
|
|
@ -14,6 +14,7 @@ __all__ = (
|
|||
'CircuitTerminationForm',
|
||||
'CircuitTypeForm',
|
||||
'ProviderForm',
|
||||
'ProviderAccountForm',
|
||||
'ProviderNetworkForm',
|
||||
)
|
||||
|
||||
|
@ -29,13 +30,25 @@ class ProviderForm(NetBoxModelForm):
|
|||
|
||||
fieldsets = (
|
||||
('Provider', ('name', 'slug', 'asns', 'description', 'tags')),
|
||||
('Support Info', ('account',)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = [
|
||||
'name', 'slug', 'account', 'asns', 'description', 'comments', 'tags',
|
||||
'name', 'slug', 'asns', 'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
class ProviderAccountForm(NetBoxModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all()
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
class Meta:
|
||||
model = ProviderAccount
|
||||
fields = [
|
||||
'provider', 'name', 'account', 'description', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
|
@ -74,7 +87,15 @@ class CircuitTypeForm(NetBoxModelForm):
|
|||
|
||||
class CircuitForm(TenancyForm, NetBoxModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all()
|
||||
queryset=Provider.objects.all(),
|
||||
selector=True
|
||||
)
|
||||
provider_account = DynamicModelChoiceField(
|
||||
queryset=ProviderAccount.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'provider_id': '$provider',
|
||||
}
|
||||
)
|
||||
type = DynamicModelChoiceField(
|
||||
queryset=CircuitType.objects.all()
|
||||
|
@ -82,7 +103,7 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
|
|||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
('Circuit', ('provider', 'cid', 'type', 'status', 'description', 'tags')),
|
||||
('Circuit', ('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags')),
|
||||
('Service Parameters', ('install_date', 'termination_date', 'commit_rate')),
|
||||
('Tenancy', ('tenant_group', 'tenant')),
|
||||
)
|
||||
|
@ -90,8 +111,8 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
|
|||
class Meta:
|
||||
model = Circuit
|
||||
fields = [
|
||||
'cid', 'type', 'provider', 'status', 'install_date', 'termination_date', 'commit_rate', 'description',
|
||||
'tenant_group', 'tenant', 'comments', 'tags',
|
||||
'cid', 'type', 'provider', 'provider_account', 'status', 'install_date', 'termination_date', 'commit_rate',
|
||||
'description', 'tenant_group', 'tenant', 'comments', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'install_date': DatePicker(),
|
||||
|
@ -101,18 +122,9 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
|
|||
|
||||
|
||||
class CircuitTerminationForm(NetBoxModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
queryset=Provider.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'circuits': '$circuit'
|
||||
}
|
||||
)
|
||||
circuit = DynamicModelChoiceField(
|
||||
queryset=Circuit.objects.all(),
|
||||
query_params={
|
||||
'provider_id': '$provider',
|
||||
},
|
||||
selector=True
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
|
@ -128,8 +140,8 @@ class CircuitTerminationForm(NetBoxModelForm):
|
|||
class Meta:
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'provider', 'circuit', 'term_side', 'site', 'provider_network', 'mark_connected', 'port_speed',
|
||||
'upstream_speed', 'xconnect_id', 'pp_info', 'description', 'tags',
|
||||
'circuit', 'term_side', 'site', 'provider_network', 'mark_connected', 'port_speed', 'upstream_speed',
|
||||
'xconnect_id', 'pp_info', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'port_speed': SelectSpeedWidget(),
|
||||
|
|
|
@ -31,6 +31,9 @@ class CircuitsQuery(graphene.ObjectType):
|
|||
def resolve_provider_list(root, info, **kwargs):
|
||||
return gql_query_optimizer(models.Provider.objects.all(), info)
|
||||
|
||||
provider_account = ObjectField(ProviderAccountType)
|
||||
provider_account_list = ObjectListField(ProviderAccountType)
|
||||
|
||||
provider_network = ObjectField(ProviderNetworkType)
|
||||
provider_network_list = ObjectListField(ProviderNetworkType)
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ __all__ = (
|
|||
'CircuitType',
|
||||
'CircuitTypeType',
|
||||
'ProviderType',
|
||||
'ProviderAccountType',
|
||||
'ProviderNetworkType',
|
||||
)
|
||||
|
||||
|
@ -45,6 +46,14 @@ class ProviderType(NetBoxObjectType, ContactsMixin):
|
|||
filterset_class = filtersets.ProviderFilterSet
|
||||
|
||||
|
||||
class ProviderAccountType(NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
model = models.ProviderAccount
|
||||
fields = '__all__'
|
||||
filterset_class = filtersets.ProviderAccountFilterSet
|
||||
|
||||
|
||||
class ProviderNetworkType(NetBoxObjectType):
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import utilities.json
|
||||
|
||||
|
||||
def create_provideraccounts_from_providers(apps, schema_editor):
|
||||
"""
|
||||
Migrate Account in Provider model to separate account model
|
||||
"""
|
||||
Provider = apps.get_model('circuits', 'Provider')
|
||||
ProviderAccount = apps.get_model('circuits', 'ProviderAccount')
|
||||
|
||||
provider_accounts = []
|
||||
for provider in Provider.objects.all():
|
||||
if provider.account:
|
||||
provider_accounts.append(ProviderAccount(
|
||||
provider=provider,
|
||||
account=provider.account
|
||||
))
|
||||
ProviderAccount.objects.bulk_create(provider_accounts, batch_size=100)
|
||||
|
||||
|
||||
def restore_providers_from_provideraccounts(apps, schema_editor):
|
||||
"""
|
||||
Restore Provider account values from auto-generated ProviderAccounts
|
||||
"""
|
||||
ProviderAccount = apps.get_model('circuits', 'ProviderAccount')
|
||||
provider_accounts = ProviderAccount.objects.order_by('pk')
|
||||
for provideraccount in provider_accounts:
|
||||
if provider_accounts.filter(provider=provideraccount.provider)[0] == provideraccount:
|
||||
provideraccount.provider.account = provideraccount.account
|
||||
provideraccount.provider.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0084_staging'),
|
||||
('circuits', '0041_standardize_description_comments'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ProviderAccount',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('created', models.DateTimeField(auto_now_add=True, null=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True, null=True)),
|
||||
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
|
||||
('description', models.CharField(blank=True, max_length=200)),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('account', models.CharField(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')),
|
||||
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('provider', 'account'),
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='provideraccount',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('name', ''), _negated=True), fields=('provider', 'name'), name='circuits_provideraccount_unique_provider_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='provideraccount',
|
||||
constraint=models.UniqueConstraint(fields=('provider', 'account'), name='circuits_provideraccount_unique_provider_account'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
create_provideraccounts_from_providers, restore_providers_from_provideraccounts
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='provider',
|
||||
name='account',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='circuit',
|
||||
name='provider_account',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='circuits', to='circuits.provideraccount', null=True, blank=True),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='circuit',
|
||||
options={'ordering': ['provider', 'provider_account', 'cid']},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='circuit',
|
||||
constraint=models.UniqueConstraint(fields=('provider_account', 'cid'), name='circuits_circuit_unique_provideraccount_cid'),
|
||||
),
|
||||
]
|
|
@ -29,8 +29,8 @@ class CircuitType(OrganizationalModel):
|
|||
class Circuit(PrimaryModel):
|
||||
"""
|
||||
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
|
||||
circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured
|
||||
in Kbps.
|
||||
circuits. Each circuit is also assigned a CircuitType and a Site, and may optionally be assigned to a particular
|
||||
ProviderAccount. Circuit port speed and commit rate are measured in Kbps.
|
||||
"""
|
||||
cid = models.CharField(
|
||||
max_length=100,
|
||||
|
@ -42,6 +42,13 @@ class Circuit(PrimaryModel):
|
|||
on_delete=models.PROTECT,
|
||||
related_name='circuits'
|
||||
)
|
||||
provider_account = models.ForeignKey(
|
||||
to='circuits.ProviderAccount',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='circuits',
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
type = models.ForeignKey(
|
||||
to='CircuitType',
|
||||
on_delete=models.PROTECT,
|
||||
|
@ -103,7 +110,8 @@ class Circuit(PrimaryModel):
|
|||
)
|
||||
|
||||
clone_fields = (
|
||||
'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate', 'description',
|
||||
'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate',
|
||||
'description',
|
||||
)
|
||||
prerequisite_models = (
|
||||
'circuits.CircuitType',
|
||||
|
@ -111,12 +119,16 @@ class Circuit(PrimaryModel):
|
|||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['provider', 'cid']
|
||||
ordering = ['provider', 'provider_account', 'cid']
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('provider', 'cid'),
|
||||
name='%(app_label)s_%(class)s_unique_provider_cid'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('provider_account', 'cid'),
|
||||
name='%(app_label)s_%(class)s_unique_provideraccount_cid'
|
||||
),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
|
@ -128,6 +140,12 @@ class Circuit(PrimaryModel):
|
|||
def get_status_color(self):
|
||||
return CircuitStatusChoices.colors.get(self.status)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
if self.provider_account and self.provider != self.provider_account.provider:
|
||||
raise ValidationError({'provider_account': "The assigned account must belong to the assigned provider."})
|
||||
|
||||
|
||||
class CircuitTermination(
|
||||
CustomFieldsMixin,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
@ -8,6 +9,7 @@ from netbox.models import PrimaryModel
|
|||
__all__ = (
|
||||
'ProviderNetwork',
|
||||
'Provider',
|
||||
'ProviderAccount',
|
||||
)
|
||||
|
||||
|
||||
|
@ -30,20 +32,13 @@ class Provider(PrimaryModel):
|
|||
related_name='providers',
|
||||
blank=True
|
||||
)
|
||||
account = models.CharField(
|
||||
max_length=30,
|
||||
blank=True,
|
||||
verbose_name='Account number'
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
|
||||
clone_fields = (
|
||||
'account',
|
||||
)
|
||||
clone_fields = ()
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
@ -55,6 +50,54 @@ class Provider(PrimaryModel):
|
|||
return reverse('circuits:provider', args=[self.pk])
|
||||
|
||||
|
||||
class ProviderAccount(PrimaryModel):
|
||||
"""
|
||||
This is a discrete account within a provider. Each Circuit belongs to a Provider Account.
|
||||
"""
|
||||
provider = models.ForeignKey(
|
||||
to='circuits.Provider',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='accounts'
|
||||
)
|
||||
account = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name='Account ID'
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Generic relations
|
||||
contacts = GenericRelation(
|
||||
to='tenancy.ContactAssignment'
|
||||
)
|
||||
|
||||
clone_fields = ('provider', )
|
||||
|
||||
class Meta:
|
||||
ordering = ('provider', 'account')
|
||||
constraints = (
|
||||
models.UniqueConstraint(
|
||||
fields=('provider', 'account'),
|
||||
name='%(app_label)s_%(class)s_unique_provider_account'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=('provider', 'name'),
|
||||
name='%(app_label)s_%(class)s_unique_provider_name',
|
||||
condition=~Q(name="")
|
||||
),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
if self.name:
|
||||
return f'{self.account} ({self.name})'
|
||||
return f'{self.account}'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('circuits:provideraccount', args=[self.pk])
|
||||
|
||||
|
||||
class ProviderNetwork(PrimaryModel):
|
||||
"""
|
||||
This represents a provider network which exists outside of NetBox, the details of which are unknown or
|
||||
|
|
|
@ -39,12 +39,20 @@ class ProviderIndex(SearchIndex):
|
|||
model = models.Provider
|
||||
fields = (
|
||||
('name', 100),
|
||||
('account', 200),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
|
||||
|
||||
class ProviderAccountIndex(SearchIndex):
|
||||
model = models.ProviderAccount
|
||||
fields = (
|
||||
('name', 100),
|
||||
('account', 200),
|
||||
('comments', 5000),
|
||||
)
|
||||
|
||||
|
||||
@register_search
|
||||
class ProviderNetworkIndex(SearchIndex):
|
||||
model = models.ProviderNetwork
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import django_tables2 as tables
|
||||
|
||||
from circuits.models import *
|
||||
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
|
||||
|
||||
|
@ -50,6 +51,10 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
|||
provider = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
provider_account = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='Account'
|
||||
)
|
||||
status = columns.ChoiceFieldColumn()
|
||||
termination_a = tables.TemplateColumn(
|
||||
template_code=CIRCUITTERMINATION_LINK,
|
||||
|
@ -68,9 +73,9 @@ class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
|||
class Meta(NetBoxTable.Meta):
|
||||
model = Circuit
|
||||
fields = (
|
||||
'pk', 'id', 'cid', 'provider', 'type', 'status', 'tenant', 'tenant_group', 'termination_a', 'termination_z',
|
||||
'install_date', 'termination_date', 'commit_rate', 'description', 'comments', 'contacts', 'tags', 'created',
|
||||
'last_updated',
|
||||
'pk', 'id', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'tenant_group',
|
||||
'termination_a', 'termination_z', 'install_date', 'termination_date', 'commit_rate', 'description',
|
||||
'comments', 'contacts', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description',
|
||||
|
|
|
@ -7,6 +7,7 @@ from netbox.tables import NetBoxTable, columns
|
|||
|
||||
__all__ = (
|
||||
'ProviderTable',
|
||||
'ProviderAccountTable',
|
||||
'ProviderNetworkTable',
|
||||
)
|
||||
|
||||
|
@ -15,6 +16,16 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
|
|||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
accounts = columns.ManyToManyColumn(
|
||||
linkify_item=True,
|
||||
verbose_name='Accounts'
|
||||
)
|
||||
account_count = columns.LinkedCountColumn(
|
||||
accessor=tables.A('accounts__count'),
|
||||
viewname='circuits:provideraccount_list',
|
||||
url_params={'account_id': 'pk'},
|
||||
verbose_name='Account Count'
|
||||
)
|
||||
asns = columns.ManyToManyColumn(
|
||||
linkify_item=True,
|
||||
verbose_name='ASNs'
|
||||
|
@ -39,10 +50,38 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
|
|||
class Meta(NetBoxTable.Meta):
|
||||
model = Provider
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'asns', 'account', 'asn_count', 'circuit_count', 'description', 'comments', 'contacts',
|
||||
'tags', 'created', 'last_updated',
|
||||
'pk', 'id', 'name', 'accounts', 'account_count', 'asns', 'asn_count', 'circuit_count', 'description',
|
||||
'comments', 'contacts', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'account', 'circuit_count')
|
||||
default_columns = ('pk', 'name', 'account_count', 'circuit_count')
|
||||
|
||||
|
||||
class ProviderAccountTable(ContactsColumnMixin, NetBoxTable):
|
||||
account = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
name = tables.Column()
|
||||
provider = tables.Column(
|
||||
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()
|
||||
tags = columns.TagColumn(
|
||||
url_name='circuits:provideraccount_list'
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = ProviderAccount
|
||||
fields = (
|
||||
'pk', 'id', 'account', 'name', 'provider', 'circuit_count', 'comments', 'contacts', 'tags', 'created',
|
||||
'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'account', 'name', 'provider', 'circuit_count')
|
||||
|
||||
|
||||
class ProviderNetworkTable(NetBoxTable):
|
||||
|
|
|
@ -20,7 +20,7 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
|
|||
model = Provider
|
||||
brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
|
||||
bulk_update_data = {
|
||||
'account': '1234',
|
||||
'comments': 'New comments',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
@ -106,6 +106,12 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
|
|||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
provider_accounts = (
|
||||
ProviderAccount(name='Provider Account 1', provider=providers[0], account='1234'),
|
||||
ProviderAccount(name='Provider Account 2', provider=providers[1], account='2345'),
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
|
||||
circuit_types = (
|
||||
CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
|
||||
CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
|
||||
|
@ -113,9 +119,9 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
|
|||
CircuitType.objects.bulk_create(circuit_types)
|
||||
|
||||
circuits = (
|
||||
Circuit(cid='Circuit 1', provider=providers[0], type=circuit_types[0]),
|
||||
Circuit(cid='Circuit 2', provider=providers[0], type=circuit_types[0]),
|
||||
Circuit(cid='Circuit 3', provider=providers[0], type=circuit_types[0]),
|
||||
Circuit(cid='Circuit 1', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]),
|
||||
Circuit(cid='Circuit 2', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]),
|
||||
Circuit(cid='Circuit 3', provider=providers[0], provider_account=provider_accounts[0], type=circuit_types[0]),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
|
@ -123,16 +129,19 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
|
|||
{
|
||||
'cid': 'Circuit 4',
|
||||
'provider': providers[1].pk,
|
||||
'provider_account': provider_accounts[1].pk,
|
||||
'type': circuit_types[1].pk,
|
||||
},
|
||||
{
|
||||
'cid': 'Circuit 5',
|
||||
'provider': providers[1].pk,
|
||||
'provider_account': provider_accounts[1].pk,
|
||||
'type': circuit_types[1].pk,
|
||||
},
|
||||
{
|
||||
'cid': 'Circuit 6',
|
||||
'provider': providers[1].pk,
|
||||
'provider_account': provider_accounts[1].pk,
|
||||
'type': circuit_types[1].pk,
|
||||
},
|
||||
]
|
||||
|
@ -197,6 +206,49 @@ class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
|
|||
}
|
||||
|
||||
|
||||
class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ProviderAccount
|
||||
brief_fields = ['account', 'display', 'id', 'name', 'url']
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
provider_accounts = (
|
||||
ProviderAccount(name='Provider Account 1', provider=providers[0], account='1234'),
|
||||
ProviderAccount(name='Provider Account 2', provider=providers[0], account='2345'),
|
||||
ProviderAccount(name='Provider Account 3', provider=providers[0], account='3456'),
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
|
||||
cls.create_data = [
|
||||
{
|
||||
'name': 'Provider Account 4',
|
||||
'provider': providers[0].pk,
|
||||
'account': '4567',
|
||||
},
|
||||
{
|
||||
'name': 'Provider Account 5',
|
||||
'provider': providers[0].pk,
|
||||
'account': '5678',
|
||||
},
|
||||
{
|
||||
'name': 'Provider Account 6',
|
||||
'provider': providers[0].pk,
|
||||
'account': '6789',
|
||||
},
|
||||
]
|
||||
|
||||
cls.bulk_update_data = {
|
||||
'provider': providers[1].pk,
|
||||
'description': 'New description',
|
||||
}
|
||||
|
||||
|
||||
class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ProviderNetwork
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
|
|
|
@ -25,11 +25,11 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
ASN.objects.bulk_create(asns)
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1', account='1234'),
|
||||
Provider(name='Provider 2', slug='provider-2', account='2345'),
|
||||
Provider(name='Provider 3', slug='provider-3', account='3456'),
|
||||
Provider(name='Provider 4', slug='provider-4', account='4567'),
|
||||
Provider(name='Provider 5', slug='provider-5', account='5678'),
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
Provider(name='Provider 3', slug='provider-3'),
|
||||
Provider(name='Provider 4', slug='provider-4'),
|
||||
Provider(name='Provider 5', slug='provider-5'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
providers[0].asns.set([asns[0]])
|
||||
|
@ -64,8 +64,8 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
CircuitType.objects.bulk_create(circuit_types)
|
||||
|
||||
circuits = (
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Test Circuit 1'),
|
||||
Circuit(provider=providers[1], type=circuit_types[1], cid='Test Circuit 1'),
|
||||
Circuit(provider=providers[0], type=circuit_types[0], cid='Circuit 1'),
|
||||
Circuit(provider=providers[1], type=circuit_types[1], cid='Circuit 2'),
|
||||
)
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
||||
|
@ -87,10 +87,6 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
params = {'asn_id': [asns[0].pk, asns[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_account(self):
|
||||
params = {'account': ['1234', '2345']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||
|
@ -193,9 +189,17 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
Provider(name='Provider 3', slug='provider-3'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
provider_accounts = (
|
||||
ProviderAccount(name='Provider Account 1', provider=providers[0], account='A'),
|
||||
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)
|
||||
|
||||
provider_networks = (
|
||||
ProviderNetwork(name='Provider Network 1', provider=providers[1]),
|
||||
ProviderNetwork(name='Provider Network 2', provider=providers[1]),
|
||||
|
@ -204,12 +208,12 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
ProviderNetwork.objects.bulk_create(provider_networks)
|
||||
|
||||
circuits = (
|
||||
Circuit(provider=providers[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], 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], 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], 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], 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], 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[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[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[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[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)
|
||||
|
||||
|
@ -246,6 +250,11 @@ class CircuitTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
params = {'provider': [provider.slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_provider_account(self):
|
||||
provider_accounts = ProviderAccount.objects.all()[:2]
|
||||
params = {'provider_account_id': [provider_accounts[0].pk, provider_accounts[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
|
||||
def test_provider_network(self):
|
||||
provider_networks = ProviderNetwork.objects.all()[:2]
|
||||
params = {'provider_network_id': [provider_networks[0].pk, provider_networks[1].pk]}
|
||||
|
@ -445,3 +454,44 @@ class ProviderNetworkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ProviderAccountTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = ProviderAccount.objects.all()
|
||||
filterset = ProviderAccountFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
Provider(name='Provider 3', slug='provider-3'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
provider_accounts = (
|
||||
ProviderAccount(name='Provider Account 1', provider=providers[0], description='foobar1', account='1234'),
|
||||
ProviderAccount(name='Provider Account 2', provider=providers[1], description='foobar2', account='2345'),
|
||||
ProviderAccount(name='Provider Account 3', provider=providers[2], account='3456'),
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Provider Account 1', 'Provider Account 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_account(self):
|
||||
params = {'account': ['1234', '3456']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_description(self):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_provider(self):
|
||||
providers = Provider.objects.all()[:2]
|
||||
params = {'provider_id': [providers[0].pk, providers[1].pk]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'provider': [providers[0].slug, providers[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
|
|
@ -38,7 +38,6 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||
'name': 'Provider X',
|
||||
'slug': 'provider-x',
|
||||
'asns': [asns[6].pk, asns[7].pk],
|
||||
'account': '1234',
|
||||
'comments': 'Another provider',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
@ -58,7 +57,6 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'account': '5678',
|
||||
'comments': 'New comments',
|
||||
}
|
||||
|
||||
|
@ -124,6 +122,12 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
provider_accounts = (
|
||||
ProviderAccount(name='Provider Account 1', provider=providers[0], account='1234'),
|
||||
ProviderAccount(name='Provider Account 2', provider=providers[1], account='2345'),
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
|
||||
circuittypes = (
|
||||
CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
|
||||
CircuitType(name='Circuit Type 2', slug='circuit-type-2'),
|
||||
|
@ -131,9 +135,9 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||
CircuitType.objects.bulk_create(circuittypes)
|
||||
|
||||
circuits = (
|
||||
Circuit(cid='Circuit 1', provider=providers[0], type=circuittypes[0]),
|
||||
Circuit(cid='Circuit 2', provider=providers[0], type=circuittypes[0]),
|
||||
Circuit(cid='Circuit 3', provider=providers[0], type=circuittypes[0]),
|
||||
Circuit(cid='Circuit 1', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]),
|
||||
Circuit(cid='Circuit 2', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]),
|
||||
Circuit(cid='Circuit 3', provider=providers[0], provider_account=provider_accounts[0], type=circuittypes[0]),
|
||||
)
|
||||
|
||||
Circuit.objects.bulk_create(circuits)
|
||||
|
@ -143,6 +147,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||
cls.form_data = {
|
||||
'cid': 'Circuit X',
|
||||
'provider': providers[1].pk,
|
||||
'provider_account': provider_accounts[1].pk,
|
||||
'type': circuittypes[1].pk,
|
||||
'status': CircuitStatusChoices.STATUS_DECOMMISSIONED,
|
||||
'tenant': None,
|
||||
|
@ -155,10 +160,10 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"cid,provider,type,status",
|
||||
"Circuit 4,Provider 1,Circuit Type 1,active",
|
||||
"Circuit 5,Provider 1,Circuit Type 1,active",
|
||||
"Circuit 6,Provider 1,Circuit Type 1,active",
|
||||
"cid,provider,provider_account,type,status",
|
||||
"Circuit 4,Provider 1,Provider Account 1,Circuit Type 1,active",
|
||||
"Circuit 5,Provider 1,Provider Account 1,Circuit Type 1,active",
|
||||
"Circuit 6,Provider 1,Provider Account 1,Circuit Type 1,active",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
|
@ -170,6 +175,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||
|
||||
cls.bulk_edit_data = {
|
||||
'provider': providers[1].pk,
|
||||
'provider_account': provider_accounts[1].pk,
|
||||
'type': circuittypes[1].pk,
|
||||
'status': CircuitStatusChoices.STATUS_DECOMMISSIONED,
|
||||
'tenant': None,
|
||||
|
@ -179,6 +185,57 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||
}
|
||||
|
||||
|
||||
class ProviderAccountTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = ProviderAccount
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
providers = (
|
||||
Provider(name='Provider 1', slug='provider-1'),
|
||||
Provider(name='Provider 2', slug='provider-2'),
|
||||
)
|
||||
Provider.objects.bulk_create(providers)
|
||||
|
||||
provider_accounts = (
|
||||
ProviderAccount(name='Provider Account 1', provider=providers[0], account='1234'),
|
||||
ProviderAccount(name='Provider Account 2', provider=providers[0], account='2345'),
|
||||
ProviderAccount(name='Provider Account 3', provider=providers[0], account='3456'),
|
||||
)
|
||||
ProviderAccount.objects.bulk_create(provider_accounts)
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
cls.form_data = {
|
||||
'name': 'Provider Account X',
|
||||
'provider': providers[1].pk,
|
||||
'account': 'XXXX',
|
||||
'description': 'A new provider network',
|
||||
'comments': 'Longer description goes here',
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,provider,account,description",
|
||||
"Provider Account 4,Provider 1,4567,Foo",
|
||||
"Provider Account 5,Provider 1,5678,Bar",
|
||||
"Provider Account 6,Provider 1,6789,Baz",
|
||||
)
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,name,account,description",
|
||||
f"{provider_accounts[0].pk},Provider Network 7,7890,New description7",
|
||||
f"{provider_accounts[1].pk},Provider Network 8,8901,New description8",
|
||||
f"{provider_accounts[2].pk},Provider Network 9,9012,New description9",
|
||||
)
|
||||
|
||||
cls.bulk_edit_data = {
|
||||
'provider': providers[1].pk,
|
||||
'description': 'New description',
|
||||
'comments': 'New comments',
|
||||
}
|
||||
|
||||
|
||||
class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
model = ProviderNetwork
|
||||
|
||||
|
|
|
@ -14,6 +14,14 @@ urlpatterns = [
|
|||
path('providers/delete/', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'),
|
||||
path('providers/<int:pk>/', include(get_model_urls('circuits', 'provider'))),
|
||||
|
||||
# Provider accounts
|
||||
path('provider-accounts/', views.ProviderAccountListView.as_view(), name='provideraccount_list'),
|
||||
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/edit/', views.ProviderAccountBulkEditView.as_view(), name='provideraccount_bulk_edit'),
|
||||
path('provider-accounts/delete/', views.ProviderAccountBulkDeleteView.as_view(), name='provideraccount_bulk_delete'),
|
||||
path('provider-accounts/<int:pk>/', include(get_model_urls('circuits', 'provideraccount'))),
|
||||
|
||||
# Provider networks
|
||||
path('provider-networks/', views.ProviderNetworkListView.as_view(), name='providernetwork_list'),
|
||||
path('provider-networks/add/', views.ProviderNetworkEditView.as_view(), name='providernetwork_add'),
|
||||
|
|
|
@ -31,6 +31,7 @@ class ProviderView(generic.ObjectView):
|
|||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(ProviderAccount.objects.restrict(request.user, 'view').filter(provider=instance), 'provider_id'),
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(provider=instance), 'provider_id'),
|
||||
)
|
||||
|
||||
|
@ -72,6 +73,67 @@ class ProviderBulkDeleteView(generic.BulkDeleteView):
|
|||
table = tables.ProviderTable
|
||||
|
||||
|
||||
#
|
||||
# ProviderAccounts
|
||||
#
|
||||
|
||||
class ProviderAccountListView(generic.ObjectListView):
|
||||
queryset = ProviderAccount.objects.annotate(
|
||||
count_circuits=count_related(Circuit, 'provider_account')
|
||||
)
|
||||
filterset = filtersets.ProviderAccountFilterSet
|
||||
filterset_form = forms.ProviderAccountFilterForm
|
||||
table = tables.ProviderAccountTable
|
||||
|
||||
|
||||
@register_model_view(ProviderAccount)
|
||||
class ProviderAccountView(generic.ObjectView):
|
||||
queryset = ProviderAccount.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
(Circuit.objects.restrict(request.user, 'view').filter(provider_account=instance), 'provider_account_id'),
|
||||
)
|
||||
|
||||
return {
|
||||
'related_models': related_models,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(ProviderAccount, 'edit')
|
||||
class ProviderAccountEditView(generic.ObjectEditView):
|
||||
queryset = ProviderAccount.objects.all()
|
||||
form = forms.ProviderAccountForm
|
||||
|
||||
|
||||
@register_model_view(ProviderAccount, 'delete')
|
||||
class ProviderAccountDeleteView(generic.ObjectDeleteView):
|
||||
queryset = ProviderAccount.objects.all()
|
||||
|
||||
|
||||
class ProviderAccountBulkImportView(generic.BulkImportView):
|
||||
queryset = ProviderAccount.objects.all()
|
||||
model_form = forms.ProviderAccountImportForm
|
||||
table = tables.ProviderAccountTable
|
||||
|
||||
|
||||
class ProviderAccountBulkEditView(generic.BulkEditView):
|
||||
queryset = ProviderAccount.objects.annotate(
|
||||
count_circuits=count_related(Circuit, 'provider_account')
|
||||
)
|
||||
filterset = filtersets.ProviderAccountFilterSet
|
||||
table = tables.ProviderAccountTable
|
||||
form = forms.ProviderAccountBulkEditForm
|
||||
|
||||
|
||||
class ProviderAccountBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ProviderAccount.objects.annotate(
|
||||
count_circuits=count_related(Circuit, 'provider_account')
|
||||
)
|
||||
filterset = filtersets.ProviderAccountFilterSet
|
||||
table = tables.ProviderAccountTable
|
||||
|
||||
|
||||
#
|
||||
# Provider networks
|
||||
#
|
||||
|
|
|
@ -245,6 +245,7 @@ CIRCUITS_MENU = Menu(
|
|||
label=_('Providers'),
|
||||
items=(
|
||||
get_model_item('circuits', 'provider', _('Providers')),
|
||||
get_model_item('circuits', 'provideraccount', _('Provider Accounts')),
|
||||
get_model_item('circuits', 'providernetwork', _('Provider Networks')),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -18,6 +18,10 @@
|
|||
<th scope="row">Provider</th>
|
||||
<td>{{ object.provider|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Account</th>
|
||||
<td>{{ object.provider_account|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Circuit ID</th>
|
||||
<td>{{ object.cid }}</td>
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
<div class="row mb-2">
|
||||
<h5 class="offset-sm-3">Circuit Termination</h5>
|
||||
</div>
|
||||
{% render_field form.provider %}
|
||||
{% render_field form.circuit %}
|
||||
{% render_field form.term_side %}
|
||||
{% render_field form.tags %}
|
||||
|
|
|
@ -52,6 +52,14 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Provider Accounts</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'circuits:provideraccount_list' %}?provider_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Circuits</h5>
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
{% extends 'generic/object.html' %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li class="breadcrumb-item"><a href="{% url 'circuits:provideraccount_list' %}?provider_id={{ object.provider_id }}">{{ object.provider }}</a></li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-6">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Provider Account</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Provider</th>
|
||||
<td>{{ object.provider|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Account</th>
|
||||
<td>{{ object.account }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Name</th>
|
||||
<td>{{ object.name|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/contacts.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h5 class="card-header">Circuits</h5>
|
||||
<div class="card-body htmx-container table-responsive"
|
||||
hx-get="{% url 'circuits:circuit_list' %}?provider_account_id={{ object.pk }}"
|
||||
hx-trigger="load"
|
||||
></div>
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue