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
|
||||
|
||||
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)
|
||||
|
||||
### 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
|
||||
|
||||
* [#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
|
||||
* [#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
|
||||
* [#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.models import *
|
||||
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.exceptions import ServiceUnavailable
|
||||
from netbox.api.metadata import ContentTypeMetadata
|
||||
from netbox.api.views import ModelViewSet
|
||||
from netbox.config import get_config
|
||||
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 . import serializers
|
||||
from .exceptions import MissingFilterException
|
||||
|
|
@ -516,7 +516,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
|
|||
response[method] = {'error': 'Only get_* NAPALM methods are supported'}
|
||||
continue
|
||||
try:
|
||||
response[method] = decode_dict(getattr(d, method)())
|
||||
response[method] = getattr(d, method)()
|
||||
except NotImplementedError:
|
||||
response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)}
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -229,6 +229,24 @@ class CustomLink(ChangeLoggedModel):
|
|||
def get_absolute_url(self):
|
||||
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')
|
||||
class ExportTemplate(ChangeLoggedModel):
|
||||
|
|
|
|||
|
|
@ -62,16 +62,14 @@ def custom_links(context, obj):
|
|||
# Add non-grouped links
|
||||
else:
|
||||
try:
|
||||
text_rendered = render_jinja2(cl.link_text, link_context)
|
||||
if text_rendered:
|
||||
link_rendered = render_jinja2(cl.link_url, link_context)
|
||||
link_target = ' target="_blank"' if cl.new_window else ''
|
||||
rendered = cl.render(link_context)
|
||||
if rendered:
|
||||
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:
|
||||
template_code += '<a class="btn btn-sm btn-outline-dark" disabled="disabled" title="{}">' \
|
||||
'<i class="mdi mdi-alert"></i> {}</a>\n'.format(e, cl.name)
|
||||
template_code += f'<a class="btn btn-sm btn-outline-dark" disabled="disabled" title="{e}">' \
|
||||
f'<i class="mdi mdi-alert"></i> {cl.name}</a>\n'
|
||||
|
||||
# Add grouped links to template
|
||||
for group, links in group_names.items():
|
||||
|
|
@ -80,17 +78,15 @@ def custom_links(context, obj):
|
|||
|
||||
for cl in links:
|
||||
try:
|
||||
text_rendered = render_jinja2(cl.link_text, link_context)
|
||||
if text_rendered:
|
||||
link_target = ' target="_blank"' if cl.new_window else ''
|
||||
link_rendered = render_jinja2(cl.link_url, link_context)
|
||||
rendered = cl.render(link_context)
|
||||
if rendered:
|
||||
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:
|
||||
links_rendered.append(
|
||||
'<li><a class="dropdown-item" disabled="disabled" title="{}"><span class="text-muted">'
|
||||
'<i class="mdi mdi-alert"></i> {}</span></a></li>'.format(e, cl.name)
|
||||
f'<li><a class="dropdown-item" disabled="disabled" title="{e}"><span class="text-muted">'
|
||||
f'<i class="mdi mdi-alert"></i> {cl.name}</span></a></li>'
|
||||
)
|
||||
|
||||
if links_rendered:
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ class FHRPGroupProtocolChoices(ChoiceSet):
|
|||
PROTOCOL_HSRP = 'hsrp'
|
||||
PROTOCOL_GLBP = 'glbp'
|
||||
PROTOCOL_CARP = 'carp'
|
||||
PROTOCOL_OTHER = 'other'
|
||||
|
||||
CHOICES = (
|
||||
(PROTOCOL_VRRP2, 'VRRPv2'),
|
||||
|
|
@ -113,6 +114,7 @@ class FHRPGroupProtocolChoices(ChoiceSet):
|
|||
(PROTOCOL_HSRP, 'HSRP'),
|
||||
(PROTOCOL_GLBP, 'GLBP'),
|
||||
(PROTOCOL_CARP, 'CARP'),
|
||||
(PROTOCOL_OTHER, 'Other'),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -471,6 +471,8 @@ class IPAddressForm(TenancyForm, CustomFieldModelForm):
|
|||
})
|
||||
elif selected_objects:
|
||||
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.
|
||||
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
|
||||
|
||||
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
|
||||
group_list = []
|
||||
|
|
@ -141,7 +141,7 @@ class RemoteUserBackend(_RemoteUserBackend):
|
|||
Return None if ``create_unknown_user`` is ``False`` and a ``User``
|
||||
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(
|
||||
f"trying to authenticate {remote_user} with groups {remote_groups}")
|
||||
if not remote_user:
|
||||
|
|
@ -173,7 +173,7 @@ class RemoteUserBackend(_RemoteUserBackend):
|
|||
return None
|
||||
|
||||
def _is_superuser(self, user):
|
||||
logger = logging.getLogger('netbox.authentication.RemoteUserBackend')
|
||||
logger = logging.getLogger('netbox.auth.RemoteUserBackend')
|
||||
superuser_groups = settings.REMOTE_AUTH_SUPERUSER_GROUPS
|
||||
logger.debug(f"Superuser Groups: {superuser_groups}")
|
||||
superusers = settings.REMOTE_AUTH_SUPERUSERS
|
||||
|
|
@ -189,7 +189,7 @@ class RemoteUserBackend(_RemoteUserBackend):
|
|||
return bool(result)
|
||||
|
||||
def _is_staff(self, user):
|
||||
logger = logging.getLogger('netbox.authentication.RemoteUserBackend')
|
||||
logger = logging.getLogger('netbox.auth.RemoteUserBackend')
|
||||
staff_groups = settings.REMOTE_AUTH_STAFF_GROUPS
|
||||
logger.debug(f"Superuser Groups: {staff_groups}")
|
||||
staff_users = settings.REMOTE_AUTH_STAFF_USERS
|
||||
|
|
@ -204,7 +204,7 @@ class RemoteUserBackend(_RemoteUserBackend):
|
|||
return bool(result)
|
||||
|
||||
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:
|
||||
# Assign default groups to the user
|
||||
group_list = []
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -35,11 +35,6 @@ function handleFormSubmit(event: Event, form: HTMLFormElement): void {
|
|||
for (const element of form.querySelectorAll<FormControls>('*[name]')) {
|
||||
if (!element.validity.valid) {
|
||||
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 (!element.classList.contains('is-invalid')) {
|
||||
element.classList.add('is-invalid');
|
||||
|
|
@ -49,10 +44,6 @@ function handleFormSubmit(event: Event, form: HTMLFormElement): void {
|
|||
if (element.classList.contains('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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 { initRackElevation } from './racks';
|
||||
import { initLinks } from './links';
|
||||
import { initHtmx } from './htmx';
|
||||
|
||||
function initDocument(): void {
|
||||
for (const init of [
|
||||
|
|
@ -29,6 +30,7 @@ function initDocument(): void {
|
|||
initSideNav,
|
||||
initRackElevation,
|
||||
initLinks,
|
||||
initHtmx,
|
||||
]) {
|
||||
init();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -965,6 +965,19 @@ div.card-overlay {
|
|||
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
|
||||
td pre {
|
||||
margin-bottom: 0
|
||||
|
|
|
|||
|
|
@ -104,23 +104,23 @@
|
|||
{# Static resources #}
|
||||
<link
|
||||
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'"
|
||||
/>
|
||||
<link
|
||||
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'"
|
||||
/>
|
||||
<link
|
||||
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'"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
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'"
|
||||
/>
|
||||
<link rel="icon" type="image/png" href="{% static 'netbox.ico' %}" />
|
||||
|
|
@ -129,7 +129,7 @@
|
|||
{# Javascript #}
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="{% static 'netbox.js' %}"
|
||||
src="{% static 'netbox.js' %}?v={{ settings.VERSION }}"
|
||||
onerror="window.location='{% url 'media_failure' %}?filename=netbox.js'">
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
{# Base layout for the core NetBox UI w/navbar and page content #}
|
||||
{% extends 'base/base.html' %}
|
||||
{% load helpers %}
|
||||
{% load nav %}
|
||||
{% load search_options %}
|
||||
{% load search %}
|
||||
{% load static %}
|
||||
|
||||
{% block layout %}
|
||||
|
|
@ -21,7 +20,7 @@
|
|||
</div>
|
||||
|
||||
{# 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 #}
|
||||
<div class="nav-mobile">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% load nav %}
|
||||
{% load navigation %}
|
||||
{% load static %}
|
||||
|
||||
<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 %}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,11 +41,11 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<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>
|
||||
<th scope="row">Circuits</th>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load helpers %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% block title %}Delete console port {{ consoleport }}?{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% block title %}Delete device bay {{ devicebay }}?{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% block title %}Delete interface {{ interface }}?{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Delete inventory item {{ inventoryitem }}?{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Delete power outlet {{ poweroutlet }}?{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Delete power port {{ powerport }}?{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Remove Virtual Chassis Member?{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'utilities/confirmation_form.html' %}
|
||||
{% extends 'generic/confirmation_form.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Delete {{ obj_type }}?{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{% extends 'base/layout.html' %}
|
||||
{% load get_status %}
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
|
|
@ -24,7 +23,7 @@
|
|||
{% block title %}Home{% endblock %}
|
||||
|
||||
{% block content-wrapper %}
|
||||
<div class="p-3">
|
||||
<div class="px-3">
|
||||
{# General stats #}
|
||||
<div class="row masonry">
|
||||
{% for section, items, icon in stats %}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<h5 class="card-header">
|
||||
Comments
|
||||
</h5>
|
||||
<div class="card-body rendered-markdown">
|
||||
<div class="card-body">
|
||||
{% if object.comments %}
|
||||
{{ object.comments|render_markdown }}
|
||||
{% 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><hr class="dropdown-divider" /></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
|
||||
</a>
|
||||
</li>
|
||||
|
|
@ -38,11 +38,7 @@
|
|||
</span>
|
||||
{% else %}
|
||||
<div class="btn-group">
|
||||
<a
|
||||
class="btn btn-primary ws-nowrap"
|
||||
type="button"
|
||||
href="{% url 'login' %}"
|
||||
>
|
||||
<a class="btn btn-primary ws-nowrap" type="button" href="{% url 'login' %}">
|
||||
<i class="mdi mdi-login-variant"></i> Log In
|
||||
</a>
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
|
||||
|
|
|
|||
|
|
@ -13,143 +13,135 @@
|
|||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-md-4">
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
IP Address
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Family</th>
|
||||
<td>IPv{{ object.family }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">VRF</th>
|
||||
<td>
|
||||
{% if object.vrf %}
|
||||
<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 }}
|
||||
<div class="card">
|
||||
<h5 class="card-header">
|
||||
IP Address
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">Family</th>
|
||||
<td>IPv{{ object.family }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">VRF</th>
|
||||
<td>
|
||||
{% if object.vrf %}
|
||||
<a href="{% url 'ipam:vrf' pk=object.vrf.pk %}">{{ object.vrf }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
<span>Global</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">NAT (inside)</th>
|
||||
<td>
|
||||
{% if object.nat_inside %}
|
||||
<a href="{{ object.nat_inside.get_absolute_url }}">{{ object.nat_inside }}</a>
|
||||
{% if object.nat_inside.assigned_object %}
|
||||
(<a href="{{ object.nat_inside.assigned_object.parent_object.get_absolute_url }}">{{ object.nat_inside.assigned_object.parent_object }}</a>)
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% 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/custom_fields.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 %}
|
||||
{# Custom version of panel_table.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 }}"
|
||||
</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 %}
|
||||
href="{% url 'ipam:ipaddress_list' %}?address={{ object.address.ip }}&vrf_id=null"
|
||||
<span class="text-muted">—</span>
|
||||
{% 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>
|
||||
{% plugin_right_page object %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">NAT (inside)</th>
|
||||
<td>
|
||||
{% if object.nat_inside %}
|
||||
<a href="{{ object.nat_inside.get_absolute_url }}">{{ object.nat_inside }}</a>
|
||||
{% if object.nat_inside.assigned_object %}
|
||||
(<a href="{{ object.nat_inside.assigned_object.parent_object.get_absolute_url }}">{{ object.nat_inside.assigned_object.parent_object }}</a>)
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% 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 class="row my-3">
|
||||
<div class="col col-md-4">
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
<div class="col col-md-8">
|
||||
{% include 'inc/panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
|
||||
{% if duplicate_ips_table.rows %}
|
||||
{# Custom version of panel_table.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>
|
||||
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
{% block content %}
|
||||
<form method="post">
|
||||
{% 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-body" id="object_list">
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ HTTP_REQUEST_META_SAFE_COPY = [
|
|||
'HTTP_HOST',
|
||||
'HTTP_REFERER',
|
||||
'HTTP_USER_AGENT',
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
'QUERY_STRING',
|
||||
'REMOTE_ADDR',
|
||||
'REMOTE_HOST',
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from django_tables2.data import TableQuerysetData
|
|||
from django_tables2.utils import Accessor
|
||||
|
||||
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 .paginator import EnhancedPaginator, get_paginate_count
|
||||
|
||||
|
|
@ -34,15 +34,18 @@ class BaseTable(tables.Table):
|
|||
}
|
||||
|
||||
def __init__(self, *args, user=None, extra_columns=None, **kwargs):
|
||||
if extra_columns is None:
|
||||
extra_columns = []
|
||||
|
||||
# Add custom field columns
|
||||
obj_type = ContentType.objects.get_for_model(self._meta.model)
|
||||
cf_columns = [
|
||||
(f'cf_{cf.name}', CustomFieldColumn(cf)) for cf in CustomField.objects.filter(content_types=obj_type)
|
||||
]
|
||||
if extra_columns is not None:
|
||||
extra_columns.extend(cf_columns)
|
||||
else:
|
||||
extra_columns = cf_columns
|
||||
cl_columns = [
|
||||
(f'cl_{cl.name}', CustomLinkColumn(cl)) for cl in CustomLink.objects.filter(content_type=obj_type)
|
||||
]
|
||||
extra_columns.extend([*cf_columns, *cl_columns])
|
||||
|
||||
super().__init__(*args, extra_columns=extra_columns, **kwargs)
|
||||
|
||||
|
|
@ -418,6 +421,37 @@ class CustomFieldColumn(tables.Column):
|
|||
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):
|
||||
"""
|
||||
Display a nested hierarchy for MPTT-enabled models.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ from django import template
|
|||
register = template.Library()
|
||||
|
||||
|
||||
#
|
||||
# Filters
|
||||
#
|
||||
|
||||
@register.filter()
|
||||
def getfield(form, fieldname):
|
||||
"""
|
||||
|
|
@ -12,38 +16,6 @@ def getfield(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')
|
||||
def widget_type(field):
|
||||
"""
|
||||
|
|
@ -57,7 +29,43 @@ def widget_type(field):
|
|||
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):
|
||||
"""
|
||||
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
|
||||
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)
|
||||
|
||||
|
||||
|
|
@ -380,7 +384,7 @@ def querystring(request, **kwargs):
|
|||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
return {
|
||||
'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):
|
||||
"""
|
||||
Display the active filters for a given filter form.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from netbox.navigation_menu import MENUS
|
|||
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:
|
||||
"""
|
||||
Render the navigation menu.
|
||||
|
|
@ -288,45 +288,6 @@ def flatten_dict(d, prefix='', separator='.'):
|
|||
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):
|
||||
"""
|
||||
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
|
||||
fields = [
|
||||
'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
|
||||
fields = [
|
||||
'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