diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 6ce1fba8..376e8013 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = False tag = False -current_version = 1.5.6 +current_version = 1.0.52 [bumpversion:file:setup.cfg] diff --git a/.travis.yml b/.travis.yml index 3659312f..13c2215e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,27 +1,19 @@ language: python cache: pip -services: - - postgresql +sudo: false python: - "3.6" - - "3.7" env: - global: - - PGUSER=postgres - - PGDATABASE=postgres - - PGPASSWORD= - matrix: - - DJANGO=2.2 + - DJANGO=2.2 matrix: fast_finish: true include: - { python: "3.6", env: TOXENV=isort } - { python: "3.6", env: TOXENV=docs } - - { python: "3.6", env: TOXENV=black } install: - pip install tox-travis diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 48a1d669..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,95 +0,0 @@ -# Contributing to vng-api-common - -Hi! First of all, thank you for your interest in contributing to this project, -you can do this in a number of ways :-) - -* [Reporting issues](#reporting-issues) -* [Submitting pull requests](#submitting-pull-requests) -* [Code and commit style](#code-and-commit-style) - -## Versioning - -vng-api-common is used as shared library in various downstream projects, which -each have their own versions (currently they are all in 1.0.0 release -candidate) status. - -The versioning of vng-api-common is semantic and follows the downstream -versioning. This means that the current latest version can accept new features, -i.e. things that make the API look different, in non-breaking ways, such as -adding API resource field attributes. - -Current branches/versions: - -* `master` is the latest version, currently tracking `1.1.x` versions. -* `stable/1.0.x` is input for the 1.0.0 release candidate versions. - -## Reporting issues - -If you're running into a bug or desired feature, you can -[submit an issue](https://github.com/VNG-Realisatie/vng-api-common/issues/new). - -**Feature requests** - -When you're submitting a feature request, please use the label *enhancement*. - -**Submitting a bug report** - -When submitting a bug report, please state: - -1. Which API this occurred in (ZRC, DRC, ...) -2. What the expected behaviour was -3. What the observed behaviour is - -If the issue affects older versions, please apply those tags (e.g. *1.0.x*) - -## Submitting pull requests - -Code contributions are valued, but we request some quality control! - -**Tests** - -Please make sure the tests pass. You can run the test suite by running `tox`. -While developing, you can also run the tests using `pytest` in the root of the -project. See the Tox and Pytest documentation for advanced usage. - -You can also refer to the `.travis.yml` setup to see which system requirements -are relevant, such as a working Postgres database. - -Bugfixes require a test that shows the errant behaviour and proves the bug was -fixed. - -New features should be accompanied by tests that show the interface/desired -behaviour. - -## Code and commit style - -**Imports** - -We sort imports using [isort](https://pypi.org/project/isort/). The tests -have an `isort` check, which you can run using `tox -e isort`. - -**Code formatting** - -Python code should be [black](https://github.com/psf/black) formatted, this -leaves no discussion about formatting. - -**Commit messages** - -Please use meaningful commit messages, and rebase them if you can. For example, -we'd expect a commit for adding a regression test and a second commit with -the fix. - -We encourage use of [gitmoji](https://gitmoji.carloscuesta.me/) to quickly -identify the type of change in the commit. This also helps you keep your -commits atomic - if multiple gitmoji are applicable, perhaps your commit should -be split in multiple atomic commits. - -Where applicable, please refer to the reported issue. - -Commit message template: - -``` -:bug: Fixes #123 -- fixed URL resolution - - -``` diff --git a/README.rst b/README.rst index 76aa1fd4..4a60d2e2 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ VNG-API-common - Tooling voor RESTful APIs ========================================== -|build-status| |coverage| |docs| |black| +|build-status| |coverage| |docs| |python-versions| |django-versions| |pypi-version| @@ -84,7 +84,4 @@ Features .. |pypi-version| image:: https://img.shields.io/pypi/v/vng-api-common.svg :target: https://pypi.org/project/vng-api-common/ -.. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black - .. _documentatie: https://vng-api-common.readthedocs.io/en/latest/?badge=latest diff --git a/docs/conf.py b/docs/conf.py index 99eb12c9..73c9f99a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,7 +23,6 @@ settings.configure( INSTALLED_APPS=[ - "django.contrib.sites", "rest_framework", "django_filters", "vng_api_common", diff --git a/docs/ref/http_caching.rst b/docs/ref/http_caching.rst deleted file mode 100644 index 5ca78f87..00000000 --- a/docs/ref/http_caching.rst +++ /dev/null @@ -1,75 +0,0 @@ -============ -HTTP Caching -============ - -HTTP caching is a mechanism that shines in RESTful APIs. A ``GET`` or ``HEAD`` -response on a detail resource can be augmented with the ``ETag`` header, which -essentially is a "version" of a resource. Every modification of the resource -leads to a new version. - -This can be leveraged in combination with the ``If-None-Match`` request header, -kwown as conditional requests. - -The client includes the ``ETag`` value(s) in this header, and if the current -version of the resource has the same ``ETag`` value, then the server replies -with an ``HTTP 304`` response, indicating that the content has not changed. - -This allows consumers to send less data over the wire without having to -perform extra requests if they keep resources in their own cache. - -See the `ETag on MDN`_ documentation for the full specification. - -Implementing conditional requests -================================= - -Two actions are needed to implement this in API implementations: - -Add the model mixin to save the ETag value ------------------------------------------- - -This enables us to perform fast lookups and avoid calculating the ETag value -over and over again. It happens automatically on ``save`` of a model instance. - -.. code-block:: python - :linenos: - - from vng_api_common.caching import ETagMixin - - - class MyModel(ETagMixin, models.Model): - pass - -.. note:: If you're doing ``queryset.update`` or ``bulk_create`` operations, - you need to take care of setting/calculating the value yourself. - -Decorate the viewset --------------------- - -Decorating the viewset retrieve ensures that the ``ETag`` header is set, -the conditional request handling (HTTP 200 vs. HTTP 304) is performed and the -extra methods/headers show up in the generated API schema. - -.. code-block:: python - :linenos: - - from rest_framework import viewsets - from vng_api_common.caching import conditional_retrieve - - from .models import MyModel - from .serializers import MyModelSerializer - - - @conditional_retrieve() - class MyModelViewSet(viewsets.ReadOnlyViewSet): - queryset = MyModel.objects.all() - serializer_class = MyModelSerializer - - -Public API -========== - -.. automodule:: vng_api_common.caching - :members: - - -.. _ETag on MDN: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag diff --git a/docs/ref/index.rst b/docs/ref/index.rst index 14845999..930e9ef0 100644 --- a/docs/ref/index.rst +++ b/docs/ref/index.rst @@ -13,7 +13,6 @@ Modules reference exceptions middleware viewset-mixins - http_caching database geo polymorphism diff --git a/package-lock.json b/package-lock.json index 2ba9b7b9..ca093ae4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,38 +1,33 @@ { "name": "vng-api-common", - "version": "1.5.6", + "version": "1.0.52", "lockfileVersion": 1, "requires": true, "dependencies": { "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", "requires": { - "@babel/highlight": "^7.10.4" + "@babel/highlight": "^7.0.0" } }, - "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==" - }, "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", + "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", "requires": { - "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", + "esutils": "^2.0.2", "js-tokens": "^4.0.0" } }, "@babel/runtime": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", - "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.3.tgz", + "integrity": "sha512-9lsJwJLxDh/T3Q3SZszfWOTkk3pHbkmH+3KY+zwIDmsNlxsumuhS2TH3NIpktU4kNvfzy+k3eLT7aTJSPTo0OA==", "requires": { - "regenerator-runtime": "^0.13.4" + "regenerator-runtime": "^0.13.2" } }, "@types/color-name": { @@ -52,9 +47,9 @@ } }, "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" }, "ansi-styles": { "version": "3.2.1", @@ -65,17 +60,17 @@ } }, "better-ajv-errors": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/better-ajv-errors/-/better-ajv-errors-0.6.7.tgz", - "integrity": "sha512-PYgt/sCzR4aGpyNy5+ViSQ77ognMnWq7745zM+/flYO4/Yisdtp9wDQW2IKCyVYPUxQt3E/b5GBSwfhd1LPdlg==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/better-ajv-errors/-/better-ajv-errors-0.6.1.tgz", + "integrity": "sha512-UaaWknKQQRSPT/zF+pYKdS7swLf/0NbPll52/bfYlpM72+iFFC49cBErGRxAWHC9AOsD0yy176DR/JyaIRD8+Q==", "requires": { "@babel/code-frame": "^7.0.0", "@babel/runtime": "^7.0.0", "chalk": "^2.4.1", - "core-js": "^3.2.1", + "core-js": "^2.5.7", "json-to-ast": "^2.0.3", "jsonpointer": "^4.0.1", - "leven": "^3.1.0" + "leven": "^2.1.0" } }, "call-me-maybe": { @@ -99,13 +94,13 @@ } }, "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" } }, "co": { @@ -118,6 +113,11 @@ "resolved": "https://registry.npmjs.org/code-error-fragment/-/code-error-fragment-0.0.230.tgz", "integrity": "sha512-cadkfKp6932H8UkhzE/gcUqhRMNf8jHzkAN7+5Myabswaghu4xABTgPHDCjW+dBAJxj/SpkTYokpzDqY4pCzQw==" }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -132,9 +132,21 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "core-js": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", - "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==" + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", + "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==" + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } }, "decamelize": { "version": "1.2.0", @@ -146,6 +158,14 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "^1.4.0" + } + }, "es6-promise": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", @@ -156,6 +176,25 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, "fast-deep-equal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", @@ -172,18 +211,25 @@ "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" }, "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "locate-path": "^3.0.0" } }, "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } }, "grapheme-splitter": { "version": "1.0.4", @@ -196,14 +242,29 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "http2-client": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.3.tgz", - "integrity": "sha512-nUxLymWQ9pzkzTmir24p2RtsgruLmhje7lH3hLX1IpwvyTg77fW+1brenPPP3USAR+rQ36p5sTA/x7sjCJVkAA==" + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.2.tgz", + "integrity": "sha512-CY9yoIetaoblM5CTrzHc7mJvH1Fo9/XmO6kxRkTCnWbSPq5brQYbtJ7hJrI5nKMYpyqPJYdPN9mkQbRBVvsoSQ==" + }, + "invert-kv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" }, "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "js-tokens": { "version": "4.0.0", @@ -225,23 +286,60 @@ } }, "jsonpointer": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.1.0.tgz", - "integrity": "sha512-CXcRvMyTlnR53xMcKnuMzfCA5i/nfblTnnr74CZb6C4vG39eu6w51t7nKmU5MfLfbTgGItliNyjO/ciNPDqClg==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=" + }, + "lcid": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "requires": { + "invert-kv": "^2.0.0" + } }, "leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=" }, "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "requires": { + "p-defer": "^1.0.0" + } + }, + "mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", "requires": { - "p-locate": "^4.1.0" + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" } }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, "node-fetch-h2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", @@ -258,71 +356,322 @@ "es6-promise": "^3.2.1" } }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, "oas-kit-common": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", - "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.7.tgz", + "integrity": "sha512-8+P8gBjN9bGfa5HPgyefO78o394PUwHoQjuD4hM0Bpl56BkcxoyW4MpWMPM6ATm+yIIz4qT1igmuVukUtjP/pQ==", "requires": { - "fast-safe-stringify": "^2.0.7" + "safe-json-stringify": "^1.2.0" } }, "oas-linter": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.1.3.tgz", - "integrity": "sha512-jFWBHjSoqODGo7cKA/VWqqWSLbHNtnyCEpa2nMMS64SzCUbZDk63Oe7LqQZ2qJA0K2VRreYLt6cVkYy6MqNRDg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.1.2.tgz", + "integrity": "sha512-mv3HBG9aQz8PLGvonewIN9Y2Ra8QL6jvotRvf7NCdZ20n5vg4dO4y61UZh6s+KRDfJaU1PO+9Oxrn3EUN4Xygw==", "requires": { "should": "^13.2.1", "yaml": "^1.8.3" + }, + "dependencies": { + "yaml": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", + "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==" + } } }, "oas-resolver": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.4.2.tgz", - "integrity": "sha512-iJo7wE/MhuCJefkcpCS/NlE8MunRgRvgPozpeLSZUg0zmU8PBkzUwdtzpmjGDd7QjEuUi0SZ/y1wIrFIH+FNiA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.3.2.tgz", + "integrity": "sha512-toGCUv8wyZZmUAAsw4jn+511xNpUFW2ZLp4sAZ7xpERIeosrbxBxtkVxot9kXvdUHtPjRafi5+bkJ56TwQeYSQ==", "requires": { "node-fetch-h2": "^2.3.0", "oas-kit-common": "^1.0.8", - "reftools": "^1.1.4", + "reftools": "^1.1.1", "yaml": "^1.8.3", "yargs": "^15.3.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "oas-kit-common": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", + "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", + "requires": { + "fast-safe-stringify": "^2.0.7" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "reftools": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.1.tgz", + "integrity": "sha512-7ySkzK7YpUeJP16rzJqEXTZ7IrAq/AL/p+wWejD9wdKQOe+mYYVAOB3w5ZTs2eoHfmAidwr/6PcC+q+LzPF/DQ==" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yaml": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", + "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==" + }, + "yargs": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", + "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.1" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } } }, "oas-schema-walker": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz", - "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.2.tgz", + "integrity": "sha512-Q9xqeUtc17ccP/dpUfARci4kwFFszyJAgR/wbDhrRR/73GqsY5uSmKaIK+RmBqO8J4jVYrrDPjQKvt1IcpQdGw==" }, "oas-validator": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-4.0.7.tgz", - "integrity": "sha512-ppSW68iIIhvzFwSvY51NJPLM0uFjkHKAdoXKO+Pq6Ej1qU5Nvi9I3dQt6W8y/B+UYIP8yXr9YTEuvzG7sQH/ww==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-3.4.0.tgz", + "integrity": "sha512-l/SxykuACi2U51osSsBXTxdsFc8Fw41xI7AsZkzgVgWJAzoEFaaNptt35WgY9C3757RUclsm6ye5GvSyYoozLQ==", "requires": { "ajv": "^5.5.2", "better-ajv-errors": "^0.6.7", "call-me-maybe": "^1.0.1", - "oas-kit-common": "^1.0.8", - "oas-linter": "^3.1.3", - "oas-resolver": "^2.4.2", - "oas-schema-walker": "^1.1.5", - "reftools": "^1.1.4", + "oas-kit-common": "^1.0.7", + "oas-linter": "^3.1.0", + "oas-resolver": "^2.3.0", + "oas-schema-walker": "^1.1.3", + "reftools": "^1.1.0", "should": "^13.2.1", "yaml": "^1.8.3" + }, + "dependencies": { + "better-ajv-errors": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/better-ajv-errors/-/better-ajv-errors-0.6.7.tgz", + "integrity": "sha512-PYgt/sCzR4aGpyNy5+ViSQ77ognMnWq7745zM+/flYO4/Yisdtp9wDQW2IKCyVYPUxQt3E/b5GBSwfhd1LPdlg==", + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/runtime": "^7.0.0", + "chalk": "^2.4.1", + "core-js": "^3.2.1", + "json-to-ast": "^2.0.3", + "jsonpointer": "^4.0.1", + "leven": "^3.1.0" + } + }, + "core-js": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", + "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==" + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==" + }, + "oas-schema-walker": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.4.tgz", + "integrity": "sha512-foVDDS0RJYMfhQEDh/WdBuCzydTcsCnGo9EeD8SpWq1uW10JXiz+8SfYVDA7LO87kjmlnTRZle/2gr5qxabaEA==" + }, + "reftools": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.1.tgz", + "integrity": "sha512-7ySkzK7YpUeJP16rzJqEXTZ7IrAq/AL/p+wWejD9wdKQOe+mYYVAOB3w5ZTs2eoHfmAidwr/6PcC+q+LzPF/DQ==" + }, + "yaml": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", + "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==" + } + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "os-locale": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "requires": { + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" } }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" + }, "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", "requires": { "p-try": "^2.0.0" } }, "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "requires": { - "p-limit": "^2.2.0" + "p-limit": "^2.0.0" } }, "p-try": { @@ -331,19 +680,33 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } }, "reftools": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.4.tgz", - "integrity": "sha512-Y8VEk3OXPwuU+ZAAPiR0YhYy9iBSO3NBRnXHGfqW6c2mo2V+fQ0cwRA38Ny6z9ZzcAo6HjmVvAri+wz3+8fsdQ==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.0.7.tgz", + "integrity": "sha512-J4rugWI8+trddvJxXzK0VeEW9YBfofY5SOJzmvRRiVYRzbR8RbFjtlP2eZbJlqz5GwkvO9iCJZLvkem7dGA5zg==" }, "regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz", + "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==" }, "require-directory": { "version": "2.1.1", @@ -351,15 +714,38 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "safe-json-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==" + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, "should": { "version": "13.2.3", "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", @@ -408,24 +794,33 @@ "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==" }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" } }, "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "requires": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^3.0.0" } }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -435,21 +830,29 @@ } }, "swagger2openapi": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-6.2.2.tgz", - "integrity": "sha512-A8RWwzkymhF/ZfO0AylEZ2eCaN2jvAJU3bdtXQzkW5QvuyBMUBcyfB2tkHMTpXWYDmt7uKHwPGRZ0doPejtARA==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-5.2.5.tgz", + "integrity": "sha512-AZl3kJsIsqQgY2yPIaCUwd62e722CG7qUsCES3/gCPzlfpkXikEdttkH4V2T4ANNSIbZdRI8jI0Yzbp0gi7shA==", "requires": { "better-ajv-errors": "^0.6.1", "call-me-maybe": "^1.0.1", "node-fetch-h2": "^2.3.0", "node-readfiles": "^0.2.0", - "oas-kit-common": "^1.0.8", - "oas-resolver": "^2.4.2", - "oas-schema-walker": "^1.1.5", - "oas-validator": "^4.0.7", - "reftools": "^1.1.4", - "yaml": "^1.8.3", - "yargs": "^15.3.1" + "oas-kit-common": "^1.0.7", + "oas-resolver": "^2.2.3", + "oas-schema-walker": "^1.1.2", + "oas-validator": "^3.2.4", + "reftools": "^1.0.7", + "yaml": "^1.3.1", + "yargs": "^12.0.5" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" } }, "which-module": { @@ -458,71 +861,88 @@ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" }, "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" + "number-is-nan": "^1.0.0" } }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { - "color-name": "~1.1.4" + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" } }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } } } }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" }, "yaml": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", - "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.5.0.tgz", + "integrity": "sha512-nKxSWOa7vxAP2pikrGxbkZsG/garQseRiLn9mIDjzwoQsyVy7ZWIpLoARejnINGGLA4fttuzRFFNxxbsztdJgw==", + "requires": { + "@babel/runtime": "^7.4.3" + } }, "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", "requires": { - "cliui": "^6.0.0", + "cliui": "^4.0.0", "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^3.0.0", "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", + "require-main-filename": "^1.0.1", "set-blocking": "^2.0.0", - "string-width": "^4.2.0", + "string-width": "^2.0.0", "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^11.1.1" } }, "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" diff --git a/package.json b/package.json index b6e4e6c1..35b5d777 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vng-api-common", - "version": "1.5.6", + "version": "1.0.52", "description": "NodeJS build tooling for vng-api-common", "main": "index.js", "directories": { @@ -22,6 +22,6 @@ }, "homepage": "https://github.com/vng-Realisatie/gemma-zaken-common#readme", "dependencies": { - "swagger2openapi": "^6.2.2" + "swagger2openapi": "^5.2.5" } } diff --git a/setup.cfg b/setup.cfg index f618edf7..91060412 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ # see http://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files [metadata] name = vng-api-common -version = 1.5.6 +version = 1.0.52 description = VNG API tooling long_description = file: README.rst url = https://github.com/VNG-Realisatie/vng-api-common @@ -34,16 +34,15 @@ scripts = bin/use_external_components bin/use_external_components.cmd install_requires = - django>=2.2,<3.0 + django<3.0 django-choices django-filter>=2.0 django-solo djangorestframework<3.10 djangorestframework_camel_case - django-rest-framework-condition drf-yasg==1.16.0 drf-nested-routers - gemma-zds-client>=0.13.0 + gemma-zds-client>=0.9.4 iso-639 isodate oyaml @@ -54,7 +53,6 @@ install_requires = tests_require = pytest pytest-django - pytest-factoryboy tox isort black @@ -66,7 +64,6 @@ tests = psycopg2 pytest pytest-django - pytest-factoryboy tox isort black diff --git a/testapp/factories.py b/testapp/factories.py deleted file mode 100644 index 9b2dc642..00000000 --- a/testapp/factories.py +++ /dev/null @@ -1,24 +0,0 @@ -import factory - - -class GroupFactory(factory.django.DjangoModelFactory): - name = factory.Faker("bs") - - class Meta: - model = "testapp.Group" - - -class PersonFactory(factory.django.DjangoModelFactory): - name = factory.Faker("name") - address_street = factory.Faker("street_name") - address_number = factory.Faker("building_number") - - class Meta: - model = "testapp.Person" - - -class HobbyFactory(factory.django.DjangoModelFactory): - name = factory.Faker("bs") - - class Meta: - model = "testapp.Hobby" diff --git a/testapp/migrations/0003_person__etag.py b/testapp/migrations/0003_person__etag.py deleted file mode 100644 index 68951026..00000000 --- a/testapp/migrations/0003_person__etag.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.1.8 on 2019-09-05 06:12 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("testapp", "0002_auto_20190620_0849")] - - operations = [ - migrations.AddField( - model_name="person", - name="_etag", - field=models.CharField( - default="", - editable=False, - help_text="MD5 hash of the resource representation in its current version.", - max_length=32, - verbose_name="etag value", - ), - preserve_default=False, - ) - ] diff --git a/testapp/migrations/0004_auto_20190909_0206.py b/testapp/migrations/0004_auto_20190909_0206.py deleted file mode 100644 index 7cd618cb..00000000 --- a/testapp/migrations/0004_auto_20190909_0206.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 2.1.8 on 2019-09-09 02:06 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("testapp", "0003_person__etag")] - - operations = [ - migrations.CreateModel( - name="Hobby", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "_etag", - models.CharField( - editable=False, - help_text="MD5 hash of the resource representation in its current version.", - max_length=32, - verbose_name="etag value", - ), - ), - ("name", models.CharField(max_length=100, verbose_name="name")), - ], - options={"abstract": False}, - ), - migrations.AddField( - model_name="person", - name="hobbies", - field=models.ManyToManyField( - blank=True, related_name="people", to="testapp.Hobby" - ), - ), - ] diff --git a/testapp/migrations/0005_group_name.py b/testapp/migrations/0005_group_name.py deleted file mode 100644 index 0452b6d5..00000000 --- a/testapp/migrations/0005_group_name.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.1.8 on 2019-09-09 04:35 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [("testapp", "0004_auto_20190909_0206")] - - operations = [ - migrations.AddField( - model_name="group", - name="name", - field=models.CharField(default="", max_length=100, verbose_name="name"), - preserve_default=False, - ) - ] diff --git a/testapp/migrations/0006_merge_20200416_0440.py b/testapp/migrations/0006_merge_20200416_0440.py deleted file mode 100644 index 76dc985b..00000000 --- a/testapp/migrations/0006_merge_20200416_0440.py +++ /dev/null @@ -1,13 +0,0 @@ -# Generated by Django 2.2.6 on 2020-04-16 04:40 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("testapp", "0003_auto_20200416_0423"), - ("testapp", "0005_group_name"), - ] - - operations = [] diff --git a/testapp/models.py b/testapp/models.py index 8b5cc143..98e82466 100644 --- a/testapp/models.py +++ b/testapp/models.py @@ -1,13 +1,10 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -from vng_api_common.caching import ETagMixin from vng_api_common.descriptors import GegevensGroepType class Group(models.Model): - name = models.CharField(_("name"), max_length=100) - subgroup_field_1 = models.CharField(max_length=50, blank=True) subgroup_field_2 = models.CharField(max_length=50, blank=True, default="baz") @@ -17,20 +14,12 @@ class Group(models.Model): ) -class Person(ETagMixin, models.Model): +class Person(models.Model): name = models.CharField(_("name"), max_length=50) address_street = models.CharField(_("street name"), max_length=255) address_number = models.CharField(_("house number"), max_length=10) - address = GegevensGroepType( - {"street": address_street, "number": address_number}, required=False - ) + address = GegevensGroepType({"street": address_street, "number": address_number}, required=False) group = models.ForeignKey("Group", null=True, on_delete=models.CASCADE) - - hobbies = models.ManyToManyField("Hobby", related_name="people", blank=True) - - -class Hobby(ETagMixin, models.Model): - name = models.CharField(_("name"), max_length=100) diff --git a/testapp/serializers.py b/testapp/serializers.py index a2f07a58..d4591671 100644 --- a/testapp/serializers.py +++ b/testapp/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from testapp.models import Group, Hobby, Person +from testapp.models import Group, Person from vng_api_common.serializers import GegevensGroepSerializer @@ -12,14 +12,10 @@ class Meta: class PersonSerializer(serializers.ModelSerializer): address = AddressSerializer(allow_null=True) - group_name = serializers.SerializerMethodField() class Meta: model = Person - fields = ("address", "name", "group_name") - - def get_group_name(self, obj) -> str: - return obj.group.name if obj.group_id else "" + fields = ("address", "name") class PersonSerializer2(serializers.ModelSerializer): @@ -36,9 +32,3 @@ class GroupSerializer(serializers.ModelSerializer): class Meta: model = Group fields = ("person",) - - -class HobbySerializer(serializers.ModelSerializer): - class Meta: - model = Hobby - fields = ("name", "people") diff --git a/testapp/settings.py b/testapp/settings.py index 663e334a..9a7a471e 100644 --- a/testapp/settings.py +++ b/testapp/settings.py @@ -2,8 +2,6 @@ from vng_api_common.conf.api import * # noqa -SITE_ID = 1 - DEBUG = os.getenv("DEBUG", "no").lower() in ["yes", "true", "1"] BASE_DIR = os.path.abspath(os.path.dirname(__file__)) @@ -74,8 +72,6 @@ REST_FRAMEWORK = BASE_REST_FRAMEWORK.copy() -SECURITY_DEFINITION_NAME = "JWT-Claims" - SWAGGER_SETTINGS = BASE_SWAGGER_SETTINGS.copy() SWAGGER_SETTINGS["DEFAULT_FIELD_INSPECTORS"] = SWAGGER_SETTINGS[ @@ -83,8 +79,5 @@ ][1:] SWAGGER_SETTINGS.update( - { - "DEFAULT_INFO": "testapp.schema.info", - "SECURITY_DEFINITIONS": {SECURITY_DEFINITION_NAME: {}}, - } + {"DEFAULT_INFO": "testapp.schema.info", "SECURITY_DEFINITIONS": {}} ) diff --git a/testapp/urls.py b/testapp/urls.py index ce0edc5b..4d1c8072 100644 --- a/testapp/urls.py +++ b/testapp/urls.py @@ -6,13 +6,12 @@ from .schema import SchemaView from .views import NotificationView -from .viewsets import GroupViewSet, HobbyViewSet, PersonViewSet +from .viewsets import GroupViewSet -router = routers.DefaultRouter(trailing_slash=False) -router.register("persons", PersonViewSet) -router.register("hobbies", HobbyViewSet) +router = routers.DefaultRouter() router.register("groups", GroupViewSet) + urlpatterns = [ path("admin/", admin.site.urls), path( @@ -34,7 +33,6 @@ + router.urls ), ), - path("api/", include(router.urls)), path("api/", include("vng_api_common.api.urls")), path("ref/", include("vng_api_common.urls")), # this is a hack to get the parameter to show up in the API spec diff --git a/testapp/viewsets.py b/testapp/viewsets.py index fcb7d123..8a4c4425 100644 --- a/testapp/viewsets.py +++ b/testapp/viewsets.py @@ -1,32 +1,7 @@ from rest_framework import viewsets -from vng_api_common.caching import conditional_retrieve - -from .models import Group, Hobby, Person -from .serializers import GroupSerializer, HobbySerializer, PersonSerializer - - -@conditional_retrieve() -class PersonViewSet(viewsets.ReadOnlyModelViewSet): - """ - Title - - Summary - - More summary - - retrieve: - Some description - """ - - queryset = Person.objects.all() - serializer_class = PersonSerializer - - -@conditional_retrieve() -class HobbyViewSet(viewsets.ReadOnlyModelViewSet): - queryset = Hobby.objects.all() - serializer_class = HobbySerializer +from .models import Group +from .serializers import GroupSerializer class GroupViewSet(viewsets.ModelViewSet): diff --git a/tests/conftest.py b/tests/conftest.py index 419e5059..824b6992 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,27 +1,6 @@ -""" -Define pytest configuration and setup. - -The urls import is needed to make sure all urls/subclasses are registered -BEFORE fixtures run. -""" from django.urls import clear_script_prefix, set_script_prefix import pytest -from pytest_factoryboy import register -from rest_framework.test import APIClient - -from testapp import urls # noqa -from testapp.factories import GroupFactory, HobbyFactory, PersonFactory - -register(PersonFactory, "person") -register(HobbyFactory) -register(GroupFactory) - - -@pytest.fixture -def api_client(): - client = APIClient() - return client @pytest.fixture diff --git a/tests/test_cache_headers.py b/tests/test_cache_headers.py deleted file mode 100644 index 4ae18f74..00000000 --- a/tests/test_cache_headers.py +++ /dev/null @@ -1,197 +0,0 @@ -import pytest -from drf_yasg import openapi -from drf_yasg.generators import SchemaGenerator -from rest_framework import status -from rest_framework.reverse import reverse -from rest_framework.test import APIRequestFactory -from rest_framework.views import APIView - -from testapp.viewsets import PersonViewSet -from vng_api_common.inspectors.cache import get_cache_headers - -pytestmark = pytest.mark.django_db(transaction=True) - - -def test_etag_header_present(api_client, person): - path = reverse("person-detail", kwargs={"pk": person.pk}) - - response = api_client.get(path) - - person.refresh_from_db() - assert response.status_code == status.HTTP_200_OK - assert "ETag" in response - assert response["ETag"] == f'"{person._etag}"' - - -def test_304_on_cached_resource(api_client, person): - person.calculate_etag_value() - path = reverse("person-detail", kwargs={"pk": person.pk}) - - response = api_client.get(path, HTTP_IF_NONE_MATCH=f'"{person._etag}"') - - assert response.status_code == status.HTTP_304_NOT_MODIFIED - assert "Etag" in response - - -def test_200_on_stale_resource(api_client, person): - path = reverse("person-detail", kwargs={"pk": person.pk}) - - response = api_client.get(path, HTTP_IF_NONE_MATCH='"stale"') - - assert response.status_code == status.HTTP_200_OK - - -def test_cache_headers_detected(): - request = APIRequestFactory().get("/api/persons/1") - request = APIView().initialize_request(request) - callback = PersonViewSet.as_view({"get": "retrieve"}, detail=True) - generator = SchemaGenerator() - - view = generator.create_view(callback, "GET", request=request) - - headers = get_cache_headers(view) - - assert "ETag" in headers - assert isinstance(headers["ETag"], openapi.Schema) - - -def test_etag_changes_m2m_changes_forward(api_client, hobby, person): - # ensure etags are calculted - person_path = reverse("person-detail", kwargs={"pk": person.pk}) - hobby_path = reverse("hobby-detail", kwargs={"pk": hobby.pk}) - person_response = api_client.get(person_path) - hobby_response = api_client.get(hobby_path) - person.refresh_from_db() - hobby.refresh_from_db() - - # change the m2m, in the forward direction - person.hobbies.add(hobby) - - # compare the new ETags - person_response2 = api_client.get(person_path) - hobby_response2 = api_client.get(hobby_path) - assert person_response["ETag"] - assert person_response["ETag"] != '""' - assert person_response["ETag"] == person_response2["ETag"] - - assert hobby_response["ETag"] - assert hobby_response["ETag"] != '""' - assert hobby_response["ETag"] != hobby_response2["ETag"] - - -def test_etag_changes_m2m_changes_reverse(api_client, hobby, person): - path = reverse("hobby-detail", kwargs={"pk": hobby.pk}) - response = api_client.get(path) - hobby.refresh_from_db() - assert "ETag" in response - etag = response["ETag"] - - # change the m2m - reverse direction - hobby.people.add(person) - - response2 = api_client.get(path) - assert "ETag" in response2 - assert response2["ETag"] - assert response2["ETag"] != '""' - assert response2["ETag"] != etag - - -def test_remove_m2m(api_client, person, hobby): - hobby_path = reverse("hobby-detail", kwargs={"pk": hobby.pk}) - person.hobbies.add(hobby) - - etag = api_client.get(hobby_path)["ETag"] - hobby.refresh_from_db() - assert etag - assert etag != '""' - - # this changes the output of the hobby resource - person.hobbies.remove(hobby) - - new_etag = api_client.get(hobby_path)["ETag"] - assert new_etag - assert new_etag != '""' - assert new_etag != etag - - -def test_remove_m2m_reverse(api_client, person, hobby): - hobby_path = reverse("hobby-detail", kwargs={"pk": hobby.pk}) - person.hobbies.add(hobby) - - etag = api_client.get(hobby_path)["ETag"] - hobby.refresh_from_db() - assert etag - assert etag != '""' - - # this changes the output of the hobby resource - hobby.people.remove(person) - - new_etag = api_client.get(hobby_path)["ETag"] - assert new_etag - assert new_etag != '""' - assert new_etag != etag - - -def test_related_object_changes_etag(api_client, person, group): - path = reverse("person-detail", kwargs={"pk": person.pk}) - - # set up group object for person - person.group = group - person.save() - - etag1 = api_client.get(path)["ETag"] - person.refresh_from_db() - assert etag1 - assert etag1 != '""' - - # change the group name, should change the ETag - group.name = "bar" - group.save() - - etag2 = api_client.get(path)["ETag"] - - assert etag2 - assert etag2 != '""' - assert etag2 != etag1 - - -def test_etag_clearing_without_raw_key_in_kwargs(person): - person.delete() - - -def test_delete_resource_after_get(api_client, person): - path = reverse("person-detail", kwargs={"pk": person.pk}) - - api_client.get(path) - - person.refresh_from_db() - person.delete() - - -def test_fetching_cache_enabled_deleted_resource_404s(api_client, person): - path = reverse("person-detail", kwargs={"pk": person.pk}) - person.delete() - - response = api_client.get(path) - - assert response.status_code == 404 - - -def test_m2m_clear_schedules_etag_clear(api_client, person, hobby): - person.hobbies.add(hobby) - person_path = reverse("person-detail", kwargs={"pk": person.pk}) - person_etag = api_client.get(person_path)["ETag"] - assert person_etag - assert person_etag != '""' - hobby_path = reverse("hobby-detail", kwargs={"pk": hobby.pk}) - hobby_etag = api_client.get(hobby_path)["ETag"] - assert hobby_etag - assert hobby_etag != '""' - - person.hobbies.clear() - - hobby.refresh_from_db() - person.refresh_from_db() - - assert not hobby._etag - assert not person._etag diff --git a/tests/test_content_type_headers.py b/tests/test_content_type_headers.py deleted file mode 100644 index 068a2531..00000000 --- a/tests/test_content_type_headers.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -Test that the required content type headers are present. -""" -from django.urls import path -from django.utils.translation import ugettext_lazy as _ - -from drf_yasg import openapi -from rest_framework.parsers import JSONParser, MultiPartParser -from rest_framework.renderers import JSONRenderer -from rest_framework.response import Response -from rest_framework.views import APIView - -from vng_api_common.generators import OpenAPISchemaGenerator - - -class DummyView(APIView): - renderer_classes = (JSONRenderer,) - - def get(self, request): - return Response({}) - - def post(self, request): - return Response({}) - - -class JSONView(DummyView): - parser_classes = (JSONParser,) - - -class MultiPartView(DummyView): - parser_classes = (MultiPartParser,) - - -urlpatterns = [ - path("json", JSONView.as_view(), name="json"), - path("multipart", MultiPartView.as_view(), name="multipart"), -] - - -def _generate_schema(): - generator = OpenAPISchemaGenerator( - info=openapi.Info("dummy", ""), - patterns=urlpatterns, - ) - return generator.get_schema() - - -def test_json_content_type(): - schema = _generate_schema() - - get_operation = schema.paths["/json"]["get"] - post_operation = schema.paths["/json"]["post"] - - assert get_operation["parameters"] == [] - assert post_operation["parameters"] == [ - openapi.Parameter( - name="Content-Type", - in_=openapi.IN_HEADER, - type=openapi.TYPE_STRING, - required=True, - enum=["application/json"], - description=_("Content type of the request body."), - ) - ] - - -def test_multipart_content_type(): - schema = _generate_schema() - - get_operation = schema.paths["/multipart"]["get"] - post_operation = schema.paths["/multipart"]["post"] - - assert get_operation["parameters"] == [] - assert post_operation["parameters"] == [ - openapi.Parameter( - name="Content-Type", - in_=openapi.IN_HEADER, - type=openapi.TYPE_STRING, - required=True, - enum=["multipart/form-data"], - description=_("Content type of the request body."), - ) - ] diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 8c0110b3..104c5962 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -1,21 +1,12 @@ -from typing import List from unittest.mock import patch from django.db.models import ObjectDoesNotExist from django.utils.translation import ugettext as _ -import pytest -from rest_framework import status -from rest_framework.response import Response from rest_framework.test import APIRequestFactory from rest_framework.views import APIView -from vng_api_common.authorizations.models import Applicatie, Autorisatie -from vng_api_common.constants import ComponentTypes -from vng_api_common.middleware import JWTAuth from vng_api_common.permissions import BaseAuthRequired -from vng_api_common.scopes import Scope -from vng_api_common.tests import generate_jwt_auth class Permissions(BaseAuthRequired): @@ -49,49 +40,3 @@ def test_failed_db_lookup(): "code": "object-does-not-exist", "reason": _("The object does not exist in the database"), } - - -class DummyView(APIView): - permission_classes = (BaseAuthRequired,) - required_scopes = {"post": Scope("dummy", private=True)} - - def post(self, request): - return Response({}) - - -class Auth(JWTAuth): - def __init__(self, scopes: List[str]): - self._scopes = scopes - - @property - def applicaties(self): - app = Applicatie.objects.create(client_ids=["dummy"]) - Autorisatie.objects.create( - applicatie=app, component=ComponentTypes.zrc, scopes=self._scopes - ) - return Applicatie.objects.filter(id=app.id) - - -@pytest.mark.django_db -class TestMethodPermissions: - """ - Test that it's possible to define permissions on HTTP methods. - """ - - def _request(self, scopes: List[str]) -> Response: - auth = generate_jwt_auth("dummy", "dummy") - - factory = APIRequestFactory() - request = factory.post("/foo", {}, format="json", HTTP_AUTHORIZATION=auth) - request.jwt_auth = Auth(scopes) - return DummyView.as_view()(request) - - def test_post_not_allowed(self): - response = self._request([]) - - assert response.status_code == status.HTTP_403_FORBIDDEN - - def test_post_allowed(self): - response = self._request(["dummy"]) - - assert response.status_code == status.HTTP_200_OK diff --git a/tests/test_schema_root_tags.py b/tests/test_schema_root_tags.py deleted file mode 100644 index 4b4fea46..00000000 --- a/tests/test_schema_root_tags.py +++ /dev/null @@ -1,33 +0,0 @@ -from unittest import mock - -import pytest -from rest_framework.test import APIRequestFactory -from rest_framework.views import APIView - -from testapp.viewsets import PersonViewSet -from vng_api_common.generators import OpenAPISchemaGenerator -from vng_api_common.utils import get_view_summary - -pytestmark = pytest.mark.django_db(transaction=True) - - -def test_schema_root_tags(): - request = APIRequestFactory().get("/api/persons/1") - request = APIView().initialize_request(request) - request._request.jwt_auth = mock.Mock() - - generator = OpenAPISchemaGenerator(info=mock.Mock()) - - schema = generator.get_schema(request) - assert hasattr(schema, "tags") - - # Convert list of ordereddicts to simple dict. - tags = dict([dict(od).values() for od in schema.tags]) - assert "persons" in tags - assert tags["persons"] == "Summary\n\nMore summary" - - -def test_view_summary(): - summary = get_view_summary(PersonViewSet) - - assert summary == "Summary\n\nMore summary" diff --git a/tox.ini b/tox.ini index 0fef8b9f..a634b0cc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,8 @@ [tox] envlist = - py{36,37}-django22 + py36-django22 isort docs - black skip_missing_interpreters = true [travis:env] @@ -31,11 +30,6 @@ extras = tests skipsdist = True commands = isort --recursive --check-only --diff . -[testenv:black] -extras = tests -skipsdist = True -commands = black --check vng_api_common tests testapp docs - [testenv:docs] basepython=python changedir=docs diff --git a/vng_api_common/api/permissions.py b/vng_api_common/api/permissions.py index f51566fb..cd48a466 100644 --- a/vng_api_common/api/permissions.py +++ b/vng_api_common/api/permissions.py @@ -17,5 +17,5 @@ def has_permission(self, request: Request, view: APIView) -> bool: if not hasattr(view, "action"): view.action = "create" - scopes_required = get_required_scopes(request, view) + scopes_required = get_required_scopes(view) return request.jwt_auth.has_auth(scopes_required, component=ComponentTypes.ac) diff --git a/vng_api_common/apps.py b/vng_api_common/apps.py index f50627a7..24d5956b 100644 --- a/vng_api_common/apps.py +++ b/vng_api_common/apps.py @@ -30,7 +30,6 @@ class ZDSSchemaConfig(AppConfig): def ready(self): from . import checks # noqa - from .caching import signals # noqa patch_duration_type() register_serializer_field() diff --git a/vng_api_common/audittrails/api/serializers.py b/vng_api_common/audittrails/api/serializers.py index b88be757..ceb164e3 100644 --- a/vng_api_common/audittrails/api/serializers.py +++ b/vng_api_common/audittrails/api/serializers.py @@ -19,7 +19,6 @@ class Meta: fields = ( "uuid", "bron", - "request_id", "applicatie_id", "applicatie_weergave", "gebruikers_id", diff --git a/vng_api_common/audittrails/migrations/0012_auto_20200619_0545.py b/vng_api_common/audittrails/migrations/0012_auto_20200619_0545.py deleted file mode 100644 index 99553acb..00000000 --- a/vng_api_common/audittrails/migrations/0012_auto_20200619_0545.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 2.2.6 on 2020-06-19 05:45 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("audittrails", "0011_auto_20190918_1335"), - ] - - operations = [ - migrations.AlterField( - model_name="audittrail", - name="bron", - field=models.CharField( - choices=[ - ("ac", "Autorisatiecomponent"), - ("nrc", "Notificatierouteringcomponent"), - ("zrc", "Zaakregistratiecomponent"), - ("ztc", "Zaaktypecatalogus"), - ("drc", "Documentregistratiecomponent"), - ("brc", "Besluitregistratiecomponent"), - ("kic", "Klantinteractiescomponent"), - ], - help_text="De naam van het component waar de wijziging in is gedaan.", - max_length=50, - ), - ), - ] diff --git a/vng_api_common/authorizations/migrations/0011_auto_20191114_0728.py b/vng_api_common/authorizations/migrations/0011_auto_20191114_0728.py index 8bbc2f5d..41d84121 100644 --- a/vng_api_common/authorizations/migrations/0011_auto_20191114_0728.py +++ b/vng_api_common/authorizations/migrations/0011_auto_20191114_0728.py @@ -32,7 +32,6 @@ class Migration(migrations.Migration): operations = [ migrations.RunPython( - Renamer("zaaktypes", "catalogi"), - Renamer("catalogi", "zaaktypes"), + Renamer("zaaktypes", "catalogi"), Renamer("catalogi", "zaaktypes"), ) ] diff --git a/vng_api_common/authorizations/migrations/0012_auto_20200619_0545.py b/vng_api_common/authorizations/migrations/0012_auto_20200619_0545.py deleted file mode 100644 index 42f2a9de..00000000 --- a/vng_api_common/authorizations/migrations/0012_auto_20200619_0545.py +++ /dev/null @@ -1,49 +0,0 @@ -# Generated by Django 2.2.6 on 2020-06-19 05:45 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("authorizations", "0011_auto_20191114_0728"), - ] - - operations = [ - migrations.AlterField( - model_name="authorizationsconfig", - name="component", - field=models.CharField( - choices=[ - ("ac", "Autorisatiecomponent"), - ("nrc", "Notificatierouteringcomponent"), - ("zrc", "Zaakregistratiecomponent"), - ("ztc", "Zaaktypecatalogus"), - ("drc", "Documentregistratiecomponent"), - ("brc", "Besluitregistratiecomponent"), - ("kic", "Klantinteractiescomponent"), - ], - default="zrc", - max_length=50, - verbose_name="component", - ), - ), - migrations.AlterField( - model_name="autorisatie", - name="component", - field=models.CharField( - choices=[ - ("ac", "Autorisatiecomponent"), - ("nrc", "Notificatierouteringcomponent"), - ("zrc", "Zaakregistratiecomponent"), - ("ztc", "Zaaktypecatalogus"), - ("drc", "Documentregistratiecomponent"), - ("brc", "Besluitregistratiecomponent"), - ("kic", "Klantinteractiescomponent"), - ], - help_text="Component waarop autorisatie van toepassing is.", - max_length=50, - verbose_name="component", - ), - ), - ] diff --git a/vng_api_common/caching/__init__.py b/vng_api_common/caching/__init__.py deleted file mode 100644 index 81d53e8b..00000000 --- a/vng_api_common/caching/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -Facilitate HTTP caching mechanisms. - -This package contains the tooling required to (efficiently) apply and use ETag -HTTP headers. - -On a request level, the API will return HTTP 304 statuses if the client has -an up to date version of the resource. It will reply with a HTTP 200 otherwise, -including the ETag header. - -This package provides a model mixin to save the ETag header value to the db, -and a decorator to enable conditional requests on viewsets. The rest are -implementation details. -""" -from .decorators import conditional_retrieve -from .etags import calculate_etag -from .models import ETagMixin - -# public API -__all__ = ["ETagMixin", "calculate_etag", "conditional_retrieve"] diff --git a/vng_api_common/caching/decorators.py b/vng_api_common/caching/decorators.py deleted file mode 100644 index 6037f267..00000000 --- a/vng_api_common/caching/decorators.py +++ /dev/null @@ -1,23 +0,0 @@ -from functools import partial - -from rest_framework_condition.decorators import condition as drf_condition - -from .etags import etag_func - - -def conditional_retrieve(action="retrieve", etag_field="_etag"): - """ - Decorate a viewset to apply conditional GET requests. - """ - - def decorator(viewset: type): - condition = drf_condition(etag_func=partial(etag_func, etag_field=etag_field)) - original_handler = getattr(viewset, action) - handler = condition(original_handler) - setattr(viewset, action, handler) - if not hasattr(viewset, "_conditional_retrieves"): - viewset._conditional_retrieves = [] - viewset._conditional_retrieves.append(action) - return viewset - - return decorator diff --git a/vng_api_common/caching/etags.py b/vng_api_common/caching/etags.py deleted file mode 100644 index 8b36f0a1..00000000 --- a/vng_api_common/caching/etags.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -Calculate ETag values for API resources. -""" -import functools -import hashlib - -from django.conf import settings -from django.contrib.sites.models import Site -from django.core.exceptions import ObjectDoesNotExist -from django.db import models -from django.http import Http404, HttpRequest - -from djangorestframework_camel_case.render import CamelCaseJSONRenderer -from rest_framework import serializers -from rest_framework.request import Request -from rest_framework.settings import api_settings - -from ..serializers import GegevensGroepSerializer -from ..utils import get_resource_for_path, get_subclasses - - -class StaticRequest(HttpRequest): - def get_host(self) -> str: - site = Site.objects.get_current() - return site.domain - - def _get_scheme(self) -> str: - return "https" if settings.IS_HTTPS else "http" - - -def calculate_etag(instance: models.Model) -> str: - """ - Calculate the MD5 hash of a resource representation in the API. - - The serializer for the model class is retrieved, and then used to construct - the representation of the instance. Then, the representation is rendered - to camelCase JSON, after which the MD5 hash is calculated of this result. - """ - model_class = type(instance) - serializer_class = _get_serializer_for_models()[model_class] - - # build a dummy request with the configured domain, since we're doing STRONG - # comparison. Required as context for hyperlinked serializers - request = Request(StaticRequest()) - request.version = api_settings.DEFAULT_VERSION - request.versioning_scheme = api_settings.DEFAULT_VERSIONING_CLASS() - - serializer = serializer_class(instance=instance, context={"request": request}) - - # render the output to json, which is used as hash input - renderer = CamelCaseJSONRenderer() - rendered = renderer.render(serializer.data, "application/json") - - # calculate md5 hash - return hashlib.md5(rendered).hexdigest() - - -def etag_func(request: HttpRequest, etag_field: str = "_etag", **view_kwargs): - try: - obj = get_resource_for_path(request.path) - except ObjectDoesNotExist: - raise Http404 - etag_value = getattr(obj, etag_field) - if not etag_value: # calculate missing value and store it - etag_value = obj.calculate_etag_value() - return etag_value - - -@functools.lru_cache(maxsize=None) -def _get_serializer_for_models(): - """ - Map models to the serializer to use. - - If multiple serializers exist for a model, it must be explicitly defined. - """ - model_serializers = {} - for serializer_class in get_subclasses(serializers.ModelSerializer): - if not hasattr(serializer_class, "Meta"): - continue - - if issubclass(serializer_class, GegevensGroepSerializer): - continue - - model = serializer_class.Meta.model - if model in model_serializers: - continue - - model_serializers[model] = serializer_class - return model_serializers diff --git a/vng_api_common/caching/introspection.py b/vng_api_common/caching/introspection.py deleted file mode 100644 index af86f891..00000000 --- a/vng_api_common/caching/introspection.py +++ /dev/null @@ -1,18 +0,0 @@ -from rest_framework import mixins -from rest_framework.generics import GenericAPIView - - -def has_cache_header(view: GenericAPIView) -> bool: - if view.request is None: - return False - - if view.request.method not in ("GET", "HEAD"): - return False - - if hasattr(view, "detail") and not view.detail: - return False - - if not isinstance(view, mixins.RetrieveModelMixin): - return False - - return True diff --git a/vng_api_common/caching/models.py b/vng_api_common/caching/models.py deleted file mode 100644 index d8f288d8..00000000 --- a/vng_api_common/caching/models.py +++ /dev/null @@ -1,34 +0,0 @@ -import warnings - -from django.db import models -from django.utils.translation import ugettext_lazy as _ - -from .etags import calculate_etag - - -class ETagMixin(models.Model): - """ - Automatically calculate the (new) ETag value on save. - """ - - _etag = models.CharField( - _("etag value"), - max_length=32, - help_text=_("MD5 hash of the resource representation in its current version."), - editable=False, - ) - - class Meta: - abstract = True - - def calculate_etag_value(self) -> str: - """ - Calculate and save the ETag value. - """ - if not self.pk: - warnings.warn( - "You should not calculate ETags on unsaved objects", RuntimeWarning - ) - self._etag = calculate_etag(self) - self.save(update_fields=["_etag"]) - return self._etag diff --git a/vng_api_common/caching/signals.py b/vng_api_common/caching/signals.py deleted file mode 100644 index 7cbed5af..00000000 --- a/vng_api_common/caching/signals.py +++ /dev/null @@ -1,128 +0,0 @@ -""" -Signals listen to changes in models using ETagMixin and models related to those. - -A changed m2m for example may affect the output of the ETag. - -The signal does nothing except clearing the calculated value, which will be -re-calculated on the next fetch. -""" -from django.core.exceptions import FieldDoesNotExist -from django.db import models, transaction -from django.db.models.base import ModelBase -from django.db.models.fields.related import ManyToOneRel -from django.db.models.signals import m2m_changed, post_delete, post_save -from django.dispatch import receiver - - -def clear_etag(instance: models.Model): - """ - Clear the value of the ETag field. - """ - instance._etag = "" - instance.save(update_fields=["_etag"]) - - -def _schedule_clear_etag(instance: models.Model): - transaction.on_commit(lambda: clear_etag(instance)) - - -def is_etag_model(model: ModelBase) -> bool: - try: - model._meta.get_field("_etag") - # model doesn't support ETags, nothing to do - except FieldDoesNotExist: - return False - - return True - - -def handle_related_etag_instances(instance: models.Model): - model = type(instance) - fields = [ - field - for field in model._meta.get_fields() - if field.is_relation and isinstance(field, ManyToOneRel) - ] - - for field in fields: - source_model = field.remote_field.model - if not is_etag_model(source_model): - continue # TODO: what with more deeply nested objects? - - related_instances = getattr(instance, field.get_accessor_name()).all() - for related_instance in related_instances: - _schedule_clear_etag(related_instance) - - -def handle_m2m_cleared( - sender: ModelBase, instance: models.Model, model: ModelBase -) -> None: - """ - Clear the etag on the remote side of a m2m_field.clear() - """ - - def _get_through(field): - if hasattr(field, "through"): - return field.through - return field.remote_field.through - - # figure out which field is involved - m2m_fields = [ - field - for field in instance._meta.get_fields() - if getattr(field, "related_model") is model and _get_through(field) is sender - ] - assert len(m2m_fields) == 1, "This should resolve to a single m2m field" - m2m_field = m2m_fields[0] - - qs = getattr(instance, m2m_field.name).all() - for instance in qs: - _schedule_clear_etag(instance) - - -@receiver([post_save, post_delete]) -def schedule_etag_clearing(sender: ModelBase, instance: models.Model, **kwargs): - if kwargs.get("raw"): - return - - if not is_etag_model(sender): - handle_related_etag_instances(instance) - return - - # no value set for the ETag, nothing to do - if not instance._etag: - return - - if "update_fields" not in kwargs: - return - - # only updating the _etag field - either to clear it, or to set the computed - # value - if kwargs["update_fields"] == {"_etag"}: - return - - # clear existing value - _schedule_clear_etag(instance) - - -@receiver(m2m_changed) -def schedule_etag_clearing_m2m( - sender: ModelBase, instance: models.Model, action: str, model: ModelBase, **kwargs -): - if action == "pre_clear": - handle_m2m_cleared(sender, instance, model) - return - - if action not in ["post_add", "post_clear", "post_remove"]: - return - - if not is_etag_model(model): - return - - if is_etag_model(type(instance)): - _schedule_clear_etag(instance) - - pk_set = kwargs["pk_set"] or () - instances = model.objects.filter(pk__in=pk_set) - for instance in instances: - _schedule_clear_etag(instance) diff --git a/vng_api_common/conf/api.py b/vng_api_common/conf/api.py index c5fb74d3..cd8bf431 100644 --- a/vng_api_common/conf/api.py +++ b/vng_api_common/conf/api.py @@ -28,9 +28,6 @@ # 'rest_framework.authentication.SessionAuthentication', # 'rest_framework.authentication.BasicAuthentication' ), - # there is no authentication of 'end-users', only authorization (via JWT) - # of applications - "DEFAULT_AUTHENTICATION_CLASSES": (), # 'DEFAULT_PERMISSION_CLASSES': ( # 'oauth2_provider.contrib.rest_framework.TokenHasReadWriteScope', # # 'rest_framework.permissions.IsAuthenticated', @@ -61,7 +58,22 @@ } BASE_SWAGGER_SETTINGS = { - "DEFAULT_GENERATOR_CLASS": "vng_api_common.generators.OpenAPISchemaGenerator", + # 'SECURITY_DEFINITIONS': { + # 'OAuth2': { + # 'type': 'oauth2', + # 'flow': 'application', + # 'tokenUrl': '/oauth2/token/', + # 'scopes': { + # 'write': 'Schrijftoegang tot de catalogus en gerelateerde objecten.', + # 'read': 'Leestoegang tot de catalogus en gerelateerde objecten.' + # } + # }, + # 'Bearer': { + # 'type': 'apiKey', + # 'name': 'Authorization', + # 'in': 'header' + # }, + # }, "DEFAULT_AUTO_SCHEMA_CLASS": "vng_api_common.inspectors.view.AutoSchema", "DEFAULT_INFO": "must.be.overridden", "DEFAULT_FIELD_INSPECTORS": ( diff --git a/vng_api_common/constants.py b/vng_api_common/constants.py index 41b60c18..29444732 100644 --- a/vng_api_common/constants.py +++ b/vng_api_common/constants.py @@ -1,5 +1,3 @@ -import warnings - from django.utils.translation import ugettext_lazy as _ from djchoices import ChoiceItem, DjangoChoices @@ -83,27 +81,9 @@ class RolTypes(DjangoChoices): medewerker = ChoiceItem("medewerker", "Medewerker") -BESLUIT_CONST = "besluit" -BESLUIT_CHOICE = ChoiceItem(BESLUIT_CONST, _("Besluit")) - -ZAAK_CONST = "zaak" -ZAAK_CHOICE = ChoiceItem(ZAAK_CONST, _("Zaak")) - -VERZOEK_CONST = "verzoek" -VERZOEK_CHOICE = ChoiceItem(VERZOEK_CONST, _("Verzoek")) - - class ObjectTypes(DjangoChoices): - besluit = BESLUIT_CHOICE - zaak = ZAAK_CHOICE - - def __init__(self, *args, **kwargs): - warnings.warn( - "The use of ObjectTypes is deprecated. Create your own " - "enumeration based on the relevant objects you need to support.", - DeprecationWarning, - ) - super().__init__(*args, **kwargs) + besluit = ChoiceItem("besluit", _("Besluit")) + zaak = ChoiceItem("zaak", _("Zaak")) class Archiefnominatie(DjangoChoices): @@ -295,7 +275,6 @@ class ComponentTypes(DjangoChoices): ztc = ChoiceItem("ztc", "Zaaktypecatalogus") drc = ChoiceItem("drc", "Documentregistratiecomponent") brc = ChoiceItem("brc", "Besluitregistratiecomponent") - kic = ChoiceItem("kic", "Klantinteractiescomponent") class CommonResourceAction(DjangoChoices): diff --git a/vng_api_common/generators.py b/vng_api_common/generators.py deleted file mode 100644 index 30d63a56..00000000 --- a/vng_api_common/generators.py +++ /dev/null @@ -1,108 +0,0 @@ -from collections import OrderedDict -from typing import List - -from drf_yasg import openapi -from drf_yasg.generators import ( - EndpointEnumerator as _EndpointEnumerator, - OpenAPISchemaGenerator as _OpenAPISchemaGenerator, -) -from rest_framework.schemas.utils import is_list_view - -from vng_api_common.utils import get_view_summary - - -class EndpointEnumerator(_EndpointEnumerator): - def get_allowed_methods(self, callback) -> list: - methods = super().get_allowed_methods(callback) - - # head requests are explicitly supported for endpoint that provide caching - conditional_retrieves = getattr(callback.cls, "_conditional_retrieves", []) - if not conditional_retrieves: - return methods - - if set(conditional_retrieves).intersection(callback.actions.values()): - methods.append("HEAD") - - return methods - - -class OpenAPISchemaGenerator(_OpenAPISchemaGenerator): - endpoint_enumerator_class = EndpointEnumerator - - def get_tags(self, request=None, public=False): - """Retrieve the tags for the root schema. - - :param request: the request used for filtering accessible endpoints and finding the spec URI - :param bool public: if True, all endpoints are included regardless of access through `request` - - :return: List of tags containing the tag name and a description. - """ - tags = {} - - endpoints = self.get_endpoints(request) - for path, (view_cls, methods) in sorted(endpoints.items()): - if "{" in path: - continue - - tag = path.rsplit("/", 1)[-1] - if tag in tags: - continue - - # exclude special non-rest actions - if tag.startswith("_"): - continue - tags[tag] = get_view_summary(view_cls) - - return [ - OrderedDict([("name", operation), ("description", desc)]) - for operation, desc in sorted(tags.items()) - ] - - def get_schema(self, request=None, public=False): - result = super().get_schema(request, public) - - # Add the tags on the root schema. - result.tags = self.get_tags(request, public) - - return result - - def get_path_parameters(self, path, view_cls): - """Return a list of Parameter instances corresponding to any templated path variables. - - :param str path: templated request path - :param type view_cls: the view class associated with the path - :return: path parameters - :rtype: list[openapi.Parameter] - """ - parameters = super().get_path_parameters(path, view_cls) - - # see if we can specify UUID a bit more - for parameter in parameters: - # the most pragmatic of checks - if not parameter.name.endswith("_uuid"): - continue - parameter.format = openapi.FORMAT_UUID - parameter.description = "Unieke resource identifier (UUID4)" - return parameters - - def get_operation_keys(self, subpath, method, view) -> List[str]: - if method != "HEAD": - return super().get_operation_keys(subpath, method, view) - - assert not is_list_view( - subpath, method, view - ), "HEAD requests are only supported on detail endpoints" - - # taken from DRF schema generation - named_path_components = [ - component - for component in subpath.strip("/").split("/") - if "{" not in component - ] - - return named_path_components + ["headers"] - - def get_overrides(self, view, method) -> dict: - if method == "HEAD": - return {} - return super().get_overrides(view, method) diff --git a/vng_api_common/inspectors/cache.py b/vng_api_common/inspectors/cache.py deleted file mode 100644 index c1d4fc7f..00000000 --- a/vng_api_common/inspectors/cache.py +++ /dev/null @@ -1,57 +0,0 @@ -from collections import OrderedDict - -from django.utils.translation import ugettext_lazy as _ - -from drf_yasg import openapi -from rest_framework.views import APIView - -from ..caching.introspection import has_cache_header - -CACHE_REQUEST_HEADERS = [ - openapi.Parameter( - name="If-None-Match", - type=openapi.TYPE_STRING, - in_=openapi.IN_HEADER, - required=False, - description=_( - "Perform conditional requests. This header should contain one or " - "multiple ETag values of resources the client has cached. If the " - "current resource ETag value is in this set, then an HTTP 304 " - "empty body will be returned. See " - "[MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) " - "for details." - ), - examples={ - "oneValue": { - "summary": _("One ETag value"), - "value": '"79054025255fb1a26e4bc422aef54eb4"', - }, - "multipleValues": { - "summary": _("Multiple ETag values"), - "value": '"79054025255fb1a26e4bc422aef54eb4", "e4d909c290d0fb1ca068ffaddf22cbd0"', - }, - }, - ) -] - - -def get_cache_headers(view: APIView) -> OrderedDict: - if not has_cache_header(view): - return OrderedDict() - - return OrderedDict( - ( - ( - "ETag", - openapi.Schema( - type=openapi.TYPE_STRING, - description=_( - "De ETag berekend op de response body JSON. " - "Indien twee resources exact dezelfde ETag hebben, dan zijn " - "deze resources identiek aan elkaar. Je kan de ETag gebruiken " - "om caching te implementeren." - ), - ), - ), - ) - ) diff --git a/vng_api_common/inspectors/query.py b/vng_api_common/inspectors/query.py index adf6d7e4..a14cd7fb 100644 --- a/vng_api_common/inspectors/query.py +++ b/vng_api_common/inspectors/query.py @@ -34,7 +34,7 @@ def get_filter_parameters(self, filter_backend): help_text = filter_field.extra.get( "help_text", - getattr(model_field, "help_text", "") if model_field else "", + getattr(model_field, "help_text", "") if model_field else "" ) if isinstance(filter_field, URLModelChoiceFilter): diff --git a/vng_api_common/inspectors/view.py b/vng_api_common/inspectors/view.py index 94f89896..88a33c21 100644 --- a/vng_api_common/inspectors/view.py +++ b/vng_api_common/inspectors/view.py @@ -2,7 +2,7 @@ import logging from collections import OrderedDict from itertools import chain -from typing import Optional, Tuple, Union +from typing import Union from django.apps import apps from django.conf import settings @@ -10,7 +10,6 @@ from drf_yasg import openapi from drf_yasg.inspectors import SwaggerAutoSchema -from drf_yasg.utils import get_consumes from rest_framework import exceptions, serializers, status, viewsets from ..constants import HEADER_APPLICATION, HEADER_AUDIT, HEADER_USER_ID, VERSION_HEADER @@ -23,7 +22,6 @@ ValidatieFoutSerializer, add_choice_values_help_text, ) -from .cache import CACHE_REQUEST_HEADERS, get_cache_headers, has_cache_header logger = logging.getLogger(__name__) @@ -363,23 +361,17 @@ def get_default_responses(self) -> OrderedDict: # inject any headers _responses = OrderedDict() - custom_headers = OrderedDict() for status_, schema in responses.items(): - if serializer is not None: - custom_headers = ( - self.probe_inspectors( - self.field_inspectors, - "get_response_headers", - serializer, - {"field_inspectors": self.field_inspectors}, - status=status_, - ) - or OrderedDict() + custom_headers = ( + self.probe_inspectors( + self.field_inspectors, + "get_response_headers", + serializer, + {"field_inspectors": self.field_inspectors}, + status=status_, ) - - # add the cache headers, if applicable - for header, header_schema in get_cache_headers(self.view).items(): - custom_headers[header] = header_schema + or None + ) assert isinstance(schema, openapi.Schema.OR_REF) or schema == "" response = openapi.Response( @@ -427,48 +419,23 @@ def get_response_schemas(self, response_serializers): return responses - def get_request_content_type_header(self) -> Optional[openapi.Parameter]: - if self.method not in ["POST", "PUT", "PATCH"]: - return None - - consumes = get_consumes(self.get_parser_classes()) - return openapi.Parameter( - name="Content-Type", - in_=openapi.IN_HEADER, - type=openapi.TYPE_STRING, - required=True, - enum=consumes, - description=_("Content type of the request body."), - ) - def add_manual_parameters(self, parameters): base = super().add_manual_parameters(parameters) - - content_type = self.get_request_content_type_header() - if content_type is not None: - base = [content_type] + base - if self._is_search_view: serializer = self.get_request_serializer() else: serializer = self.get_request_serializer() or self.get_view_serializer() - - extra = [] - if serializer is not None: - extra = ( - self.probe_inspectors( - self.field_inspectors, - "get_request_header_parameters", - serializer, - {"field_inspectors": self.field_inspectors}, - ) - or [] + extra = ( + self.probe_inspectors( + self.field_inspectors, + "get_request_header_parameters", + serializer, + {"field_inspectors": self.field_inspectors}, ) + or [] + ) result = base + extra - if has_cache_header(self.view): - result += CACHE_REQUEST_HEADERS - if _view_supports_audittrail(self.view): result += AUDIT_REQUEST_HEADERS @@ -499,7 +466,7 @@ def get_security(self): required_scopes = [] for perm in scope_permissions: - scopes = get_required_scopes(self.request, self.view) + scopes = get_required_scopes(self.view) if scopes is None: continue required_scopes.append(scopes) @@ -512,27 +479,6 @@ def get_security(self): # operation level security return [{settings.SECURITY_DEFINITION_NAME: scopes}] - # all of these break if you accept method HEAD because the view.action is None - def is_list_view(self) -> bool: - if self.method == "HEAD": - return False - return super().is_list_view() - - def get_summary_and_description(self) -> Tuple[str, str]: - if self.method != "HEAD": - return super().get_summary_and_description() - - default_description = _( - "De headers voor een specifiek(e) {model_name} opvragen" - ).format(model_name=self.model._meta.model_name.upper()) - default_summary = _( - "Vraag de headers op die je bij een GET request zou krijgen." - ) - - description = self.overrides.get("operation_description", default_description) - summary = self.overrides.get("operation_summary", default_summary) - return description, summary - # patch around drf-yasg not taking overrides into account # TODO: contribute back in PR def get_produces(self) -> list: diff --git a/vng_api_common/locale/nl/LC_MESSAGES/django.mo b/vng_api_common/locale/nl/LC_MESSAGES/django.mo index 8427ef10..54d03c2d 100644 Binary files a/vng_api_common/locale/nl/LC_MESSAGES/django.mo and b/vng_api_common/locale/nl/LC_MESSAGES/django.mo differ diff --git a/vng_api_common/locale/nl/LC_MESSAGES/django.po b/vng_api_common/locale/nl/LC_MESSAGES/django.po index c7e216d9..22dbd5af 100644 --- a/vng_api_common/locale/nl/LC_MESSAGES/django.po +++ b/vng_api_common/locale/nl/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-01-06 04:04-0600\n" +"POT-Creation-Date: 2019-07-18 07:04-0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -20,66 +20,59 @@ msgstr "" #: admin.py:18 msgid "external API" -msgstr "externe API" +msgstr "" -#: admin.py:20 +#: admin.py:24 msgid "credentials" -msgstr "authenticatiegegevens" +msgstr "" -#: admin.py:23 +#: admin.py:25 msgid "" "Credentials that indicate how this API or application identifies itself at " "the external API." msgstr "" -"Authenticatiegegevens die aangeven hoe deze API of applicatie zichzelf " -"identificeert bij de externe API." -#: audittrails/models.py:18 +#: audittrails/models.py:16 msgid "Unieke identificatie van de audit regel." msgstr "" -#: audittrails/models.py:24 -msgid "" -"Een globaal \"request\" ID om een verzoek door het netwerk heen te traceren." -msgstr "" - -#: audittrails/models.py:30 +#: audittrails/models.py:20 msgid "De naam van het component waar de wijziging in is gedaan." msgstr "" -#: audittrails/models.py:33 +#: audittrails/models.py:25 msgid "De uitgevoerde handeling." msgstr "" -#: audittrails/models.py:35 +#: audittrails/models.py:30 msgid "Vriendelijke naam van de actie." msgstr "" -#: audittrails/models.py:39 +#: audittrails/models.py:33 msgid "HTTP status code van de API response van de uitgevoerde handeling." msgstr "" -#: audittrails/models.py:44 +#: audittrails/models.py:38 msgid "De URL naar het hoofdobject van een component." msgstr "" -#: audittrails/models.py:47 +#: audittrails/models.py:42 msgid "Het type resource waarop de actie gebeurde." msgstr "" -#: audittrails/models.py:50 +#: audittrails/models.py:46 msgid "De URL naar het object." msgstr "" -#: audittrails/models.py:53 +#: audittrails/models.py:50 msgid "De datum waarop de handeling is gedaan." msgstr "" -#: audittrails/models.py:56 +#: audittrails/models.py:54 msgid "Vriendelijke identificatie van het object." msgstr "" -#: audittrails/models.py:61 +#: audittrails/models.py:59 msgid "Unieke identificatie van de applicatie, binnen de organisatie." msgstr "" @@ -87,35 +80,35 @@ msgstr "" msgid "Vriendelijke naam van de applicatie." msgstr "" -#: audittrails/models.py:70 +#: audittrails/models.py:69 msgid "" "Volledige JSON body van het object zoals dat bestond voordat de actie heeft " "plaatsgevonden." msgstr "" -#: audittrails/models.py:76 +#: audittrails/models.py:74 msgid "Volledige JSON body van het object na de actie." msgstr "" -#: audittrails/models.py:83 +#: audittrails/models.py:80 msgid "" "Unieke identificatie van de gebruiker die binnen de organisatie herleid kan " "worden naar een persoon." msgstr "" -#: audittrails/models.py:88 +#: audittrails/models.py:86 msgid "Vriendelijke naam van de gebruiker." msgstr "" -#: audittrails/models.py:91 +#: audittrails/models.py:89 msgid "toelichting" msgstr "" -#: audittrails/models.py:93 +#: audittrails/models.py:91 msgid "Toelichting waarom de handeling is uitgevoerd." msgstr "" -#: authorizations/models.py:16 authorizations/models.py:67 +#: authorizations/models.py:16 authorizations/models.py:63 msgid "component" msgstr "" @@ -131,77 +124,73 @@ msgstr "" msgid "Komma-gescheiden lijst van consumer identifiers (hun client_id)." msgstr "" -#: authorizations/models.py:42 +#: authorizations/models.py:41 msgid "Een leesbare representatie van de applicatie, voor eindgebruikers." msgstr "" -#: authorizations/models.py:46 +#: authorizations/models.py:44 msgid "heeft alle autorisaties" msgstr "" -#: authorizations/models.py:49 +#: authorizations/models.py:46 msgid "" "Indien alle autorisaties gegeven zijn, dan hoeven deze niet individueel " "opgegeven te worden. Gebruik dit alleen als je de consumer helemaal " "vertrouwt." msgstr "" -#: authorizations/models.py:64 +#: authorizations/models.py:60 msgid "applicatie" msgstr "" -#: authorizations/models.py:70 +#: authorizations/models.py:64 msgid "Component waarop autorisatie van toepassing is." msgstr "" -#: authorizations/models.py:74 +#: authorizations/models.py:68 msgid "scopes" msgstr "" -#: authorizations/models.py:75 +#: authorizations/models.py:69 msgid "Komma-gescheiden lijst van scope labels." msgstr "" -#: authorizations/models.py:80 +#: authorizations/models.py:74 msgid "zaaktype" msgstr "" -#: authorizations/models.py:81 +#: authorizations/models.py:75 msgid "URL naar het zaaktype waarop de autorisatie van toepassing is." msgstr "" -#: authorizations/models.py:88 +#: authorizations/models.py:81 msgid "informatieobjecttype" msgstr "" -#: authorizations/models.py:90 +#: authorizations/models.py:82 msgid "" "URL naar het informatieobjecttype waarop de autorisatie van toepassing is." msgstr "" -#: authorizations/models.py:98 +#: authorizations/models.py:88 msgid "besluittype" msgstr "" -#: authorizations/models.py:100 +#: authorizations/models.py:89 msgid "URL naar het besluittype waarop de autorisatie van toepassing is." msgstr "" -#: authorizations/models.py:108 +#: authorizations/models.py:95 msgid "Maximaal toegelaten vertrouwelijkheidaanduiding (inclusief)." msgstr "" -#: authorizations/serializers.py:37 -msgid "Omschrijving van `component`." -msgstr "" - -#: authorizations/serializers.py:46 +#: authorizations/serializers.py:47 msgid "" "Lijst van scope labels. Elke scope geeft toegang tot een set van acties/" "operaties, zoals gedocumenteerd bij de betreffende component." msgstr "" -#: authorizations/serializers.py:79 +#: authorizations/serializers.py:82 msgid "" "Lijst van consumer identifiers (hun 'client_id'). Een `client_id` mag " "slechts bij één applicatie-object voorkomen." @@ -209,162 +198,145 @@ msgstr "" #: authorizations/serializers.py:103 msgid "Either autorisaties or heeft_alle_autorisaties can be specified" -msgstr "Specifieer of autorisaties, of heeft_alle_autorisaties" +msgstr "" -#: authorizations/serializers.py:109 +#: authorizations/serializers.py:108 msgid "Either autorisaties or heeft_alle_autorisaties should be specified" -msgstr "Je moet of autorisaties, of heeft_alle_autorisaties specifiëren" +msgstr "" -#: authorizations/validators.py:16 +#: authorizations/validators.py:12 #, python-brace-format msgid "The clientID(s) {client_id} are already used in application(s) {app_id}" -msgstr "De client ID(s) {client_id} wordt al gebruikt in applicatie(s) {app_id}" - -#: authorizations/validators.py:51 -#, python-brace-format -msgid "This field is required if `component` is {component}" -msgstr "Dit veld is verplicht als de `component` {component} is" - -#: caching/models.py:15 -msgid "etag value" -msgstr "waarde etag" - -#: caching/models.py:17 -msgid "MD5 hash of the resource representation in its current version." -msgstr "MD5-hash van de resourceweergave in de huidige versie." +msgstr "" -#: constants.py:87 +#: constants.py:75 msgid "Besluit" msgstr "" -#: constants.py:90 +#: constants.py:76 msgid "Zaak" msgstr "" -#: constants.py:93 -msgid "Verzoek" -msgstr "" - -#: constants.py:113 +#: constants.py:82 msgid "" "Het zaakdossier moet bewaard blijven en op de Archiefactiedatum overgedragen " "worden naar een archiefbewaarplaats." msgstr "" -#: constants.py:119 +#: constants.py:87 msgid "Het zaakdossier moet op of na de Archiefactiedatum vernietigd worden." msgstr "" -#: constants.py:126 +#: constants.py:94 msgid "De zaak cq. het zaakdossier is nog niet als geheel gearchiveerd." msgstr "" -#: constants.py:131 +#: constants.py:98 msgid "" "De zaak cq. het zaakdossier is als geheel niet-wijzigbaar bewaarbaar gemaakt." msgstr "" -#: constants.py:137 +#: constants.py:102 msgid "" "De zaak cq. het zaakdossier is als geheel niet-wijzigbaar bewaarbaar gemaakt " "maar de vernietigingsdatum kan nog niet bepaald worden." msgstr "" -#: constants.py:149 +#: constants.py:113 msgid "" "De zaak cq. het zaakdossier is overgebracht naar een archiefbewaarplaats." msgstr "" -#: constants.py:156 +#: constants.py:119 msgid "Afgehandeld" msgstr "" -#: constants.py:158 +#: constants.py:120 msgid "" "De termijn start op de datum waarop de zaak is afgehandeld (ZAAK.Einddatum " "in het RGBZ)." msgstr "" -#: constants.py:164 +#: constants.py:124 msgid "Ander datumkenmerk" msgstr "" -#: constants.py:166 +#: constants.py:125 msgid "" "De termijn start op de datum die is vastgelegd in een ander datumveld dan de " "datumvelden waarop de overige waarden (van deze attribuutsoort) betrekking " "hebben. `Objecttype`, `Registratie` en `Datumkenmerk` zijn niet leeg." msgstr "" -#: constants.py:175 +#: constants.py:132 msgid "Eigenschap" msgstr "" -#: constants.py:177 +#: constants.py:133 msgid "" "De termijn start op de datum die vermeld is in een zaaktype-specifieke " "eigenschap (zijnde een `datumveld`). `ResultaatType.ZaakType` heeft een " "`Eigenschap`; `Objecttype`, en `Datumkenmerk` zijn niet leeg." msgstr "" -#: constants.py:185 +#: constants.py:139 msgid "Gerelateerde zaak" msgstr "" -#: constants.py:187 +#: constants.py:140 msgid "" "De termijn start op de datum waarop de gerelateerde zaak is afgehandeld " "(`ZAAK.Einddatum` of `ZAAK.Gerelateerde_zaak.Einddatum` in het RGBZ). " "`ResultaatType.ZaakType` heeft gerelateerd `ZaakType`" msgstr "" -#: constants.py:195 -msgid "Hoofdzaak" +#: constants.py:146 +msgid "Hoofzaak" msgstr "" -#: constants.py:197 +#: constants.py:147 msgid "" "De termijn start op de datum waarop de gerelateerde zaak is afgehandeld, " "waarvan de zaak een deelzaak is (`ZAAK.Einddatum` van de hoofdzaak in het " "RGBZ). ResultaatType.ZaakType is deelzaaktype van ZaakType." msgstr "" -#: constants.py:205 +#: constants.py:153 msgid "Ingangsdatum besluit" msgstr "" -#: constants.py:207 +#: constants.py:154 msgid "" "De termijn start op de datum waarop het besluit van kracht wordt (`BESLUIT." "Ingangsdatum` in het RGBZ).\tResultaatType.ZaakType heeft relevant " "BesluitType" msgstr "" -#: constants.py:214 +#: constants.py:159 msgid "Termijn" msgstr "" -#: constants.py:216 +#: constants.py:160 msgid "" "De termijn start een vast aantal jaren na de datum waarop de zaak is " "afgehandeld (`ZAAK.Einddatum` in het RGBZ)." msgstr "" -#: constants.py:223 +#: constants.py:165 msgid "Vervaldatum besluit" msgstr "" -#: constants.py:225 +#: constants.py:166 msgid "" "De termijn start op de dag na de datum waarop het besluit vervalt (`BESLUIT." "Vervaldatum` in het RGBZ). ResultaatType.ZaakType heeft relevant BesluitType" msgstr "" -#: constants.py:232 +#: constants.py:171 msgid "Zaakobject" msgstr "" -#: constants.py:234 +#: constants.py:172 msgid "" "De termijn start op de einddatum geldigheid van het zaakobject waarop de " "zaak betrekking heeft (bijvoorbeeld de overlijdendatum van een Persoon). M.b." @@ -373,41 +345,41 @@ msgid "" "datum-attribuutsoort van het zaakobjecttype het betreft." msgstr "" -#: constants.py:302 +#: constants.py:226 msgid "Object aangemaakt" msgstr "" -#: constants.py:303 +#: constants.py:227 msgid "Lijst van objecten opgehaald" msgstr "" -#: constants.py:304 +#: constants.py:228 msgid "Object opgehaald" msgstr "" -#: constants.py:305 +#: constants.py:229 msgid "Object verwijderd" msgstr "" -#: constants.py:306 +#: constants.py:230 msgid "Object bijgewerkt" msgstr "" -#: constants.py:307 +#: constants.py:231 msgid "Object deels bijgewerkt" msgstr "" -#: constants.py:311 +#: constants.py:235 msgid "Hoort bij, omgekeerd: kent" msgstr "" -#: constants.py:313 +#: constants.py:236 msgid "Legt vast, omgekeerd: kan vastgelegd zijn als" msgstr "" #: exceptions.py:9 msgid "A conflict occurred" -msgstr "Er is een conflict" +msgstr "" #: exceptions.py:15 msgid "The resource is gone" @@ -425,96 +397,57 @@ msgstr "" msgid "BSN" msgstr "" -#: fields.py:190 +#: fields.py:187 msgid "Specifieer de duur als 'DD 00:00'" msgstr "" -#: filters.py:101 +#: filters.py:76 #, python-format msgid "Invalid resource type supplied, expected %r" msgstr "Invalide resourcetype opgegeven, verwachtte %r" -#: inspectors/cache.py:17 -msgid "" -"Perform conditional requests. This header should contain one or multiple " -"ETag values of resources the client has cached. If the current resource ETag " -"value is in this set, then an HTTP 304 empty body will be returned. See [MDN]" -"(https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) " -"for details." -msgstr "" -"Voer een voorwaardelijk verzoek uit. Deze header moet één of meerdere ETag-" -"waardes bevatten van resources die de consumer gecached heeft. Indien de " -"waarde van de ETag van de huidige resource voorkomt in deze set, dan " -"antwoordt de provider met een lege HTTP 304 request. Zie [MDN](https://" -"developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match) voor meer " -"informatie." - -#: inspectors/cache.py:26 -msgid "One ETag value" -msgstr "Eén ETag-waarde" - -#: inspectors/cache.py:30 -msgid "Multiple ETag values" -msgstr "Meerdere ETag-waardes" - -#: inspectors/cache.py:49 -msgid "" -"De ETag berekend op de response body JSON. Indien twee resources exact " -"dezelfde ETag hebben, dan zijn deze resources identiek aan elkaar. Je kan de " -"ETag gebruiken om caching te implementeren." -msgstr "" +#: filters.py:85 +msgid "Invalid resource URL supplied" +msgstr "Ongeldige resource URL opgegeven" -#: inspectors/files.py:48 +#: inspectors/files.py:38 msgid "Base64 encoded binary content." msgstr "Binaire inhoud, in base64 geëncodeerd." -#: inspectors/files.py:54 +#: inspectors/files.py:44 msgid "Download URL of the binary content." msgstr "Download URL van de binaire inhoud." -#: inspectors/query.py:40 +#: inspectors/query.py:36 #, python-brace-format msgid "URL to the related {resource}" msgstr "URL naar de/het gerelateerde {resource}" -#: inspectors/view.py:117 +#: inspectors/view.py:129 msgid "Application that performs request" msgstr "" "Identificatie van de applicatie die het verzoek stuurt (indien NLX wordt " "gebruikt)." -#: inspectors/view.py:124 +#: inspectors/view.py:136 msgid "Identifier of the user that performs request" msgstr "" "Identificatie van de gebruiker die het verzoek stuurt (indien NLX wordt " "gebruikt)." -#: inspectors/view.py:131 +#: inspectors/view.py:143 msgid "Explanation why the request is done" msgstr "Toelichting waarom een bepaald verzoek wordt gedaan" -#: inspectors/view.py:375 -msgid "Content type of the request body." -msgstr "Content type van de verzoekinhoud." - -#: inspectors/view.py:460 -#, python-brace-format -msgid "De headers voor een specifiek(e) {model_name} opvragen" -msgstr "" - -#: inspectors/view.py:463 -msgid "Vraag de headers op die je bij een GET request zou krijgen." -msgstr "" - -#: inspectors/view.py:478 +#: inspectors/view.py:426 msgid "A page number within the paginated result set." msgstr "Een pagina binnen de gepagineerde set resultaten." -#: inspectors/view.py:479 +#: inspectors/view.py:427 msgid "Number of results to return per page." msgstr "Het aantal resultaten terug te geven per pagina." -#: middleware.py:65 +#: middleware.py:71 msgid "" "Component could not authenticate against the AC - authorizations could not " "be retrieved" @@ -522,250 +455,191 @@ msgstr "" "De component kon zich niet autoriseren met het AC - de autorisaties konden " "daardoor niet opgehaald worden." -#: middleware.py:105 middleware.py:120 +#: middleware.py:109 middleware.py:122 msgid "JWT could not be decoded. Possibly you made a copy-paste mistake." msgstr "" "Het JWT kon niet gedecodeerd worden. Mogelijks heb je een copy-paste fout " "gemaakt" -#: middleware.py:135 +#: middleware.py:134 msgid "" "The support of authorization old format will be terminated soon. Please use " "a new format with the separate Authorization Component" msgstr "" -"De ondersteuning van het oude autorisatiesformaat wordt binnenkort gestopt. " -"Gelieve het nieuwe formaat met de Autorisaties API te gebruiken." -#: models.py:46 models.py:87 notifications/models.py:43 +#: models.py:48 models.py:80 notifications/models.py:40 msgid "client ID" msgstr "" -#: models.py:50 +#: models.py:49 msgid "" "Client ID to identify external API's and applications that access this API." msgstr "" "Client ID om externe APIs and applicaties te identificeren die toegang " "hebben tot deze API." -#: models.py:54 models.py:92 +#: models.py:52 models.py:84 msgid "secret" -msgstr "geheime sleutel" +msgstr "" -#: models.py:54 models.py:92 +#: models.py:53 models.py:85 msgid "Secret belonging to the client ID." -msgstr "Geheime sleutel die bij de client ID hoort." +msgstr "" -#: models.py:58 +#: models.py:57 msgid "client credential" -msgstr "Autorisatiegegeven" +msgstr "" -#: models.py:59 +#: models.py:58 msgid "client credentials" -msgstr "Autorisatiegegevens" +msgstr "" -#: models.py:74 +#: models.py:72 msgid "API-root" msgstr "" -#: models.py:77 +#: models.py:73 msgid "" "URL of the external API, ending in a trailing slash. Example: https://" "example.com/api/v1/" msgstr "" -"URL van de externe API, eindigend met een slash (/). Bijvoorbeeld: " -"https://example.com/api/v1/" -#: models.py:81 +#: models.py:76 msgid "label" msgstr "" -#: models.py:84 +#: models.py:77 msgid "Human readable label of the external API." -msgstr "Vriendelijke weergave van de externe API." +msgstr "" -#: models.py:89 +#: models.py:81 msgid "Client ID to identify this API at the external API." -msgstr "Client ID om deze API te identificeren bij de externe API." +msgstr "" -#: models.py:95 +#: models.py:88 msgid "user ID" -msgstr "gebruikers ID" +msgstr "" -#: models.py:98 +#: models.py:89 msgid "" "User ID to use for the audit trail. Although these external API credentials " "are typically used bythis API itself instead of a user, the user ID is " "required." msgstr "" -"Gebruikers ID om in de audittrail te gebruiken. Ondanks dat deze externe " -"API-autorisatiegegevens typisch gebruikt worden door deze API zelf en niet " -"een eindgebruiker, is het gebruikers ID verplicht." -#: models.py:103 +#: models.py:93 msgid "user representation" -msgstr "gebruikersweergave" +msgstr "" -#: models.py:106 +#: models.py:94 msgid "Human readable representation of the user." -msgstr "Vriendelijke weergave van de eindgebruiker." +msgstr "" -#: models.py:110 +#: models.py:98 msgid "external API credential" msgstr "externe API credential" -#: models.py:111 +#: models.py:99 msgid "external API credentials" msgstr "externe API credentials" -#: models.py:146 +#: models.py:135 msgid "api root" msgstr "" -#: notifications/admin.py:33 +#: notifications/admin.py:29 msgid "Register the webhooks" -msgstr "Registreer de webhooks" - -#: notifications/api/serializers.py:10 -msgid "kanaal" -msgstr "" - -#: notifications/api/serializers.py:13 -msgid "" -"De naam van het kanaal (`KANAAL.naam`) waar het bericht op moet worden " -"gepubliceerd." msgstr "" -#: notifications/api/serializers.py:18 -msgid "hoofd object" +#: notifications/api/serializers.py:7 +msgid "kanaal" msgstr "" -#: notifications/api/serializers.py:20 -msgid "" -"URL-referentie naar het hoofd object van de publicerende API die betrekking " -"heeft op de `resource`." +#: notifications/api/serializers.py:8 +msgid "URL naar het hoofdobject" msgstr "" -#: notifications/api/serializers.py:25 +#: notifications/api/serializers.py:9 msgid "resource" msgstr "" -#: notifications/api/serializers.py:27 -msgid "De resourcenaam waar de notificatie over gaat." -msgstr "" - -#: notifications/api/serializers.py:30 -msgid "resource URL" -msgstr "" - -#: notifications/api/serializers.py:31 -msgid "URL-referentie naar de `resource` van de publicerende API." +#: notifications/api/serializers.py:10 +msgid "URL naar de resource waarover de notificatie gaat" msgstr "" -#: notifications/api/serializers.py:34 +#: notifications/api/serializers.py:11 msgid "actie" msgstr "" -#: notifications/api/serializers.py:37 -msgid "" -"De actie die door de publicerende API is gedaan. De publicerende API " -"specificeert de toegestane acties." -msgstr "" - -#: notifications/api/serializers.py:42 +#: notifications/api/serializers.py:12 msgid "aanmaakdatum" msgstr "" -#: notifications/api/serializers.py:44 -msgid "Datum en tijd waarop de actie heeft plaatsgevonden." -msgstr "" - -#: notifications/api/serializers.py:47 -msgid "kenmerken" -msgstr "" - -#: notifications/api/serializers.py:50 -msgid "kenmerk" -msgstr "" - -#: notifications/api/serializers.py:52 -msgid "Een waarde behorende bij de sleutel." -msgstr "" - -#: notifications/api/serializers.py:55 -msgid "" -"Mapping van kenmerken (sleutel/waarde) van de notificatie. De publicerende " -"API specificeert de toegestane kenmerken." -msgstr "" - #: notifications/apps.py:7 msgid "Notificaties" msgstr "" -#: notifications/models.py:21 +#: notifications/models.py:20 msgid "Notificatiescomponentconfiguratie" msgstr "" -#: notifications/models.py:40 +#: notifications/models.py:36 msgid "callback url" msgstr "" -#: notifications/models.py:40 +#: notifications/models.py:37 msgid "Where to send the notifications (webhook url)" msgstr "" -#: notifications/models.py:45 +#: notifications/models.py:41 msgid "Client ID to construct the auth token" msgstr "" -#: notifications/models.py:48 +#: notifications/models.py:44 msgid "client secret" msgstr "" -#: notifications/models.py:50 +#: notifications/models.py:45 msgid "Secret to construct the auth token" msgstr "" -#: notifications/models.py:54 +#: notifications/models.py:49 msgid "channels" msgstr "" -#: notifications/models.py:55 +#: notifications/models.py:50 msgid "Comma-separated list of channels to subscribe to" msgstr "" -#: notifications/models.py:59 +#: notifications/models.py:54 msgid "NC subscription" msgstr "" -#: notifications/models.py:62 +#: notifications/models.py:55 msgid "Subscription as it is known in the NC" msgstr "" -#: notifications/models.py:66 +#: notifications/models.py:59 msgid "Webhook subscription" msgstr "" -#: notifications/models.py:67 +#: notifications/models.py:60 msgid "Webhook subscriptions" msgstr "" -#: permissions.py:112 -msgid "The object does not exist in the database" -msgstr "" - -#: validators.py:117 +#: validators.py:111 #, python-brace-format msgid "" "The URL {url} responded with HTTP {status_code}. Please provide a valid URL." msgstr "" "De URL {url} gaf als antwoord HTTP {status_code}. Geef een geldige URL op." -#: validators.py:141 +#: validators.py:134 #, python-brace-format msgid "The URL {url} could not be fetched. Exception: {exc}" msgstr "De URL {url} kon niet opgehaald worden. Foutmelding: {exc}" -#: validators.py:169 +#: validators.py:161 #, python-brace-format msgid "" "The URL {url} resource did not look like a(n) `{resource}`. Please provide a " @@ -774,109 +648,105 @@ msgstr "" "De URL {url} resource lijkt niet op een `{resource}`. Geef een validate URL " "op." -#: validators.py:229 +#: validators.py:217 msgid "Het informatieobject is in het DRC nog niet gerelateerd aan dit object." msgstr "" -#: validators.py:275 +#: validators.py:259 msgid "Ensure this value is not in the future." msgstr "Deze waarde mag niet in de toekomst liggen." -#: validators.py:312 +#: validators.py:296 msgid "Deze identificatie bestaat al binnen de organisatie" msgstr "" -#: validators.py:367 +#: validators.py:349 msgid "Dit veld mag niet gewijzigd worden." msgstr "" -#: validators.py:395 -msgid "The resource {url} is not published." -msgstr "De resource {url} is (nog) niet gepubliceerd." - -#: views.py:118 +#: views.py:101 msgid "Site domain" msgstr "" -#: views.py:119 +#: views.py:102 msgid "HTTPS" msgstr "" -#: views.py:136 +#: views.py:119 msgid "Type of component" msgstr "" -#: views.py:137 +#: views.py:120 msgid "AC" msgstr "" -#: views.py:139 +#: views.py:122 msgid "Credentials for AC" msgstr "" -#: views.py:140 views.py:185 +#: views.py:123 views.py:174 msgid "Configured" msgstr "" -#: views.py:140 views.py:185 +#: views.py:123 views.py:174 msgid "Missing" msgstr "" -#: views.py:155 +#: views.py:139 msgid "Could not connect with AC" msgstr "" -#: views.py:159 +#: views.py:142 #, python-brace-format msgid "Cannot retrieve authorizations: HTTP {status_code} - {error_code}" msgstr "" -#: views.py:162 +#: views.py:147 msgid "Can retrieve authorizations" msgstr "" -#: views.py:164 +#: views.py:150 msgid "AC connection and authorizations" msgstr "" -#: views.py:182 +#: views.py:171 msgid "NRC" msgstr "" -#: views.py:184 +#: views.py:173 msgid "Credentials for NRC" msgstr "" -#: views.py:198 +#: views.py:187 msgid "Could not connect with NRC" msgstr "" -#: views.py:202 +#: views.py:190 #, python-brace-format msgid "Cannot retrieve kanalen: HTTP {status_code} - {error_code}" msgstr "" -#: views.py:205 +#: views.py:195 msgid "Can retrieve kanalen" msgstr "" -#: views.py:207 +#: views.py:198 msgid "NRC connection and authorizations" msgstr "" -#: views.py:225 +#: views.py:219 msgid "Listens to AC notifications?" msgstr "" -#: views.py:225 +#: views.py:220 msgid "Yes" msgstr "" -#: views.py:225 +#: views.py:220 msgid "No" msgstr "" -#: viewsets.py:62 +#: viewsets.py:61 #, python-format msgid "Onbekende query parameters: %s" msgstr "" diff --git a/vng_api_common/management/commands/generate_swagger.py b/vng_api_common/management/commands/generate_swagger.py index 67942f7c..cf683980 100644 --- a/vng_api_common/management/commands/generate_swagger.py +++ b/vng_api_common/management/commands/generate_swagger.py @@ -2,11 +2,10 @@ import os from django.apps import apps -from django.conf import settings -from django.contrib.auth import get_user_model +from django.contrib.auth.models import User from django.core.exceptions import ImproperlyConfigured from django.template.loader import render_to_string -from django.urls import NoReverseMatch, reverse +from django.urls import reverse from django.utils.module_loading import import_string from drf_yasg import openapi @@ -14,6 +13,7 @@ from drf_yasg.management.commands import generate_swagger from rest_framework.settings import api_settings +from ...schema import OpenAPISchemaGenerator from ...version import get_major_version @@ -85,12 +85,11 @@ def handle( format, api_url, mock, - api_version, user, private, - generator_class_name, info=None, urlconf=None, + *args, **options, ): # disable logs of WARNING and below @@ -100,7 +99,6 @@ def handle( info = import_string(info) else: info = getattr(swagger_settings, "DEFAULT_INFO", None) - if not isinstance(info, openapi.Info): raise ImproperlyConfigured( 'settings.SWAGGER_SETTINGS["DEFAULT_INFO"] should be an ' @@ -112,41 +110,25 @@ def handle( format = "yaml" format = format or "json" - try: - api_root = reverse("api-root", kwargs={"version": get_major_version()}) - except NoReverseMatch: - api_root = reverse("api-root") - + api_root = reverse("api-root", kwargs={"version": get_major_version()}) api_url = ( api_url or swagger_settings.DEFAULT_API_URL # noqa or f"http://example.com{api_root}" # noqa ) - if user: - # Only call get_user_model if --user was passed in order to - # avoid crashing if auth is not configured in the project - user = get_user_model().objects.get(username=user) - - mock = mock or private or (user is not None) or (api_version is not None) + user = User.objects.get(username=user) if user else None + mock = mock or private or (user is not None) if mock and not api_url: raise ImproperlyConfigured( "--mock-request requires an API url; either provide " "the --url argument or set the DEFAULT_API_URL setting" ) - request = None - if mock: - request = self.get_mock_request(api_url, format, user) + request = self.get_mock_request(api_url, format, user) if mock else None - api_version = api_version or api_settings.DEFAULT_VERSION - if request and api_version: - request.version = api_version - - generator = self.get_schema_generator( - generator_class_name, info, settings.API_VERSION, api_url - ) - schema = self.get_schema(generator, request, not private) + generator = OpenAPISchemaGenerator(info=info, url=api_url, urlconf=urlconf) + schema = generator.get_schema(request=request, public=not private) if output_file == "-": self.write_schema(schema, self.stdout, format) diff --git a/vng_api_common/middleware.py b/vng_api_common/middleware.py index b7cdfc1b..049b2ac1 100644 --- a/vng_api_common/middleware.py +++ b/vng_api_common/middleware.py @@ -1,7 +1,7 @@ # https://pyjwt.readthedocs.io/en/latest/usage.html#reading-headers-without-validation # -> we can put the organization/service in the headers itself import logging -from typing import Any, Dict, List, Optional +from typing import List, Optional, Union from django.conf import settings from django.db import models, transaction @@ -28,7 +28,7 @@ def __init__(self, encoded: str = None): self.encoded = encoded @property - def applicaties(self) -> Optional[list]: + def applicaties(self) -> Union[list, None]: if self.client_id is None: return [] @@ -88,7 +88,7 @@ def _save_auth(self, auth_data): return applicaties @property - def payload(self) -> Optional[Dict[str, Any]]: + def payload(self): if self.encoded is None: return None @@ -111,16 +111,35 @@ def payload(self) -> Optional[Dict[str, Any]]: try: client_id = payload["client_id"] except KeyError: - raise PermissionDenied( - "Client identifier is niet aanwezig in JWT", - code="missing-client-identifier", - ) - - # find client_id in DB and retrieve its secret + try: + header = jwt.get_unverified_header(self.encoded) + except jwt.DecodeError: + logger.info("Invalid JWT encountered") + raise PermissionDenied( + _( + "JWT could not be decoded. Possibly you made a copy-paste mistake." + ), + code="jwt-decode-error", + ) + else: + try: + client_id = header["client_identifier"] + except KeyError: + raise PermissionDenied( + "Client identifier is niet aanwezig in JWT", + code="missing-client-identifier", + ) + else: + logger.warning( + _( + "The support of authorization old format will be terminated soon. " + "Please use a new format with the separate Authorization Component" + ) + ) + + # find client_id in DB and retrieve it's secret try: - jwt_secret = JWTSecret.objects.exclude(secret="").get( - identifier=client_id - ) + jwt_secret = JWTSecret.objects.get(identifier=client_id) except JWTSecret.DoesNotExist: raise PermissionDenied( "Client identifier bestaat niet", code="invalid-client-identifier" @@ -137,14 +156,18 @@ def payload(self) -> Optional[Dict[str, Any]]: "Client credentials zijn niet geldig", code="invalid-jwt-signature" ) + if "client_id" not in payload: + payload["client_id"] = client_id + self._payload = payload return self._payload @property - def client_id(self) -> str: + def client_id(self): if not self.payload: return None + return self.payload["client_id"] def filter_vertrouwelijkheidaanduiding(self, base: QuerySet, value) -> QuerySet: diff --git a/vng_api_common/notifications/constants.py b/vng_api_common/notifications/constants.py index ccdca81b..c35927ee 100644 --- a/vng_api_common/notifications/constants.py +++ b/vng_api_common/notifications/constants.py @@ -1,12 +1,3 @@ # Exernally defined scopes. SCOPE_NOTIFICATIES_CONSUMEREN_LABEL = "notificaties.consumeren" SCOPE_NOTIFICATIES_PUBLICEREN_LABEL = "notificaties.publiceren" - -# Known channel names -KANAAL_AUTORISATIES = "autorisaties" -KANAAL_BESLUITEN = "besluiten" -KANAAL_BESLUITTYPEN = "besluittypen" -KANAAL_DOCUMENTEN = "documenten" -KANAAL_INFORMATIEOBJECTTYPEN = "informatieobjecttypen" -KANAAL_ZAAKTYPEN = "zaaktypen" -KANAAL_ZAKEN = "zaken" diff --git a/vng_api_common/notifications/handlers.py b/vng_api_common/notifications/handlers.py index 521b10cc..0635c73e 100644 --- a/vng_api_common/notifications/handlers.py +++ b/vng_api_common/notifications/handlers.py @@ -7,7 +7,6 @@ from ..client import get_client from ..constants import CommonResourceAction from ..utils import get_uuid_from_path -from .constants import KANAAL_AUTORISATIES class LoggingHandler: @@ -62,4 +61,4 @@ def handle(self, message: dict): log = LoggingHandler() auth = AuthHandler() -default = RoutingHandler({KANAAL_AUTORISATIES: auth}, default=log) +default = RoutingHandler({"autorisaties": auth}, default=log) diff --git a/vng_api_common/notifications/management/commands/register_kanaal.py b/vng_api_common/notifications/management/commands/register_kanaal.py index c8999f4f..0a42bbed 100644 --- a/vng_api_common/notifications/management/commands/register_kanaal.py +++ b/vng_api_common/notifications/management/commands/register_kanaal.py @@ -52,7 +52,7 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("kanaal", nargs="?", type=str, help="Name of kanaal") parser.add_argument( - "--notificaties-api-root", + "--nc-api-root", help="API root of the NC, default value taken from notifications config", ) @@ -60,7 +60,7 @@ def handle(self, **options): config = NotificationsConfig.get_solo() # use CLI arg or fall back to database config - api_root = options["notificaties_api_root"] or config.api_root + api_root = options["nc_api_root"] or config.api_root # use CLI arg or fall back to setting kanaal = options["kanaal"] or settings.NOTIFICATIONS_KANAAL diff --git a/vng_api_common/notifications/viewsets.py b/vng_api_common/notifications/viewsets.py index c02c785c..48fadc06 100644 --- a/vng_api_common/notifications/viewsets.py +++ b/vng_api_common/notifications/viewsets.py @@ -160,10 +160,7 @@ def notify( "Could not deliver message to %s", client.base_url, exc_info=True, - extra={ - "notification_msg": message, - "status_code": status_code, - }, + extra={"notification_msg": message, "status_code": status_code,}, ) diff --git a/vng_api_common/permissions.py b/vng_api_common/permissions.py index e412bc81..03831d99 100644 --- a/vng_api_common/permissions.py +++ b/vng_api_common/permissions.py @@ -10,41 +10,24 @@ from rest_framework.renderers import BrowsableAPIRenderer from rest_framework.request import Request from rest_framework.serializers import ValidationError -from rest_framework.views import APIView -from rest_framework.viewsets import ViewSetMixin from .scopes import Scope from .utils import get_resource_for_path -def get_required_scopes( - request: Request, view: Union[APIView, ViewSetMixin] -) -> Union[Scope, None]: +def get_required_scopes(view) -> Union[Scope, None]: if not hasattr(view, "required_scopes"): raise ImproperlyConfigured( "The View(Set) must have a `required_scopes` attribute" ) - # viewsets mark detail routes by providing the initkwarg, see - # rest_framework.routers.SimpleRouter.get_urls - detail = getattr(view, "detail", False) - action = getattr(view, "action", None) - - # auto-generated head method doesn't get an action assigned - if action is None and detail and view.request.method == "HEAD": - action = "retrieve" - - # if action is not set, fall back to the request method - if action is None and not isinstance(view, ViewSetMixin): - action = request.method.lower() - - scopes_required = view.required_scopes.get(action) + scopes_required = view.required_scopes.get(view.action) return scopes_required def bypass_permissions(request: Request) -> bool: """ - Bypass permission checks in DEBUG when using the browsable API renderer + Bypass permission checks in DBEUG when using the browsable API renderer """ return settings.DEBUG and isinstance( request.accepted_renderer, BrowsableAPIRenderer @@ -69,36 +52,8 @@ def has_object_permission(self, request: Request, view, obj) -> bool: class BaseAuthRequired(permissions.BasePermission): """ - Perform a permission check based on required scopes. - - An :class:`APIView` or :class:`rest_framework.viewsets.ViewSet` needs to - define the ``required_scopes`` attribute, mapping ``action`` to which - scope is required. For :class:`APIView` you can specify which HTTP method - they apply to. Viewset example: - - >>> class SomeViewSet(viewsets.ModelViewSet): - ... queryset = Some.objects.all() - ... permission_classes = (MainObjAuthScopesRequired,) - ... required_scopes = { - ... "retrieve": Scope("some.scope"), - ... "list": Scope("some.scope"), - ... "create": Scope("some.scope"), - ... "update": Scope("some.scope"), - ... "partial_update": Scope("some.scope"), - ... "destroy": Scope("some.scope"), - ... } - - Or for APIView: - - >>> class SomeView(APIView): - ... permission_classes = (BaseAuthRequiredSubclass,) - ... required_scopes = {"get": Scope("some.scope")} - ... - ... def get(self, request): - ... ... - - Note that you need a subclass setting :attr:`get_obj` or implementing - :meth:`_get_object`. + Look at the scopes required for the current action + and check that they are present in the AC for this client """ permission_fields = () @@ -128,37 +83,33 @@ def _get_obj_from_path(self, obj): def _extract_field_value(self, main_obj, field): return getattr(main_obj, field) - def _has_create_permission( - self, request: Request, view: APIView, scopes_required: Scope - ) -> bool: - try: - main_obj = self._get_obj(view, request) - except ObjectDoesNotExist: - raise ValidationError( - { - # using self.obj_path here ASSUMES that the same serializer is used - # for input as output - self.obj_path: ValidationError( - _("The object does not exist in the database"), - code="object-does-not-exist", - ).detail - } - ) - fields = { - k: self._extract_field_value(main_obj, k) for k in self.permission_fields - } - return request.jwt_auth.has_auth(scopes_required, **fields) - def has_permission(self, request: Request, view) -> bool: from rest_framework.viewsets import ViewSetMixin if bypass_permissions(request): return True - scopes_required = get_required_scopes(request, view) - - if getattr(view, "action", None) == "create": - return self._has_create_permission(request, view, scopes_required) + scopes_required = get_required_scopes(view) + + if view.action == "create": + try: + main_obj = self._get_obj(view, request) + except ObjectDoesNotExist: + raise ValidationError( + { + # using self.obj_path here ASSUMES that the same serializer is used + # for input as output + self.obj_path: ValidationError( + _("The object does not exist in the database"), + code="object-does-not-exist", + ).detail + } + ) + fields = { + k: self._extract_field_value(main_obj, k) + for k in self.permission_fields + } + return request.jwt_auth.has_auth(scopes_required, **fields) # detect if this is an unsupported method - if it's a viewset and the # action was not mapped, it's not supported and DRF will catch it @@ -172,7 +123,7 @@ def has_object_permission(self, request: Request, view, obj) -> bool: if bypass_permissions(request): return True - scopes_required = get_required_scopes(request, view) + scopes_required = get_required_scopes(view) main_obj = self._get_obj_from_path(obj) fields = {k: getattr(main_obj, k) for k in self.permission_fields} @@ -188,10 +139,6 @@ def _get_obj_from_path(self, obj): class MainObjAuthScopesRequired(BaseAuthRequired): - """ - Perform permission checks based on the main resource of the endpoint. - """ - def _get_obj(self, view, request): return request.data @@ -203,10 +150,6 @@ def _extract_field_value(self, main_obj, field): class RelatedObjAuthScopesRequired(BaseAuthRequired): - """ - Perform permission checks based on an object related to the endpoint resource. - """ - def _get_obj(self, view, request): main_obj_str = request.data.get(self.obj_path, None) main_obj_url = urlparse(main_obj_str).path diff --git a/vng_api_common/polymorphism.py b/vng_api_common/polymorphism.py index d18967ac..8a8c6243 100644 --- a/vng_api_common/polymorphism.py +++ b/vng_api_common/polymorphism.py @@ -158,7 +158,7 @@ def _sanitize_discriminator(cls, name, attrs) -> Union[Discriminator, None]: values = {choice[0] for choice in field.choices} difference = values - values_seen if difference: - logger.warning( + logger.warn( "'%s': not all possible values map to a serializer. Missing %s", name, difference, diff --git a/vng_api_common/routers.py b/vng_api_common/routers.py index 526528f5..2960d04b 100644 --- a/vng_api_common/routers.py +++ b/vng_api_common/routers.py @@ -26,7 +26,7 @@ def register(self, prefix, viewset, nested=None, *args, **kwargs): if not nested: return - base_name = kwargs.get("base_name") or self.get_default_base_name(viewset) + base_name = kwargs.get("base_name", self.get_default_base_name(viewset)) self._nested_router = NestedSimpleRouter( self, prefix, lookup=base_name, trailing_slash=False diff --git a/vng_api_common/schema.py b/vng_api_common/schema.py index 9c94af79..28cf8626 100644 --- a/vng_api_common/schema.py +++ b/vng_api_common/schema.py @@ -78,6 +78,7 @@ def get_path_parameters(self, path, view_cls): DefaultSchemaView = get_schema_view( # validators=['flex', 'ssv'], + generator_class=OpenAPISchemaGenerator, public=True, permission_classes=(permissions.AllowAny,), ) @@ -102,7 +103,7 @@ def render(self, data, media_type=None, renderer_context=None): ) -class SchemaMixin: +class SchemaView(DefaultSchemaView): """ Always serve the v3 version, which is kept in version control. @@ -171,7 +172,3 @@ def get(self, request, version="", *args, **kwargs): server["url"] = request.build_absolute_uri(server_path) return Response(data=schema, headers={"X-OAS-Version": schema["openapi"]}) - - -class SchemaView(SchemaMixin, DefaultSchemaView): - pass diff --git a/vng_api_common/scopes.py b/vng_api_common/scopes.py index c6088522..3a5541cc 100644 --- a/vng_api_common/scopes.py +++ b/vng_api_common/scopes.py @@ -1,11 +1,3 @@ -""" -Define scopes to manage authorizations on API resources. - -Scope objects hold their own definition and documentation. Public scopes get -added to the scope registry, which can be introspected for automatic -documentation. -""" - from typing import List OPERATOR_OR = "OR" @@ -16,23 +8,6 @@ class Scope: - """ - Define a single scope object. - - A scope is characterized by a label, whereas the actual permissions related - to it are implemented in the view(set)s. Scopes can be OR-ed together: - - >>> Scope("foo") | Scope("bar") - Scope("foo | bar") - - this is interpreted as: you have permission if you have one of either - scopes in your authorization configuration. - - :arg label: A label identifying the scope. Labels must be unique. - :arg description: An optional description of what the scope allows/means. - :arg private: Private scopes are not added to the registry. - """ - def __init__(self, label: str, description: str = None, private: bool = False): self.label = label self.description = description diff --git a/vng_api_common/search.py b/vng_api_common/search.py index d4d2887b..9e854e89 100644 --- a/vng_api_common/search.py +++ b/vng_api_common/search.py @@ -1,11 +1,8 @@ from django.db import models -from rest_framework.response import Response - def is_search_view(view): - _action = getattr(view, "action", None) - if _action is None: + if not hasattr(view, "action"): return action = getattr(view, view.action) return getattr(action, "is_search_action", False) @@ -29,4 +26,4 @@ def get_search_output(self, queryset: models.QuerySet): return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True) - return Response(serializer.data) + return serializer.data diff --git a/vng_api_common/tests/__init__.py b/vng_api_common/tests/__init__.py index 6b1c1804..461b16a7 100644 --- a/vng_api_common/tests/__init__.py +++ b/vng_api_common/tests/__init__.py @@ -1,10 +1,17 @@ -from .auth import AuthCheckMixin, JWTAuthMixin, generate_jwt_auth -from .caching import CacheMixin +from .auth import ( + AuthCheckMixin, + JWTAuthMixin, + JWTScopesMixin, + generate_jwt, + generate_jwt_auth, +) from .schema import TypeCheckMixin, get_operation_url, get_validation_errors from .urls import reverse, reverse_lazy __all__ = [ + "generate_jwt", "AuthCheckMixin", + "JWTScopesMixin", "get_operation_url", "TypeCheckMixin", "get_validation_errors", @@ -12,5 +19,4 @@ "reverse_lazy", "JWTAuthMixin", "generate_jwt_auth", - "CacheMixin", ] diff --git a/vng_api_common/tests/auth.py b/vng_api_common/tests/auth.py index e8e8b268..6834b614 100644 --- a/vng_api_common/tests/auth.py +++ b/vng_api_common/tests/auth.py @@ -9,6 +9,31 @@ from ..models import JWTSecret +def generate_jwt(scopes: list, secret: str = "letmein", zaaktypes: list = None) -> str: + scope_labels = sum((_get_scope_labels(scope) for scope in scopes), []) + payload = { + # standard claims + "iss": "testsuite", + "iat": int(time.time()), + # custom claims + "zds": {"scopes": scope_labels, "zaaktypes": zaaktypes or []}, + } + headers = {"client_identifier": "testsuite"} + encoded = jwt.encode(payload, secret, headers=headers, algorithm="HS256") + encoded = encoded.decode("ascii") + return f"Bearer {encoded}" + + +def _get_scope_labels(scope) -> list: + if not scope.children: + return [scope.label] + + labels = [] + for child in scope.children: + labels += _get_scope_labels(child) + return sorted(set(labels)) + + class AuthCheckMixin: @classmethod def setUpTestData(cls): @@ -42,7 +67,45 @@ def assertForbidden(self, url, method="get", request_kwargs=None): response.status_code, status.HTTP_403_FORBIDDEN, response.data ) + def assertForbiddenWithCorrectScope( + self, url: str, scopes: list, method="get", request_kwargs=None, **extra_claims + ): + + do_request = getattr(self.client, method) + request_kwargs = request_kwargs or {} + + jwt = generate_jwt(scopes=scopes, **extra_claims) + self.client.credentials(HTTP_AUTHORIZATION=jwt) + + response = do_request(url, **request_kwargs) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + +class JWTScopesMixin: + + scopes = None + zaaktypes = None + + @classmethod + def setUpTestData(cls): + super().setUpTestData() + + JWTSecret.objects.get_or_create( + identifier="testsuite", defaults={"secret": "letmein"} + ) + + def setUp(self): + super().setUp() + + if self.scopes is not None: + token = generate_jwt( + scopes=self.scopes, zaaktypes=self.zaaktypes or [], secret="letmein" + ) + self.client.credentials(HTTP_AUTHORIZATION=token) + +# tools fot testing with new authorization format def generate_jwt_auth( client_id, secret, user_id="test_user_id", user_representation="Test User" ): diff --git a/vng_api_common/tests/caching.py b/vng_api_common/tests/caching.py deleted file mode 100644 index d89f29f8..00000000 --- a/vng_api_common/tests/caching.py +++ /dev/null @@ -1,17 +0,0 @@ -from rest_framework import status -from rest_framework.response import Response - - -class CacheMixin: - def assertHasETag(self, response: Response, status_code=status.HTTP_200_OK): - self.assertEqual(response.status_code, status_code) - self.assertIn("ETag", response) - self.assertNotEqual(response["ETag"], "") - - def assertHeadHasETag(self, url: str, status_code=status.HTTP_200_OK, **extra): - response = self.client.head(url, **extra) - - self.assertHasETag(response) - - # head requests should not return a response body, only headers - self.assertEqual(response.content, b"") diff --git a/vng_api_common/utils.py b/vng_api_common/utils.py index 549b9e10..39da5f42 100644 --- a/vng_api_common/utils.py +++ b/vng_api_common/utils.py @@ -8,10 +8,7 @@ from django.db import models from django.http import HttpRequest from django.urls import Resolver404, get_resolver, get_script_prefix -from django.utils.encoding import smart_text -from django.utils.module_loading import import_string -from rest_framework.utils import formatting from zds_client.client import ClientError from .client import get_client