mirror of
https://github.com/netbox-community/netbox.git
synced 2025-12-21 04:42:22 -06:00
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:
@@ -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=''):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
#
|
||||
|
||||
@@ -3,6 +3,7 @@ from rest_framework.exceptions import APIException
|
||||
|
||||
__all__ = (
|
||||
'AbortRequest',
|
||||
'AbortScript',
|
||||
'AbortTransaction',
|
||||
'PermissionsViolation',
|
||||
'RQWorkerNotRunningException',
|
||||
|
||||
@@ -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 ''"
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import hashlib
|
||||
|
||||
__all__ = (
|
||||
'sha256_hash',
|
||||
)
|
||||
|
||||
|
||||
def sha256_hash(filepath):
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from .constants import *
|
||||
from .fields import *
|
||||
from .forms import *
|
||||
from .mixins import *
|
||||
from .utils import *
|
||||
from .widgets import *
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from .array import *
|
||||
from .content_types import *
|
||||
from .csv import *
|
||||
from .dynamic import *
|
||||
|
||||
24
netbox/utilities/forms/fields/array.py
Normal file
24
netbox/utilities/forms/fields/array.py
Normal 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)
|
||||
@@ -1,6 +1,5 @@
|
||||
from django import forms
|
||||
|
||||
from utilities.forms import widgets
|
||||
from utilities.utils import content_type_name
|
||||
|
||||
__all__ = (
|
||||
|
||||
@@ -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__ = (
|
||||
|
||||
@@ -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):
|
||||
|
||||
62
netbox/utilities/forms/mixins.py
Normal file
62
netbox/utilities/forms/mixins.py
Normal 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
|
||||
4
netbox/utilities/forms/widgets/__init__.py
Normal file
4
netbox/utilities/forms/widgets/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .apiselect import *
|
||||
from .datetime import *
|
||||
from .misc import *
|
||||
from .select import *
|
||||
@@ -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)
|
||||
37
netbox/utilities/forms/widgets/datetime.py
Normal file
37
netbox/utilities/forms/widgets/datetime.py
Normal 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'
|
||||
28
netbox/utilities/forms/widgets/misc.py
Normal file
28
netbox/utilities/forms/widgets/misc.py
Normal 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'
|
||||
79
netbox/utilities/forms/widgets/select.py
Normal file
79
netbox/utilities/forms/widgets/select.py
Normal 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'
|
||||
@@ -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)
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
from urllib.parse import urlparse
|
||||
|
||||
__all__ = (
|
||||
'is_embedded',
|
||||
'is_htmx',
|
||||
)
|
||||
|
||||
|
||||
def is_htmx(request):
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import markdown
|
||||
from markdown.inlinepatterns import SimpleTagPattern
|
||||
|
||||
__all__ = (
|
||||
'StrikethroughExtension',
|
||||
)
|
||||
|
||||
STRIKE_RE = r'(~{2})(.+?)(~{2})'
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@ from timezone_field import TimeZoneField
|
||||
|
||||
from netbox.config import ConfigItem
|
||||
|
||||
__all__ = (
|
||||
'custom_deconstruct',
|
||||
)
|
||||
|
||||
|
||||
SKIP_FIELDS = (
|
||||
TimeZoneField,
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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+)/)?' \
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
__all__ = (
|
||||
'linkify_phone',
|
||||
)
|
||||
|
||||
|
||||
def linkify_phone(value):
|
||||
"""
|
||||
Render a telephone number as a hyperlink.
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
from django import template
|
||||
|
||||
__all__ = (
|
||||
'badge',
|
||||
'checkmark',
|
||||
'customfield_value',
|
||||
'tag',
|
||||
)
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
from django import template
|
||||
|
||||
__all__ = (
|
||||
'getfield',
|
||||
'render_custom_fields',
|
||||
'render_errors',
|
||||
'render_field',
|
||||
'render_form',
|
||||
'widget_type',
|
||||
)
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -4,6 +4,10 @@ from django.template import Context
|
||||
|
||||
from netbox.navigation.menu import MENUS
|
||||
|
||||
__all__ = (
|
||||
'nav',
|
||||
)
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
from django import template
|
||||
|
||||
__all__ = (
|
||||
'can_add',
|
||||
'can_change',
|
||||
'can_delete',
|
||||
'can_sync',
|
||||
'can_view',
|
||||
)
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user