diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85ded41..744bd1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,7 +74,7 @@ jobs: python-version: '3.11' optimize-postgres: 'yes' pg-service: 'postgres' - setup-node: 'no' + setup-node: 'yes' apt-packages: 'gettext' - name: Run tests diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index dae9fdf..70bb9d4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'python' ] + language: [ 'javascript', 'python' ] steps: - name: Checkout repository diff --git a/.gitignore b/.gitignore index 20cedbd..75016f1 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,8 @@ local.py # Static files src/open_producten/static/bundles/ +src/open_producten/static/wysimark/wysimark.js +src/open_producten/static/wysimark/index* src/open_producten/fonts/ tmp/* diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..fb4371f --- /dev/null +++ b/.jshintrc @@ -0,0 +1,162 @@ +{ + // JSHint Default Configuration File (as on JSHint website) + // See http://jshint.com/docs/ for more details + + "maxerr": 50, + // {int} Maximum error before stopping + + // Enforcing + "bitwise": true, + // true: Prohibit bitwise operators (&, |, ^, etc.) + "camelcase": false, + // true: Identifiers must be in camelCase + "curly": true, + // true: Require {} for every new block or scope + "eqeqeq": true, + // true: Require triple equals (===) for comparison + "forin": true, + // true: Require filtering for..in loops with obj.hasOwnProperty() + "freeze": true, + // true: prohibits overwriting prototypes of native objects such as Array, Date etc. + "immed": false, + // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` + "latedef": false, + // true: Require variables/functions to be defined before being used + "newcap": false, + // true: Require capitalization of all constructor functions e.g. `new F()` + "noarg": true, + // true: Prohibit use of `arguments.caller` and `arguments.callee` + "noempty": true, + // true: Prohibit use of empty blocks + "nonbsp": true, + // true: Prohibit "non-breaking whitespace" characters. + "nonew": false, + // true: Prohibit use of constructors for side-effects (without assignment) + "plusplus": false, + // true: Prohibit use of `++` and `--` + "quotmark": "single", + // Quotation mark consistency: + // false : do nothing (default) + // true : ensure whatever is used is consistent + // "single" : require single quotes + // "double" : require double quotes + "undef": true, + // true: Require all non-global variables to be declared (prevents global leaks) + "unused": true, + // Unused variables: + // true : all variables, last function parameter + // "vars" : all variables only + // "strict" : all variables, all function parameters + "strict": false, + // true: Requires all functions run in ES5 Strict Mode + "maxparams": false, + // {int} Max number of formal params allowed per function + "maxdepth": 3, + // {int} Max depth of nested blocks (within functions) + "maxstatements": false, + // {int} Max number statements per function + "maxcomplexity": false, + // {int} Max cyclomatic complexity per function + "maxlen": false, + // {int} Max number of characters per line + "varstmt": false, + // true: Disallow any var statements. Only `let` and `const` are allowed. + + // Relaxing + "asi": false, + // true: Tolerate Automatic Semicolon Insertion (no semicolons) + "boss": false, + // true: Tolerate assignments where comparisons would be expected + "debug": false, + // true: Allow debugger statements e.g. browser breakpoints. + "eqnull": false, + // true: Tolerate use of `== null` + "esversion": 6, + // {int} Specify the ECMAScript version to which the code must adhere. + "moz": false, + // true: Allow Mozilla specific syntax (extends and overrides esnext features) + // (ex: `for each`, multiple try/catch, function expression�) + "evil": false, + // true: Tolerate use of `eval` and `new Function()` + "expr": false, + // true: Tolerate `ExpressionStatement` as Programs + "funcscope": false, + // true: Tolerate defining variables inside control statements + "globalstrict": false, + // true: Allow global "use strict" (also enables 'strict') + "iterator": false, + // true: Tolerate using the `__iterator__` property + "lastsemic": false, + // true: Tolerate omitting a semicolon for the last statement of a 1-line block + "laxbreak": false, + // true: Tolerate possibly unsafe line breakings + "laxcomma": false, + // true: Tolerate comma-first style coding + "loopfunc": false, + // true: Tolerate functions being defined in loops + "multistr": false, + // true: Tolerate multi-line strings + "noyield": false, + // true: Tolerate generator functions with no yield statement in them. + "notypeof": false, + // true: Tolerate invalid typeof operator values + "proto": false, + // true: Tolerate using the `__proto__` property + "scripturl": false, + // true: Tolerate script-targeted URLs + "shadow": false, + // true: Allows re-define variables later in code e.g. `var x=1; x=2;` + "sub": false, + // true: Tolerate using `[]` notation when it can still be expressed in dot notation + "supernew": false, + // true: Tolerate `new function () { ... };` and `new Object;` + "validthis": false, + // true: Tolerate using this in a non-constructor function + + // Environments + "browser": true, + // Web Browser (window, document, etc) + "browserify": false, + // Browserify (node.js code in the browser) + "couch": false, + // CouchDB + "devel": true, + // Development/debugging (alert, confirm, etc) + "dojo": false, + // Dojo Toolkit + "jasmine": false, + // Jasmine + "jquery": false, + // jQuery + "mocha": true, + // Mocha + "mootools": false, + // MooTools + "node": true, + // Node.js + "nonstandard": false, + // Widely adopted globals (escape, unescape, etc) + "phantom": false, + // PhantomJS + "prototypejs": false, + // Prototype and Scriptaculous + "qunit": false, + // QUnit + "rhino": false, + // Rhino + "shelljs": false, + // ShellJS + "typed": false, + // Globals for typed array constructions + "worker": false, + // Web Workers + "wsh": false, + // Windows Scripting Host + "yui": false, + // Yahoo User Interface + + // Custom Globals + "globals": { + "google": true + } +} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..9a2a0e2 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v20 diff --git a/Dockerfile b/Dockerfile index 58223d8..5e7d57c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,26 +26,27 @@ RUN pip install -r requirements/production.txt # Stage 2 - Install frontend deps and build assets -#FROM node:20-bookworm-slim AS frontend-build -# -#RUN apt-get update && apt-get install -y --no-install-recommends \ -# git \ -# && rm -rf /var/lib/apt/lists/* -# -#WORKDIR /app -# -## copy configuration/build files +FROM node:20-bookworm-slim AS frontend-build + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# copy configuration/build files #COPY ./build /app/build/ -#COPY ./*.json ./*.js ./.babelrc /app/ -# -## install WITH dev tooling -#RUN npm ci -# -## copy source code -#COPY ./src /app/src -# -## build frontend -#RUN npm run build +COPY ./*.json ./*.js /app/ +# ./.babelrc + +# install WITH dev tooling +RUN npm ci + +# copy source code +COPY ./src /app/src + +# build frontend +RUN npm run build # Stage 3 - Build docker image suitable for production @@ -80,7 +81,7 @@ COPY --from=backend-build /usr/local/bin/celery /usr/local/bin/celery COPY --from=backend-build /app/src/ /app/src/ # copy frontend build statics -# COPY --from=frontend-build /app/src/open_producten/static /app/src/open_producten/static +COPY --from=frontend-build /app/src/open_producten/static /app/src/open_producten/static # copy source code COPY ./src /app/src diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..756f30c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2431 @@ +{ + "name": "open_producten", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "open_producten", + "version": "0.0.1", + "license": "UNLICENSED", + "dependencies": { + "wysimark-standalone": "^1.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "dependencies": { + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", + "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", + "dependencies": { + "@babel/types": "^7.26.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", + "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz", + "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.7", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", + "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@emoji-mart/data": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz", + "integrity": "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==" + }, + "node_modules/@emoji-mart/react": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@emoji-mart/react/-/react-1.1.1.tgz", + "integrity": "sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==", + "peerDependencies": { + "emoji-mart": "^5.2", + "react": "^16.8 || ^17 || ^18" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/core": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@emotion/core/-/core-11.0.0.tgz", + "integrity": "sha512-w4sE3AmHmyG6RDKf6mIbtHpgJUSJ2uGvPQb8VXFL7hFjMPibE8IiehG8cMX3Ztm4svfCQV6KqusQbeIOkurBcA==" + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", + "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" + }, + "node_modules/@emotion/styled": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", + "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@portive/api-types": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@portive/api-types/-/api-types-9.0.0.tgz", + "integrity": "sha512-uyzC0taTJW9bL3EouGgI731ngMktX00XMHCpnuU4ijCVeKYKpReN3uNDL+JjFR1EzRXlF1ccRG1qtb6JLPNPLQ==", + "dependencies": { + "@thesunny/assert-type": "^0.1.13", + "superstruct": "^0.15.4" + } + }, + "node_modules/@portive/client": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@portive/client/-/client-10.0.3.tgz", + "integrity": "sha512-tyEZbV5fKucd7cfADgVWNcH6F09pzME15sAy+a0imU7ykpbMKabPg+zo19g+b/pEG/+OWlTuQBgMMhlO8clZ+Q==", + "dependencies": { + "@portive/api-types": "^9.0.0", + "axios": "^0.27.2", + "resolvable-value": "^1.0.2" + } + }, + "node_modules/@thesunny/assert-type": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@thesunny/assert-type/-/assert-type-0.1.13.tgz", + "integrity": "sha512-9uz7HcY7n+SdTCQvTy5e24pCn9oBge5XnnSWCYuXC9PUx+/TwjU4KpjqswdowVZUJ58URWXqLwpfok8GrkeREw==", + "dependencies": { + "ts-node": "^10.9.1" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/is-hotkey": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.10.tgz", + "integrity": "sha512-RvC8KMw5BCac1NvRRyaHgMMEtBaZ6wh0pyPTBu7izn4Sj/AX9Y4aXU5c7rX8PnM/knsuUpC1IeoBkANtxBypsQ==" + }, + "node_modules/@types/lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==" + }, + "node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + }, + "node_modules/@types/node": { + "version": "22.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.12.0.tgz", + "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==", + "peer": true, + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, + "node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/@wysimark/standalone": { + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/@wysimark/standalone/-/standalone-3.0.20.tgz", + "integrity": "sha512-F6gLYKPcP1FB/xtQqqj+4WwmL6IEyZ/LtLy3zTqWi+07pzEF9raTZwmCb+0vau6kXTcGnyW4x2oRGU1FurL6Ow==", + "dependencies": { + "@emoji-mart/data": "^1.1.0", + "@emoji-mart/react": "^1.1.0", + "@emotion/core": "^11.0.0", + "@emotion/react": "^11.10.6", + "@emotion/styled": "^11.10.6", + "@portive/client": "^10.0.3", + "clsx": "^1.2.1", + "emoji-mart": "^5.4.0", + "is-hotkey": "^0.2.0", + "just-map-values": "^3.2.0", + "lodash.throttle": "^4.1.1", + "nanoid": "^3.3.6", + "prismjs": "^1.29.0", + "react": ">=17.x", + "react-dom": ">=17.x", + "slate": "^0.85.0", + "slate-history": "^0.85.0", + "slate-react": "^0.83.2", + "zustand": "^4.1.5" + } + }, + "node_modules/@wysimark/standalone/node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@wysimark/standalone/node_modules/react-dom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "dependencies": { + "scheduler": "^0.25.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/@wysimark/standalone/node_modules/slate-react": { + "version": "0.83.2", + "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.83.2.tgz", + "integrity": "sha512-V5qtPsCOiDVCMU3ovj/CWndxx9/as5/wGJxnbJDRVzuazSh+NVw/YuGTlus1fCt+Nlt6UHOhFvM7C9pXYaftPA==", + "dependencies": { + "@types/is-hotkey": "^0.1.1", + "@types/lodash": "^4.14.149", + "direction": "^1.0.3", + "is-hotkey": "^0.1.6", + "is-plain-object": "^5.0.0", + "lodash": "^4.17.4", + "scroll-into-view-if-needed": "^2.2.20", + "tiny-invariant": "1.0.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.65.3" + } + }, + "node_modules/@wysimark/standalone/node_modules/slate-react/node_modules/is-hotkey": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.1.8.tgz", + "integrity": "sha512-qs3NZ1INIS+H+yeo7cD9pDfwYV/jqRh1JG9S9zYrNudkoUQg7OL7ziXqRKu+InFjUIDoP2o6HIkLYMh1pcWgyQ==" + }, + "node_modules/@wysimark/vue": { + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/@wysimark/vue/-/vue-3.0.20.tgz", + "integrity": "sha512-nMWluzoNybcOh4JKC//M4GyNAA9dTSaT/2+/9r+4ET3YM4xjKV924enUkYTkBqPQN7mu0VyYg+qmX9EDTWpySw==", + "dependencies": { + "@emoji-mart/data": "^1.1.0", + "@emoji-mart/react": "^1.1.0", + "@emotion/core": "^11.0.0", + "@emotion/react": "^11.10.6", + "@emotion/styled": "^11.10.6", + "@portive/client": "^10.0.3", + "clsx": "^1.2.1", + "dotenv": "^16.3", + "emoji-mart": "^5.4.0", + "is-hotkey": "^0.2.0", + "just-map-values": "^3.2.0", + "lodash.throttle": "^4.1.1", + "nanoid": "^3.3.6", + "prismjs": "^1.29.0", + "react": ">=17.x", + "react-dom": ">=17.x", + "slate": "^0.85.0", + "slate-history": "^0.85.0", + "slate-react": "^0.83.2", + "zustand": "^4.1.5" + } + }, + "node_modules/@wysimark/vue/node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@wysimark/vue/node_modules/react-dom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "dependencies": { + "scheduler": "^0.25.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/@wysimark/vue/node_modules/slate-react": { + "version": "0.83.2", + "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.83.2.tgz", + "integrity": "sha512-V5qtPsCOiDVCMU3ovj/CWndxx9/as5/wGJxnbJDRVzuazSh+NVw/YuGTlus1fCt+Nlt6UHOhFvM7C9pXYaftPA==", + "dependencies": { + "@types/is-hotkey": "^0.1.1", + "@types/lodash": "^4.14.149", + "direction": "^1.0.3", + "is-hotkey": "^0.1.6", + "is-plain-object": "^5.0.0", + "lodash": "^4.17.4", + "scroll-into-view-if-needed": "^2.2.20", + "tiny-invariant": "1.0.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.65.3" + } + }, + "node_modules/@wysimark/vue/node_modules/slate-react/node_modules/is-hotkey": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.1.8.tgz", + "integrity": "sha512-qs3NZ1INIS+H+yeo7cD9pDfwYV/jqRh1JG9S9zYrNudkoUQg7OL7ziXqRKu+InFjUIDoP2o6HIkLYMh1pcWgyQ==" + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", + "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==" + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/direction": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/direction/-/direction-1.0.4.tgz", + "integrity": "sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/emoji-mart": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz", + "integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==" + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-hotkey": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.2.0.tgz", + "integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==" + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/just-map-values": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/just-map-values/-/just-map-values-3.2.0.tgz", + "integrity": "sha512-TyqCKtK3NxiUgOjRYMIKURvBTHesi3XzomDY0QVPZ3rYzLCF+nNq5rSi0B/L5aOd/WMTZo6ukzA4wih4HUbrDg==" + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-definitions": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", + "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz", + "integrity": "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==", + "dependencies": { + "@types/mdast": "^3.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz", + "integrity": "sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==", + "dependencies": { + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-gfm-autolink-literal": "^1.0.0", + "mdast-util-gfm-footnote": "^1.0.0", + "mdast-util-gfm-strikethrough": "^1.0.0", + "mdast-util-gfm-table": "^1.0.0", + "mdast-util-gfm-task-list-item": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz", + "integrity": "sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==", + "dependencies": { + "@types/mdast": "^3.0.0", + "ccount": "^2.0.0", + "mdast-util-find-and-replace": "^2.0.0", + "micromark-util-character": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz", + "integrity": "sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0", + "micromark-util-normalize-identifier": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz", + "integrity": "sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz", + "integrity": "sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==", + "dependencies": { + "@types/mdast": "^3.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz", + "integrity": "sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", + "dependencies": { + "@types/mdast": "^3.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", + "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz", + "integrity": "sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^1.0.0", + "micromark-extension-gfm-footnote": "^1.0.0", + "micromark-extension-gfm-strikethrough": "^1.0.0", + "micromark-extension-gfm-table": "^1.0.0", + "micromark-extension-gfm-tagfilter": "^1.0.0", + "micromark-extension-gfm-task-list-item": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz", + "integrity": "sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz", + "integrity": "sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==", + "dependencies": { + "micromark-core-commonmark": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz", + "integrity": "sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz", + "integrity": "sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz", + "integrity": "sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==", + "dependencies": { + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz", + "integrity": "sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/remark-gfm": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz", + "integrity": "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-gfm": "^2.0.0", + "micromark-extension-gfm": "^2.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", + "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolvable-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/resolvable-value/-/resolvable-value-1.0.2.tgz", + "integrity": "sha512-H6HzqoYUAAd5JXT1wJBOf2NRZ+Q3nQAWA+xkIidN2ykzq6Ovb3pRfz6ft+J2148GnYB+X1W2qWiR7+r5jRGOug==" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==" + }, + "node_modules/scroll-into-view-if-needed": { + "version": "2.2.31", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz", + "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==", + "dependencies": { + "compute-scroll-into-view": "^1.0.20" + } + }, + "node_modules/slate": { + "version": "0.85.0", + "resolved": "https://registry.npmjs.org/slate/-/slate-0.85.0.tgz", + "integrity": "sha512-SHlgOI7fDG50sL+lfAI+nXF9FQpoqf7KNRofggyyE4ngsj1nINOEpFSYKGPkZgVUUkPflWK6PuBJMR6nEuUiKg==", + "dependencies": { + "immer": "^9.0.6", + "is-plain-object": "^5.0.0", + "tiny-warning": "^1.0.3" + } + }, + "node_modules/slate-history": { + "version": "0.85.0", + "resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.85.0.tgz", + "integrity": "sha512-4rPx+Y7ubROlc85mYzK9iXRMgjmor6v87AMx5Oh6eOuW4elCz+uAiBWT6ates7JjCNiDXBHxtMvBEa37UG5dWg==", + "dependencies": { + "is-plain-object": "^5.0.0" + }, + "peerDependencies": { + "slate": ">=0.65.3" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, + "node_modules/superstruct": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.15.5.tgz", + "integrity": "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==" + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tiny-invariant": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz", + "integrity": "sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA==" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "peer": true + }, + "node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "dependencies": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "bin": { + "uvu": "bin.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uvu/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, + "node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/wysimark-standalone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wysimark-standalone/-/wysimark-standalone-1.0.0.tgz", + "integrity": "sha512-zl9hdtVrW7/bXZqekHhePnVWcSX2mWJsASXfXwqh1+/WkST/+1ZpiZ+55kCzVwEOz1Fh50Mp/661/gKVjk62Eg==", + "dependencies": { + "@wysimark/react": "^3.0.20", + "@wysimark/standalone": "^3.0.20", + "@wysimark/vue": "^3.0.20" + } + }, + "node_modules/wysimark-standalone/node_modules/@wysimark/react": { + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/@wysimark/react/-/react-3.0.20.tgz", + "integrity": "sha512-R3AdEoThZ+dV8omjbwgLc3lBLkz9f1Ogo9TOARC9mq8brEIJMCrKpBtjFfu1SzFJLzCty3t8fA6BkWkOZaF09Q==", + "dependencies": { + "@emoji-mart/data": "^1.1.0", + "@emoji-mart/react": "^1.1.0", + "@emotion/core": "^11.0.0", + "@emotion/react": "^11.10.6", + "@emotion/styled": "^11.10.6", + "@portive/client": "^10.0.3", + "clsx": "^1.2.1", + "dotenv": "^16.3", + "emoji-mart": "^5.4.0", + "is-hotkey": "^0.2.0", + "lodash.throttle": "^4.1.1", + "mdast-util-definitions": "^5.1.1", + "nanoid": "^3.3.6", + "prismjs": "^1.29.0", + "remark-gfm": "^3.0.1", + "remark-parse": "^10.0.1", + "slate": "^0.85.0", + "slate-history": "^0.85.0", + "slate-react": "^0.83.2", + "unified": "^10.1.2", + "unist-util-visit": "^4.1.2", + "zustand": "^4.1.5" + }, + "peerDependencies": { + "react": ">=17.x", + "react-dom": ">=17.x" + } + }, + "node_modules/wysimark-standalone/node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wysimark-standalone/node_modules/react-dom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "peer": true, + "dependencies": { + "scheduler": "^0.25.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/wysimark-standalone/node_modules/slate-react": { + "version": "0.83.2", + "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.83.2.tgz", + "integrity": "sha512-V5qtPsCOiDVCMU3ovj/CWndxx9/as5/wGJxnbJDRVzuazSh+NVw/YuGTlus1fCt+Nlt6UHOhFvM7C9pXYaftPA==", + "dependencies": { + "@types/is-hotkey": "^0.1.1", + "@types/lodash": "^4.14.149", + "direction": "^1.0.3", + "is-hotkey": "^0.1.6", + "is-plain-object": "^5.0.0", + "lodash": "^4.17.4", + "scroll-into-view-if-needed": "^2.2.20", + "tiny-invariant": "1.0.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0", + "slate": ">=0.65.3" + } + }, + "node_modules/wysimark-standalone/node_modules/slate-react/node_modules/is-hotkey": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.1.8.tgz", + "integrity": "sha512-qs3NZ1INIS+H+yeo7cD9pDfwYV/jqRh1JG9S9zYrNudkoUQg7OL7ziXqRKu+InFjUIDoP2o6HIkLYMh1pcWgyQ==" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/zustand": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", + "integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..53820ec --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "open_producten", + "version": "0.0.1", + "description": "open-producten project", + "main": "src/static/open-producten/js/open-producten.js", + "directories": { + "doc": "docs" + }, + "scripts": { + "build": "npm run collect", + "collect": "cp node_modules/wysimark-standalone/dist/javascript/index.cjs.js src/open_producten/static/wysimark/wysimark.js && cp node_modules/wysimark-standalone/dist/javascript/index.cjs.js.map src/open_producten/static/wysimark/index.cjs.js.map" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/maykinmedia/open-producten.git" + }, + "author": "Maykin", + "license": "UNLICENSED", + "homepage": "https://maykinmedia.nl", + "dependencies": { + "wysimark-standalone": "^1.0.0" + } +} diff --git a/requirements/base.in b/requirements/base.in index a4b03e2..708a0a7 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -9,6 +9,8 @@ open-api-framework # django-extra-fields django-markdownx django-localflavor +django-parler +django-parler-rest django-celery-beat flower diff --git a/requirements/base.txt b/requirements/base.txt index 3e6fa0b..9e5416c 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -91,6 +91,8 @@ django==4.2.17 # django-markdownx # django-markup # django-otp + # django-parler + # django-parler-rest # django-phonenumber-field # django-privates # django-redis @@ -153,6 +155,12 @@ django-ordered-model==3.7.4 # via django-admin-index django-otp==1.5.4 # via django-two-factor-auth +django-parler==2.3 + # via + # -r requirements/base.in + # django-parler-rest +django-parler-rest==2.2 + # via -r requirements/base.in django-phonenumber-field==8.0.0 # via django-two-factor-auth django-privates==2.0.0.post1 @@ -187,6 +195,7 @@ django-two-factor-auth==1.17.0 djangorestframework==3.15.2 # via # commonground-api-common + # django-parler-rest # djangorestframework-gis # djangorestframework-inclusions # drf-nested-routers diff --git a/requirements/ci.txt b/requirements/ci.txt index e763c39..0de5feb 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -161,6 +161,8 @@ django==4.2.17 # django-markdownx # django-markup # django-otp + # django-parler + # django-parler-rest # django-phonenumber-field # django-privates # django-redis @@ -262,6 +264,15 @@ django-otp==1.5.4 # -c requirements/base.txt # -r requirements/base.txt # django-two-factor-auth +django-parler==2.3 + # via + # -c requirements/base.txt + # -r requirements/base.txt + # django-parler-rest +django-parler-rest==2.2 + # via + # -c requirements/base.txt + # -r requirements/base.txt django-phonenumber-field==8.0.0 # via # -c requirements/base.txt @@ -337,6 +348,7 @@ djangorestframework==3.15.2 # -c requirements/base.txt # -r requirements/base.txt # commonground-api-common + # django-parler-rest # djangorestframework-gis # djangorestframework-inclusions # drf-nested-routers diff --git a/requirements/dev.txt b/requirements/dev.txt index 7268cd0..ff4a074 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -188,6 +188,8 @@ django==4.2.17 # django-markdownx # django-markup # django-otp + # django-parler + # django-parler-rest # django-phonenumber-field # django-privates # django-redis @@ -293,6 +295,15 @@ django-otp==1.5.4 # -c requirements/ci.txt # -r requirements/ci.txt # django-two-factor-auth +django-parler==2.3 + # via + # -c requirements/ci.txt + # -r requirements/ci.txt + # django-parler-rest +django-parler-rest==2.2 + # via + # -c requirements/ci.txt + # -r requirements/ci.txt django-phonenumber-field==8.0.0 # via # -c requirements/ci.txt @@ -370,6 +381,7 @@ djangorestframework==3.15.2 # -c requirements/ci.txt # -r requirements/ci.txt # commonground-api-common + # django-parler-rest # djangorestframework-gis # djangorestframework-inclusions # drf-nested-routers diff --git a/src/open_producten/conf/base.py b/src/open_producten/conf/base.py index ebb2366..0cece82 100644 --- a/src/open_producten/conf/base.py +++ b/src/open_producten/conf/base.py @@ -1,3 +1,5 @@ +from django.utils.translation import gettext_lazy as _ + from celery.schedules import crontab from open_api_framework.conf.base import * from open_api_framework.conf.utils import config @@ -20,6 +22,7 @@ "rest_framework.authtoken", "timeline_logger", "localflavor", + "parler", "markdownx", "django_celery_beat", "open_producten.accounts", @@ -41,6 +44,10 @@ } } +MIDDLEWARE.insert( + MIDDLEWARE.index("django.middleware.common.CommonMiddleware"), + "django.middleware.locale.LocaleMiddleware", +) # # CELERY @@ -165,3 +172,24 @@ SUBPATH = config("SUBPATH", None) if SUBPATH: SUBPATH = f"/{SUBPATH.strip('/')}" + + +LANGUAGES = [ + ("nl", _("Dutch")), + ("en", _("English")), +] + +PARLER_LANGUAGES = { + 1: ( + { + "code": "nl", + }, + { + "code": "en", + }, + ), + "default": { + "fallbacks": ["nl"], + "hide_untranslated": False, + }, +} diff --git a/src/open_producten/conf/dev.py b/src/open_producten/conf/dev.py index 831360b..65cbbe5 100644 --- a/src/open_producten/conf/dev.py +++ b/src/open_producten/conf/dev.py @@ -93,10 +93,6 @@ r"django\.db\.models\.fields", ) -LANGUAGE_CODE = "en-us" - -# MIDDLEWARE = ["django.middleware.locale.LocaleMiddleware"] + MIDDLEWARE - # Override settings with local settings. try: from .local import * # noqa diff --git a/src/open_producten/fixtures/default_admin_index.json b/src/open_producten/fixtures/default_admin_index.json index f58acbb..db6b104 100644 --- a/src/open_producten/fixtures/default_admin_index.json +++ b/src/open_producten/fixtures/default_admin_index.json @@ -1,257 +1,249 @@ [ -{ - "model": "admin_index.appgroup", - "fields": { - "order": 0, - "translations": { - "en": "Products" - }, - "name": "Producten", - "slug": "Producten", - "models": [ - [ - "producten", - "product" + { + "model": "admin_index.appgroup", + "fields": { + "order": 0, + "translations": { + "en": "Products" + }, + "name": "Producten", + "slug": "Producten", + "models": [ + [ + "producten", + "product" + ] ] - ] - } -}, -{ - "model": "admin_index.appgroup", - "fields": { - "order": 1, - "translations": { - "en": "Product types" - }, - "name": "Product typen", - "slug": "product-typen", - "models": [ - [ - "producttypen", - "bestand" - ], - [ - "producttypen", - "link" - ], - [ - "producttypen", - "prijs" - ], - [ - "producttypen", - "producttype" - ], - [ - "producttypen", - "thema" - ], - [ - "producttypen", - "uniformeproductnaam" - ], - [ - "producttypen", - "vraag" + } + }, + { + "model": "admin_index.appgroup", + "fields": { + "order": 1, + "translations": { + "en": "Product types" + }, + "name": "Product typen", + "slug": "product-typen", + "models": [ + [ + "producttypen", + "bestand" + ], + [ + "producttypen", + "link" + ], + [ + "producttypen", + "prijs" + ], + [ + "producttypen", + "producttype" + ], + [ + "producttypen", + "thema" + ], + [ + "producttypen", + "uniformeproductnaam" + ], + [ + "producttypen", + "contentlabel" + ] ] - ] - } -}, -{ - "model": "admin_index.appgroup", - "fields": { - "order": 3, - "translations": { - "nl": "Gebruikers" - }, - "name": "Users", - "slug": "users", - "models": [ - [ - "accounts", - "user" - ], - [ - "auth", - "group" - ], - [ - "auth", - "permission" - ], - [ - "authtoken", - "tokenproxy" - ], - [ - "otp_static", - "staticdevice" - ], - [ - "otp_static", - "statictoken" - ], - [ - "otp_totp", - "totpdevice" + } + }, + { + "model": "admin_index.appgroup", + "fields": { + "order": 3, + "translations": { + "nl": "Gebruikers" + }, + "name": "Users", + "slug": "users", + "models": [ + [ + "accounts", + "user" + ], + [ + "auth", + "group" + ], + [ + "auth", + "permission" + ], + [ + "authtoken", + "tokenproxy" + ], + [ + "otp_static", + "staticdevice" + ], + [ + "otp_static", + "statictoken" + ], + [ + "otp_totp", + "totpdevice" + ] ] - ] - } -}, -{ - "model": "admin_index.appgroup", - "fields": { - "order": 4, - "translations": { - "nl": "Configuratie" - }, - "name": "Config", - "slug": "config", - "models": [ - [ - "admin_index", - "appgroup" - ], - [ - "admin_index", - "applink" - ], - [ - "admin_index", - "contenttypeproxy" - ], - [ - "django_celery_beat", - "periodictask" + } + }, + { + "model": "admin_index.appgroup", + "fields": { + "order": 4, + "translations": { + "nl": "Configuratie" + }, + "name": "Config", + "slug": "config", + "models": [ + [ + "admin_index", + "appgroup" + ], + [ + "admin_index", + "applink" + ], + [ + "django_celery_beat", + "periodictask" + ] ] - ] - } -}, -{ - "model": "admin_index.appgroup", - "fields": { - "order": 6, - "translations": { - "nl": "Misc" - }, - "name": "Misc", - "slug": "all", - "models": [ - [ - "contenttypes", - "contenttype" - ], - [ - "log_outgoing_requests", - "outgoingrequestslog" - ], - [ - "log_outgoing_requests", - "outgoingrequestslogconfig" - ], - [ - "mozilla_django_oidc_db", - "openidconnectconfig" - ], - [ - "notifications_api_common", - "notificationsconfig" - ], - [ - "notifications_api_common", - "subscription" - ], - [ - "sessions", - "session" - ], - [ - "simple_certmanager", - "certificate" - ], - [ - "sites", - "site" - ], - [ - "two_factor_webauthn", - "webauthndevice" - ], - [ - "vng_api_common", - "apicredential" - ], - [ - "vng_api_common", - "jwtsecret" - ], - [ - "zgw_consumers", - "certificate" - ], - [ - "zgw_consumers", - "nlxconfig" - ], - [ - "zgw_consumers", - "service" + } + }, + { + "model": "admin_index.appgroup", + "fields": { + "order": 6, + "translations": { + "nl": "Misc" + }, + "name": "Misc", + "slug": "all", + "models": [ + [ + "contenttypes", + "contenttype" + ], + [ + "log_outgoing_requests", + "outgoingrequestslog" + ], + [ + "log_outgoing_requests", + "outgoingrequestslogconfig" + ], + [ + "mozilla_django_oidc_db", + "openidconnectconfig" + ], + [ + "notifications_api_common", + "notificationsconfig" + ], + [ + "notifications_api_common", + "subscription" + ], + [ + "sessions", + "session" + ], + [ + "simple_certmanager", + "certificate" + ], + [ + "sites", + "site" + ], + [ + "two_factor_webauthn", + "webauthndevice" + ], + [ + "vng_api_common", + "jwtsecret" + ], + [ + "zgw_consumers", + "certificate" + ], + [ + "zgw_consumers", + "nlxconfig" + ], + [ + "zgw_consumers", + "service" + ] ] - ] - } -}, -{ - "model": "admin_index.appgroup", - "fields": { - "order": 5, - "translations": { - "nl": "logs" - }, - "name": "Logs", - "slug": "logs", - "models": [ - [ - "admin", - "logentry" - ], - [ - "axes", - "accessattempt" - ], - [ - "axes", - "accessfailurelog" - ], - [ - "axes", - "accesslog" + } + }, + { + "model": "admin_index.appgroup", + "fields": { + "order": 5, + "translations": { + "nl": "logs" + }, + "name": "Logs", + "slug": "logs", + "models": [ + [ + "admin", + "logentry" + ], + [ + "axes", + "accessattempt" + ], + [ + "axes", + "accessfailurelog" + ], + [ + "axes", + "accesslog" + ] ] - ] - } -}, -{ - "model": "admin_index.appgroup", - "fields": { - "order": 2, - "translations": { - "en": "Locations & Organisations" - }, - "name": "Locaties & Organisaties", - "slug": "locaties-organisaties", - "models": [ - [ - "locaties", - "contact" - ], - [ - "locaties", - "locatie" - ], - [ - "locaties", - "organisatie" + } + }, + { + "model": "admin_index.appgroup", + "fields": { + "order": 2, + "translations": { + "en": "Locations & Organisations" + }, + "name": "Locaties & Organisaties", + "slug": "locaties-organisaties", + "models": [ + [ + "locaties", + "contact" + ], + [ + "locaties", + "locatie" + ], + [ + "locaties", + "organisatie" + ] ] - ] + } } -} ] diff --git a/src/open_producten/locaties/migrations/0004_alter_locatie_huisnummer_alter_locatie_naam_and_more.py b/src/open_producten/locaties/migrations/0004_alter_locatie_huisnummer_alter_locatie_naam_and_more.py new file mode 100644 index 0000000..db7ad52 --- /dev/null +++ b/src/open_producten/locaties/migrations/0004_alter_locatie_huisnummer_alter_locatie_naam_and_more.py @@ -0,0 +1,75 @@ +# Generated by Django 4.2.17 on 2025-01-31 13:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("locaties", "0003_organisatie_code"), + ] + + operations = [ + migrations.AlterField( + model_name="locatie", + name="huisnummer", + field=models.CharField( + blank=True, max_length=10, null=True, verbose_name="huisnummer" + ), + ), + migrations.AlterField( + model_name="locatie", + name="naam", + field=models.CharField(max_length=255, verbose_name="naam"), + ), + migrations.AlterField( + model_name="locatie", + name="stad", + field=models.CharField( + blank=True, max_length=255, null=True, verbose_name="stad" + ), + ), + migrations.AlterField( + model_name="locatie", + name="straat", + field=models.CharField( + blank=True, max_length=255, null=True, verbose_name="straat" + ), + ), + migrations.AlterField( + model_name="organisatie", + name="code", + field=models.CharField( + help_text="code van de organisatie.", + max_length=255, + unique=True, + verbose_name="code", + ), + ), + migrations.AlterField( + model_name="organisatie", + name="huisnummer", + field=models.CharField( + blank=True, max_length=10, null=True, verbose_name="huisnummer" + ), + ), + migrations.AlterField( + model_name="organisatie", + name="naam", + field=models.CharField(max_length=255, verbose_name="naam"), + ), + migrations.AlterField( + model_name="organisatie", + name="stad", + field=models.CharField( + blank=True, max_length=255, null=True, verbose_name="stad" + ), + ), + migrations.AlterField( + model_name="organisatie", + name="straat", + field=models.CharField( + blank=True, max_length=255, null=True, verbose_name="straat" + ), + ), + ] diff --git a/src/open_producten/locaties/models/locatie.py b/src/open_producten/locaties/models/locatie.py index 6b0b367..5d034b2 100644 --- a/src/open_producten/locaties/models/locatie.py +++ b/src/open_producten/locaties/models/locatie.py @@ -8,7 +8,7 @@ class BaseLocatie(BaseModel): naam = models.CharField( verbose_name=_("naam"), - max_length=100, + max_length=255, ) email = models.EmailField( verbose_name=_("email adres"), @@ -23,12 +23,12 @@ class BaseLocatie(BaseModel): validators=[validate_phone_number], ) - straat = models.CharField(_("straat"), null=True, blank=True, max_length=250) + straat = models.CharField(_("straat"), null=True, blank=True, max_length=255) huisnummer = models.CharField( _("huisnummer"), blank=True, null=True, - max_length=250, + max_length=10, ) postcode = models.CharField( @@ -40,7 +40,7 @@ class BaseLocatie(BaseModel): ) stad = models.CharField( _("stad"), - max_length=250, + max_length=255, blank=True, null=True, ) diff --git a/src/open_producten/locaties/models/organisatie.py b/src/open_producten/locaties/models/organisatie.py index 7766e84..f87f7bb 100644 --- a/src/open_producten/locaties/models/organisatie.py +++ b/src/open_producten/locaties/models/organisatie.py @@ -11,7 +11,7 @@ class Meta: code = models.CharField( verbose_name=_("code"), - max_length=100, + max_length=255, help_text=_("code van de organisatie."), unique=True, ) diff --git a/src/open_producten/producten/admin/product.py b/src/open_producten/producten/admin/product.py index 6517092..3086815 100644 --- a/src/open_producten/producten/admin/product.py +++ b/src/open_producten/producten/admin/product.py @@ -95,14 +95,14 @@ class ProductAdmin(AdminAuditLogMixin, admin.ModelAdmin): "show_actions", ) list_filter = ( - "product_type__naam", + ("product_type__translations__naam"), "aanmaak_datum", "start_datum", "eind_datum", "status", ) autocomplete_fields = ("product_type",) - search_fields = ("product_type__naam",) + search_fields = ("product_type__translations__naam",) form = ProductAdminForm @admin.display(description="Product Type") diff --git a/src/open_producten/producten/tests/api/test_product.py b/src/open_producten/producten/tests/api/test_product.py index 554ee95..4e9c8c9 100644 --- a/src/open_producten/producten/tests/api/test_product.py +++ b/src/open_producten/producten/tests/api/test_product.py @@ -77,10 +77,7 @@ def test_create_product(self): "update_datum": product.update_datum.astimezone().isoformat(), "product_type": { "id": str(product_type.id), - "naam": product_type.naam, "code": product_type.code, - "samenvatting": product_type.samenvatting, - "beschrijving": product_type.beschrijving, "uniforme_product_naam": product_type.uniforme_product_naam.uri, "gepubliceerd": True, "toegestane_statussen": ["gereed"], @@ -229,10 +226,7 @@ def test_read_producten(self): "update_datum": product1.update_datum.astimezone().isoformat(), "product_type": { "id": str(self.product_type.id), - "naam": self.product_type.naam, "code": self.product_type.code, - "samenvatting": self.product_type.samenvatting, - "beschrijving": self.product_type.beschrijving, "uniforme_product_naam": self.product_type.uniforme_product_naam.uri, "toegestane_statussen": ["gereed"], "gepubliceerd": True, @@ -255,10 +249,7 @@ def test_read_producten(self): "update_datum": product2.update_datum.astimezone().isoformat(), "product_type": { "id": str(self.product_type.id), - "naam": self.product_type.naam, "code": self.product_type.code, - "samenvatting": self.product_type.samenvatting, - "beschrijving": self.product_type.beschrijving, "uniforme_product_naam": self.product_type.uniforme_product_naam.uri, "toegestane_statussen": ["gereed"], "gepubliceerd": True, @@ -292,10 +283,7 @@ def test_read_product(self): "update_datum": "2025-12-31T01:00:00+01:00", "product_type": { "id": str(product_type.id), - "naam": product_type.naam, "code": product_type.code, - "samenvatting": product_type.samenvatting, - "beschrijving": product_type.beschrijving, "uniforme_product_naam": product_type.uniforme_product_naam.uri, "toegestane_statussen": ["gereed"], "gepubliceerd": True, diff --git a/src/open_producten/producttypen/admin/__init__.py b/src/open_producten/producttypen/admin/__init__.py index 29eef27..7f39396 100644 --- a/src/open_producten/producttypen/admin/__init__.py +++ b/src/open_producten/producttypen/admin/__init__.py @@ -1,17 +1,17 @@ from .bestand import BestandAdmin +from .content import ContentLabelAdmin from .link import LinkAdmin from .prijs import PrijsAdmin from .producttype import ProductTypeAdmin from .thema import ThemaAdmin from .upn import UniformeProductNaamAdmin -from .vraag import VraagAdmin __all__ = [ "ProductTypeAdmin", "BestandAdmin", "LinkAdmin", - "VraagAdmin", "UniformeProductNaamAdmin", "PrijsAdmin", "ThemaAdmin", + "ContentLabelAdmin", ] diff --git a/src/open_producten/producttypen/admin/content.py b/src/open_producten/producttypen/admin/content.py new file mode 100644 index 0000000..2bd8283 --- /dev/null +++ b/src/open_producten/producttypen/admin/content.py @@ -0,0 +1,31 @@ +from django.contrib import admin + +from ordered_model.admin import OrderedInlineMixin +from parler.admin import TranslatableStackedInline +from parler.forms import TranslatableModelForm + +from open_producten.producttypen.models import ContentElement, ContentLabel +from open_producten.utils.widgets import WysimarkWidget + + +@admin.register(ContentLabel) +class ContentLabelAdmin(admin.ModelAdmin): + search_fields = ("naam",) + + +class ContentElementInlineForm(TranslatableModelForm): + + class Meta: + model = ContentElement + fields = "__all__" + widgets = {"content": WysimarkWidget()} + + +class ContentElementInline(OrderedInlineMixin, TranslatableStackedInline): + model = ContentElement + readonly_fields = ("move_up_down_links",) + ordering = ("order",) + fields = ("move_up_down_links", "content", "labels") + autocomplete_fields = ("labels",) + extra = 1 + form = ContentElementInlineForm diff --git a/src/open_producten/producttypen/admin/link.py b/src/open_producten/producttypen/admin/link.py index 6472510..95a177c 100644 --- a/src/open_producten/producttypen/admin/link.py +++ b/src/open_producten/producttypen/admin/link.py @@ -12,8 +12,8 @@ class LinkInline(admin.TabularInline): @admin.register(Link) class LinkAdmin(admin.ModelAdmin): list_display = ("product_type", "naam", "url") - list_filter = ("product_type__naam",) - search_fields = ("naam", "product_type__naam") + list_filter = ("product_type__code",) + search_fields = ("naam", "product_type__translations__naam") def get_queryset(self, request): return super().get_queryset(request).select_related("product_type") diff --git a/src/open_producten/producttypen/admin/prijs.py b/src/open_producten/producttypen/admin/prijs.py index a31ed15..8fbe582 100644 --- a/src/open_producten/producttypen/admin/prijs.py +++ b/src/open_producten/producttypen/admin/prijs.py @@ -34,7 +34,7 @@ class PrijsAdmin(admin.ModelAdmin): model = Prijs inlines = [PrijsOptieInline] list_display = ("__str__", "actief_vanaf") - list_filter = ("product_type__naam", "actief_vanaf") + list_filter = ("product_type__code",) def get_queryset(self, request): return super().get_queryset(request).select_related("product_type") diff --git a/src/open_producten/producttypen/admin/producttype.py b/src/open_producten/producttypen/admin/producttype.py index c4b7e32..e85c288 100644 --- a/src/open_producten/producttypen/admin/producttype.py +++ b/src/open_producten/producttypen/admin/producttype.py @@ -3,16 +3,22 @@ from django.contrib.admin.widgets import FilteredSelectMultiple from django.utils.translation import gettext as _ +from ordered_model.admin import OrderedInlineModelAdminMixin +from parler.admin import TranslatableAdmin +from parler.forms import TranslatableModelForm + +from ...utils.widgets import WysimarkWidget from ..models import ProductType, Thema from .bestand import BestandInline +from .content import ContentElementInline from .link import LinkInline -from .vraag import VraagInline -class ProductTypeAdminForm(forms.ModelForm): +class ProductTypeAdminForm(TranslatableModelForm): class Meta: model = ProductType fields = "__all__" + widgets = {"samenvatting": WysimarkWidget()} themas = forms.ModelMultipleChoiceField( label=_("thema's"), @@ -29,7 +35,7 @@ def clean(self): @admin.register(ProductType) -class ProductTypeAdmin(admin.ModelAdmin): +class ProductTypeAdmin(OrderedInlineModelAdminMixin, TranslatableAdmin): list_display = ( "naam", "uniforme_product_naam", @@ -47,10 +53,22 @@ class ProductTypeAdmin(admin.ModelAdmin): "locaties", ) search_fields = ("naam", "uniforme_product_naam__naam", "keywords") - ordering = ("naam",) save_on_top = True form = ProductTypeAdminForm - inlines = (BestandInline, LinkInline, VraagInline) + inlines = (BestandInline, LinkInline, ContentElementInline) + fields = ( + "naam", + "gepubliceerd", + "code", + "uniforme_product_naam", + "toegestane_statussen", + "samenvatting", + "themas", + "keywords", + "organisaties", + "locaties", + "contacten", + ) def get_queryset(self, request): return ( diff --git a/src/open_producten/producttypen/admin/thema.py b/src/open_producten/producttypen/admin/thema.py index 91102a9..4a94e7d 100644 --- a/src/open_producten/producttypen/admin/thema.py +++ b/src/open_producten/producttypen/admin/thema.py @@ -1,11 +1,13 @@ +from django import forms from django.contrib import admin from django.db.models import Count from django.urls import reverse from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ +from open_producten.utils.widgets import WysimarkWidget + from ..models import ProductType, Thema -from .vraag import VraagInline class ProductTypeInline(admin.TabularInline): @@ -25,14 +27,19 @@ def has_add_permission(self, request, obj=None): return False +class ThemaAdminForm(forms.ModelForm): + class Meta: + model = Thema + fields = "__all__" + widgets = {"beschrijving": WysimarkWidget()} + + @admin.register(Thema) class ThemaAdmin(admin.ModelAdmin): - inlines = ( - VraagInline, - ProductTypeInline, - ) + inlines = (ProductTypeInline,) search_fields = ("naam", "hoofd_thema__naam") list_display = ("naam", "hoofd_thema", "gepubliceerd", "product_typen_count") + form = ThemaAdminForm @admin.display(description=_("Aantal product typen")) def product_typen_count(self, obj): diff --git a/src/open_producten/producttypen/admin/vraag.py b/src/open_producten/producttypen/admin/vraag.py deleted file mode 100644 index a4a5c0b..0000000 --- a/src/open_producten/producttypen/admin/vraag.py +++ /dev/null @@ -1,35 +0,0 @@ -from django.contrib import admin - -from markdownx.admin import MarkdownxModelAdmin -from ordered_model.admin import OrderedModelAdmin - -from ..models import Vraag - - -@admin.register(Vraag) -class VraagAdmin(OrderedModelAdmin, MarkdownxModelAdmin): - list_filter = ("thema",) - list_display = ( - "vraag", - "thema", - "product_type", - ) - search_fields = ( - "vraag", - "antwoord", - "thema__naam", - "product_type__naam", - ) - - def get_queryset(self, request): - return super().get_queryset(request).select_related("thema", "product_type") - - -class VraagInline(admin.TabularInline): - model = Vraag - extra = 1 - - fields = [ - "vraag", - "antwoord", - ] diff --git a/src/open_producten/producttypen/migrations/0006_contentelement_contentelementtranslation_and_more.py b/src/open_producten/producttypen/migrations/0006_contentelement_contentelementtranslation_and_more.py new file mode 100644 index 0000000..c228054 --- /dev/null +++ b/src/open_producten/producttypen/migrations/0006_contentelement_contentelementtranslation_and_more.py @@ -0,0 +1,259 @@ +# Generated by Django 4.2.17 on 2025-02-04 09:59 + +from django.db import migrations, models +import django.db.models.deletion +import parler.fields +import parler.models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "producttypen", + "0005_producttype_code_producttype_toegestane_statussen_and_more", + ), + ] + + operations = [ + migrations.CreateModel( + name="ContentElement", + fields=[ + ( + "order", + models.PositiveIntegerField( + db_index=True, editable=False, verbose_name="order" + ), + ), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ], + options={ + "verbose_name": "content element", + "verbose_name_plural": "content elementen", + "ordering": ("product_type", "order"), + }, + bases=(parler.models.TranslatableModel, models.Model), + ), + migrations.CreateModel( + name="ContentElementTranslation", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "language_code", + models.CharField( + db_index=True, max_length=15, verbose_name="Language" + ), + ), + ( + "content", + models.TextField( + help_text="De content van dit content element", + verbose_name="content", + ), + ), + ( + "master", + parler.fields.TranslationsForeignKey( + editable=False, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="translations", + to="producttypen.contentelement", + ), + ), + ], + options={ + "verbose_name": "content element Translation", + "db_table": "producttypen_contentelement_translation", + "db_tablespace": "", + "managed": True, + "default_permissions": (), + "unique_together": {("language_code", "master")}, + }, + bases=(parler.models.TranslatableModel, models.Model), + ), + migrations.CreateModel( + name="ContentLabel", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("naam", models.CharField(max_length=255, unique=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="ProductTypeTranslation", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "language_code", + models.CharField( + db_index=True, max_length=15, verbose_name="Language" + ), + ), + ( + "samenvatting", + models.TextField( + default="", + help_text="Korte samenvatting van het product type.", + verbose_name="samenvatting", + ), + ), + ( + "naam", + models.CharField( + help_text="naam van het product type.", + max_length=255, + verbose_name="product type naam", + ), + ), + ], + options={ + "verbose_name": "Product type Translation", + "db_table": "producttypen_producttype_translation", + "db_tablespace": "", + "managed": True, + "default_permissions": (), + }, + bases=(parler.models.TranslatableModel, models.Model), + ), + migrations.RemoveField( + model_name="producttype", + name="beschrijving", + ), + migrations.RemoveField( + model_name="producttype", + name="naam", + ), + migrations.RemoveField( + model_name="producttype", + name="samenvatting", + ), + migrations.AlterField( + model_name="link", + name="naam", + field=models.CharField( + help_text="Naam van de link.", max_length=255, verbose_name="naam" + ), + ), + migrations.AlterField( + model_name="prijsoptie", + name="beschrijving", + field=models.CharField( + help_text="Korte beschrijving van de optie.", + max_length=255, + verbose_name="beschrijving", + ), + ), + migrations.AlterField( + model_name="producttype", + name="code", + field=models.CharField( + help_text="code van het product type.", + max_length=255, + unique=True, + verbose_name="code", + ), + ), + migrations.AlterField( + model_name="thema", + name="beschrijving", + field=models.TextField( + blank=True, + default="", + help_text="Beschrijving van het thema, ondersteund markdown format.", + verbose_name="beschrijving", + ), + ), + migrations.AlterField( + model_name="thema", + name="naam", + field=models.CharField( + help_text="Naam van het thema.", max_length=255, verbose_name="naam" + ), + ), + migrations.AlterField( + model_name="uniformeproductnaam", + name="naam", + field=models.CharField( + help_text="Uniforme product naam", + max_length=255, + unique=True, + verbose_name="naam", + ), + ), + migrations.DeleteModel( + name="Vraag", + ), + migrations.AddField( + model_name="producttypetranslation", + name="master", + field=parler.fields.TranslationsForeignKey( + editable=False, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="translations", + to="producttypen.producttype", + ), + ), + migrations.AddField( + model_name="contentelement", + name="labels", + field=models.ManyToManyField( + blank=True, + help_text="De labels van dit content element", + related_name="content_elements", + to="producttypen.contentlabel", + verbose_name="labels", + ), + ), + migrations.AddField( + model_name="contentelement", + name="product_type", + field=models.ForeignKey( + help_text="Het product type van dit content element", + on_delete=django.db.models.deletion.CASCADE, + related_name="content_elementen", + to="producttypen.producttype", + verbose_name="label", + ), + ), + migrations.AlterUniqueTogether( + name="producttypetranslation", + unique_together={("language_code", "master")}, + ), + ] diff --git a/src/open_producten/producttypen/models/__init__.py b/src/open_producten/producttypen/models/__init__.py index 5d50441..b893885 100644 --- a/src/open_producten/producttypen/models/__init__.py +++ b/src/open_producten/producttypen/models/__init__.py @@ -1,18 +1,19 @@ from .bestand import Bestand +from .content import ContentElement, ContentLabel from .link import Link from .prijs import Prijs, PrijsOptie from .producttype import ProductType from .thema import Thema from .upn import UniformeProductNaam -from .vraag import Vraag __all__ = [ "UniformeProductNaam", - "Vraag", "Thema", "Link", "Prijs", "PrijsOptie", "ProductType", "Bestand", + "ContentElement", + "ContentLabel", ] diff --git a/src/open_producten/producttypen/models/content.py b/src/open_producten/producttypen/models/content.py new file mode 100644 index 0000000..eb84a34 --- /dev/null +++ b/src/open_producten/producttypen/models/content.py @@ -0,0 +1,58 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from ordered_model.models import OrderedModel, OrderedModelManager, OrderedModelQuerySet +from parler.managers import TranslatableQuerySet +from parler.models import TranslatableModel, TranslatedFields + +from open_producten.utils.models import BaseModel + + +class ContentLabel(BaseModel): + naam = models.CharField(max_length=255, unique=True) + + def __str__(self): + return self.naam + + +class ContentElementQuerySet(TranslatableQuerySet, OrderedModelQuerySet): + pass + + +class ContentElementManager(OrderedModelManager.from_queryset(ContentElementQuerySet)): + pass + + +class ContentElement(TranslatableModel, OrderedModel, BaseModel): + labels = models.ManyToManyField( + ContentLabel, + verbose_name=_("labels"), + blank=True, + related_name="content_elements", + help_text=_("De labels van dit content element"), + ) + product_type = models.ForeignKey( + "ProductType", + verbose_name=_("label"), + on_delete=models.CASCADE, + help_text=_("Het product type van dit content element"), + related_name="content_elementen", + ) + + translations = TranslatedFields( + content=models.TextField( + _("content"), + help_text=_("De content van dit content element"), + ) + ) + + order_with_respect_to = "product_type" + objects = ContentElementManager() + + def __str__(self): + return f"{self.product_type} - {','.join(list(self.labels.values_list('naam', flat=True)))}" + + class Meta: + verbose_name = _("content element") + verbose_name_plural = _("content elementen") + ordering = ("product_type", "order") diff --git a/src/open_producten/producttypen/models/link.py b/src/open_producten/producttypen/models/link.py index 3ec1fb7..239f492 100644 --- a/src/open_producten/producttypen/models/link.py +++ b/src/open_producten/producttypen/models/link.py @@ -15,7 +15,7 @@ class Link(BaseModel): help_text=_("Het product type waarbij deze link hoort."), ) naam = models.CharField( - verbose_name=_("naam"), max_length=100, help_text=_("Naam van de link.") + verbose_name=_("naam"), max_length=255, help_text=_("Naam van de link.") ) url = models.URLField(verbose_name=_("Url"), help_text=_("Url van de link.")) diff --git a/src/open_producten/producttypen/models/prijs.py b/src/open_producten/producttypen/models/prijs.py index 39a03c2..143f854 100644 --- a/src/open_producten/producttypen/models/prijs.py +++ b/src/open_producten/producttypen/models/prijs.py @@ -50,7 +50,7 @@ class PrijsOptie(BaseModel): ) beschrijving = models.CharField( verbose_name=_("beschrijving"), - max_length=100, + max_length=255, help_text=_("Korte beschrijving van de optie."), ) diff --git a/src/open_producten/producttypen/models/producttype.py b/src/open_producten/producttypen/models/producttype.py index fb46f61..a553dae 100644 --- a/src/open_producten/producttypen/models/producttype.py +++ b/src/open_producten/producttypen/models/producttype.py @@ -4,7 +4,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from markdownx.models import MarkdownxField +from parler.models import TranslatableModel, TranslatedFields from open_producten.locaties.models import Contact, Locatie, Organisatie from open_producten.utils.fields import ChoiceArrayField @@ -24,16 +24,11 @@ class ProductStateChoices(models.TextChoices): VERLOPEN = "verlopen", _("Verlopen") -class ProductType(BasePublishableModel): - naam = models.CharField( - verbose_name=_("product type naam"), - max_length=100, - help_text=_("naam van het product type."), - ) +class ProductType(BasePublishableModel, TranslatableModel): code = models.CharField( verbose_name=_("code"), - max_length=100, + max_length=255, help_text=_("code van het product type."), unique=True, ) @@ -52,18 +47,6 @@ class ProductType(BasePublishableModel): help_text=_("toegestane statussen voor producten van dit type."), ) - samenvatting = models.TextField( - verbose_name=_("samenvatting"), - default="", - max_length=300, - help_text=_("Korte beschrijving van het product type, maximaal 300 karakters."), - ) - - beschrijving = MarkdownxField( - verbose_name=_("beschrijving"), - help_text=_("Product type beschrijving, ondersteund markdown format."), - ) - keywords = ArrayField( models.CharField(max_length=100, blank=True), verbose_name=_("Keywords"), @@ -112,6 +95,19 @@ class ProductType(BasePublishableModel): help_text=_("De locaties waar het product beschikbaar is."), ) + translations = TranslatedFields( + samenvatting=models.TextField( + verbose_name=_("samenvatting"), + default="", + help_text=_("Korte samenvatting van het product type."), + ), + naam=models.CharField( + verbose_name=_("product type naam"), + max_length=255, + help_text=_("naam van het product type."), + ), + ) + class Meta: verbose_name = _("Product type") verbose_name_plural = _("Product typen") diff --git a/src/open_producten/producttypen/models/thema.py b/src/open_producten/producttypen/models/thema.py index e4301b1..387cb9d 100644 --- a/src/open_producten/producttypen/models/thema.py +++ b/src/open_producten/producttypen/models/thema.py @@ -2,15 +2,13 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from markdownx.models import MarkdownxField - from open_producten.utils.models import BasePublishableModel class Thema(BasePublishableModel): naam = models.CharField( - verbose_name=_("naam"), max_length=100, help_text=_("Naam van het thema.") + verbose_name=_("naam"), max_length=255, help_text=_("Naam van het thema.") ) hoofd_thema = models.ForeignKey( @@ -23,7 +21,7 @@ class Thema(BasePublishableModel): on_delete=models.PROTECT, ) - beschrijving = MarkdownxField( + beschrijving = models.TextField( verbose_name=_("beschrijving"), blank=True, default="", diff --git a/src/open_producten/producttypen/models/upn.py b/src/open_producten/producttypen/models/upn.py index fd5509c..83773e3 100644 --- a/src/open_producten/producttypen/models/upn.py +++ b/src/open_producten/producttypen/models/upn.py @@ -7,7 +7,7 @@ class UniformeProductNaam(BaseModel): naam = models.CharField( verbose_name=_("naam"), - max_length=100, + max_length=255, help_text=_("Uniforme product naam"), unique=True, ) diff --git a/src/open_producten/producttypen/models/vraag.py b/src/open_producten/producttypen/models/vraag.py deleted file mode 100644 index 9f9da07..0000000 --- a/src/open_producten/producttypen/models/vraag.py +++ /dev/null @@ -1,62 +0,0 @@ -from django.core.validators import ValidationError -from django.db import models -from django.utils.translation import gettext_lazy as _ - -from markdownx.models import MarkdownxField - -from open_producten.utils.models import BaseModel - -from .producttype import ProductType -from .thema import Thema - - -class Vraag(BaseModel): - thema = models.ForeignKey( - Thema, - verbose_name=_("thema"), - on_delete=models.CASCADE, - blank=True, - null=True, - related_name="vragen", - help_text=_("Het thema waarbij deze vraag hoort."), - ) - product_type = models.ForeignKey( - ProductType, - verbose_name=_("Product type"), - on_delete=models.CASCADE, - blank=True, - null=True, - related_name="vragen", - help_text=_("Het product type waarbij deze vraag hoort."), - ) - vraag = models.CharField( - verbose_name=_("vraag"), - max_length=250, - help_text=_("De vraag die wordt beantwoord."), - ) - antwoord = MarkdownxField( - verbose_name=_("antwoord"), - help_text=_("Het antwoord op de vraag, ondersteund markdown format."), - ) - - class Meta: - verbose_name = _("Vraag") - verbose_name_plural = _("Vragen") - - def clean(self): - validate_thema_or_product_type(self.thema, self.product_type) - - def __str__(self): - return self.vraag - - -def validate_thema_or_product_type(thema, product_type): - if thema and product_type: - raise ValidationError( - _("Een vraag kan niet gelink zijn aan een thema en een product type.") - ) - - if not thema and not product_type: - raise ValidationError( - _("Een vraag moet gelinkt zijn aan een thema of een product type.") - ) diff --git a/src/open_producten/producttypen/serializers/__init__.py b/src/open_producten/producttypen/serializers/__init__.py index fc1dba5..be747d9 100644 --- a/src/open_producten/producttypen/serializers/__init__.py +++ b/src/open_producten/producttypen/serializers/__init__.py @@ -3,7 +3,6 @@ from .prijs import PrijsOptieSerializer, PrijsSerializer from .producttype import ProductTypeActuelePrijsSerializer, ProductTypeSerializer from .thema import ThemaSerializer -from .vraag import VraagSerializer __all__ = [ "LinkSerializer", @@ -13,5 +12,4 @@ "PrijsOptieSerializer", "ProductTypeSerializer", "ProductTypeActuelePrijsSerializer", - "VraagSerializer", ] diff --git a/src/open_producten/producttypen/serializers/content.py b/src/open_producten/producttypen/serializers/content.py new file mode 100644 index 0000000..4f18dad --- /dev/null +++ b/src/open_producten/producttypen/serializers/content.py @@ -0,0 +1,90 @@ +from django.utils.translation import gettext_lazy as _ + +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field +from parler_rest.serializers import TranslatableModelSerializer +from rest_framework import serializers + +from open_producten.producttypen.models import ContentElement, ContentLabel, ProductType + + +class ContentElementSerializer(TranslatableModelSerializer): + labels = serializers.SlugRelatedField( + slug_field="naam", + queryset=ContentLabel.objects.all(), + many=True, + required=False, + ) + + content = serializers.CharField( + required=True, + max_length=255, + help_text=_("De content van dit content element."), + ) + + product_type_id = serializers.PrimaryKeyRelatedField( + source="product_type", queryset=ProductType.objects.all() + ) + + taal = serializers.SerializerMethodField( + read_only=True, help_text=_("De huidige taal van het content element.") + ) + + @extend_schema_field(OpenApiTypes.STR) + def get_taal(self, obj): + requested_language = self.context["request"].LANGUAGE_CODE + return requested_language if obj.has_translation(requested_language) else "nl" + + class Meta: + model = ContentElement + fields = ("id", "content", "labels", "product_type_id", "taal") + + def create(self, validated_data): + content = validated_data.pop("content") + + content_element = super().create(validated_data) + content_element.set_current_language("nl") + content_element.content = content + + return content_element + + def update(self, instance, validated_data): + content = validated_data.pop("content", None) + + instance = super().update(instance, validated_data) + + instance.set_current_language("nl") + if content: + instance.content = content + + return instance + + +class NestedContentElementSerializer(ContentElementSerializer): + + class Meta: + model = ContentElement + fields = ( + "id", + "taal", + "content", + "labels", + ) + + +class ContentElementTranslationSerializer(serializers.ModelSerializer): + content = serializers.CharField( + required=True, + help_text=_("De content van dit content element."), + ) + + class Meta: + model = ContentElement + fields = ("id", "content") + + +class ContentLabelSerializer(serializers.ModelSerializer): + + class Meta: + model = ContentLabel + fields = ("naam",) diff --git a/src/open_producten/producttypen/serializers/producttype.py b/src/open_producten/producttypen/serializers/producttype.py index 2ef768d..8dfb2a1 100644 --- a/src/open_producten/producttypen/serializers/producttype.py +++ b/src/open_producten/producttypen/serializers/producttype.py @@ -1,6 +1,9 @@ from django.db import transaction from django.utils.translation import gettext_lazy as _ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field +from parler_rest.serializers import TranslatableModelSerializer from rest_framework import serializers from open_producten.locaties.models import Contact, Locatie, Organisatie @@ -15,7 +18,6 @@ from .bestand import NestedBestandSerializer from .link import NestedLinkSerializer from .prijs import NestedPrijsSerializer, PrijsSerializer -from .vraag import NestedVraagSerializer class NestedThemaSerializer(serializers.ModelSerializer): @@ -32,7 +34,7 @@ class Meta: ) -class ProductTypeSerializer(serializers.ModelSerializer): +class ProductTypeSerializer(TranslatableModelSerializer): uniforme_product_naam = serializers.SlugRelatedField( slug_field="uri", queryset=UniformeProductNaam.objects.all() ) @@ -72,11 +74,27 @@ class ProductTypeSerializer(serializers.ModelSerializer): source="contacten", ) - vragen = NestedVraagSerializer(many=True, read_only=True) prijzen = NestedPrijsSerializer(many=True, read_only=True) links = NestedLinkSerializer(many=True, read_only=True) bestanden = NestedBestandSerializer(many=True, read_only=True) + naam = serializers.CharField( + required=True, max_length=255, help_text=_("naam van het product type.") + ) + samenvatting = serializers.CharField( + required=True, + help_text=_("Korte beschrijving van het product type."), + ) + + taal = serializers.SerializerMethodField( + read_only=True, help_text=_("De huidige taal van het product type.") + ) + + @extend_schema_field(OpenApiTypes.STR) + def get_taal(self, obj): + requested_language = self.context["request"].LANGUAGE_CODE + return requested_language if obj.has_translation(requested_language) else "nl" + class Meta: model = ProductType fields = "__all__" @@ -97,6 +115,8 @@ def create(self, validated_data): locaties = validated_data.pop("locaties") organisaties = validated_data.pop("organisaties") contacten = validated_data.pop("contacten") + naam = validated_data.pop("naam") + samenvatting = validated_data.pop("samenvatting") product_type = ProductType.objects.create(**validated_data) product_type.themas.set(themas) @@ -104,7 +124,10 @@ def create(self, validated_data): product_type.organisaties.set(organisaties) product_type.contacten.set(contacten) - product_type.save() + product_type.set_current_language("nl") + product_type.naam = naam + product_type.samenvatting = samenvatting + product_type.add_contact_organisaties() return product_type @@ -115,6 +138,9 @@ def update(self, instance, validated_data): organisaties = validated_data.pop("organisaties", None) contacten = validated_data.pop("contacten", None) + naam = validated_data.pop("naam", None) + samenvatting = validated_data.pop("samenvatting", None) + instance = super().update(instance, validated_data) if themas: @@ -126,7 +152,12 @@ def update(self, instance, validated_data): if contacten: instance.contacten.set(contacten) - instance.save() + instance.set_current_language("nl") + if naam: + instance.naam = naam + if samenvatting: + instance.samenvatting = samenvatting + instance.add_contact_organisaties() return instance @@ -138,4 +169,23 @@ class ProductTypeActuelePrijsSerializer(serializers.ModelSerializer): class Meta: model = ProductType - fields = ("id", "naam", "upl_naam", "upl_uri", "actuele_prijs") + fields = ("id", "code", "upl_naam", "upl_uri", "actuele_prijs") + + +class ProductTypeTranslationSerializer(serializers.ModelSerializer): + + naam = serializers.CharField( + required=True, max_length=255, help_text=_("naam van het product type.") + ) + samenvatting = serializers.CharField( + required=True, + help_text=_("Korte beschrijving van het product type."), + ) + + class Meta: + model = ProductType + fields = ( + "id", + "naam", + "samenvatting", + ) diff --git a/src/open_producten/producttypen/serializers/thema.py b/src/open_producten/producttypen/serializers/thema.py index a3968f5..f31df78 100644 --- a/src/open_producten/producttypen/serializers/thema.py +++ b/src/open_producten/producttypen/serializers/thema.py @@ -7,7 +7,6 @@ from ...utils.drf_validators import DuplicateIdValidator from .validators import ThemaGepubliceerdStateValidator, ThemaSelfReferenceValidator -from .vraag import NestedVraagSerializer class NestedProductTypeSerializer(serializers.ModelSerializer): @@ -19,10 +18,7 @@ class Meta: model = ProductType fields = ( "id", - "naam", "code", - "samenvatting", - "beschrijving", "keywords", "uniforme_product_naam", "toegestane_statussen", @@ -39,7 +35,6 @@ class ThemaSerializer(serializers.ModelSerializer): help_text=_("Het hoofd thema waaronder dit thema valt."), ) product_typen = NestedProductTypeSerializer(many=True, read_only=True) - vragen = NestedVraagSerializer(many=True, read_only=True) # TODO: remove? product_type_ids = serializers.PrimaryKeyRelatedField( @@ -55,7 +50,6 @@ class Meta: "id", "naam", "beschrijving", - "vragen", "gepubliceerd", "aanmaak_datum", "update_datum", @@ -75,7 +69,6 @@ def create(self, validated_data): thema = Thema.objects.create(**validated_data) thema.product_typen.set(product_typen) - thema.save() return thema @@ -93,5 +86,5 @@ def update(self, instance, validated_data): if product_typen: instance.product_typen.set(product_typen) - instance.save() + return instance diff --git a/src/open_producten/producttypen/serializers/validators.py b/src/open_producten/producttypen/serializers/validators.py index 51f8f25..c62f184 100644 --- a/src/open_producten/producttypen/serializers/validators.py +++ b/src/open_producten/producttypen/serializers/validators.py @@ -6,21 +6,6 @@ from ...utils.serializers import get_from_serializer_data_or_instance from ..models import PrijsOptie from ..models.thema import disallow_self_reference, validate_gepubliceerd_state -from ..models.vraag import validate_thema_or_product_type - - -class ProductTypeOrThemaValidator: - requires_context = True - - def __call__(self, value, serializer): - thema = get_from_serializer_data_or_instance("thema", value, serializer) - product_type = get_from_serializer_data_or_instance( - "product_type", value, serializer - ) - try: - validate_thema_or_product_type(thema, product_type) - except ValidationError as e: - raise serializers.ValidationError({"product_type_or_thema": e.message}) class ThemaGepubliceerdStateValidator: diff --git a/src/open_producten/producttypen/serializers/vraag.py b/src/open_producten/producttypen/serializers/vraag.py deleted file mode 100644 index 7d252bb..0000000 --- a/src/open_producten/producttypen/serializers/vraag.py +++ /dev/null @@ -1,32 +0,0 @@ -from rest_framework import serializers - -from open_producten.producttypen.models import ProductType, Thema, Vraag -from open_producten.producttypen.serializers.validators import ( - ProductTypeOrThemaValidator, -) - - -class VraagSerializer(serializers.ModelSerializer): - product_type_id = serializers.PrimaryKeyRelatedField( - source="product_type", - queryset=ProductType.objects.all(), - required=False, - allow_null=True, - ) - thema_id = serializers.PrimaryKeyRelatedField( - source="thema", - queryset=Thema.objects.all(), - required=False, - allow_null=True, - ) - - class Meta: - model = Vraag - fields = ("id", "product_type_id", "thema_id", "vraag", "antwoord") - validators = [ProductTypeOrThemaValidator()] - - -class NestedVraagSerializer(VraagSerializer): - class Meta: - model = Vraag - fields = ("id", "vraag", "antwoord") diff --git a/src/open_producten/producttypen/tests/api/test_content.py b/src/open_producten/producttypen/tests/api/test_content.py new file mode 100644 index 0000000..40cbec1 --- /dev/null +++ b/src/open_producten/producttypen/tests/api/test_content.py @@ -0,0 +1,226 @@ +from django.urls import reverse +from django.utils.translation import gettext as _ + +from rest_framework import status +from rest_framework.exceptions import ErrorDetail +from rest_framework.test import APIClient + +from open_producten.producttypen.models import ContentElement, ContentLabel +from open_producten.utils.tests.cases import BaseApiTestCase + +from ..factories import ContentElementFactory, ContentLabelFactory, ProductTypeFactory + + +class TestContentElement(BaseApiTestCase): + + def setUp(self): + super().setUp() + self.product_type = ProductTypeFactory.create() + self.label = ContentLabel.objects.create(naam="voorwaarden") + + self.data = { + "labels": [self.label.naam], + "content": "Voorwaarden", + "product_type_id": self.product_type.id, + } + self.content_element = ContentElementFactory( + content="Test Content", product_type=self.product_type + ) + + self.content_element.labels.add(self.label) + self.content_element.save() + + self.path = reverse("content-list") + self.detail_path = reverse("content-detail", args=[self.content_element.id]) + + def test_read_content_element_without_credentials_returns_error(self): + response = APIClient().get(self.path) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_required_fields(self): + response = self.client.post(self.path, {}) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + response.data, + { + "content": [ + ErrorDetail(string=_("This field is required."), code="required") + ], + "product_type_id": [ + ErrorDetail(_("This field is required."), code="required") + ], + }, + ) + + def test_create_content_element(self): + response = self.client.post(self.path, self.data) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(ContentElement.objects.count(), 2) + + response.data.pop("id") + expected_data = { + "labels": [self.label.naam], + "content": "Voorwaarden", + "product_type_id": self.product_type.id, + "taal": "nl", + } + self.assertEqual(response.data, expected_data) + + def test_update_content_element(self): + data = self.data | {"content": "update"} + response = self.client.put(self.detail_path, data) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(ContentElement.objects.count(), 1) + self.assertEqual(ContentElement.objects.first().content, "update") + + def test_partial_update_content_element(self): + data = {"content": "update"} + response = self.client.patch(self.detail_path, data) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(ContentElement.objects.count(), 1) + self.assertEqual(ContentElement.objects.first().content, "update") + + def test_read_content_element(self): + response = self.client.get(self.detail_path) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + expected_data = { + "id": str(self.content_element.id), + "content": self.content_element.content, + "labels": [ + self.label.naam, + ], + "product_type_id": self.product_type.id, + "taal": "nl", + } + self.assertEqual(response.data, expected_data) + + def test_read_content_element_in_other_language(self): + content_element = ContentElementFactory.create() + content_element.set_current_language("en") + content_element.content = "content element EN" + content_element.save() + + path = reverse("content-detail", args=[content_element.id]) + + response = self.client.get(path, headers={"Accept-Language": "en"}) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["content"], "content element EN") + self.assertEqual(response.data["taal"], "en") + + def test_read_content_element_in_fallback_language(self): + content_element = ContentElementFactory.create(content="content element NL") + path = reverse("content-detail", args=[content_element.id]) + + response = self.client.get(path, headers={"Accept-Language": "de"}) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["content"], "content element NL") + self.assertEqual(response.data["taal"], "nl") + + def test_delete_content_element(self): + response = self.client.delete(self.detail_path) + + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(ContentElement.objects.count(), 0) + + +class TestContentElementActions(BaseApiTestCase): + + def setUp(self): + super().setUp() + self.content_element = ContentElementFactory.create() + + def test_put_vertaling(self): + path = reverse("content-vertaling", args=(self.content_element.id, "en")) + + data = {"content": "content EN"} + response = self.client.put(path, data) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data, + { + "id": str(self.content_element.id), + "content": "content EN", + }, + ) + self.content_element.set_current_language("en") + self.assertEqual(self.content_element.content, "content EN") + + self.content_element.set_current_language("nl") + self.assertNotEqual(self.content_element.content, "content EN") + + def test_put_nl_vertaling(self): + path = reverse("content-vertaling", args=(self.content_element.id, "nl")) + + data = {"content": "content NL"} + response = self.client.put(path, data) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_put_vertaling_with_unsupported_language(self): + path = reverse("content-vertaling", args=(self.content_element.id, "fr")) + + data = {"naam": "name FR", "samenvatting": "summary FR"} + response = self.client.put(path, data) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_delete_nonexistent_vertaling(self): + path = reverse("content-vertaling", args=(self.content_element.id, "de")) + + response = self.client.delete(path) + + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_delete_nl_vertaling(self): + path = reverse("content-vertaling", args=(self.content_element.id, "nl")) + + response = self.client.delete(path) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_delete_vertaling(self): + self.content_element.set_current_language("en") + self.content_element.content = "content EN" + self.content_element.save() + + path = reverse("content-vertaling", args=(self.content_element.id, "en")) + + response = self.client.delete(path) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + self.content_element.refresh_from_db() + self.assertFalse(self.content_element.has_translation("en")) + + +class TestContentLabel(BaseApiTestCase): + def setUp(self): + super().setUp() + + self.path = reverse("contentlabel-list") + + def test_read_content_labels(self): + label1 = ContentLabelFactory.create() + label2 = ContentLabelFactory.create() + + response = self.client.get(self.path) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 2) + expected_data = [ + { + "naam": label1.naam, + }, + { + "naam": label2.naam, + }, + ] + self.assertCountEqual(response.data["results"], expected_data) diff --git a/src/open_producten/producttypen/tests/api/test_producttype.py b/src/open_producten/producttypen/tests/api/test_producttype.py index e90a578..60e1a85 100644 --- a/src/open_producten/producttypen/tests/api/test_producttype.py +++ b/src/open_producten/producttypen/tests/api/test_producttype.py @@ -16,13 +16,13 @@ from open_producten.producttypen.models import Link, ProductType from open_producten.producttypen.tests.factories import ( BestandFactory, + ContentElementFactory, LinkFactory, PrijsFactory, PrijsOptieFactory, ProductTypeFactory, ThemaFactory, UniformeProductNaamFactory, - VraagFactory, ) from open_producten.utils.tests.cases import BaseApiTestCase @@ -38,7 +38,6 @@ def setUp(self): "naam": "test-product-type", "code": "PT=12345", "samenvatting": "test", - "beschrijving": "test test", "uniforme_product_naam": upn.uri, "thema_ids": [self.thema.id], } @@ -68,7 +67,7 @@ def test_required_fields(self): "thema_ids": [ ErrorDetail(string=_("This field is required."), code="required") ], - "beschrijving": [ + "samenvatting": [ ErrorDetail(string=_("This field is required."), code="required") ], "code": [ @@ -90,10 +89,9 @@ def test_create_minimal_product_type(self): "naam": product_type.naam, "code": product_type.code, "samenvatting": product_type.samenvatting, - "beschrijving": product_type.beschrijving, + "taal": "nl", "uniforme_product_naam": product_type.uniforme_product_naam.uri, "toegestane_statussen": [], - "vragen": [], "prijzen": [], "links": [], "bestanden": [], @@ -240,10 +238,9 @@ def test_create_complete_product_type(self): "naam": product_type.naam, "code": product_type.code, "samenvatting": product_type.samenvatting, - "beschrijving": product_type.beschrijving, + "taal": "nl", "uniforme_product_naam": product_type.uniforme_product_naam.uri, "toegestane_statussen": [], - "vragen": [], "prijzen": [], "links": [], "bestanden": [], @@ -508,23 +505,6 @@ def test_read_product_type_link(self): self.assertEqual(response.data["links"], expected_data) - def test_read_product_type_vraag(self): - product_type = ProductTypeFactory.create() - vraag = VraagFactory.create(product_type=product_type) - - response = self.client.get(self.detail_path(product_type)) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - expected_data = [ - { - "id": str(vraag.id), - "vraag": vraag.vraag, - "antwoord": vraag.antwoord, - } - ] - - self.assertEqual(response.data["vragen"], expected_data) - def test_read_product_type_bestand(self): product_type = ProductTypeFactory.create() bestand = BestandFactory.create(product_type=product_type) @@ -582,10 +562,9 @@ def test_read_product_typen(self): "naam": product_type1.naam, "code": product_type1.code, "samenvatting": product_type1.samenvatting, - "beschrijving": product_type1.beschrijving, + "taal": "nl", "uniforme_product_naam": product_type1.uniforme_product_naam.uri, "toegestane_statussen": [], - "vragen": [], "prijzen": [], "links": [], "bestanden": [], @@ -613,10 +592,9 @@ def test_read_product_typen(self): "naam": product_type2.naam, "code": product_type2.code, "samenvatting": product_type2.samenvatting, - "beschrijving": product_type2.beschrijving, + "taal": "nl", "uniforme_product_naam": product_type2.uniforme_product_naam.uri, "toegestane_statussen": [], - "vragen": [], "prijzen": [], "links": [], "bestanden": [], @@ -655,10 +633,9 @@ def test_read_product_type(self): "naam": product_type.naam, "code": product_type.code, "samenvatting": product_type.samenvatting, - "beschrijving": product_type.beschrijving, + "taal": "nl", "uniforme_product_naam": product_type.uniforme_product_naam.uri, "toegestane_statussen": [], - "vragen": [], "prijzen": [], "links": [], "bestanden": [], @@ -684,6 +661,39 @@ def test_read_product_type(self): self.assertEqual(response.data, expected_data) + def test_read_product_type_in_other_language(self): + product_type = ProductTypeFactory.create() + product_type.themas.add(self.thema) + product_type.set_current_language("en") + product_type.naam = "product type EN" + product_type.samenvatting = "summary" + product_type.save() + + response = self.client.get( + self.detail_path(product_type), headers={"Accept-Language": "en"} + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["naam"], "product type EN") + self.assertEqual(response.data["samenvatting"], "summary") + self.assertEqual(response.data["taal"], "en") + + def test_read_product_type_in_fallback_language(self): + product_type = ProductTypeFactory.create( + naam="product type NL", samenvatting="samenvatting" + ) + product_type.themas.add(self.thema) + product_type.save() + + response = self.client.get( + self.detail_path(product_type), headers={"Accept-Language": "de"} + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["naam"], "product type NL") + self.assertEqual(response.data["samenvatting"], "samenvatting") + self.assertEqual(response.data["taal"], "nl") + def test_delete_product_type(self): product_type = ProductTypeFactory.create() @@ -709,7 +719,7 @@ def setUp(self): self.expected_data = { "id": str(self.product_type.id), - "naam": self.product_type.naam, + "code": self.product_type.code, "upl_naam": self.product_type.uniforme_product_naam.naam, "upl_uri": self.product_type.uniforme_product_naam.uri, "actuele_prijs": None, @@ -809,3 +819,141 @@ def test_get_actuele_prijs_when_product_type_has_actuele_prijs(self): }, }, ) + + def test_put_vertaling(self): + path = reverse("producttype-vertaling", args=(self.product_type.id, "en")) + + data = {"naam": "name EN", "samenvatting": "summary EN"} + response = self.client.put(path, data) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data, + { + "id": str(self.product_type.id), + "naam": "name EN", + "samenvatting": "summary EN", + }, + ) + self.product_type.set_current_language("en") + self.assertEqual(self.product_type.naam, "name EN") + + self.product_type.set_current_language("nl") + self.assertNotEqual(self.product_type.naam, "name EN") + + def test_put_nl_vertaling(self): + path = reverse("producttype-vertaling", args=(self.product_type.id, "nl")) + + data = {"naam": "name NL", "samenvatting": "summary NL"} + response = self.client.put(path, data) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_put_vertaling_with_unsupported_language(self): + path = reverse("producttype-vertaling", args=(self.product_type.id, "fr")) + + data = {"naam": "name FR", "samenvatting": "summary FR"} + response = self.client.put(path, data) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_patch_vertaling(self): + self.product_type.set_current_language("en") + self.product_type.naam = "name EN" + self.product_type.samenvatting = "summary EN" + self.product_type.save() + + path = reverse("producttype-vertaling", args=(self.product_type.id, "en")) + + data = {"naam": "name EN 2"} + response = self.client.patch(path, data) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.product_type.refresh_from_db() + self.assertEqual(self.product_type.naam, "name EN 2") + + def test_delete_nonexistent_vertaling(self): + path = reverse("producttype-vertaling", args=(self.product_type.id, "en")) + + response = self.client.delete(path) + + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_delete_nl_vertaling(self): + path = reverse("producttype-vertaling", args=(self.product_type.id, "nl")) + + response = self.client.delete(path) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_delete_vertaling(self): + self.product_type.set_current_language("en") + self.product_type.naam = "name EN" + self.product_type.samenvatting = "summary EN" + self.product_type.save() + + path = reverse("producttype-vertaling", args=(self.product_type.id, "en")) + + response = self.client.delete(path) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + self.product_type.refresh_from_db() + self.assertFalse(self.product_type.has_translation("en")) + + def test_nl_content(self): + element1 = ContentElementFactory.create(product_type=self.product_type) + element2 = ContentElementFactory.create(product_type=self.product_type) + + path = reverse("producttype-content", args=(self.product_type.id,)) + response = self.client.get(path, headers={"Accept-Language": "nl"}) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assertCountEqual( + response.data, + [ + { + "id": str(element1.id), + "taal": "nl", + "content": element1.content, + "labels": [], + }, + { + "id": str(element2.id), + "taal": "nl", + "content": element2.content, + "labels": [], + }, + ], + ) + + def test_en_content_and_fallback(self): + element1 = ContentElementFactory.create(product_type=self.product_type) + element1.set_current_language("en") + element1.content = "EN content" + element1.save() + + element2 = ContentElementFactory.create(product_type=self.product_type) + + path = reverse("producttype-content", args=(self.product_type.id,)) + response = self.client.get(path, headers={"Accept-Language": "en"}) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assertCountEqual( + response.data, + [ + { + "id": str(element1.id), + "taal": "en", + "content": "EN content", + "labels": [], + }, + { + "id": str(element2.id), + "taal": "nl", + "content": element2.content, + "labels": [], + }, + ], + ) diff --git a/src/open_producten/producttypen/tests/api/test_thema.py b/src/open_producten/producttypen/tests/api/test_thema.py index 5885df1..612cbdf 100644 --- a/src/open_producten/producttypen/tests/api/test_thema.py +++ b/src/open_producten/producttypen/tests/api/test_thema.py @@ -5,12 +5,8 @@ from rest_framework.exceptions import ErrorDetail from rest_framework.test import APIClient -from open_producten.producttypen.models import Thema, Vraag -from open_producten.producttypen.tests.factories import ( - ProductTypeFactory, - ThemaFactory, - VraagFactory, -) +from open_producten.producttypen.models import Thema +from open_producten.producttypen.tests.factories import ProductTypeFactory, ThemaFactory from open_producten.utils.tests.cases import BaseApiTestCase @@ -61,7 +57,6 @@ def test_create_minimal_thema(self): "id": str(thema.id), "naam": thema.naam, "beschrijving": thema.beschrijving, - "vragen": [], "gepubliceerd": False, "hoofd_thema": None, "product_typen": [], @@ -85,7 +80,6 @@ def test_create_thema_with_hoofd_thema(self): "id": str(thema.id), "naam": thema.naam, "beschrijving": thema.beschrijving, - "vragen": [], "gepubliceerd": False, "hoofd_thema": thema.hoofd_thema.id, "product_typen": [], @@ -377,23 +371,6 @@ def test_partial_update_published_hoofd_thema_to_unpublished_with_published_sub_ }, ) - def test_read_vraag(self): - thema = ThemaFactory.create() - vraag = VraagFactory.create(thema=thema) - - response = self.client.get(self.detail_path(thema)) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - expected_data = [ - { - "id": str(vraag.id), - "vraag": vraag.vraag, - "antwoord": vraag.antwoord, - } - ] - - self.assertEqual(response.data["vragen"], expected_data) - def test_read_product_type(self): thema = ThemaFactory.create() product_type = ProductTypeFactory.create() @@ -406,10 +383,7 @@ def test_read_product_type(self): expected_data = [ { "id": str(product_type.id), - "naam": product_type.naam, "code": product_type.code, - "samenvatting": product_type.samenvatting, - "beschrijving": product_type.beschrijving, "uniforme_product_naam": product_type.uniforme_product_naam.uri, "gepubliceerd": True, "toegestane_statussen": [], @@ -434,7 +408,6 @@ def test_read_themas(self): "id": str(thema1.id), "naam": thema1.naam, "beschrijving": thema1.beschrijving, - "vragen": [], "gepubliceerd": True, "hoofd_thema": None, "product_typen": [], @@ -445,7 +418,6 @@ def test_read_themas(self): "id": str(thema2.id), "naam": thema2.naam, "beschrijving": thema2.beschrijving, - "vragen": [], "gepubliceerd": True, "hoofd_thema": None, "product_typen": [], @@ -465,7 +437,6 @@ def test_read_thema(self): "id": str(thema.id), "naam": thema.naam, "beschrijving": thema.beschrijving, - "vragen": [], "gepubliceerd": True, "hoofd_thema": None, "product_typen": [], @@ -476,13 +447,11 @@ def test_read_thema(self): def test_delete_thema(self): thema = ThemaFactory.create() - VraagFactory.create(thema=thema) response = self.client.delete(self.detail_path(thema)) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(Thema.objects.count(), 0) - self.assertEqual(Vraag.objects.count(), 0) def test_delete_thema_when_linked_product_type_has_one_thema(self): thema = ThemaFactory.create() diff --git a/src/open_producten/producttypen/tests/api/test_vraag.py b/src/open_producten/producttypen/tests/api/test_vraag.py deleted file mode 100644 index 42a79fa..0000000 --- a/src/open_producten/producttypen/tests/api/test_vraag.py +++ /dev/null @@ -1,245 +0,0 @@ -from django.urls import reverse -from django.utils.translation import gettext as _ - -from rest_framework import status -from rest_framework.exceptions import ErrorDetail -from rest_framework.test import APIClient - -from open_producten.producttypen.models import ProductType, Vraag -from open_producten.producttypen.tests.factories import ( - ProductTypeFactory, - ThemaFactory, - VraagFactory, -) -from open_producten.utils.tests.cases import BaseApiTestCase - - -class TestProductTypeVraag(BaseApiTestCase): - - def setUp(self): - super().setUp() - self.product_type = ProductTypeFactory.create() - self.thema = ThemaFactory.create() - self.data = {"vraag": "18?", "antwoord": "in aanmerking"} - self.vraag = VraagFactory.create(product_type=self.product_type) - - self.path = reverse("vraag-list") - self.detail_path = reverse("vraag-detail", args=[self.vraag.id]) - - def test_read_vraag_without_credentials_returns_error(self): - response = APIClient().get(self.path) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - - def test_required_fields(self): - response = self.client.post(self.path, {}) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.data, - { - "vraag": [ - ErrorDetail(string=_("This field is required."), code="required") - ], - "antwoord": [ - ErrorDetail(string=_("This field is required."), code="required") - ], - }, - ) - - def test_create_vraag_with_product_type(self): - response = self.client.post( - self.path, self.data | {"product_type_id": self.product_type.id} - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(Vraag.objects.count(), 2) - response.data.pop("id") - self.assertEqual( - response.data, - self.data | {"product_type_id": self.product_type.id, "thema_id": None}, - ) - - def test_create_vraag_with_thema(self): - response = self.client.post(self.path, self.data | {"thema_id": self.thema.id}) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(Vraag.objects.count(), 2) - response.data.pop("id") - self.assertEqual( - response.data, - self.data | {"thema_id": self.thema.id, "product_type_id": None}, - ) - - def test_create_vraag_with_both_product_type_and_thema(self): - response = self.client.post( - self.path, - self.data - | { - "thema_id": self.thema.id, - "product_type_id": self.product_type.id, - }, - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.data, - { - "product_type_or_thema": [ - ErrorDetail( - string=_( - "Een vraag kan niet gelink zijn aan een thema en een product type." - ), - code="invalid", - ) - ] - }, - ) - - def test_create_vraag_without_product_type_or_thema(self): - response = self.client.post(self.path, self.data) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.data, - { - "product_type_or_thema": [ - ErrorDetail( - string=_( - "Een vraag moet gelinkt zijn aan een thema of een product type." - ), - code="invalid", - ) - ] - }, - ) - - def test_update_vraag(self): - data = self.data | {"vraag": "21?", "product_type_id": self.product_type.id} - response = self.client.put(self.detail_path, data) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(Vraag.objects.count(), 1) - self.assertEqual(ProductType.objects.first().vragen.first().vraag, "21?") - - def test_update_vraag_with_thema(self): - data = self.data | {"vraag": "21?", "thema_id": self.thema.id} - response = self.client.put(self.detail_path, data) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.data, - { - "product_type_or_thema": [ - ErrorDetail( - string=_( - "Een vraag kan niet gelink zijn aan een thema en een product type." - ), - code="invalid", - ) - ] - }, - ) - - def test_update_vraag_with_thema_and_product_type(self): - data = self.data | { - "vraag": "21?", - "thema_id": self.thema.id, - "product_type_id": self.product_type.id, - } - response = self.client.put(self.detail_path, data) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.data, - { - "product_type_or_thema": [ - ErrorDetail( - string=_( - "Een vraag kan niet gelink zijn aan een thema en een product type." - ), - code="invalid", - ) - ] - }, - ) - - def test_update_vraag_change_to_thema(self): - data = self.data | { - "thema_id": self.thema.id, - "product_type_id": None, - } - response = self.client.put(self.detail_path, data) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(Vraag.objects.count(), 1) - self.assertEqual(Vraag.objects.first().product_type, None) - self.assertEqual(Vraag.objects.first().thema, self.thema) - - def test_partial_update_vraag(self): - data = {"vraag": "21?"} - response = self.client.patch(self.detail_path, data) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(Vraag.objects.count(), 1) - self.assertEqual(ProductType.objects.first().vragen.first().vraag, "21?") - - def test_partial_update_vraag_with_thema(self): - data = {"vraag": "21?", "thema_id": self.thema.id} - response = self.client.patch(self.detail_path, data) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.data, - { - "product_type_or_thema": [ - ErrorDetail( - string=_( - "Een vraag kan niet gelink zijn aan een thema en een product type." - ), - code="invalid", - ) - ] - }, - ) - - def test_read_vragen(self): - vraag = VraagFactory.create(product_type=self.product_type) - response = self.client.get(self.path) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["count"], 2) - expected_data = [ - { - "id": str(self.vraag.id), - "vraag": self.vraag.vraag, - "antwoord": self.vraag.antwoord, - "product_type_id": self.product_type.id, - "thema_id": None, - }, - { - "id": str(vraag.id), - "vraag": vraag.vraag, - "antwoord": vraag.antwoord, - "product_type_id": self.product_type.id, - "thema_id": None, - }, - ] - self.assertCountEqual(response.data["results"], expected_data) - - def test_read_vraag(self): - response = self.client.get(self.detail_path) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - expected_data = { - "id": str(self.vraag.id), - "vraag": self.vraag.vraag, - "antwoord": self.vraag.antwoord, - "product_type_id": self.product_type.id, - "thema_id": None, - } - - self.assertEqual(response.data, expected_data) - - def test_delete_vraag(self): - response = self.client.delete(self.detail_path) - - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(Vraag.objects.count(), 0) diff --git a/src/open_producten/producttypen/tests/factories.py b/src/open_producten/producttypen/tests/factories.py index 02e1501..8bfa9da 100644 --- a/src/open_producten/producttypen/tests/factories.py +++ b/src/open_producten/producttypen/tests/factories.py @@ -1,16 +1,20 @@ import factory.fuzzy +from faker import Faker from ..models import ( Bestand, + ContentElement, + ContentLabel, Link, Prijs, PrijsOptie, ProductType, Thema, UniformeProductNaam, - Vraag, ) +fake = Faker() + class UniformeProductNaamFactory(factory.django.DjangoModelFactory): naam = factory.Sequence(lambda n: f"upn {n}") @@ -21,9 +25,6 @@ class Meta: class ProductTypeFactory(factory.django.DjangoModelFactory): - naam = factory.Sequence(lambda n: f"product type {n}") - samenvatting = factory.Faker("sentence") - beschrijving = factory.Faker("paragraph") code = factory.Sequence(lambda n: f"product type code {n}") gepubliceerd = True uniforme_product_naam = factory.SubFactory(UniformeProductNaamFactory) @@ -31,6 +32,18 @@ class ProductTypeFactory(factory.django.DjangoModelFactory): class Meta: model = ProductType + @factory.post_generation + def naam(self, create, extracted, **kwargs): + self.set_current_language("nl") + self.naam = extracted or fake.word() + self.save() + + @factory.post_generation + def samenvatting(self, create, extracted, **kwargs): + self.set_current_language("nl") + self.samenvatting = extracted or fake.sentence() + self.save() + class ThemaFactory(factory.django.DjangoModelFactory): naam = factory.Sequence(lambda n: f"thema {n}") @@ -41,14 +54,6 @@ class Meta: model = Thema -class VraagFactory(factory.django.DjangoModelFactory): - vraag = factory.Faker("sentence") - antwoord = factory.Faker("text") - - class Meta: - model = Vraag - - class PrijsFactory(factory.django.DjangoModelFactory): actief_vanaf = factory.Faker("date") product_type = factory.SubFactory(ProductTypeFactory) @@ -80,3 +85,23 @@ class LinkFactory(factory.django.DjangoModelFactory): class Meta: model = Link + + +class ContentElementFactory(factory.django.DjangoModelFactory): + product_type = factory.SubFactory(ProductTypeFactory) + + class Meta: + model = ContentElement + + @factory.post_generation + def content(self, create, extracted, **kwargs): + self.set_current_language("nl") + self.content = extracted or fake.word() + self.save() + + +class ContentLabelFactory(factory.django.DjangoModelFactory): + naam = factory.Sequence(lambda n: f"label {n}") + + class Meta: + model = ContentLabel diff --git a/src/open_producten/producttypen/tests/test_prijs.py b/src/open_producten/producttypen/tests/test_prijs.py index d079644..00d5d4b 100644 --- a/src/open_producten/producttypen/tests/test_prijs.py +++ b/src/open_producten/producttypen/tests/test_prijs.py @@ -28,9 +28,13 @@ def test_unique_validation(self): ) duplicate.full_clean() + @freeze_time("2024-01-02") def test_min_date_validation(self): + product_type = ProductTypeFactory.create() with self.assertRaises(ValidationError): - prijs = PrijsFactory.build(actief_vanaf=date(2020, 1, 1)) + prijs = PrijsFactory.build( + product_type=product_type, actief_vanaf=date(2020, 1, 1) + ) prijs.full_clean() @freeze_time("2024-01-02") diff --git a/src/open_producten/producttypen/tests/test_thema.py b/src/open_producten/producttypen/tests/test_thema.py index 5f91818..1492501 100644 --- a/src/open_producten/producttypen/tests/test_thema.py +++ b/src/open_producten/producttypen/tests/test_thema.py @@ -5,7 +5,7 @@ from .factories import ThemaFactory -class TestVraag(TestCase): +class TestThema(TestCase): def test_hoofd_thema_must_be_published_when_publishing_sub_thema(self): hoofd_thema = ThemaFactory.create(gepubliceerd=False) diff --git a/src/open_producten/producttypen/tests/test_vraag.py b/src/open_producten/producttypen/tests/test_vraag.py deleted file mode 100644 index f6129f7..0000000 --- a/src/open_producten/producttypen/tests/test_vraag.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.core.exceptions import ValidationError -from django.test import TestCase -from django.utils.translation import gettext as _ - -from .factories import ProductTypeFactory, ThemaFactory, VraagFactory - - -class TestVraag(TestCase): - - def setUp(self): - self.productType = ProductTypeFactory.create() - self.thema = ThemaFactory.create() - - def test_error_when_linked_to_product_type_and_thema(self): - vraag = VraagFactory.build(product_type=self.productType, thema=self.thema) - - with self.assertRaisesMessage( - ValidationError, - _("Een vraag kan niet gelink zijn aan een thema en een product type."), - ): - vraag.clean() - - def test_error_when_not_linked_to_product_type_or_thema(self): - vraag = VraagFactory.build() - - with self.assertRaisesMessage( - ValidationError, - _("Een vraag moet gelinkt zijn aan een thema of een product type."), - ): - vraag.clean() diff --git a/src/open_producten/producttypen/urls.py b/src/open_producten/producttypen/urls.py index fd43e82..8f166da 100644 --- a/src/open_producten/producttypen/urls.py +++ b/src/open_producten/producttypen/urls.py @@ -11,11 +11,12 @@ from open_producten.locaties.urls import LocatieRouter from open_producten.producttypen.views import ( BestandViewSet, + ContentElementViewSet, + ContentLabelViewSet, LinkViewSet, PrijsViewSet, ProductTypeViewSet, ThemaViewSet, - VraagViewSet, ) ProductTypenRouter = DefaultRouter() @@ -25,12 +26,16 @@ ProductTypenRouter.register("prijzen", PrijsViewSet, basename="prijs") -ProductTypenRouter.register("vragen", VraagViewSet, basename="vraag") - ProductTypenRouter.register("themas", ThemaViewSet, basename="thema") ProductTypenRouter.register("bestanden", BestandViewSet, basename="bestand") +ProductTypenRouter.register("content", ContentElementViewSet, basename="content") + +ProductTypenRouter.register( + "contentlabels", ContentLabelViewSet, basename="contentlabel" +) + description = """ Een Api voor Product typen. @@ -45,7 +50,6 @@ Daarnaast kunnen de volgende modellen per product worden aangemaakt: - prijzen - links -- vragen Een aantal velden verwachten een lijst/array van bijvoorbeeld product_type_ids. Let op dat als dit veld in een PATCH request wordt meegestuurd de gehele lijst/array zal worden overschreven. @@ -66,16 +70,20 @@ }, {"name": "prijzen", "description": "Opvragen en bewerken van PRIJZEN."}, {"name": "links", "description": "Opvragen en bewerken van LINKS."}, - { - "name": "vragen", - "description": "Opvragen en bewerken van VRAGEN. Een vraag kan gelinkt zijn aan een PRODUCTTYPE OF THEMA.", - }, {"name": "locaties", "description": "Opvragen en bewerken van LOCATIES."}, { "name": "organisaties", "description": "Opvragen en bewerken van ORGANISATIES.", }, {"name": "contacten", "description": "Opvragen en bewerken van CONTACTEN."}, + { + "name": "content", + "description": "Opvragen en bewerken van PRODUCTTYPE CONTENT.", + }, + { + "name": "contentlabels", + "description": "Opvragen van CONTENTLABELS.", + }, ], } diff --git a/src/open_producten/producttypen/views.py b/src/open_producten/producttypen/views.py index ba4cf19..4cf061a 100644 --- a/src/open_producten/producttypen/views.py +++ b/src/open_producten/producttypen/views.py @@ -2,19 +2,27 @@ from django.utils.translation import gettext_lazy as _ from django_filters.rest_framework import DjangoFilterBackend -from drf_spectacular.utils import OpenApiExample, extend_schema, extend_schema_view -from rest_framework import status +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import ( + OpenApiExample, + OpenApiParameter, + extend_schema, + extend_schema_view, +) +from rest_framework import mixins, status from rest_framework.decorators import action from rest_framework.parsers import MultiPartParser from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet from open_producten.producttypen.models import ( Bestand, + ContentElement, + ContentLabel, Link, Prijs, ProductType, Thema, - Vraag, ) from open_producten.producttypen.serializers import ( BestandSerializer, @@ -23,9 +31,17 @@ ProductTypeActuelePrijsSerializer, ProductTypeSerializer, ThemaSerializer, - VraagSerializer, ) -from open_producten.utils.views import OrderedModelViewSet +from open_producten.producttypen.serializers.content import ( + ContentElementSerializer, + ContentElementTranslationSerializer, + ContentLabelSerializer, + NestedContentElementSerializer, +) +from open_producten.producttypen.serializers.producttype import ( + ProductTypeTranslationSerializer, +) +from open_producten.utils.views import OrderedModelViewSet, TranslatableViewSetMixin @extend_schema_view( @@ -70,13 +86,50 @@ summary="Verwijder een PRODUCTTYPE.", ), ) -class ProductTypeViewSet(OrderedModelViewSet): +class ProductTypeViewSet(TranslatableViewSetMixin, OrderedModelViewSet): queryset = ProductType.objects.all() serializer_class = ProductTypeSerializer lookup_url_kwarg = "id" filter_backends = [DjangoFilterBackend] filterset_fields = ["gepubliceerd"] + @extend_schema( + summary="De vertaling van een producttype aanpassen.", + description="nl kan worden aangepast via het model.", + parameters=[ + OpenApiParameter( + name="taal", + required=True, + type=OpenApiTypes.STR, + location=OpenApiParameter.PATH, + ), + ], + ) + @action( + detail=True, + methods=["put", "patch"], + serializer_class=ProductTypeTranslationSerializer, + url_path="vertaling/(?P[^/.]+)", + ) + def vertaling(self, request, taal, **kwargs): + return super().update_vertaling(request, taal, **kwargs) + + @extend_schema( + summary="De vertaling van een producttype verwijderen.", + description="nl kan niet worden verwijderd.", + parameters=[ + OpenApiParameter( + name="taal", + required=True, + type=OpenApiTypes.STR, + location=OpenApiParameter.PATH, + ), + ], + ) + @vertaling.mapping.delete + def delete_vertaling(self, request, taal, **kwargs): + return super().delete_vertaling(request, taal, **kwargs) + @extend_schema( "actuele_prijzen", summary="Alle ACTUELE PRIJZEN opvragen.", @@ -107,6 +160,24 @@ def actuele_prijs(self, request, id=None): serializer = ProductTypeActuelePrijsSerializer(product_type) return Response(serializer.data) + @extend_schema( + "content", + summary="De CONTENT van een PRODUCTTYPE opvragen.", + description="Geeft de content van een PRODUCTTYPE terug.", + ) + @action( + detail=True, + serializer_class=NestedContentElementSerializer, + url_path="content", + ) + def content(self, request, id=None): + product_type = self.get_object() + + queryset = product_type.content_elementen + + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + @extend_schema_view( list=extend_schema( @@ -193,44 +264,6 @@ class PrijsViewSet(OrderedModelViewSet): filterset_fields = ["product_type_id"] -@extend_schema_view( - list=extend_schema( - summary="Alle VRAGEN opvragen.", - description="Deze lijst kan gefilterd wordt met query-string parameters.", - ), - retrieve=extend_schema( - summary="Een specifieke VRAAG opvragen.", - ), - create=extend_schema( - summary="Maak een VRAAG aan.", - examples=[ - OpenApiExample( - "Create vraag", - value={ - "product_type_id": "95792000-d57f-4d3a-b14c-c4c7aa964907", - "vraag": "Kom ik in aanmerking voor dit product?", - "antwoord": "Ja", - }, - request_only=True, - ) - ], - ), - update=extend_schema( - summary="Werk een VRAAG in zijn geheel bij.", - ), - partial_update=extend_schema(summary="Werk een VRAAG deels bij."), - destroy=extend_schema( - summary="Verwijder een VRAAG.", - ), -) -class VraagViewSet(OrderedModelViewSet): - queryset = Vraag.objects.all() - serializer_class = VraagSerializer - lookup_url_kwarg = "id" - filter_backends = [DjangoFilterBackend] - filterset_fields = ["product_type_id", "thema_id"] - - @extend_schema_view( list=extend_schema( summary="Alle BESTANDEN opvragen.", @@ -342,3 +375,94 @@ def destroy(self, request, *args, **kwargs): }, status=status.HTTP_400_BAD_REQUEST, ) + + +@extend_schema_view( + retrieve=extend_schema( + summary="Een specifiek CONTENTELEMENT opvragen.", + ), + create=extend_schema( + summary="Maak een CONTENTELEMENT aan.", + examples=[ + OpenApiExample( + "Create content", + value={ + "labels": ["openingstijden"], + "content": "ma-vr 8:00-17:00", + "product_type_id": "5f6a2219-5768-4e11-8a8e-ffbafff32482", + }, + request_only=True, + ) + ], + ), + update=extend_schema( + summary="Werk een CONTENTELEMENT in zijn geheel bij.", + ), + partial_update=extend_schema( + summary="Werk een CONTENTELEMENT deels bij.", + description="Als product_type_ids in een patch request wordt meegegeven wordt deze lijst geheel overschreven.", + ), + destroy=extend_schema( + summary="Verwijder een CONTENTELEMENT.", + ), +) +class ContentElementViewSet( + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + TranslatableViewSetMixin, + GenericViewSet, +): + queryset = ContentElement.objects.all() + serializer_class = ContentElementSerializer + lookup_url_kwarg = "id" + + @extend_schema( + summary="De vertaling van een content element aanpassen.", + description="nl kan worden aangepast via het model.", + parameters=[ + OpenApiParameter( + name="taal", + required=True, + type=OpenApiTypes.STR, + location=OpenApiParameter.PATH, + ), + ], + ) + @action( + detail=True, + methods=["put"], + serializer_class=ContentElementTranslationSerializer, + url_path="vertaling/(?P[^/.]+)", + ) + def vertaling(self, request, taal, **kwargs): + return super().update_vertaling(request, taal, **kwargs) + + @extend_schema( + summary="De vertaling van een content element verwijderen.", + description="nl kan niet worden verwijderd.", + parameters=[ + OpenApiParameter( + name="taal", + required=True, + type=OpenApiTypes.STR, + location=OpenApiParameter.PATH, + ), + ], + ) + @vertaling.mapping.delete + def delete_vertaling(self, request, taal, **kwargs): + return super().delete_vertaling(request, taal, **kwargs) + + +@extend_schema_view( + list=extend_schema( + summary="Alle CONTENTELEMENTLABELS opvragen.", + description="Deze lijst kan gefilterd wordt met query-string parameters.", + ), +) +class ContentLabelViewSet(mixins.ListModelMixin, GenericViewSet): + queryset = ContentLabel.objects.all() + serializer_class = ContentLabelSerializer + lookup_field = "id" diff --git a/src/open_producten/static/css/wysimark-tweaks.css b/src/open_producten/static/css/wysimark-tweaks.css new file mode 100644 index 0000000..c513108 --- /dev/null +++ b/src/open_producten/static/css/wysimark-tweaks.css @@ -0,0 +1,32 @@ +#wysimark-container > div > div:first-child { + background-color: var(--darkened-bg); + border-radius: 0.5em; +} + +#wysimark-container > div > div:first-child [data-item-type="button"] { + color: var(--body-fg); +} + +[data-slate-editor=true] { + color: var(--body-fg); + background: var(--body-bg); + border-radius: 0.5em; +} + +#wysimark-container { + width: 50em; +} + +h2[data-slate-node] { + padding: initial; + font-size: 1.5em; + text-align: initial; + margin-top: 1em; + font-weight: bold; + color: var(--body-fg); + background: var(--body-bg); + + &:first-child { + margin-top: 0; + } +} diff --git a/src/open_producten/static/wysimark/textarea-wysimark.js b/src/open_producten/static/wysimark/textarea-wysimark.js new file mode 100644 index 0000000..de02806 --- /dev/null +++ b/src/open_producten/static/wysimark/textarea-wysimark.js @@ -0,0 +1,19 @@ +document.addEventListener("DOMContentLoaded", function () { + const textareas = document.getElementsByClassName("wysimark-textarea"); + + for (const textarea of textareas) { + const container = document.createElement('div'); + container.id = "wysimark-container"; + + textarea.style.display = 'none'; + textarea.parentNode.insertBefore(container, textarea.nextSibling); + + createWysimark(container, { + initialMarkdown: textarea.value, + onChange: function (markdown) { + textarea.value = markdown; + }, + height: "30em", + }); + } +}); diff --git a/src/open_producten/templates/admin/parler/language_tabs.html b/src/open_producten/templates/admin/parler/language_tabs.html new file mode 100644 index 0000000..53f06dc --- /dev/null +++ b/src/open_producten/templates/admin/parler/language_tabs.html @@ -0,0 +1,12 @@ +{% load i18n %} +{% if language_tabs %} +
+ {% for url,name,code,status in language_tabs %} + {% if status == 'current' %} + + {{ name }} + {% else %} + {{ name }} + {% endif %} + {% endfor %} +
{% endif %} diff --git a/src/open_producten/utils/checks.py b/src/open_producten/utils/checks.py index fa5341c..1ff8fe3 100644 --- a/src/open_producten/utils/checks.py +++ b/src/open_producten/utils/checks.py @@ -5,6 +5,7 @@ from django.forms import ModelForm from django_celery_beat.admin import PeriodicTaskForm +from parler.forms import TranslatableModelForm def get_subclasses(cls): @@ -32,6 +33,11 @@ def check_modelform_exclude(app_configs, **kwargs): # PeriodicTaskForm.Meta from django_celery_beat has no fields attribute. if form is PeriodicTaskForm: continue + + # TranslatableModelForm.Meta from parler has no fields attribute. + if form is TranslatableModelForm: + continue + # ok, fields is defined if form._meta.fields or getattr(form.Meta, "fields", None): continue diff --git a/src/open_producten/utils/views.py b/src/open_producten/utils/views.py index 0a96d78..94a6090 100644 --- a/src/open_producten/utils/views.py +++ b/src/open_producten/utils/views.py @@ -1,9 +1,14 @@ from django import http +from django.conf import settings from django.template import TemplateDoesNotExist, loader +from django.utils.translation import gettext_lazy as _ from django.views.decorators.csrf import requires_csrf_token from django.views.defaults import ERROR_500_TEMPLATE_NAME from django.views.generic import TemplateView +from rest_framework import status +from rest_framework.exceptions import NotFound, ParseError +from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet @@ -42,3 +47,48 @@ def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({"component": self.component}) return context + + +class TranslatableViewSetMixin: + + _supported_languages = { + language["code"] + for site in settings.PARLER_LANGUAGES + if isinstance(site, int) + for language in settings.PARLER_LANGUAGES[site] + } + + def update_vertaling(self, request, taal, **kwargs): + partial = request.method == "PATCH" + + instance = self.get_object() + + taal = taal.lower() + + if taal == "nl": + raise ParseError(_("nl vertaling kan worden aangepast via het model zelf.")) + + if taal not in self._supported_languages: + raise ParseError(_("{} vertaling wordt niet ondersteunt.").format(taal)) + + if partial and not request.data: + raise ParseError(_("patch request mag niet leeg zijn.")) + + instance.set_current_language(taal) + serializer = self.get_serializer(instance, data=request.data, partial=partial) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + + return Response(serializer.data) + + def delete_vertaling(self, request, taal, **kwargs): + instance = self.get_object() + + if taal.lower() == "nl": + raise ParseError(_("nl vertaling kan worden aangepast via het model zelf.")) + + if not instance.has_translation(taal): + raise NotFound(_("{} vertaling bestaat niet.").format(taal)) + + instance.delete_translation(taal) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/src/open_producten/utils/widgets.py b/src/open_producten/utils/widgets.py new file mode 100644 index 0000000..8a0795c --- /dev/null +++ b/src/open_producten/utils/widgets.py @@ -0,0 +1,14 @@ +from django.forms import Textarea + + +class WysimarkWidget(Textarea): + + def __init__(self, attrs=None): + default_attrs = {"class": "wysimark-textarea"} + if attrs: + default_attrs.update(attrs) + super().__init__(attrs=default_attrs) + + class Media: + js = ("wysimark/wysimark.js", "wysimark/textarea-wysimark.js") + css = {"all": ("css/wysimark-tweaks.css",)} diff --git a/src/producten-openapi.yaml b/src/producten-openapi.yaml index d4e9612..2917fd5 100644 --- a/src/producten-openapi.yaml +++ b/src/producten-openapi.yaml @@ -413,22 +413,10 @@ components: type: string format: uuid readOnly: true - naam: - type: string - title: Product type naam - description: naam van het product type. - maxLength: 100 code: type: string description: code van het product type. - maxLength: 100 - samenvatting: - type: string - description: Korte beschrijving van het product type, maximaal 300 karakters. - maxLength: 300 - beschrijving: - type: string - description: Product type beschrijving, ondersteund markdown format. + maxLength: 255 keywords: type: array items: @@ -459,35 +447,18 @@ components: description: De datum waarop het object voor het laatst is gewijzigd. required: - aanmaak_datum - - beschrijving - code - id - - naam - uniforme_product_naam - update_datum NestedProductTypeRequest: type: object properties: - naam: - type: string - minLength: 1 - title: Product type naam - description: naam van het product type. - maxLength: 100 code: type: string minLength: 1 description: code van het product type. - maxLength: 100 - samenvatting: - type: string - minLength: 1 - description: Korte beschrijving van het product type, maximaal 300 karakters. - maxLength: 300 - beschrijving: - type: string - minLength: 1 - description: Product type beschrijving, ondersteund markdown format. + maxLength: 255 keywords: type: array items: @@ -508,9 +479,7 @@ components: type: boolean description: Geeft aan of het object getoond kan worden. required: - - beschrijving - code - - naam - uniforme_product_naam PaginatedProductList: type: object diff --git a/src/producttypen-openapi.yaml b/src/producttypen-openapi.yaml index 53579c7..7def9d8 100644 --- a/src/producttypen-openapi.yaml +++ b/src/producttypen-openapi.yaml @@ -589,6 +589,397 @@ paths: schema: $ref: '#/components/schemas/DetailError' description: '' + /content/: + post: + operationId: content_create + summary: Maak een CONTENTELEMENT aan. + tags: + - content + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ContentElementRequest' + examples: + CreateContent: + value: + labels: + - openingstijden + content: ma-vr 8:00-17:00 + product_type_id: 5f6a2219-5768-4e11-8a8e-ffbafff32482 + summary: Create content + required: true + security: + - tokenAuth: [] + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/ContentElement' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + BadRequestExample: + value: + veld_a: + - Dit veld is vereist. + veld_b: + - ‘’ is geen geldige UUID. + summary: Bad request example + description: Errors worden per veld teruggegeven. Hieronder volgt + een voorbeeld. + description: Validation error + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/DetailError' + description: '' + /content/{id}/: + get: + operationId: content_retrieve + summary: Een specifiek CONTENTELEMENT opvragen. + parameters: + - in: path + name: id + schema: + type: string + format: uuid + description: A UUID string identifying this content element. + required: true + tags: + - content + security: + - tokenAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ContentElement' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + BadRequestExample: + value: + veld_a: + - Dit veld is vereist. + veld_b: + - ‘’ is geen geldige UUID. + summary: Bad request example + description: Errors worden per veld teruggegeven. Hieronder volgt + een voorbeeld. + description: Validation error + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/DetailError' + description: '' + put: + operationId: content_update + summary: Werk een CONTENTELEMENT in zijn geheel bij. + parameters: + - in: path + name: id + schema: + type: string + format: uuid + description: A UUID string identifying this content element. + required: true + tags: + - content + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ContentElementRequest' + required: true + security: + - tokenAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ContentElement' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + BadRequestExample: + value: + veld_a: + - Dit veld is vereist. + veld_b: + - ‘’ is geen geldige UUID. + summary: Bad request example + description: Errors worden per veld teruggegeven. Hieronder volgt + een voorbeeld. + description: Validation error + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/DetailError' + description: '' + patch: + operationId: content_partial_update + description: Als product_type_ids in een patch request wordt meegegeven wordt + deze lijst geheel overschreven. + summary: Werk een CONTENTELEMENT deels bij. + parameters: + - in: path + name: id + schema: + type: string + format: uuid + description: A UUID string identifying this content element. + required: true + tags: + - content + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedContentElementRequest' + security: + - tokenAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ContentElement' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + BadRequestExample: + value: + veld_a: + - Dit veld is vereist. + veld_b: + - ‘’ is geen geldige UUID. + summary: Bad request example + description: Errors worden per veld teruggegeven. Hieronder volgt + een voorbeeld. + description: Validation error + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/DetailError' + description: '' + delete: + operationId: content_destroy + summary: Verwijder een CONTENTELEMENT. + parameters: + - in: path + name: id + schema: + type: string + format: uuid + description: A UUID string identifying this content element. + required: true + tags: + - content + security: + - tokenAuth: [] + responses: + '204': + description: No response body + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + BadRequestExample: + value: + veld_a: + - Dit veld is vereist. + veld_b: + - ‘’ is geen geldige UUID. + summary: Bad request example + description: Errors worden per veld teruggegeven. Hieronder volgt + een voorbeeld. + description: Validation error + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/DetailError' + description: '' + /content/{id}/vertaling/{taal}/: + put: + operationId: content_vertaling_update + description: nl kan worden aangepast via het model. + summary: De vertaling van een content element aanpassen. + parameters: + - in: path + name: id + schema: + type: string + format: uuid + description: A UUID string identifying this content element. + required: true + - in: path + name: taal + schema: + type: string + required: true + tags: + - content + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ContentElementTranslationRequest' + required: true + security: + - tokenAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ContentElementTranslation' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + BadRequestExample: + value: + veld_a: + - Dit veld is vereist. + veld_b: + - ‘’ is geen geldige UUID. + summary: Bad request example + description: Errors worden per veld teruggegeven. Hieronder volgt + een voorbeeld. + description: Validation error + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/DetailError' + description: '' + delete: + operationId: content_vertaling_destroy + description: nl kan niet worden verwijderd. + summary: De vertaling van een content element verwijderen. + parameters: + - in: path + name: id + schema: + type: string + format: uuid + description: A UUID string identifying this content element. + required: true + - in: path + name: taal + schema: + type: string + required: true + tags: + - content + security: + - tokenAuth: [] + responses: + '204': + description: No response body + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + BadRequestExample: + value: + veld_a: + - Dit veld is vereist. + veld_b: + - ‘’ is geen geldige UUID. + summary: Bad request example + description: Errors worden per veld teruggegeven. Hieronder volgt + een voorbeeld. + description: Validation error + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/DetailError' + description: '' + /contentlabels/: + get: + operationId: contentlabels_list + description: Deze lijst kan gefilterd wordt met query-string parameters. + summary: Alle CONTENTELEMENTLABELS opvragen. + parameters: + - name: page + required: false + in: query + description: Een pagina binnen de gepagineerde set resultaten. + schema: + type: integer + - name: page_size + required: false + in: query + description: Het aantal resultaten terug te geven per pagina. + schema: + type: integer + tags: + - contentlabels + security: + - tokenAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedContentLabelList' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + BadRequestExample: + value: + veld_a: + - Dit veld is vereist. + veld_b: + - ‘’ is geen geldige UUID. + summary: Bad request example + description: Errors worden per veld teruggegeven. Hieronder volgt + een voorbeeld. + description: Validation error + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/DetailError' + description: '' /links/: get: operationId: links_list @@ -2070,164 +2461,19 @@ paths: name: id schema: type: string - format: uuid - description: A UUID string identifying this Product type. - required: true - tags: - - producttypen - security: - - tokenAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/ProductTypeActuelePrijs' - description: '' - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - examples: - BadRequestExample: - value: - veld_a: - - Dit veld is vereist. - veld_b: - - ‘’ is geen geldige UUID. - summary: Bad request example - description: Errors worden per veld teruggegeven. Hieronder volgt - een voorbeeld. - description: Validation error - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/DetailError' - description: '' - /producttypen/actuele-prijzen/: - get: - operationId: actuele_prijzen - description: Geeft de huidige prijzen van alle PRODUCTTYPEN terug. - summary: Alle ACTUELE PRIJZEN opvragen. - tags: - - producttypen - security: - - tokenAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/ProductTypeActuelePrijs' - description: '' - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - examples: - BadRequestExample: - value: - veld_a: - - Dit veld is vereist. - veld_b: - - ‘’ is geen geldige UUID. - summary: Bad request example - description: Errors worden per veld teruggegeven. Hieronder volgt - een voorbeeld. - description: Validation error - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/DetailError' - description: '' - /themas/: - get: - operationId: themas_list - description: Deze lijst kan gefilterd wordt met query-string parameters. - summary: Alle THEMA'S opvragen. - parameters: - - in: query - name: gepubliceerd - schema: - type: boolean - - name: page - required: false - in: query - description: Een pagina binnen de gepagineerde set resultaten. - schema: - type: integer - - name: page_size - required: false - in: query - description: Het aantal resultaten terug te geven per pagina. - schema: - type: integer - tags: - - themas - security: - - tokenAuth: [] - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/PaginatedThemaList' - description: '' - '400': - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - examples: - BadRequestExample: - value: - veld_a: - - Dit veld is vereist. - veld_b: - - ‘’ is geen geldige UUID. - summary: Bad request example - description: Errors worden per veld teruggegeven. Hieronder volgt - een voorbeeld. - description: Validation error - '403': - content: - application/json: - schema: - $ref: '#/components/schemas/DetailError' - description: '' - post: - operationId: themas_create - summary: Maak een THEMA aan. - tags: - - themas - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/ThemaRequest' - examples: - CreateThema: - value: - hoofd_thema: 5f6a2219-5768-4e11-8a8e-ffbafff32482 - product_type_ids: - - 95792000-d57f-4d3a-b14c-c4c7aa964907 - gepubliceerd: true - naam: Parkeren - beschrijving: Parkeren in gemeente ABC - summary: Create thema + format: uuid + description: A UUID string identifying this Product type. required: true + tags: + - producttypen security: - tokenAuth: [] responses: - '201': + '200': content: application/json: schema: - $ref: '#/components/schemas/Thema' + $ref: '#/components/schemas/ProductTypeActuelePrijs' description: '' '400': content: @@ -2251,20 +2497,21 @@ paths: schema: $ref: '#/components/schemas/DetailError' description: '' - /themas/{id}/: + /producttypen/{id}/content/: get: - operationId: themas_retrieve - summary: Een specifiek THEMA opvragen. + operationId: content + description: Geeft de content van een PRODUCTTYPE terug. + summary: De CONTENT van een PRODUCTTYPE opvragen. parameters: - in: path name: id schema: type: string format: uuid - description: A UUID string identifying this thema. + description: A UUID string identifying this Product type. required: true tags: - - themas + - producttypen security: - tokenAuth: [] responses: @@ -2272,7 +2519,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Thema' + $ref: '#/components/schemas/NestedContentElement' description: '' '400': content: @@ -2296,24 +2543,31 @@ paths: schema: $ref: '#/components/schemas/DetailError' description: '' + /producttypen/{id}/vertaling/{taal}/: put: - operationId: themas_update - summary: Werk een THEMA in zijn geheel bij. + operationId: producttypen_vertaling_update + description: nl kan worden aangepast via het model. + summary: De vertaling van een producttype aanpassen. parameters: - in: path name: id schema: type: string format: uuid - description: A UUID string identifying this thema. + description: A UUID string identifying this Product type. + required: true + - in: path + name: taal + schema: + type: string required: true tags: - - themas + - producttypen requestBody: content: application/json: schema: - $ref: '#/components/schemas/ThemaRequest' + $ref: '#/components/schemas/ProductTypeTranslationRequest' required: true security: - tokenAuth: [] @@ -2322,7 +2576,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Thema' + $ref: '#/components/schemas/ProductTypeTranslation' description: '' '400': content: @@ -2347,25 +2601,29 @@ paths: $ref: '#/components/schemas/DetailError' description: '' patch: - operationId: themas_partial_update - description: Als product_type_ids in een patch request wordt meegegeven wordt - deze lijst geheel overschreven. - summary: Werk een THEMA deels bij. + operationId: producttypen_vertaling_partial_update + description: nl kan worden aangepast via het model. + summary: De vertaling van een producttype aanpassen. parameters: - in: path name: id schema: type: string format: uuid - description: A UUID string identifying this thema. + description: A UUID string identifying this Product type. + required: true + - in: path + name: taal + schema: + type: string required: true tags: - - themas + - producttypen requestBody: content: application/json: schema: - $ref: '#/components/schemas/PatchedThemaRequest' + $ref: '#/components/schemas/PatchedProductTypeTranslationRequest' security: - tokenAuth: [] responses: @@ -2373,7 +2631,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Thema' + $ref: '#/components/schemas/ProductTypeTranslation' description: '' '400': content: @@ -2398,18 +2656,24 @@ paths: $ref: '#/components/schemas/DetailError' description: '' delete: - operationId: themas_destroy - summary: Verwijder een THEMA. + operationId: producttypen_vertaling_destroy + description: nl kan niet worden verwijderd. + summary: De vertaling van een producttype verwijderen. parameters: - in: path name: id schema: type: string format: uuid - description: A UUID string identifying this thema. + description: A UUID string identifying this Product type. + required: true + - in: path + name: taal + schema: + type: string required: true tags: - - themas + - producttypen security: - tokenAuth: [] responses: @@ -2437,12 +2701,54 @@ paths: schema: $ref: '#/components/schemas/DetailError' description: '' - /vragen/: + /producttypen/actuele-prijzen/: + get: + operationId: actuele_prijzen + description: Geeft de huidige prijzen van alle PRODUCTTYPEN terug. + summary: Alle ACTUELE PRIJZEN opvragen. + tags: + - producttypen + security: + - tokenAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ProductTypeActuelePrijs' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + BadRequestExample: + value: + veld_a: + - Dit veld is vereist. + veld_b: + - ‘’ is geen geldige UUID. + summary: Bad request example + description: Errors worden per veld teruggegeven. Hieronder volgt + een voorbeeld. + description: Validation error + '403': + content: + application/json: + schema: + $ref: '#/components/schemas/DetailError' + description: '' + /themas/: get: - operationId: vragen_list + operationId: themas_list description: Deze lijst kan gefilterd wordt met query-string parameters. - summary: Alle VRAGEN opvragen. + summary: Alle THEMA'S opvragen. parameters: + - in: query + name: gepubliceerd + schema: + type: boolean - name: page required: false in: query @@ -2455,22 +2761,8 @@ paths: description: Het aantal resultaten terug te geven per pagina. schema: type: integer - - in: query - name: product_type_id - schema: - type: string - format: uuid - nullable: true - description: Het product type waarbij deze vraag hoort. - - in: query - name: thema_id - schema: - type: string - format: uuid - nullable: true - description: Het thema waarbij deze vraag hoort. tags: - - vragen + - themas security: - tokenAuth: [] responses: @@ -2478,7 +2770,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PaginatedVraagList' + $ref: '#/components/schemas/PaginatedThemaList' description: '' '400': content: @@ -2503,22 +2795,25 @@ paths: $ref: '#/components/schemas/DetailError' description: '' post: - operationId: vragen_create - summary: Maak een VRAAG aan. + operationId: themas_create + summary: Maak een THEMA aan. tags: - - vragen + - themas requestBody: content: application/json: schema: - $ref: '#/components/schemas/VraagRequest' + $ref: '#/components/schemas/ThemaRequest' examples: - CreateVraag: + CreateThema: value: - product_type_id: 95792000-d57f-4d3a-b14c-c4c7aa964907 - vraag: Kom ik in aanmerking voor dit product? - antwoord: Ja - summary: Create vraag + hoofd_thema: 5f6a2219-5768-4e11-8a8e-ffbafff32482 + product_type_ids: + - 95792000-d57f-4d3a-b14c-c4c7aa964907 + gepubliceerd: true + naam: Parkeren + beschrijving: Parkeren in gemeente ABC + summary: Create thema required: true security: - tokenAuth: [] @@ -2527,7 +2822,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Vraag' + $ref: '#/components/schemas/Thema' description: '' '400': content: @@ -2551,20 +2846,20 @@ paths: schema: $ref: '#/components/schemas/DetailError' description: '' - /vragen/{id}/: + /themas/{id}/: get: - operationId: vragen_retrieve - summary: Een specifieke VRAAG opvragen. + operationId: themas_retrieve + summary: Een specifiek THEMA opvragen. parameters: - in: path name: id schema: type: string format: uuid - description: A UUID string identifying this Vraag. + description: A UUID string identifying this thema. required: true tags: - - vragen + - themas security: - tokenAuth: [] responses: @@ -2572,7 +2867,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Vraag' + $ref: '#/components/schemas/Thema' description: '' '400': content: @@ -2597,23 +2892,23 @@ paths: $ref: '#/components/schemas/DetailError' description: '' put: - operationId: vragen_update - summary: Werk een VRAAG in zijn geheel bij. + operationId: themas_update + summary: Werk een THEMA in zijn geheel bij. parameters: - in: path name: id schema: type: string format: uuid - description: A UUID string identifying this Vraag. + description: A UUID string identifying this thema. required: true tags: - - vragen + - themas requestBody: content: application/json: schema: - $ref: '#/components/schemas/VraagRequest' + $ref: '#/components/schemas/ThemaRequest' required: true security: - tokenAuth: [] @@ -2622,7 +2917,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Vraag' + $ref: '#/components/schemas/Thema' description: '' '400': content: @@ -2647,23 +2942,25 @@ paths: $ref: '#/components/schemas/DetailError' description: '' patch: - operationId: vragen_partial_update - summary: Werk een VRAAG deels bij. + operationId: themas_partial_update + description: Als product_type_ids in een patch request wordt meegegeven wordt + deze lijst geheel overschreven. + summary: Werk een THEMA deels bij. parameters: - in: path name: id schema: type: string format: uuid - description: A UUID string identifying this Vraag. + description: A UUID string identifying this thema. required: true tags: - - vragen + - themas requestBody: content: application/json: schema: - $ref: '#/components/schemas/PatchedVraagRequest' + $ref: '#/components/schemas/PatchedThemaRequest' security: - tokenAuth: [] responses: @@ -2671,7 +2968,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Vraag' + $ref: '#/components/schemas/Thema' description: '' '400': content: @@ -2696,18 +2993,18 @@ paths: $ref: '#/components/schemas/DetailError' description: '' delete: - operationId: vragen_destroy - summary: Verwijder een VRAAG. + operationId: themas_destroy + summary: Verwijder een THEMA. parameters: - in: path name: id schema: type: string format: uuid - description: A UUID string identifying this Vraag. + description: A UUID string identifying this thema. required: true tags: - - vragen + - themas security: - tokenAuth: [] responses: @@ -2838,6 +3135,84 @@ components: required: - achternaam - voornaam + ContentElement: + type: object + description: Serializer that saves :class:`TranslatedFieldsField` automatically. + properties: + id: + type: string + format: uuid + readOnly: true + content: + type: string + description: De content van dit content element. + maxLength: 255 + labels: + type: array + items: + type: string + product_type_id: + type: string + format: uuid + taal: + type: string + readOnly: true + description: De huidige taal van het content element. + required: + - content + - id + - product_type_id + - taal + ContentElementRequest: + type: object + description: Serializer that saves :class:`TranslatedFieldsField` automatically. + properties: + content: + type: string + minLength: 1 + description: De content van dit content element. + maxLength: 255 + labels: + type: array + items: + type: string + minLength: 1 + product_type_id: + type: string + format: uuid + required: + - content + - product_type_id + ContentElementTranslation: + type: object + properties: + id: + type: string + format: uuid + readOnly: true + content: + type: string + description: De content van dit content element. + required: + - content + - id + ContentElementTranslationRequest: + type: object + properties: + content: + type: string + minLength: 1 + description: De content van dit content element. + required: + - content + ContentLabel: + type: object + properties: + naam: + type: string + maxLength: 255 + required: + - naam DetailError: type: object properties: @@ -2869,7 +3244,7 @@ components: naam: type: string description: Naam van de link. - maxLength: 100 + maxLength: 255 url: type: string format: uri @@ -2890,7 +3265,7 @@ components: type: string minLength: 1 description: Naam van de link. - maxLength: 100 + maxLength: 255 url: type: string format: uri @@ -2913,7 +3288,7 @@ components: readOnly: true naam: type: string - maxLength: 100 + maxLength: 255 email: type: string format: email @@ -2927,11 +3302,11 @@ components: straat: type: string nullable: true - maxLength: 250 + maxLength: 255 huisnummer: type: string nullable: true - maxLength: 250 + maxLength: 10 postcode: type: string nullable: true @@ -2940,7 +3315,7 @@ components: stad: type: string nullable: true - maxLength: 250 + maxLength: 255 required: - id - naam @@ -2950,7 +3325,7 @@ components: naam: type: string minLength: 1 - maxLength: 100 + maxLength: 255 email: type: string format: email @@ -2964,11 +3339,11 @@ components: straat: type: string nullable: true - maxLength: 250 + maxLength: 255 huisnummer: type: string nullable: true - maxLength: 250 + maxLength: 10 postcode: type: string nullable: true @@ -2977,7 +3352,7 @@ components: stad: type: string nullable: true - maxLength: 250 + maxLength: 255 required: - naam NestedBestand: @@ -3001,6 +3376,30 @@ components: format: binary required: - bestand + NestedContentElement: + type: object + description: Serializer that saves :class:`TranslatedFieldsField` automatically. + properties: + id: + type: string + format: uuid + readOnly: true + taal: + type: string + readOnly: true + description: De huidige taal van het content element. + content: + type: string + description: De content van dit content element. + maxLength: 255 + labels: + type: array + items: + type: string + required: + - content + - id + - taal NestedLink: type: object properties: @@ -3011,7 +3410,7 @@ components: naam: type: string description: Naam van de link. - maxLength: 100 + maxLength: 255 url: type: string format: uri @@ -3028,7 +3427,7 @@ components: type: string minLength: 1 description: Naam van de link. - maxLength: 100 + maxLength: 255 url: type: string format: uri @@ -3080,22 +3479,10 @@ components: type: string format: uuid readOnly: true - naam: - type: string - title: Product type naam - description: naam van het product type. - maxLength: 100 code: type: string description: code van het product type. - maxLength: 100 - samenvatting: - type: string - description: Korte beschrijving van het product type, maximaal 300 karakters. - maxLength: 300 - beschrijving: - type: string - description: Product type beschrijving, ondersteund markdown format. + maxLength: 255 keywords: type: array items: @@ -3126,35 +3513,18 @@ components: description: De datum waarop het object voor het laatst is gewijzigd. required: - aanmaak_datum - - beschrijving - code - id - - naam - uniforme_product_naam - update_datum NestedProductTypeRequest: - type: object - properties: - naam: - type: string - minLength: 1 - title: Product type naam - description: naam van het product type. - maxLength: 100 + type: object + properties: code: type: string minLength: 1 description: code van het product type. - maxLength: 100 - samenvatting: - type: string - minLength: 1 - description: Korte beschrijving van het product type, maximaal 300 karakters. - maxLength: 300 - beschrijving: - type: string - minLength: 1 - description: Product type beschrijving, ondersteund markdown format. + maxLength: 255 keywords: type: array items: @@ -3175,9 +3545,7 @@ components: type: boolean description: Geeft aan of het object getoond kan worden. required: - - beschrijving - code - - naam - uniforme_product_naam NestedThema: type: object @@ -3189,7 +3557,7 @@ components: naam: type: string description: Naam van het thema. - maxLength: 100 + maxLength: 255 beschrijving: type: string description: Beschrijving van het thema, ondersteund markdown format. @@ -3223,7 +3591,7 @@ components: type: string minLength: 1 description: Naam van het thema. - maxLength: 100 + maxLength: 255 beschrijving: type: string description: Beschrijving van het thema, ondersteund markdown format. @@ -3237,39 +3605,6 @@ components: description: Het hoofd thema van het thema. required: - naam - NestedVraag: - type: object - properties: - id: - type: string - format: uuid - readOnly: true - vraag: - type: string - description: De vraag die wordt beantwoord. - maxLength: 250 - antwoord: - type: string - description: Het antwoord op de vraag, ondersteund markdown format. - required: - - antwoord - - id - - vraag - NestedVraagRequest: - type: object - properties: - vraag: - type: string - minLength: 1 - description: De vraag die wordt beantwoord. - maxLength: 250 - antwoord: - type: string - minLength: 1 - description: Het antwoord op de vraag, ondersteund markdown format. - required: - - antwoord - - vraag Organisatie: type: object properties: @@ -3279,7 +3614,7 @@ components: readOnly: true naam: type: string - maxLength: 100 + maxLength: 255 email: type: string format: email @@ -3293,11 +3628,11 @@ components: straat: type: string nullable: true - maxLength: 250 + maxLength: 255 huisnummer: type: string nullable: true - maxLength: 250 + maxLength: 10 postcode: type: string nullable: true @@ -3306,11 +3641,11 @@ components: stad: type: string nullable: true - maxLength: 250 + maxLength: 255 code: type: string description: code van de organisatie. - maxLength: 100 + maxLength: 255 required: - code - id @@ -3321,7 +3656,7 @@ components: naam: type: string minLength: 1 - maxLength: 100 + maxLength: 255 email: type: string format: email @@ -3335,11 +3670,11 @@ components: straat: type: string nullable: true - maxLength: 250 + maxLength: 255 huisnummer: type: string nullable: true - maxLength: 250 + maxLength: 10 postcode: type: string nullable: true @@ -3348,12 +3683,12 @@ components: stad: type: string nullable: true - maxLength: 250 + maxLength: 255 code: type: string minLength: 1 description: code van de organisatie. - maxLength: 100 + maxLength: 255 required: - code - naam @@ -3403,7 +3738,7 @@ components: type: array items: $ref: '#/components/schemas/Contact' - PaginatedLinkList: + PaginatedContentLabelList: type: object required: - count @@ -3425,8 +3760,8 @@ components: results: type: array items: - $ref: '#/components/schemas/Link' - PaginatedLocatieList: + $ref: '#/components/schemas/ContentLabel' + PaginatedLinkList: type: object required: - count @@ -3448,8 +3783,8 @@ components: results: type: array items: - $ref: '#/components/schemas/Locatie' - PaginatedOrganisatieList: + $ref: '#/components/schemas/Link' + PaginatedLocatieList: type: object required: - count @@ -3471,8 +3806,8 @@ components: results: type: array items: - $ref: '#/components/schemas/Organisatie' - PaginatedPrijsList: + $ref: '#/components/schemas/Locatie' + PaginatedOrganisatieList: type: object required: - count @@ -3494,8 +3829,8 @@ components: results: type: array items: - $ref: '#/components/schemas/Prijs' - PaginatedProductTypeList: + $ref: '#/components/schemas/Organisatie' + PaginatedPrijsList: type: object required: - count @@ -3517,8 +3852,8 @@ components: results: type: array items: - $ref: '#/components/schemas/ProductType' - PaginatedThemaList: + $ref: '#/components/schemas/Prijs' + PaginatedProductTypeList: type: object required: - count @@ -3540,8 +3875,8 @@ components: results: type: array items: - $ref: '#/components/schemas/Thema' - PaginatedVraagList: + $ref: '#/components/schemas/ProductType' + PaginatedThemaList: type: object required: - count @@ -3563,7 +3898,7 @@ components: results: type: array items: - $ref: '#/components/schemas/Vraag' + $ref: '#/components/schemas/Thema' PatchedBestandRequest: type: object properties: @@ -3604,6 +3939,23 @@ components: type: string description: De rol/functie van het contact maxLength: 100 + PatchedContentElementRequest: + type: object + description: Serializer that saves :class:`TranslatedFieldsField` automatically. + properties: + content: + type: string + minLength: 1 + description: De content van dit content element. + maxLength: 255 + labels: + type: array + items: + type: string + minLength: 1 + product_type_id: + type: string + format: uuid PatchedLinkRequest: type: object properties: @@ -3611,7 +3963,7 @@ components: type: string minLength: 1 description: Naam van de link. - maxLength: 100 + maxLength: 255 url: type: string format: uri @@ -3627,7 +3979,7 @@ components: naam: type: string minLength: 1 - maxLength: 100 + maxLength: 255 email: type: string format: email @@ -3641,11 +3993,11 @@ components: straat: type: string nullable: true - maxLength: 250 + maxLength: 255 huisnummer: type: string nullable: true - maxLength: 250 + maxLength: 10 postcode: type: string nullable: true @@ -3654,14 +4006,14 @@ components: stad: type: string nullable: true - maxLength: 250 + maxLength: 255 PatchedOrganisatieRequest: type: object properties: naam: type: string minLength: 1 - maxLength: 100 + maxLength: 255 email: type: string format: email @@ -3675,11 +4027,11 @@ components: straat: type: string nullable: true - maxLength: 250 + maxLength: 255 huisnummer: type: string nullable: true - maxLength: 250 + maxLength: 10 postcode: type: string nullable: true @@ -3688,12 +4040,12 @@ components: stad: type: string nullable: true - maxLength: 250 + maxLength: 255 code: type: string minLength: 1 description: code van de organisatie. - maxLength: 100 + maxLength: 255 PatchedPrijsRequest: type: object properties: @@ -3711,6 +4063,7 @@ components: description: De datum vanaf wanneer de prijs actief is. PatchedProductTypeRequest: type: object + description: Serializer that saves :class:`TranslatedFieldsField` automatically. properties: uniforme_product_naam: type: string @@ -3745,40 +4098,46 @@ components: format: uuid writeOnly: true default: [] - gepubliceerd: - type: boolean - description: Geeft aan of het object getoond kan worden. naam: type: string minLength: 1 - title: Product type naam description: naam van het product type. - maxLength: 100 + maxLength: 255 + samenvatting: + type: string + minLength: 1 + description: Korte beschrijving van het product type. + gepubliceerd: + type: boolean + description: Geeft aan of het object getoond kan worden. code: type: string minLength: 1 description: code van het product type. - maxLength: 100 + maxLength: 255 toegestane_statussen: type: array items: $ref: '#/components/schemas/ToegestaneStatussenEnum' description: toegestane statussen voor producten van dit type. - samenvatting: - type: string - minLength: 1 - description: Korte beschrijving van het product type, maximaal 300 karakters. - maxLength: 300 - beschrijving: - type: string - minLength: 1 - description: Product type beschrijving, ondersteund markdown format. keywords: type: array items: type: string maxLength: 100 description: Lijst van keywords waarop kan worden gezocht. + PatchedProductTypeTranslationRequest: + type: object + properties: + naam: + type: string + minLength: 1 + description: naam van het product type. + maxLength: 255 + samenvatting: + type: string + minLength: 1 + description: Korte beschrijving van het product type. PatchedThemaRequest: type: object properties: @@ -3786,7 +4145,7 @@ components: type: string minLength: 1 description: Naam van het thema. - maxLength: 100 + maxLength: 255 beschrijving: type: string description: Beschrijving van het thema, ondersteund markdown format. @@ -3805,26 +4164,6 @@ components: format: uuid writeOnly: true writeOnly: true - PatchedVraagRequest: - type: object - properties: - product_type_id: - type: string - format: uuid - nullable: true - thema_id: - type: string - format: uuid - nullable: true - vraag: - type: string - minLength: 1 - description: De vraag die wordt beantwoord. - maxLength: 250 - antwoord: - type: string - minLength: 1 - description: Het antwoord op de vraag, ondersteund markdown format. Prijs: type: object properties: @@ -3863,7 +4202,7 @@ components: beschrijving: type: string description: Korte beschrijving van de optie. - maxLength: 100 + maxLength: 255 required: - bedrag - beschrijving @@ -3882,7 +4221,7 @@ components: type: string minLength: 1 description: Korte beschrijving van de optie. - maxLength: 100 + maxLength: 255 required: - bedrag - beschrijving @@ -3907,6 +4246,7 @@ components: - product_type_id ProductType: type: object + description: Serializer that saves :class:`TranslatedFieldsField` automatically. properties: id: type: string @@ -3936,11 +4276,6 @@ components: items: $ref: '#/components/schemas/Contact' readOnly: true - vragen: - type: array - items: - $ref: '#/components/schemas/NestedVraag' - readOnly: true prijzen: type: array items: @@ -3956,6 +4291,17 @@ components: items: $ref: '#/components/schemas/NestedBestand' readOnly: true + naam: + type: string + description: naam van het product type. + maxLength: 255 + samenvatting: + type: string + description: Korte beschrijving van het product type. + taal: + type: string + readOnly: true + description: De huidige taal van het product type. gepubliceerd: type: boolean description: Geeft aan of het object getoond kan worden. @@ -3969,27 +4315,15 @@ components: format: date-time readOnly: true description: De datum waarop het object voor het laatst is gewijzigd. - naam: - type: string - title: Product type naam - description: naam van het product type. - maxLength: 100 code: type: string description: code van het product type. - maxLength: 100 + maxLength: 255 toegestane_statussen: type: array items: $ref: '#/components/schemas/ToegestaneStatussenEnum' description: toegestane statussen voor producten van dit type. - samenvatting: - type: string - description: Korte beschrijving van het product type, maximaal 300 karakters. - maxLength: 300 - beschrijving: - type: string - description: Product type beschrijving, ondersteund markdown format. keywords: type: array items: @@ -3998,7 +4332,6 @@ components: description: Lijst van keywords waarop kan worden gezocht. required: - aanmaak_datum - - beschrijving - bestanden - code - contacten @@ -4008,10 +4341,11 @@ components: - naam - organisaties - prijzen + - samenvatting + - taal - themas - uniforme_product_naam - update_datum - - vragen ProductTypeActuelePrijs: type: object properties: @@ -4019,11 +4353,10 @@ components: type: string format: uuid readOnly: true - naam: + code: type: string - title: Product type naam - description: naam van het product type. - maxLength: 100 + description: code van het product type. + maxLength: 255 upl_naam: type: string description: Uniforme product naam @@ -4039,12 +4372,13 @@ components: nullable: true required: - actuele_prijs + - code - id - - naam - upl_naam - upl_uri ProductTypeRequest: type: object + description: Serializer that saves :class:`TranslatedFieldsField` automatically. properties: uniforme_product_naam: type: string @@ -4079,34 +4413,28 @@ components: format: uuid writeOnly: true default: [] - gepubliceerd: - type: boolean - description: Geeft aan of het object getoond kan worden. naam: type: string minLength: 1 - title: Product type naam description: naam van het product type. - maxLength: 100 + maxLength: 255 + samenvatting: + type: string + minLength: 1 + description: Korte beschrijving van het product type. + gepubliceerd: + type: boolean + description: Geeft aan of het object getoond kan worden. code: type: string minLength: 1 description: code van het product type. - maxLength: 100 + maxLength: 255 toegestane_statussen: type: array items: $ref: '#/components/schemas/ToegestaneStatussenEnum' description: toegestane statussen voor producten van dit type. - samenvatting: - type: string - minLength: 1 - description: Korte beschrijving van het product type, maximaal 300 karakters. - maxLength: 300 - beschrijving: - type: string - minLength: 1 - description: Product type beschrijving, ondersteund markdown format. keywords: type: array items: @@ -4114,11 +4442,44 @@ components: maxLength: 100 description: Lijst van keywords waarop kan worden gezocht. required: - - beschrijving - code - naam + - samenvatting - thema_ids - uniforme_product_naam + ProductTypeTranslation: + type: object + properties: + id: + type: string + format: uuid + readOnly: true + naam: + type: string + description: naam van het product type. + maxLength: 255 + samenvatting: + type: string + description: Korte beschrijving van het product type. + required: + - id + - naam + - samenvatting + ProductTypeTranslationRequest: + type: object + properties: + naam: + type: string + minLength: 1 + description: naam van het product type. + maxLength: 255 + samenvatting: + type: string + minLength: 1 + description: Korte beschrijving van het product type. + required: + - naam + - samenvatting Thema: type: object properties: @@ -4129,15 +4490,10 @@ components: naam: type: string description: Naam van het thema. - maxLength: 100 + maxLength: 255 beschrijving: type: string description: Beschrijving van het thema, ondersteund markdown format. - vragen: - type: array - items: - $ref: '#/components/schemas/NestedVraag' - readOnly: true gepubliceerd: type: boolean description: Geeft aan of het object getoond kan worden. @@ -4168,7 +4524,6 @@ components: - naam - product_typen - update_datum - - vragen ThemaRequest: type: object properties: @@ -4176,7 +4531,7 @@ components: type: string minLength: 1 description: Naam van het thema. - maxLength: 100 + maxLength: 255 beschrijving: type: string description: Beschrijving van het thema, ondersteund markdown format. @@ -4213,55 +4568,6 @@ components: * `ingetrokken` - Ingetrokken * `geweigerd` - Geweigerd * `verlopen` - Verlopen - Vraag: - type: object - properties: - id: - type: string - format: uuid - readOnly: true - product_type_id: - type: string - format: uuid - nullable: true - thema_id: - type: string - format: uuid - nullable: true - vraag: - type: string - description: De vraag die wordt beantwoord. - maxLength: 250 - antwoord: - type: string - description: Het antwoord op de vraag, ondersteund markdown format. - required: - - antwoord - - id - - vraag - VraagRequest: - type: object - properties: - product_type_id: - type: string - format: uuid - nullable: true - thema_id: - type: string - format: uuid - nullable: true - vraag: - type: string - minLength: 1 - description: De vraag die wordt beantwoord. - maxLength: 250 - antwoord: - type: string - minLength: 1 - description: Het antwoord op de vraag, ondersteund markdown format. - required: - - antwoord - - vraag securitySchemes: tokenAuth: type: apiKey