mirror of
https://github.com/netbox-community/netbox.git
synced 2026-03-21 20:18:38 -06:00
feat(circuits): Add UI layout panels for circuits app
Implement comprehensive UI panel layouts for all circuit models using the new panel system. Add panels for providers, circuits, terminations, groups, and virtual circuits with proper attribute rendering and actions.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.ui import actions, attrs, panels
|
||||
from utilities.data import resolve_attr_path
|
||||
|
||||
|
||||
class CircuitCircuitTerminationPanel(panels.ObjectPanel):
|
||||
"""
|
||||
A panel showing the CircuitTermination assigned to the object.
|
||||
"""
|
||||
|
||||
template_name = 'circuits/panels/circuit_circuit_termination.html'
|
||||
title = _('Termination')
|
||||
|
||||
def __init__(self, accessor=None, side=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if accessor is not None:
|
||||
self.accessor = accessor
|
||||
if side is not None:
|
||||
self.side = side
|
||||
|
||||
def get_context(self, context):
|
||||
return {
|
||||
**super().get_context(context),
|
||||
'side': self.side,
|
||||
'termination': resolve_attr_path(context, f'{self.accessor}.termination_{self.side.lower()}'),
|
||||
}
|
||||
|
||||
|
||||
class CircuitGroupAssignmentsPanel(panels.ObjectsTablePanel):
|
||||
"""
|
||||
A panel showing all Circuit Groups attached to the object.
|
||||
"""
|
||||
|
||||
title = _('Group Assignments')
|
||||
actions = [
|
||||
actions.AddObject(
|
||||
'circuits.CircuitGroupAssignment',
|
||||
url_params={
|
||||
'member_type': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk,
|
||||
'member': lambda ctx: ctx['object'].pk,
|
||||
'return_url': lambda ctx: ctx['object'].get_absolute_url(),
|
||||
},
|
||||
label=_('Assign Group'),
|
||||
),
|
||||
]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
'circuits.CircuitGroupAssignment',
|
||||
filters={
|
||||
'member_type_id': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk,
|
||||
'member_id': lambda ctx: ctx['object'].pk,
|
||||
},
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class CircuitGroupPanel(panels.OrganizationalObjectPanel):
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
|
||||
|
||||
class CircuitGroupAssignmentPanel(panels.ObjectAttributesPanel):
|
||||
group = attrs.RelatedObjectAttr('group', linkify=True)
|
||||
provider = attrs.RelatedObjectAttr('member.provider', linkify=True)
|
||||
member = attrs.GenericForeignKeyAttr('member', linkify=True)
|
||||
priority = attrs.ChoiceAttr('priority')
|
||||
|
||||
|
||||
class CircuitPanel(panels.ObjectAttributesPanel):
|
||||
provider = attrs.RelatedObjectAttr('provider', linkify=True)
|
||||
provider_account = attrs.RelatedObjectAttr('provider_account', linkify=True)
|
||||
cid = attrs.TextAttr('cid', label=_('Circuit ID'), style='font-monospace', copy_button=True)
|
||||
type = attrs.RelatedObjectAttr('type', linkify=True)
|
||||
status = attrs.ChoiceAttr('status')
|
||||
distance = attrs.NumericAttr('distance', unit_accessor='get_distance_unit_display')
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
install_date = attrs.DateTimeAttr('install_date', spec='date')
|
||||
termination_date = attrs.DateTimeAttr('termination_date', spec='date')
|
||||
commit_rate = attrs.TemplatedAttr('commit_rate', template_name='circuits/circuit/attrs/commit_rate.html')
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class CircuitTypePanel(panels.OrganizationalObjectPanel):
|
||||
color = attrs.ColorAttr('color')
|
||||
|
||||
|
||||
class ProviderPanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
asns = attrs.RelatedObjectListAttr('asns', linkify=True, label=_('ASNs'))
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class ProviderAccountPanel(panels.ObjectAttributesPanel):
|
||||
provider = attrs.RelatedObjectAttr('provider', linkify=True)
|
||||
account = attrs.TextAttr('account', style='font-monospace', copy_button=True)
|
||||
name = attrs.TextAttr('name')
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class ProviderNetworkPanel(panels.ObjectAttributesPanel):
|
||||
provider = attrs.RelatedObjectAttr('provider', linkify=True)
|
||||
name = attrs.TextAttr('name')
|
||||
service_id = attrs.TextAttr('service_id', label=_('Service ID'), style='font-monospace', copy_button=True)
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class VirtualCircuitTypePanel(panels.OrganizationalObjectPanel):
|
||||
color = attrs.ColorAttr('color')
|
||||
|
||||
|
||||
class VirtualCircuitPanel(panels.ObjectAttributesPanel):
|
||||
provider = attrs.RelatedObjectAttr('provider', linkify=True)
|
||||
provider_network = attrs.RelatedObjectAttr('provider_network', linkify=True)
|
||||
provider_account = attrs.RelatedObjectAttr('provider_account', linkify=True)
|
||||
cid = attrs.TextAttr('cid', label=_('Circuit ID'), style='font-monospace', copy_button=True)
|
||||
type = attrs.RelatedObjectAttr('type', linkify=True)
|
||||
status = attrs.ChoiceAttr('status')
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class VirtualCircuitTerminationPanel(panels.ObjectAttributesPanel):
|
||||
provider = attrs.RelatedObjectAttr('virtual_circuit.provider', linkify=True)
|
||||
provider_network = attrs.RelatedObjectAttr('virtual_circuit.provider_network', linkify=True)
|
||||
provider_account = attrs.RelatedObjectAttr('virtual_circuit.provider_account', linkify=True)
|
||||
virtual_circuit = attrs.RelatedObjectAttr('virtual_circuit', linkify=True)
|
||||
role = attrs.ChoiceAttr('role')
|
||||
|
||||
|
||||
class VirtualCircuitTerminationInterfacePanel(panels.ObjectAttributesPanel):
|
||||
title = _('Interface')
|
||||
|
||||
device = attrs.RelatedObjectAttr('interface.device', linkify=True)
|
||||
interface = attrs.RelatedObjectAttr('interface', linkify=True)
|
||||
type = attrs.ChoiceAttr('interface.type')
|
||||
description = attrs.TextAttr('interface.description')
|
||||
@@ -1,13 +1,23 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.views import PathTraceView
|
||||
from extras.ui.panels import CustomFieldsPanel, ImageAttachmentsPanel, TagsPanel
|
||||
from ipam.models import ASN
|
||||
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
|
||||
from netbox.ui import actions, layout
|
||||
from netbox.ui.panels import (
|
||||
CommentsPanel,
|
||||
ObjectsTablePanel,
|
||||
Panel,
|
||||
RelatedObjectsPanel,
|
||||
)
|
||||
from netbox.views import generic
|
||||
from utilities.query import count_related
|
||||
from utilities.views import GetRelatedModelsMixin, register_model_view
|
||||
|
||||
from . import filtersets, forms, tables
|
||||
from .models import *
|
||||
from .ui import panels
|
||||
|
||||
#
|
||||
# Providers
|
||||
@@ -29,6 +39,35 @@ class ProviderListView(generic.ObjectListView):
|
||||
@register_model_view(Provider)
|
||||
class ProviderView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Provider.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.ProviderPanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
model='circuits.ProviderAccount',
|
||||
filters={'provider_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'circuits.ProviderAccount', url_params={'provider': lambda ctx: ctx['object'].pk}
|
||||
),
|
||||
],
|
||||
),
|
||||
ObjectsTablePanel(
|
||||
model='circuits.Circuit',
|
||||
filters={'provider_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject('circuits.Circuit', url_params={'provider': lambda ctx: ctx['object'].pk}),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
@@ -108,6 +147,32 @@ class ProviderAccountListView(generic.ObjectListView):
|
||||
@register_model_view(ProviderAccount)
|
||||
class ProviderAccountView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = ProviderAccount.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.ProviderAccountPanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
CommentsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
model='circuits.Circuit',
|
||||
filters={'provider_account_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'circuits.Circuit',
|
||||
url_params={
|
||||
'provider': lambda ctx: ctx['object'].provider.pk,
|
||||
'provider_account': lambda ctx: ctx['object'].pk,
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
@@ -174,6 +239,32 @@ class ProviderNetworkListView(generic.ObjectListView):
|
||||
@register_model_view(ProviderNetwork)
|
||||
class ProviderNetworkView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.ProviderNetworkPanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
CommentsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
model='circuits.Circuit',
|
||||
filters={'provider_network_id': lambda ctx: ctx['object'].pk},
|
||||
),
|
||||
ObjectsTablePanel(
|
||||
model='circuits.VirtualCircuit',
|
||||
filters={'provider_network_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'circuits.VirtualCircuit', url_params={'provider_network': lambda ctx: ctx['object'].pk}
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
@@ -251,6 +342,17 @@ class CircuitTypeListView(generic.ObjectListView):
|
||||
@register_model_view(CircuitType)
|
||||
class CircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = CircuitType.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.CircuitTypePanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
CommentsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
@@ -318,6 +420,20 @@ class CircuitListView(generic.ObjectListView):
|
||||
@register_model_view(Circuit)
|
||||
class CircuitView(generic.ObjectView):
|
||||
queryset = Circuit.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.CircuitPanel(),
|
||||
panels.CircuitGroupAssignmentsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
panels.CircuitCircuitTerminationPanel(side='A'),
|
||||
panels.CircuitCircuitTerminationPanel(side='Z'),
|
||||
ImageAttachmentsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(Circuit, 'add', detail=False)
|
||||
@@ -390,6 +506,18 @@ class CircuitTerminationListView(generic.ObjectListView):
|
||||
@register_model_view(CircuitTermination)
|
||||
class CircuitTerminationView(generic.ObjectView):
|
||||
queryset = CircuitTermination.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
Panel(
|
||||
template_name='circuits/panels/circuit_termination.html',
|
||||
title=_('Circuit Termination'),
|
||||
)
|
||||
],
|
||||
right_panels=[
|
||||
CustomFieldsPanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(CircuitTermination, 'add', detail=False)
|
||||
@@ -446,6 +574,17 @@ class CircuitGroupListView(generic.ObjectListView):
|
||||
@register_model_view(CircuitGroup)
|
||||
class CircuitGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = CircuitGroup.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.CircuitGroupPanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
CommentsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
@@ -508,6 +647,15 @@ class CircuitGroupAssignmentListView(generic.ObjectListView):
|
||||
@register_model_view(CircuitGroupAssignment)
|
||||
class CircuitGroupAssignmentView(generic.ObjectView):
|
||||
queryset = CircuitGroupAssignment.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.CircuitGroupAssignmentPanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(CircuitGroupAssignment, 'add', detail=False)
|
||||
@@ -560,6 +708,17 @@ class VirtualCircuitTypeListView(generic.ObjectListView):
|
||||
@register_model_view(VirtualCircuitType)
|
||||
class VirtualCircuitTypeView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = VirtualCircuitType.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.VirtualCircuitTypePanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
CommentsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
@@ -627,6 +786,30 @@ class VirtualCircuitListView(generic.ObjectListView):
|
||||
@register_model_view(VirtualCircuit)
|
||||
class VirtualCircuitView(generic.ObjectView):
|
||||
queryset = VirtualCircuit.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.VirtualCircuitPanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
panels.CircuitGroupAssignmentsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
model='circuits.VirtualCircuitTermination',
|
||||
title=_('Terminations'),
|
||||
filters={'virtual_circuit_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'circuits.VirtualCircuitTermination',
|
||||
url_params={'virtual_circuit': lambda ctx: ctx['object'].pk},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuit, 'add', detail=False)
|
||||
@@ -698,6 +881,16 @@ class VirtualCircuitTerminationListView(generic.ObjectListView):
|
||||
@register_model_view(VirtualCircuitTermination)
|
||||
class VirtualCircuitTerminationView(generic.ObjectView):
|
||||
queryset = VirtualCircuitTermination.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.VirtualCircuitTerminationPanel(),
|
||||
TagsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
panels.VirtualCircuitTerminationInterfacePanel(),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(VirtualCircuitTermination, 'edit')
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from circuits.choices import CircuitStatusChoices, VirtualCircuitTerminationRoleChoices
|
||||
from circuits.models import (
|
||||
Provider,
|
||||
ProviderNetwork,
|
||||
VirtualCircuit,
|
||||
VirtualCircuitTermination,
|
||||
VirtualCircuitType,
|
||||
)
|
||||
from dcim.choices import InterfaceTypeChoices
|
||||
from dcim.models import Interface
|
||||
from netbox.ui import attrs
|
||||
from utilities.testing import create_test_device
|
||||
from vpn.choices import (
|
||||
AuthenticationAlgorithmChoices,
|
||||
AuthenticationMethodChoices,
|
||||
DHGroupChoices,
|
||||
EncryptionAlgorithmChoices,
|
||||
IKEModeChoices,
|
||||
IKEVersionChoices,
|
||||
IPSecModeChoices,
|
||||
)
|
||||
from vpn.models import IKEPolicy, IKEProposal, IPSecPolicy, IPSecProfile
|
||||
|
||||
|
||||
class ChoiceAttrTest(TestCase):
|
||||
"""
|
||||
Test class for validating the behavior of ChoiceAttr attribute accessor.
|
||||
|
||||
This test class verifies that the ChoiceAttr class correctly handles
|
||||
choice field attributes on Django model instances, including both direct
|
||||
field access and related object field access. It tests the retrieval of
|
||||
display values and associated context information such as color values
|
||||
for choice fields. The test data includes a network topology with devices,
|
||||
interfaces, providers, and virtual circuits to cover various scenarios of
|
||||
choice field access patterns.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
device = create_test_device('Device 1')
|
||||
interface = Interface.objects.create(
|
||||
device=device,
|
||||
name='vlan.100',
|
||||
type=InterfaceTypeChoices.TYPE_VIRTUAL,
|
||||
)
|
||||
|
||||
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
|
||||
provider_network = ProviderNetwork.objects.create(
|
||||
provider=provider,
|
||||
name='Provider Network 1',
|
||||
)
|
||||
virtual_circuit_type = VirtualCircuitType.objects.create(
|
||||
name='Virtual Circuit Type 1',
|
||||
slug='virtual-circuit-type-1',
|
||||
)
|
||||
virtual_circuit = VirtualCircuit.objects.create(
|
||||
cid='VC-100',
|
||||
provider_network=provider_network,
|
||||
type=virtual_circuit_type,
|
||||
status=CircuitStatusChoices.STATUS_ACTIVE,
|
||||
)
|
||||
|
||||
cls.termination = VirtualCircuitTermination.objects.create(
|
||||
virtual_circuit=virtual_circuit,
|
||||
role=VirtualCircuitTerminationRoleChoices.ROLE_PEER,
|
||||
interface=interface,
|
||||
)
|
||||
|
||||
def test_choice_attr_direct_accessor(self):
|
||||
attr = attrs.ChoiceAttr('role')
|
||||
|
||||
self.assertEqual(
|
||||
attr.get_value(self.termination),
|
||||
self.termination.get_role_display(),
|
||||
)
|
||||
self.assertEqual(
|
||||
attr.get_context(self.termination, {}),
|
||||
{'bg_color': self.termination.get_role_color()},
|
||||
)
|
||||
|
||||
def test_choice_attr_related_accessor(self):
|
||||
attr = attrs.ChoiceAttr('interface.type')
|
||||
|
||||
self.assertEqual(
|
||||
attr.get_value(self.termination),
|
||||
self.termination.interface.get_type_display(),
|
||||
)
|
||||
self.assertEqual(
|
||||
attr.get_context(self.termination, {}),
|
||||
{'bg_color': None},
|
||||
)
|
||||
|
||||
def test_choice_attr_related_accessor_with_color(self):
|
||||
attr = attrs.ChoiceAttr('virtual_circuit.status')
|
||||
|
||||
self.assertEqual(
|
||||
attr.get_value(self.termination),
|
||||
self.termination.virtual_circuit.get_status_display(),
|
||||
)
|
||||
self.assertEqual(
|
||||
attr.get_context(self.termination, {}),
|
||||
{'bg_color': self.termination.virtual_circuit.get_status_color()},
|
||||
)
|
||||
|
||||
|
||||
class RelatedObjectListAttrTest(TestCase):
|
||||
"""
|
||||
Test suite for RelatedObjectListAttr functionality.
|
||||
|
||||
This test class validates the behavior of the RelatedObjectListAttr class,
|
||||
which is used to render related objects as HTML lists. It tests various
|
||||
scenarios including direct accessor access, related accessor access through
|
||||
foreign keys, empty related object sets, and rendering with maximum item
|
||||
limits and overflow indicators. The tests use IKE and IPSec VPN policy
|
||||
models to verify proper rendering of one-to-many and many-to-many
|
||||
relationships between objects.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.proposals = (
|
||||
IKEProposal.objects.create(
|
||||
name='IKE Proposal 1',
|
||||
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
|
||||
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
|
||||
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
|
||||
group=DHGroupChoices.GROUP_14,
|
||||
),
|
||||
IKEProposal.objects.create(
|
||||
name='IKE Proposal 2',
|
||||
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
|
||||
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
|
||||
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
|
||||
group=DHGroupChoices.GROUP_14,
|
||||
),
|
||||
IKEProposal.objects.create(
|
||||
name='IKE Proposal 3',
|
||||
authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
|
||||
encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
|
||||
authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
|
||||
group=DHGroupChoices.GROUP_14,
|
||||
),
|
||||
)
|
||||
|
||||
cls.ike_policy = IKEPolicy.objects.create(
|
||||
name='IKE Policy 1',
|
||||
version=IKEVersionChoices.VERSION_1,
|
||||
mode=IKEModeChoices.MAIN,
|
||||
)
|
||||
cls.ike_policy.proposals.set(cls.proposals)
|
||||
|
||||
cls.empty_ike_policy = IKEPolicy.objects.create(
|
||||
name='IKE Policy 2',
|
||||
version=IKEVersionChoices.VERSION_1,
|
||||
mode=IKEModeChoices.MAIN,
|
||||
)
|
||||
|
||||
cls.ipsec_policy = IPSecPolicy.objects.create(name='IPSec Policy 1')
|
||||
|
||||
cls.profile = IPSecProfile.objects.create(
|
||||
name='IPSec Profile 1',
|
||||
mode=IPSecModeChoices.ESP,
|
||||
ike_policy=cls.ike_policy,
|
||||
ipsec_policy=cls.ipsec_policy,
|
||||
)
|
||||
cls.empty_profile = IPSecProfile.objects.create(
|
||||
name='IPSec Profile 2',
|
||||
mode=IPSecModeChoices.ESP,
|
||||
ike_policy=cls.empty_ike_policy,
|
||||
ipsec_policy=cls.ipsec_policy,
|
||||
)
|
||||
|
||||
def test_related_object_list_attr_direct_accessor(self):
|
||||
attr = attrs.RelatedObjectListAttr('proposals', linkify=False)
|
||||
rendered = attr.render(self.ike_policy, {'name': 'proposals'})
|
||||
|
||||
self.assertIn('list-unstyled mb-0', rendered)
|
||||
self.assertInHTML('<li>IKE Proposal 1</li>', rendered)
|
||||
self.assertInHTML('<li>IKE Proposal 2</li>', rendered)
|
||||
self.assertInHTML('<li>IKE Proposal 3</li>', rendered)
|
||||
self.assertEqual(rendered.count('<li'), 3)
|
||||
|
||||
def test_related_object_list_attr_related_accessor(self):
|
||||
attr = attrs.RelatedObjectListAttr('ike_policy.proposals', linkify=False)
|
||||
rendered = attr.render(self.profile, {'name': 'proposals'})
|
||||
|
||||
self.assertIn('list-unstyled mb-0', rendered)
|
||||
self.assertInHTML('<li>IKE Proposal 1</li>', rendered)
|
||||
self.assertInHTML('<li>IKE Proposal 2</li>', rendered)
|
||||
self.assertInHTML('<li>IKE Proposal 3</li>', rendered)
|
||||
self.assertEqual(rendered.count('<li'), 3)
|
||||
|
||||
def test_related_object_list_attr_empty_related_accessor(self):
|
||||
attr = attrs.RelatedObjectListAttr('ike_policy.proposals', linkify=False)
|
||||
|
||||
self.assertEqual(
|
||||
attr.render(self.empty_profile, {'name': 'proposals'}),
|
||||
attr.placeholder,
|
||||
)
|
||||
|
||||
def test_related_object_list_attr_max_items(self):
|
||||
attr = attrs.RelatedObjectListAttr(
|
||||
'ike_policy.proposals',
|
||||
linkify=False,
|
||||
max_items=2,
|
||||
overflow_indicator='…',
|
||||
)
|
||||
rendered = attr.render(self.profile, {'name': 'proposals'})
|
||||
|
||||
self.assertInHTML('<li>IKE Proposal 1</li>', rendered)
|
||||
self.assertInHTML('<li>IKE Proposal 2</li>', rendered)
|
||||
self.assertNotIn('IKE Proposal 3', rendered)
|
||||
self.assertIn('…', rendered)
|
||||
+106
-10
@@ -18,6 +18,7 @@ __all__ = (
|
||||
'NumericAttr',
|
||||
'ObjectAttribute',
|
||||
'RelatedObjectAttr',
|
||||
'RelatedObjectListAttr',
|
||||
'TemplatedAttr',
|
||||
'TextAttr',
|
||||
'TimezoneAttr',
|
||||
@@ -145,22 +146,40 @@ class ChoiceAttr(ObjectAttribute):
|
||||
"""
|
||||
A selection from a set of choices.
|
||||
|
||||
The class calls get_FOO_display() on the object to retrieve the human-friendly choice label. If a get_FOO_color()
|
||||
method exists on the object, it will be used to render a background color for the attribute value.
|
||||
The class calls get_FOO_display() on the terminal object resolved by the accessor
|
||||
to retrieve the human-friendly choice label. For example, accessor="interface.type"
|
||||
will call interface.get_type_display().
|
||||
If a get_FOO_color() method exists on that object, it will be used to render a
|
||||
background color for the attribute value.
|
||||
"""
|
||||
template_name = 'ui/attrs/choice.html'
|
||||
|
||||
def _resolve_target(self, obj):
|
||||
if not self.accessor or '.' not in self.accessor:
|
||||
return obj, self.accessor
|
||||
|
||||
object_accessor, field_name = self.accessor.rsplit('.', 1)
|
||||
return resolve_attr_path(obj, object_accessor), field_name
|
||||
|
||||
def get_value(self, obj):
|
||||
try:
|
||||
return getattr(obj, f'get_{self.accessor}_display')()
|
||||
except AttributeError:
|
||||
return resolve_attr_path(obj, self.accessor)
|
||||
target, field_name = self._resolve_target(obj)
|
||||
if target is None:
|
||||
return None
|
||||
|
||||
display = getattr(target, f'get_{field_name}_display', None)
|
||||
if callable(display):
|
||||
return display()
|
||||
|
||||
return resolve_attr_path(target, field_name)
|
||||
|
||||
def get_context(self, obj, context):
|
||||
try:
|
||||
bg_color = getattr(obj, f'get_{self.accessor}_color')()
|
||||
except AttributeError:
|
||||
bg_color = None
|
||||
target, field_name = self._resolve_target(obj)
|
||||
if target is None:
|
||||
return {'bg_color': None}
|
||||
|
||||
get_color = getattr(target, f'get_{field_name}_color', None)
|
||||
bg_color = get_color() if callable(get_color) else None
|
||||
|
||||
return {
|
||||
'bg_color': bg_color,
|
||||
}
|
||||
@@ -254,6 +273,83 @@ class RelatedObjectAttr(ObjectAttribute):
|
||||
}
|
||||
|
||||
|
||||
class RelatedObjectListAttr(RelatedObjectAttr):
|
||||
"""
|
||||
An attribute representing a list of related objects.
|
||||
|
||||
The accessor may resolve to a related manager or queryset.
|
||||
|
||||
Parameters:
|
||||
max_items (int): Maximum number of items to display
|
||||
overflow_indicator (str | None): Marker rendered as a final list item when
|
||||
additional objects exist beyond `max_items`; set to None to suppress it
|
||||
"""
|
||||
|
||||
template_name = 'ui/attrs/object_list.html'
|
||||
|
||||
def __init__(self, *args, max_items=None, overflow_indicator='…', **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if max_items is not None and (type(max_items) is not int or max_items < 1):
|
||||
raise ValueError(
|
||||
_('Invalid max_items value: {max_items}! Must be a positive integer or None.').format(
|
||||
max_items=max_items
|
||||
)
|
||||
)
|
||||
|
||||
self.max_items = max_items
|
||||
self.overflow_indicator = overflow_indicator
|
||||
|
||||
def _get_items(self, obj):
|
||||
"""
|
||||
Retrieve items from the given object using the accessor path.
|
||||
|
||||
Returns a tuple of (items, has_more) where items is a list of resolved objects
|
||||
and has_more indicates whether additional items exist beyond the max_items limit.
|
||||
"""
|
||||
items = resolve_attr_path(obj, self.accessor)
|
||||
if items is None:
|
||||
return [], False
|
||||
|
||||
if hasattr(items, 'all'):
|
||||
items = items.all()
|
||||
|
||||
if self.max_items is None:
|
||||
return list(items), False
|
||||
|
||||
items = list(items[:self.max_items + 1])
|
||||
has_more = len(items) > self.max_items
|
||||
|
||||
return items[:self.max_items], has_more
|
||||
|
||||
def get_context(self, obj, context):
|
||||
items, has_more = self._get_items(obj)
|
||||
|
||||
return {
|
||||
'linkify': self.linkify,
|
||||
'items': [
|
||||
{
|
||||
'value': item,
|
||||
'group': getattr(item, self.grouped_by, None) if self.grouped_by else None,
|
||||
}
|
||||
for item in items
|
||||
],
|
||||
'overflow_indicator': self.overflow_indicator if has_more else None,
|
||||
}
|
||||
|
||||
def render(self, obj, context):
|
||||
context = context or {}
|
||||
context_data = self.get_context(obj, context)
|
||||
|
||||
if not context_data['items']:
|
||||
return self.placeholder
|
||||
|
||||
return render_to_string(self.template_name, {
|
||||
'name': context.get('name'),
|
||||
**context_data,
|
||||
})
|
||||
|
||||
|
||||
class NestedObjectAttr(ObjectAttribute):
|
||||
"""
|
||||
An attribute representing a related nested object. Similar to `RelatedObjectAttr`, but includes the ancestors of the
|
||||
|
||||
@@ -67,6 +67,7 @@ class Panel:
|
||||
return {
|
||||
'request': context.get('request'),
|
||||
'object': context.get('object'),
|
||||
'perms': context.get('perms'),
|
||||
'title': self.title,
|
||||
'actions': self.actions,
|
||||
'panel_class': self.__class__.__name__,
|
||||
|
||||
@@ -1,104 +1,6 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li class="breadcrumb-item"><a href="{% url 'circuits:circuit_list' %}?provider_id={{ object.provider.pk }}">{{ object.provider }}</a></li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Circuit" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider" %}</th>
|
||||
<td>{{ object.provider|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Account" %}</th>
|
||||
<td>{{ object.provider_account|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Circuit ID" %}</th>
|
||||
<td>{{ object.cid }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Type" %}</th>
|
||||
<td>{{ object.type|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Distance" %}</th>
|
||||
<td>
|
||||
{% if object.distance is not None %}
|
||||
{{ object.distance|floatformat }} {{ object.get_distance_unit_display }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Install Date" %}</th>
|
||||
<td>{{ object.install_date|isodate|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Termination Date" %}</th>
|
||||
<td>{{ object.termination_date|isodate|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Commit Rate" %}</th>
|
||||
<td>{{ object.commit_rate|humanize_speed|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Group Assignments" %}
|
||||
{% if perms.circuits.add_circuitgroupassignment %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'circuits:circuitgroupassignment_add' %}?member_type={{ object|content_type_id }}&member={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Assign Group" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'circuits:circuitgroupassignment_list' circuit_id=object.pk %}
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
|
||||
{% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
|
||||
{% include 'inc/panels/image_attachments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load helpers %}
|
||||
{{ value|humanize_speed }}
|
||||
@@ -1,30 +0,0 @@
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Swap Circuit Terminations" %}{% endblock %}
|
||||
|
||||
{% block message %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Swap these terminations for circuit {{ circuit }}?
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>{% trans "A side" %}:</strong>
|
||||
{% if termination_a %}
|
||||
{{ termination_a.site }} {% if termination_a.interface %}- {{ termination_a.interface.device }} {{ termination_a.interface }}{% endif %}
|
||||
{% else %}
|
||||
{% trans "None" %}
|
||||
{% endif %}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{% trans "Z side" %}:</strong>
|
||||
{% if termination_z %}
|
||||
{{ termination_z.site }} {% if termination_z.interface %}- {{ termination_z.interface.device }} {{ termination_z.interface }}{% endif %}
|
||||
{% else %}
|
||||
{% trans "None" %}
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
@@ -1,8 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
@@ -17,40 +13,3 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Circuit Group" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
@@ -11,42 +6,3 @@
|
||||
<a href="{% url 'circuits:circuitgroupassignment_list' %}?group_id={{ object.group_id }}">{{ object.group }}</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Circuit Group Assignment" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Group" %}</th>
|
||||
<td>{{ object.group|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider" %}</th>
|
||||
<td>{{ object.member.provider|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Circuit" %}</th>
|
||||
<td>{{ object.member|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Priority" %}</th>
|
||||
<td>{{ object.get_priority_display }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -7,45 +7,3 @@
|
||||
{{ block.super }}
|
||||
<li class="breadcrumb-item"><a href="{% url 'circuits:circuit_list' %}?provider_id={{ object.circuit.provider.pk }}">{{ object.circuit.provider }}</a></li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
|
||||
<div class="card">
|
||||
{% if object %}
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Circuit" %}</th>
|
||||
<td>
|
||||
{{ object.circuit|linkify }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider" %}</th>
|
||||
<td>
|
||||
{{ object.circuit.provider|linkify }}
|
||||
</td>
|
||||
</tr>
|
||||
{% include 'circuits/inc/circuit_termination_fields.html' with termination=object %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="card-body">
|
||||
<span class="text-muted">{% trans "None" %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_controls %}
|
||||
@@ -11,46 +8,3 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Circuit Type" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Color" %}</th>
|
||||
<td>
|
||||
{% if object.color %}
|
||||
<span class="badge color-label" style="background-color: #{{ object.color }}"> </span>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=circuits.circuittermination&return_url={{ object.get_absolute_url }}">{% trans "Circuit Termination" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-header d-flex justify-content-between">
|
||||
{% blocktrans %}Termination{% endblocktrans %} {{ side }}
|
||||
<div class="card-actions">
|
||||
{% if not termination and perms.circuits.add_circuittermination %}
|
||||
<a href="{% url 'circuits:circuittermination_add' %}?circuit={{ object.pk }}&term_side={{ side }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-ghost-primary">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if termination and perms.circuits.change_circuittermination %}
|
||||
<a href="{% url 'circuits:circuittermination_edit' pk=termination.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-ghost-warning">
|
||||
<span class="mdi mdi-pencil" aria-hidden="true"></span> {% trans "Edit" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if termination and perms.circuits.delete_circuittermination %}
|
||||
<a href="{% url 'circuits:circuittermination_delete' pk=termination.pk %}?return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-ghost-danger">
|
||||
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> {% trans "Delete" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</h2>
|
||||
{% if termination %}
|
||||
<table class="table table-hover attr-table">
|
||||
{% include 'circuits/inc/circuit_termination_fields.html' with termination=termination %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tags" %}</th>
|
||||
<td>
|
||||
{% for tag in termination.tags.all %}
|
||||
{% tag tag %}
|
||||
{% empty %}
|
||||
{{ ''|placeholder }}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% for group_name, fields in termination.get_custom_fields_by_group.items %}
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<strong>{{ group_name|default:"Custom Fields" }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
{% for field, value in fields.items %}
|
||||
<tr>
|
||||
<th scope="row">{{ field }}
|
||||
{% if field.description %}
|
||||
<i
|
||||
class="mdi mdi-information text-primary"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="right"
|
||||
title="{{ field.description|escape }}"
|
||||
></i>
|
||||
{% endif %}
|
||||
</th>
|
||||
<td>
|
||||
{% customfield_value field value %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="card-body">
|
||||
<span class="text-muted">{% trans "None" %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
{% extends "ui/panels/_base.html" %}
|
||||
{% load helpers i18n %}
|
||||
|
||||
{% block panel_content %}
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Circuit" %}</th>
|
||||
<td>{{ object.circuit|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider" %}</th>
|
||||
<td>{{ object.circuit.provider|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
{% if object %}
|
||||
{% include 'circuits/inc/circuit_termination_fields.html' with termination=object %}
|
||||
{% endif %}
|
||||
</table>
|
||||
{% endblock panel_content %}
|
||||
@@ -12,52 +12,3 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Provider" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "ASNs" %}</th>
|
||||
<td>
|
||||
{% for asn in object.asns.all %}
|
||||
{{ asn|linkify }}{% if not forloop.last %}, {% endif %}
|
||||
{% empty %}
|
||||
{{ ''|placeholder }}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Provider Accounts" %}</h2>
|
||||
{% htmx_table 'circuits:provideraccount_list' provider_id=object.pk %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Circuits" %}</h2>
|
||||
{% htmx_table 'circuits:circuit_list' provider_id=object.pk %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,54 +1,6 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% 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-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Provider Account" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider" %}</th>
|
||||
<td>{{ object.provider|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Account" %}</th>
|
||||
<td>{{ object.account }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Circuits" %}</h2>
|
||||
{% htmx_table 'circuits:circuit_list' provider_account_id=object.pk %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,69 +1,6 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li class="breadcrumb-item"><a href="{% url 'circuits:providernetwork_list' %}?provider_id={{ object.provider_id }}">{{ object.provider }}</a></li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Provider Network" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider" %}</th>
|
||||
<td>{{ object.provider|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Service ID" %}</th>
|
||||
<td>{{ object.service_id|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Circuits" %}</h2>
|
||||
{% htmx_table 'circuits:circuit_list' provider_network_id=object.pk %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Virtual Circuits" %}
|
||||
{% if perms.circuits.add_virtualcircuit %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'circuits:virtualcircuit_add' %}?provider_network={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Virtual Circuit" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'circuits:virtualcircuit_list' provider_network_id=object.pk %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
@@ -12,90 +9,3 @@
|
||||
<a href="{% url 'circuits:virtualcircuit_list' %}?provider_network_id={{ object.provider_network.pk }}">{{ object.provider_network }}</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Virtual circuit" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider" %}</th>
|
||||
<td>{{ object.provider|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider Network" %}</th>
|
||||
<td>{{ object.provider_network|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider account" %}</th>
|
||||
<td>{{ object.provider_account|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Circuit ID" %}</th>
|
||||
<td>{{ object.cid }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Type" %}</th>
|
||||
<td>{{ object.type|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Group Assignments" %}
|
||||
{% if perms.circuits.add_circuitgroupassignment %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'circuits:circuitgroupassignment_add' %}?member_type={{ object|content_type_id }}&member={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Assign Group" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'circuits:circuitgroupassignment_list' virtual_circuit_id=object.pk %}
|
||||
</div>
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Terminations" %}
|
||||
{% if perms.circuits.add_virtualcircuittermination %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'circuits:virtualcircuittermination_add' %}?virtual_circuit={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Termination" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'circuits:virtualcircuittermination_list' virtual_circuit_id=object.pk %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
@@ -15,67 +13,3 @@
|
||||
<a href="{% url 'circuits:virtualcircuittermination_list' %}?virtual_circuit_id={{ object.virtual_circuit.pk }}">{{ object.virtual_circuit }}</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Virtual Circuit Termination" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider" %}</th>
|
||||
<td>{{ object.virtual_circuit.provider|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider Network" %}</th>
|
||||
<td>{{ object.virtual_circuit.provider_network|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Provider account" %}</th>
|
||||
<td>{{ object.virtual_circuit.provider_account|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Virtual circuit" %}</th>
|
||||
<td>{{ object.virtual_circuit|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Role" %}</th>
|
||||
<td>{% badge object.get_role_display bg_color=object.get_role_color %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Interface" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Device" %}</th>
|
||||
<td>{{ object.interface.device|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Interface" %}</th>
|
||||
<td>{{ object.interface|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Type" %}</th>
|
||||
<td>{{ object.interface.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.interface.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_controls %}
|
||||
@@ -11,46 +8,3 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Virtual Circuit Type" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Color" %}</th>
|
||||
<td>
|
||||
{% if object.color %}
|
||||
<span class="badge color-label" style="background-color: #{{ object.color }}"> </span>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<span{% if name %} id="attr_{{ name }}"{% endif %}>
|
||||
{% if linkify %}{{ value|linkify }}{% else %}{{ value }}{% endif %}{% if content_type %} ({{ content_type }}){% endif %}
|
||||
</span>
|
||||
{% load helpers %}
|
||||
<div{% if name %} id="attr_{{ name }}"{% endif %}>
|
||||
{% if linkify %}{{ value|linkify }}{% else %}{{ value }}{% endif %}
|
||||
{% if content_type %}<div class="fs-5 text-muted">{{ content_type|bettertitle }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<ul class="list-unstyled mb-0">
|
||||
{% for item in items %}
|
||||
<li>
|
||||
{% include "ui/attrs/object.html" with value=item.value group=item.group linkify=linkify only %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if overflow_indicator %}
|
||||
<li class="text-muted">{{ overflow_indicator }}</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
Reference in New Issue
Block a user