1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00
Jeremy Stretch d470848b29 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
2023-04-14 10:33:53 -04:00

161 lines
6.6 KiB
Python

import json
from typing import Dict, List, Tuple
from django import forms
from django.conf import settings
__all__ = (
'APISelect',
'APISelectMultiple',
)
class APISelect(forms.Select):
"""
A select widget populated via an API call
:param api_url: API endpoint URL. Required if not set automatically by the parent field.
"""
template_name = 'widgets/apiselect.html'
option_template_name = 'widgets/select_option.html'
dynamic_params: Dict[str, str]
static_params: Dict[str, List[str]]
def __init__(self, api_url=None, full=False, *args, **kwargs):
super().__init__(*args, **kwargs)
self.attrs['class'] = 'netbox-api-select'
self.dynamic_params: Dict[str, List[str]] = {}
self.static_params: Dict[str, List[str]] = {}
if api_url:
self.attrs['data-url'] = '/{}{}'.format(settings.BASE_PATH, api_url.lstrip('/')) # Inject BASE_PATH
def __deepcopy__(self, memo):
"""Reset `static_params` and `dynamic_params` when APISelect is deepcopied."""
result = super().__deepcopy__(memo)
result.dynamic_params = {}
result.static_params = {}
return result
def _process_query_param(self, key, value) -> None:
"""
Based on query param value's type and value, update instance's dynamic/static params.
"""
if isinstance(value, str):
# Coerce `True` boolean.
if value.lower() == 'true':
value = True
# Coerce `False` boolean.
elif value.lower() == 'false':
value = False
# Query parameters cannot have a `None` (or `null` in JSON) type, convert
# `None` types to `'null'` so that ?key=null is used in the query URL.
elif value is None:
value = 'null'
# Check type of `value` again, since it may have changed.
if isinstance(value, str):
if value.startswith('$'):
# A value starting with `$` indicates a dynamic query param, where the
# initial value is unknown and will be updated at the JavaScript layer
# as the related form field's value changes.
field_name = value.strip('$')
self.dynamic_params[field_name] = key
else:
# A value _not_ starting with `$` indicates a static query param, where
# the value is already known and should not be changed at the JavaScript
# layer.
if key in self.static_params:
current = self.static_params[key]
self.static_params[key] = [v for v in set([*current, value])]
else:
self.static_params[key] = [value]
else:
# Any non-string values are passed through as static query params, since
# dynamic query param values have to be a string (in order to start with
# `$`).
if key in self.static_params:
current = self.static_params[key]
self.static_params[key] = [v for v in set([*current, value])]
else:
self.static_params[key] = [value]
def _process_query_params(self, query_params):
"""
Process an entire query_params dictionary, and handle primitive or list values.
"""
for key, value in query_params.items():
if isinstance(value, (List, Tuple)):
# If value is a list/tuple, iterate through each item.
for item in value:
self._process_query_param(key, item)
else:
self._process_query_param(key, value)
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`.
"""
# Deserialize the current serialized value from the widget, using an empty JSON
# array as a fallback in the event one is not defined.
current = json.loads(self.attrs.get(key, '[]'))
# Combine the current values with the updated values and serialize the result as
# JSON. Note: the `separators` kwarg effectively removes extra whitespace from
# the serialized JSON string, which is ideal since these will be passed as
# attributes to HTML elements and parsed on the client.
self.attrs[key] = json.dumps([*current, *params], separators=(',', ':'))
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.
"""
key = 'data-dynamic-params'
if len(self.dynamic_params) > 0:
try:
update = [{'fieldName': f, 'queryParam': q} for (f, q) in self.dynamic_params.items()]
self._serialize_params(key, update)
except IndexError as error:
raise RuntimeError(f"Missing required value for dynamic query param: '{self.dynamic_params}'") from error
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.
"""
key = 'data-static-params'
if len(self.static_params) > 0:
try:
update = [{'queryParam': k, 'queryValue': v} for (k, v) in self.static_params.items()]
self._serialize_params(key, update)
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):
"""
Proccess & add a dictionary of URL query parameters to the widget attributes.
"""
# Process query parameters. This populates `self.dynamic_params` and `self.static_params`.
self._process_query_params(query_params)
# Add processed dynamic parameters to widget attributes.
self._add_dynamic_params()
# Add processed static parameters to widget attributes.
self._add_static_params()
def add_query_param(self, key, value) -> None:
"""
Process & add a key/value pair of URL query parameters to the widget attributes.
"""
self.add_query_params({key: value})
class APISelectMultiple(APISelect, forms.SelectMultiple):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.attrs['data-multiple'] = 1