mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Improve APISelect query parameter handling (#7040)
* Fixes #7035: Refactor APISelect query_param logic * Add filter_fields to extras.ObjectVar & fix default value handling * Update ObjectVar docs to reflect new filter_fields attribute * Revert changes from89b7f3f
* Maintain current `query_params` API for form fields, transform data structure in widget * Revert changes fromd0208d4
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
import json
|
||||
from typing import Dict, Sequence, List, Tuple, Union
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
@ -26,6 +27,11 @@ __all__ = (
|
||||
'TimePicker',
|
||||
)
|
||||
|
||||
JSONPrimitive = Union[str, bool, int, float, None]
|
||||
QueryParamValue = Union[JSONPrimitive, Sequence[JSONPrimitive]]
|
||||
QueryParam = Dict[str, QueryParamValue]
|
||||
ProcessedParams = Sequence[Dict[str, Sequence[JSONPrimitive]]]
|
||||
|
||||
|
||||
class SmallTextarea(forms.Textarea):
|
||||
"""
|
||||
@ -135,29 +141,132 @@ class APISelect(SelectWithDisabled):
|
||||
|
||||
:param api_url: API endpoint URL. Required if not set automatically by the parent field.
|
||||
"""
|
||||
|
||||
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 add_query_param(self, name, value):
|
||||
def _process_query_param(self, key: str, value: JSONPrimitive) -> None:
|
||||
"""
|
||||
Add details for an additional query param in the form of a data-* JSON-encoded list attribute.
|
||||
|
||||
:param name: The name of the query param
|
||||
:param value: The value of the query param
|
||||
Based on query param value's type and value, update instance's dynamic/static params.
|
||||
"""
|
||||
key = f'data-query-param-{name}'
|
||||
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'
|
||||
|
||||
values = json.loads(self.attrs.get(key, '[]'))
|
||||
if type(value) in (list, tuple):
|
||||
values.extend([str(v) for v in value])
|
||||
# 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] = [*current, value]
|
||||
else:
|
||||
self.static_params[key] = [value]
|
||||
else:
|
||||
values.append(str(value))
|
||||
# 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] = [*current, value]
|
||||
else:
|
||||
self.static_params[key] = [value]
|
||||
|
||||
self.attrs[key] = json.dumps(values)
|
||||
def _process_query_params(self, query_params: QueryParam) -> None:
|
||||
"""
|
||||
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: str, params: ProcessedParams) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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: QueryParam) -> None:
|
||||
"""
|
||||
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: str, value: QueryParamValue) -> 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):
|
||||
|
Reference in New Issue
Block a user