Revert "Form improvements (#5837)" (#5913)

This reverts commit 0d193d8cff.
This commit is contained in:
Oliver 2023-11-14 14:49:45 +11:00 committed by GitHub
parent 2fcd6ae0b9
commit 247ad4dcf9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 36 additions and 354 deletions

View File

@ -10,7 +10,6 @@ from rest_framework.utils import model_meta
import InvenTree.permissions
import users.models
from InvenTree.helpers import str2bool
from InvenTree.serializers import DependentField
logger = logging.getLogger('inventree')
@ -243,10 +242,6 @@ class InvenTreeMetadata(SimpleMetadata):
We take the regular DRF metadata and add our own unique flavor
"""
# Try to add the child property to the dependent field to be used by the super call
if self.label_lookup[field] == 'dependent field':
field.get_child(raise_exception=True)
# Run super method first
field_info = super().get_field_info(field)
@ -280,11 +275,4 @@ class InvenTreeMetadata(SimpleMetadata):
else:
field_info['api_url'] = model.get_api_url()
# Add more metadata about dependent fields
if field_info['type'] == 'dependent field':
field_info['depends_on'] = field.depends_on
return field_info
InvenTreeMetadata.label_lookup[DependentField] = "dependent field"

View File

@ -2,7 +2,6 @@
import os
from collections import OrderedDict
from copy import deepcopy
from decimal import Decimal
from django.conf import settings
@ -95,93 +94,6 @@ class InvenTreeCurrencySerializer(serializers.ChoiceField):
super().__init__(*args, **kwargs)
class DependentField(serializers.Field):
"""A dependent field can be used to dynamically return child fields based on the value of other fields."""
child = None
def __init__(self, *args, depends_on, field_serializer, **kwargs):
"""A dependent field can be used to dynamically return child fields based on the value of other fields.
Example:
This example adds two fields. If the client selects integer, an integer field will be shown, but if he
selects char, an char field will be shown. For any other value, nothing will be shown.
class TestSerializer(serializers.Serializer):
select_type = serializers.ChoiceField(choices=[
("integer", "Integer"),
("char", "Char"),
])
my_field = DependentField(depends_on=["select_type"], field_serializer="get_my_field")
def get_my_field(self, fields):
if fields["select_type"] == "integer":
return serializers.IntegerField()
if fields["select_type"] == "char":
return serializers.CharField()
"""
super().__init__(*args, **kwargs)
self.depends_on = depends_on
self.field_serializer = field_serializer
def get_child(self, raise_exception=False):
"""This method tries to extract the child based on the provided data in the request by the client."""
data = deepcopy(self.context["request"].data)
def visit_parent(node):
"""Recursively extract the data for the parent field/serializer in reverse."""
nonlocal data
if node.parent:
visit_parent(node.parent)
# only do for composite fields and stop right before the current field
if hasattr(node, "child") and node is not self and isinstance(data, dict):
data = data.get(node.field_name, None)
visit_parent(self)
# ensure that data is a dictionary and that a parent exists
if not isinstance(data, dict) or self.parent is None:
return
# check if the request data contains the dependent fields, otherwise skip getting the child
for f in self.depends_on:
if not data.get(f, None):
return
# partially validate the data for options requests that set raise_exception while calling .get_child(...)
if raise_exception:
validation_data = {k: v for k, v in data.items() if k in self.depends_on}
serializer = self.parent.__class__(context=self.context, data=validation_data, partial=True)
serializer.is_valid(raise_exception=raise_exception)
# try to get the field serializer
field_serializer = getattr(self.parent, self.field_serializer)
child = field_serializer(data)
if not child:
return
self.child = child
self.child.bind(field_name='', parent=self)
def to_internal_value(self, data):
"""This method tries to convert the data to an internal representation based on the defined to_internal_value method on the child."""
self.get_child()
if self.child:
return self.child.to_internal_value(data)
return None
def to_representation(self, value):
"""This method tries to convert the data to representation based on the defined to_representation method on the child."""
self.get_child()
if self.child:
return self.child.to_representation(value)
return None
class InvenTreeModelSerializer(serializers.ModelSerializer):
"""Inherits the standard Django ModelSerializer class, but also ensures that the underlying model class data are checked on validation."""

View File

@ -159,8 +159,7 @@ class LabelPrintMixin(LabelFilterMixin):
# Check the request to determine if the user has selected a label printing plugin
plugin = self.get_plugin(self.request)
kwargs.setdefault('context', self.get_serializer_context())
serializer = plugin.get_printing_options_serializer(self.request, *args, **kwargs)
serializer = plugin.get_printing_options_serializer(self.request)
# if no serializer is defined, return an empty serializer
if not serializer:
@ -227,7 +226,7 @@ class LabelPrintMixin(LabelFilterMixin):
raise ValidationError('Label has invalid dimensions')
# if the plugin returns a serializer, validate the data
if serializer := plugin.get_printing_options_serializer(request, data=request.data, context=self.get_serializer_context()):
if serializer := plugin.get_printing_options_serializer(request, data=request.data):
serializer.is_valid(raise_exception=True)
# At this point, we offload the label(s) to the selected plugin.

View File

@ -298,7 +298,7 @@ function constructDeleteForm(fields, options) {
* - closeText: Text for the "close" button
* - fields: list of fields to display, with the following options
* - filters: API query filters
* - onEdit: callback or array of callbacks which get fired when field is edited
* - onEdit: callback when field is edited
* - secondary: Define a secondary modal form for this field
* - label: Specify custom label
* - help_text: Specify custom help_text
@ -493,30 +493,6 @@ function constructFormBody(fields, options) {
html += options.header_html;
}
// process every field by recursively walking down nested fields
const processField = (name, field, optionsField) => {
if (field.type === "nested object") {
for (const [k, v] of Object.entries(field.children)) {
processField(`${name}__${k}`, v, optionsField.children[k]);
}
}
if (field.type === "dependent field") {
if(field.child) {
// copy child attribute from parameters to options
optionsField.child = field.child;
processField(name, field.child, optionsField.child);
} else {
delete optionsField.child;
}
}
}
for (const [k,v] of Object.entries(fields)) {
processField(k, v, options.fields[k]);
}
// Client must provide set of fields to be displayed,
// otherwise *all* fields will be displayed
var displayed_fields = options.fields || fields;
@ -623,6 +599,14 @@ function constructFormBody(fields, options) {
var field = fields[field_name];
switch (field.type) {
// Skip field types which are simply not supported
case 'nested object':
continue;
default:
break;
}
html += constructField(field_name, field, options);
}
@ -826,7 +810,7 @@ function insertSecondaryButtons(options) {
/*
* Extract all specified form values as a single object
*/
function extractFormData(fields, options, includeLocal = true) {
function extractFormData(fields, options) {
var data = {};
@ -839,7 +823,6 @@ function extractFormData(fields, options, includeLocal = true) {
if (!field) continue;
if (field.type == 'candy') continue;
if (!includeLocal && field.localOnly) continue;
data[name] = getFormFieldValue(name, field, options);
}
@ -1048,17 +1031,6 @@ function updateFieldValue(name, value, field, options) {
}
// TODO - Specify an actual value!
break;
case 'nested object':
for (const [k, v] of Object.entries(value)) {
if (!(k in field.children)) continue;
updateFieldValue(`${name}__${k}`, v, field.children[k], options);
}
break;
case 'dependent field':
if (field.child) {
updateFieldValue(name, value, field.child, options);
}
break;
case 'file upload':
case 'image upload':
break;
@ -1193,17 +1165,6 @@ function getFormFieldValue(name, field={}, options={}) {
case 'email':
value = sanitizeInputString(el.val());
break;
case 'nested object':
value = {};
for (const [name, subField] of Object.entries(field.children)) {
value[name] = getFormFieldValue(subField.name, subField, options);
}
break;
case 'dependent field':
if(!field.child) return undefined;
value = getFormFieldValue(name, field.child, options);
break;
default:
value = el.val();
break;
@ -1488,28 +1449,19 @@ function handleFormErrors(errors, fields={}, options={}) {
var field = fields[field_name] || {};
var field_errors = errors[field_name];
// for nested objects with children and dependent fields with a child defined, extract nested errors
if (((field.type == 'nested object') && ('children' in field)) || ((field.type == 'dependent field') && ('child' in field))) {
if ((field.type == 'nested object') && ('children' in field)) {
// Handle multi-level nested errors
const handleNestedError = (parent_name, sub_field_errors) => {
for (const sub_field in sub_field_errors) {
const sub_sub_field_name = `${parent_name}__${sub_field}`;
const sub_sub_field_errors = sub_field_errors[sub_field];
if (!first_error_field && sub_sub_field_errors && isFieldVisible(sub_sub_field_name, options)) {
first_error_field = sub_sub_field_name;
}
for (var sub_field in field_errors) {
var sub_field_name = `${field_name}__${sub_field}`;
var sub_field_errors = field_errors[sub_field];
// if the error is an object, its a nested object, recursively handle the errors
if (typeof sub_sub_field_errors === "object" && !Array.isArray(sub_sub_field_errors)) {
handleNestedError(sub_sub_field_name, sub_sub_field_errors)
} else {
addFieldErrorMessage(sub_sub_field_name, sub_sub_field_errors, options);
}
if (!first_error_field && sub_field_errors && isFieldVisible(sub_field_name, options)) {
first_error_field = sub_field_name;
}
}
handleNestedError(field_name, field_errors);
addFieldErrorMessage(sub_field_name, sub_field_errors, options);
}
} else if ((field.type == 'field') && ('child' in field)) {
// This is a "nested" array field
handleNestedArrayErrors(errors, field_name, options);
@ -1604,7 +1556,7 @@ function addFieldCallbacks(fields, options) {
var field = fields[name];
if (!field || field.type === "candy") continue;
if (!field || !field.onEdit) continue;
addFieldCallback(name, field, options);
}
@ -1612,34 +1564,15 @@ function addFieldCallbacks(fields, options) {
function addFieldCallback(name, field, options) {
const el = getFormFieldElement(name, options);
if (field.onEdit) {
el.change(function() {
var el = getFormFieldElement(name, options);
var value = getFormFieldValue(name, field, options);
let onEditHandlers = field.onEdit;
el.change(function() {
if (!Array.isArray(onEditHandlers)) {
onEditHandlers = [onEditHandlers];
}
var value = getFormFieldValue(name, field, options);
for (const onEdit of onEditHandlers) {
onEdit(value, name, field, options);
}
});
}
// attach field callback for nested fields
if(field.type === "nested object") {
for (const [c_name, c_field] of Object.entries(field.children)) {
addFieldCallback(`${name}__${c_name}`, c_field, options);
}
}
if(field.type === "dependent field" && field.child) {
addFieldCallback(name, field.child, options);
}
field.onEdit(value, name, field, options);
});
}
@ -1794,32 +1727,16 @@ function initializeRelatedFields(fields, options={}) {
if (!field || field.hidden) continue;
initializeRelatedFieldsRecursively(field, fields, options);
}
}
function initializeRelatedFieldsRecursively(field, fields, options) {
switch (field.type) {
case 'related field':
initializeRelatedField(field, fields, options);
break;
case 'choice':
initializeChoiceField(field, fields, options);
break;
case 'nested object':
for (const [c_name, c_field] of Object.entries(field.children)) {
if(!c_field.name) c_field.name = `${field.name}__${c_name}`;
initializeRelatedFieldsRecursively(c_field, field.children, options);
switch (field.type) {
case 'related field':
initializeRelatedField(field, fields, options);
break;
case 'choice':
initializeChoiceField(field, fields, options);
break;
default:
break;
}
break;
case 'dependent field':
if (field.child) {
if(!field.child.name) field.child.name = field.name;
initializeRelatedFieldsRecursively(field.child, fields, options);
}
break;
default:
break;
}
}
@ -2429,7 +2346,7 @@ function constructField(name, parameters, options={}) {
html += `<div id='div_id_${field_name}' class='${form_classes}' ${hover_title} ${css}>`;
// Add a label
if (!options.hideLabels && parameters.type !== "nested object" && parameters.type !== "dependent field") {
if (!options.hideLabels) {
html += constructLabel(name, parameters);
}
@ -2584,12 +2501,6 @@ function constructInput(name, parameters, options={}) {
case 'raw':
func = constructRawInput;
break;
case 'nested object':
func = constructNestedObject;
break;
case 'dependent field':
func = constructDependentField;
break;
default:
// Unsupported field type!
break;
@ -2869,129 +2780,6 @@ function constructRawInput(name, parameters) {
}
/*
* Construct a nested object input
*/
function constructNestedObject(name, parameters, options) {
let html = `
<div id="div_id_${name}" class='panel form-panel' style="margin-bottom: 0; padding-bottom: 0;">
<div class='panel-heading form-panel-heading'>
<div>
<h6 style='display: inline;'>${parameters.label}</h6>
</div>
</div>
<div class='panel-content form-panel-content' id="id_${name}">
`;
parameters.field_names = [];
for (const [key, field] of Object.entries(parameters.children)) {
const subFieldName = `${name}__${key}`;
field.name = subFieldName;
parameters.field_names.push(subFieldName);
html += constructField(subFieldName, field, options);
}
html += "</div></div>";
return html;
}
function getFieldByNestedPath(name, fields) {
if (typeof name === "string") {
name = name.split("__");
}
if (name.length === 0) return fields;
if (fields.type === "nested object") fields = fields.children;
if (!(name[0] in fields)) return null;
let field = fields[name[0]];
if (field.type === "dependent field" && field.child) {
field = field.child;
}
return getFieldByNestedPath(name.slice(1), field);
}
/*
* Construct a dependent field input
*/
function constructDependentField(name, parameters, options) {
// add onEdit handler to all fields this dependent field depends on
for (let d_field_name of parameters.depends_on) {
const d_field = getFieldByNestedPath([...name.split("__").slice(0, -1), d_field_name], options.fields);
if (!d_field) continue;
const onEdit = (value, name, field, options) => {
if(value === undefined) return;
// extract the current form data to include in OPTIONS request
const data = extractFormData(options.fields, options, false)
$.ajax({
url: options.url,
type: "OPTIONS",
data: JSON.stringify(data),
contentType: "application/json",
dataType: "json",
accepts: { json: "application/json" },
success: (res) => {
const fields = res.actions[options.method];
// merge already entered values in the newly constructed form
options.data = extractFormData(options.fields, options);
// remove old submit handlers
$(options.modal).off('click', '#modal-form-submit');
if (options.method === "POST") {
constructCreateForm(fields, options);
}
if (options.method === "PUT" || options.method === "PATCH") {
constructChangeForm(fields, options);
}
if (options.method === "DELETE") {
constructDeleteForm(fields, options);
}
},
error: (xhr) => showApiError(xhr, options.url)
});
}
// attach on edit handler
const originalOnEdit = d_field.onEdit;
d_field.onEdit = [onEdit];
if(typeof originalOnEdit === "function") {
d_field.onEdit.push(originalOnEdit);
} else if (Array.isArray(originalOnEdit)) {
// push old onEdit handlers, but omit the old
d_field.onEdit.push(...originalOnEdit.filter(h => h !== d_field._currentDependentFieldOnEdit));
}
// track current onEdit handler function
d_field._currentDependentFieldOnEdit = onEdit;
}
// child is not specified already, return a dummy div with id so no errors can happen
if (!parameters.child) {
return `<div id="id_${name}" hidden></div>`;
}
// copy label to child if not already provided
if(!parameters.child.label) {
parameters.child.label = parameters.label;
}
// construct the provided child field
return constructField(name, parameters.child, options);
}
/*
* Construct a 'help text' div based on the field parameters

View File

@ -137,11 +137,6 @@ function printLabels(options) {
// update form
updateForm(formOptions);
// workaround to fix a bug where one cannot scroll after changing the plugin
// without opening and closing the select box again manually
$("#id__plugin").select2("open");
$("#id__plugin").select2("close");
}
const printingFormOptions = {