mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
Merge branch 'develop' into feature
This commit is contained in:
commit
2dd165bbef
@ -55,3 +55,7 @@ The link will only appear when viewing a device with a manufacturer name of "Cis
|
|||||||
## Link Groups
|
## Link Groups
|
||||||
|
|
||||||
Group names can be specified to organize links into groups. Links with the same group name will render as a dropdown menu beneath a single button bearing the name of the group.
|
Group names can be specified to organize links into groups. Links with the same group name will render as a dropdown menu beneath a single button bearing the name of the group.
|
||||||
|
|
||||||
|
## Table Columns
|
||||||
|
|
||||||
|
Custom links can also be included in object tables by selecting the desired links from the table configuration form. When displayed, each link will render as a hyperlink for its corresponding object. When exported (e.g. as CSV data), each link render only its URL.
|
||||||
|
@ -2,10 +2,23 @@
|
|||||||
|
|
||||||
## v3.1.3 (FUTURE)
|
## v3.1.3 (FUTURE)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#6782](https://github.com/netbox-community/netbox/issues/6782) - Enable the inclusion of custom links in tables
|
||||||
|
* [#8100](https://github.com/netbox-community/netbox/issues/8100) - Add "other" choice for FHRP group protocol
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#7246](https://github.com/netbox-community/netbox/issues/7246) - Don't attempt to URL-decode NAPALM response payloads
|
||||||
|
* [#7887](https://github.com/netbox-community/netbox/issues/7887) - Forward `HTTP_X_FORWARDED_FOR` to custom scripts
|
||||||
* [#7962](https://github.com/netbox-community/netbox/issues/7962) - Fix user menu under report/script result view
|
* [#7962](https://github.com/netbox-community/netbox/issues/7962) - Fix user menu under report/script result view
|
||||||
|
* [#7972](https://github.com/netbox-community/netbox/issues/7972) - Standardize name of `RemoteUserBackend` logger
|
||||||
|
* [#8097](https://github.com/netbox-community/netbox/issues/8097) - Fix styling of Markdown tables
|
||||||
|
* [#8127](https://github.com/netbox-community/netbox/issues/8127) - Fix disassociation of interface under IP address edit view
|
||||||
* [#8131](https://github.com/netbox-community/netbox/issues/8131) - Restore annotation of available IPs under prefix IPs view
|
* [#8131](https://github.com/netbox-community/netbox/issues/8131) - Restore annotation of available IPs under prefix IPs view
|
||||||
|
* [#8134](https://github.com/netbox-community/netbox/issues/8134) - Fix bulk editing of objects within dynamic tables
|
||||||
|
* [#8139](https://github.com/netbox-community/netbox/issues/8139) - Fix rendering of table configuration form under VM interfaces view
|
||||||
|
* [#8140](https://github.com/netbox-community/netbox/issues/8140) - Restore missing fields on wireless LAN & link REST API serializers
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -15,14 +15,14 @@ from circuits.models import Circuit
|
|||||||
from dcim import filtersets
|
from dcim import filtersets
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
|
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
|
||||||
from ipam.models import Prefix, VLAN, ASN
|
from ipam.models import Prefix, VLAN
|
||||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
||||||
from netbox.api.exceptions import ServiceUnavailable
|
from netbox.api.exceptions import ServiceUnavailable
|
||||||
from netbox.api.metadata import ContentTypeMetadata
|
from netbox.api.metadata import ContentTypeMetadata
|
||||||
from netbox.api.views import ModelViewSet
|
from netbox.api.views import ModelViewSet
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
from utilities.utils import count_related, decode_dict
|
from utilities.utils import count_related
|
||||||
from virtualization.models import VirtualMachine
|
from virtualization.models import VirtualMachine
|
||||||
from . import serializers
|
from . import serializers
|
||||||
from .exceptions import MissingFilterException
|
from .exceptions import MissingFilterException
|
||||||
@ -516,7 +516,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
|
|||||||
response[method] = {'error': 'Only get_* NAPALM methods are supported'}
|
response[method] = {'error': 'Only get_* NAPALM methods are supported'}
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
response[method] = decode_dict(getattr(d, method)())
|
response[method] = getattr(d, method)()
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)}
|
response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -229,6 +229,24 @@ class CustomLink(ChangeLoggedModel):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('extras:customlink', args=[self.pk])
|
return reverse('extras:customlink', args=[self.pk])
|
||||||
|
|
||||||
|
def render(self, context):
|
||||||
|
"""
|
||||||
|
Render the CustomLink given the provided context, and return the text, link, and link_target.
|
||||||
|
|
||||||
|
:param context: The context passed to Jinja2
|
||||||
|
"""
|
||||||
|
text = render_jinja2(self.link_text, context)
|
||||||
|
if not text:
|
||||||
|
return {}
|
||||||
|
link = render_jinja2(self.link_url, context)
|
||||||
|
link_target = ' target="_blank"' if self.new_window else ''
|
||||||
|
|
||||||
|
return {
|
||||||
|
'text': text,
|
||||||
|
'link': link,
|
||||||
|
'link_target': link_target,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@extras_features('webhooks', 'export_templates')
|
@extras_features('webhooks', 'export_templates')
|
||||||
class ExportTemplate(ChangeLoggedModel):
|
class ExportTemplate(ChangeLoggedModel):
|
||||||
|
@ -62,16 +62,14 @@ def custom_links(context, obj):
|
|||||||
# Add non-grouped links
|
# Add non-grouped links
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
text_rendered = render_jinja2(cl.link_text, link_context)
|
rendered = cl.render(link_context)
|
||||||
if text_rendered:
|
if rendered:
|
||||||
link_rendered = render_jinja2(cl.link_url, link_context)
|
|
||||||
link_target = ' target="_blank"' if cl.new_window else ''
|
|
||||||
template_code += LINK_BUTTON.format(
|
template_code += LINK_BUTTON.format(
|
||||||
link_rendered, link_target, cl.button_class, text_rendered
|
rendered['link'], rendered['link_target'], cl.button_class, rendered['text']
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
template_code += '<a class="btn btn-sm btn-outline-dark" disabled="disabled" title="{}">' \
|
template_code += f'<a class="btn btn-sm btn-outline-dark" disabled="disabled" title="{e}">' \
|
||||||
'<i class="mdi mdi-alert"></i> {}</a>\n'.format(e, cl.name)
|
f'<i class="mdi mdi-alert"></i> {cl.name}</a>\n'
|
||||||
|
|
||||||
# Add grouped links to template
|
# Add grouped links to template
|
||||||
for group, links in group_names.items():
|
for group, links in group_names.items():
|
||||||
@ -80,17 +78,15 @@ def custom_links(context, obj):
|
|||||||
|
|
||||||
for cl in links:
|
for cl in links:
|
||||||
try:
|
try:
|
||||||
text_rendered = render_jinja2(cl.link_text, link_context)
|
rendered = cl.render(link_context)
|
||||||
if text_rendered:
|
if rendered:
|
||||||
link_target = ' target="_blank"' if cl.new_window else ''
|
|
||||||
link_rendered = render_jinja2(cl.link_url, link_context)
|
|
||||||
links_rendered.append(
|
links_rendered.append(
|
||||||
GROUP_LINK.format(link_rendered, link_target, text_rendered)
|
GROUP_LINK.format(rendered['link'], rendered['link_target'], rendered['text'])
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
links_rendered.append(
|
links_rendered.append(
|
||||||
'<li><a class="dropdown-item" disabled="disabled" title="{}"><span class="text-muted">'
|
f'<li><a class="dropdown-item" disabled="disabled" title="{e}"><span class="text-muted">'
|
||||||
'<i class="mdi mdi-alert"></i> {}</span></a></li>'.format(e, cl.name)
|
f'<i class="mdi mdi-alert"></i> {cl.name}</span></a></li>'
|
||||||
)
|
)
|
||||||
|
|
||||||
if links_rendered:
|
if links_rendered:
|
||||||
|
@ -106,6 +106,7 @@ class FHRPGroupProtocolChoices(ChoiceSet):
|
|||||||
PROTOCOL_HSRP = 'hsrp'
|
PROTOCOL_HSRP = 'hsrp'
|
||||||
PROTOCOL_GLBP = 'glbp'
|
PROTOCOL_GLBP = 'glbp'
|
||||||
PROTOCOL_CARP = 'carp'
|
PROTOCOL_CARP = 'carp'
|
||||||
|
PROTOCOL_OTHER = 'other'
|
||||||
|
|
||||||
CHOICES = (
|
CHOICES = (
|
||||||
(PROTOCOL_VRRP2, 'VRRPv2'),
|
(PROTOCOL_VRRP2, 'VRRPv2'),
|
||||||
@ -113,6 +114,7 @@ class FHRPGroupProtocolChoices(ChoiceSet):
|
|||||||
(PROTOCOL_HSRP, 'HSRP'),
|
(PROTOCOL_HSRP, 'HSRP'),
|
||||||
(PROTOCOL_GLBP, 'GLBP'),
|
(PROTOCOL_GLBP, 'GLBP'),
|
||||||
(PROTOCOL_CARP, 'CARP'),
|
(PROTOCOL_CARP, 'CARP'),
|
||||||
|
(PROTOCOL_OTHER, 'Other'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -471,6 +471,8 @@ class IPAddressForm(TenancyForm, CustomFieldModelForm):
|
|||||||
})
|
})
|
||||||
elif selected_objects:
|
elif selected_objects:
|
||||||
self.instance.assigned_object = self.cleaned_data[selected_objects[0]]
|
self.instance.assigned_object = self.cleaned_data[selected_objects[0]]
|
||||||
|
else:
|
||||||
|
self.instance.assigned_object = None
|
||||||
|
|
||||||
# Primary IP assignment is only available if an interface has been assigned.
|
# Primary IP assignment is only available if an interface has been assigned.
|
||||||
interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
|
interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
|
||||||
|
@ -105,7 +105,7 @@ class RemoteUserBackend(_RemoteUserBackend):
|
|||||||
return settings.REMOTE_AUTH_AUTO_CREATE_USER
|
return settings.REMOTE_AUTH_AUTO_CREATE_USER
|
||||||
|
|
||||||
def configure_groups(self, user, remote_groups):
|
def configure_groups(self, user, remote_groups):
|
||||||
logger = logging.getLogger('netbox.authentication.RemoteUserBackend')
|
logger = logging.getLogger('netbox.auth.RemoteUserBackend')
|
||||||
|
|
||||||
# Assign default groups to the user
|
# Assign default groups to the user
|
||||||
group_list = []
|
group_list = []
|
||||||
@ -141,7 +141,7 @@ class RemoteUserBackend(_RemoteUserBackend):
|
|||||||
Return None if ``create_unknown_user`` is ``False`` and a ``User``
|
Return None if ``create_unknown_user`` is ``False`` and a ``User``
|
||||||
object with the given username is not found in the database.
|
object with the given username is not found in the database.
|
||||||
"""
|
"""
|
||||||
logger = logging.getLogger('netbox.authentication.RemoteUserBackend')
|
logger = logging.getLogger('netbox.auth.RemoteUserBackend')
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"trying to authenticate {remote_user} with groups {remote_groups}")
|
f"trying to authenticate {remote_user} with groups {remote_groups}")
|
||||||
if not remote_user:
|
if not remote_user:
|
||||||
@ -173,7 +173,7 @@ class RemoteUserBackend(_RemoteUserBackend):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _is_superuser(self, user):
|
def _is_superuser(self, user):
|
||||||
logger = logging.getLogger('netbox.authentication.RemoteUserBackend')
|
logger = logging.getLogger('netbox.auth.RemoteUserBackend')
|
||||||
superuser_groups = settings.REMOTE_AUTH_SUPERUSER_GROUPS
|
superuser_groups = settings.REMOTE_AUTH_SUPERUSER_GROUPS
|
||||||
logger.debug(f"Superuser Groups: {superuser_groups}")
|
logger.debug(f"Superuser Groups: {superuser_groups}")
|
||||||
superusers = settings.REMOTE_AUTH_SUPERUSERS
|
superusers = settings.REMOTE_AUTH_SUPERUSERS
|
||||||
@ -189,7 +189,7 @@ class RemoteUserBackend(_RemoteUserBackend):
|
|||||||
return bool(result)
|
return bool(result)
|
||||||
|
|
||||||
def _is_staff(self, user):
|
def _is_staff(self, user):
|
||||||
logger = logging.getLogger('netbox.authentication.RemoteUserBackend')
|
logger = logging.getLogger('netbox.auth.RemoteUserBackend')
|
||||||
staff_groups = settings.REMOTE_AUTH_STAFF_GROUPS
|
staff_groups = settings.REMOTE_AUTH_STAFF_GROUPS
|
||||||
logger.debug(f"Superuser Groups: {staff_groups}")
|
logger.debug(f"Superuser Groups: {staff_groups}")
|
||||||
staff_users = settings.REMOTE_AUTH_STAFF_USERS
|
staff_users = settings.REMOTE_AUTH_STAFF_USERS
|
||||||
@ -204,7 +204,7 @@ class RemoteUserBackend(_RemoteUserBackend):
|
|||||||
return bool(result)
|
return bool(result)
|
||||||
|
|
||||||
def configure_user(self, request, user):
|
def configure_user(self, request, user):
|
||||||
logger = logging.getLogger('netbox.authentication.RemoteUserBackend')
|
logger = logging.getLogger('netbox.auth.RemoteUserBackend')
|
||||||
if not settings.REMOTE_AUTH_GROUP_SYNC_ENABLED:
|
if not settings.REMOTE_AUTH_GROUP_SYNC_ENABLED:
|
||||||
# Assign default groups to the user
|
# Assign default groups to the user
|
||||||
group_list = []
|
group_list = []
|
||||||
|
BIN
netbox/project-static/dist/netbox-dark.css
vendored
BIN
netbox/project-static/dist/netbox-dark.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-light.css
vendored
BIN
netbox/project-static/dist/netbox-light.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-print.css
vendored
BIN
netbox/project-static/dist/netbox-print.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -35,11 +35,6 @@ function handleFormSubmit(event: Event, form: HTMLFormElement): void {
|
|||||||
for (const element of form.querySelectorAll<FormControls>('*[name]')) {
|
for (const element of form.querySelectorAll<FormControls>('*[name]')) {
|
||||||
if (!element.validity.valid) {
|
if (!element.validity.valid) {
|
||||||
invalids.add(element.name);
|
invalids.add(element.name);
|
||||||
|
|
||||||
// If the field is invalid, but contains the .is-valid class, remove it.
|
|
||||||
if (element.classList.contains('is-valid')) {
|
|
||||||
element.classList.remove('is-valid');
|
|
||||||
}
|
|
||||||
// If the field is invalid, but doesn't contain the .is-invalid class, add it.
|
// If the field is invalid, but doesn't contain the .is-invalid class, add it.
|
||||||
if (!element.classList.contains('is-invalid')) {
|
if (!element.classList.contains('is-invalid')) {
|
||||||
element.classList.add('is-invalid');
|
element.classList.add('is-invalid');
|
||||||
@ -49,10 +44,6 @@ function handleFormSubmit(event: Event, form: HTMLFormElement): void {
|
|||||||
if (element.classList.contains('is-invalid')) {
|
if (element.classList.contains('is-invalid')) {
|
||||||
element.classList.remove('is-invalid');
|
element.classList.remove('is-invalid');
|
||||||
}
|
}
|
||||||
// If the field is valid, but doesn't contain the .is-valid class, add it.
|
|
||||||
if (!element.classList.contains('is-valid')) {
|
|
||||||
element.classList.add('is-valid');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
23
netbox/project-static/src/htmx.ts
Normal file
23
netbox/project-static/src/htmx.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { getElements, isTruthy } from './util';
|
||||||
|
import { initButtons } from './buttons';
|
||||||
|
|
||||||
|
function initDepedencies(): void {
|
||||||
|
for (const init of [initButtons]) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook into HTMX's event system to reinitialize specific native event listeners when HTMX swaps
|
||||||
|
* elements.
|
||||||
|
*/
|
||||||
|
export function initHtmx(): void {
|
||||||
|
for (const element of getElements('[hx-target]')) {
|
||||||
|
const targetSelector = element.getAttribute('hx-target');
|
||||||
|
if (isTruthy(targetSelector)) {
|
||||||
|
for (const target of getElements(targetSelector)) {
|
||||||
|
target.addEventListener('htmx:afterSettle', initDepedencies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,7 @@ import { initInterfaceTable } from './tables';
|
|||||||
import { initSideNav } from './sidenav';
|
import { initSideNav } from './sidenav';
|
||||||
import { initRackElevation } from './racks';
|
import { initRackElevation } from './racks';
|
||||||
import { initLinks } from './links';
|
import { initLinks } from './links';
|
||||||
|
import { initHtmx } from './htmx';
|
||||||
|
|
||||||
function initDocument(): void {
|
function initDocument(): void {
|
||||||
for (const init of [
|
for (const init of [
|
||||||
@ -29,6 +30,7 @@ function initDocument(): void {
|
|||||||
initSideNav,
|
initSideNav,
|
||||||
initRackElevation,
|
initRackElevation,
|
||||||
initLinks,
|
initLinks,
|
||||||
|
initHtmx,
|
||||||
]) {
|
]) {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
@ -965,6 +965,19 @@ div.card-overlay {
|
|||||||
max-width: unset;
|
max-width: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Rendered Markdown */
|
||||||
|
.rendered-markdown table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.rendered-markdown th {
|
||||||
|
border-bottom: 2px solid #dddddd;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
.rendered-markdown td {
|
||||||
|
border-top: 1px solid #dddddd;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
// Preformatted text blocks
|
// Preformatted text blocks
|
||||||
td pre {
|
td pre {
|
||||||
margin-bottom: 0
|
margin-bottom: 0
|
||||||
|
@ -104,23 +104,23 @@
|
|||||||
{# Static resources #}
|
{# Static resources #}
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="{% static 'netbox-external.css'%}"
|
href="{% static 'netbox-external.css'%}?v={{ settings.VERSION }}"
|
||||||
onerror="window.location='{% url 'media_failure' %}?filename=netbox-external.css'"
|
onerror="window.location='{% url 'media_failure' %}?filename=netbox-external.css'"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="{% static 'netbox-light.css'%}"
|
href="{% static 'netbox-light.css'%}?v={{ settings.VERSION }}"
|
||||||
onerror="window.location='{% url 'media_failure' %}?filename=netbox-light.css'"
|
onerror="window.location='{% url 'media_failure' %}?filename=netbox-light.css'"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="{% static 'netbox-dark.css'%}"
|
href="{% static 'netbox-dark.css'%}?v={{ settings.VERSION }}"
|
||||||
onerror="window.location='{% url 'media_failure' %}?filename=netbox-dark.css'"
|
onerror="window.location='{% url 'media_failure' %}?filename=netbox-dark.css'"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
media="print"
|
media="print"
|
||||||
href="{% static 'netbox-print.css'%}"
|
href="{% static 'netbox-print.css'%}?v={{ settings.VERSION }}"
|
||||||
onerror="window.location='{% url 'media_failure' %}?filename=netbox-print.css'"
|
onerror="window.location='{% url 'media_failure' %}?filename=netbox-print.css'"
|
||||||
/>
|
/>
|
||||||
<link rel="icon" type="image/png" href="{% static 'netbox.ico' %}" />
|
<link rel="icon" type="image/png" href="{% static 'netbox.ico' %}" />
|
||||||
@ -129,7 +129,7 @@
|
|||||||
{# Javascript #}
|
{# Javascript #}
|
||||||
<script
|
<script
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
src="{% static 'netbox.js' %}"
|
src="{% static 'netbox.js' %}?v={{ settings.VERSION }}"
|
||||||
onerror="window.location='{% url 'media_failure' %}?filename=netbox.js'">
|
onerror="window.location='{% url 'media_failure' %}?filename=netbox.js'">
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
{# Base layout for the core NetBox UI w/navbar and page content #}
|
{# Base layout for the core NetBox UI w/navbar and page content #}
|
||||||
{% extends 'base/base.html' %}
|
{% extends 'base/base.html' %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load nav %}
|
{% load search %}
|
||||||
{% load search_options %}
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block layout %}
|
{% block layout %}
|
||||||
@ -21,7 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Top bar #}
|
{# Top bar #}
|
||||||
<nav class="navbar navbar-light sticky-top flex-md-nowrap ps-6 p-3 search container-fluid noprint">
|
<nav class="navbar navbar-light sticky-top flex-md-nowrap p-1 mb-3 search container-fluid border-bottom bg-light bg-gradient noprint">
|
||||||
|
|
||||||
{# Mobile Navigation #}
|
{# Mobile Navigation #}
|
||||||
<div class="nav-mobile">
|
<div class="nav-mobile">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% load nav %}
|
{% load navigation %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
<nav class="sidenav noprint" id="sidenav" data-simplebar>
|
<nav class="sidenav noprint" id="sidenav" data-simplebar>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'utilities/confirmation_form.html' %}
|
{% extends 'generic/confirmation_form.html' %}
|
||||||
|
|
||||||
{% block title %}Swap Circuit Terminations{% endblock %}
|
{% block title %}Swap Circuit Terminations{% endblock %}
|
||||||
|
|
||||||
|
@ -41,11 +41,11 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">NOC Contact</th>
|
<th scope="row">NOC Contact</th>
|
||||||
<td class="rendered-markdown">{{ object.noc_contact|render_markdown|placeholder }}</td>
|
<td>{{ object.noc_contact|render_markdown|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Admin Contact</th>
|
<th scope="row">Admin Contact</th>
|
||||||
<td class="rendered-markdown">{{ object.admin_contact|render_markdown|placeholder }}</td>
|
<td>{{ object.admin_contact|render_markdown|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Circuits</th>
|
<th scope="row">Circuits</th>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'utilities/confirmation_form.html' %}
|
{% extends 'generic/confirmation_form.html' %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
|
||||||
{% block title %}Disconnect {{ obj_type_plural|bettertitle }}{% endblock %}
|
{% block title %}Disconnect {{ obj_type_plural|bettertitle }}{% endblock %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'utilities/confirmation_form.html' %}
|
{% extends 'generic/confirmation_form.html' %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}Delete console port {{ consoleport }}?{% endblock %}
|
{% block title %}Delete console port {{ consoleport }}?{% endblock %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'utilities/confirmation_form.html' %}
|
{% extends 'generic/confirmation_form.html' %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}Delete console server port {{ consoleserverport }}?{% endblock %}
|
{% block title %}Delete console server port {{ consoleserverport }}?{% endblock %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'utilities/confirmation_form.html' %}
|
{% extends 'generic/confirmation_form.html' %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}Delete device bay {{ devicebay }}?{% endblock %}
|
{% block title %}Delete device bay {{ devicebay }}?{% endblock %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'utilities/confirmation_form.html' %}
|
{% extends 'generic/confirmation_form.html' %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}Remove {{ device_bay.installed_device }} from {{ device_bay }}?{% endblock %}
|
{% block title %}Remove {{ device_bay.installed_device }} from {{ device_bay }}?{% endblock %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'utilities/confirmation_form.html' %}
|
{% extends 'generic/confirmation_form.html' %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}Delete interface {{ interface }}?{% endblock %}
|
{% block title %}Delete interface {{ interface }}?{% endblock %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'utilities/confirmation_form.html' %}
|
{% extends 'generic/confirmation_form.html' %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}Delete inventory item {{ inventoryitem }}?{% endblock %}
|
{% block title %}Delete inventory item {{ inventoryitem }}?{% endblock %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'utilities/confirmation_form.html' %}
|
{% extends 'generic/confirmation_form.html' %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}Delete power outlet {{ poweroutlet }}?{% endblock %}
|
{% block title %}Delete power outlet {{ poweroutlet }}?{% endblock %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'utilities/confirmation_form.html' %}
|
{% extends 'generic/confirmation_form.html' %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}Delete power port {{ powerport }}?{% endblock %}
|
{% block title %}Delete power port {{ powerport }}?{% endblock %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'utilities/confirmation_form.html' %}
|
{% extends 'generic/confirmation_form.html' %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}Remove Virtual Chassis Member?{% endblock %}
|
{% block title %}Remove Virtual Chassis Member?{% endblock %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'utilities/confirmation_form.html' %}
|
{% extends 'generic/confirmation_form.html' %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block title %}Delete {{ obj_type }}?{% endblock %}
|
{% block title %}Delete {{ obj_type }}?{% endblock %}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
{% extends 'base/layout.html' %}
|
{% extends 'base/layout.html' %}
|
||||||
{% load get_status %}
|
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load render_table from django_tables2 %}
|
{% load render_table from django_tables2 %}
|
||||||
|
|
||||||
@ -24,7 +23,7 @@
|
|||||||
{% block title %}Home{% endblock %}
|
{% block title %}Home{% endblock %}
|
||||||
|
|
||||||
{% block content-wrapper %}
|
{% block content-wrapper %}
|
||||||
<div class="p-3">
|
<div class="px-3">
|
||||||
{# General stats #}
|
{# General stats #}
|
||||||
<div class="row masonry">
|
<div class="row masonry">
|
||||||
{% for section, items, icon in stats %}
|
{% for section, items, icon in stats %}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<h5 class="card-header">
|
<h5 class="card-header">
|
||||||
Comments
|
Comments
|
||||||
</h5>
|
</h5>
|
||||||
<div class="card-body rendered-markdown">
|
<div class="card-body">
|
||||||
{% if object.comments %}
|
{% if object.comments %}
|
||||||
{{ object.comments|render_markdown }}
|
{{ object.comments|render_markdown }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
{% load helpers %}
|
|
||||||
<li class="dropdown">
|
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Plugins <span class="caret"></span></a>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
{% for section_name, menu_items in registry.plugin_menu_items.items %}
|
|
||||||
<li class="dropdown-header">{{ section_name }}</li>
|
|
||||||
{% for menu_item in menu_items %}
|
|
||||||
{% if not menu_item.permissions or request.user|has_perms:menu_item.permissions %}
|
|
||||||
<li>
|
|
||||||
{% if menu_item.buttons %}
|
|
||||||
<div class="buttons float-end">
|
|
||||||
{% for button in menu_item.buttons %}
|
|
||||||
{% if not button.permissions or request.user|has_perms:button.permissions %}
|
|
||||||
<a href="{% url button.link %}" class="btn btn-sm btn-{{ button.color }}" title="{{ button.title }}"><i class="{{ button.icon_class }}"></i></a>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<a href="{% url menu_item.link %}">{{ menu_item.link_text }}</a>
|
|
||||||
</li>
|
|
||||||
{% else %}
|
|
||||||
<li class="disabled"><a href="#">{{ menu_item.link_text }}</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% if not forloop.last %}
|
|
||||||
<li class="divider"></li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
@ -30,7 +30,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li><hr class="dropdown-divider" /></li>
|
<li><hr class="dropdown-divider" /></li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item text-danger" href="{% url 'logout' %}">
|
<a class="dropdown-item" href="{% url 'logout' %}">
|
||||||
<i class="mdi mdi-logout-variant"></i> Log Out
|
<i class="mdi mdi-logout-variant"></i> Log Out
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -38,11 +38,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<a
|
<a class="btn btn-primary ws-nowrap" type="button" href="{% url 'login' %}">
|
||||||
class="btn btn-primary ws-nowrap"
|
|
||||||
type="button"
|
|
||||||
href="{% url 'login' %}"
|
|
||||||
>
|
|
||||||
<i class="mdi mdi-login-variant"></i> Log In
|
<i class="mdi mdi-login-variant"></i> Log In
|
||||||
</a>
|
</a>
|
||||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
|
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
|
||||||
|
@ -13,143 +13,135 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-4">
|
<div class="col col-md-4">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">
|
||||||
IP Address
|
IP Address
|
||||||
</h5>
|
</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-hover attr-table">
|
<table class="table table-hover attr-table">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Family</th>
|
<th scope="row">Family</th>
|
||||||
<td>IPv{{ object.family }}</td>
|
<td>IPv{{ object.family }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">VRF</th>
|
<th scope="row">VRF</th>
|
||||||
<td>
|
<td>
|
||||||
{% if object.vrf %}
|
{% if object.vrf %}
|
||||||
<a href="{% url 'ipam:vrf' pk=object.vrf.pk %}">{{ object.vrf }}</a>
|
<a href="{% url 'ipam:vrf' pk=object.vrf.pk %}">{{ object.vrf }}</a>
|
||||||
{% else %}
|
|
||||||
<span>Global</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Tenant</th>
|
|
||||||
<td>
|
|
||||||
{% if object.tenant %}
|
|
||||||
{% if object.tenant.group %}
|
|
||||||
<a href="{{ object.tenant.group.get_absolute_url }}">{{ object.tenant.group }}</a> /
|
|
||||||
{% endif %}
|
|
||||||
<a href="{{ object.tenant.get_absolute_url }}">{{ object.tenant }}</a>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">None</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Status</th>
|
|
||||||
<td>
|
|
||||||
<span class="badge bg-{{ object.get_status_class }}">{{ object.get_status_display }}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Role</th>
|
|
||||||
<td>
|
|
||||||
{% if object.role %}
|
|
||||||
<a href="{% url 'ipam:ipaddress_list' %}?role={{ object.role }}">{{ object.get_role_display }}</a>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">None</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">DNS Name</th>
|
|
||||||
<td>{{ object.dns_name|placeholder }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Description</th>
|
|
||||||
<td>{{ object.description|placeholder }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">Assignment</th>
|
|
||||||
<td>
|
|
||||||
{% if object.assigned_object %}
|
|
||||||
{% if object.assigned_object.parent_object %}
|
|
||||||
<a href="{{ object.assigned_object.parent_object.get_absolute_url }}">{{ object.assigned_object.parent_object }}</a> /
|
|
||||||
{% endif %}
|
|
||||||
<a href="{{ object.assigned_object.get_absolute_url }}">{{ object.assigned_object }}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">—</span>
|
<span>Global</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">NAT (inside)</th>
|
<th scope="row">Tenant</th>
|
||||||
<td>
|
<td>
|
||||||
{% if object.nat_inside %}
|
{% if object.tenant %}
|
||||||
<a href="{{ object.nat_inside.get_absolute_url }}">{{ object.nat_inside }}</a>
|
{% if object.tenant.group %}
|
||||||
{% if object.nat_inside.assigned_object %}
|
<a href="{{ object.tenant.group.get_absolute_url }}">{{ object.tenant.group }}</a> /
|
||||||
(<a href="{{ object.nat_inside.assigned_object.parent_object.get_absolute_url }}">{{ object.nat_inside.assigned_object.parent_object }}</a>)
|
{% endif %}
|
||||||
{% endif %}
|
<a href="{{ object.tenant.get_absolute_url }}">{{ object.tenant }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">None</span>
|
<span class="text-muted">None</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">NAT (outside)</th>
|
<th scope="row">Status</th>
|
||||||
<td>
|
<td>
|
||||||
{% if object.nat_outside %}
|
<span class="badge bg-{{ object.get_status_class }}">{{ object.get_status_display }}</span>
|
||||||
<a href="{{ object.nat_outside.get_absolute_url }}">{{ object.nat_outside }}</a>
|
</td>
|
||||||
{% else %}
|
</tr>
|
||||||
<span class="text-muted">None</span>
|
<tr>
|
||||||
{% endif %}
|
<th scope="row">Role</th>
|
||||||
</td>
|
<td>
|
||||||
</tr>
|
{% if object.role %}
|
||||||
</table>
|
<a href="{% url 'ipam:ipaddress_list' %}?role={{ object.role }}">{{ object.get_role_display }}</a>
|
||||||
</div>
|
{% else %}
|
||||||
</div>
|
<span class="text-muted">None</span>
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
{% endif %}
|
||||||
|
</td>
|
||||||
{% plugin_left_page object %}
|
</tr>
|
||||||
</div>
|
<tr>
|
||||||
|
<th scope="row">DNS Name</th>
|
||||||
<div class="col col-md-8">
|
<td>{{ object.dns_name|placeholder }}</td>
|
||||||
{% include 'inc/panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
|
</tr>
|
||||||
{% if duplicate_ips_table.rows %}
|
<tr>
|
||||||
{# Custom version of panel_table.html #}
|
<th scope="row">Description</th>
|
||||||
<div class="card border-danger">
|
<td>{{ object.description|placeholder }}</td>
|
||||||
<h5 class="card-header">
|
</tr>
|
||||||
<span class="text-danger">Duplicate IP Addresses</span>
|
<tr>
|
||||||
{% if more_duplicate_ips %}
|
<th scope="row">Assignment</th>
|
||||||
<div class="float-end">
|
<td>
|
||||||
<a type="button" class="btn btn-primary btn-sm"
|
{% if object.assigned_object %}
|
||||||
{% if object.vrf %}
|
{% if object.assigned_object.parent_object %}
|
||||||
href="{% url 'ipam:ipaddress_list' %}?address={{ object.address.ip }}&vrf_id={{ object.vrf.pk }}"
|
<a href="{{ object.assigned_object.parent_object.get_absolute_url }}">{{ object.assigned_object.parent_object }}</a> /
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ object.assigned_object.get_absolute_url }}">{{ object.assigned_object }}
|
||||||
{% else %}
|
{% else %}
|
||||||
href="{% url 'ipam:ipaddress_list' %}?address={{ object.address.ip }}&vrf_id=null"
|
<span class="text-muted">—</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
>Show all</a>
|
</td>
|
||||||
</div>
|
</tr>
|
||||||
{% endif %}
|
<tr>
|
||||||
</h5>
|
<th scope="row">NAT (inside)</th>
|
||||||
<div class="card-body table-responsive">
|
<td>
|
||||||
{% render_table duplicate_ips_table 'inc/table.html' %}
|
{% if object.nat_inside %}
|
||||||
</div>
|
<a href="{{ object.nat_inside.get_absolute_url }}">{{ object.nat_inside }}</a>
|
||||||
</div>
|
{% if object.nat_inside.assigned_object %}
|
||||||
{% endif %}
|
(<a href="{{ object.nat_inside.assigned_object.parent_object.get_absolute_url }}">{{ object.nat_inside.assigned_object.parent_object }}</a>)
|
||||||
<div class="my-3">
|
{% endif %}
|
||||||
{% include 'inc/panel_table.html' with table=related_ips_table heading='Related IP Addresses' %}
|
{% else %}
|
||||||
</div>
|
<span class="text-muted">None</span>
|
||||||
{% plugin_right_page object %}
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">NAT (outside)</th>
|
||||||
|
<td>
|
||||||
|
{% if object.nat_outside %}
|
||||||
|
<a href="{{ object.nat_outside.get_absolute_url }}">{{ object.nat_outside }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% include 'inc/panels/tags.html' %}
|
||||||
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col col-md-8">
|
||||||
|
{% include 'inc/panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
|
||||||
<div class="row my-3">
|
{% if duplicate_ips_table.rows %}
|
||||||
<div class="col col-md-4">
|
{# Custom version of panel_table.html #}
|
||||||
{% include 'inc/panels/tags.html' %}
|
<div class="card border-danger">
|
||||||
|
<h5 class="card-header">
|
||||||
|
<span class="text-danger">Duplicate IP Addresses</span>
|
||||||
|
{% if more_duplicate_ips %}
|
||||||
|
<div class="float-end">
|
||||||
|
<a type="button" class="btn btn-primary btn-sm"
|
||||||
|
{% if object.vrf %}
|
||||||
|
href="{% url 'ipam:ipaddress_list' %}?address={{ object.address.ip }}&vrf_id={{ object.vrf.pk }}"
|
||||||
|
{% else %}
|
||||||
|
href="{% url 'ipam:ipaddress_list' %}?address={{ object.address.ip }}&vrf_id=null"
|
||||||
|
{% endif %}
|
||||||
|
>Show all</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</h5>
|
||||||
|
<div class="card-body table-responsive">
|
||||||
|
{% render_table duplicate_ips_table 'inc/table.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="my-3">
|
||||||
|
{% include 'inc/panel_table.html' with table=related_ips_table heading='Related IP Addresses' %}
|
||||||
</div>
|
</div>
|
||||||
|
{% plugin_right_page object %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include 'inc/table_controls_htmx.html' with table_modal="VMInterfaceTable_config" %}
|
{% include 'inc/table_controls_htmx.html' with table_modal="VirtualMachineVMInterfaceTable_config" %}
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body" id="object_list">
|
<div class="card-body" id="object_list">
|
||||||
|
@ -57,6 +57,7 @@ HTTP_REQUEST_META_SAFE_COPY = [
|
|||||||
'HTTP_HOST',
|
'HTTP_HOST',
|
||||||
'HTTP_REFERER',
|
'HTTP_REFERER',
|
||||||
'HTTP_USER_AGENT',
|
'HTTP_USER_AGENT',
|
||||||
|
'HTTP_X_FORWARDED_FOR',
|
||||||
'QUERY_STRING',
|
'QUERY_STRING',
|
||||||
'REMOTE_ADDR',
|
'REMOTE_ADDR',
|
||||||
'REMOTE_HOST',
|
'REMOTE_HOST',
|
||||||
|
@ -12,7 +12,7 @@ from django_tables2.data import TableQuerysetData
|
|||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
|
||||||
from extras.choices import CustomFieldTypeChoices
|
from extras.choices import CustomFieldTypeChoices
|
||||||
from extras.models import CustomField
|
from extras.models import CustomField, CustomLink
|
||||||
from .utils import content_type_identifier, content_type_name
|
from .utils import content_type_identifier, content_type_name
|
||||||
from .paginator import EnhancedPaginator, get_paginate_count
|
from .paginator import EnhancedPaginator, get_paginate_count
|
||||||
|
|
||||||
@ -34,15 +34,18 @@ class BaseTable(tables.Table):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, user=None, extra_columns=None, **kwargs):
|
def __init__(self, *args, user=None, extra_columns=None, **kwargs):
|
||||||
|
if extra_columns is None:
|
||||||
|
extra_columns = []
|
||||||
|
|
||||||
# Add custom field columns
|
# Add custom field columns
|
||||||
obj_type = ContentType.objects.get_for_model(self._meta.model)
|
obj_type = ContentType.objects.get_for_model(self._meta.model)
|
||||||
cf_columns = [
|
cf_columns = [
|
||||||
(f'cf_{cf.name}', CustomFieldColumn(cf)) for cf in CustomField.objects.filter(content_types=obj_type)
|
(f'cf_{cf.name}', CustomFieldColumn(cf)) for cf in CustomField.objects.filter(content_types=obj_type)
|
||||||
]
|
]
|
||||||
if extra_columns is not None:
|
cl_columns = [
|
||||||
extra_columns.extend(cf_columns)
|
(f'cl_{cl.name}', CustomLinkColumn(cl)) for cl in CustomLink.objects.filter(content_type=obj_type)
|
||||||
else:
|
]
|
||||||
extra_columns = cf_columns
|
extra_columns.extend([*cf_columns, *cl_columns])
|
||||||
|
|
||||||
super().__init__(*args, extra_columns=extra_columns, **kwargs)
|
super().__init__(*args, extra_columns=extra_columns, **kwargs)
|
||||||
|
|
||||||
@ -418,6 +421,37 @@ class CustomFieldColumn(tables.Column):
|
|||||||
return self.default
|
return self.default
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLinkColumn(tables.Column):
|
||||||
|
"""
|
||||||
|
Render a custom links as a table column.
|
||||||
|
"""
|
||||||
|
def __init__(self, customlink, *args, **kwargs):
|
||||||
|
self.customlink = customlink
|
||||||
|
kwargs['accessor'] = Accessor('pk')
|
||||||
|
if 'verbose_name' not in kwargs:
|
||||||
|
kwargs['verbose_name'] = customlink.name
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def render(self, record):
|
||||||
|
try:
|
||||||
|
rendered = self.customlink.render({'obj': record})
|
||||||
|
if rendered:
|
||||||
|
return mark_safe(f'<a href="{rendered["link"]}"{rendered["link_target"]}>{rendered["text"]}</a>')
|
||||||
|
except Exception as e:
|
||||||
|
return mark_safe(f'<span class="text-danger" title="{e}"><i class="mdi mdi-alert"></i> Error</span>')
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def value(self, record):
|
||||||
|
try:
|
||||||
|
rendered = self.customlink.render({'obj': record})
|
||||||
|
if rendered:
|
||||||
|
return rendered['link']
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class MPTTColumn(tables.TemplateColumn):
|
class MPTTColumn(tables.TemplateColumn):
|
||||||
"""
|
"""
|
||||||
Display a nested hierarchy for MPTT-enabled models.
|
Display a nested hierarchy for MPTT-enabled models.
|
||||||
|
@ -4,6 +4,10 @@ from django import template
|
|||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Filters
|
||||||
|
#
|
||||||
|
|
||||||
@register.filter()
|
@register.filter()
|
||||||
def getfield(form, fieldname):
|
def getfield(form, fieldname):
|
||||||
"""
|
"""
|
||||||
@ -12,38 +16,6 @@ def getfield(form, fieldname):
|
|||||||
return form[fieldname]
|
return form[fieldname]
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('utilities/render_field.html')
|
|
||||||
def render_field(field, bulk_nullable=False, label=None):
|
|
||||||
"""
|
|
||||||
Render a single form field from template
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'field': field,
|
|
||||||
'label': label,
|
|
||||||
'bulk_nullable': bulk_nullable,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('utilities/render_custom_fields.html')
|
|
||||||
def render_custom_fields(form):
|
|
||||||
"""
|
|
||||||
Render all custom fields in a form
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'form': form,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('utilities/render_form.html')
|
|
||||||
def render_form(form):
|
|
||||||
"""
|
|
||||||
Render an entire form from template
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'form': form,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name='widget_type')
|
@register.filter(name='widget_type')
|
||||||
def widget_type(field):
|
def widget_type(field):
|
||||||
"""
|
"""
|
||||||
@ -57,7 +29,43 @@ def widget_type(field):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('utilities/render_errors.html')
|
#
|
||||||
|
# Inclusion tags
|
||||||
|
#
|
||||||
|
|
||||||
|
@register.inclusion_tag('form_helpers/render_field.html')
|
||||||
|
def render_field(field, bulk_nullable=False, label=None):
|
||||||
|
"""
|
||||||
|
Render a single form field from template
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'field': field,
|
||||||
|
'label': label,
|
||||||
|
'bulk_nullable': bulk_nullable,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register.inclusion_tag('form_helpers/render_custom_fields.html')
|
||||||
|
def render_custom_fields(form):
|
||||||
|
"""
|
||||||
|
Render all custom fields in a form
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'form': form,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register.inclusion_tag('form_helpers/render_form.html')
|
||||||
|
def render_form(form):
|
||||||
|
"""
|
||||||
|
Render an entire form from template
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'form': form,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register.inclusion_tag('form_helpers/render_errors.html')
|
||||||
def render_errors(form):
|
def render_errors(form):
|
||||||
"""
|
"""
|
||||||
Render form errors, if they exist.
|
Render form errors, if they exist.
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
from django import template
|
|
||||||
|
|
||||||
register = template.Library()
|
|
||||||
|
|
||||||
TERMS_DANGER = ("delete", "deleted", "remove", "removed")
|
|
||||||
TERMS_WARNING = ("changed", "updated", "change", "update")
|
|
||||||
TERMS_SUCCESS = ("created", "added", "create", "add")
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
|
||||||
def get_status(text: str) -> str:
|
|
||||||
lower = text.lower()
|
|
||||||
|
|
||||||
if lower in TERMS_DANGER:
|
|
||||||
return "danger"
|
|
||||||
elif lower in TERMS_WARNING:
|
|
||||||
return "warning"
|
|
||||||
elif lower in TERMS_SUCCESS:
|
|
||||||
return "success"
|
|
||||||
else:
|
|
||||||
return "info"
|
|
@ -59,6 +59,10 @@ def render_markdown(value):
|
|||||||
# Render Markdown
|
# Render Markdown
|
||||||
html = markdown(value, extensions=['fenced_code', 'tables', StrikethroughExtension()])
|
html = markdown(value, extensions=['fenced_code', 'tables', StrikethroughExtension()])
|
||||||
|
|
||||||
|
# If the string is not empty wrap it in rendered-markdown to style tables
|
||||||
|
if html:
|
||||||
|
html = f'<div class="rendered-markdown">{html}</div>'
|
||||||
|
|
||||||
return mark_safe(html)
|
return mark_safe(html)
|
||||||
|
|
||||||
|
|
||||||
@ -380,7 +384,7 @@ def querystring(request, **kwargs):
|
|||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('utilities/templatetags/utilization_graph.html')
|
@register.inclusion_tag('helpers/utilization_graph.html')
|
||||||
def utilization_graph(utilization, warning_threshold=75, danger_threshold=90):
|
def utilization_graph(utilization, warning_threshold=75, danger_threshold=90):
|
||||||
"""
|
"""
|
||||||
Display a horizontal bar graph indicating a percentage of utilization.
|
Display a horizontal bar graph indicating a percentage of utilization.
|
||||||
@ -399,7 +403,7 @@ def utilization_graph(utilization, warning_threshold=75, danger_threshold=90):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('utilities/templatetags/tag.html')
|
@register.inclusion_tag('helpers/tag.html')
|
||||||
def tag(tag, url_name=None):
|
def tag(tag, url_name=None):
|
||||||
"""
|
"""
|
||||||
Display a tag, optionally linked to a filtered list of objects.
|
Display a tag, optionally linked to a filtered list of objects.
|
||||||
@ -410,7 +414,7 @@ def tag(tag, url_name=None):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('utilities/templatetags/badge.html')
|
@register.inclusion_tag('helpers/badge.html')
|
||||||
def badge(value, bg_class='secondary', show_empty=False):
|
def badge(value, bg_class='secondary', show_empty=False):
|
||||||
"""
|
"""
|
||||||
Display the specified number as a badge.
|
Display the specified number as a badge.
|
||||||
@ -422,7 +426,7 @@ def badge(value, bg_class='secondary', show_empty=False):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('utilities/templatetags/table_config_form.html')
|
@register.inclusion_tag('helpers/table_config_form.html')
|
||||||
def table_config_form(table, table_name=None):
|
def table_config_form(table, table_name=None):
|
||||||
return {
|
return {
|
||||||
'table_name': table_name or table.__class__.__name__,
|
'table_name': table_name or table.__class__.__name__,
|
||||||
@ -430,7 +434,7 @@ def table_config_form(table, table_name=None):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag('utilities/templatetags/applied_filters.html')
|
@register.inclusion_tag('helpers/applied_filters.html')
|
||||||
def applied_filters(form, query_params):
|
def applied_filters(form, query_params):
|
||||||
"""
|
"""
|
||||||
Display the active filters for a given filter form.
|
Display the active filters for a given filter form.
|
||||||
|
@ -8,7 +8,7 @@ from netbox.navigation_menu import MENUS
|
|||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag("navigation/nav_items.html", takes_context=True)
|
@register.inclusion_tag("navigation/menu.html", takes_context=True)
|
||||||
def nav(context: Context) -> Dict:
|
def nav(context: Context) -> Dict:
|
||||||
"""
|
"""
|
||||||
Render the navigation menu.
|
Render the navigation menu.
|
@ -288,45 +288,6 @@ def flatten_dict(d, prefix='', separator='.'):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def decode_dict(encoded_dict: Dict, *, decode_keys: bool = True) -> Dict:
|
|
||||||
"""
|
|
||||||
Recursively URL decode string keys and values of a dict.
|
|
||||||
|
|
||||||
For example, `{'1%2F1%2F1': {'1%2F1%2F2': ['1%2F1%2F3', '1%2F1%2F4']}}` would
|
|
||||||
become: `{'1/1/1': {'1/1/2': ['1/1/3', '1/1/4']}}`
|
|
||||||
|
|
||||||
:param encoded_dict: Dictionary to be decoded.
|
|
||||||
:param decode_keys: (Optional) Enable/disable decoding of dict keys.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def decode_value(value: Any, _decode_keys: bool) -> Any:
|
|
||||||
"""
|
|
||||||
Handle URL decoding of any supported value type.
|
|
||||||
"""
|
|
||||||
# Decode string values.
|
|
||||||
if isinstance(value, str):
|
|
||||||
return urllib.parse.unquote(value)
|
|
||||||
# Recursively decode each list item.
|
|
||||||
elif isinstance(value, list):
|
|
||||||
return [decode_value(v, _decode_keys) for v in value]
|
|
||||||
# Recursively decode each tuple item.
|
|
||||||
elif isinstance(value, Tuple):
|
|
||||||
return tuple(decode_value(v, _decode_keys) for v in value)
|
|
||||||
# Recursively decode each dict key/value pair.
|
|
||||||
elif isinstance(value, dict):
|
|
||||||
# Don't decode keys, if `decode_keys` is false.
|
|
||||||
if not _decode_keys:
|
|
||||||
return {k: decode_value(v, _decode_keys) for k, v in value.items()}
|
|
||||||
return {urllib.parse.unquote(k): decode_value(v, _decode_keys) for k, v in value.items()}
|
|
||||||
return value
|
|
||||||
|
|
||||||
if not decode_keys:
|
|
||||||
# Don't decode keys, if `decode_keys` is false.
|
|
||||||
return {k: decode_value(v, decode_keys) for k, v in encoded_dict.items()}
|
|
||||||
|
|
||||||
return {urllib.parse.unquote(k): decode_value(v, decode_keys) for k, v in encoded_dict.items()}
|
|
||||||
|
|
||||||
|
|
||||||
def array_to_string(array):
|
def array_to_string(array):
|
||||||
"""
|
"""
|
||||||
Generate an efficient, human-friendly string from a set of integers. Intended for use with ArrayField.
|
Generate an efficient, human-friendly string from a set of integers. Intended for use with ArrayField.
|
||||||
|
@ -40,6 +40,7 @@ class WirelessLANSerializer(PrimaryModelSerializer):
|
|||||||
model = WirelessLAN
|
model = WirelessLAN
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'ssid', 'description', 'group', 'vlan', 'auth_type', 'auth_cipher', 'auth_psk',
|
'id', 'url', 'display', 'ssid', 'description', 'group', 'vlan', 'auth_type', 'auth_cipher', 'auth_psk',
|
||||||
|
'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -55,5 +56,5 @@ class WirelessLinkSerializer(PrimaryModelSerializer):
|
|||||||
model = WirelessLink
|
model = WirelessLink
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'status', 'description', 'auth_type',
|
'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'status', 'description', 'auth_type',
|
||||||
'auth_cipher', 'auth_psk',
|
'auth_cipher', 'auth_psk', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user