Closes #12246: General cleanup of utilities modules

* Clean up base modules

* Clean up forms modules

* Clean up templatetags modules

* Replace custom simplify_decimal filter with floatformat

* Misc cleanup

* Merge ReturnURLForm into ConfirmationForm

* Clean up import statements for utilities.forms

* Fix field class references in docs
This commit is contained in:
Jeremy Stretch
2023-04-14 10:33:53 -04:00
committed by GitHub
parent 59a6b3e71b
commit d470848b29
87 changed files with 585 additions and 406 deletions

View File

@@ -10,6 +10,14 @@ from rest_framework.utils import formatting
from netbox.api.exceptions import GraphQLTypeNotFound, SerializerNotFound
from .utils import dynamic_import
__all__ = (
'get_graphql_type_for_model',
'get_serializer_for_model',
'get_view_name',
'is_api_request',
'rest_api_server_error',
)
def get_serializer_for_model(model, prefix=''):
"""

View File

@@ -31,21 +31,6 @@ FILTER_TREENODE_NEGATION_LOOKUP_MAP = dict(
n='in'
)
# Keys for PostgreSQL advisory locks. These are arbitrary bigints used by
# the advisory_lock contextmanager. When a lock is acquired,
# one of these keys will be used to identify said lock.
#
# When adding a new key, pick something arbitrary and unique so
# that it is easily searchable in query logs.
ADVISORY_LOCK_KEYS = {
'available-prefixes': 100100,
'available-ips': 100200,
'available-vlans': 100300,
'available-asns': 100400,
}
#
# HTTP Request META safe copy
#

View File

@@ -3,6 +3,7 @@ from rest_framework.exceptions import APIException
__all__ = (
'AbortRequest',
'AbortScript',
'AbortTransaction',
'PermissionsViolation',
'RQWorkerNotRunningException',

View File

@@ -1,21 +1,23 @@
from collections import defaultdict
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.validators import RegexValidator
from django.db import models
from utilities.ordering import naturalize
from .forms import ColorSelect
from .forms.widgets import ColorSelect
from .validators import ColorValidator
ColorValidator = RegexValidator(
regex='^[0-9a-f]{6}$',
message='Enter a valid hexadecimal RGB color code.',
code='invalid'
__all__ = (
'ColorField',
'NaturalOrderingField',
'NullableCharField',
'RestrictedGenericForeignKey',
)
# Deprecated: Retained only to ensure successful migration from early releases
# Use models.CharField(null=True) instead
# TODO: Remove in v4.0
class NullableCharField(models.CharField):
description = "Stores empty values as NULL rather than ''"

View File

@@ -1,5 +1,9 @@
import hashlib
__all__ = (
'sha256_hash',
)
def sha256_hash(filepath):
"""

View File

@@ -6,6 +6,22 @@ from django_filters.constants import EMPTY_VALUES
from drf_spectacular.utils import extend_schema_field
from drf_spectacular.types import OpenApiTypes
__all__ = (
'ContentTypeFilter',
'MACAddressFilter',
'MultiValueCharFilter',
'MultiValueDateFilter',
'MultiValueDateTimeFilter',
'MultiValueDecimalFilter',
'MultiValueMACAddressFilter',
'MultiValueNumberFilter',
'MultiValueTimeFilter',
'MultiValueWWNFilter',
'NullableCharFieldFilter',
'NumericArrayFilter',
'TreeNodeMultipleChoiceFilter',
)
def multivalue_field_factory(field_class):
"""

View File

@@ -1,5 +1,4 @@
from .constants import *
from .fields import *
from .forms import *
from .mixins import *
from .utils import *
from .widgets import *

View File

@@ -1,3 +1,4 @@
from .array import *
from .content_types import *
from .csv import *
from .dynamic import *

View File

@@ -0,0 +1,24 @@
from django import forms
from django.contrib.postgres.forms import SimpleArrayField
from ..utils import parse_numeric_range
__all__ = (
'NumericArrayField',
)
class NumericArrayField(SimpleArrayField):
def clean(self, value):
if value and not self.to_python(value):
raise forms.ValidationError(f'Invalid list ({value}). '
f'Must be numeric and ranges must be in ascending order')
return super().clean(value)
def to_python(self, value):
if not value:
return []
if isinstance(value, str):
value = ','.join([str(n) for n in parse_numeric_range(value)])
return super().to_python(value)

View File

@@ -1,6 +1,5 @@
from django import forms
from utilities.forms import widgets
from utilities.utils import content_type_name
__all__ = (

View File

@@ -1,14 +1,9 @@
import csv
from io import StringIO
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.db.models import Q
from django.utils.translation import gettext as _
from utilities.choices import unpack_grouped_choices
from utilities.forms.utils import parse_csv, validate_csv
from utilities.utils import content_type_identifier
__all__ = (

View File

@@ -2,96 +2,31 @@ import re
from django import forms
from django.utils.translation import gettext as _
from .widgets import APISelect, APISelectMultiple, ClearableFileInput
from .mixins import BootstrapMixin
__all__ = (
'BootstrapMixin',
'BulkEditForm',
'BulkRenameForm',
'ConfirmationForm',
'CSVModelForm',
'FilterForm',
'ReturnURLForm',
'TableConfigForm',
)
#
# Mixins
#
class BootstrapMixin:
class ConfirmationForm(BootstrapMixin, forms.Form):
"""
Add the base Bootstrap CSS classes to form elements.
A generic confirmation form. The form is not valid unless the `confirm` field is checked.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
exempt_widgets = [
forms.FileInput,
forms.RadioSelect,
APISelect,
APISelectMultiple,
ClearableFileInput,
]
for field_name, field in self.fields.items():
css = field.widget.attrs.get('class', '')
if field.widget.__class__ in exempt_widgets:
continue
elif isinstance(field.widget, forms.CheckboxInput):
field.widget.attrs['class'] = f'{css} form-check-input'
elif isinstance(field.widget, forms.SelectMultiple):
if 'size' not in field.widget.attrs:
field.widget.attrs['class'] = f'{css} netbox-static-select'
elif isinstance(field.widget, forms.Select):
field.widget.attrs['class'] = f'{css} netbox-static-select'
else:
field.widget.attrs['class'] = f'{css} form-control'
if field.required and not isinstance(field.widget, forms.FileInput):
field.widget.attrs['required'] = 'required'
if 'placeholder' not in field.widget.attrs and field.label is not None:
field.widget.attrs['placeholder'] = field.label
def is_valid(self):
is_valid = super().is_valid()
# Apply is-invalid CSS class to fields with errors
if not is_valid:
for field_name in self.errors:
# Ignore e.g. __all__
if field := self.fields.get(field_name):
css = field.widget.attrs.get('class', '')
field.widget.attrs['class'] = f'{css} is-invalid'
return is_valid
#
# Form classes
#
class ReturnURLForm(forms.Form):
"""
Provides a hidden return URL field to control where the user is directed after the form is submitted.
"""
return_url = forms.CharField(required=False, widget=forms.HiddenInput())
class ConfirmationForm(BootstrapMixin, ReturnURLForm):
"""
A generic confirmation form. The form is not valid unless the confirm field is checked.
"""
confirm = forms.BooleanField(required=True, widget=forms.HiddenInput(), initial=True)
return_url = forms.CharField(
required=False,
widget=forms.HiddenInput()
)
confirm = forms.BooleanField(
required=True,
widget=forms.HiddenInput(),
initial=True
)
class BulkEditForm(BootstrapMixin, forms.Form):

View File

@@ -0,0 +1,62 @@
from django import forms
from .widgets import APISelect, APISelectMultiple, ClearableFileInput
__all__ = (
'BootstrapMixin',
)
class BootstrapMixin:
"""
Add the base Bootstrap CSS classes to form elements.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
exempt_widgets = [
forms.FileInput,
forms.RadioSelect,
APISelect,
APISelectMultiple,
ClearableFileInput,
]
for field_name, field in self.fields.items():
css = field.widget.attrs.get('class', '')
if field.widget.__class__ in exempt_widgets:
continue
elif isinstance(field.widget, forms.CheckboxInput):
field.widget.attrs['class'] = f'{css} form-check-input'
elif isinstance(field.widget, forms.SelectMultiple):
if 'size' not in field.widget.attrs:
field.widget.attrs['class'] = f'{css} netbox-static-select'
elif isinstance(field.widget, forms.Select):
field.widget.attrs['class'] = f'{css} netbox-static-select'
else:
field.widget.attrs['class'] = f'{css} form-control'
if field.required and not isinstance(field.widget, forms.FileInput):
field.widget.attrs['required'] = 'required'
if 'placeholder' not in field.widget.attrs and field.label is not None:
field.widget.attrs['placeholder'] = field.label
def is_valid(self):
is_valid = super().is_valid()
# Apply is-invalid CSS class to fields with errors
if not is_valid:
for field_name in self.errors:
# Ignore e.g. __all__
if field := self.fields.get(field_name):
css = field.widget.attrs.get('class', '')
field.widget.attrs['class'] = f'{css} is-invalid'
return is_valid

View File

@@ -0,0 +1,4 @@
from .apiselect import *
from .datetime import *
from .misc import *
from .select import *

View File

@@ -1,120 +1,14 @@
import json
from typing import Dict, Sequence, List, Tuple, Union
from typing import Dict, List, Tuple
from django import forms
from django.conf import settings
from django.contrib.postgres.forms import SimpleArrayField
from utilities.choices import ColorChoices
from .utils import add_blank_choice, parse_numeric_range
__all__ = (
'APISelect',
'APISelectMultiple',
'BulkEditNullBooleanSelect',
'ClearableFileInput',
'ColorSelect',
'DatePicker',
'DateTimePicker',
'HTMXSelect',
'MarkdownWidget',
'NumericArrayField',
'SelectDurationWidget',
'SelectSpeedWidget',
'SelectWithPK',
'SlugWidget',
'TimePicker',
)
JSONPrimitive = Union[str, bool, int, float, None]
QueryParamValue = Union[JSONPrimitive, Sequence[JSONPrimitive]]
QueryParam = Dict[str, QueryParamValue]
ProcessedParams = Sequence[Dict[str, Sequence[JSONPrimitive]]]
class SlugWidget(forms.TextInput):
"""
Subclass TextInput and add a slug regeneration button next to the form field.
"""
template_name = 'widgets/sluginput.html'
class ColorSelect(forms.Select):
"""
Extends the built-in Select widget to colorize each <option>.
"""
option_template_name = 'widgets/colorselect_option.html'
def __init__(self, *args, **kwargs):
kwargs['choices'] = add_blank_choice(ColorChoices)
super().__init__(*args, **kwargs)
self.attrs['class'] = 'netbox-color-select'
class BulkEditNullBooleanSelect(forms.NullBooleanSelect):
"""
A Select widget for NullBooleanFields
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Override the built-in choice labels
self.choices = (
('1', '---------'),
('2', 'Yes'),
('3', 'No'),
)
self.attrs['class'] = 'netbox-static-select'
class SelectWithPK(forms.Select):
"""
Include the primary key of each option in the option label (e.g. "Router7 (4721)").
"""
option_template_name = 'widgets/select_option_with_pk.html'
class SelectSpeedWidget(forms.NumberInput):
"""
Speed field with dropdown selections for convenience.
"""
template_name = 'widgets/select_speed.html'
class SelectDurationWidget(forms.NumberInput):
"""
Dropdown to select one of several common options for a time duration (in minutes).
"""
template_name = 'widgets/select_duration.html'
class MarkdownWidget(forms.Textarea):
template_name = 'widgets/markdown_input.html'
class NumericArrayField(SimpleArrayField):
def clean(self, value):
if value and not self.to_python(value):
raise forms.ValidationError(f'Invalid list ({value}). '
f'Must be numeric and ranges must be in ascending order')
return super().clean(value)
def to_python(self, value):
if not value:
return []
if isinstance(value, str):
value = ','.join([str(n) for n in parse_numeric_range(value)])
return super().to_python(value)
class ClearableFileInput(forms.ClearableFileInput):
"""
Override Django's stock ClearableFileInput with a custom template.
"""
template_name = 'widgets/clearable_file_input.html'
class APISelect(forms.Select):
"""
@@ -144,7 +38,7 @@ class APISelect(forms.Select):
result.static_params = {}
return result
def _process_query_param(self, key: str, value: JSONPrimitive) -> None:
def _process_query_param(self, key, value) -> None:
"""
Based on query param value's type and value, update instance's dynamic/static params.
"""
@@ -187,7 +81,7 @@ class APISelect(forms.Select):
else:
self.static_params[key] = [value]
def _process_query_params(self, query_params: QueryParam) -> None:
def _process_query_params(self, query_params):
"""
Process an entire query_params dictionary, and handle primitive or list values.
"""
@@ -199,7 +93,7 @@ class APISelect(forms.Select):
else:
self._process_query_param(key, value)
def _serialize_params(self, key: str, params: ProcessedParams) -> None:
def _serialize_params(self, key, params):
"""
Serialize dynamic or static query params to JSON and add the serialized value to
the widget attributes by `key`.
@@ -214,7 +108,7 @@ class APISelect(forms.Select):
# attributes to HTML elements and parsed on the client.
self.attrs[key] = json.dumps([*current, *params], separators=(',', ':'))
def _add_dynamic_params(self) -> None:
def _add_dynamic_params(self):
"""
Convert post-processed dynamic query params to data structure expected by front-
end, serialize the value to JSON, and add it to the widget attributes.
@@ -227,7 +121,7 @@ class APISelect(forms.Select):
except IndexError as error:
raise RuntimeError(f"Missing required value for dynamic query param: '{self.dynamic_params}'") from error
def _add_static_params(self) -> None:
def _add_static_params(self):
"""
Convert post-processed static query params to data structure expected by front-
end, serialize the value to JSON, and add it to the widget attributes.
@@ -240,7 +134,7 @@ class APISelect(forms.Select):
except IndexError as error:
raise RuntimeError(f"Missing required value for static query param: '{self.static_params}'") from error
def add_query_params(self, query_params: QueryParam) -> None:
def add_query_params(self, query_params):
"""
Proccess & add a dictionary of URL query parameters to the widget attributes.
"""
@@ -251,7 +145,7 @@ class APISelect(forms.Select):
# Add processed static parameters to widget attributes.
self._add_static_params()
def add_query_param(self, key: str, value: QueryParamValue) -> None:
def add_query_param(self, key, value) -> None:
"""
Process & add a key/value pair of URL query parameters to the widget attributes.
"""
@@ -264,49 +158,3 @@ class APISelectMultiple(APISelect, forms.SelectMultiple):
super().__init__(*args, **kwargs)
self.attrs['data-multiple'] = 1
class DatePicker(forms.TextInput):
"""
Date picker using Flatpickr.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.attrs['class'] = 'date-picker'
self.attrs['placeholder'] = 'YYYY-MM-DD'
class DateTimePicker(forms.TextInput):
"""
DateTime picker using Flatpickr.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.attrs['class'] = 'datetime-picker'
self.attrs['placeholder'] = 'YYYY-MM-DD hh:mm:ss'
class TimePicker(forms.TextInput):
"""
Time picker using Flatpickr.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.attrs['class'] = 'time-picker'
self.attrs['placeholder'] = 'hh:mm:ss'
class HTMXSelect(forms.Select):
"""
Selection widget that will re-generate the HTML form upon the selection of a new option.
"""
def __init__(self, hx_url='.', hx_target_id='form_fields', attrs=None, **kwargs):
_attrs = {
'hx-get': hx_url,
'hx-include': f'#{hx_target_id}',
'hx-target': f'#{hx_target_id}',
}
if attrs:
_attrs.update(attrs)
super().__init__(attrs=_attrs, **kwargs)

View File

@@ -0,0 +1,37 @@
from django import forms
__all__ = (
'DatePicker',
'DateTimePicker',
'TimePicker',
)
class DatePicker(forms.TextInput):
"""
Date picker using Flatpickr.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.attrs['class'] = 'date-picker'
self.attrs['placeholder'] = 'YYYY-MM-DD'
class DateTimePicker(forms.TextInput):
"""
DateTime picker using Flatpickr.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.attrs['class'] = 'datetime-picker'
self.attrs['placeholder'] = 'YYYY-MM-DD hh:mm:ss'
class TimePicker(forms.TextInput):
"""
Time picker using Flatpickr.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.attrs['class'] = 'time-picker'
self.attrs['placeholder'] = 'hh:mm:ss'

View File

@@ -0,0 +1,28 @@
from django import forms
__all__ = (
'ClearableFileInput',
'MarkdownWidget',
'SlugWidget',
)
class ClearableFileInput(forms.ClearableFileInput):
"""
Override Django's stock ClearableFileInput with a custom template.
"""
template_name = 'widgets/clearable_file_input.html'
class MarkdownWidget(forms.Textarea):
"""
Provide a live preview for Markdown-formatted content.
"""
template_name = 'widgets/markdown_input.html'
class SlugWidget(forms.TextInput):
"""
Subclass TextInput and add a slug regeneration button next to the form field.
"""
template_name = 'widgets/sluginput.html'

View File

@@ -0,0 +1,79 @@
from django import forms
from utilities.choices import ColorChoices
from ..utils import add_blank_choice
__all__ = (
'BulkEditNullBooleanSelect',
'ColorSelect',
'HTMXSelect',
'SelectDurationWidget',
'SelectSpeedWidget',
'SelectWithPK',
)
class BulkEditNullBooleanSelect(forms.NullBooleanSelect):
"""
A Select widget for NullBooleanFields
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Override the built-in choice labels
self.choices = (
('1', '---------'),
('2', 'Yes'),
('3', 'No'),
)
self.attrs['class'] = 'netbox-static-select'
class ColorSelect(forms.Select):
"""
Extends the built-in Select widget to colorize each <option>.
"""
option_template_name = 'widgets/colorselect_option.html'
def __init__(self, *args, **kwargs):
kwargs['choices'] = add_blank_choice(ColorChoices)
super().__init__(*args, **kwargs)
self.attrs['class'] = 'netbox-color-select'
class HTMXSelect(forms.Select):
"""
Selection widget that will re-generate the HTML form upon the selection of a new option.
"""
def __init__(self, hx_url='.', hx_target_id='form_fields', attrs=None, **kwargs):
_attrs = {
'hx-get': hx_url,
'hx-include': f'#{hx_target_id}',
'hx-target': f'#{hx_target_id}',
}
if attrs:
_attrs.update(attrs)
super().__init__(attrs=_attrs, **kwargs)
class SelectWithPK(forms.Select):
"""
Include the primary key of each option in the option label (e.g. "Router7 (4721)").
"""
option_template_name = 'widgets/select_option_with_pk.html'
class SelectDurationWidget(forms.NumberInput):
"""
Dropdown to select one of several common options for a time duration (in minutes).
"""
template_name = 'widgets/select_duration.html'
class SelectSpeedWidget(forms.NumberInput):
"""
Speed field with dropdown selections for convenience.
"""
template_name = 'widgets/select_speed.html'

View File

@@ -1,20 +1,23 @@
import functools
import graphql
from django.core.exceptions import FieldDoesNotExist
from django.db.models import ForeignKey, Prefetch
from django.db.models import ForeignKey
from django.db.models.constants import LOOKUP_SEP
from django.db.models.fields.reverse_related import ManyToOneRel
from graphene import InputObjectType
from graphene.types.generic import GenericScalar
from graphene.types.resolver import default_resolver
from graphene_django import DjangoObjectType
from graphql import FieldNode, GraphQLObjectType, GraphQLResolveInfo, GraphQLSchema
from graphql import GraphQLResolveInfo, GraphQLSchema
from graphql.execution.execute import get_field_def
from graphql.language.ast import FragmentSpreadNode, InlineFragmentNode, VariableNode
from graphql.pyutils import Path
from graphql.type.definition import GraphQLInterfaceType, GraphQLUnionType
__all__ = (
'gql_query_optimizer',
)
def gql_query_optimizer(queryset, info, **options):
return QueryOptimizer(info).optimize(queryset)

View File

@@ -1,5 +1,10 @@
from urllib.parse import urlparse
__all__ = (
'is_embedded',
'is_htmx',
)
def is_htmx(request):
"""

View File

@@ -1,6 +1,10 @@
import markdown
from markdown.inlinepatterns import SimpleTagPattern
__all__ = (
'StrikethroughExtension',
)
STRIKE_RE = r'(~{2})(.+?)(~{2})'

View File

@@ -3,6 +3,10 @@ from timezone_field import TimeZoneField
from netbox.config import ConfigItem
__all__ = (
'custom_deconstruct',
)
SKIP_FIELDS = (
TimeZoneField,

View File

@@ -4,6 +4,11 @@ from mptt.querysets import TreeQuerySet as TreeQuerySet_
from django.db.models import Manager
from .querysets import RestrictedQuerySet
__all__ = (
'TreeManager',
'TreeQuerySet',
)
class TreeQuerySet(TreeQuerySet_, RestrictedQuerySet):
"""

View File

@@ -1,5 +1,10 @@
import re
__all__ = (
'naturalize',
'naturalize_interface',
)
INTERFACE_NAME_REGEX = r'(^(?P<type>[^\d\.:]+)?)' \
r'((?P<slot>\d+)/)?' \
r'((?P<subslot>\d+)/)?' \

View File

@@ -2,6 +2,12 @@ from django.core.paginator import Paginator, Page
from netbox.config import get_config
__all__ = (
'EnhancedPage',
'EnhancedPaginator',
'get_paginate_count',
)
class EnhancedPaginator(Paginator):
default_page_lengths = (

View File

@@ -1,5 +1,10 @@
from django.contrib.postgres.aggregates import JSONBAgg
from django.db.models import F, Func
from django.db.models import Func
__all__ = (
'CollateAsChar',
'EmptyGroupByJSONBAgg',
)
class CollateAsChar(Func):

View File

@@ -3,6 +3,11 @@ from django.db.models import Prefetch, QuerySet
from users.constants import CONSTRAINT_TOKEN_USER
from utilities.permissions import permission_is_exempt, qs_filter_from_constraints
__all__ = (
'RestrictedPrefetch',
'RestrictedQuerySet',
)
class RestrictedPrefetch(Prefetch):
"""

View File

@@ -1,3 +1,8 @@
__all__ = (
'linkify_phone',
)
def linkify_phone(value):
"""
Render a telephone number as a hyperlink.

View File

@@ -13,6 +13,21 @@ from netbox.config import get_config
from utilities.markdown import StrikethroughExtension
from utilities.utils import clean_html, foreground_color, title
__all__ = (
'bettertitle',
'content_type',
'content_type_id',
'fgcolor',
'linkify',
'meta',
'placeholder',
'render_json',
'render_markdown',
'render_yaml',
'split',
'tzoffset',
)
register = template.Library()

View File

@@ -1,5 +1,12 @@
from django import template
__all__ = (
'badge',
'checkmark',
'customfield_value',
'tag',
)
register = template.Library()

View File

@@ -5,6 +5,18 @@ from django.urls import NoReverseMatch, reverse
from extras.models import ExportTemplate
from utilities.utils import get_viewname, prepare_cloned_fields
__all__ = (
'add_button',
'bulk_delete_button',
'bulk_edit_button',
'clone_button',
'delete_button',
'edit_button',
'export_button',
'import_button',
'sync_button',
)
register = template.Library()

View File

@@ -1,5 +1,14 @@
from django import template
__all__ = (
'getfield',
'render_custom_fields',
'render_errors',
'render_field',
'render_form',
'widget_type',
)
register = template.Library()

View File

@@ -1,5 +1,4 @@
import datetime
import decimal
import json
from urllib.parse import quote
from typing import Dict, Any
@@ -15,6 +14,29 @@ from django.utils.safestring import mark_safe
from utilities.forms import get_selected_values, TableConfigForm
from utilities.utils import get_viewname
__all__ = (
'annotated_date',
'annotated_now',
'applied_filters',
'as_range',
'divide',
'get_item',
'get_key',
'humanize_megabytes',
'humanize_speed',
'icon_from_status',
'kg_to_pounds',
'meters_to_feet',
'percentage',
'querystring',
'startswith',
'status_from_tag',
'table_config_form',
'utilization_graph',
'validated_viewname',
'viewname',
)
register = template.Library()
@@ -83,19 +105,6 @@ def humanize_megabytes(mb):
return f'{mb} MB'
@register.filter()
def simplify_decimal(value):
"""
Return the simplest expression of a decimal value. Examples:
1.00 => '1'
1.20 => '1.2'
1.23 => '1.23'
"""
if type(value) is not decimal.Decimal:
return value
return str(value).rstrip('0').rstrip('.')
@register.filter(expects_localtime=True)
def annotated_date(date_value):
"""
@@ -145,14 +154,6 @@ def percentage(x, y):
return round(x / y * 100, 1)
@register.filter()
def has_perms(user, permissions_list):
"""
Return True if the user has *all* permissions in the list.
"""
return user.has_perms(permissions_list)
@register.filter()
def as_range(n):
"""

View File

@@ -4,6 +4,10 @@ from django.template import Context
from netbox.navigation.menu import MENUS
__all__ = (
'nav',
)
register = template.Library()

View File

@@ -1,5 +1,13 @@
from django import template
__all__ = (
'can_add',
'can_change',
'can_delete',
'can_sync',
'can_view',
)
register = template.Library()

View File

@@ -6,6 +6,10 @@ from django.utils.module_loading import import_string
from netbox.registry import registry
from utilities.utils import get_viewname
__all__ = (
'model_view_tabs',
)
register = template.Library()

View File

@@ -4,6 +4,10 @@ from django.views.generic import View
from netbox.registry import registry
__all__ = (
'get_model_urls',
)
def get_model_urls(app_label, model_name):
"""

View File

@@ -1,10 +1,24 @@
import re
from django.core.exceptions import ValidationError
from django.core.validators import _lazy_re_compile, BaseValidator, URLValidator
from django.core.validators import BaseValidator, RegexValidator, URLValidator, _lazy_re_compile
from netbox.config import get_config
__all__ = (
'ColorValidator',
'EnhancedURLValidator',
'ExclusionValidator',
'validate_regex',
)
ColorValidator = RegexValidator(
regex='^[0-9a-f]{6}$',
message='Enter a valid hexadecimal RGB color code.',
code='invalid'
)
class EnhancedURLValidator(URLValidator):
"""