From 1488a0e72f94a018b3802107c70af981bee116e5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 17 May 2022 00:18:47 +1000 Subject: [PATCH 01/10] Adds a custom exception handler for DRF - Handles common exceptions not captured by DRF - Returns exeption data as a JSON object --- InvenTree/InvenTree/exceptions.py | 80 +++++++++++++++++++++++++++++++ InvenTree/InvenTree/settings.py | 2 +- 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 InvenTree/InvenTree/exceptions.py diff --git a/InvenTree/InvenTree/exceptions.py b/InvenTree/InvenTree/exceptions.py new file mode 100644 index 0000000000..f57df8ac00 --- /dev/null +++ b/InvenTree/InvenTree/exceptions.py @@ -0,0 +1,80 @@ +""" +Custom exception handling for the DRF API +""" + +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import json + +from django.db.utils import OperationalError, ProgrammingError, IntegrityError +from django.conf import settings +from django.core.exceptions import ValidationError as DjangoValidationError +from django.utils.translation import gettext_lazy as _ + +from error_report.models import Error + +from rest_framework.exceptions import ValidationError as DRFValidationError +from rest_framework.response import Response +from rest_framework import serializers +import rest_framework.views as drfviews + + +def exception_handler(exc, context): + """ + Custom exception handler for DRF framework. + Ref: https://www.django-rest-framework.org/api-guide/exceptions/#custom-exception-handling + + Catches any errors not natively handled by DRF, and re-throws as an error DRF can handle + """ + + response = None + + # Catch any django validation error, and re-throw a DRF validation error + if isinstance(exc, DjangoValidationError): + exc = DRFValidationError(detail=serializers.as_serializer_error(exc)) + + # Exceptions we will manually check for here + handled_exceptions = [ + IntegrityError, + OperationalError, + ProgrammingError, + ValueError, + TypeError, + NameError, + ] + + if any([isinstance(exc, err_type) for err_type in handled_exceptions]): + + if settings.DEBUG: + error_detail = str(exc) + else: + error_detail = _("Error details can be found in the admin panel") + + response_data = { + 'error': type(exc).__name__, + 'error_class': str(type(exc)), + 'detail': error_detail, + 'path': context['request'].path, + 'status_code': 500, + } + + response = Response(response_data, status=500) + + # Log the exception to the database, too + Error.objects.create( + kind="Unhandled DRF API Exception", + info=str(type(exc)), + data=str(exc), + path=context['request'].path, + ) + + else: + # Fallback to the default DRF exception handler + response = drfviews.exception_handler(exc, context) + + if response is not None: + # For an error response, include status code information + response.data['status_code'] = response.status_code + + return response diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index cba4ab4c6a..5c6bca4dea 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -353,7 +353,7 @@ TEMPLATES = [ ] REST_FRAMEWORK = { - 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler', + 'EXCEPTION_HANDLER': 'InvenTree.exceptions.exception_handler', 'DATETIME_FORMAT': '%Y-%m-%d %H:%M', 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.BasicAuthentication', From ae50546ca67ea40872fe213ed88befdce2b63256 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 17 May 2022 00:25:32 +1000 Subject: [PATCH 02/10] Display API error information if available --- InvenTree/templates/js/translated/api.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/InvenTree/templates/js/translated/api.js b/InvenTree/templates/js/translated/api.js index 119376c310..5ec63ba51c 100644 --- a/InvenTree/templates/js/translated/api.js +++ b/InvenTree/templates/js/translated/api.js @@ -225,6 +225,20 @@ function showApiError(xhr, url) { default: title = '{% trans "Unhandled Error Code" %}'; message = `{% trans "Error code" %}: ${xhr.status}`; + + var response = xhr.responseJSON; + + // The server may have provided some extra information about this error + if (response) { + if (response.error) { + title = response.error + } + + if (response.detail) { + message = response.detail; + } + } + break; } From 6512c0061eed3f4356faaeff81fb27ec497806e2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 17 May 2022 00:35:24 +1000 Subject: [PATCH 03/10] Catch a 500 and make it a 400 While we are at it, convert __all__ to non_field_errors automatically --- InvenTree/InvenTree/exceptions.py | 5 +++++ InvenTree/order/serializers.py | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/exceptions.py b/InvenTree/InvenTree/exceptions.py index f57df8ac00..78a494c9f2 100644 --- a/InvenTree/InvenTree/exceptions.py +++ b/InvenTree/InvenTree/exceptions.py @@ -77,4 +77,9 @@ def exception_handler(exc, context): # For an error response, include status code information response.data['status_code'] = response.status_code + # Convert errors returned under the label '__all__' to 'non_field_errors' + if '__all__' in response.data: + response.data['non_field_errors'] = response.data['all'] + del response.data['__all__'] + return response diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index c97090e4f6..1c261e7a86 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -1287,13 +1287,17 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer): with transaction.atomic(): for entry in items: + # Create a new SalesOrderAllocation - order.models.SalesOrderAllocation.objects.create( + allocation = order.models.SalesOrderAllocation( line=entry.get('line_item'), item=entry.get('stock_item'), quantity=entry.get('quantity'), shipment=shipment, ) + + allocation.full_clean() + allocation.save() class SalesOrderExtraLineSerializer(AbstractExtraLineSerializer, InvenTreeModelSerializer): From 263ac01727b91063a7c4baeff385c16f5db485c0 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 17 May 2022 00:35:59 +1000 Subject: [PATCH 04/10] Typo fix --- InvenTree/InvenTree/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/exceptions.py b/InvenTree/InvenTree/exceptions.py index 78a494c9f2..5ba0d80bec 100644 --- a/InvenTree/InvenTree/exceptions.py +++ b/InvenTree/InvenTree/exceptions.py @@ -79,7 +79,7 @@ def exception_handler(exc, context): # Convert errors returned under the label '__all__' to 'non_field_errors' if '__all__' in response.data: - response.data['non_field_errors'] = response.data['all'] + response.data['non_field_errors'] = response.data['__all__'] del response.data['__all__'] return response From 3373bb19f1f57ecbbd3b9e08ad5d3746f2897fad Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 17 May 2022 00:36:30 +1000 Subject: [PATCH 05/10] Remove unique_together requirement on SalesOrderAllocation model --- ...lter_salesorderallocation_unique_together.py | 17 +++++++++++++++++ InvenTree/order/models.py | 6 ------ 2 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 InvenTree/order/migrations/0068_alter_salesorderallocation_unique_together.py diff --git a/InvenTree/order/migrations/0068_alter_salesorderallocation_unique_together.py b/InvenTree/order/migrations/0068_alter_salesorderallocation_unique_together.py new file mode 100644 index 0000000000..23915cf9d0 --- /dev/null +++ b/InvenTree/order/migrations/0068_alter_salesorderallocation_unique_together.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.13 on 2022-05-16 14:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0067_auto_20220516_1120'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='salesorderallocation', + unique_together=set(), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index e918f0a30c..7460e81e56 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -1269,12 +1269,6 @@ class SalesOrderAllocation(models.Model): def get_api_url(): return reverse('api-so-allocation-list') - class Meta: - unique_together = [ - # Cannot allocate any given StockItem to the same line more than once - ('line', 'item'), - ] - def clean(self): """ Validate the SalesOrderAllocation object: From 27930cd8977ab805f2ad3f2bd838bde47e207886 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 17 May 2022 00:41:03 +1000 Subject: [PATCH 06/10] PEP style fixes --- InvenTree/InvenTree/exceptions.py | 4 +--- InvenTree/order/serializers.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/InvenTree/InvenTree/exceptions.py b/InvenTree/InvenTree/exceptions.py index 5ba0d80bec..0730442a08 100644 --- a/InvenTree/InvenTree/exceptions.py +++ b/InvenTree/InvenTree/exceptions.py @@ -5,8 +5,6 @@ Custom exception handling for the DRF API # -*- coding: utf-8 -*- from __future__ import unicode_literals -import json - from django.db.utils import OperationalError, ProgrammingError, IntegrityError from django.conf import settings from django.core.exceptions import ValidationError as DjangoValidationError @@ -76,7 +74,7 @@ def exception_handler(exc, context): if response is not None: # For an error response, include status code information response.data['status_code'] = response.status_code - + # Convert errors returned under the label '__all__' to 'non_field_errors' if '__all__' in response.data: response.data['non_field_errors'] = response.data['__all__'] diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 1c261e7a86..ede4c806ef 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -1295,7 +1295,7 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer): quantity=entry.get('quantity'), shipment=shipment, ) - + allocation.full_clean() allocation.save() From 2509db2b88d3b8f6aa1ce6ceae1f7a32aeec7ca1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 17 May 2022 00:52:01 +1000 Subject: [PATCH 07/10] JS linting --- InvenTree/templates/js/translated/api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/js/translated/api.js b/InvenTree/templates/js/translated/api.js index 5ec63ba51c..17b1b2fca0 100644 --- a/InvenTree/templates/js/translated/api.js +++ b/InvenTree/templates/js/translated/api.js @@ -231,7 +231,7 @@ function showApiError(xhr, url) { // The server may have provided some extra information about this error if (response) { if (response.error) { - title = response.error + title = response.error; } if (response.detail) { From 048f1ad6019913ec667de6f95ff4943ad8edcad3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 17 May 2022 01:03:02 +1000 Subject: [PATCH 08/10] Simplify DRF exception handler - Check the default handler first - If *any* other API requets throws an exception, will now pass through the custom handler --- InvenTree/InvenTree/exceptions.py | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/InvenTree/InvenTree/exceptions.py b/InvenTree/InvenTree/exceptions.py index 0730442a08..20d74fd4ac 100644 --- a/InvenTree/InvenTree/exceptions.py +++ b/InvenTree/InvenTree/exceptions.py @@ -5,7 +5,8 @@ Custom exception handling for the DRF API # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db.utils import OperationalError, ProgrammingError, IntegrityError +import traceback + from django.conf import settings from django.core.exceptions import ValidationError as DjangoValidationError from django.utils.translation import gettext_lazy as _ @@ -32,17 +33,11 @@ def exception_handler(exc, context): if isinstance(exc, DjangoValidationError): exc = DRFValidationError(detail=serializers.as_serializer_error(exc)) - # Exceptions we will manually check for here - handled_exceptions = [ - IntegrityError, - OperationalError, - ProgrammingError, - ValueError, - TypeError, - NameError, - ] + # Default to the built-in DRF exception handler + response = drfviews.exception_handler(exc, context) - if any([isinstance(exc, err_type) for err_type in handled_exceptions]): + if response is None: + # DRF handler did not provide a default response for this exception if settings.DEBUG: error_detail = str(exc) @@ -59,18 +54,17 @@ def exception_handler(exc, context): response = Response(response_data, status=500) + # Format error traceback + trace = ''.join(traceback.format_exception(type(exc), exc, exc.__traceback__)) + # Log the exception to the database, too Error.objects.create( - kind="Unhandled DRF API Exception", + kind="Unhandled API Exception", info=str(type(exc)), - data=str(exc), + data=trace, path=context['request'].path, ) - else: - # Fallback to the default DRF exception handler - response = drfviews.exception_handler(exc, context) - if response is not None: # For an error response, include status code information response.data['status_code'] = response.status_code From 027a7c88de4644b3e6826e3be75605e6f9924f2e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 17 May 2022 01:17:48 +1000 Subject: [PATCH 09/10] Copy error implementation from django-error-report lib Ref: https://github.com/mhsiddiqui/django-error-report/blob/master/error_report/middleware.py --- InvenTree/InvenTree/exceptions.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/InvenTree/InvenTree/exceptions.py b/InvenTree/InvenTree/exceptions.py index 20d74fd4ac..8403cbf4c8 100644 --- a/InvenTree/InvenTree/exceptions.py +++ b/InvenTree/InvenTree/exceptions.py @@ -6,10 +6,12 @@ Custom exception handling for the DRF API from __future__ import unicode_literals import traceback +import sys from django.conf import settings from django.core.exceptions import ValidationError as DjangoValidationError from django.utils.translation import gettext_lazy as _ +from django.views.debug import ExceptionReporter from error_report.models import Error @@ -54,15 +56,15 @@ def exception_handler(exc, context): response = Response(response_data, status=500) - # Format error traceback - trace = ''.join(traceback.format_exception(type(exc), exc, exc.__traceback__)) - # Log the exception to the database, too + kind, info, data = sys.exc_info() + Error.objects.create( - kind="Unhandled API Exception", - info=str(type(exc)), - data=trace, + kind=kind.__name__, + info=info, + data='\n'.join(traceback.format_exception(kind, info, data)), path=context['request'].path, + html=ExceptionReporter(context['request'], kind, info, data).get_traceback_html(), ) if response is not None: From aa7fcb36012da7d45bbe0dcc2ff844c1ca5bd8bd Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 17 May 2022 08:23:39 +1000 Subject: [PATCH 10/10] Remove status_code addition --- InvenTree/InvenTree/exceptions.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/InvenTree/InvenTree/exceptions.py b/InvenTree/InvenTree/exceptions.py index 8403cbf4c8..46b1a1ee0a 100644 --- a/InvenTree/InvenTree/exceptions.py +++ b/InvenTree/InvenTree/exceptions.py @@ -68,9 +68,6 @@ def exception_handler(exc, context): ) if response is not None: - # For an error response, include status code information - response.data['status_code'] = response.status_code - # Convert errors returned under the label '__all__' to 'non_field_errors' if '__all__' in response.data: response.data['non_field_errors'] = response.data['__all__']