Skip to content

Commit

Permalink
CUI dependent/nested fields (#5924)
Browse files Browse the repository at this point in the history
* Added backend changes to support printing options

* Pass printing options seperatly via kwargs for easier api refactor later

* Implemented printing options in CUI

* Fix js linting

* Use translations for printing dialog

* Support nested fields in CUI

* Added docs

* Remove plugin and template fields from send printing options

* Fix docs

* Added tests

* Fix tests

* Fix options response and added test for it

* Fix tests

* Bump api version

* Update docs

* Apply suggestions from code review

* Fix api change date

* Added dependent field and improved nested object fields on CUI

* Fix: cui js style

* Fix process field implementation if the 'old' __ syntax is used for nested fields
  • Loading branch information
wolflu05 authored Nov 16, 2023
1 parent acb3192 commit 0b168c1
Show file tree
Hide file tree
Showing 5 changed files with 356 additions and 36 deletions.
12 changes: 12 additions & 0 deletions InvenTree/InvenTree/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import InvenTree.permissions
import users.models
from InvenTree.helpers import str2bool
from InvenTree.serializers import DependentField

logger = logging.getLogger('inventree')

Expand Down Expand Up @@ -242,6 +243,10 @@ def get_field_info(self, field):
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)

Expand Down Expand Up @@ -275,4 +280,11 @@ def get_field_info(self, field):
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"
88 changes: 88 additions & 0 deletions InvenTree/InvenTree/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import os
from collections import OrderedDict
from copy import deepcopy
from decimal import Decimal

from django.conf import settings
Expand Down Expand Up @@ -94,6 +95,93 @@ def __init__(self, *args, **kwargs):
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."""

Expand Down
5 changes: 3 additions & 2 deletions InvenTree/label/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ def get_serializer(self, *args, **kwargs):
# Check the request to determine if the user has selected a label printing plugin
plugin = self.get_plugin(self.request)

serializer = plugin.get_printing_options_serializer(self.request)
kwargs.setdefault('context', self.get_serializer_context())
serializer = plugin.get_printing_options_serializer(self.request, *args, **kwargs)

# if no serializer is defined, return an empty serializer
if not serializer:
Expand Down Expand Up @@ -226,7 +227,7 @@ def print(self, request, items_to_print):
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):
if serializer := plugin.get_printing_options_serializer(request, data=request.data, context=self.get_serializer_context()):
serializer.is_valid(raise_exception=True)

# At this point, we offload the label(s) to the selected plugin.
Expand Down
Loading

0 comments on commit 0b168c1

Please sign in to comment.