From 6a96bef3a09ca33b83c0d266ed082b89f472954c Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Mon, 29 Mar 2021 11:22:05 +0200 Subject: [PATCH 01/10] Fix horrible typo --- binder/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binder/views.py b/binder/views.py index e42dd221..e4c188fb 100644 --- a/binder/views.py +++ b/binder/views.py @@ -2052,7 +2052,7 @@ def _multi_put_deletions(self, deletions, new_id_map, request): def multi_put(self, request): - logger.info('ACTIVATING THE MULTI-PUT!!!1!') + logger.info('ACTIVATING THE MULTI-PUT!!!!!') # Hack to communicate to _store() that we're not interested in # the new data (for perf reasons). From fd3c0499c5ce1f7cd562c1fe6a3c528e82d92378 Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Mon, 29 Mar 2021 13:27:38 +0200 Subject: [PATCH 02/10] Add standalone validation feature --- binder/exceptions.py | 10 ++++++++++ binder/views.py | 37 ++++++++++++++++++++++++++++++++----- docs/api.md | 11 ++++++++++- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/binder/exceptions.py b/binder/exceptions.py index d5aec299..fbf9bd6f 100644 --- a/binder/exceptions.py +++ b/binder/exceptions.py @@ -235,3 +235,13 @@ def __add__(self, other): else: errors[model] = other.errors[model] return BinderValidationError(errors) + + +class BinderSkipSave(BinderException): + """Used to abort the database transaction when performing a non-save validation request.""" + http_code = 200 + code = 'SkipSave' + + def __init__(self): + super().__init__() + self.fields['message'] = 'No validation errors were encountered.' diff --git a/binder/views.py b/binder/views.py index e4c188fb..ad7086e0 100644 --- a/binder/views.py +++ b/binder/views.py @@ -25,7 +25,11 @@ from django.db.models.fields.reverse_related import ForeignObjectRel -from .exceptions import BinderException, BinderFieldTypeError, BinderFileSizeExceeded, BinderForbidden, BinderImageError, BinderImageSizeExceeded, BinderInvalidField, BinderIsDeleted, BinderIsNotDeleted, BinderMethodNotAllowed, BinderNotAuthenticated, BinderNotFound, BinderReadOnlyFieldError, BinderRequestError, BinderValidationError, BinderFileTypeIncorrect, BinderInvalidURI +from .exceptions import ( + BinderException, BinderFieldTypeError, BinderFileSizeExceeded, BinderForbidden, BinderImageError, BinderImageSizeExceeded, + BinderInvalidField, BinderIsDeleted, BinderIsNotDeleted, BinderMethodNotAllowed, BinderNotAuthenticated, BinderNotFound, + BinderReadOnlyFieldError, BinderRequestError, BinderValidationError, BinderFileTypeIncorrect, BinderInvalidURI, BinderSkipSave +) from . import history from .orderable_agg import OrderableArrayAgg, GroupConcat from .models import FieldFilter, BinderModel, ContextAnnotation, OptionalAnnotation, BinderFileField @@ -263,6 +267,9 @@ class ModelView(View): # NOTE: custom _store__foo() methods will still be called for unupdatable fields. unupdatable_fields = [] + # Allow validation without saving. + allow_standalone_validation = False + # Fields to use for ?search=foo. Empty tuple for disabled search. # NOTE: only string fields and 'id' are supported. # id is hardcoded to be treated as an integer. @@ -1349,6 +1356,17 @@ def binder_validation_error(self, obj, validation_error, pk=None): }) + + def _abort_when_standalone_validation(self, request): + """Raise a `BinderSkipSave` exception when this is a standalone request.""" + if self.allow_standalone_validation: + if 'validate' in request.GET: + raise BinderSkipSave + else: + raise BinderException('Standalone validation not enabled. You must enable this feature explicitly.') + + + # Deserialize JSON to Django Model objects. # obj: Model object to update (for PUT), newly created object (for POST) # values: Python dict of {field name: value} (parsed JSON) @@ -2060,13 +2078,15 @@ def multi_put(self, request): data, deletions = self._multi_put_parse_request(request) objects = self._multi_put_collect_objects(data) - objects, overrides = self._multi_put_override_superclass(objects) + objects, overrides = self._multi_put_override_superclass(objects) # model inheritance objects = self._multi_put_convert_backref_to_forwardref(objects) dependencies = self._multi_put_calculate_dependencies(objects) ordered_objects = self._multi_put_order_dependencies(dependencies) - new_id_map = self._multi_put_save_objects(ordered_objects, objects, request) - self._multi_put_id_map_add_overrides(new_id_map, overrides) - new_id_map = self._multi_put_deletions(deletions, new_id_map, request) + new_id_map = self._multi_put_save_objects(ordered_objects, objects, request) # may raise validation errors + self._multi_put_id_map_add_overrides(new_id_map, overrides) # model inheritance + new_id_map = self._multi_put_deletions(deletions, new_id_map, request) # may raise validation errors + + self._abort_when_standalone_validation(request) output = defaultdict(list) for (model, oid), nid in new_id_map.items(): @@ -2102,6 +2122,8 @@ def put(self, request, pk=None): data = self._store(obj, values, request) + self._abort_when_standalone_validation(request) + new = dict(data) new.pop('_meta', None) @@ -2132,6 +2154,8 @@ def post(self, request, pk=None): data = self._store(self.model(), values, request) + self._abort_when_standalone_validation(request) + new = dict(data) new.pop('_meta', None) @@ -2169,6 +2193,9 @@ def delete(self, request, pk=None, undelete=False, skip_body_check=False): raise BinderNotFound() self.delete_obj(obj, undelete, request) + + self._abort_when_standalone_validation(request) + logger.info('{}DELETEd {} #{}'.format('UN' if undelete else '', self._model_name(), pk)) return HttpResponse(status=204) # No content diff --git a/docs/api.md b/docs/api.md index 31b0cbf1..51ba97be 100644 --- a/docs/api.md +++ b/docs/api.md @@ -67,7 +67,7 @@ Ordering is a simple matter of enumerating the fields in the `order_by` query pa The default sort order is ascending. If you want to sort in descending order, simply prefix the attribute name with a minus sign. This honors the scoping, so `api/animal?order_by=-name,id` will sort by `name` in descending order and by `id` in ascending order. -### Saving a model +### Saving or updating a model Creating a new model is possible with `POST api/animal/`, and updating a model with `PUT api/animal/`. Both requests accept a JSON body, like this: @@ -161,6 +161,15 @@ If this request succeeds, you'll get back a mapping of the fake ids and the real It is also possible to update existing models with multi PUT. If you use a "real" id instead of a fake one, the model will be updated instead of created. + +#### Standalone Validation (without saving models) + +Sometimes you want to validate the model that you are going to save without actually saving it. This is useful, for example, when you want to inform the user of validation errors on the frontend, without having to implement the validation logic again. You may check for validation errors by sending a `POST`, `PUT` or `PATCH` request with an additional query parameter `validate`. + +Currently this is implemented by raising an `BinderValidateOnly` exception, which makes sure that the atomic database transaction is aborted. Ideally, you would only want to call the validation logic on the models, so only calling validation for fields and validation for model (`clean()`). But for now, we do it this way, at the cost of a performance penalty. + +It is important to realize that in this way, the normal `save()` function is called on a model, so it is possible that possible side effects are triggered, when these are implemented directly in `save()`, as opposed to in a signal method, which would be preferable. In other words, we cannot guarantee that the request will be idempotent. Therefore, the validation only feature is disabled by default and must be enabled by setting `allow_standalone_validation=True` on the view. + ### Uploading files To upload a file, you have to add it to the `file_fields` of the `ModelView`: From ea061e6d2f28121e6512f9ddb1a8b5b33a162c25 Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Mon, 29 Mar 2021 14:17:05 +0200 Subject: [PATCH 03/10] Correctly extract querystring parameter for PUT --- binder/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/binder/views.py b/binder/views.py index ad7086e0..e7cc5c6d 100644 --- a/binder/views.py +++ b/binder/views.py @@ -15,7 +15,7 @@ from django.views.generic import View from django.core.exceptions import ObjectDoesNotExist, FieldError, ValidationError, FieldDoesNotExist from django.http import HttpResponse, StreamingHttpResponse, HttpResponseForbidden -from django.http.request import RawPostDataException +from django.http.request import RawPostDataException, QueryDict from django.db import models, connections from django.db.models import Q, F from django.db.models.lookups import Transform @@ -1360,7 +1360,8 @@ def binder_validation_error(self, obj, validation_error, pk=None): def _abort_when_standalone_validation(self, request): """Raise a `BinderSkipSave` exception when this is a standalone request.""" if self.allow_standalone_validation: - if 'validate' in request.GET: + params = QueryDict(request.body) + if 'validate' in params: raise BinderSkipSave else: raise BinderException('Standalone validation not enabled. You must enable this feature explicitly.') From 796d32320276bcb1b8660af078eb916037944618 Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Mon, 29 Mar 2021 14:42:13 +0200 Subject: [PATCH 04/10] Raise the correct exception when flag is not set --- binder/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binder/views.py b/binder/views.py index e7cc5c6d..63367893 100644 --- a/binder/views.py +++ b/binder/views.py @@ -1364,7 +1364,7 @@ def _abort_when_standalone_validation(self, request): if 'validate' in params: raise BinderSkipSave else: - raise BinderException('Standalone validation not enabled. You must enable this feature explicitly.') + raise BinderRequestError('Standalone validation not enabled. You must enable this feature explicitly.') From dccd770dfb29fe43c58ecbfed078225e1bbd9e79 Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Fri, 25 Jun 2021 12:11:54 +0200 Subject: [PATCH 05/10] Improve comment --- binder/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binder/exceptions.py b/binder/exceptions.py index fbf9bd6f..2844f488 100644 --- a/binder/exceptions.py +++ b/binder/exceptions.py @@ -238,7 +238,7 @@ def __add__(self, other): class BinderSkipSave(BinderException): - """Used to abort the database transaction when performing a non-save validation request.""" + """Used to abort the database transaction when non-save validation was successfull.""" http_code = 200 code = 'SkipSave' From 42808415227a65713c911860a6a6c1f209643e81 Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Mon, 29 Mar 2021 14:52:39 +0200 Subject: [PATCH 06/10] Fix stupid logic --- binder/views.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/binder/views.py b/binder/views.py index 63367893..1bd57ab7 100644 --- a/binder/views.py +++ b/binder/views.py @@ -1359,12 +1359,12 @@ def binder_validation_error(self, obj, validation_error, pk=None): def _abort_when_standalone_validation(self, request): """Raise a `BinderSkipSave` exception when this is a standalone request.""" - if self.allow_standalone_validation: - params = QueryDict(request.body) - if 'validate' in params: + if 'validate' in params: + if self.allow_standalone_validation: + params = QueryDict(request.body) raise BinderSkipSave - else: - raise BinderRequestError('Standalone validation not enabled. You must enable this feature explicitly.') + else: + raise BinderRequestError('Standalone validation not enabled. You must enable this feature explicitly.') From b83003cb74da240740d63eae3293b44668c3baaa Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Fri, 25 Jun 2021 14:53:22 +0200 Subject: [PATCH 07/10] Stop earlier when flag not set --- binder/views.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/binder/views.py b/binder/views.py index 599eebec..5df3ceaf 100644 --- a/binder/views.py +++ b/binder/views.py @@ -378,6 +378,10 @@ def dispatch(self, request, *args, **kwargs): response = None try: + # only allow standalone validation if you know what you are doing + if 'validate' in request.GET and request.GET['validate'] == 'true' and not self.allow_standalone_validation: + raise BinderRequestError('Standalone validation not enabled. You must enable this feature explicitly.') + #### START TRANSACTION with ExitStack() as stack, history.atomic(source='http', user=request.user, uuid=request.request_id): transaction_dbs = ['default'] @@ -1375,11 +1379,12 @@ def binder_validation_error(self, obj, validation_error, pk=None): def _abort_when_standalone_validation(self, request): """Raise a `BinderSkipSave` exception when this is a standalone request.""" - if 'validate' in params: + if 'validate' in request.GET and request.GET['validate'] == 'true': if self.allow_standalone_validation: params = QueryDict(request.body) raise BinderSkipSave else: + print('validate not enabled') raise BinderRequestError('Standalone validation not enabled. You must enable this feature explicitly.') @@ -2142,6 +2147,9 @@ def put(self, request, pk=None): if hasattr(obj, 'deleted') and obj.deleted: raise BinderIsDeleted() + + logger.info('storing') + data = self._store(obj, values, request) self._abort_when_standalone_validation(request) From 31a61bfcb67d7b1aaf1c60ff34a2c6c246f3a8a3 Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Wed, 30 Jun 2021 14:26:01 +0200 Subject: [PATCH 08/10] Add tests for validation flow. We check if the action is not performed, and thus aborted internally by a BinderSkipSave exception, for POST, PUT, MULTI-PUT and DELETE. --- tests/test_model_validation.py | 228 ++++++++++++++++++++++++++ tests/testapp/models/animal.py | 2 +- tests/testapp/views/caretaker.py | 3 + tests/testapp/views/contact_person.py | 3 + 4 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 tests/test_model_validation.py diff --git a/tests/test_model_validation.py b/tests/test_model_validation.py new file mode 100644 index 00000000..59dac762 --- /dev/null +++ b/tests/test_model_validation.py @@ -0,0 +1,228 @@ +from re import I +from tests.testapp.models import contact_person +from tests.testapp.models.contact_person import ContactPerson +from django.test import TestCase, Client + +import json +from binder.json import jsonloads +from django.contrib.auth.models import User +from .testapp.models import Animal, Caretaker, ContactPerson + + +class TestModelValidation(TestCase): + """ + Test the validate-only functionality. + + We check that the validation is executed as normal, but that the models + are not created when the validate query paramter is set to true. + + We check validation for: + - post + - put + - multi-put + - delete + """ + + + def setUp(self): + super().setUp() + u = User(username='testuser', is_active=True, is_superuser=True) + u.set_password('test') + u.save() + self.client = Client() + r = self.client.login(username='testuser', password='test') + self.assertTrue(r) + + # some update payload + self.model_data_with_error = { + 'name': 'very_special_forbidden_contact_person_name', # see `contact_person.py` + } + self.model_data = { + 'name': 'Scooooooby', + } + + + ### helpers ### + + + def assert_validation_error(self, response, person_id=None): + if person_id is None: + person_id = 'null' # for post + + self.assertEqual(response.status_code, 400) + + returned_data = jsonloads(response.content) + + # check that there were validation errors + self.assertEqual(returned_data.get('code'), 'ValidationError') + + # check that the validation error is present + validation_error = returned_data.get('errors').get('contact_person').get(str(person_id)).get('__all__')[0] + self.assertEqual(validation_error.get('code'), 'invalid') + self.assertEqual(validation_error.get('message'), 'Very special validation check that we need in `tests.M2MStoreErrorsTest`.') + + + def assert_multi_put_validation_error(self, response): + self.assertEqual(response.status_code, 400) + + returned_data = jsonloads(response.content) + + # check that there were validation errors + self.assertEqual(returned_data.get('code'), 'ValidationError') + + # check that all (two) the validation errors are present + for error in returned_data.get('errors').get('contact_person').values(): + validation_error = error.get('__all__')[0] + self.assertEqual(validation_error.get('code'), 'invalid') + self.assertEqual(validation_error.get('message'), 'Very special validation check that we need in `tests.M2MStoreErrorsTest`.') + + + ### tests ### + + + def assert_no_validation_error(self, response): + self.assertEqual(response.status_code, 200) + + # check that the validation was successful + returned_data = jsonloads(response.content) + self.assertEqual(returned_data.get('code'), 'SkipSave') + self.assertEqual(returned_data.get('message'), 'No validation errors were encountered.') + + + def test_validate_on_post(self): + self.assertEqual(0, ContactPerson.objects.count()) + + # trigger a validation error + response = self.client.post('/contact_person/?validate=true', data=json.dumps(self.model_data_with_error), content_type='application/json') + self.assert_validation_error(response) + self.assertEqual(0, ContactPerson.objects.count()) + + # now without validation errors + response = self.client.post('/contact_person/?validate=true', data=json.dumps(self.model_data), content_type='application/json') + self.assert_no_validation_error(response) + self.assertEqual(0, ContactPerson.objects.count()) + + # now for real + response = self.client.post('/contact_person/', data=json.dumps(self.model_data), content_type='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual('Scooooooby', ContactPerson.objects.first().name) + + + def test_validate_on_put(self): + person_id = ContactPerson.objects.create(name='Scooby Doo').id + self.assertEqual('Scooby Doo', ContactPerson.objects.first().name) + + # trigger a validation error + response = self.client.put(f'/contact_person/{person_id}/?validate=true', data=json.dumps(self.model_data_with_error), content_type='application/json') + self.assert_validation_error(response, person_id) + self.assertEqual('Scooby Doo', ContactPerson.objects.first().name) + + # now without validation errors + response = self.client.put(f'/contact_person/{person_id}/?validate=true', data=json.dumps(self.model_data), content_type='application/json') + self.assert_no_validation_error(response) + self.assertEqual('Scooby Doo', ContactPerson.objects.first().name) + + # now for real + response = self.client.put(f'/contact_person/{person_id}/', data=json.dumps(self.model_data), content_type='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual('Scooooooby', ContactPerson.objects.first().name) + + + def test_validate_on_multiput(self): + person_1_id = ContactPerson.objects.create(name='Scooby Doo 1').id + person_2_id = ContactPerson.objects.create(name='Scooby Doo 2').id + + multi_put_data = {'data': [ + { + 'id': person_1_id, + 'name': 'New Scooby', + }, + { + 'id': person_2_id, + 'name': 'New Doo' + } + ]} + + multi_put_data_with_error = {'data': [ + { + 'id': person_1_id, + 'name': 'very_special_forbidden_contact_person_name', + }, + { + 'id': person_2_id, + 'name': 'very_special_forbidden_contact_person_name' + } + ]} + + # trigger a validation error + response = self.client.put(f'/contact_person/?validate=true', data=json.dumps(multi_put_data_with_error), content_type='application/json') + self.assert_multi_put_validation_error(response) + self.assertEqual('Scooby Doo 1', ContactPerson.objects.get(id=person_1_id).name) + self.assertEqual('Scooby Doo 2', ContactPerson.objects.get(id=person_2_id).name) + + + # now without validation error + response = self.client.put(f'/contact_person/?validate=true', data=json.dumps(multi_put_data), content_type='application/json') + self.assert_no_validation_error(response) + self.assertEqual('Scooby Doo 1', ContactPerson.objects.get(id=person_1_id).name) + self.assertEqual('Scooby Doo 2', ContactPerson.objects.get(id=person_2_id).name) + + # now for real + response = self.client.put(f'/contact_person/', data=json.dumps(multi_put_data), content_type='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual('New Scooby', ContactPerson.objects.get(id=person_1_id).name) + self.assertEqual('New Doo', ContactPerson.objects.get(id=person_2_id).name) + + + def test_validate_on_delete(self): + '''Check if deletion is cancelled when we only attempt to validate + the delete operation. This test only covers validation of the + on_delete=PROTECT constraint of a fk.''' + + def is_deleted(obj): + '''Whether the obj was soft-deleted, so when the 'deleted' + attribute is not present, or when it is True.''' + + try: + obj.refresh_from_db() + except obj.DoesNotExist: + return True # hard-deleted + return animal.__dict__.get('deleted') or False + + + # animal has a fk to caretaker with on_delete=PROTECT + caretaker = Caretaker.objects.create(name='Connie Care') + animal = Animal.objects.create(name='Pony', caretaker=caretaker) + + + ### with validation error + + response = self.client.delete(f'/caretaker/{caretaker.id}/?validate=true') + # assert validation error + # and check that it was about the PROTECTED constraint + self.assertEqual(response.status_code, 400) + returned_data = jsonloads(response.content) + self.assertEqual(returned_data.get('code'), 'ValidationError') + self.assertEqual(returned_data.get('errors').get('caretaker').get(str(caretaker.id)).get('id')[0].get('code'), 'protected') + + self.assertFalse(is_deleted(caretaker)) + + + ### without validation error + + # now we delete the animal to make sure that deletion is possible + # note that soft-deleting will of course not remove the validation error + animal.delete() + + # now no validation error should be trown + response = self.client.delete(f'/caretaker/{caretaker.id}/?validate=true') + print(response.content) + self.assert_no_validation_error(response) + + self.assertFalse(is_deleted(caretaker)) + + + ### now for real + + response = self.client.delete(f'/caretaker/{caretaker.id}/') + self.assertTrue(is_deleted(caretaker)) diff --git a/tests/testapp/models/animal.py b/tests/testapp/models/animal.py index 85d844e0..49d48987 100644 --- a/tests/testapp/models/animal.py +++ b/tests/testapp/models/animal.py @@ -14,7 +14,7 @@ class Animal(LoadedValuesMixin, BinderModel): name = models.TextField(max_length=64) zoo = models.ForeignKey('Zoo', on_delete=models.CASCADE, related_name='animals', blank=True, null=True) zoo_of_birth = models.ForeignKey('Zoo', on_delete=models.CASCADE, related_name='+', blank=True, null=True) # might've been born outside captivity - caretaker = models.ForeignKey('Caretaker', on_delete=models.PROTECT, related_name='animals', blank=True, null=True) + caretaker = models.ForeignKey('Caretaker', on_delete=models.PROTECT, related_name='animals', blank=True, null=True) # we use the fact that this one is PROTECT in `test_model_validation.py` deleted = models.BooleanField(default=False) # Softdelete def __str__(self): diff --git a/tests/testapp/views/caretaker.py b/tests/testapp/views/caretaker.py index b74006b4..0db69683 100644 --- a/tests/testapp/views/caretaker.py +++ b/tests/testapp/views/caretaker.py @@ -7,3 +7,6 @@ class CaretakerView(ModelView): unwritable_fields = ['last_seen'] unupdatable_fields = ['first_seen'] model = Caretaker + + # see `test_model_validation.py` + allow_standalone_validation = True diff --git a/tests/testapp/views/contact_person.py b/tests/testapp/views/contact_person.py index c1e90c5b..bc2f25fe 100644 --- a/tests/testapp/views/contact_person.py +++ b/tests/testapp/views/contact_person.py @@ -6,3 +6,6 @@ class ContactPersonView(ModelView): model = ContactPerson m2m_fields = ['zoos'] unwritable_fields = ['created_at', 'updated_at'] + + # see `test_model_validation.py` + allow_standalone_validation = True From 9be35a226e4f296522951e45b05272ad2f4aab1d Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Wed, 30 Jun 2021 14:30:59 +0200 Subject: [PATCH 09/10] Some cleanup --- binder/exceptions.py | 4 +++- binder/views.py | 6 +----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/binder/exceptions.py b/binder/exceptions.py index 2844f488..75e4e572 100644 --- a/binder/exceptions.py +++ b/binder/exceptions.py @@ -238,7 +238,9 @@ def __add__(self, other): class BinderSkipSave(BinderException): - """Used to abort the database transaction when non-save validation was successfull.""" + """Used to abort the database transaction when validation was successfull. + Validation is possible when saving (post, put, multi-put) or deleting models.""" + http_code = 200 code = 'SkipSave' diff --git a/binder/views.py b/binder/views.py index 5df3ceaf..8d486cb8 100644 --- a/binder/views.py +++ b/binder/views.py @@ -1378,13 +1378,12 @@ def binder_validation_error(self, obj, validation_error, pk=None): def _abort_when_standalone_validation(self, request): - """Raise a `BinderSkipSave` exception when this is a standalone request.""" + """Raise a `BinderSkipSave` exception when this is a validation request.""" if 'validate' in request.GET and request.GET['validate'] == 'true': if self.allow_standalone_validation: params = QueryDict(request.body) raise BinderSkipSave else: - print('validate not enabled') raise BinderRequestError('Standalone validation not enabled. You must enable this feature explicitly.') @@ -2147,9 +2146,6 @@ def put(self, request, pk=None): if hasattr(obj, 'deleted') and obj.deleted: raise BinderIsDeleted() - - logger.info('storing') - data = self._store(obj, values, request) self._abort_when_standalone_validation(request) From d36367096af722982b243081095f8de69d3653b7 Mon Sep 17 00:00:00 2001 From: Jeroen van Riel Date: Thu, 1 Jul 2021 14:15:00 +0200 Subject: [PATCH 10/10] Add a comment in the exception --- binder/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/binder/views.py b/binder/views.py index 8d486cb8..b7414e6d 100644 --- a/binder/views.py +++ b/binder/views.py @@ -1384,7 +1384,8 @@ def _abort_when_standalone_validation(self, request): params = QueryDict(request.body) raise BinderSkipSave else: - raise BinderRequestError('Standalone validation not enabled. You must enable this feature explicitly.') + raise BinderRequestError('Standalone validation not enabled. You must enable this feature explicitly ' \ + 'by setting the `allow_standalone_validation` property on this view (see documentation).')