diff --git a/netbox/utilities/constants.py b/netbox/utilities/constants.py
index c3fbd0687..08e9dd9cf 100644
--- a/netbox/utilities/constants.py
+++ b/netbox/utilities/constants.py
@@ -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',
diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py
index 16abd273a..183d64023 100644
--- a/netbox/utilities/tables.py
+++ b/netbox/utilities/tables.py
@@ -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'
{rendered["text"]}')
+ except Exception as e:
+ return mark_safe(f'
Error')
+ 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.
diff --git a/netbox/templates/utilities/render_custom_fields.html b/netbox/utilities/templates/form_helpers/render_custom_fields.html
similarity index 100%
rename from netbox/templates/utilities/render_custom_fields.html
rename to netbox/utilities/templates/form_helpers/render_custom_fields.html
diff --git a/netbox/templates/utilities/render_errors.html b/netbox/utilities/templates/form_helpers/render_errors.html
similarity index 100%
rename from netbox/templates/utilities/render_errors.html
rename to netbox/utilities/templates/form_helpers/render_errors.html
diff --git a/netbox/templates/utilities/render_field.html b/netbox/utilities/templates/form_helpers/render_field.html
similarity index 100%
rename from netbox/templates/utilities/render_field.html
rename to netbox/utilities/templates/form_helpers/render_field.html
diff --git a/netbox/templates/utilities/render_form.html b/netbox/utilities/templates/form_helpers/render_form.html
similarity index 100%
rename from netbox/templates/utilities/render_form.html
rename to netbox/utilities/templates/form_helpers/render_form.html
diff --git a/netbox/templates/utilities/templatetags/applied_filters.html b/netbox/utilities/templates/helpers/applied_filters.html
similarity index 100%
rename from netbox/templates/utilities/templatetags/applied_filters.html
rename to netbox/utilities/templates/helpers/applied_filters.html
diff --git a/netbox/templates/utilities/templatetags/badge.html b/netbox/utilities/templates/helpers/badge.html
similarity index 100%
rename from netbox/templates/utilities/templatetags/badge.html
rename to netbox/utilities/templates/helpers/badge.html
diff --git a/netbox/templates/utilities/templatetags/table_config_form.html b/netbox/utilities/templates/helpers/table_config_form.html
similarity index 100%
rename from netbox/templates/utilities/templatetags/table_config_form.html
rename to netbox/utilities/templates/helpers/table_config_form.html
diff --git a/netbox/templates/utilities/templatetags/tag.html b/netbox/utilities/templates/helpers/tag.html
similarity index 100%
rename from netbox/templates/utilities/templatetags/tag.html
rename to netbox/utilities/templates/helpers/tag.html
diff --git a/netbox/templates/utilities/templatetags/utilization_graph.html b/netbox/utilities/templates/helpers/utilization_graph.html
similarity index 100%
rename from netbox/templates/utilities/templatetags/utilization_graph.html
rename to netbox/utilities/templates/helpers/utilization_graph.html
diff --git a/netbox/utilities/templates/navigation/nav_items.html b/netbox/utilities/templates/navigation/menu.html
similarity index 100%
rename from netbox/utilities/templates/navigation/nav_items.html
rename to netbox/utilities/templates/navigation/menu.html
diff --git a/netbox/utilities/templatetags/form_helpers.py b/netbox/utilities/templatetags/form_helpers.py
index 808b24b4d..03cc0ac0b 100644
--- a/netbox/utilities/templatetags/form_helpers.py
+++ b/netbox/utilities/templatetags/form_helpers.py
@@ -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.
diff --git a/netbox/utilities/templatetags/get_status.py b/netbox/utilities/templatetags/get_status.py
deleted file mode 100644
index fde677053..000000000
--- a/netbox/utilities/templatetags/get_status.py
+++ /dev/null
@@ -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"
diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py
index db9c40fc5..c054785fc 100644
--- a/netbox/utilities/templatetags/helpers.py
+++ b/netbox/utilities/templatetags/helpers.py
@@ -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'
{html}
'
+
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.
diff --git a/netbox/utilities/templatetags/nav.py b/netbox/utilities/templatetags/navigation.py
similarity index 81%
rename from netbox/utilities/templatetags/nav.py
rename to netbox/utilities/templatetags/navigation.py
index 205d1b590..ede8792fa 100644
--- a/netbox/utilities/templatetags/nav.py
+++ b/netbox/utilities/templatetags/navigation.py
@@ -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.
diff --git a/netbox/utilities/templatetags/search_options.py b/netbox/utilities/templatetags/search.py
similarity index 100%
rename from netbox/utilities/templatetags/search_options.py
rename to netbox/utilities/templatetags/search.py
diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py
index 3fc50ddc4..444b87523 100644
--- a/netbox/utilities/utils.py
+++ b/netbox/utilities/utils.py
@@ -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.
diff --git a/netbox/wireless/api/serializers.py b/netbox/wireless/api/serializers.py
index 68e8181f1..f1fa6d58d 100644
--- a/netbox/wireless/api/serializers.py
+++ b/netbox/wireless/api/serializers.py
@@ -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',
]