From de656023d95ca94243c4b101affd1aa2e44ef1d7 Mon Sep 17 00:00:00 2001 From: Erik Belak <32141582+Serbel97@users.noreply.github.com> Date: Thu, 8 Sep 2022 17:51:31 +0200 Subject: [PATCH] =?UTF-8?q?Fixed=20missing=20validation=20errors=20(#42)?= =?UTF-8?q?=20=F0=9F=91=A8=F0=9F=8F=BD=E2=80=8D=F0=9F=8F=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * DictionaryField exception handling * Fixed missing validation errors * Added valid nested forms test * Fixed flake8 codestyle * Removed str option from Form.add_error() argument --- CHANGELOG.md | 5 + django_api_forms/exceptions.py | 10 +- django_api_forms/fields.py | 2 +- django_api_forms/forms.py | 21 ++-- docs/tutorial.md | 6 +- poetry.lock | 114 ++++++++--------- tests/data/invalid_concert.json | 107 ++++++++++++++++ tests/data/valid_concert.json | 100 +++++++++++++++ tests/test_nested_forms.py | 211 ++++++++++++++++++++++++++++++++ tests/testapp/forms.py | 21 +++- 10 files changed, 527 insertions(+), 70 deletions(-) create mode 100644 tests/data/invalid_concert.json create mode 100644 tests/data/valid_concert.json create mode 100644 tests/test_nested_forms.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d3ad665..bf5ca4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 1.0.0-rc.4 : 06.09.2022 + +- **Fixed**: Fixed missing validation errors +- **Changed**: `Form.add_error()` now takes only `Tuple` as a `field` argument + ## 1.0.0-rc.3 : 04.09.2022 - **Fixed**: Removed validation of non-required fields if they are not present in the request diff --git a/django_api_forms/exceptions.py b/django_api_forms/exceptions.py index d981854..0fcafa6 100644 --- a/django_api_forms/exceptions.py +++ b/django_api_forms/exceptions.py @@ -15,15 +15,19 @@ class UnsupportedMediaType(ApiFormException): class DetailValidationError(ValidationError): def __init__(self, error: ValidationError, path: Tuple): - super().__init__(error.message, error.code, error.params) + if not hasattr(error, 'message') and isinstance(error.error_list, list): + for item in error.error_list: + item.path = path + + super().__init__(error) self._path = path @property def path(self) -> Tuple: return self._path - def prepend(self, key: str): - self._path = (key, ) + self._path + def prepend(self, key: Tuple): + self._path = key + self._path def to_list(self) -> list: return list(self.path) diff --git a/django_api_forms/fields.py b/django_api_forms/fields.py index 03544e7..2fcdeee 100644 --- a/django_api_forms/fields.py +++ b/django_api_forms/fields.py @@ -142,7 +142,7 @@ def to_python(self, value): result.append(form.cleaned_data) else: for error in form.errors: - error.prepend(position) + error.prepend((position, )) errors.append(error) if errors: diff --git a/django_api_forms/forms.py b/django_api_forms/forms.py index 20727e4..4442215 100644 --- a/django_api_forms/forms.py +++ b/django_api_forms/forms.py @@ -1,5 +1,5 @@ import copy -from typing import Union, List, Tuple +from typing import List, Tuple from django.core.exceptions import ValidationError from django.forms import fields_for_model @@ -95,18 +95,25 @@ def errors(self) -> dict: def is_valid(self) -> bool: return not self.errors - def add_error(self, field: Union[str, Tuple], errors: ValidationError): + def add_error(self, field: Tuple, errors: ValidationError): if hasattr(errors, 'error_dict'): - for item in errors.error_dict.values(): - for error in item: + for key, items in errors.error_dict.items(): + for error in items: if isinstance(error, DetailValidationError): error.prepend(field) self.add_error(error.path, error) + elif isinstance(error, ValidationError): + self.add_error(field + (key, ), error) elif not hasattr(errors, 'message') and isinstance(errors.error_list, list): for item in errors.error_list: if isinstance(item, DetailValidationError): item.prepend(field) self.add_error(item.path, item) + elif isinstance(item, ValidationError): + path = field + if hasattr(item, 'path'): + path = field + item.path + self.add_error(path, item) else: self._errors.append( DetailValidationError(errors, (field,) if isinstance(field, str) else field) @@ -132,13 +139,13 @@ def full_clean(self): if hasattr(self, f"clean_{key}"): self.cleaned_data[key] = getattr(self, f"clean_{key}")() except ValidationError as e: - self.add_error(key, e) + self.add_error((key, ), e) except (AttributeError, TypeError, ValueError): - self.add_error(key, ValidationError(_("Invalid value"))) + self.add_error((key, ), ValidationError(_("Invalid value"))) try: cleaned_data = self.clean() except ValidationError as e: - self.add_error('$body', e) + self.add_error(('$body', ), e) else: if cleaned_data is not None: self.cleaned_data = cleaned_data diff --git a/docs/tutorial.md b/docs/tutorial.md index 1c1eec2..e939791 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -81,7 +81,7 @@ This process is much more simple than in classic Django form. It consists of: 1. Iterating over form attributes: - calling `Field.clean(value)` method - calling `Form.clean_` method - - calling `Form.add_error(field_name, error)` in case of failures in clean methods + - calling `Form.add_error((field_name, ), error)` in case of failures in clean methods - if field is marked as dirty, normalized attribute is saved to `Form.clean_data` property 2. Calling `Form.clean` method which returns final normalized values which will be presented in `Form.clean_data` (feel free to override it, by default does nothing, useful for conditional validation, you can still add errors u @@ -95,7 +95,7 @@ Validation errors are presented for each field in `Form.errors: List[ValidationE As was mentioned above, you can extend property validation or normalisation by creating form method like `clean_`. You can add additional [ValidationError](https://docs.djangoproject.com/en/3.1/ref/forms/validation/#raising-validationerror) -objects using `Form.add_error(field: Union[str, Tuple], error: ValidationError)` method. Result is final normalised +objects using `Form.add_error(field: Tuple, error: ValidationError)` method. Result is final normalised value of the attribute. ```python @@ -109,7 +109,7 @@ class BookForm(Form): def clean_title(self): if self.cleaned_data['title'] == "The Hitchhiker's Guide to the Galaxy": - self.add_error('title', ValidationError("Too cool!", code='too-cool')) + self.add_error(('title', ), ValidationError("Too cool!", code='too-cool')) return self.cleaned_data['title'].upper() def clean(self): diff --git a/poetry.lock b/poetry.lock index 4ad685e..0673487 100644 --- a/poetry.lock +++ b/poetry.lock @@ -7,7 +7,7 @@ optional = false python-versions = ">=3.7" [package.extras] -tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] +tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] [[package]] name = "backports.zoneinfo" @@ -34,7 +34,7 @@ webencodings = "*" [package.extras] css = ["tinycss2 (>=1.1.0,<1.2)"] -dev = ["Sphinx (==4.3.2)", "black (==22.3.0)", "build (==0.8.0)", "flake8 (==4.0.1)", "hashin (==0.17.0)", "mypy (==0.961)", "pip-tools (==6.6.2)", "pytest (==7.1.2)", "tox (==3.25.0)", "twine (==4.0.1)", "wheel (==0.37.1)"] +dev = ["build (==0.8.0)", "flake8 (==4.0.1)", "hashin (==0.17.0)", "pip-tools (==6.6.2)", "pytest (==7.1.2)", "Sphinx (==4.3.2)", "tox (==3.25.0)", "twine (==4.0.1)", "wheel (==0.37.1)", "black (==22.3.0)", "mypy (==0.961)"] [[package]] name = "certifi" @@ -94,7 +94,7 @@ optional = false python-versions = "*" [package.extras] -test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] +test = ["hypothesis (==3.55.3)", "flake8 (==3.7.8)"] [[package]] name = "coverage" @@ -112,7 +112,7 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "37.0.4" +version = "38.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "dev" optional = false @@ -123,15 +123,15 @@ cffi = ">=1.12" [package.extras] docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] -sdist = ["setuptools_rust (>=0.11.4)"] +sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] +test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] [[package]] name = "django" -version = "4.1" +version = "4.1.1" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." category = "main" optional = false @@ -180,7 +180,7 @@ python-versions = "*" python-dateutil = ">=2.8.1" [package.extras] -dev = ["flake8", "markdown", "twine", "wheel"] +dev = ["wheel", "flake8", "markdown", "twine"] [[package]] name = "idna" @@ -202,9 +202,9 @@ python-versions = ">=3.7" zipp = ">=0.5" [package.extras] -docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] [[package]] name = "jaraco.classes" @@ -218,8 +218,8 @@ python-versions = ">=3.7" more-itertools = "*" [package.extras] -docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] -testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] [[package]] name = "jeepney" @@ -230,8 +230,8 @@ optional = false python-versions = ">=3.7" [package.extras] -test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] -trio = ["async-generator", "trio"] +test = ["pytest", "pytest-trio", "pytest-asyncio (>=0.17)", "testpath", "trio", "async-timeout"] +trio = ["trio", "async-generator"] [[package]] name = "jinja2" @@ -249,7 +249,7 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "keyring" -version = "23.9.0" +version = "23.9.1" description = "Store and access your passwords safely." category = "dev" optional = false @@ -263,8 +263,8 @@ pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_ SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [package.extras] -docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] -testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] [[package]] name = "markdown" @@ -329,7 +329,7 @@ i18n = ["babel (>=2.9.0)"] [[package]] name = "mkdocs-material" -version = "8.4.2" +version = "8.4.3" description = "Documentation that simply works" category = "dev" optional = false @@ -399,7 +399,7 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.extras] -testing = ["coverage", "nose"] +testing = ["nose", "coverage"] [[package]] name = "pycodestyle" @@ -456,7 +456,7 @@ optional = false python-versions = ">=3.6.8" [package.extras] -diagrams = ["jinja2", "railroad-diagrams"] +diagrams = ["railroad-diagrams", "jinja2"] [[package]] name = "python-dateutil" @@ -656,8 +656,8 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] @@ -688,8 +688,8 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] -testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "1.1" @@ -862,32 +862,36 @@ coverage = [ {file = "coverage-6.4.4.tar.gz", hash = "sha256:e16c45b726acb780e1e6f88b286d3c10b3914ab03438f32117c4aa52d7f30d58"}, ] cryptography = [ - {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"}, - {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280"}, - {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3"}, - {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59"}, - {file = "cryptography-37.0.4-cp36-abi3-win32.whl", hash = "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157"}, - {file = "cryptography-37.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327"}, - {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b"}, - {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"}, - {file = "cryptography-37.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67"}, - {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d"}, - {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282"}, - {file = "cryptography-37.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa"}, - {file = "cryptography-37.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441"}, - {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596"}, - {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a"}, - {file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"}, - {file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"}, + {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f"}, + {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd"}, + {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6"}, + {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a"}, + {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294"}, + {file = "cryptography-38.0.1-cp36-abi3-win32.whl", hash = "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0"}, + {file = "cryptography-38.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a"}, + {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d"}, + {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9"}, + {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d"}, + {file = "cryptography-38.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818"}, + {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6"}, + {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750"}, + {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013"}, + {file = "cryptography-38.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5"}, + {file = "cryptography-38.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61"}, + {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac"}, + {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb"}, + {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a"}, + {file = "cryptography-38.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b"}, + {file = "cryptography-38.0.1.tar.gz", hash = "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7"}, ] django = [ - {file = "Django-4.1-py3-none-any.whl", hash = "sha256:031ccb717782f6af83a0063a1957686e87cb4581ea61b47b3e9addf60687989a"}, - {file = "Django-4.1.tar.gz", hash = "sha256:032f8a6fc7cf05ccd1214e4a2e21dfcd6a23b9d575c6573cacc8c67828dbe642"}, + {file = "Django-4.1.1-py3-none-any.whl", hash = "sha256:acb21fac9275f9972d81c7caf5761a89ec3ea25fe74545dd26b8a48cb3a0203e"}, + {file = "Django-4.1.1.tar.gz", hash = "sha256:a153ffd5143bf26a877bfae2f4ec736ebd8924a46600ca089ad96b54a1d4e28e"}, ] docutils = [ {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, @@ -922,8 +926,8 @@ jinja2 = [ {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] keyring = [ - {file = "keyring-23.9.0-py3-none-any.whl", hash = "sha256:98f060ec95ada2ab910c195a2d4317be6ef87936a766b239c46aa3c7aac4f0db"}, - {file = "keyring-23.9.0.tar.gz", hash = "sha256:4c32a31174faaee48f43a7e2c7e9c3216ec5e95acf22a2bebfb4a1d05056ee44"}, + {file = "keyring-23.9.1-py3-none-any.whl", hash = "sha256:3565b9e4ea004c96e158d2d332a49f466733d565bb24157a60fd2e49f41a0fd1"}, + {file = "keyring-23.9.1.tar.gz", hash = "sha256:39e4f6572238d2615a82fcaa485e608b84b503cf080dc924c43bbbacb11c1c18"}, ] markdown = [ {file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"}, @@ -984,8 +988,8 @@ mkdocs = [ {file = "mkdocs-1.3.1.tar.gz", hash = "sha256:a41a2ff25ce3bbacc953f9844ba07d106233cd76c88bac1f59cb1564ac0d87ed"}, ] mkdocs-material = [ - {file = "mkdocs-material-8.4.2.tar.gz", hash = "sha256:704c64c3fff126a3923c2961d95f26b19be621342a6a4e49ed039f0bb7a5c540"}, - {file = "mkdocs_material-8.4.2-py2.py3-none-any.whl", hash = "sha256:166287bb0e4197804906bf0389a852d5ced43182c30127ac8b48a4e497ecd7e5"}, + {file = "mkdocs-material-8.4.3.tar.gz", hash = "sha256:f39af3234ce0b60024b7712995af0de5b5227ab6504f0b9c8709c9a770bd94bf"}, + {file = "mkdocs_material-8.4.3-py2.py3-none-any.whl", hash = "sha256:d5cc6f5023061a663514f61810052ad266f5199feafcf15ad23ea4891b21e6bc"}, ] mkdocs-material-extensions = [ {file = "mkdocs-material-extensions-1.0.3.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"}, @@ -1064,8 +1068,8 @@ pillow = [ {file = "Pillow-9.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e"}, {file = "Pillow-9.2.0-cp310-cp310-win32.whl", hash = "sha256:c79698d4cd9318d9481d89a77e2d3fcaeff5486be641e60a4b49f3d2ecca4e28"}, {file = "Pillow-9.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:254164c57bab4b459f14c64e93df11eff5ded575192c294a0c49270f22c5d93d"}, - {file = "Pillow-9.2.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:408673ed75594933714482501fe97e055a42996087eeca7e5d06e33218d05aa8"}, - {file = "Pillow-9.2.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:727dd1389bc5cb9827cbd1f9d40d2c2a1a0c9b32dd2261db522d22a604a6eec9"}, + {file = "Pillow-9.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:adabc0bce035467fb537ef3e5e74f2847c8af217ee0be0455d4fec8adc0462fc"}, + {file = "Pillow-9.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:336b9036127eab855beec9662ac3ea13a4544a523ae273cbf108b228ecac8437"}, {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50dff9cc21826d2977ef2d2a205504034e3a4563ca6f5db739b0d1026658e004"}, {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb6259196a589123d755380b65127ddc60f4c64b21fc3bb46ce3a6ea663659b0"}, {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0554af24df2bf96618dac71ddada02420f946be943b181108cac55a7a2dcd4"}, diff --git a/tests/data/invalid_concert.json b/tests/data/invalid_concert.json new file mode 100644 index 0000000..124fb3d --- /dev/null +++ b/tests/data/invalid_concert.json @@ -0,0 +1,107 @@ +{ + "place": "Bratislava", + "emails": [ + "not valid email", + "valid@email.com", + "v@lid.com" + ], + "organizer_id": 1, + "bands":[ + { + "name": "Queen", + "formed": 1970, + "has_award": false, + "emails": { + "0": "not valid email", + "1": "valid@email.com", + "2": "v@lid.com" + }, + "albums": [ + { + "title": "Unknown Pleasures", + "year": 1979, + "type": "vinyl", + "artist": { + "name": "Joy Division", + "genres": [ + "rock", + "punk" + ], + "members": 4 + }, + "songs": [ + { + "title": "Disorder", + "duration": "3:29" + }, + { + "title": "Day of the Lords", + "duration": "4:48", + "metadata": { + "_section": { + "type": "ID3v2", + "offset": 0, + "byteLength": 2048 + } + } + } + ], + "metadata": { + "created_at": "2019-10-21T18:57:03+0100", + "updated_at": "2019-10-21T18:57:03+0100" + } + } + ] + }, + { + "name": "The Beatles", + "formed": 1960, + "has_award": true, + "albums": [ + { + "title": "Unknown Pleasures", + "type": "vinyl", + "artist": { + "name": "Nirvana", + "genres": [ + "rock", + "punk" + ], + "members": 4 + }, + "year": 1998, + "songs": [ + { + "metadata": { + "_section": { + "type": "ID3v2", + "offset": 0, + "byteLength": 2048 + } + } + }, + { + "duration": "3:29" + }, + { + "title": "Day of the Lords", + "duration": "4:48", + "metadata": { + "_section": { + "type": "ID3v2", + "offset": 0, + "byteLength": 2048 + } + } + } + ], + "metadata": { + "created_at": "2019-10-21T18:57:03+0100", + "updated_at": "2019-10-21T18:57:03+0100", + "error_at": "blah" + } + } + ] + } + ] +} diff --git a/tests/data/valid_concert.json b/tests/data/valid_concert.json new file mode 100644 index 0000000..fb4ba3e --- /dev/null +++ b/tests/data/valid_concert.json @@ -0,0 +1,100 @@ +{ + "place": "Bratislava", + "emails": [ + "em@il.com", + "v@lid.com" + ], + "organizer_id": 1, + "bands":[ + { + "name": "Queen", + "formed": 1970, + "has_award": false, + "emails": { + "0": "em@il.com", + "1": "v@lid.com" + }, + "albums": [ + { + "title": "Unknown Pleasures", + "year": 1979, + "type": "vinyl", + "artist": { + "name": "Joy Division", + "genres": [ + "rock", + "punk" + ], + "members": 4 + }, + "songs": [ + { + "title": "Disorder", + "duration": "3:29" + }, + { + "title": "Day of the Lords", + "duration": "4:48", + "metadata": { + "_section": { + "type": "ID3v2", + "offset": 0, + "byteLength": 2048 + } + } + } + ], + "metadata": { + "created_at": "2019-10-21T18:57:03+0100", + "updated_at": "2019-10-21T18:57:03+0100" + } + } + ] + }, + { + "name": "The Beatles", + "formed": 1960, + "has_award": true, + "albums": [ + { + "title": "Unknown Pleasures", + "type": "vinyl", + "artist": { + "name": "Nirvana", + "genres": [ + "rock", + "punk" + ], + "members": 4 + }, + "year": 1997, + "songs": [ + { + "title": "Hey jude", + "duration": "2:20" + }, + { + "title": "Let it be", + "duration": "2:51" + }, + { + "title": "Day of the Lords", + "duration": "4:48", + "metadata": { + "_section": { + "type": "ID3v2", + "offset": 0, + "byteLength": 2048 + } + } + } + ], + "metadata": { + "created_at": "2019-10-21T18:57:03+0100", + "updated_at": "2019-10-21T18:57:03+0100" + } + } + ] + } + ] +} diff --git a/tests/test_nested_forms.py b/tests/test_nested_forms.py new file mode 100644 index 0000000..16b7f92 --- /dev/null +++ b/tests/test_nested_forms.py @@ -0,0 +1,211 @@ +import datetime + +from django.test import TestCase, RequestFactory +from django.conf import settings + +from tests.testapp.forms import ConcertForm +from tests.testapp.models import Artist, Album + + +class NestedFormsTests(TestCase): + + def setUp(self) -> None: + self._my_artist = Artist.objects.create( + id=1, + name='Organizer', + genres=['rock', 'punk'], + members=4 + ) + + def test_invalid(self): + expected = { + "errors": [ + { + 'code': 'invalid', + 'message': 'Enter a valid email address.', + 'path': ['bands', 0, 'emails', '0'] + }, + { + 'code': 'max_length', + 'message': '', + 'path': ['bands', 0, 'emails', '0'] + }, + { + 'code': 'max_length', + 'message': '', + 'path': ['bands', 0, 'emails', '1'] + }, + { + "code": "required", + "message": "This field is required.", + "path": ["bands", 1, "albums", 0, "songs", 0, "title"] + }, + { + "code": "required", + "message": "This field is required.", + "path": ["bands", 1, "albums", 0, "songs", 0, "duration"] + }, + { + "code": "required", + "message": "This field is required.", + "path": ["bands", 1, "albums", 0, "songs", 1, "title"] + }, + { + "code": "invalid", + "message": "Enter a valid date/time.", + "path": ["bands", 1, "albums", 0, "metadata", "error_at"] + }, + { + "code": "time-traveling", + "message": "Sounds like a bullshit", + "path": ["bands", 1, "albums", 0, "$body"] + }, + { + "code": "invalid", + "message": "Enter a valid email address.", + "path": ["emails", 0] + }, + { + "code": "max_length", + "message": "", + "path": ["emails", 0] + }, + { + "code": "max_length", + "message": "", + "path": ["emails", 1] + } + ] + } + rf = RequestFactory() + + with open(f"{settings.BASE_DIR}/data/invalid_concert.json") as f: + request = rf.post('/foo/bar', data=f.read(), content_type='application/json') + + form = ConcertForm.create_from_request(request) + + self.assertFalse(form.is_valid()) + error = { + 'errors': [item.to_dict() for item in form._errors] + } + self.assertEqual(error, expected) + + def test_valid(self): + expected = { + "place": "Bratislava", + "bands": [ + { + "name": "Queen", + "formed": 1970, + "has_award": False, + "emails": { + "0": "em@il.com", + "1": "v@lid.com" + }, + "albums": [ + { + "title": "Unknown Pleasures", + "year": 1979, + "artist": { + "name": "Joy Division", + "genres": [ + "rock", + "punk" + ], + "members": 4 + }, + "songs": [ + { + "title": "Disorder", + "duration": datetime.timedelta(seconds=209) + }, + { + "title": "Day of the Lords", + "duration": datetime.timedelta(seconds=288), + "metadata": { + "_section": { + "type": "ID3v2", + "offset": 0, + "byteLength": 2048 + } + } + } + ], + "type": Album.AlbumType.VINYL, + "metadata": { + "created_at": datetime.datetime.strptime( + "2019-10-21T18:57:03+0100", "%Y-%m-%dT%H:%M:%S%z" + ), + "updated_at": datetime.datetime.strptime( + "2019-10-21T18:57:03+0100", "%Y-%m-%dT%H:%M:%S%z" + ), + } + } + ] + }, + { + "name": "The Beatles", + "formed": 1960, + "has_award": True, + "albums": [ + { + "title": "Unknown Pleasures", + "year": 1997, + "artist": { + "name": "Nirvana", + "genres": [ + "rock", + "punk" + ], + "members": 4 + }, + "songs": [ + { + "title": "Hey jude", + "duration": datetime.timedelta(seconds=140) + }, + { + "title": "Let it be", + "duration": datetime.timedelta(seconds=171) + }, + { + "title": "Day of the Lords", + "duration": datetime.timedelta(seconds=288), + "metadata": { + "_section": { + "type": "ID3v2", + "offset": 0, + "byteLength": 2048 + } + } + } + ], + "type": Album.AlbumType.VINYL, + "metadata": { + "created_at": datetime.datetime.strptime( + "2019-10-21T18:57:03+0100", "%Y-%m-%dT%H:%M:%S%z" + ), + "updated_at": datetime.datetime.strptime( + "2019-10-21T18:57:03+0100", "%Y-%m-%dT%H:%M:%S%z" + ), + } + } + ] + } + ], + "organizer_id": self._my_artist, + "emails": [ + "em@il.com", + "v@lid.com" + ] + } + + rf = RequestFactory() + + with open(f"{settings.BASE_DIR}/data/valid_concert.json") as f: + request = rf.post('/foo/bar', data=f.read(), content_type='application/json') + + form = ConcertForm.create_from_request(request) + + self.assertTrue(form.is_valid()) + self.assertEqual(form.cleaned_data, expected) diff --git a/tests/testapp/forms.py b/tests/testapp/forms.py index 44a81f6..ba2ad9a 100644 --- a/tests/testapp/forms.py +++ b/tests/testapp/forms.py @@ -1,5 +1,5 @@ from django.core.exceptions import ValidationError -from django.forms import fields +from django.forms import fields, ModelChoiceField from django_api_forms import Form, FieldList, AnyField, FormField, FormFieldList, EnumField, DictionaryField, \ ModelForm, BooleanField @@ -61,3 +61,22 @@ class Meta: name = fields.CharField(max_length=100) formed = fields.IntegerField() has_award = BooleanField() + emails = DictionaryField(fields.EmailField(max_length=14), required=False) + albums = FormFieldList(form=AlbumForm, required=False) + + +class ConcertForm(Form): + class Meta: + field_type_strategy = { + 'django_api_forms.fields.BooleanField': 'tests.testapp.population_strategies.BooleanField' + } + + field_strategy = { + 'artist': 'tests.testapp.population_strategies.PopulateArtistStrategy', + 'formed': 'tests.testapp.population_strategies.FormedStrategy' + } + + place = fields.CharField(max_length=15) + bands = FormFieldList(form=BandForm) + organizer_id = ModelChoiceField(queryset=Artist.objects.all()) + emails = FieldList(fields.EmailField(max_length=14))