From 8fd41f5ecf36f47a685e7a53f787b9cbe015361f Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 21 Jun 2021 21:46:22 +1000 Subject: [PATCH 01/11] Unit testing for default values --- InvenTree/part/test_api.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 0f5f59d3a3..5c39c3dce5 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -292,6 +292,29 @@ class PartAPITest(InvenTreeAPITestCase): self.assertEqual(len(data['results']), n) + def test_default_values(self): + """ + Tests for 'default' values: + + Ensure that unspecified fields revert to "default" values + (as specified in the model field definition) + """ + + url = reverse('api-part-list') + + response = self.client.post(url, { + 'name': 'all defaults', + 'description': 'my test part', + 'category': 1, + }) + + data = response.data + + # Check that the un-specified fields have used correct default values + self.assertTrue(data['active']) + self.assertFalse(data['virtual']) + self.assertTrue(data['purchaseable']) + class PartAPIAggregationTest(InvenTreeAPITestCase): """ From ba1c70e86b93b6695a990e9a5df2333bab3e9efc Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 26 Jun 2021 20:48:09 +1000 Subject: [PATCH 02/11] Intercept is_valid() method to set default values --- InvenTree/InvenTree/serializers.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index fa7674723c..5be6d44c0c 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -7,6 +7,7 @@ Serializers used in various InvenTree apps from __future__ import unicode_literals from rest_framework import serializers +from rest_framework.utils import model_meta import os @@ -39,6 +40,35 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): but also ensures that the underlying model class data are checked on validation. """ + def is_valid(self, raise_exception=False): + """ + Override the 'is_valid' method of the underlying ModelSerializer class. + + - This is so we can intercept the data before save() is called. + - If we are creating a *new* model, replace any "missing" fields with the default values + - Default values are those specified by the database model + """ + + # This serializer is *not* associated with a model instance + # This means that we are trying to *create* a new model + if self.instance is None: + ModelClass = self.Meta.model + + fields = model_meta.get_field_info(ModelClass) + + for field_name, field in fields.fields.items(): + + # Check if the field has a default value + if field.has_default(): + + # Value not specified? + if field_name not in self.initial_data: + + self.initial_data[field_name] = field.default + + + return super().is_valid(raise_exception=raise_exception) + def validate(self, data): """ Perform serializer validation. In addition to running validators on the serializer fields, From b2aa38fefa66922165071690058686cfef7bee93 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 26 Jun 2021 21:14:10 +1000 Subject: [PATCH 03/11] Override get_initial() rather than is_valid() --- InvenTree/InvenTree/serializers.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index f19ef23c00..82584bdbb0 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -45,17 +45,15 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): but also ensures that the underlying model class data are checked on validation. """ - def is_valid(self, raise_exception=False): + def get_initial(self): """ - Override the 'is_valid' method of the underlying ModelSerializer class. - - - This is so we can intercept the data before save() is called. - - If we are creating a *new* model, replace any "missing" fields with the default values - - Default values are those specified by the database model + Construct initial data for the serializer. + Use the 'default' values specified by the django model definition """ - # This serializer is *not* associated with a model instance - # This means that we are trying to *create* a new model + initials = super().get_initial() + + # Are we creating a new instance? if self.instance is None: ModelClass = self.Meta.model @@ -63,16 +61,10 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): for field_name, field in fields.fields.items(): - # Check if the field has a default value if field.has_default(): + initials[field_name] = field.default - # Value not specified? - if field_name not in self.initial_data: - - self.initial_data[field_name] = field.default - - - return super().is_valid(raise_exception=raise_exception) + return initials def run_validation(self, data=empty): """ Perform serializer validation. From a0390f08219b6690bf35eda117c69e8af557108b Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 26 Jun 2021 21:14:47 +1000 Subject: [PATCH 04/11] PEP style fixes --- InvenTree/InvenTree/serializers.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 82584bdbb0..9e8e811b90 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -2,13 +2,9 @@ Serializers used in various InvenTree apps """ - # -*- coding: utf-8 -*- from __future__ import unicode_literals -from rest_framework import serializers -from rest_framework.utils import model_meta - import os from django.conf import settings @@ -16,6 +12,7 @@ from django.contrib.auth.models import User from django.core.exceptions import ValidationError as DjangoValidationError from rest_framework import serializers +from rest_framework.utils import model_meta from rest_framework.fields import empty from rest_framework.exceptions import ValidationError From ae1a1e139fa9137f2bb5fe11d42f2c3d4f2c4365 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 27 Jun 2021 00:01:40 +1000 Subject: [PATCH 05/11] Further fixes for default API values - Account for callable defaults - Extra check in is_valid() --- InvenTree/InvenTree/api_tester.py | 7 +++++- InvenTree/InvenTree/serializers.py | 39 +++++++++++++++++++++++++++++- InvenTree/part/test_api.py | 34 +++++++++++++++++++++++++- 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/api_tester.py b/InvenTree/InvenTree/api_tester.py index a803e6797f..1cbc62ec0a 100644 --- a/InvenTree/InvenTree/api_tester.py +++ b/InvenTree/InvenTree/api_tester.py @@ -18,6 +18,7 @@ class InvenTreeAPITestCase(APITestCase): email = 'test@testing.com' superuser = False + is_staff = True auto_login = True # Set list of roles automatically associated with the user @@ -40,8 +41,12 @@ class InvenTreeAPITestCase(APITestCase): if self.superuser: self.user.is_superuser = True - self.user.save() + if self.is_staff: + self.user.is_staff = True + + self.user.save() + for role in self.roles: self.assignRole(role) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 9e8e811b90..c00c9ef690 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -59,10 +59,47 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): for field_name, field in fields.fields.items(): if field.has_default(): - initials[field_name] = field.default + + value = field.default + + # Account for callable functions + if callable(value): + value = value() + + initials[field_name] = value return initials + def is_valid(self, raise_exception=False): + """ + Also override the is_valid() method, as in some cases get_initial() is not actually called. + """ + + # Calling super().is_valid creates self._validated_data + valid = super().is_valid(raise_exception) + + # Are we creating a new instance? + if self.instance is None: + ModelClass = self.Meta.model + + fields = model_meta.get_field_info(ModelClass) + + for field_name, field in fields.fields.items(): + + if field.has_default(): + if field not in self._validated_data: + + value = field.default + + # Account for callable functions + if callable(value): + value = value() + + self._validated_data[field_name] = value + + + return valid + def run_validation(self, data=empty): """ Perform serializer validation. In addition to running validators on the serializer fields, diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 787c5d3f85..29df621914 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -13,6 +13,7 @@ from InvenTree.status_codes import StockStatus from part.models import Part, PartCategory from stock.models import StockItem from company.models import Company +from common.models import InvenTreeSetting class PartAPITest(InvenTreeAPITestCase): @@ -332,7 +333,38 @@ class PartAPITest(InvenTreeAPITestCase): # Check that the un-specified fields have used correct default values self.assertTrue(data['active']) self.assertFalse(data['virtual']) - self.assertTrue(data['purchaseable']) + + # By default, parts are not purchaseable + self.assertFalse(data['purchaseable']) + + # Set the default 'purchaseable' status to True + InvenTreeSetting.set_setting( + 'PART_PURCHASEABLE', + True, + self.user + ) + + response = self.client.post(url, { + 'name': 'all defaults', + 'description': 'my test part 2', + 'category': 1, + }) + + # Part should now be purchaseable by default + self.assertTrue(response.data['purchaseable']) + + # "default" values should not be used if the value is specified + response = self.client.post(url, { + 'name': 'all defaults', + 'description': 'my test part 2', + 'category': 1, + 'active': False, + 'purchaseable': False, + }) + + self.assertFalse(response.data['active']) + self.assertFalse(response.data['purchaseable']) + class PartDetailTests(InvenTreeAPITestCase): From 8913b74f4183686e2074c16c05e15d5b5522917b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 27 Jun 2021 00:12:10 +1000 Subject: [PATCH 06/11] Typo fixes --- InvenTree/InvenTree/serializers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index c00c9ef690..6b5bdaef22 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -58,7 +58,7 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): for field_name, field in fields.fields.items(): - if field.has_default(): + if field.has_default() and field_name not in initials: value = field.default @@ -87,7 +87,7 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): for field_name, field in fields.fields.items(): if field.has_default(): - if field not in self._validated_data: + if field_name not in self._validated_data.keys(): value = field.default @@ -97,7 +97,6 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): self._validated_data[field_name] = value - return valid def run_validation(self, data=empty): From 232899e0c43b1448cbcc9ca7b68b464fc9c08b13 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 27 Jun 2021 00:25:23 +1000 Subject: [PATCH 07/11] Simpler implementation --- InvenTree/InvenTree/serializers.py | 65 +++++++++++++++++------------- InvenTree/part/test_api.py | 1 - 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 6b5bdaef22..9d695ab0de 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -7,6 +7,8 @@ from __future__ import unicode_literals import os +from collections import OrderedDict + from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import ValidationError as DjangoValidationError @@ -42,6 +44,38 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): but also ensures that the underlying model class data are checked on validation. """ + def __init__(self, instance=None, data=empty, **kwargs): + + self.instance = instance + + # If instance is None, we are creating a new instance + if instance is None: + + if data is empty: + data = OrderedDict() + else: + # Required to side-step immutability of a QueryDict + data = data.copy() + + # Add missing fields which have default values + ModelClass = self.Meta.model + + fields = model_meta.get_field_info(ModelClass) + + for field_name, field in fields.fields.items(): + + if field.has_default() and field_name not in data: + + value = field.default + + # Account for callable functions + if callable(value): + value = value() + + data[field_name] = value + + super().__init__(instance, data, **kwargs) + def get_initial(self): """ Construct initial data for the serializer. @@ -50,6 +84,8 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): initials = super().get_initial() + print("initials:", initials) + # Are we creating a new instance? if self.instance is None: ModelClass = self.Meta.model @@ -70,35 +106,6 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): return initials - def is_valid(self, raise_exception=False): - """ - Also override the is_valid() method, as in some cases get_initial() is not actually called. - """ - - # Calling super().is_valid creates self._validated_data - valid = super().is_valid(raise_exception) - - # Are we creating a new instance? - if self.instance is None: - ModelClass = self.Meta.model - - fields = model_meta.get_field_info(ModelClass) - - for field_name, field in fields.fields.items(): - - if field.has_default(): - if field_name not in self._validated_data.keys(): - - value = field.default - - # Account for callable functions - if callable(value): - value = value() - - self._validated_data[field_name] = value - - return valid - def run_validation(self, data=empty): """ Perform serializer validation. In addition to running validators on the serializer fields, diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 29df621914..3fbf26f8bb 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -366,7 +366,6 @@ class PartAPITest(InvenTreeAPITestCase): self.assertFalse(response.data['purchaseable']) - class PartDetailTests(InvenTreeAPITestCase): """ Test that we can create / edit / delete Part objects via the API From d3e9803fd48626d30dbddbcc26d9dd0a6ce6eea7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 27 Jun 2021 00:25:49 +1000 Subject: [PATCH 08/11] Remove debug statement --- InvenTree/InvenTree/serializers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 9d695ab0de..1f7b8c0290 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -84,8 +84,6 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): initials = super().get_initial() - print("initials:", initials) - # Are we creating a new instance? if self.instance is None: ModelClass = self.Meta.model From 7505d7b3c57bd92738be6c0f0eadb0087c48072c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 27 Jun 2021 00:47:12 +1000 Subject: [PATCH 09/11] Unit test fixes --- InvenTree/stock/test_api.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py index e1fb616335..729bf25a9b 100644 --- a/InvenTree/stock/test_api.py +++ b/InvenTree/stock/test_api.py @@ -354,16 +354,18 @@ class StockItemTest(StockAPITestCase): self.assertContains(response, 'does not exist', status_code=status.HTTP_400_BAD_REQUEST) # POST without quantity - response = self.client.post( + response = self.post( self.list_url, - data={ + { 'part': 1, 'location': 1, - } + }, + expected_code=201, ) - self.assertContains(response, 'This field is required', status_code=status.HTTP_400_BAD_REQUEST) - + # Item should have been created with default quantity + self.assertEqual(response.data['quantity'], 1) + # POST with quantity and part and location response = self.client.post( self.list_url, From 0cc999410b443c1173b46bfb5282dedc79691e36 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 27 Jun 2021 01:03:54 +1000 Subject: [PATCH 10/11] More unit test fixes --- InvenTree/part/test_api.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 3fbf26f8bb..ee01df89cf 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -445,7 +445,14 @@ class PartDetailTests(InvenTreeAPITestCase): # Try to remove the part response = self.client.delete(url) - + + # As the part is 'active' we cannot delete it + self.assertEqual(response.status_code, 405) + + # So, let's make it not active + response = self.patch(url, {'active': False}, expected_code=200) + + response = self.client.delete(url) self.assertEqual(response.status_code, 204) # Part count should have reduced From 34a374ce9ac0a212ca05774ffb5f344d601cb92d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 27 Jun 2021 01:18:09 +1000 Subject: [PATCH 11/11] Add try/except around callable default --- InvenTree/InvenTree/serializers.py | 10 ++++++++-- InvenTree/part/test_api.py | 2 -- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index 1f7b8c0290..50a37d8cba 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -70,7 +70,10 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): # Account for callable functions if callable(value): - value = value() + try: + value = value() + except: + continue data[field_name] = value @@ -98,7 +101,10 @@ class InvenTreeModelSerializer(serializers.ModelSerializer): # Account for callable functions if callable(value): - value = value() + try: + value = value() + except: + continue initials[field_name] = value diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index ee01df89cf..4922ed4e04 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -603,8 +603,6 @@ class PartDetailTests(InvenTreeAPITestCase): # And now check that the image has been set p = Part.objects.get(pk=pk) - print("Image:", p.image.file) - class PartAPIAggregationTest(InvenTreeAPITestCase): """