mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-01 21:36:25 -06:00
Maintain current query_params
API for form fields, transform data structure in widget
This commit is contained in:
parent
809de8683b
commit
658ac89af0
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -1,13 +1,14 @@
|
|||||||
import queryString from 'query-string';
|
|
||||||
import debounce from 'just-debounce-it';
|
|
||||||
import { readableColor } from 'color2k';
|
import { readableColor } from 'color2k';
|
||||||
|
import debounce from 'just-debounce-it';
|
||||||
|
import queryString from 'query-string';
|
||||||
import SlimSelect from 'slim-select';
|
import SlimSelect from 'slim-select';
|
||||||
import { createToast } from '../../bs';
|
import { createToast } from '../../bs';
|
||||||
import { hasUrl, hasExclusions, isTrigger } from '../util';
|
import { hasUrl, hasExclusions, isTrigger } from '../util';
|
||||||
import { FilterFieldMap } from './filterFields';
|
import { DynamicParamsMap } from './dynamicParams';
|
||||||
|
import { isStaticParams } from './types';
|
||||||
import {
|
import {
|
||||||
isTruthy,
|
|
||||||
hasMore,
|
hasMore,
|
||||||
|
isTruthy,
|
||||||
hasError,
|
hasError,
|
||||||
getElement,
|
getElement,
|
||||||
getApiData,
|
getApiData,
|
||||||
@ -90,8 +91,6 @@ export class APISelect {
|
|||||||
* Form Field Names → Object containing:
|
* Form Field Names → Object containing:
|
||||||
* - Query parameter key name
|
* - Query parameter key name
|
||||||
* - Query value
|
* - Query value
|
||||||
* - Other options such as a default value, and the option to include
|
|
||||||
* null values.
|
|
||||||
*
|
*
|
||||||
* This is different from `queryParams` in that it tracks all _possible_ related fields and their
|
* This is different from `queryParams` in that it tracks all _possible_ related fields and their
|
||||||
* values, even if they are empty. Further, the keys in `queryParams` correspond to the actual
|
* values, even if they are empty. Further, the keys in `queryParams` correspond to the actual
|
||||||
@ -99,7 +98,12 @@ export class APISelect {
|
|||||||
* the model. For example, `tenant_group` would be the field name, but `group_id` would be the
|
* the model. For example, `tenant_group` would be the field name, but `group_id` would be the
|
||||||
* query parameter.
|
* query parameter.
|
||||||
*/
|
*/
|
||||||
private readonly filterFields: FilterFieldMap = new FilterFieldMap();
|
private readonly dynamicParams: DynamicParamsMap = new DynamicParamsMap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API query parameters that are already known by the server and should not change.
|
||||||
|
*/
|
||||||
|
private readonly staticParams: QueryFilter = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapping of URL template key/value pairs. If this element's URL contains Django template tags
|
* Mapping of URL template key/value pairs. If this element's URL contains Django template tags
|
||||||
@ -187,30 +191,21 @@ export class APISelect {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Initialize API query properties.
|
// Initialize API query properties.
|
||||||
// this.getFilteredBy();
|
this.getStaticParams();
|
||||||
this.getFilterFields();
|
this.getDynamicParams();
|
||||||
this.getPathKeys();
|
this.getPathKeys();
|
||||||
|
|
||||||
// for (const filter of this.filterParams.keys()) {
|
// Populate static query parameters.
|
||||||
// this.updateQueryParams(filter);
|
for (const [key, value] of this.staticParams.entries()) {
|
||||||
// }
|
this.queryParams.set(key, value);
|
||||||
for (const filter of this.filterFields.keys()) {
|
}
|
||||||
|
|
||||||
|
// Populate dynamic query parameters with any form values that are already known.
|
||||||
|
for (const filter of this.dynamicParams.keys()) {
|
||||||
this.updateQueryParams(filter);
|
this.updateQueryParams(filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add any already-resolved key/value pairs to the API query parameters.
|
// Populate dynamic path values with any form values that are already known.
|
||||||
// for (const [key, value] of this.filterParams.entries()) {
|
|
||||||
// if (isTruthy(value)) {
|
|
||||||
// this.queryParams.set(key, value);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
for (const value of this.filterFields.values()) {
|
|
||||||
const { queryParam, queryValue } = value;
|
|
||||||
if (isTruthy(queryValue)) {
|
|
||||||
this.queryParams.set(queryParam, queryValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const filter of this.pathValues.keys()) {
|
for (const filter of this.pathValues.keys()) {
|
||||||
this.updatePathValues(filter);
|
this.updatePathValues(filter);
|
||||||
}
|
}
|
||||||
@ -365,7 +360,7 @@ export class APISelect {
|
|||||||
// Create a unique iterator of all possible form fields which, when changed, should cause this
|
// Create a unique iterator of all possible form fields which, when changed, should cause this
|
||||||
// element to update its API query.
|
// element to update its API query.
|
||||||
// const dependencies = new Set([...this.filterParams.keys(), ...this.pathValues.keys()]);
|
// const dependencies = new Set([...this.filterParams.keys(), ...this.pathValues.keys()]);
|
||||||
const dependencies = new Set([...this.filterFields.keys(), ...this.pathValues.keys()]);
|
const dependencies = new Set([...this.dynamicParams.keys(), ...this.pathValues.keys()]);
|
||||||
|
|
||||||
for (const dep of dependencies) {
|
for (const dep of dependencies) {
|
||||||
const filterElement = document.querySelector(`[name="${dep}"]`);
|
const filterElement = document.querySelector(`[name="${dep}"]`);
|
||||||
@ -559,11 +554,6 @@ export class APISelect {
|
|||||||
this.updatePathValues(target.name);
|
this.updatePathValues(target.name);
|
||||||
this.updateQueryUrl();
|
this.updateQueryUrl();
|
||||||
|
|
||||||
console.group(this.name, this.queryUrl);
|
|
||||||
console.log(this.filterFields);
|
|
||||||
console.log(this.queryParams);
|
|
||||||
console.groupEnd();
|
|
||||||
|
|
||||||
// Load new data.
|
// Load new data.
|
||||||
Promise.all([this.loadData()]);
|
Promise.all([this.loadData()]);
|
||||||
}
|
}
|
||||||
@ -655,18 +645,27 @@ export class APISelect {
|
|||||||
|
|
||||||
if (elementValue.length > 0) {
|
if (elementValue.length > 0) {
|
||||||
// If the field has a value, add it to the map.
|
// If the field has a value, add it to the map.
|
||||||
this.filterFields.updateValue(fieldName, elementValue);
|
this.dynamicParams.updateValue(fieldName, elementValue);
|
||||||
|
// Get the updated value.
|
||||||
const current = this.filterFields.get(fieldName);
|
const current = this.dynamicParams.get(fieldName);
|
||||||
|
|
||||||
if (typeof current !== 'undefined') {
|
if (typeof current !== 'undefined') {
|
||||||
const { queryParam, queryValue, includeNull } = current;
|
const { queryParam, queryValue } = current;
|
||||||
let value = [] as Stringifiable[];
|
let value = [] as Stringifiable[];
|
||||||
if (includeNull) {
|
|
||||||
value = [...value, null];
|
if (this.staticParams.has(queryParam)) {
|
||||||
|
// If the field is defined in `staticParams`, we should merge the dynamic value with
|
||||||
|
// the static value.
|
||||||
|
const staticValue = this.staticParams.get(queryParam);
|
||||||
|
if (typeof staticValue !== 'undefined') {
|
||||||
|
value = [...staticValue, ...queryValue];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the field is _not_ defined in `staticParams`, we should replace the current value
|
||||||
|
// with the new dynamic value.
|
||||||
|
value = queryValue;
|
||||||
}
|
}
|
||||||
if (queryValue.length > 0) {
|
if (value.length > 0) {
|
||||||
value = [...value, ...queryValue];
|
|
||||||
this.queryParams.set(queryParam, value);
|
this.queryParams.set(queryParam, value);
|
||||||
} else {
|
} else {
|
||||||
this.queryParams.delete(queryParam);
|
this.queryParams.delete(queryParam);
|
||||||
@ -674,7 +673,7 @@ export class APISelect {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, delete it (we don't want to send an empty query like `?site_id=`)
|
// Otherwise, delete it (we don't want to send an empty query like `?site_id=`)
|
||||||
const queryParam = this.filterFields.queryParam(fieldName);
|
const queryParam = this.dynamicParams.queryParam(fieldName);
|
||||||
if (queryParam !== null) {
|
if (queryParam !== null) {
|
||||||
this.queryParams.delete(queryParam);
|
this.queryParams.delete(queryParam);
|
||||||
}
|
}
|
||||||
@ -773,17 +772,48 @@ export class APISelect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if a select element should be filtered by the value of another select element.
|
* Determine if a this instances' options should be filtered by the value of another select
|
||||||
|
* element.
|
||||||
*
|
*
|
||||||
* Looks for the DOM attribute `data-filter-fields`, the value of which is a JSON array of
|
* Looks for the DOM attribute `data-dynamic-params`, the value of which is a JSON array of
|
||||||
* objects containing information about how to handle the related field.
|
* objects containing information about how to handle the related field.
|
||||||
*/
|
*/
|
||||||
private getFilterFields(): void {
|
private getDynamicParams(): void {
|
||||||
const serialized = this.base.getAttribute('data-filter-fields');
|
const serialized = this.base.getAttribute('data-dynamic-params');
|
||||||
try {
|
try {
|
||||||
this.filterFields.addFromJson(serialized);
|
this.dynamicParams.addFromJson(serialized);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.group(`Unable to determine filter fields for select field '${this.name}'`);
|
console.group(`Unable to determine dynamic query parameters for select field '${this.name}'`);
|
||||||
|
console.warn(err);
|
||||||
|
console.groupEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if this instance's options should be filtered by static values passed from the
|
||||||
|
* server.
|
||||||
|
*
|
||||||
|
* Looks for the DOM attribute `data-static-params`, the value of which is a JSON array of
|
||||||
|
* objects containing key/value pairs to add to `this.staticParams`.
|
||||||
|
*/
|
||||||
|
private getStaticParams(): void {
|
||||||
|
const serialized = this.base.getAttribute('data-static-params');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isTruthy(serialized)) {
|
||||||
|
const deserialized = JSON.parse(serialized);
|
||||||
|
if (isStaticParams(deserialized)) {
|
||||||
|
for (const { queryParam, queryValue } of deserialized) {
|
||||||
|
if (Array.isArray(queryValue)) {
|
||||||
|
this.staticParams.set(queryParam, queryValue);
|
||||||
|
} else {
|
||||||
|
this.staticParams.set(queryParam, [queryValue]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.group(`Unable to determine static query parameters for select field '${this.name}'`);
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
import { isTruthy } from '../../util';
|
import { isTruthy } from '../../util';
|
||||||
import { isDataFilterFields } from './types';
|
import { isDataDynamicParams } from './types';
|
||||||
|
|
||||||
import type { Stringifiable } from 'query-string';
|
import type { QueryParam } from './types';
|
||||||
import type { FilterFieldValue } from './types';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extension of built-in `Map` to add convenience functions.
|
* Extension of built-in `Map` to add convenience functions.
|
||||||
*/
|
*/
|
||||||
export class FilterFieldMap extends Map<string, FilterFieldValue> {
|
export class DynamicParamsMap extends Map<string, QueryParam> {
|
||||||
/**
|
/**
|
||||||
* Get the query parameter key based on field name.
|
* Get the query parameter key based on field name.
|
||||||
*
|
*
|
||||||
* @param fieldName Related field name.
|
* @param fieldName Related field name.
|
||||||
* @returns `queryParam` key.
|
* @returns `queryParam` key.
|
||||||
*/
|
*/
|
||||||
public queryParam(fieldName: string): Nullable<FilterFieldValue['queryParam']> {
|
public queryParam(fieldName: string): Nullable<QueryParam['queryParam']> {
|
||||||
const value = this.get(fieldName);
|
const value = this.get(fieldName);
|
||||||
if (typeof value !== 'undefined') {
|
if (typeof value !== 'undefined') {
|
||||||
return value.queryParam;
|
return value.queryParam;
|
||||||
@ -28,7 +27,7 @@ export class FilterFieldMap extends Map<string, FilterFieldValue> {
|
|||||||
* @param fieldName Related field name.
|
* @param fieldName Related field name.
|
||||||
* @returns `queryValue` value, or an empty array if there is no corresponding Map entry.
|
* @returns `queryValue` value, or an empty array if there is no corresponding Map entry.
|
||||||
*/
|
*/
|
||||||
public queryValue(fieldName: string): FilterFieldValue['queryValue'] {
|
public queryValue(fieldName: string): QueryParam['queryValue'] {
|
||||||
const value = this.get(fieldName);
|
const value = this.get(fieldName);
|
||||||
if (typeof value !== 'undefined') {
|
if (typeof value !== 'undefined') {
|
||||||
return value.queryValue;
|
return value.queryValue;
|
||||||
@ -43,43 +42,33 @@ export class FilterFieldMap extends Map<string, FilterFieldValue> {
|
|||||||
* @param queryValue New value.
|
* @param queryValue New value.
|
||||||
* @returns `true` if the update was successful, `false` if there was no corresponding Map entry.
|
* @returns `true` if the update was successful, `false` if there was no corresponding Map entry.
|
||||||
*/
|
*/
|
||||||
public updateValue(fieldName: string, queryValue: FilterFieldValue['queryValue']): boolean {
|
public updateValue(fieldName: string, queryValue: QueryParam['queryValue']): boolean {
|
||||||
const current = this.get(fieldName);
|
const current = this.get(fieldName);
|
||||||
if (isTruthy(current)) {
|
if (isTruthy(current)) {
|
||||||
const { queryParam, includeNull } = current;
|
const { queryParam } = current;
|
||||||
this.set(fieldName, { queryParam, queryValue, includeNull });
|
this.set(fieldName, { queryParam, queryValue });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populate the underlying map based on the JSON passed in the `data-filter-fields` attribute.
|
* Populate the underlying map based on the JSON passed in the `data-dynamic-params` attribute.
|
||||||
*
|
*
|
||||||
* @param json Raw JSON string from `data-filter-fields` attribute.
|
* @param json Raw JSON string from `data-dynamic-params` attribute.
|
||||||
*/
|
*/
|
||||||
public addFromJson(json: string | null | undefined): void {
|
public addFromJson(json: string | null | undefined): void {
|
||||||
if (isTruthy(json)) {
|
if (isTruthy(json)) {
|
||||||
const deserialized = JSON.parse(json);
|
const deserialized = JSON.parse(json);
|
||||||
// Ensure the value is the data structure we expect.
|
// Ensure the value is the data structure we expect.
|
||||||
if (isDataFilterFields(deserialized)) {
|
if (isDataDynamicParams(deserialized)) {
|
||||||
for (const { queryParam, fieldName, defaultValue, includeNull } of deserialized) {
|
for (const { queryParam, fieldName } of deserialized) {
|
||||||
let queryValue = [] as Stringifiable[];
|
|
||||||
if (isTruthy(defaultValue)) {
|
|
||||||
// Add the default value, if it exists.
|
|
||||||
if (Array.isArray(defaultValue)) {
|
|
||||||
// If the default value is an array, add all elements to the value.
|
|
||||||
queryValue = [...queryValue, ...defaultValue];
|
|
||||||
} else {
|
|
||||||
queryValue = [defaultValue];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Populate the underlying map with the initial data.
|
// Populate the underlying map with the initial data.
|
||||||
this.set(fieldName, { queryParam, queryValue, includeNull });
|
this.set(fieldName, { queryParam, queryValue: [] });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Data from 'data-filter-fields' attribute is improperly formatted: '${json}'`,
|
`Data from 'data-dynamic-params' attribute is improperly formatted: '${json}'`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -27,6 +27,40 @@ export type FilterFieldValue = {
|
|||||||
includeNull: boolean;
|
includeNull: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON data structure from `data-dynamic-params` attribute.
|
||||||
|
*/
|
||||||
|
export type DataDynamicParam = {
|
||||||
|
/**
|
||||||
|
* Name of form field to track.
|
||||||
|
*
|
||||||
|
* @example [name="tenant_group"]
|
||||||
|
*/
|
||||||
|
fieldName: string;
|
||||||
|
/**
|
||||||
|
* Query param key.
|
||||||
|
*
|
||||||
|
* @example group_id
|
||||||
|
*/
|
||||||
|
queryParam: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `queryParams` Map value.
|
||||||
|
*/
|
||||||
|
export type QueryParam = {
|
||||||
|
queryParam: string;
|
||||||
|
queryValue: Stringifiable[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON data structure from `data-static-params` attribute.
|
||||||
|
*/
|
||||||
|
export type DataStaticParam = {
|
||||||
|
queryParam: string;
|
||||||
|
queryValue: Stringifiable | Stringifiable[];
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON data passed from Django on the `data-filter-fields` attribute.
|
* JSON data passed from Django on the `data-filter-fields` attribute.
|
||||||
*/
|
*/
|
||||||
@ -109,3 +143,47 @@ export function isDataFilterFields(value: unknown): value is DataFilterFields[]
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strict Type Guard to determine if a deserialized value from the `data-dynamic-params` attribute
|
||||||
|
* is of type `DataDynamicParam[]`.
|
||||||
|
*
|
||||||
|
* @param value Deserialized value from `data-dynamic-params` attribute.
|
||||||
|
*/
|
||||||
|
export function isDataDynamicParams(value: unknown): value is DataDynamicParam[] {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const item of value) {
|
||||||
|
if (typeof item === 'object' && item !== null) {
|
||||||
|
if ('fieldName' in item && 'queryParam' in item) {
|
||||||
|
return (
|
||||||
|
typeof (item as DataDynamicParam).fieldName === 'string' &&
|
||||||
|
typeof (item as DataDynamicParam).queryParam === 'string'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strict Type Guard to determine if a deserialized value from the `data-static-params` attribute
|
||||||
|
* is of type `DataStaticParam[]`.
|
||||||
|
*
|
||||||
|
* @param value Deserialized value from `data-static-params` attribute.
|
||||||
|
*/
|
||||||
|
export function isStaticParams(value: unknown): value is DataStaticParam[] {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const item of value) {
|
||||||
|
if (typeof item === 'object' && item !== null) {
|
||||||
|
if ('queryParam' in item && 'queryValue' in item) {
|
||||||
|
return (
|
||||||
|
typeof (item as DataStaticParam).queryParam === 'string' &&
|
||||||
|
typeof (item as DataStaticParam).queryValue !== 'undefined'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
from typing import Dict, Sequence, Union
|
from typing import Dict, Sequence, List, Tuple, Union
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -28,6 +28,9 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
JSONPrimitive = Union[str, bool, int, float, None]
|
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):
|
class SmallTextarea(forms.Textarea):
|
||||||
@ -138,88 +141,132 @@ class APISelect(SelectWithDisabled):
|
|||||||
|
|
||||||
:param api_url: API endpoint URL. Required if not set automatically by the parent field.
|
: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):
|
def __init__(self, api_url=None, full=False, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.attrs['class'] = 'netbox-api-select'
|
self.attrs['class'] = 'netbox-api-select'
|
||||||
|
self.dynamic_params: Dict[str, List[str]] = {}
|
||||||
|
self.static_params: Dict[str, List[str]] = {}
|
||||||
|
|
||||||
if api_url:
|
if api_url:
|
||||||
self.attrs['data-url'] = '/{}{}'.format(settings.BASE_PATH, api_url.lstrip('/')) # Inject BASE_PATH
|
self.attrs['data-url'] = '/{}{}'.format(settings.BASE_PATH, api_url.lstrip('/')) # Inject BASE_PATH
|
||||||
|
|
||||||
def add_query_param(self, key: str, value: JSONPrimitive) -> None:
|
def _process_query_param(self, key: str, value: JSONPrimitive) -> None:
|
||||||
"""
|
"""
|
||||||
Add a query parameter with a static value to the API request.
|
Based on query param value's type and value, update instance's dynamic/static params.
|
||||||
"""
|
"""
|
||||||
self.add_filter_fields({'accessor': key, 'field_name': key, 'default_value': value})
|
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'
|
||||||
|
|
||||||
def add_filter_fields(self, filter_fields: Union[Dict[str, JSONPrimitive], Sequence[Dict[str, JSONPrimitive]]]) -> None:
|
# 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:
|
||||||
|
# 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]
|
||||||
|
|
||||||
|
def _process_query_params(self, query_params: QueryParam) -> None:
|
||||||
"""
|
"""
|
||||||
Add details about another form field, the value for which should
|
Process an entire query_params dictionary, and handle primitive or list values.
|
||||||
be added to this APISelect's URL query parameters.
|
"""
|
||||||
|
for key, value in query_params.items():
|
||||||
:Example:
|
if isinstance(value, (List, Tuple)):
|
||||||
|
# If value is a list/tuple, iterate through each item.
|
||||||
```python
|
for item in value:
|
||||||
{
|
self._process_query_param(key, item)
|
||||||
'field_name': 'tenant_group',
|
else:
|
||||||
'accessor': 'tenant',
|
self._process_query_param(key, value)
|
||||||
'default_value': 1,
|
|
||||||
'include_null': False,
|
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`.
|
||||||
:param filter_fields: Dict or list of dicts with the following properties:
|
|
||||||
|
|
||||||
- accessor: The related field's property name. For example, on the
|
|
||||||
`Tenant`model, a related model might be `TenantGroup`. In
|
|
||||||
this case, `accessor` would be `group_id`.
|
|
||||||
|
|
||||||
- field_name: The related field's form name. In the above `Tenant`
|
|
||||||
example, `field_name` would be `tenant_group`.
|
|
||||||
|
|
||||||
- default_value: (Optional) Set a default initial value, which can be
|
|
||||||
overridden if the field changes.
|
|
||||||
|
|
||||||
- include_null: (Optional) Include `null` on queries for the related
|
|
||||||
field. For example, if `True`, `?<fieldName>=null` will
|
|
||||||
be added to all API queries for this field.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
key = 'data-filter-fields'
|
|
||||||
# Deserialize the current serialized value from the widget, using an empty JSON
|
# Deserialize the current serialized value from the widget, using an empty JSON
|
||||||
# array as a fallback in the event one is not defined.
|
# array as a fallback in the event one is not defined.
|
||||||
current = json.loads(self.attrs.get(key, '[]'))
|
current = json.loads(self.attrs.get(key, '[]'))
|
||||||
|
|
||||||
# Create a new list of filter fields using camelCse to align with front-end code standards
|
|
||||||
# (this value will be read and used heavily at the JavaScript layer).
|
|
||||||
update: Sequence[Dict[str, str]] = []
|
|
||||||
try:
|
|
||||||
if isinstance(filter_fields, Sequence):
|
|
||||||
update = [
|
|
||||||
{
|
|
||||||
'fieldName': field['field_name'],
|
|
||||||
'queryParam': field['accessor'],
|
|
||||||
'defaultValue': field.get('default_value'),
|
|
||||||
'includeNull': field.get('include_null', False),
|
|
||||||
} for field in filter_fields
|
|
||||||
]
|
|
||||||
elif isinstance(filter_fields, Dict):
|
|
||||||
update = [
|
|
||||||
{
|
|
||||||
'fieldName': filter_fields['field_name'],
|
|
||||||
'queryParam': filter_fields['accessor'],
|
|
||||||
'defaultValue': filter_fields.get('default_value'),
|
|
||||||
'includeNull': filter_fields.get('include_null', False),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
except KeyError as error:
|
|
||||||
raise KeyError(f"Missing required property '{error.args[0]}' on APISelect.filter_fields") from error
|
|
||||||
|
|
||||||
# Combine the current values with the updated values and serialize the result as
|
# Combine the current values with the updated values and serialize the result as
|
||||||
# JSON. Note: the `separators` kwarg effectively removes extra whitespace from
|
# JSON. Note: the `separators` kwarg effectively removes extra whitespace from
|
||||||
# the serialized JSON string, which is ideal since these will be passed as
|
# the serialized JSON string, which is ideal since these will be passed as
|
||||||
# attributes to HTML elements and parsed on the client.
|
# attributes to HTML elements and parsed on the client.
|
||||||
self.attrs[key] = json.dumps([*current, *update], separators=(',', ':'))
|
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):
|
class APISelectMultiple(APISelect, forms.SelectMultiple):
|
||||||
|
Loading…
Reference in New Issue
Block a user