mirror of
https://github.com/inventree/InvenTree.git
synced 2026-09-27 14:16:02 +00:00
Fix validation of dependent serializer fields (#12871)
This commit is contained in:
@@ -693,7 +693,7 @@ class DependentField(serializers.Field):
|
|||||||
"""This method tries to convert the data to an internal representation based on the defined to_internal_value method on the child."""
|
"""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()
|
self.get_child()
|
||||||
if self.child:
|
if self.child:
|
||||||
return self.child.to_internal_value(data)
|
return self.child.run_validation(data)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -3,21 +3,110 @@
|
|||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.test import SimpleTestCase
|
||||||
from django.urls import path, reverse
|
from django.urls import path, reverse
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
from rest_framework.serializers import SerializerMethodField
|
from rest_framework.serializers import SerializerMethodField
|
||||||
|
|
||||||
import InvenTree.serializers
|
import InvenTree.serializers
|
||||||
from InvenTree.mixins import ListCreateAPI, OutputOptionsMixin
|
from InvenTree.mixins import ListCreateAPI, OutputOptionsMixin
|
||||||
from InvenTree.serializers import OptionalField
|
from InvenTree.serializers import DependentField, OptionalField
|
||||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||||
from InvenTree.urls import backendpatterns
|
from InvenTree.urls import backendpatterns
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
|
|
||||||
|
|
||||||
|
class DependentFieldTests(SimpleTestCase):
|
||||||
|
"""Test validation of dynamically selected child fields and serializers."""
|
||||||
|
|
||||||
|
def get_serializer(self, child, value):
|
||||||
|
"""Bind a dependent child using the same data as the request."""
|
||||||
|
|
||||||
|
class ParentSerializer(serializers.Serializer):
|
||||||
|
kind = serializers.CharField()
|
||||||
|
options = DependentField(
|
||||||
|
depends_on=['kind'], field_serializer='get_options'
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_options(self, fields):
|
||||||
|
return child
|
||||||
|
|
||||||
|
data = {'kind': 'test', 'options': value}
|
||||||
|
return ParentSerializer(
|
||||||
|
data=data, context={'request': SimpleNamespace(data=data)}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_child_field_validators(self):
|
||||||
|
"""Run validators on the selected child field."""
|
||||||
|
serializer = self.get_serializer(serializers.IntegerField(min_value=1), '0')
|
||||||
|
|
||||||
|
self.assertFalse(serializer.is_valid())
|
||||||
|
self.assertEqual(serializer.errors['options'][0].code, 'min_value')
|
||||||
|
|
||||||
|
def test_child_serializer_validation(self):
|
||||||
|
"""Propagate object-level validation errors under the dependent field."""
|
||||||
|
|
||||||
|
class OptionsSerializer(serializers.Serializer):
|
||||||
|
copies = serializers.IntegerField()
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
raise serializers.ValidationError('Invalid options', code='options')
|
||||||
|
|
||||||
|
serializer = self.get_serializer(OptionsSerializer(), {'copies': 1})
|
||||||
|
|
||||||
|
with self.assertRaises(serializers.ValidationError) as error:
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
detail = error.exception.detail['options']['non_field_errors'][0]
|
||||||
|
self.assertEqual(detail, 'Invalid options')
|
||||||
|
self.assertEqual(detail.code, 'options')
|
||||||
|
|
||||||
|
def test_valid_child_field(self):
|
||||||
|
"""Preserve conversion of valid child field input."""
|
||||||
|
serializer = self.get_serializer(serializers.IntegerField(min_value=1), '2')
|
||||||
|
|
||||||
|
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||||
|
self.assertEqual(serializer.validated_data['options'], 2)
|
||||||
|
|
||||||
|
def test_valid_child_serializer(self):
|
||||||
|
"""Use the data returned by the child serializer's validate method."""
|
||||||
|
|
||||||
|
class OptionsSerializer(serializers.Serializer):
|
||||||
|
copies = serializers.IntegerField()
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
attrs['copies'] *= 2
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
serializer = self.get_serializer(OptionsSerializer(), {'copies': '2'})
|
||||||
|
|
||||||
|
self.assertTrue(serializer.is_valid(), serializer.errors)
|
||||||
|
self.assertEqual(serializer.validated_data['options'], {'copies': 4})
|
||||||
|
|
||||||
|
def test_child_conversion_error(self):
|
||||||
|
"""Preserve ordinary child field conversion errors."""
|
||||||
|
serializer = self.get_serializer(serializers.IntegerField(), 'invalid')
|
||||||
|
|
||||||
|
self.assertFalse(serializer.is_valid())
|
||||||
|
self.assertEqual(serializer.errors['options'][0].code, 'invalid')
|
||||||
|
|
||||||
|
def test_child_serializer_field_error(self):
|
||||||
|
"""Preserve nested field errors from a child serializer."""
|
||||||
|
|
||||||
|
class OptionsSerializer(serializers.Serializer):
|
||||||
|
copies = serializers.IntegerField()
|
||||||
|
|
||||||
|
serializer = self.get_serializer(OptionsSerializer(), {'copies': 'invalid'})
|
||||||
|
|
||||||
|
self.assertFalse(serializer.is_valid())
|
||||||
|
self.assertEqual(serializer.errors['options']['copies'][0].code, 'invalid')
|
||||||
|
|
||||||
|
|
||||||
class SampleSerializer(
|
class SampleSerializer(
|
||||||
InvenTree.serializers.FilterableSerializerMixin,
|
InvenTree.serializers.FilterableSerializerMixin,
|
||||||
InvenTree.serializers.InvenTreeModelSerializer,
|
InvenTree.serializers.InvenTreeModelSerializer,
|
||||||
|
|||||||
@@ -9,9 +9,12 @@ from django.urls import reverse
|
|||||||
|
|
||||||
from pdfminer.high_level import extract_text
|
from pdfminer.high_level import extract_text
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
from InvenTree.config import get_testfolder_dir
|
from InvenTree.config import get_testfolder_dir
|
||||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||||
|
from machine import registry as machine_registry
|
||||||
|
from machine.models import MachineConfig
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
from plugin import InvenTreePlugin, PluginMixinEnum, registry
|
from plugin import InvenTreePlugin, PluginMixinEnum, registry
|
||||||
from plugin.base.label.mixins import LabelPrintingMixin
|
from plugin.base.label.mixins import LabelPrintingMixin
|
||||||
@@ -269,6 +272,57 @@ class LabelMixinTests(PrintTestMixins, InvenTreeAPITestCase):
|
|||||||
print_label.call_args.kwargs['printing_options'], {'amount': 13}
|
print_label.call_args.kwargs['printing_options'], {'amount': 13}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_machine_driver_options_validate_before_print_task(self):
|
||||||
|
"""Test that machine driver option validation runs before the print task."""
|
||||||
|
self.ensurePluginsLoaded()
|
||||||
|
apps.get_app_config('report').create_default_labels()
|
||||||
|
machine_registry.initialize()
|
||||||
|
registry.set_plugin_state('label-printer-test-plugin', True)
|
||||||
|
|
||||||
|
machine_config = MachineConfig.objects.create(
|
||||||
|
machine_type='label-printer',
|
||||||
|
driver='test-label-printer-api',
|
||||||
|
name='Test label printer',
|
||||||
|
active=True,
|
||||||
|
)
|
||||||
|
machine = machine_registry.get_machine(str(machine_config.pk))
|
||||||
|
self.assertIsNotNone(machine)
|
||||||
|
|
||||||
|
template = LabelTemplate.objects.filter(enabled=True, model_type='part').first()
|
||||||
|
assert template
|
||||||
|
part = Part.objects.first()
|
||||||
|
assert part
|
||||||
|
|
||||||
|
class RejectingOptionsSerializer(serializers.Serializer):
|
||||||
|
copies = serializers.IntegerField(required=False, default=1)
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
raise serializers.ValidationError('preflight failed')
|
||||||
|
|
||||||
|
driver = machine.driver
|
||||||
|
with (
|
||||||
|
mock.patch('InvenTree.tasks.offload_task') as offload_task,
|
||||||
|
mock.patch.object(
|
||||||
|
driver,
|
||||||
|
'get_printing_options_serializer',
|
||||||
|
side_effect=lambda *args, **kwargs: RejectingOptionsSerializer(),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
response = self.post(
|
||||||
|
self.printing_url,
|
||||||
|
{
|
||||||
|
'plugin': 'inventreelabelmachine',
|
||||||
|
'template': template.pk,
|
||||||
|
'items': [part.pk],
|
||||||
|
'machine': str(machine_config.pk),
|
||||||
|
'driver_options': {'copies': 1},
|
||||||
|
},
|
||||||
|
expected_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
offload_task.assert_not_called()
|
||||||
|
self.assertIn('preflight failed', str(response.data))
|
||||||
|
|
||||||
def test_printing_endpoints(self):
|
def test_printing_endpoints(self):
|
||||||
"""Cover the endpoints not covered by `test_printing_process`."""
|
"""Cover the endpoints not covered by `test_printing_process`."""
|
||||||
# Activate the label components
|
# Activate the label components
|
||||||
|
|||||||
Reference in New Issue
Block a user