Compare commits

..

1 Commits

Author SHA1 Message Date
Martin Hauser
e902b13edb fix(tables): Disable ordering on non-orderable accessor columns
Mark provider, member, and action_object columns as non-orderable since
they use complex accessors that cannot be sorted. Add regression tests
to verify all orderable columns render without exceptions.

Fixes table rendering errors when attempting to sort columns with
multi-level field accessors that don't support database ordering.
2026-03-26 19:48:09 +01:00
64 changed files with 2324 additions and 1468 deletions

View File

@@ -54,8 +54,7 @@ python manage.py nbshell # NetBox-enhanced shell
## Architecture Conventions
- **Apps**: Each Django app owns its models, views, API serializers, filtersets, forms, and tests.
- **Views**: Use `register_model_view()` to register model views by action (e.g. "add", "list", etc.). List views typically don't need to add `select_related()` or `prefetch_related()` on their querysets: Prefetching is handled dynamically by the table class so that only relevant fields are prefetched.
- **REST API**: DRF serializers live in `<app>/api/serializers.py`; viewsets in `<app>/api/views.py`; URLs auto-registered in `<app>/api/urls.py`. REST API views typically don't need to add `select_related()` or `prefetch_related()` on their querysets: Prefetching is handled dynamically by the serializer so that only relevant fields are prefetched.
- **REST API**: DRF serializers live in `<app>/api/serializers.py`; viewsets in `<app>/api/views.py`; URLs auto-registered in `<app>/api/urls.py`.
- **GraphQL**: Strawberry types in `<app>/graphql/types.py`.
- **Filtersets**: `<app>/filtersets.py` — used for both UI filtering and API `?filter=` params.
- **Tables**: `django-tables2` used for all object list views (`<app>/tables.py`).
@@ -69,8 +68,6 @@ python manage.py nbshell # NetBox-enhanced shell
- API serializers must include a `url` field (absolute URL of the object).
- Use `FeatureQuery` for generic relations (config contexts, custom fields, tags, etc.).
- Avoid adding new dependencies without strong justification.
- Avoid running `ruff format` on existing files, as this tends to introduce unnecessary style changes.
- Don't craft Django database migrations manually: Prompt the user to run `manage.py makemigrations` instead.
## Branch & PR Conventions
- Branch naming: `<issue-number>-short-description` (e.g., `1234-device-typerror`)

View File

@@ -190,14 +190,16 @@ class CircuitGroupAssignmentTable(NetBoxTable):
provider = tables.Column(
accessor='member__provider',
verbose_name=_('Provider'),
linkify=True
orderable=False,
linkify=True,
)
member_type = columns.ContentTypeColumn(
verbose_name=_('Type')
)
member = tables.Column(
verbose_name=_('Circuit'),
linkify=True
orderable=False,
linkify=True,
)
priority = tables.Column(
verbose_name=_('Priority'),

View File

@@ -1,23 +1,48 @@
from django.test import RequestFactory, TestCase, tag
from circuits.models import CircuitTermination
from circuits.tables import CircuitTerminationTable
from circuits.models import CircuitGroupAssignment, CircuitTermination
from circuits.tables import CircuitGroupAssignmentTable, CircuitTerminationTable
@tag('regression')
class CircuitTerminationTableTest(TestCase):
def test_every_orderable_field_does_not_throw_exception(self):
terminations = CircuitTermination.objects.all()
disallowed = {'actions', }
disallowed = {
'actions',
}
orderable_columns = [
column.name for column in CircuitTerminationTable(terminations).columns
column.name
for column in CircuitTerminationTable(terminations).columns
if column.orderable and column.name not in disallowed
]
fake_request = RequestFactory().get("/")
fake_request = RequestFactory().get('/')
for col in orderable_columns:
for dir in ('-', ''):
for direction in ('-', ''):
table = CircuitTerminationTable(terminations)
table.order_by = f'{dir}{col}'
table.order_by = f'{direction}{col}'
table.as_html(fake_request)
@tag('regression')
class CircuitGroupAssignmentTableTest(TestCase):
def test_every_orderable_field_does_not_throw_exception(self):
assignment = CircuitGroupAssignment.objects.all()
disallowed = {
'actions',
}
orderable_columns = [
column.name
for column in CircuitGroupAssignmentTable(assignment).columns
if column.orderable and column.name not in disallowed
]
fake_request = RequestFactory().get('/')
for col in orderable_columns:
for direction in ('-', ''):
table = CircuitGroupAssignmentTable(assignment)
table.order_by = f'{direction}{col}'
table.as_html(fake_request)

View File

@@ -1,91 +0,0 @@
from django.utils.translation import gettext_lazy as _
from netbox.ui import attrs, panels
class DataSourcePanel(panels.ObjectAttributesPanel):
title = _('Data Source')
name = attrs.TextAttr('name')
type = attrs.ChoiceAttr('type')
enabled = attrs.BooleanAttr('enabled')
status = attrs.ChoiceAttr('status')
sync_interval = attrs.ChoiceAttr('sync_interval', label=_('Sync interval'))
last_synced = attrs.DateTimeAttr('last_synced', label=_('Last synced'))
description = attrs.TextAttr('description')
source_url = attrs.TemplatedAttr(
'source_url',
label=_('URL'),
template_name='core/datasource/attrs/source_url.html',
)
ignore_rules = attrs.TemplatedAttr(
'ignore_rules',
label=_('Ignore rules'),
template_name='core/datasource/attrs/ignore_rules.html',
)
class DataSourceBackendPanel(panels.ObjectPanel):
template_name = 'core/panels/datasource_backend.html'
title = _('Backend')
class DataFilePanel(panels.ObjectAttributesPanel):
title = _('Data File')
source = attrs.RelatedObjectAttr('source', linkify=True)
path = attrs.TextAttr('path', style='font-monospace', copy_button=True)
last_updated = attrs.DateTimeAttr('last_updated')
size = attrs.TemplatedAttr('size', template_name='core/datafile/attrs/size.html')
hash = attrs.TextAttr('hash', label=_('SHA256 hash'), style='font-monospace', copy_button=True)
class DataFileContentPanel(panels.ObjectPanel):
template_name = 'core/panels/datafile_content.html'
title = _('Content')
class JobPanel(panels.ObjectAttributesPanel):
title = _('Job')
object_type = attrs.TemplatedAttr(
'object_type',
label=_('Object type'),
template_name='core/job/attrs/object_type.html',
)
name = attrs.TextAttr('name')
status = attrs.ChoiceAttr('status')
error = attrs.TextAttr('error')
user = attrs.TextAttr('user', label=_('Created by'))
class JobSchedulingPanel(panels.ObjectAttributesPanel):
title = _('Scheduling')
created = attrs.DateTimeAttr('created')
scheduled = attrs.TemplatedAttr('scheduled', template_name='core/job/attrs/scheduled.html')
started = attrs.DateTimeAttr('started')
completed = attrs.DateTimeAttr('completed')
queue = attrs.TextAttr('queue_name', label=_('Queue'))
class ObjectChangePanel(panels.ObjectAttributesPanel):
title = _('Change')
time = attrs.DateTimeAttr('time')
user = attrs.TemplatedAttr(
'user_name',
label=_('User'),
template_name='core/objectchange/attrs/user.html',
)
action = attrs.ChoiceAttr('action')
changed_object_type = attrs.TextAttr(
'changed_object_type',
label=_('Object type'),
)
changed_object = attrs.TemplatedAttr(
'object_repr',
label=_('Object'),
template_name='core/objectchange/attrs/changed_object.html',
)
message = attrs.TextAttr('message')
request_id = attrs.TemplatedAttr(
'request_id',
label=_('Request ID'),
template_name='core/objectchange/attrs/request_id.html',
)

View File

@@ -23,20 +23,9 @@ from rq.worker import Worker
from rq.worker_registration import clean_worker_registry
from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs_from_status, requeue_rq_job, stop_rq_job
from extras.ui.panels import CustomFieldsPanel, TagsPanel
from netbox.config import PARAMS, get_config
from netbox.object_actions import AddObject, BulkDelete, BulkExport, DeleteObject
from netbox.plugins.utils import get_installed_plugins
from netbox.ui import layout
from netbox.ui.panels import (
CommentsPanel,
ContextTablePanel,
JSONPanel,
ObjectsTablePanel,
PluginContentPanel,
RelatedObjectsPanel,
TemplatePanel,
)
from netbox.views import generic
from netbox.views.generic.base import BaseObjectView
from netbox.views.generic.mixins import TableMixin
@@ -59,7 +48,6 @@ from .jobs import SyncDataSourceJob
from .models import *
from .plugins import get_catalog_plugins, get_local_plugins
from .tables import CatalogPluginTable, JobLogEntryTable, PluginVersionTable
from .ui import panels
#
# Data sources
@@ -79,24 +67,6 @@ class DataSourceListView(generic.ObjectListView):
@register_model_view(DataSource)
class DataSourceView(GetRelatedModelsMixin, generic.ObjectView):
queryset = DataSource.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.DataSourcePanel(),
TagsPanel(),
CommentsPanel(),
],
right_panels=[
panels.DataSourceBackendPanel(),
RelatedObjectsPanel(),
CustomFieldsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
model='core.DataFile',
filters={'source_id': lambda ctx: ctx['object'].pk},
),
],
)
def get_extra_context(self, request, instance):
return {
@@ -187,14 +157,6 @@ class DataFileListView(generic.ObjectListView):
class DataFileView(generic.ObjectView):
queryset = DataFile.objects.all()
actions = (DeleteObject,)
layout = layout.Layout(
layout.Row(
layout.Column(
panels.DataFilePanel(),
panels.DataFileContentPanel(),
),
),
)
@register_model_view(DataFile, 'delete')
@@ -226,17 +188,6 @@ class JobListView(generic.ObjectListView):
class JobView(generic.ObjectView):
queryset = Job.objects.all()
actions = (DeleteObject,)
layout = layout.SimpleLayout(
left_panels=[
panels.JobPanel(),
],
right_panels=[
panels.JobSchedulingPanel(),
],
bottom_panels=[
JSONPanel('data', title=_('Data')),
],
)
@register_model_view(Job, 'log')
@@ -249,13 +200,6 @@ class JobLogView(generic.ObjectView):
badge=lambda obj: len(obj.log_entries),
weight=500,
)
layout = layout.Layout(
layout.Row(
layout.Column(
ContextTablePanel('table', title=_('Log Entries')),
),
),
)
def get_extra_context(self, request, instance):
table = JobLogEntryTable(instance.log_entries)
@@ -297,26 +241,6 @@ class ObjectChangeListView(generic.ObjectListView):
@register_model_view(ObjectChange)
class ObjectChangeView(generic.ObjectView):
queryset = None
layout = layout.Layout(
layout.Row(
layout.Column(panels.ObjectChangePanel()),
layout.Column(TemplatePanel('core/panels/objectchange_difference.html')),
),
layout.Row(
layout.Column(TemplatePanel('core/panels/objectchange_prechange.html')),
layout.Column(TemplatePanel('core/panels/objectchange_postchange.html')),
),
layout.Row(
layout.Column(PluginContentPanel('left_page')),
layout.Column(PluginContentPanel('right_page')),
),
layout.Row(
layout.Column(
TemplatePanel('core/panels/objectchange_related.html'),
PluginContentPanel('full_width_page'),
),
),
)
def get_queryset(self, request):
return ObjectChange.objects.valid_models()
@@ -388,14 +312,6 @@ class ConfigRevisionListView(generic.ObjectListView):
@register_model_view(ConfigRevision)
class ConfigRevisionView(generic.ObjectView):
queryset = ConfigRevision.objects.all()
layout = layout.Layout(
layout.Row(
layout.Column(
TemplatePanel('core/panels/configrevision_data.html'),
TemplatePanel('core/panels/configrevision_comment.html'),
),
),
)
def get_extra_context(self, request, instance):
"""

View File

@@ -510,8 +510,9 @@ class EventRuleTable(NetBoxTable):
verbose_name=_('Type'),
)
action_object = tables.Column(
linkify=True,
verbose_name=_('Object'),
orderable=False,
linkify=True,
)
object_types = columns.ContentTypesColumn(
verbose_name=_('Object Types'),

View File

@@ -0,0 +1,24 @@
from django.test import RequestFactory, TestCase, tag
from extras.models import EventRule
from extras.tables import EventRuleTable
@tag('regression')
class EventRuleTableTest(TestCase):
def test_every_orderable_field_does_not_throw_exception(self):
rule = EventRule.objects.all()
disallowed = {
'actions',
}
orderable_columns = [
column.name for column in EventRuleTable(rule).columns if column.orderable and column.name not in disallowed
]
fake_request = RequestFactory().get('/')
for col in orderable_columns:
for direction in ('-', ''):
table = EventRuleTable(rule)
table.order_by = f'{direction}{col}'
table.as_html(fake_request)

View File

@@ -367,16 +367,6 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
def get_status_color(self):
return PrefixStatusChoices.colors.get(self.status)
@cached_property
def aggregate(self):
"""
Return the containing Aggregate for this Prefix, if any.
"""
try:
return Aggregate.objects.get(prefix__net_contains_or_equals=str(self.prefix))
except Aggregate.DoesNotExist:
return None
def get_parents(self, include_self=False):
"""
Return all containing Prefixes in the hierarchy.

View File

@@ -1,24 +0,0 @@
from django.template.loader import render_to_string
from netbox.ui import attrs
class VRFDisplayAttr(attrs.ObjectAttribute):
"""
Renders a VRF reference, displaying 'Global' when no VRF is assigned. Optionally includes
the route distinguisher (RD).
"""
template_name = 'ipam/attrs/vrf.html'
def __init__(self, *args, show_rd=False, **kwargs):
super().__init__(*args, **kwargs)
self.show_rd = show_rd
def render(self, obj, context):
value = self.get_value(obj)
return render_to_string(self.template_name, {
**self.get_context(obj, context),
'name': context['name'],
'value': value,
'show_rd': self.show_rd,
})

View File

@@ -2,15 +2,14 @@ from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from netbox.ui import actions, attrs, panels
from .attrs import VRFDisplayAttr
from netbox.ui import actions, panels
class FHRPGroupAssignmentsPanel(panels.ObjectPanel):
"""
A panel which lists all FHRP group assignments for a given object.
"""
template_name = 'ipam/panels/fhrp_groups.html'
title = _('FHRP Groups')
actions = [
@@ -36,220 +35,3 @@ class FHRPGroupAssignmentsPanel(panels.ObjectPanel):
label=_('Assign Group'),
),
]
class VRFPanel(panels.ObjectAttributesPanel):
rd = attrs.TextAttr('rd', label=_('Route Distinguisher'), style='font-monospace')
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
enforce_unique = attrs.BooleanAttr('enforce_unique', label=_('Unique IP Space'))
description = attrs.TextAttr('description')
class RouteTargetPanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name', style='font-monospace')
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
description = attrs.TextAttr('description')
class RIRPanel(panels.OrganizationalObjectPanel):
is_private = attrs.BooleanAttr('is_private', label=_('Private'))
class ASNRangePanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
rir = attrs.RelatedObjectAttr('rir', linkify=True, label=_('RIR'))
range = attrs.TextAttr('range_as_string_with_asdot', label=_('Range'))
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
description = attrs.TextAttr('description')
class ASNPanel(panels.ObjectAttributesPanel):
asn = attrs.TextAttr('asn_with_asdot', label=_('AS Number'))
rir = attrs.RelatedObjectAttr('rir', linkify=True, label=_('RIR'))
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
description = attrs.TextAttr('description')
class AggregatePanel(panels.ObjectAttributesPanel):
family = attrs.TextAttr('family', format_string='IPv{}', label=_('Family'))
rir = attrs.RelatedObjectAttr('rir', linkify=True, label=_('RIR'))
utilization = attrs.TemplatedAttr(
'prefix',
template_name='ipam/aggregate/attrs/utilization.html',
label=_('Utilization'),
)
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
date_added = attrs.DateTimeAttr('date_added', spec='date', label=_('Date Added'))
description = attrs.TextAttr('description')
class RolePanel(panels.OrganizationalObjectPanel):
weight = attrs.NumericAttr('weight')
class IPRangePanel(panels.ObjectAttributesPanel):
family = attrs.TextAttr('family', format_string='IPv{}', label=_('Family'))
start_address = attrs.TextAttr('start_address', label=_('Starting Address'))
end_address = attrs.TextAttr('end_address', label=_('Ending Address'))
size = attrs.NumericAttr('size')
mark_populated = attrs.BooleanAttr('mark_populated', label=_('Marked Populated'))
mark_utilized = attrs.BooleanAttr('mark_utilized', label=_('Marked Utilized'))
utilization = attrs.TemplatedAttr(
'utilization',
template_name='ipam/iprange/attrs/utilization.html',
label=_('Utilization'),
)
vrf = VRFDisplayAttr('vrf', label=_('VRF'), show_rd=True)
role = attrs.RelatedObjectAttr('role', linkify=True)
status = attrs.ChoiceAttr('status')
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
description = attrs.TextAttr('description')
class IPAddressPanel(panels.ObjectAttributesPanel):
family = attrs.TextAttr('family', format_string='IPv{}', label=_('Family'))
vrf = VRFDisplayAttr('vrf', label=_('VRF'))
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
status = attrs.ChoiceAttr('status')
role = attrs.ChoiceAttr('role')
dns_name = attrs.TextAttr('dns_name', label=_('DNS Name'))
description = attrs.TextAttr('description')
assigned_object = attrs.RelatedObjectAttr(
'assigned_object',
linkify=True,
grouped_by='parent_object',
label=_('Assignment'),
)
nat_inside = attrs.TemplatedAttr(
'nat_inside',
template_name='ipam/ipaddress/attrs/nat_inside.html',
label=_('NAT (inside)'),
)
nat_outside = attrs.TemplatedAttr(
'nat_outside',
template_name='ipam/ipaddress/attrs/nat_outside.html',
label=_('NAT (outside)'),
)
is_primary_ip = attrs.BooleanAttr('is_primary_ip', label=_('Primary IP'))
is_oob_ip = attrs.BooleanAttr('is_oob_ip', label=_('OOB IP'))
class PrefixPanel(panels.ObjectAttributesPanel):
family = attrs.TextAttr('family', format_string='IPv{}', label=_('Family'))
vrf = VRFDisplayAttr('vrf', label=_('VRF'))
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
aggregate = attrs.TemplatedAttr(
'aggregate',
template_name='ipam/prefix/attrs/aggregate.html',
label=_('Aggregate'),
)
scope = attrs.GenericForeignKeyAttr('scope', linkify=True)
vlan = attrs.RelatedObjectAttr('vlan', linkify=True, label=_('VLAN'), grouped_by='group')
status = attrs.ChoiceAttr('status')
role = attrs.RelatedObjectAttr('role', linkify=True)
description = attrs.TextAttr('description')
is_pool = attrs.BooleanAttr('is_pool', label=_('Is a pool'))
class VLANGroupPanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
description = attrs.TextAttr('description')
scope = attrs.GenericForeignKeyAttr('scope', linkify=True)
vid_ranges = attrs.TemplatedAttr(
'vid_ranges_items',
template_name='ipam/vlangroup/attrs/vid_ranges.html',
label=_('VLAN IDs'),
)
utilization = attrs.UtilizationAttr('utilization')
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
class VLANTranslationPolicyPanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
description = attrs.TextAttr('description')
class VLANTranslationRulePanel(panels.ObjectAttributesPanel):
policy = attrs.RelatedObjectAttr('policy', linkify=True)
local_vid = attrs.NumericAttr('local_vid', label=_('Local VID'))
remote_vid = attrs.NumericAttr('remote_vid', label=_('Remote VID'))
description = attrs.TextAttr('description')
class FHRPGroupPanel(panels.ObjectAttributesPanel):
protocol = attrs.ChoiceAttr('protocol')
group_id = attrs.NumericAttr('group_id', label=_('Group ID'))
name = attrs.TextAttr('name')
description = attrs.TextAttr('description')
member_count = attrs.NumericAttr('member_count', label=_('Members'))
class FHRPGroupAuthPanel(panels.ObjectAttributesPanel):
title = _('Authentication')
auth_type = attrs.ChoiceAttr('auth_type', label=_('Authentication Type'))
auth_key = attrs.TextAttr('auth_key', label=_('Authentication Key'))
class VLANPanel(panels.ObjectAttributesPanel):
region = attrs.NestedObjectAttr('site.region', linkify=True, label=_('Region'))
site = attrs.RelatedObjectAttr('site', linkify=True)
group = attrs.RelatedObjectAttr('group', linkify=True)
vid = attrs.NumericAttr('vid', label=_('VLAN ID'))
name = attrs.TextAttr('name')
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
status = attrs.ChoiceAttr('status')
role = attrs.RelatedObjectAttr('role', linkify=True)
description = attrs.TextAttr('description')
qinq_role = attrs.ChoiceAttr('qinq_role', label=_('Q-in-Q Role'))
qinq_svlan = attrs.RelatedObjectAttr('qinq_svlan', linkify=True, label=_('Q-in-Q SVLAN'))
l2vpn = attrs.RelatedObjectAttr('l2vpn_termination.l2vpn', linkify=True, label=_('L2VPN'))
class VLANCustomerVLANsPanel(panels.ObjectsTablePanel):
"""
A panel listing customer VLANs (C-VLANs) for an S-VLAN. Only renders when the VLAN has Q-in-Q
role 'svlan'.
"""
def __init__(self):
super().__init__(
'ipam.vlan',
filters={'qinq_svlan_id': lambda ctx: ctx['object'].pk},
title=_('Customer VLANs'),
actions=[
actions.AddObject(
'ipam.vlan',
url_params={
'qinq_role': 'cvlan',
'qinq_svlan': lambda ctx: ctx['object'].pk,
},
label=_('Add a VLAN'),
),
],
)
def render(self, context):
obj = context.get('object')
if not obj or obj.qinq_role != 'svlan':
return ''
return super().render(context)
class ServiceTemplatePanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
protocol = attrs.ChoiceAttr('protocol')
ports = attrs.TextAttr('port_list', label=_('Ports'))
description = attrs.TextAttr('description')
class ServicePanel(panels.ObjectAttributesPanel):
name = attrs.TextAttr('name')
parent = attrs.RelatedObjectAttr('parent', linkify=True)
protocol = attrs.ChoiceAttr('protocol')
ports = attrs.TextAttr('port_list', label=_('Ports'))
ip_addresses = attrs.TemplatedAttr(
'ipaddresses',
template_name='ipam/service/attrs/ip_addresses.html',
label=_('IP Addresses'),
)
description = attrs.TextAttr('description')

View File

@@ -9,16 +9,8 @@ from circuits.models import Provider
from dcim.filtersets import InterfaceFilterSet
from dcim.forms import InterfaceFilterForm
from dcim.models import Device, Interface, Site
from extras.ui.panels import CustomFieldsPanel, TagsPanel
from ipam.tables import VLANTranslationRuleTable
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
from netbox.ui import actions, layout
from netbox.ui.panels import (
CommentsPanel,
ContextTablePanel,
ObjectsTablePanel,
RelatedObjectsPanel,
TemplatePanel,
)
from netbox.views import generic
from utilities.query import count_related
from utilities.tables import get_table_ordering
@@ -31,7 +23,6 @@ from . import filtersets, forms, tables
from .choices import PrefixStatusChoices
from .constants import *
from .models import *
from .ui import panels
from .utils import add_available_vlans, add_requested_prefixes, annotate_ip_space
#
@@ -50,27 +41,6 @@ class VRFListView(generic.ObjectListView):
@register_model_view(VRF)
class VRFView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VRF.objects.all()
layout = layout.Layout(
layout.Row(
layout.Column(
panels.VRFPanel(),
TagsPanel(),
),
layout.Column(
RelatedObjectsPanel(),
CustomFieldsPanel(),
CommentsPanel(),
),
),
layout.Row(
layout.Column(
ContextTablePanel('import_targets_table', title=_('Import route targets')),
),
layout.Column(
ContextTablePanel('export_targets_table', title=_('Export route targets')),
),
),
)
def get_extra_context(self, request, instance):
import_targets_table = tables.RouteTargetTable(
@@ -164,50 +134,6 @@ class RouteTargetListView(generic.ObjectListView):
@register_model_view(RouteTarget)
class RouteTargetView(generic.ObjectView):
queryset = RouteTarget.objects.all()
layout = layout.Layout(
layout.Row(
layout.Column(
panels.RouteTargetPanel(),
TagsPanel(),
),
layout.Column(
CustomFieldsPanel(),
CommentsPanel(),
),
),
layout.Row(
layout.Column(
ObjectsTablePanel(
'ipam.vrf',
filters={'import_target_id': lambda ctx: ctx['object'].pk},
title=_('Importing VRFs'),
),
),
layout.Column(
ObjectsTablePanel(
'ipam.vrf',
filters={'export_target_id': lambda ctx: ctx['object'].pk},
title=_('Exporting VRFs'),
),
),
),
layout.Row(
layout.Column(
ObjectsTablePanel(
'vpn.l2vpn',
filters={'import_target_id': lambda ctx: ctx['object'].pk},
title=_('Importing L2VPNs'),
),
),
layout.Column(
ObjectsTablePanel(
'vpn.l2vpn',
filters={'export_target_id': lambda ctx: ctx['object'].pk},
title=_('Exporting L2VPNs'),
),
),
),
)
@register_model_view(RouteTarget, 'add', detail=False)
@@ -266,17 +192,6 @@ class RIRListView(generic.ObjectListView):
@register_model_view(RIR)
class RIRView(GetRelatedModelsMixin, generic.ObjectView):
queryset = RIR.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.RIRPanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CommentsPanel(),
CustomFieldsPanel(),
],
)
def get_extra_context(self, request, instance):
return {
@@ -342,16 +257,6 @@ class ASNRangeListView(generic.ObjectListView):
@register_model_view(ASNRange)
class ASNRangeView(generic.ObjectView):
queryset = ASNRange.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.ASNRangePanel(),
TagsPanel(),
],
right_panels=[
CommentsPanel(),
CustomFieldsPanel(),
],
)
@register_model_view(ASNRange, 'asns')
@@ -432,17 +337,6 @@ class ASNListView(generic.ObjectListView):
@register_model_view(ASN)
class ASNView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ASN.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.ASNPanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CustomFieldsPanel(),
CommentsPanel(),
],
)
def get_extra_context(self, request, instance):
return {
@@ -518,16 +412,6 @@ class AggregateListView(generic.ObjectListView):
@register_model_view(Aggregate)
class AggregateView(generic.ObjectView):
queryset = Aggregate.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.AggregatePanel(),
],
right_panels=[
CustomFieldsPanel(),
TagsPanel(),
CommentsPanel(),
],
)
@register_model_view(Aggregate, 'prefixes')
@@ -622,17 +506,6 @@ class RoleListView(generic.ObjectListView):
@register_model_view(Role)
class RoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Role.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.RolePanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CommentsPanel(),
CustomFieldsPanel(),
],
)
def get_extra_context(self, request, instance):
return {
@@ -696,23 +569,15 @@ class PrefixListView(generic.ObjectListView):
@register_model_view(Prefix)
class PrefixView(generic.ObjectView):
queryset = Prefix.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.PrefixPanel(),
],
right_panels=[
TemplatePanel('ipam/panels/prefix_addressing.html'),
CustomFieldsPanel(),
TagsPanel(),
CommentsPanel(),
],
bottom_panels=[
ContextTablePanel('duplicate_prefix_table', title=_('Duplicate prefixes')),
ContextTablePanel('parent_prefix_table', title=_('Parent prefixes')),
],
)
def get_extra_context(self, request, instance):
try:
aggregate = Aggregate.objects.restrict(request.user, 'view').get(
prefix__net_contains_or_equals=str(instance.prefix)
)
except Aggregate.DoesNotExist:
aggregate = None
# Parent prefixes table
parent_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
Q(vrf=instance.vrf) | Q(vrf__isnull=True, status=PrefixStatusChoices.STATUS_CONTAINER)
@@ -743,12 +608,11 @@ class PrefixView(generic.ObjectView):
)
duplicate_prefix_table.configure(request)
context = {
return {
'aggregate': aggregate,
'parent_prefix_table': parent_prefix_table,
'duplicate_prefix_table': duplicate_prefix_table,
}
if duplicate_prefixes.exists():
context['duplicate_prefix_table'] = duplicate_prefix_table
return context
@register_model_view(Prefix, 'prefixes')
@@ -892,19 +756,6 @@ class IPRangeListView(generic.ObjectListView):
@register_model_view(IPRange)
class IPRangeView(generic.ObjectView):
queryset = IPRange.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.IPRangePanel(),
],
right_panels=[
TagsPanel(),
CustomFieldsPanel(),
CommentsPanel(),
],
bottom_panels=[
ContextTablePanel('parent_prefixes_table', title=_('Parent prefixes')),
],
)
def get_extra_context(self, request, instance):
@@ -1002,23 +853,6 @@ class IPAddressListView(generic.ObjectListView):
@register_model_view(IPAddress)
class IPAddressView(generic.ObjectView):
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
layout = layout.SimpleLayout(
left_panels=[
panels.IPAddressPanel(),
TagsPanel(),
CustomFieldsPanel(),
CommentsPanel(),
],
right_panels=[
ContextTablePanel('parent_prefixes_table', title=_('Parent prefixes')),
ContextTablePanel('duplicate_ips_table', title=_('Duplicate IPs')),
ObjectsTablePanel(
'ipam.service',
filters={'ip_address_id': lambda ctx: ctx['object'].pk},
title=_('Application services'),
),
],
)
def get_extra_context(self, request, instance):
# Parent prefixes table
@@ -1051,12 +885,10 @@ class IPAddressView(generic.ObjectView):
duplicate_ips_table = tables.IPAddressTable(duplicate_ips[:10], orderable=False)
duplicate_ips_table.configure(request)
context = {
return {
'parent_prefixes_table': parent_prefixes_table,
'duplicate_ips_table': duplicate_ips_table,
}
if duplicate_ips.exists():
context['duplicate_ips_table'] = duplicate_ips_table
return context
@register_model_view(IPAddress, 'add', detail=False)
@@ -1206,17 +1038,6 @@ class VLANGroupListView(generic.ObjectListView):
@register_model_view(VLANGroup)
class VLANGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VLANGroup.objects.annotate_utilization()
layout = layout.SimpleLayout(
left_panels=[
panels.VLANGroupPanel(),
TagsPanel(),
],
right_panels=[
RelatedObjectsPanel(),
CommentsPanel(),
CustomFieldsPanel(),
],
)
def get_extra_context(self, request, instance):
return {
@@ -1304,32 +1125,19 @@ class VLANTranslationPolicyListView(generic.ObjectListView):
@register_model_view(VLANTranslationPolicy)
class VLANTranslationPolicyView(generic.ObjectView):
class VLANTranslationPolicyView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VLANTranslationPolicy.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.VLANTranslationPolicyPanel(),
],
right_panels=[
TagsPanel(),
CustomFieldsPanel(),
CommentsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
'ipam.vlantranslationrule',
filters={'policy_id': lambda ctx: ctx['object'].pk},
title=_('VLAN translation rules'),
actions=[
actions.AddObject(
'ipam.vlantranslationrule',
url_params={'policy': lambda ctx: ctx['object'].pk},
label=_('Add Rule'),
),
],
),
],
)
def get_extra_context(self, request, instance):
vlan_translation_table = VLANTranslationRuleTable(
data=instance.rules.all(),
orderable=False
)
vlan_translation_table.configure(request)
return {
'vlan_translation_table': vlan_translation_table,
}
@register_model_view(VLANTranslationPolicy, 'add', detail=False)
@@ -1385,17 +1193,13 @@ class VLANTranslationRuleListView(generic.ObjectListView):
@register_model_view(VLANTranslationRule)
class VLANTranslationRuleView(generic.ObjectView):
class VLANTranslationRuleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VLANTranslationRule.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.VLANTranslationRulePanel(),
],
right_panels=[
TagsPanel(),
CustomFieldsPanel(),
],
)
def get_extra_context(self, request, instance):
return {
'related_models': self.get_related_models(request, instance),
}
@register_model_view(VLANTranslationRule, 'add', detail=False)
@@ -1447,36 +1251,7 @@ class FHRPGroupListView(generic.ObjectListView):
@register_model_view(FHRPGroup)
class FHRPGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = FHRPGroup.objects.annotate(
member_count=count_related(FHRPGroupAssignment, 'group')
)
layout = layout.SimpleLayout(
left_panels=[
panels.FHRPGroupPanel(),
TagsPanel(),
CommentsPanel(),
],
right_panels=[
panels.FHRPGroupAuthPanel(),
RelatedObjectsPanel(),
CustomFieldsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
'ipam.ipaddress',
filters={'fhrpgroup_id': lambda ctx: ctx['object'].pk},
title=_('Virtual IP addresses'),
actions=[
actions.AddObject(
'ipam.ipaddress',
url_params={'fhrpgroup': lambda ctx: ctx['object'].pk},
label=_('Add IP Address'),
),
],
),
ContextTablePanel('members_table', title=_('Members')),
],
)
queryset = FHRPGroup.objects.all()
def get_extra_context(self, request, instance):
# Get assigned interfaces
@@ -1501,6 +1276,7 @@ class FHRPGroupView(GetRelatedModelsMixin, generic.ObjectView):
),
),
'members_table': members_table,
'member_count': FHRPGroupAssignment.objects.filter(group=instance).count(),
}
@@ -1603,35 +1379,17 @@ class VLANListView(generic.ObjectListView):
@register_model_view(VLAN)
class VLANView(generic.ObjectView):
queryset = VLAN.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.VLANPanel(),
],
right_panels=[
CustomFieldsPanel(),
TagsPanel(),
CommentsPanel(),
],
bottom_panels=[
ObjectsTablePanel(
'ipam.prefix',
filters={'vlan_id': lambda ctx: ctx['object'].pk},
title=_('Prefixes'),
actions=[
actions.AddObject(
'ipam.prefix',
url_params={
'tenant': lambda ctx: ctx['object'].tenant.pk if ctx['object'].tenant else None,
'site': lambda ctx: ctx['object'].site.pk if ctx['object'].site else None,
'vlan': lambda ctx: ctx['object'].pk,
},
label=_('Add a Prefix'),
),
],
),
panels.VLANCustomerVLANsPanel(),
],
)
def get_extra_context(self, request, instance):
prefixes = Prefix.objects.restrict(request.user, 'view').filter(vlan=instance).prefetch_related(
'vrf', 'scope', 'role', 'tenant'
)
prefix_table = tables.PrefixTable(list(prefixes), exclude=('vlan', 'utilization'), orderable=False)
prefix_table.configure(request)
return {
'prefix_table': prefix_table,
}
@register_model_view(VLAN, 'interfaces')
@@ -1725,16 +1483,6 @@ class ServiceTemplateListView(generic.ObjectListView):
@register_model_view(ServiceTemplate)
class ServiceTemplateView(generic.ObjectView):
queryset = ServiceTemplate.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.ServiceTemplatePanel(),
],
right_panels=[
CustomFieldsPanel(),
TagsPanel(),
CommentsPanel(),
],
)
@register_model_view(ServiceTemplate, 'add', detail=False)
@@ -1791,16 +1539,6 @@ class ServiceListView(generic.ObjectListView):
@register_model_view(Service)
class ServiceView(generic.ObjectView):
queryset = Service.objects.all()
layout = layout.SimpleLayout(
left_panels=[
panels.ServicePanel(),
],
right_panels=[
CustomFieldsPanel(),
TagsPanel(),
CommentsPanel(),
],
)
def get_extra_context(self, request, instance):
context = {}

View File

@@ -1,7 +1,10 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load custom_links %}
{% load helpers %}
{% load perms %}
{% load plugins %}
{% load static %}
{% load i18n %}
{% block breadcrumbs %}
@@ -24,3 +27,22 @@
</div>
{% endif %}
{% endblock subtitle %}
{% block content %}
<div class="row">
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">{% trans "Configuration Data" %}</h2>
{% include 'core/inc/config_data.html' %}
</div>
<div class="card">
<h2 class="card-header">{% trans "Comment" %}</h2>
<div class="card-body">
{{ object.comment|placeholder }}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,7 +1,62 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load custom_links %}
{% load helpers %}
{% load perms %}
{% load plugins %}
{% load i18n %}
{% block breadcrumbs %}
{{ block.super }}
<li class="breadcrumb-item"><a href="{% url 'core:datafile_list' %}?source_id={{ object.source.pk }}">{{ object.source }}</a></li>
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col">
<div class="card">
<h2 class="card-header">{% trans "Data File" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Source" %}</th>
<td>{{ object.source|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Path" %}</th>
<td>
<span class="font-monospace" id="datafile_path">{{ object.path }}</span>
{% copy_content "datafile_path" %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Last Updated" %}</th>
<td>{{ object.last_updated }}</td>
</tr>
<tr>
<th scope="row">{% trans "Size" %}</th>
<td>{{ object.size }} {% trans "bytes" %}</td>
</tr>
<tr>
<th scope="row">{% trans "SHA256 Hash" %}</th>
<td>
<span class="font-monospace" id="datafile_hash">{{ object.hash }}</span>
{% copy_content "datafile_hash" %}
</td>
</tr>
</table>
</div>
<div class="card">
<h2 class="card-header">{% trans "Content" %}</h2>
<div class="card-body">
<pre>{{ object.data_as_string }}</pre>
</div>
</div>
{% plugin_left_page object %}
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1 +0,0 @@
{% load i18n %}{{ value }} {% trans "bytes" %}

View File

@@ -1,4 +1,8 @@
{% extends 'generic/object.html' %}
{% load static %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block extra_controls %}
@@ -19,3 +23,102 @@
{% endif %}
{% endif %}
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Data Source" %}</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 "Type" %}</th>
<td>{{ object.get_type_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Enabled" %}</th>
<td>{% checkmark object.enabled %}</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 "Sync interval" %}</th>
<td>{{ object.get_sync_interval_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Last synced" %}</th>
<td>{{ object.last_synced|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "URL" %}</th>
<td>
{% if not object.type.is_local %}
<a href="{{ object.source_url }}">{{ object.source_url }}</a>
{% else %}
{{ object.source_url }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Ignore rules" %}</th>
<td>
{% if object.ignore_rules %}
<pre>{{ object.ignore_rules }}</pre>
{% else %}
{{ ''|placeholder }}
{% endif %}</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">
<div class="card">
<h2 class="card-header">{% trans "Backend" %}</h2>
{% with backend=object.backend_class %}
<table class="table table-hover attr-table">
{% for name, field in backend.parameters.items %}
<tr>
<th scope="row">{{ field.label }}</th>
{% if name in backend.sensitive_parameters %}
<td>********</td>
{% else %}
<td>{{ object.parameters|get_key:name|placeholder }}</td>
{% endif %}
</tr>
{% empty %}
<tr>
<td colspan="2" class="text-muted">
{% trans "No parameters defined" %}
</td>
</tr>
{% endfor %}
</table>
{% endwith %}
</div>
{% 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 "Files" %}</h2>
{% htmx_table 'core:datafile_list' source_id=object.pk %}
</div>
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1 +0,0 @@
<pre>{{ value }}</pre>

View File

@@ -1 +0,0 @@
{% if not object.type.is_local %}<a href="{{ value }}">{{ value }}</a>{% else %}{{ value }}{% endif %}

View File

@@ -1 +1,78 @@
{% extends 'core/job/base.html' %}
{% load i18n %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Job" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Object Type" %}</th>
<td>
<a href="{% url 'core:job_list' %}?object_type={{ object.object_type_id }}">{{ object.object_type }}</a>
</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Status" %}</th>
<td>{% badge object.get_status_display object.get_status_color %}</td>
</tr>
{% if object.error %}
<tr>
<th scope="row">{% trans "Error" %}</th>
<td>{{ object.error }}</td>
</tr>
{% endif %}
<tr>
<th scope="row">{% trans "Created By" %}</th>
<td>{{ object.user|placeholder }}</td>
</tr>
</table>
</div>
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Scheduling" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Created" %}</th>
<td>{{ object.created|isodatetime }}</td>
</tr>
<tr>
<th scope="row">{% trans "Scheduled" %}</th>
<td>
{{ object.scheduled|isodatetime|placeholder }}
{% if object.interval %}
({% blocktrans with interval=object.interval %}every {{ interval }} minutes{% endblocktrans %})
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Started" %}</th>
<td>{{ object.started|isodatetime|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Completed" %}</th>
<td>{{ object.completed|isodatetime|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Queue" %}</th>
<td>{{ object.queue_name|placeholder }}</td>
</tr>
</table>
</div>
</div>
</div>
<div class="row">
<div class="col col-12">
<div class="card">
<h2 class="card-header">{% trans "Data" %}</h2>
<pre class="card-body m-0">{{ object.data|json }}</pre>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1 +0,0 @@
<a href="{% url 'core:job_list' %}?object_type={{ object.object_type_id }}">{{ value }}</a>

View File

@@ -1,3 +0,0 @@
{% load helpers %}
{% load i18n %}
{{ value|isodatetime }}{% if object.interval %} ({% blocktrans with interval=object.interval %}every {{ interval }} minutes{% endblocktrans %}){% endif %}

View File

@@ -1 +1,12 @@
{% extends 'core/job/base.html' %}
{% load render_table from django_tables2 %}
{% block content %}
<div class="row mb-3">
<div class="col">
<div class="card">
{% render_table table %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,4 +1,6 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block title %}{{ object }}{% endblock %}
@@ -19,3 +21,161 @@
{# ObjectChange does not support the default add/edit/delete controls #}
{% block control-buttons %}{% endblock %}
{% block subtitle %}{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-5">
<div class="card">
<h2 class="card-header">{% trans "Change" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Time" %}</th>
<td>{{ object.time|isodatetime }}</td>
</tr>
<tr>
<th scope="row">{% trans "User" %}</th>
<td>
{% if object.user.get_full_name %}
{{ object.user.get_full_name }} ({{ object.user_name }})
{% else %}
{{ object.user_name }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Action" %}</th>
<td>
{{ object.get_action_display }}
</td>
</tr>
<tr>
<th scope="row">{% trans "Object Type" %}</th>
<td>
{{ object.changed_object_type }}
</td>
</tr>
<tr>
<th scope="row">{% trans "Object" %}</th>
<td>
{% if object.changed_object and object.changed_object.get_absolute_url %}
{{ object.changed_object|linkify }}
{% else %}
{{ object.object_repr }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Message" %}</th>
<td>
{{ object.message|placeholder }}
</td>
</tr>
<tr>
<th scope="row">{% trans "Request ID" %}</th>
<td>
<a href="{% url 'core:objectchange_list' %}?request_id={{ object.request_id }}">{{ object.request_id }}</a>
</td>
</tr>
</table>
</div>
</div>
<div class="col col-12 col-md-7">
<div class="card">
<h2 class="card-header d-flex justify-content-between">
{% trans "Difference" %}
<div class="btn-group btn-group-sm d-print-none">
<a {% if prev_change %}href="{% url 'core:objectchange' pk=prev_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
<i class="mdi mdi-chevron-left" aria-hidden="true"></i> {% trans "Previous" %}
</a>
<a {% if next_change %}href="{% url 'core:objectchange' pk=next_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
{% trans "Next" %} <i class="mdi mdi-chevron-right" aria-hidden="true"></i>
</a>
</div>
</h2>
<div class="card-body">
{% if diff_added == diff_removed %}
<span class="text-muted" style="margin-left: 10px;">
{% if object.action == 'create' %}
{% trans "Object Created" %}
{% elif object.action == 'delete' %}
{% trans "Object Deleted" %}
{% else %}
{% trans "No Changes" %}
{% endif %}
</span>
{% else %}
<pre class="change-diff change-removed">{{ diff_removed|json }}</pre>
<pre class="change-diff change-added">{{ diff_added|json }}</pre>
{% endif %}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Pre-Change Data" %}</h2>
<div class="card-body">
{% if object.prechange_data %}
{% spaceless %}
<pre class="change-data">
{% for k, v in object.prechange_data_clean.items %}
<span{% if k in diff_removed %} class="removed"{% endif %}>{{ k }}: {{ v|json }}</span>
{% endfor %}
</pre>
{% endspaceless %}
{% elif non_atomic_change %}
{% trans "Warning: Comparing non-atomic change to previous change record" %} (<a href="{% url 'core:objectchange' pk=prev_change.pk %}">{{ prev_change.pk }}</a>)
{% else %}
<span class="text-muted">{% trans "None" %}</span>
{% endif %}
</div>
</div>
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Post-Change Data" %}</h2>
<div class="card-body">
{% if object.postchange_data %}
{% spaceless %}
<pre class="change-data">
{% for k, v in object.postchange_data_clean.items %}
<span{% if k in diff_added %} class="added"{% endif %}>{{ k }}: {{ v|json }}</span>
{% endfor %}
</pre>
{% endspaceless %}
{% else %}
<span class="text-muted">{% trans "None" %}</span>
{% endif %}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col col-12 col-md-6">
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% include 'inc/panel_table.html' with table=related_changes_table heading='Related Changes' panel_class='default' %}
{% if related_changes_count > related_changes_table.rows|length %}
<div class="float-end">
<a href="{% url 'core:objectchange_list' %}?request_id={{ object.request_id }}" class="btn btn-primary">
{% blocktrans trimmed with count=related_changes_count|add:"1" %}
See All {{ count }} Changes
{% endblocktrans %}
</a>
</div>
{% endif %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,2 +0,0 @@
{% load helpers %}
{% if object.changed_object and object.changed_object.get_absolute_url %}{{ object.changed_object|linkify }}{% else %}{{ value }}{% endif %}

View File

@@ -1 +0,0 @@
<a href="{% url 'core:objectchange_list' %}?request_id={{ value }}">{{ value }}</a>

View File

@@ -1 +0,0 @@
{% if object.user and object.user.get_full_name %}{{ object.user.get_full_name }} ({{ value }}){% else %}{{ value }}{% endif %}

View File

@@ -1,11 +0,0 @@
{% load i18n %}
<div class="card">
<h2 class="card-header">{% trans "Comment" %}</h2>
<div class="card-body">
{% if object.comment %}
{{ object.comment }}
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</div>
</div>

View File

@@ -1,5 +0,0 @@
{% load i18n %}
<div class="card">
<h2 class="card-header">{% trans "Configuration Data" %}</h2>
{% include 'core/inc/config_data.html' %}
</div>

View File

@@ -1,8 +0,0 @@
{% extends "ui/panels/_base.html" %}
{% load i18n %}
{% block panel_content %}
<div class="card-body">
<pre>{{ object.data_as_string }}</pre>
</div>
{% endblock panel_content %}

View File

@@ -1,26 +0,0 @@
{% extends "ui/panels/_base.html" %}
{% load helpers %}
{% load i18n %}
{% block panel_content %}
{% with backend=object.backend_class %}
<table class="table table-hover attr-table">
{% for name, field in backend.parameters.items %}
<tr>
<th scope="row">{{ field.label }}</th>
{% if name in backend.sensitive_parameters %}
<td>********</td>
{% else %}
<td>{{ object.parameters|get_key:name|placeholder }}</td>
{% endif %}
</tr>
{% empty %}
<tr>
<td colspan="2" class="text-muted">
{% trans "No parameters defined" %}
</td>
</tr>
{% endfor %}
</table>
{% endwith %}
{% endblock panel_content %}

View File

@@ -1,31 +0,0 @@
{% load helpers %}
{% load i18n %}
<div class="card">
<h2 class="card-header d-flex justify-content-between">
{% trans "Difference" %}
<div class="btn-group btn-group-sm d-print-none">
<a {% if prev_change %}href="{% url 'core:objectchange' pk=prev_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
<i class="mdi mdi-chevron-left" aria-hidden="true"></i> {% trans "Previous" %}
</a>
<a {% if next_change %}href="{% url 'core:objectchange' pk=next_change.pk %}"{% else %}disabled{% endif %} class="btn btn-outline-secondary">
{% trans "Next" %} <i class="mdi mdi-chevron-right" aria-hidden="true"></i>
</a>
</div>
</h2>
<div class="card-body">
{% if diff_added == diff_removed %}
<span class="text-muted" style="margin-left: 10px;">
{% if object.action == 'create' %}
{% trans "Object Created" %}
{% elif object.action == 'delete' %}
{% trans "Object Deleted" %}
{% else %}
{% trans "No Changes" %}
{% endif %}
</span>
{% else %}
<pre class="change-diff change-removed">{{ diff_removed|json }}</pre>
<pre class="change-diff change-added">{{ diff_added|json }}</pre>
{% endif %}
</div>
</div>

View File

@@ -1,18 +0,0 @@
{% load helpers %}
{% load i18n %}
<div class="card">
<h2 class="card-header">{% trans "Post-Change Data" %}</h2>
<div class="card-body">
{% if object.postchange_data %}
{% spaceless %}
<pre class="change-data">
{% for k, v in object.postchange_data_clean.items %}
<span{% if k in diff_added %} class="added"{% endif %}>{{ k }}: {{ v|json }}</span>
{% endfor %}
</pre>
{% endspaceless %}
{% else %}
<span class="text-muted">{% trans "None" %}</span>
{% endif %}
</div>
</div>

View File

@@ -1,20 +0,0 @@
{% load helpers %}
{% load i18n %}
<div class="card">
<h2 class="card-header">{% trans "Pre-Change Data" %}</h2>
<div class="card-body">
{% if object.prechange_data %}
{% spaceless %}
<pre class="change-data">
{% for k, v in object.prechange_data_clean.items %}
<span{% if k in diff_removed %} class="removed"{% endif %}>{{ k }}: {{ v|json }}</span>
{% endfor %}
</pre>
{% endspaceless %}
{% elif non_atomic_change %}
{% trans "Warning: Comparing non-atomic change to previous change record" %} (<a href="{% url 'core:objectchange' pk=prev_change.pk %}">{{ prev_change.pk }}</a>)
{% else %}
<span class="text-muted">{% trans "None" %}</span>
{% endif %}
</div>
</div>

View File

@@ -1,11 +0,0 @@
{% load i18n %}
{% include 'inc/panel_table.html' with table=related_changes_table heading='Related Changes' panel_class='default' %}
{% if related_changes_count > related_changes_table.rows|length %}
<div class="float-end">
<a href="{% url 'core:objectchange_list' %}?request_id={{ object.request_id }}" class="btn btn-primary">
{% blocktrans trimmed with count=related_changes_count|add:"1" %}
See All {{ count }} Changes
{% endblocktrans %}
</a>
</div>
{% endif %}

View File

@@ -3,7 +3,7 @@
<div class="card {% if panel_class %}border-{{ panel_class }}{% endif %}">
{% if heading %}
<h2 class="card-header{% if panel_class %} text-{{ panel_class }}{% endif %}">{% trans heading %}</h2>
<h2 class="card-header{% if panel_class %} text-{{ panel_class }}{% endif %}">{{ heading }}</h2>
{% endif %}
{% if table.rows %}
<div class="table-responsive">

View File

@@ -1 +1,62 @@
{% extends 'ipam/aggregate/base.html' %}
{% load buttons %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Aggregate" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Family" %}</th>
<td>IPv{{ object.family }}</td>
</tr>
<tr>
<th scope="row">{% trans "RIR" %}</th>
<td>
<a href="{% url 'ipam:aggregate_list' %}?rir={{ object.rir.slug }}">{{ object.rir }}</a>
</td>
</tr>
<tr>
<th scope="row">{% trans "Utilization" %}</th>
<td>
{% utilization_graph object.get_utilization %}
</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 "Date Added" %}</th>
<td>{{ object.date_added|isodate|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</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' %}
{% include 'inc/panels/comments.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 %}

View File

@@ -1,2 +0,0 @@
{% load helpers %}
{% utilization_graph object.get_utilization %}

View File

@@ -1,4 +1,8 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block breadcrumbs %}
@@ -8,3 +12,51 @@
<li class="breadcrumb-item"><a href="{% url 'ipam:asn_list' %}?range_id={{ object.range.pk }}">{{ object.range }}</a></li>
{% endif %}
{% endblock breadcrumbs %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "ASN" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "AS Number" %}</th>
<td>{{ object.asn_with_asdot }}</td>
</tr>
<tr>
<th scope="row">{% trans "RIR" %}</th>
<td>
<a href="{% url 'ipam:asn_list' %}?rir={{ object.rir.slug }}">{{ object.rir }}</a>
</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>
{% plugin_left_page object %}
{% include 'inc/panels/tags.html' %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock content %}

View File

@@ -1 +1,57 @@
{% extends 'ipam/asnrange/base.html' %}
{% load buttons %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "ASN Range" %}</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 "RIR" %}</th>
<td>
<a href="{% url 'ipam:asnrange_list' %}?rir={{ object.rir.slug }}">{{ object.rir }}</a>
</td>
</tr>
<tr>
<th scope="row">{% trans "Range" %}</th>
<td>{{ object.range_as_string_with_asdot }}</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>
{% plugin_left_page object %}
{% include 'inc/panels/tags.html' %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panels/comments.html' %}
{% 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 content %}

View File

@@ -1,2 +0,0 @@
{% load helpers i18n %}
{% if value %}{{ value|linkify }}{% if show_rd %} ({{ value.rd }}){% endif %}{% else %}<span>{% trans "Global" %}</span>{% endif %}

View File

@@ -1,4 +1,7 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{# Omit assigned IP addresses from object representation #}
@@ -8,3 +11,75 @@
{{ block.super }}
<li class="breadcrumb-item"><a href="{% url 'ipam:fhrpgroup_list' %}?protocol={{ object.protocol }}">{{ object.get_protocol_display }}</a></li>
{% endblock breadcrumbs %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "FHRP Group" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Protocol" %}</th>
<td>{{ object.get_protocol_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Group ID" %}</th>
<td>{{ object.group_id }}</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>
<tr>
<th scope="row">{% trans "Members" %}</th>
<td>{{ member_count }}</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">
<div class="card">
<h2 class="card-header">{% trans "Authentication" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Authentication Type" %}</th>
<td>{{ object.get_auth_type_display|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Authentication Key" %}</th>
<td>{{ object.auth_key|placeholder }}</td>
</tr>
</table>
</div>
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">
{% trans "Virtual IP Addresses" %}
{% if perms.ipam.add_ipaddress %}
<div class="card-actions">
<a href="{% url 'ipam:ipaddress_add' %}?fhrpgroup={{ 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 IP Address" %}
</a>
</div>
{% endif %}
</h2>
{% htmx_table 'ipam:ipaddress_list' fhrpgroup_id=object.pk %}
</div>
{% include 'inc/panel_table.html' with table=members_table heading='Members' %}
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1 +1,129 @@
{% extends 'ipam/ipaddress/base.html' %}
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-4">
<div class="card">
<h2 class="card-header">{% trans "IP Address" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Family" %}</th>
<td>IPv{{ object.family }}</td>
</tr>
<tr>
<th scope="row">{% trans "VRF" %}</th>
<td>
{% if object.vrf %}
<a href="{% url 'ipam:vrf' pk=object.vrf.pk %}">{{ object.vrf }}</a>
{% else %}
<span>{% trans "Global" %}</span>
{% 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 "Status" %}</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
</tr>
<tr>
<th scope="row">{% trans "Role" %}</th>
<td>
{% if object.role %}
<a href="{% url 'ipam:ipaddress_list' %}?role={{ object.role }}">{{ object.get_role_display }}</a>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "DNS Name" %}</th>
<td>{{ object.dns_name|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Assignment" %}</th>
<td>
{% if object.assigned_object %}
{% if object.assigned_object.parent_object %}
{{ object.assigned_object.parent_object|linkify }} /
{% endif %}
{{ object.assigned_object|linkify }}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "NAT (inside)" %}</th>
<td>
{% if object.nat_inside %}
{{ object.nat_inside|linkify }}
{% if object.nat_inside.assigned_object %}
({{ object.nat_inside.assigned_object.parent_object|linkify }})
{% endif %}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "NAT (outside)" %}</th>
<td>
{% for ip in object.nat_outside.all %}
{{ ip|linkify }}
{% if ip.assigned_object %}
({{ ip.assigned_object.parent_object|linkify }})
{% endif %}<br/>
{% empty %}
{{ ''|placeholder }}
{% endfor %}
</td>
</tr>
<tr>
<th scope="row">Primary IP</th>
<td>{% checkmark object.is_primary_ip %}</td>
</tr>
<tr>
<th scope="row">OOB IP</th>
<td>{% checkmark object.is_oob_ip %}</td>
</tr>
</table>
</div>
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-8">
{% include 'inc/panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
{% if duplicate_ips_table.rows %}
{% include 'inc/panel_table.html' with table=duplicate_ips_table heading='Duplicate IPs' panel_class='danger' %}
{% endif %}
<div class="card">
<h2 class="card-header">{% trans "Application Services" %}</h2>
{% htmx_table 'ipam:service_list' ip_address_id=object.pk %}
</div>
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,2 +0,0 @@
{% load helpers %}
{% if value.parent_object %}{{ value.parent_object|linkify }} / {% endif %}{{ value|linkify }}

View File

@@ -1,2 +0,0 @@
{% load helpers %}
{{ value|linkify }}{% if value.assigned_object %} ({{ value.assigned_object.parent_object|linkify }}){% endif %}

View File

@@ -1,2 +0,0 @@
{% load helpers %}
{% for ip in value.all %}{{ ip|linkify }}{% if ip.assigned_object %} ({{ ip.assigned_object.parent_object|linkify }}){% endif %}<br/>{% empty %}<span class="text-muted">&mdash;</span>{% endfor %}

View File

@@ -1 +1,98 @@
{% extends 'ipam/iprange/base.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "IP Range" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Family" %}</th>
<td>IPv{{ object.family }}</td>
</tr>
<tr>
<th scope="row">{% trans "Starting Address" %}</th>
<td>{{ object.start_address }}</td>
</tr>
<tr>
<th scope="row">{% trans "Ending Address" %}</th>
<td>{{ object.end_address }}</td>
</tr>
<tr>
<th scope="row">{% trans "Size" %}</th>
<td>{{ object.size }}</td>
</tr>
<tr>
<th scope="row">{% trans "Marked Populated" %}</th>
<td>{% checkmark object.mark_populated %}</td>
</tr>
<tr>
<th scope="row">{% trans "Marked Utilized" %}</th>
<td>{% checkmark object.mark_utilized %}</td>
</tr>
<tr>
<th scope="row">{% trans "Utilization" %}</th>
<td>
{% if object.mark_utilized %}
{% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
{% else %}
{% utilization_graph object.utilization %}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "VRF" %}</th>
<td>
{% if object.vrf %}
{{ object.vrf|linkify }} ({{ object.vrf.rd }})
{% else %}
<span>{% trans "Global" %}</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Role" %}</th>
<td>{{ object.role|linkify|placeholder }}</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>
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% include 'inc/panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,6 +0,0 @@
{% load helpers %}
{% if object.mark_utilized %}
{% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
{% else %}
{% utilization_graph value %}
{% endif %}

View File

@@ -1,62 +0,0 @@
{% load humanize helpers i18n %}
<div class="card">
<h2 class="card-header">
{% trans "Addressing" %}
{% if object.prefix.version == 4 %}
<div class="card-actions">
<a class="btn btn-ghost-primary btn-sm" data-bs-toggle="modal" data-bs-target="#prefix-modal">
<i class="mdi mdi-information-outline" aria-hidden="true"></i> {% trans "Addressing Details" %}
</a>
</div>
{% endif %}
</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Utilization" %}</th>
<td>
{% if object.mark_utilized %}
{% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
<small>({% trans "Marked fully utilized" %})</small>
{% else %}
{% utilization_graph object.get_utilization %}
{% endif %}
</td>
</tr>
{% with child_ip_count=object.get_child_ips.count %}
<tr>
<th scope="row">{% trans "Child IPs" %}</th>
<td>
<a href="{% url 'ipam:prefix_ipaddresses' pk=object.pk %}">{{ child_ip_count }}</a>
</td>
</tr>
{% endwith %}
{% with available_count=object.get_available_ips.size %}
<tr>
<th scope="row">{% trans "Available IPs" %}</th>
<td>
{% if available_count > 1000000 %}
{{ available_count|intword }}
{% else %}
{{ available_count|intcomma }}
{% endif %}
</td>
</tr>
{% endwith %}
<tr>
<th scope="row">{% trans "First available IP" %}</th>
<td>
{% with first_available_ip=object.get_first_available_ip %}
{% if first_available_ip %}
{% if perms.ipam.add_ipaddress %}
<a href="{% url 'ipam:ipaddress_add' %}?address={{ first_available_ip }}{% if object.vrf %}&vrf={{ object.vrf_id }}{% endif %}{% if object.tenant %}&tenant={{ object.tenant.pk }}{% endif %}">{{ first_available_ip }}</a>
{% else %}
{{ first_available_ip }}
{% endif %}
{% else %}
{{ ''|placeholder }}
{% endif %}
{% endwith %}
</td>
</tr>
</table>
</div>

View File

@@ -1 +1,202 @@
{% extends 'ipam/prefix/base.html' %}
{% load humanize %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% load mptt %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Prefix" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Family" %}</th>
<td>IPv{{ object.family }}</td>
</tr>
<tr>
<th scope="row">{% trans "VRF" %}</th>
<td>
{% if object.vrf %}
<a href="{% url 'ipam:vrf' pk=object.vrf.pk %}">{{ object.vrf }}</a>
{% else %}
<span>{% trans "Global" %}</span>
{% 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 "Aggregate" %}</th>
<td>
{% if aggregate %}
<a href="{% url 'ipam:aggregate' pk=aggregate.pk %}">{{ aggregate.prefix }}</a> ({{ aggregate.rir }})
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Scope" %}</th>
{% if object.scope %}
<td>{{ object.scope|linkify }} ({% trans object.scope_type.name %})</td>
{% else %}
<td>{{ ''|placeholder }}</td>
{% endif %}
</tr>
<tr>
<th scope="row">{% trans "VLAN" %}</th>
<td>
{% if object.vlan %}
{% if object.vlan.group %}
{{ object.vlan.group|linkify }} /
{% endif %}
{{ object.vlan|linkify }}
{% else %}
{{ ''|placeholder }}
{% endif %}
</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 "Role" %}</th>
<td>{{ object.role|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Is a pool" %}</th>
<td>{% checkmark object.is_pool %}</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">
{% trans "Addressing" %}
{% if object.prefix.version == 4 %}
<div class="card-actions">
<a class="btn btn-ghost-primary btn-sm" data-bs-toggle="modal" data-bs-target="#prefix-modal">
<i class="mdi mdi-information-outline" aria-hidden="true"></i> {% trans "Addressing Details" %}
</a>
</div>
{% endif %}
</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Utilization" %}</th>
<td>
{% if object.mark_utilized %}
{% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
<small>({% trans "Marked fully utilized" %})</small>
{% else %}
{% utilization_graph object.get_utilization %}
{% endif %}
</td>
</tr>
{% with child_ip_count=object.get_child_ips.count %}
<tr>
<th scope="row">{% trans "Child IPs" %}</th>
<td>
<a href="{% url 'ipam:prefix_ipaddresses' pk=object.pk %}">{{ child_ip_count }}</a>
</td>
</tr>
{% endwith %}
{% with available_count=object.get_available_ips.size %}
<tr>
<th scope="row">{% trans "Available IPs" %}</th>
<td>
{# Use human-friendly words for counts greater than one million #}
{% if available_count > 1000000 %}
{{ available_count|intword }}
{% else %}
{{ available_count|intcomma }}
{% endif %}
</td>
</tr>
{% endwith %}
<tr>
<th scope="row">{% trans "First available IP" %}</th>
<td>
{% with first_available_ip=object.get_first_available_ip %}
{% if first_available_ip %}
{% if perms.ipam.add_ipaddress %}
<a href="{% url 'ipam:ipaddress_add' %}?address={{ first_available_ip }}{% if object.vrf %}&vrf={{ object.vrf_id }}{% endif %}{% if object.tenant %}&tenant={{ object.tenant.pk }}{% endif %}">{{ first_available_ip }}</a>
{% else %}
{{ first_available_ip }}
{% endif %}
{% else %}
{{ ''|placeholder }}
{% endif %}
{% endwith %}
</td>
</tr>
</table>
</div>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% if duplicate_prefix_table.rows %}
{% include 'inc/panel_table.html' with table=duplicate_prefix_table heading='Duplicate Prefixes' %}
{% endif %}
{% include 'inc/panel_table.html' with table=parent_prefix_table heading='Parent Prefixes' %}
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}
{% block modals %}
{{ block.super }}
{% if object.prefix.version == 4 %}
<div class="modal fade" id="prefix-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{% trans "Prefix Details" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-0">
<table class="table table-hover attr-table m-0">
<tr>
<th scope="row">{% trans "Network Address" %}</th>
<td>{{ object.prefix.network }}</td>
</tr>
<tr>
<th scope="row">{% trans "Network Mask" %}</th>
<td>{{ object.prefix.netmask }}</td>
</tr>
<tr>
<th scope="row">{% trans "Wildcard Mask" %}</th>
<td>{{ object.prefix.hostmask }}</td>
</tr>
<tr>
<th scope="row">{% trans "Broadcast Address" %}</th>
<td>{{ object.prefix.broadcast }}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -1,2 +0,0 @@
{% load helpers %}
<span>{{ value|linkify }} ({{ value.rir }})</span>

View File

@@ -1,7 +1,6 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load helpers %}
{% load i18n %}
{% block breadcrumbs %}
{{ block.super }}
@@ -9,39 +8,3 @@
<li class="breadcrumb-item"><a href="{% url 'ipam:prefix_list' %}?vrf_id={{ object.vrf.pk }}">{{ object.vrf }}</a></li>
{% endif %}
{% endblock %}
{% block modals %}
{{ block.super }}
{% if object.prefix.version == 4 %}
<div class="modal fade" id="prefix-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{% trans "Prefix Details" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-0">
<table class="table table-hover attr-table m-0">
<tr>
<th scope="row">{% trans "Network Address" %}</th>
<td>{{ object.prefix.network }}</td>
</tr>
<tr>
<th scope="row">{% trans "Network Mask" %}</th>
<td>{{ object.prefix.netmask }}</td>
</tr>
<tr>
<th scope="row">{% trans "Wildcard Mask" %}</th>
<td>{{ object.prefix.hostmask }}</td>
</tr>
<tr>
<th scope="row">{% trans "Broadcast Address" %}</th>
<td>{{ object.prefix.broadcast }}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock modals %}

View File

@@ -1,4 +1,7 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block extra_controls %}
@@ -8,3 +11,40 @@
</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 "RIR" %}</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 "Private" %}</th>
<td>{% checkmark object.is_private %}</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 %}

View File

@@ -1,4 +1,7 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block extra_controls %}
@@ -8,3 +11,40 @@
</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 "Role" %}</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 "Weight" %}</th>
<td>{{ object.weight }}</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 %}

View File

@@ -1 +1,68 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Route Target" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>
<td class="font-monospace">{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Tenant" %}</th>
<td>{{ 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' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Importing VRFs" %}</h2>
{% htmx_table 'ipam:vrf_list' import_target_id=object.pk %}
</div>
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Exporting VRFs" %}</h2>
{% htmx_table 'ipam:vrf_list' export_target_id=object.pk %}
</div>
</div>
</div>
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Importing L2VPNs" %}</h2>
{% htmx_table 'vpn:l2vpn_list' import_target_id=object.pk %}
</div>
</div>
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Exporting L2VPNs" %}</h2>
{% htmx_table 'vpn:l2vpn_list' export_target_id=object.pk %}
</div>
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,4 +1,8 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load helpers %}
{% load perms %}
{% load plugins %}
{% load i18n %}
{% block breadcrumbs %}
@@ -10,4 +14,58 @@
</a>
</li>
{% endif %}
{% endblock breadcrumbs %}
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Service" %}</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 "Parent" %}</th>
<td>{{ object.parent|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Protocol" %}</th>
<td>{{ object.get_protocol_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Ports" %}</th>
<td>{{ object.port_list }}</td>
</tr>
<tr>
<th scope="row">{% trans "IP Addresses" %}</th>
<td>
{% for ipaddress in object.ipaddresses.all %}
{{ ipaddress|linkify }}<br />
{% empty %}
{{ ''|placeholder }}
{% endfor %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</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' %}
{% include 'inc/panels/comments.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 %}

View File

@@ -1,2 +0,0 @@
{% load helpers %}
{% for ipaddress in value.all %}{{ ipaddress|linkify }}<br />{% empty %}<span class="text-muted">&mdash;</span>{% endfor %}

View File

@@ -1 +1,46 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load helpers %}
{% load perms %}
{% load plugins %}
{% load i18n %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Application Service Template" %}</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 "Protocol" %}</th>
<td>{{ object.get_protocol_display }}</td>
</tr>
<tr>
<th scope="row">{% trans "Ports" %}</th>
<td>{{ object.port_list }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</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' %}
{% include 'inc/panels/comments.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 %}

View File

@@ -1 +1,129 @@
{% extends 'ipam/vlan/base.html' %}
{% load helpers %}
{% load render_table from django_tables2 %}
{% load plugins %}
{% load i18n %}
{% load mptt %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "VLAN" %}</h2>
<table class="table table-hover attr-table">
{% if object.site.region %}
<tr>
<th scope="row">{% trans "Region" %}</th>
<td>
{% nested_tree object.site.region %}
</td>
</tr>
{% endif %}
<tr>
<th scope="row">{% trans "Site" %}</th>
<td>{{ object.site|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Group" %}</th>
<td>{{ object.group|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "VLAN ID" %}</th>
<td>{{ object.vid }}</td>
</tr>
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</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 "Status" %}</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
</tr>
<tr>
<th scope="row">{% trans "Role" %}</th>
<td>
{% if object.role %}
<a href="{% url 'ipam:vlan_list' %}?role={{ object.role.slug }}">{{ object.role }}</a>
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Q-in-Q Role" %}</th>
<td>
{% if object.qinq_role %}
{% badge object.get_qinq_role_display bg_color=object.get_qinq_role_color %}
{% else %}
{{ ''|placeholder }}
{% endif %}
</td>
</tr>
{% if object.qinq_role == 'cvlan' %}
<tr>
<th scope="row">{% trans "Q-in-Q SVLAN" %}</th>
<td>{{ object.qinq_svlan|linkify|placeholder }}</td>
</tr>
{% endif %}
<tr>
<th scope="row">{% trans "L2VPN" %}</th>
<td>{{ object.l2vpn_termination.l2vpn|linkify|placeholder }}</td>
</tr>
</table>
</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' %}
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
<div class="card">
<h2 class="card-header">
{% trans "Prefixes" %}
{% if perms.ipam.add_prefix %}
<div class="card-actions">
<a href="{% url 'ipam:prefix_add' %}?{% if object.tenant %}tenant={{ object.tenant.pk }}&{% endif %}site={{ object.site.pk }}&vlan={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Prefix" %}
</a>
</div>
{% endif %}
</h2>
{% htmx_table 'ipam:prefix_list' vlan_id=object.pk %}
</div>
{% if object.qinq_role == 'svlan' %}
<div class="card">
<h2 class="card-header">
{% trans "Customer VLANs" %}
{% if perms.ipam.add_vlan %}
<div class="card-actions">
<a href="{% url 'ipam:vlan_add' %}?qinq_role=cvlan&qinq_svlan={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a VLAN" %}
</a>
</div>
{% endif %}
</h2>
{% htmx_table 'ipam:vlan_list' qinq_svlan_id=object.pk %}
</div>
{% endif %}
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1,5 +1,7 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block breadcrumbs %}
@@ -16,4 +18,53 @@
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add VLAN" %}
</a>
{% endif %}
{% endblock extra_controls %}
{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "VLAN 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 "Scope" %}</th>
<td>{{ object.scope|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "VLAN IDs" %}</th>
<td>{{ object.vid_ranges_items|join:", " }}</td>
</tr>
<tr>
<th scope="row">Utilization</th>
<td>{% utilization_graph object.utilization %}</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 %}

View File

@@ -1 +0,0 @@
{{ value|join:", " }}

View File

@@ -1 +1,65 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-4">
<div class="card">
<h2 class="card-header">{% trans "VLAN Translation Policy" %}</h2>
<table class="table table-hover attr-table">
<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>
<tr>
<th scope="row">{% trans "Rules" %}</th>
<td>
{% if object.rules.count %}
<a href="{% url 'ipam:vlantranslationrule_list' %}?policy_id={{ object.pk }}">{{ object.rules.count }}</a>
{% else %}
0
{% endif %}
</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-8">
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.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 "VLAN Translation Rules" %}
{% if perms.ipam.add_vlantranslationrule %}
<div class="card-actions">
<a href="{% url 'ipam:vlantranslationrule_add' %}?device={{ object.device.pk }}&policy={{ 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 Rule" %}
</a>
</div>
{% endif %}
</h2>
{% htmx_table 'ipam:vlantranslationrule_list' policy_id=object.pk %}
</div>
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1 +1,45 @@
{% extends 'generic/object.html' %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-4">
<div class="card">
<h2 class="card-header">{% trans "VLAN Translation Rule" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Policy" %}</th>
<td>{{ object.policy|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Local VID" %}</th>
<td>{{ object.local_vid }}</td>
</tr>
<tr>
<th scope="row">{% trans "Remote VID" %}</th>
<td>{{ object.remote_vid }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description }}</td>
</tr>
</table>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-12 col-md-8">
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@@ -1 +1,61 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load helpers %}
{% load plugins %}
{% load i18n %}
{% block title %}{% trans "VRF" %} {{ object }}{% endblock %}
{% block content %}
<div class="row">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "VRF" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Route Distinguisher" %}</th>
<td>{{ object.rd|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>
<tr>
<th scope="row">{% trans "Unique IP Space" %}</th>
<td>{% checkmark object.enforce_unique %}</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/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-12 col-md-6">
{% include 'inc/panel_table.html' with table=import_targets_table heading="Import Route Targets" %}
</div>
<div class="col col-12 col-md-6">
{% include 'inc/panel_table.html' with table=export_targets_table heading="Export Route Targets" %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

File diff suppressed because it is too large Load Diff