diff --git a/.eslintrc.js b/.eslintrc.js index d8b08a420b..2b71a98767 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -22,24 +22,117 @@ module.exports = { 'sourceType': 'module' }, 'plugins': [ + 'simple-import-sort', 'react' ], 'ignorePatterns': ['**/static/**/*.js'], 'rules': { - 'indent': 'off', + // Disable unused rules 'no-empty-pattern': 'off', - 'linebreak-style': [ + + // Unix linebreaks only + 'linebreak-style': ['error', 'unix'], + + // Single quotes, allow double to avoid escaping + 'quotes': [ 'error', - 'unix' + 'single', + { 'avoidEscape': true } ], - 'quotes': [ + + // No semicolons + 'semi': ['error', 'never'], + + // Maximum line length of 120 characters + 'max-len': [ + 'error', + { + 'code': 120, + 'ignoreUrls': true, + 'ignoreRegExpLiterals': true + } + ], + + // Indent by 2 spaces + 'indent': ['error', 2, { + 'SwitchCase': 1, + }], + + // Enforce spaces around =, +, ==, ?, :, etc. + 'space-infix-ops': 'error', + + // Enforce separate const declarations + 'one-var': ['error', 'never'], + + // Ensure spaces after commas + 'comma-spacing': ['error', { before: false, after: true }], + + // Disallow newlines between the operands of a ternary expression + 'multiline-ternary': ['error', 'never'], + + // Ensure correct multiline imports + 'object-curly-newline': ['error', { + multiline: true, + consistent: true + }], + + // Import order + 'simple-import-sort/imports': ['error', { + groups: [ + // external imports in the given order + ['^react$', '^prop-types$', '^react', '^redux', '^[a-z]', '^lodash'], + // rdmo imports: lowercase first, then capitalized components + ['^rdmo/.*\\/[a-z]'], + ['^rdmo/.*\\/[A-Z]'], + // parent imports: lowercase first, then capitalized components + ['^\\.\\..*\\/[a-z]'], + ['^\\.\\..*\\/[A-Z]'], + // sibling imports: lowercase first, then capitalized components + ['^\\./.*\\/[a-z]'], + ['^\\./.*\\/[A-Z]'], + ], + }], + + // Export order + 'simple-import-sort/exports': 'error', + + // JSX: require a newline before the first prop when the JSX spans multiple lines and there is more than one prop + 'react/jsx-first-prop-new-line': ['error', 'multiline-multiprop'], + + // JSX: wrap multiline expressions in parens, opening paren on a new line + 'react/jsx-wrap-multilines': [ 'error', - 'single' + { + 'declaration': 'parens-new-line', + 'assignment': 'parens-new-line', + 'return': 'parens-new-line', + 'arrow': 'parens-new-line', + 'condition': 'parens-new-line', + 'logical': 'parens-new-line', + 'prop': 'parens-new-line' + } ], - 'semi': [ + + // JSX curly braces: require newlines inside for multiline expressions + 'react/jsx-curly-newline': [ 'error', - 'never' - ] + { + 'multiline': 'require', + 'singleline': 'consistent' + } + ], + + // JSX curly brace spacing: no spaces inside { } + 'react/jsx-curly-spacing': [ + 'error', + { + 'when': 'never', + 'children': true + } + ], + + // JSX double quotes for props + 'jsx-quotes': ['error', 'prefer-double'], }, 'settings': { 'react': { diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b8b6ebbbb..a1cb27c70f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,7 @@ jobs: runs-on: ubuntu-24.04 strategy: matrix: - python-version: ['3.10', '3.13'] + python-version: ['3.10', '3.14'] db-backend: [mysql, postgres] steps: - uses: actions/checkout@v6 @@ -94,7 +94,7 @@ jobs: - name: Run package status tests first run: | pytest rdmo/core/tests/test_package_status.py --nomigrations --verbose - if: matrix.python-version == '3.13' && matrix.db-backend == 'postgres' + if: matrix.python-version == '3.14' && matrix.db-backend == 'postgres' - name: Run Tests run: | pytest -p randomly -p no:cacheprovider --cov --reuse-db --numprocesses=auto --dist=loadscope @@ -114,7 +114,7 @@ jobs: runs-on: ubuntu-24.04 strategy: matrix: - python-version: ['3.13'] + python-version: ['3.14'] db-backend: [postgres] steps: - uses: actions/checkout@v6 @@ -184,7 +184,7 @@ jobs: persist-credentials: false - uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" cache: pip - run: python -Im pip install --editable .[dev] - run: python -Ic 'import rdmo; print(rdmo.__version__)' @@ -199,7 +199,7 @@ jobs: persist-credentials: false - uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" cache: pip - name: Download wheel uses: actions/download-artifact@v6 diff --git a/.gitignore b/.gitignore index ec05075999..50da21c10b 100644 --- a/.gitignore +++ b/.gitignore @@ -36,16 +36,28 @@ dist rdmo/management/static +rdmo/core/static/core/css/app-bs53.css +rdmo/core/static/core/js/app-bs53.js rdmo/core/static/core/js/base.js rdmo/core/static/core/js/base.js.LICENSE.txt +rdmo/core/static/core/js/base-bs53.js +rdmo/core/static/core/js/base-bs53.js.LICENSE.txt +rdmo/core/static/core/js/bootstrap-bs53.js +rdmo/core/static/core/js/bootstrap-bs53.js.LICENSE.txt rdmo/core/static/core/css/base.css +rdmo/core/static/core/css/base-bs53.css +rdmo/core/static/core/css/bootstrap.css +rdmo/core/static/core/css/bootstrap-bs53.css rdmo/core/static/core/fonts rdmo/projects/static/projects/css/interview.css rdmo/projects/static/projects/css/projects.css +rdmo/projects/static/projects/css/project.css rdmo/projects/static/projects/fonts/ rdmo/projects/static/projects/js/interview.js rdmo/projects/static/projects/js/interview.js.LICENSE.txt rdmo/projects/static/projects/js/projects.js rdmo/projects/static/projects/js/projects.js.LICENSE.txt +rdmo/projects/static/projects/js/project.js +rdmo/projects/static/projects/js/project.js.LICENSE.txt screenshots diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index de0884ed08..f241ae0d05 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,6 +34,7 @@ repos: additional_dependencies: - eslint@8.56.0 - eslint-plugin-react@7.37.0 + - eslint-plugin-simple-import-sort@12.1.1 - react@18.3.1 - repo: https://github.com/crate-ci/typos rev: v1.40.0 diff --git a/conftest.py b/conftest.py index 01fad1f47f..46de3de7cc 100644 --- a/conftest.py +++ b/conftest.py @@ -39,6 +39,7 @@ def django_db_setup(django_db_setup, django_db_blocker, fixtures): """Populate database with test data from fixtures directories.""" with django_db_blocker.unblock(): call_command('loaddata', *fixtures) + call_command('rebuild_mptt') set_group_permissions() diff --git a/package-lock.json b/package-lock.json index 185e4b3f9c..c23482663e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,15 @@ "dependencies": { "@codemirror/lang-html": "^6.4.2", "@codemirror/lang-javascript": "^6.2.2", + "@fontsource/open-sans": "^5.2.6", + "@fontsource/roboto-slab": "^5.2.6", "@uiw/react-codemirror": "^4.25.1", + "bootstrap": "^5.3.3", + "bootstrap-icons": "^1.11.3", "bootstrap-sass": "^3.4.1", "classnames": "^2.5.1", - "date-fns": "^4.1.0", + "date-fns": "^3.6.0", "font-awesome": "4.7.0", - "html-to-text": "^9.0.5", "jquery": "^3.7.1", "js-cookie": "^3.0.5", "lodash": "^4.17.23", @@ -34,7 +37,7 @@ "redux": "^4.1.1", "redux-logger": "^3.0.6", "redux-thunk": "^2.3.0", - "use-debounce": "^10.0.0" + "use-debounce": "^10.0.4" }, "devDependencies": { "@babel/cli": "^7.28.0", @@ -45,7 +48,8 @@ "copy-webpack-plugin": "^14.0.0", "css-loader": "^7.1.1", "eslint": "~8.56.0", - "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react": "^7.35.0", + "eslint-plugin-simple-import-sort": "^12.1.1", "file-loader": "^6.2.0", "mini-css-extract-plugin": "^2.9.0", "sass": "^1.94.2", @@ -2150,6 +2154,22 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" }, + "node_modules/@fontsource/open-sans": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-5.2.6.tgz", + "integrity": "sha512-mnfnUmBWQ+J220gqbibbzmKcc1kawV+lb3/Pspzu+Opnxza12oUffIg0ufG8g+3j1fnSznEWgyNV40MjtmJj6g==", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/roboto-slab": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@fontsource/roboto-slab/-/roboto-slab-5.2.6.tgz", + "integrity": "sha512-srUROPqdczZx5OBlCKojA3C9eNeV3iIAT+nb0YLGb21ZNv58PUf5mom5T5+x6BMaaH1ZuXDi0sT1NaKWuoagYg==", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -2634,6 +2654,16 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@react-dnd/asap": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", @@ -2649,18 +2679,6 @@ "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" }, - "node_modules/@selderee/plugin-htmlparser2": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", - "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", - "dependencies": { - "domhandler": "^5.0.3", - "selderee": "^0.11.0" - }, - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -3438,15 +3456,48 @@ "node": ">=8" } }, + "node_modules/bootstrap": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz", + "integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/bootstrap-icons": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz", + "integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ] + }, "node_modules/bootstrap-sass": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/bootstrap-sass/-/bootstrap-sass-3.4.3.tgz", "integrity": "sha512-vPgFnGMp1jWZZupOND65WS6mkR8rxhJxndT/AcMbqcq1hHMdkcH4sMPhznLzzoHOHkSCrd6J9F8pWBriPCKP2Q==" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -3867,9 +3918,9 @@ } }, "node_modules/date-fns": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -3902,14 +3953,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -3995,57 +4038,6 @@ "@babel/runtime": "^7.1.2" } }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, "node_modules/electron-to-chromium": { "version": "1.5.286", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", @@ -4074,17 +4066,6 @@ "node": ">=10.13.0" } }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/envinfo": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", @@ -4403,6 +4384,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/eslint-plugin-simple-import-sort": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", + "integrity": "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=5.0.0" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -4864,9 +4855,9 @@ } }, "node_modules/flatted": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", - "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -5161,39 +5152,6 @@ "react-is": "^16.7.0" } }, - "node_modules/html-to-text": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", - "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", - "dependencies": { - "@selderee/plugin-htmlparser2": "^0.11.0", - "deepmerge": "^4.3.1", - "dom-serializer": "^2.0.0", - "htmlparser2": "^8.0.2", - "selderee": "^0.11.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, "node_modules/icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", @@ -5847,14 +5805,6 @@ "node": ">=0.10.0" } }, - "node_modules/leac": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", - "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -6294,18 +6244,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parseley": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", - "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", - "dependencies": { - "leac": "^0.6.0", - "peberminta": "^0.9.0" - }, - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6346,24 +6284,17 @@ "node": ">=8" } }, - "node_modules/peberminta": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", - "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, "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/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">=8.6" @@ -6635,6 +6566,15 @@ "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/react-datepicker/node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/react-diff-viewer-continued": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/react-diff-viewer-continued/-/react-diff-viewer-continued-3.4.0.tgz", @@ -7321,17 +7261,6 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, - "node_modules/selderee": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", - "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", - "dependencies": { - "parseley": "^0.12.0" - }, - "funding": { - "url": "https://ko-fi.com/killymxi" - } - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -7342,9 +7271,9 @@ } }, "node_modules/serialize-javascript": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", - "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", + "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -7732,9 +7661,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -7972,9 +7901,9 @@ } }, "node_modules/use-debounce": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.3.tgz", - "integrity": "sha512-DxQSI9ZKso689WM1mjgGU3ozcxU1TJElBJ3X6S4SMzMNcm2lVH0AHmyXB+K7ewjz2BSUKJTDqTcwtSMRfB89dg==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.4.tgz", + "integrity": "sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw==", "engines": { "node": ">= 16.0.0" }, @@ -8270,9 +8199,10 @@ "dev": true }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", "engines": { "node": ">= 6" } @@ -9727,6 +9657,16 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" }, + "@fontsource/open-sans": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-5.2.6.tgz", + "integrity": "sha512-mnfnUmBWQ+J220gqbibbzmKcc1kawV+lb3/Pspzu+Opnxza12oUffIg0ufG8g+3j1fnSznEWgyNV40MjtmJj6g==" + }, + "@fontsource/roboto-slab": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@fontsource/roboto-slab/-/roboto-slab-5.2.6.tgz", + "integrity": "sha512-srUROPqdczZx5OBlCKojA3C9eNeV3iIAT+nb0YLGb21ZNv58PUf5mom5T5+x6BMaaH1ZuXDi0sT1NaKWuoagYg==" + }, "@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -10006,6 +9946,12 @@ "dev": true, "optional": true }, + "@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "peer": true + }, "@react-dnd/asap": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", @@ -10021,15 +9967,6 @@ "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" }, - "@selderee/plugin-htmlparser2": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", - "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", - "requires": { - "domhandler": "^5.0.3", - "selderee": "^0.11.0" - } - }, "@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -10624,15 +10561,26 @@ "dev": true, "optional": true }, + "bootstrap": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz", + "integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==", + "requires": {} + }, + "bootstrap-icons": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz", + "integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==" + }, "bootstrap-sass": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/bootstrap-sass/-/bootstrap-sass-3.4.3.tgz", "integrity": "sha512-vPgFnGMp1jWZZupOND65WS6mkR8rxhJxndT/AcMbqcq1hHMdkcH4sMPhznLzzoHOHkSCrd6J9F8pWBriPCKP2Q==" }, "brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "requires": { "balanced-match": "^1.0.0", @@ -10914,9 +10862,9 @@ } }, "date-fns": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==" + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==" }, "debug": { "version": "4.4.1", @@ -10937,11 +10885,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" - }, "define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -11003,39 +10946,6 @@ "@babel/runtime": "^7.1.2" } }, - "dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "requires": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - } - }, - "domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" - }, - "domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "requires": { - "domelementtype": "^2.3.0" - } - }, - "domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "requires": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - } - }, "electron-to-chromium": { "version": "1.5.286", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", @@ -11058,11 +10968,6 @@ "tapable": "^2.3.0" } }, - "entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" - }, "envinfo": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", @@ -11442,6 +11347,13 @@ } } }, + "eslint-plugin-simple-import-sort": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", + "integrity": "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==", + "dev": true, + "requires": {} + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -11641,9 +11553,9 @@ } }, "flatted": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", - "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true }, "font-awesome": { @@ -11851,29 +11763,6 @@ "react-is": "^16.7.0" } }, - "html-to-text": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", - "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", - "requires": { - "@selderee/plugin-htmlparser2": "^0.11.0", - "deepmerge": "^4.3.1", - "dom-serializer": "^2.0.0", - "htmlparser2": "^8.0.2", - "selderee": "^0.11.0" - } - }, - "htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "requires": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, "icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", @@ -12319,11 +12208,6 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, - "leac": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", - "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==" - }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -12644,15 +12528,6 @@ "lines-and-columns": "^1.1.6" } }, - "parseley": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", - "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", - "requires": { - "leac": "^0.6.0", - "peberminta": "^0.9.0" - } - }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -12681,20 +12556,15 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" }, - "peberminta": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", - "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==" - }, "picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "optional": true }, @@ -12869,6 +12739,13 @@ "@floating-ui/react": "^0.27.15", "clsx": "^2.1.1", "date-fns": "^4.1.0" + }, + "dependencies": { + "date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==" + } } }, "react-diff-viewer-continued": { @@ -13329,14 +13206,6 @@ } } }, - "selderee": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", - "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", - "requires": { - "parseley": "^0.12.0" - } - }, "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -13344,9 +13213,9 @@ "devOptional": true }, "serialize-javascript": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", - "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", + "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", "dev": true }, "set-function-length": { @@ -13609,9 +13478,9 @@ "requires": {} }, "picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true } } @@ -13769,9 +13638,9 @@ } }, "use-debounce": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.3.tgz", - "integrity": "sha512-DxQSI9ZKso689WM1mjgGU3ozcxU1TJElBJ3X6S4SMzMNcm2lVH0AHmyXB+K7ewjz2BSUKJTDqTcwtSMRfB89dg==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.4.tgz", + "integrity": "sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw==", "requires": {} }, "use-isomorphic-layout-effect": { @@ -13980,9 +13849,9 @@ "dev": true }, "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==" }, "yocto-queue": { "version": "0.1.0", diff --git a/package.json b/package.json index b156cfc1f2..55097f7646 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "build:prod": "webpack --config webpack.config.js --mode production", "build": "webpack --config webpack.config.js --mode development", "watch": "webpack --config webpack.config.js --mode development --watch", - "lint": "eslint --ext .js rdmo/" + "lint": "eslint rdmo/**/*.js" }, "author": "RDMO Arbeitsgemeinschaft ", "license": "Apache-2.0", @@ -20,12 +20,15 @@ "dependencies": { "@codemirror/lang-html": "^6.4.2", "@codemirror/lang-javascript": "^6.2.2", + "@fontsource/open-sans": "^5.2.6", + "@fontsource/roboto-slab": "^5.2.6", "@uiw/react-codemirror": "^4.25.1", + "bootstrap": "^5.3.3", + "bootstrap-icons": "^1.11.3", "bootstrap-sass": "^3.4.1", "classnames": "^2.5.1", - "date-fns": "^4.1.0", + "date-fns": "^3.6.0", "font-awesome": "4.7.0", - "html-to-text": "^9.0.5", "jquery": "^3.7.1", "js-cookie": "^3.0.5", "lodash": "^4.17.23", @@ -44,7 +47,7 @@ "redux": "^4.1.1", "redux-logger": "^3.0.6", "redux-thunk": "^2.3.0", - "use-debounce": "^10.0.0" + "use-debounce": "^10.0.4" }, "devDependencies": { "@babel/cli": "^7.28.0", @@ -55,7 +58,8 @@ "copy-webpack-plugin": "^14.0.0", "css-loader": "^7.1.1", "eslint": "~8.56.0", - "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react": "^7.35.0", + "eslint-plugin-simple-import-sort": "^12.1.1", "file-loader": "^6.2.0", "mini-css-extract-plugin": "^2.9.0", "sass": "^1.94.2", diff --git a/pyproject.toml b/pyproject.toml index a2db2477da..08fbaf2f4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ requires-python = ">=3.10" classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", - "Framework :: Django :: 4.2", + "Framework :: Django :: 5.2", "Intended Audience :: Science/Research", "Operating System :: OS Independent", "Programming Language :: Python", @@ -31,6 +31,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] dynamic = [ "version", @@ -41,7 +42,7 @@ dependencies = [ # in minor version updates anytime "defusedcsv>=2.0,<4.0", "defusedxml>=0.7.1,<1.0", - "django>=4.2,<5.0", + "django>=5.2.8,<6.0", "django-cleanup>=8.0,<10.0", "django-compressor>=4.4,<5.0", "django-extensions>=3.2,<5.0", @@ -203,15 +204,11 @@ markers = [ "e2e: marks tests as end-to-end tests using playwright (deselect with '-m \"not e2e\"')", ] filterwarnings = [ - # fail on RemovedInDjango50Warning exception - "error::django.utils.deprecation.RemovedInDjango50Warning", + # throw an error when using methods deprecated in the next django version + "error::django.utils.deprecation.RemovedInNextVersionWarning", # ignore warnings raised by widget_tweaks.py "ignore:'maxsplit' is passed as positional argument", - - # ignore warnings raised from within django itself - # django/core/files/storage/__init__.py - "ignore:django.core.files.storage.get_storage_class is deprecated:django.utils.deprecation.RemovedInDjango51Warning", ] [tool.coverage.run] @@ -234,6 +231,7 @@ default.extend-ignore-re = [ "(?Rm)^.*(#|//)\\s*spellchecker:disable-line$", # for .py files "(?Rm)^.*$", # for .html files ] +files.extend-exclude = ["rdmo/core/templates/core/bs53/home.html"] [tool.check-wheel-contents] # Ref: https://github.com/jwodder/check-wheel-contents ignore = [ diff --git a/rdmo/accounts/account.py b/rdmo/accounts/account.py index b803635e64..4e10ce44c1 100644 --- a/rdmo/accounts/account.py +++ b/rdmo/accounts/account.py @@ -1,6 +1,7 @@ from django.conf import settings from django.contrib.auth.models import Group from django.forms import BooleanField +from django.urls import reverse from allauth.account.adapter import DefaultAccountAdapter from allauth.account.forms import LoginForm as AllauthLoginForm @@ -24,6 +25,9 @@ def save_user(self, request, user, form, commit=True): return user + def get_password_change_redirect_url(self, request): + return reverse('projects') + class LoginForm(AllauthLoginForm): diff --git a/rdmo/accounts/forms.py b/rdmo/accounts/forms.py index e71127c2ab..cee252b3bb 100644 --- a/rdmo/accounts/forms.py +++ b/rdmo/accounts/forms.py @@ -71,19 +71,17 @@ def _save_additional_values(self, user=None): class RemoveForm(forms.Form): def __init__(self, *args, **kwargs): - self.request = kwargs.pop('request') - kwargs.setdefault('label_suffix', '') + self.user = kwargs.pop('user') + super().__init__(*args, **kwargs) - if not self.request.user.has_usable_password(): + if not self.user.has_usable_password(): self.fields.pop('password') email = forms.CharField(widget=forms.TextInput(attrs={'required': 'false'})) email.label = _('E-mail') - email.widget.attrs = {'class': 'form-control', 'placeholder': email.label} password = forms.CharField(widget=forms.PasswordInput) password.label = _('Password') - password.widget.attrs = {'class': 'form-control', 'placeholder': password.label} consent = forms.BooleanField(required=True) consent.label = _("I confirm that I want my profile to be completely removed. This can not be undone!") diff --git a/rdmo/accounts/models.py b/rdmo/accounts/models.py index b54f551758..33cbbaf8b5 100644 --- a/rdmo/accounts/models.py +++ b/rdmo/accounts/models.py @@ -214,6 +214,10 @@ def is_editor(self): def is_reviewer(self): return self.reviewer.filter(id=settings.SITE_ID).exists() + @cached_property + def is_site_manager(self): + return self.manager.filter(id=settings.SITE_ID).exists() + @receiver(post_save, sender=settings.AUTH_USER_MODEL) def post_save_user(sender, **kwargs): diff --git a/rdmo/accounts/serializers/v1.py b/rdmo/accounts/serializers/v1.py index a53c138774..cf5eb90011 100644 --- a/rdmo/accounts/serializers/v1.py +++ b/rdmo/accounts/serializers/v1.py @@ -2,10 +2,13 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.contrib.sites.models import Site +from django.core.exceptions import ValidationError +from django.core.validators import EmailValidator +from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from rdmo.projects.models import Membership +from rdmo.projects.models import Invite, Membership from ..models import Role @@ -66,6 +69,8 @@ class UserSerializer(serializers.ModelSerializer): role = UserRoleSerializer() memberships = UserMembershipSerializer(many=True) + is_site_manager = serializers.BooleanField(source='role.is_site_manager') + class Meta: model = get_user_model() fields = [ @@ -74,7 +79,8 @@ class Meta: 'role', 'memberships', 'is_superuser', - 'is_staff' + 'is_staff', + 'is_site_manager' ] if settings.USER_API: fields += [ @@ -85,3 +91,47 @@ class Meta: 'last_login', 'date_joined', ] + +class UserLookupSerializer(serializers.Serializer): + first_name = serializers.CharField(source='user.first_name', read_only=True) + last_name = serializers.CharField(source='user.last_name', read_only=True) + lookup = serializers.CharField( + required=False, write_only=True, help_text=_("The username or e-mail of the user.") + ) + + def validate_lookup(self, value: str) -> str: + if "@" in value: + validator = EmailValidator() + try: + validator(value) + except ValidationError as e: + raise serializers.ValidationError(validator.message) from e + return value + + def resolve_lookup(self, value): + User = get_user_model() + + # 1) Try exact username match first — even if it contains '@' + try: + user = User.objects.get(username=value) + except User.DoesNotExist: + # 2) Try case-insensitive email match + try: + user = User.objects.get(email__iexact=value) + except User.DoesNotExist as e: + if ( + "@" in value and + self.Meta.model is Invite and + settings.PROJECT_SEND_INVITE + ): + # return an email when invite send is allowed + return None, value + raise serializers.ValidationError({"lookup": _("No user found.")}) from e + except User.MultipleObjectsReturned as e: + raise serializers.ValidationError({"lookup": _("Multiple users found with that e-mail.")}) from e + else: + return user, user.email + except User.MultipleObjectsReturned as e: + raise serializers.ValidationError({'lookup': _('Multiple users found with that username.')}) from e + else: + return user, user.email diff --git a/rdmo/accounts/static/accounts/img/orcid-logo.png b/rdmo/accounts/static/accounts/img/orcid-logo.png new file mode 100644 index 0000000000..bebd05a7bf Binary files /dev/null and b/rdmo/accounts/static/accounts/img/orcid-logo.png differ diff --git a/rdmo/accounts/static/accounts/img/orcid-signin.png b/rdmo/accounts/static/accounts/img/orcid-signin.png deleted file mode 100644 index 2a2af05d20..0000000000 Binary files a/rdmo/accounts/static/accounts/img/orcid-signin.png and /dev/null differ diff --git a/rdmo/accounts/static/accounts/img/orcid_16x16.png b/rdmo/accounts/static/accounts/img/orcid_16x16.png deleted file mode 100644 index 6b697e4d7b..0000000000 Binary files a/rdmo/accounts/static/accounts/img/orcid_16x16.png and /dev/null differ diff --git a/rdmo/accounts/templates/account/account_token.html b/rdmo/accounts/templates/account/account_token.html index 9d8b1256cb..3b08e2590e 100644 --- a/rdmo/accounts/templates/account/account_token.html +++ b/rdmo/accounts/templates/account/account_token.html @@ -1,7 +1,7 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} -{% block page %} +{% block main %}

{% trans "API token" %}

@@ -9,7 +9,10 @@

{% trans "API token" %}

{% csrf_token %} - + +

diff --git a/rdmo/accounts/templates/account/email.html b/rdmo/accounts/templates/account/email.html index 6fb9e86652..649a0f2ecf 100644 --- a/rdmo/accounts/templates/account/email.html +++ b/rdmo/accounts/templates/account/email.html @@ -1,7 +1,7 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} -{% block page %} +{% block main %}

{% trans "E-mail Addresses" %}

@@ -9,74 +9,114 @@

{% trans "E-mail Addresses" %}

{% trans 'The following e-mail addresses are associated with your account:' %}

-
+ {% csrf_token %} -
+ {% for emailaddress in user.emailaddress_set.all %} - {% for emailaddress in user.emailaddress_set.all %} + {% if forloop.first %} +
+ {% endif%} -
+
{% else %}

- {% trans 'Warning:'%} {% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %} + {% trans 'Warning:'%} + {% blocktrans trimmed %} + You currently do not have any e-mail address set up. You should really add an + e-mail address so you can receive notifications, reset your password, etc. + {% endblocktrans %}

{% endif %}

{% trans "Add E-mail Address" %}

-
+ {% csrf_token %} - {% include 'core/bootstrap_form_fields.html' %} +
+ - - + {% if field.help_text %} +

+ {{ field.help_text }} +

+ {% endif %} -{% endblock %} + + + {% for error in form.email.errors %} +
{{ error }}
+ {% endfor %} +
+ + + -{% block extra_body %} - {% endblock %} diff --git a/rdmo/accounts/templates/account/email_confirm.html b/rdmo/accounts/templates/account/email_confirm.html index 3abc4b0402..5d3c1c3a0e 100644 --- a/rdmo/accounts/templates/account/email_confirm.html +++ b/rdmo/accounts/templates/account/email_confirm.html @@ -1,8 +1,8 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} {% load account %} -{% block page %} +{% block main %}

{% trans "Confirm E-mail Address" %}

@@ -18,7 +18,10 @@

{% trans "Confirm E-mail Address" %}

{% csrf_token %} - + +
{% else %} @@ -33,4 +36,4 @@

{% trans "Confirm E-mail Address" %}

{% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/rdmo/accounts/templates/account/login.html b/rdmo/accounts/templates/account/login.html index 5a0ed3cf27..d99dac3654 100644 --- a/rdmo/accounts/templates/account/login.html +++ b/rdmo/accounts/templates/account/login.html @@ -1,10 +1,24 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} -{% block page %} +{% block main %}

{% trans "Login" %}

- {% include 'account/login_form.html'%} +
+ {% if settings.LOGIN_FORM %} + {% include 'account/login_form.html' %} + {% endif %} + + {% if settings.SHIBBOLETH %} + {% include 'account/login_shibboleth.html' %} + {% endif %} + + {% if settings.SOCIALACCOUNT %} +
+ {% include "socialaccount/snippets/provider_list.html" with process="login" %} +
+ {% endif %} +
{% endblock %} diff --git a/rdmo/accounts/templates/account/login_form.html b/rdmo/accounts/templates/account/login_form.html index 017723a52e..d577d9ca5a 100644 --- a/rdmo/accounts/templates/account/login_form.html +++ b/rdmo/accounts/templates/account/login_form.html @@ -1,33 +1,45 @@ {% load i18n %} +{% load core_tags %} -{% if settings.SHIBBOLETH %} -

- - {% trans 'Login with Shibboleth' %} - -

-{% endif %} +
+
+ {% csrf_token %} -{% if settings.LOGIN_FORM %} - - {% csrf_token %} + {% if redirect_field_value %} + + {% elif next %} + + {% endif %} - {% if redirect_field_value %} - - {% elif next %} - - {% endif %} + {% for field in form.visible_fields %} + {% bootstrap_form_field field %} + {% endfor %} - {% include 'core/bootstrap_form_fields.html' %} + - -
-{% endif %} + {% for error in form.non_field_errors %} + + {% endfor %} + -{% if settings.ACCOUNT %} -{% include 'account/login_form_account.html' %} -{% endif %} + {% if settings.ACCOUNT %} +
+ {% url 'account_reset_password' as reset_url %} + {% blocktrans trimmed %} + If you forgot your password and want to reset it, click here. + {% endblocktrans %} +
+ {% endif %} -{% if settings.SOCIALACCOUNT %} -{% include 'account/login_form_socialaccount.html' %} -{% endif %} + {% if settings.ACCOUNT_SIGNUP %} +
+ {% blocktrans trimmed %} + If you have not created an account yet, then please sign up first. + {% endblocktrans %} +
+ {% endif %} +
diff --git a/rdmo/accounts/templates/account/login_form_account.html b/rdmo/accounts/templates/account/login_form_account.html deleted file mode 100644 index b4fec228f9..0000000000 --- a/rdmo/accounts/templates/account/login_form_account.html +++ /dev/null @@ -1,15 +0,0 @@ -{% load i18n %} -{% load account %} - -{% if settings.ACCOUNT_SIGNUP %} - -

- {% blocktrans %}If you have not created an account yet, then please sign up first.{% endblocktrans %} -

- -{% endif %} - -

- {% url 'account_reset_password' as reset_url %} - {% blocktrans %}If you forgot your password and want to reset it, click here.{% endblocktrans %} -

diff --git a/rdmo/accounts/templates/account/login_form_inline.html b/rdmo/accounts/templates/account/login_form_inline.html new file mode 100644 index 0000000000..be689d1698 --- /dev/null +++ b/rdmo/accounts/templates/account/login_form_inline.html @@ -0,0 +1,56 @@ +{% load i18n %} +{% load core_tags %} + +
+
+ {% csrf_token %} + + {% if form.login %} +
+ + +
+ + {% else %} + +
+ + +
+ + {% endif %} + +
+ + +
+
+ + {% if settings.ACCOUNT_SIGNUP %} + {% trans 'Sign up' %} + {% endif %} +
+
+ + {% if settings.ACCOUNT %} + {% url 'account_reset_password' as reset_url %} + + {% endif %} +
diff --git a/rdmo/accounts/templates/account/login_form_socialaccount.html b/rdmo/accounts/templates/account/login_form_socialaccount.html deleted file mode 100644 index c0bd6e41f8..0000000000 --- a/rdmo/accounts/templates/account/login_form_socialaccount.html +++ /dev/null @@ -1,20 +0,0 @@ -{% load i18n %} -{% load socialaccount %} - -{% get_providers as socialaccount_providers %} - -{% if socialaccount_providers %} - -

- {% blocktrans with site.name as site_name %}Alternatively, you can login using one of the following third party accounts:{% endblocktrans %} -

- -
- -
- -{% include "socialaccount/snippets/login_extra.html" %} - -{% endif %} diff --git a/rdmo/accounts/templates/account/login_shibboleth.html b/rdmo/accounts/templates/account/login_shibboleth.html new file mode 100644 index 0000000000..3933805495 --- /dev/null +++ b/rdmo/accounts/templates/account/login_shibboleth.html @@ -0,0 +1,7 @@ +{% load i18n %} + +
+ + {% trans 'Sign in with Shibboleth' %} + +
diff --git a/rdmo/accounts/templates/account/logout.html b/rdmo/accounts/templates/account/logout.html index 09df13e630..1e80b5b0fd 100644 --- a/rdmo/accounts/templates/account/logout.html +++ b/rdmo/accounts/templates/account/logout.html @@ -1,7 +1,7 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} -{% block page %} +{% block main %}

{% trans "Logout" %}

@@ -16,7 +16,9 @@

{% trans "Logout" %}

{% endif %} - + {% endblock %} diff --git a/rdmo/accounts/templates/account/logout_form.html b/rdmo/accounts/templates/account/logout_form.html deleted file mode 100644 index 9df56a2759..0000000000 --- a/rdmo/accounts/templates/account/logout_form.html +++ /dev/null @@ -1,8 +0,0 @@ -{% load i18n %} - -
- {% csrf_token %} - -
diff --git a/rdmo/accounts/templates/account/password_change.html b/rdmo/accounts/templates/account/password_change.html index 847052f859..14ead05a7c 100644 --- a/rdmo/accounts/templates/account/password_change.html +++ b/rdmo/accounts/templates/account/password_change.html @@ -1,7 +1,8 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} +{% load core_tags %} -{% block page %} +{% block main %}

{% trans "Change password" %}

@@ -12,9 +13,13 @@

{% trans "Change password" %}

{% csrf_token %} - {% include 'core/bootstrap_form_fields.html' %} + {% for field in form.visible_fields %} + {% bootstrap_form_field field %} + {% endfor %} - +
{% endblock %} diff --git a/rdmo/accounts/templates/account/password_reset.html b/rdmo/accounts/templates/account/password_reset.html index 09a76bd93f..e6371cda4e 100644 --- a/rdmo/accounts/templates/account/password_reset.html +++ b/rdmo/accounts/templates/account/password_reset.html @@ -1,7 +1,8 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} +{% load core_tags %} -{% block page %} +{% block main %}

{% trans "Password reset" %}

@@ -16,9 +17,13 @@

{% trans "Password reset" %}

{% csrf_token %} - {% include 'core/bootstrap_form_fields.html' %} + {% for field in form.visible_fields %} + {% bootstrap_form_field field %} + {% endfor %} - +
{% endblock %} diff --git a/rdmo/accounts/templates/account/password_reset_done.html b/rdmo/accounts/templates/account/password_reset_done.html index c0b9a3246e..7d51430f31 100644 --- a/rdmo/accounts/templates/account/password_reset_done.html +++ b/rdmo/accounts/templates/account/password_reset_done.html @@ -1,7 +1,7 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} -{% block page %} +{% block main %}

{% trans "Password reset sent" %}

diff --git a/rdmo/accounts/templates/account/password_reset_from_key.html b/rdmo/accounts/templates/account/password_reset_from_key.html index 8bddbe38d0..599498964e 100644 --- a/rdmo/accounts/templates/account/password_reset_from_key.html +++ b/rdmo/accounts/templates/account/password_reset_from_key.html @@ -1,7 +1,8 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} +{% load core_tags %} -{% block page %} +{% block main %} {% if token_fail %} @@ -22,9 +23,13 @@

{% trans "Enter new password" %}

{% csrf_token %} - {% include 'core/bootstrap_form_fields.html' %} + {% for field in form.visible_fields %} + {% bootstrap_form_field field %} + {% endfor %} - +
{% else %} diff --git a/rdmo/accounts/templates/account/password_reset_from_key_done.html b/rdmo/accounts/templates/account/password_reset_from_key_done.html index 1a91b201f3..1c58a125c2 100644 --- a/rdmo/accounts/templates/account/password_reset_from_key_done.html +++ b/rdmo/accounts/templates/account/password_reset_from_key_done.html @@ -1,8 +1,8 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} {% load core_tags %} -{% block page %} +{% block main %}

{% trans "Password reset complete" %}

@@ -11,7 +11,7 @@

{% trans "Password reset complete" %}

- {% trans 'Login' %} + {% trans 'Sign in' %}

{% endblock %} diff --git a/rdmo/accounts/templates/account/password_set.html b/rdmo/accounts/templates/account/password_set.html index d5f324c0df..fd2b9c4e90 100644 --- a/rdmo/accounts/templates/account/password_set.html +++ b/rdmo/accounts/templates/account/password_set.html @@ -1,7 +1,7 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} -{% block page %} +{% block main %}

{% trans "Set new password" %}

@@ -12,9 +12,13 @@

{% trans "Set new password" %}

{% csrf_token %} - {% include 'core/bootstrap_form_fields.html' %} + {% for field in form.visible_fields %} + {% bootstrap_form_field field %} + {% endfor %} - +
{% endblock %} diff --git a/rdmo/accounts/templates/account/signup.html b/rdmo/accounts/templates/account/signup.html index c3d29ed70f..f63275a753 100644 --- a/rdmo/accounts/templates/account/signup.html +++ b/rdmo/accounts/templates/account/signup.html @@ -1,9 +1,9 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} -{% block page %} +{% block main %} -

{% trans "Create a new account" %}

+

{% trans "Create a new account" %}

{% blocktrans trimmed %} @@ -11,50 +11,10 @@

{% trans "Create a new account" %}

{% endblocktrans %}

-
- {% csrf_token %} - - {% if redirect_field_value %} - - {% endif %} - - {% for field in form.visible_fields %} - - {% if field.html_name != 'consent' %} - {% include 'core/bootstrap_form_field.html' with field=field %} - {% endif %} - - {% endfor %} - {% if settings.ACCOUNT_TERMS_OF_USE %} - {% with field=form.consent %} -
-
- -
-
- - {% if field.errors %} -
-

{% trans 'You need to agree to the terms of use to proceed.' %}

-
- {% endif %} - {% endwith %} - {% endif %} - - -
- - + {% include 'account/signup_form.html' %} {% if settings.ACCOUNT_TERMS_OF_USE %} - {% include 'account/signup_modal_terms_of_use.html' %} + {% include 'account/terms_of_use_modal.html' %} {% endif %} {% endblock %} diff --git a/rdmo/accounts/templates/account/signup_closed.html b/rdmo/accounts/templates/account/signup_closed.html index e26c41d45a..af197d0d00 100644 --- a/rdmo/accounts/templates/account/signup_closed.html +++ b/rdmo/accounts/templates/account/signup_closed.html @@ -1,9 +1,9 @@ {% extends 'core/page.html' %} {% load i18n %} -{% block page %} +{% block main %} -

{% trans "Sign up closed" %}

+

{% trans "Sign up closed" %}

{% trans "We are sorry, but the sign up is currently closed." %} diff --git a/rdmo/accounts/templates/account/signup_form.html b/rdmo/accounts/templates/account/signup_form.html new file mode 100644 index 0000000000..f8ad3b2bf1 --- /dev/null +++ b/rdmo/accounts/templates/account/signup_form.html @@ -0,0 +1,26 @@ +{% load i18n %} +{% load core_tags %} + +

+
+ {% csrf_token %} + + {% if redirect_field_value %} + + {% endif %} + + {% for field in form.visible_fields %} + {% if field.html_name != 'consent' %} + {% bootstrap_form_field field %} + {% endif %} + {% endfor %} + + {% if settings.ACCOUNT_TERMS_OF_USE %} + {% include 'account/terms_of_use_consent.html' %} + {% endif %} + + +
+
diff --git a/rdmo/accounts/templates/account/signup_modal_terms_of_use.html b/rdmo/accounts/templates/account/signup_modal_terms_of_use.html deleted file mode 100644 index a0a2d0711a..0000000000 --- a/rdmo/accounts/templates/account/signup_modal_terms_of_use.html +++ /dev/null @@ -1,51 +0,0 @@ -{% load i18n %} - - - - diff --git a/rdmo/accounts/templates/account/terms_of_use.html b/rdmo/accounts/templates/account/terms_of_use.html index fb95b5b564..9f6ec292a9 100644 --- a/rdmo/accounts/templates/account/terms_of_use.html +++ b/rdmo/accounts/templates/account/terms_of_use.html @@ -1,7 +1,7 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} -{% block page %} +{% block main %}

{% trans 'Terms of use' %}

diff --git a/rdmo/accounts/templates/account/terms_of_use_accept.html b/rdmo/accounts/templates/account/terms_of_use_accept.html new file mode 100644 index 0000000000..63c14950e7 --- /dev/null +++ b/rdmo/accounts/templates/account/terms_of_use_accept.html @@ -0,0 +1,32 @@ +{% extends 'account/terms_of_use.html' %} +{% load i18n %} + +{% block main %} + + {{ block.super }} + +
+ {% if not has_consented %} + +
+ {% csrf_token %} + + {% include 'account/terms_of_use_consent.html' %} + + + + {% for error in form.non_field_errors %} +
{{ error }}
+ {% endfor %} +
+ + {% else %} +

+ {% trans "You have already accepted the terms of use." %} +

+ {% endif %} +
+ +{% endblock %} diff --git a/rdmo/accounts/templates/account/terms_of_use_accept_form.html b/rdmo/accounts/templates/account/terms_of_use_accept_form.html deleted file mode 100644 index 6354e0e206..0000000000 --- a/rdmo/accounts/templates/account/terms_of_use_accept_form.html +++ /dev/null @@ -1,46 +0,0 @@ -{% extends 'core/page.html' %} -{% load i18n %} - -{% block page %} - -

{% trans 'Terms of use' %}

- -

- {% get_current_language as lang %} - {% if lang == 'en' %} - {% include 'account/terms_of_use_en.html' %} - {% elif lang == 'de' %} - {% include 'account/terms_of_use_de.html' %} - {% endif %} -

- -
- {% if not has_consented %} -
- {% csrf_token %} -
- -
- -
- {% else %} -

- {% trans "You have accepted the terms of use." %} -

- {% endif %} - - {% if form.non_field_errors %} - - {% endif %} -
- -{% endblock %} diff --git a/rdmo/accounts/templates/account/terms_of_use_consent.html b/rdmo/accounts/templates/account/terms_of_use_consent.html new file mode 100644 index 0000000000..cbe5495927 --- /dev/null +++ b/rdmo/accounts/templates/account/terms_of_use_consent.html @@ -0,0 +1,18 @@ +{% load i18n %} + +
+ + + + + {% if form.consent.errors %} +
+ {% trans 'You need to agree to the terms of use to proceed.' %} +
+ {% endif %} +
diff --git a/rdmo/accounts/templates/account/terms_of_use_modal.html b/rdmo/accounts/templates/account/terms_of_use_modal.html new file mode 100644 index 0000000000..81dc490e4c --- /dev/null +++ b/rdmo/accounts/templates/account/terms_of_use_modal.html @@ -0,0 +1,50 @@ +{% load i18n %} + + + + diff --git a/rdmo/accounts/templates/account/verification_sent.html b/rdmo/accounts/templates/account/verification_sent.html index 404aa95516..061adedfaa 100644 --- a/rdmo/accounts/templates/account/verification_sent.html +++ b/rdmo/accounts/templates/account/verification_sent.html @@ -1,7 +1,7 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} -{% block page %} +{% block main %}

{% trans "Verify your e-mail address" %} diff --git a/rdmo/accounts/templates/account/verified_email_required.html b/rdmo/accounts/templates/account/verified_email_required.html index d011aedd4d..a9146d0172 100644 --- a/rdmo/accounts/templates/account/verified_email_required.html +++ b/rdmo/accounts/templates/account/verified_email_required.html @@ -1,4 +1,4 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} {% block content %} diff --git a/rdmo/accounts/templates/profile/profile_remove_closed.html b/rdmo/accounts/templates/profile/profile_remove_closed.html index d9d9ab0a4d..74fb480a83 100644 --- a/rdmo/accounts/templates/profile/profile_remove_closed.html +++ b/rdmo/accounts/templates/profile/profile_remove_closed.html @@ -1,9 +1,9 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} -{% block page %} +{% block main %} -

{% trans "Delete profile" %}

+

{% trans "Delete profile" %}

{% trans "We are sorry, but you cannot remove your profile here." %} diff --git a/rdmo/accounts/templates/profile/profile_remove_failed.html b/rdmo/accounts/templates/profile/profile_remove_failed.html index d9fe5fbd93..a27e753834 100644 --- a/rdmo/accounts/templates/profile/profile_remove_failed.html +++ b/rdmo/accounts/templates/profile/profile_remove_failed.html @@ -1,9 +1,9 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} -{% block page %} +{% block main %} -

{% trans "Delete profile" %}

+

{% trans "Delete profile" %}

{% trans "Profile removal failed. Please make sure that you enter the correct data." %} diff --git a/rdmo/accounts/templates/profile/profile_remove_form.html b/rdmo/accounts/templates/profile/profile_remove_form.html index 815abcddf7..955e8e3b2e 100644 --- a/rdmo/accounts/templates/profile/profile_remove_form.html +++ b/rdmo/accounts/templates/profile/profile_remove_form.html @@ -1,8 +1,8 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} {% load core_tags %} -{% block page %} +{% block main %}

{% trans "Delete profile" %}

@@ -12,10 +12,13 @@

{% trans "Delete profile" %}

{% csrf_token %} - {% include 'core/bootstrap_form_fields.html' %} + {% for field in form.visible_fields %} + {% bootstrap_form_field field %} + {% endfor %} - - + {% endblock %} diff --git a/rdmo/accounts/templates/profile/profile_remove_success.html b/rdmo/accounts/templates/profile/profile_remove_success.html index 073c6e7ca2..77d7c827e6 100644 --- a/rdmo/accounts/templates/profile/profile_remove_success.html +++ b/rdmo/accounts/templates/profile/profile_remove_success.html @@ -1,9 +1,9 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} -{% block page %} +{% block main %} -

{% trans "Delete profile" %}

+

{% trans "Delete profile" %}

{% trans "Your profile was successfully removed." %} diff --git a/rdmo/accounts/templates/profile/profile_update_closed.html b/rdmo/accounts/templates/profile/profile_update_closed.html index 60766b0c53..26c09b46a2 100644 --- a/rdmo/accounts/templates/profile/profile_update_closed.html +++ b/rdmo/accounts/templates/profile/profile_update_closed.html @@ -1,9 +1,9 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} -{% block page %} +{% block main %} -

{% trans "Profile update" %}

+

{% trans "Profile update" %}

{% trans "We are sorry, but you cannot update your profile here." %} diff --git a/rdmo/accounts/templates/profile/profile_update_form.html b/rdmo/accounts/templates/profile/profile_update_form.html index 437e6aa4c9..f1f0623495 100644 --- a/rdmo/accounts/templates/profile/profile_update_form.html +++ b/rdmo/accounts/templates/profile/profile_update_form.html @@ -1,7 +1,8 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} +{% load core_tags %} -{% block page %} +{% block main %}

{% trans "Update profile" %}

@@ -15,14 +16,17 @@

{% trans "Update profile" %}

{% endif %} -
+ {% csrf_token %} - {% include 'core/bootstrap_form_fields.html' %} + {% for field in form.visible_fields %} + {% bootstrap_form_field field %} + {% endfor %} - - +
{% if settings.PROFILE_DELETE %} diff --git a/rdmo/accounts/templates/socialaccount/authentication_error.html b/rdmo/accounts/templates/socialaccount/authentication_error.html index c826248f8e..9611cf6b76 100644 --- a/rdmo/accounts/templates/socialaccount/authentication_error.html +++ b/rdmo/accounts/templates/socialaccount/authentication_error.html @@ -1,10 +1,10 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} {% load account %} {% load socialaccount %} {% load core_tags %} -{% block page %} +{% block main %}

{% trans "Social Network Login Failure" %}

diff --git a/rdmo/accounts/templates/socialaccount/connections.html b/rdmo/accounts/templates/socialaccount/connections.html index 1f39855b24..a839348c54 100644 --- a/rdmo/accounts/templates/socialaccount/connections.html +++ b/rdmo/accounts/templates/socialaccount/connections.html @@ -1,78 +1,76 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} -{% load static %} -{% load accounts_tags %} -{% block page %} +{% block main %}

{% trans "Account connections" %}

-

{% trans 'Current connections' %}

+
- {% if form.accounts %} +

{% trans 'Current connections' %}

-

- {% blocktrans trimmed %} - You can sign in to your account using any of the following third party accounts: - {% endblocktrans %} -

+ {% if form.accounts %} -
- {% csrf_token %} +

+ {% blocktrans trimmed %} + You can sign in to your account using any of the following third party accounts: + {% endblocktrans %} +

-
- {% if form.non_field_errors %} -
{{ form.non_field_errors }}
- {% endif %} + + {% csrf_token %} {% for base_account in form.accounts %} {% with base_account.get_provider_account as account %} - +
+ + + + + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ +
{% endwith %} {% endfor %} -
- -
-
-
+ + + + {% else %} + +

+ {% trans 'You currently have no social network accounts connected to this account.' %} +

- + {% endif %} - {% else %} +
-

- {% trans 'You currently have no social network accounts connected to this account.' %} -

+
- {% endif %} +

{% trans 'Add an additional account' %}

- {% get_inactive_providers as inactive_providers %} - {% if inactive_providers %} -

{% trans 'Add an additional account' %}

+
+ {% include "socialaccount/snippets/provider_list.html" with process="connect" %} +
- - {% endif %} + {% include "socialaccount/snippets/login_extra.html" %} - {% include "socialaccount/snippets/login_extra.html" %} +
{% endblock %} diff --git a/rdmo/accounts/templates/socialaccount/login.html b/rdmo/accounts/templates/socialaccount/login.html index 6fb731a8c5..d98550de2b 100644 --- a/rdmo/accounts/templates/socialaccount/login.html +++ b/rdmo/accounts/templates/socialaccount/login.html @@ -1,21 +1,45 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} -{% block head_title %}{% trans "Sign In" %}{% endblock %} +{% block head_title %} +{% trans "Sign In" %} +{% endblock %} + +{% block main %} -{% block page %} {% if process == "connect" %} -

{% blocktrans with provider.name as provider %}Connect {{ provider }}{% endblocktrans %}

-

{% blocktrans with provider.name as provider %}You are about to connect a new third party account from {{ provider }}.{% endblocktrans %}

+

+ {% blocktrans with provider.name as provider %} + Connect {{ provider }} + {% endblocktrans %} +

+ +

+ {% blocktrans with provider.name as provider %} + You are about to connect a new third party account from {{ provider }}. + {% endblocktrans %} +

+ {% else %} -

{% blocktrans with provider.name as provider %}Sign In Via {{ provider }}{% endblocktrans %}

-

{% blocktrans with provider.name as provider %}You are about to sign in using a third party account from {{ provider }}.{% endblocktrans %}

+

+ {% blocktrans with provider.name as provider %} + Sign In Via {{ provider }} + {% endblocktrans %} +

+ +

+ {% blocktrans with provider.name as provider %} + You are about to sign in using a third party account from {{ provider }}. + {% endblocktrans %} +

+ {% endif %}
{% csrf_token %}
+ {% endblock %} diff --git a/rdmo/accounts/templates/socialaccount/login_cancelled.html b/rdmo/accounts/templates/socialaccount/login_cancelled.html index 768986d2f4..0434a6e107 100644 --- a/rdmo/accounts/templates/socialaccount/login_cancelled.html +++ b/rdmo/accounts/templates/socialaccount/login_cancelled.html @@ -1,10 +1,10 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} {% load account %} {% load socialaccount %} {% load core_tags %} -{% block page %} +{% block main %}

{% trans "Login Cancelled" %}

diff --git a/rdmo/accounts/templates/socialaccount/signup.html b/rdmo/accounts/templates/socialaccount/signup.html index 3eccb49c7d..b1ee1866a2 100644 --- a/rdmo/accounts/templates/socialaccount/signup.html +++ b/rdmo/accounts/templates/socialaccount/signup.html @@ -1,59 +1,20 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/main.html' %} {% load i18n %} -{% block page %} +{% block main %} -

{% trans "Create a new account" %}

+

{% trans "Create a new account" %}

{% blocktrans trimmed with provider_name=account.get_provider.name site_name=site.name %} - You are about to use your {{provider_name}} account to login to {{site_name}}. As a final step, please complete the following form:{% endblocktrans %} + You are about to use your {{provider_name}} account to login to {{site_name}}. + As a final step, please complete the following form:{% endblocktrans %}

-
- {% csrf_token %} - - {% if redirect_field_value %} - - {% endif %} - - {% for field in form.visible_fields %} - - {% if field.html_name != 'consent' %} - {% include 'core/bootstrap_form_field.html' with field=field %} - {% endif %} - - {% endfor %} - {% if settings.ACCOUNT_TERMS_OF_USE %} - {% with field=form.consent %} -
-
- -
-
- - {% if field.errors %} -
-

{% trans 'You need to agree to the terms of use to proceed.' %}

-
- {% endif %} - {% endwith %} - {% endif %} - - -
- - + {% include 'account/signup_form.html' %} {% if settings.ACCOUNT_TERMS_OF_USE %} - {% include 'account/signup_modal_terms_of_use.html' %} + {% include 'account/terms_of_use_modal.html' %} {% endif %} {% endblock %} diff --git a/rdmo/accounts/templates/socialaccount/snippets/provider_button_generic.html b/rdmo/accounts/templates/socialaccount/snippets/provider_button_generic.html new file mode 100644 index 0000000000..f484637a29 --- /dev/null +++ b/rdmo/accounts/templates/socialaccount/snippets/provider_button_generic.html @@ -0,0 +1,9 @@ +{% load socialaccount %} +{% load accounts_tags %} + + + + {% sign_in_text provider.name %} + diff --git a/rdmo/accounts/templates/socialaccount/snippets/provider_button_openid.html b/rdmo/accounts/templates/socialaccount/snippets/provider_button_openid.html new file mode 100644 index 0000000000..542fda56b3 --- /dev/null +++ b/rdmo/accounts/templates/socialaccount/snippets/provider_button_openid.html @@ -0,0 +1,7 @@ +{% for brand in provider.get_brands %} + + {{ brand.name }} + +{% endfor %} diff --git a/rdmo/accounts/templates/socialaccount/snippets/provider_button_openid_connect.html b/rdmo/accounts/templates/socialaccount/snippets/provider_button_openid_connect.html new file mode 100644 index 0000000000..6c4a95eb3d --- /dev/null +++ b/rdmo/accounts/templates/socialaccount/snippets/provider_button_openid_connect.html @@ -0,0 +1,29 @@ +{% load i18n %} +{% load static %} +{% load socialaccount %} +{% load accounts_tags %} + + + +
+ {% if provider.app.settings.login_logo %} + + {% else %} + + {% endif %} + + {% if provider.app.settings.login_text %} + + {{ provider.app.settings.login_text }} + + {% elif provider.app.settings.login_text == False %} + {# Do not display a text on the button #} + {% else %} + {% sign_in_text provider.app.name %} + {% endif %} +
+
diff --git a/rdmo/accounts/templates/socialaccount/snippets/provider_button_orcid.html b/rdmo/accounts/templates/socialaccount/snippets/provider_button_orcid.html new file mode 100644 index 0000000000..32f5b58f01 --- /dev/null +++ b/rdmo/accounts/templates/socialaccount/snippets/provider_button_orcid.html @@ -0,0 +1,12 @@ +{% load static %} +{% load socialaccount %} +{% load accounts_tags %} + + +
+ + {% sign_in_text 'ORCID' %} +
+
diff --git a/rdmo/accounts/templates/socialaccount/snippets/provider_list.html b/rdmo/accounts/templates/socialaccount/snippets/provider_list.html index c19a603bf9..2a98770be6 100644 --- a/rdmo/accounts/templates/socialaccount/snippets/provider_list.html +++ b/rdmo/accounts/templates/socialaccount/snippets/provider_list.html @@ -1,64 +1,30 @@ {% load socialaccount %} -{% load static %} +{% load accounts_tags %} -{% if not socialaccount_providers %} {% get_providers as socialaccount_providers %} -{% endif %} +{% get_current_provider_ids as current_providers %} {% for provider in socialaccount_providers %} -{% if provider.id == "openid" %} -{% for brand in provider.get_brands %} -
  • - - {{brand.name}} - -
  • -{% endfor %} -{% endif %} +{% if provider.id not in current_providers %} -{% if provider.id == 'orcid' %} + {% if provider.id == "openid" %} -
  • - - ORCID sign in - -
  • + {% include 'socialaccount/snippets/provider_button_openid.html' %} -{% elif provider.id == 'openid_connect' %} + {% elif provider.id == 'openid_connect' %} -{% if provider.app.provider_id == 'keycloak' %} -
  • - - Keycloak sign in - -
  • -{% else %} -
  • - - {{ provider.name }} - -
  • -{% endif %} + {% include 'socialaccount/snippets/provider_button_openid_connect.html' %} + + {% elif provider.id == 'orcid' %} + + {% include 'socialaccount/snippets/provider_button_orcid.html' %} + {% else %} -{% else %} + {% include 'socialaccount/snippets/provider_button_generic.html' %} -
  • - - {% if provider.id == 'dummy' %} - - {% else %} - - {% endif %} - -
  • + {% endif %} {% endif %} diff --git a/rdmo/accounts/templatetags/accounts_tags.py b/rdmo/accounts/templatetags/accounts_tags.py index ac8895b632..7d77dbd0b9 100644 --- a/rdmo/accounts/templatetags/accounts_tags.py +++ b/rdmo/accounts/templatetags/accounts_tags.py @@ -39,3 +39,20 @@ def get_inactive_providers(context=None): for provider in get_providers(context) if provider.id not in providers ] + + +@register.simple_tag(takes_context=True) +def get_current_provider_ids(context=None): + + context = context or {} + + if 'form' in context: + try: + return [account.provider for account in context['form'].accounts] + except (KeyError, AttributeError): + return [] + + +@register.simple_tag() +def sign_in_text(provider_name): + return _('Sign in with %s') % provider_name diff --git a/rdmo/accounts/tests/test_models.py b/rdmo/accounts/tests/test_models.py index 5c63ad7af2..4b4546ac59 100644 --- a/rdmo/accounts/tests/test_models.py +++ b/rdmo/accounts/tests/test_models.py @@ -16,6 +16,9 @@ ('example-reviewer', 'example-reviewer', 'example-reviewer@example.com'), ) +site_managers = ( + ('site', 'site', 'site@example.com'), +) @pytest.mark.parametrize('username,password,email', normal_users) @@ -31,6 +34,7 @@ def test_is_site_editor_returns_true_for_site_managers(db, client, username, pas user = get_user_model().objects.get(username=username, email=email) assert user.role.is_editor is True + @pytest.mark.parametrize('username,password,email', normal_users) def test_is_site_reviewer_returns_false_for_normal_users(db, client, username, password, email): client.login(username=username, password=password) @@ -43,3 +47,17 @@ def test_is_site_reviewer_returns_true_for_site_managers(db, client, username, p client.login(username=username, password=password) user = get_user_model().objects.get(username=username, email=email) assert user.role.is_reviewer is True + + +@pytest.mark.parametrize('username,password,email', normal_users) +def test_is_site_manager_returns_false_for_normal_users(db, client, username, password, email): + client.login(username=username, password=password) + user = get_user_model().objects.get(username=username, email=email) + assert user.role.is_site_manager is False + + +@pytest.mark.parametrize('username,password,email', site_managers) +def test_is_site_manager_returns_true_for_site_managers(db, client, username, password, email): + client.login(username=username, password=password) + user = get_user_model().objects.get(username=username, email=email) + assert user.role.is_site_manager is True diff --git a/rdmo/accounts/tests/test_utils.py b/rdmo/accounts/tests/test_utils.py index 08a7c51220..362678901c 100644 --- a/rdmo/accounts/tests/test_utils.py +++ b/rdmo/accounts/tests/test_utils.py @@ -1,10 +1,8 @@ import pytest from django.contrib.auth import get_user_model -from django.contrib.auth.models import AnonymousUser -from rdmo.accounts.models import Role -from rdmo.accounts.utils import delete_user, get_full_name, get_user_from_db_or_none, is_site_manager +from rdmo.accounts.utils import delete_user, get_full_name, get_user_from_db_or_none normal_users = ( ('user', 'user', 'user@example.com'), @@ -31,29 +29,6 @@ def test_get_full_name_returns_username(db, username, password, email): assert get_full_name(user) == username -def test_is_site_manager_returns_true_for_superuser(admin_user): - assert is_site_manager(admin_user) is True - - -def test_is_site_manager_returns_false_for_not_authenticated_user(): - assert is_site_manager(AnonymousUser()) is False - - -@pytest.mark.parametrize('username,password,email', site_managers) -def test_is_site_manager_returns_true_for_site_managers(db, client, username, password, email): - client.login(username=username, password=password) - user = get_user_model().objects.get(username=username, email=email) - assert is_site_manager(user) is True - - -@pytest.mark.parametrize('username,password,email', site_managers) -def test_is_site_manager_returns_false_when_role_doesnotexist_(db, client, username, password, email): - client.login(username=username, password=password) - Role.objects.all().delete() - user = get_user_model().objects.get(username=username, email=email) - assert is_site_manager(user) is False - - @pytest.mark.parametrize('username,password,email', users) def test_delete_user(db, username, password, email): user = get_user_model().objects.get(username=username, email=email) diff --git a/rdmo/accounts/tests/test_views.py b/rdmo/accounts/tests/test_views.py index 4393fab7b1..f8cd88cdd2 100644 --- a/rdmo/accounts/tests/test_views.py +++ b/rdmo/accounts/tests/test_views.py @@ -370,23 +370,6 @@ def test_remove_user_post(db, client, settings, django_user_model, profile_delet assert django_user_model.objects.get(username='user') -@pytest.mark.parametrize('profile_update', boolean_toggle) -def test_remove_user_post_cancelled(db, client, settings, django_user_model, profile_update): - settings.PROFILE_UPDATE = profile_update - settings.PROFILE_DELETE = True - - client.login(username='user', password='user') - url = reverse('profile_remove') - response = client.post(url, {'cancel': 'cancel'}) - - assert response.status_code == 302 - assert django_user_model.objects.filter(username='user').exists() - if settings.PROFILE_UPDATE: - assert response.url == '/account' - else: - assert response.url == '/' - - @pytest.mark.parametrize('profile_delete', boolean_toggle) def test_remove_user_post_invalid_email(db, client, settings, django_user_model, profile_delete): settings.PROFILE_DELETE = profile_delete @@ -841,9 +824,7 @@ def test_terms_of_use_middleware_redirect_and_accept( assert response.status_code == 200 -def test_terms_of_use_middleware_invalidate_terms_version( - db, client, settings, django_user_model, enable_terms_of_use # noqa: F811 - ): +def test_terms_of_use_middleware_invalidate_terms_version(db, client, settings, django_user_model, enable_terms_of_use): # noqa: F811 # Arrange constants, settings and user past_datetime = (datetime.now() - timedelta(days=10)).strftime(format="%Y-%m-%d") future_datetime = (datetime.now() + timedelta(days=10)).strftime(format="%Y-%m-%d") diff --git a/rdmo/accounts/urls/__init__.py b/rdmo/accounts/urls/__init__.py index 8e43759928..daf4e77fcc 100644 --- a/rdmo/accounts/urls/__init__.py +++ b/rdmo/accounts/urls/__init__.py @@ -15,7 +15,7 @@ urlpatterns = [ # edit own profile re_path(r'^$', profile_update, name='profile_update'), - re_path('^remove', remove_user, name='profile_remove'), + re_path('^remove/', remove_user, name='profile_remove'), ] if settings.ACCOUNT_TERMS_OF_USE: diff --git a/rdmo/accounts/utils.py b/rdmo/accounts/utils.py index 6ba04d3e50..75348fa65b 100644 --- a/rdmo/accounts/utils.py +++ b/rdmo/accounts/utils.py @@ -1,12 +1,10 @@ import logging -from django.conf import settings from django.contrib.auth import authenticate, get_user_model from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist -from .models import Role from .settings import GROUPS log = logging.getLogger(__name__) @@ -19,19 +17,6 @@ def get_full_name(user) -> str: return user.username -def is_site_manager(user): - if user.is_authenticated: - if user.is_superuser: - return True - else: - try: - return user.role.manager.filter(pk=settings.SITE_ID).exists() - except Role.DoesNotExist: - return False - else: - return False - - def set_group_permissions(): for name, permissions in GROUPS: group = Group.objects.get(name=name) diff --git a/rdmo/accounts/views.py b/rdmo/accounts/views.py index 9f1f0728d3..011f5b2da3 100644 --- a/rdmo/accounts/views.py +++ b/rdmo/accounts/views.py @@ -4,7 +4,7 @@ from django.conf import settings from django.contrib.auth import logout from django.contrib.auth.decorators import login_required -from django.http import HttpResponseNotAllowed, HttpResponseRedirect +from django.http import HttpResponseRedirect from django.shortcuts import redirect, render from django.urls import reverse @@ -26,14 +26,9 @@ def profile_update(request): form = ProfileForm(request.POST or None, instance=request.user) - if request.method == 'POST': - if 'cancel' in request.POST: - log.debug('User %s update cancelled', request.user.username) - return HttpResponseRedirect(get_next(request)) - - if form.is_valid(): - form.save() - return HttpResponseRedirect(get_next(request)) + if request.method == 'POST' and form.is_valid(): + form.save() + return HttpResponseRedirect(get_next(request)) return render(request, 'profile/profile_update_form.html', { 'form': form, @@ -45,22 +40,12 @@ def profile_update(request): @login_required() def remove_user(request): - if not settings.PROFILE_DELETE: - log.info('Remove user form is disabled in settings PROFILE_DELETE') - return render(request, 'profile/profile_remove_closed.html') - form = RemoveForm(request.POST or None, request=request) - log.debug('Remove user form initialized for "%s"', request.user.username) - - if request.method == 'POST': - if 'cancel' in request.POST: - log.info('User %s removal cancelled', str(request.user)) + if settings.PROFILE_DELETE: + log.debug('Remove user %s', request.user.username) - if settings.PROFILE_UPDATE: - return HttpResponseRedirect('/account') - else: - return HttpResponseRedirect('/') + form = RemoveForm(request.POST or None, user=request.user) - if form.is_valid(): + if request.method == 'POST' and form.is_valid(): user_is_deleted = delete_user(user=request.user, email=request.POST['email'], password=request.POST.get('password', None)) @@ -72,10 +57,12 @@ def remove_user(request): log.info('Remove user, deletion failed for %s', request.user.username) return render(request, 'profile/profile_remove_failed.html') - return render(request, 'profile/profile_remove_form.html', { - 'form': form, - 'next': get_referer_path_info(request, default='/') - }) + return render(request, 'profile/profile_remove_form.html', { + 'form': form, + 'next': get_referer_path_info(request, default='/') + }) + else: + return render(request, 'profile/profile_remove_closed.html') def terms_of_use(request): @@ -113,30 +100,19 @@ def shibboleth_logout(request): def terms_of_use_accept(request): - if not request.user.is_authenticated: return redirect("account_login") + # Use the form to handle both update and delete actions + form = AcceptConsentForm(request.POST or None, user=request.user) + if request.method == "POST": - # Use the form to handle both update and delete actions - form = AcceptConsentForm(request.POST, user=request.user) if form.is_valid(): consent_saved = form.save(request.session) # saves the consent and sets the session key if consent_saved: return redirect("home") - # If consent was not saved, re-render the form with an error - return render(request, - "account/terms_of_use_accept_form.html", - {"form": form}, - ) - - elif request.method == "GET": - has_consented = ConsentFieldValue.objects.filter(user=request.user).exists() - return render( - request, - "account/terms_of_use_accept_form.html", - {"has_consented": has_consented}, - ) - - return HttpResponseNotAllowed(["GET", "POST"]) + return render(request, "account/terms_of_use_accept.html", { + "form": form, + "has_consented": ConsentFieldValue.objects.filter(user=request.user).exists() + }) diff --git a/rdmo/accounts/viewsets.py b/rdmo/accounts/viewsets.py index 09604f146f..da01040c96 100644 --- a/rdmo/accounts/viewsets.py +++ b/rdmo/accounts/viewsets.py @@ -10,7 +10,6 @@ from rdmo.core.permissions import HasModelPermission, HasObjectPermission from .serializers.v1 import UserSerializer -from .utils import is_site_manager class UserViewSetMixin: @@ -19,7 +18,7 @@ def get_users_for_user(self, user): if user.is_authenticated: if user.has_perm('auth.view_user'): return get_user_model().objects.all() - elif is_site_manager(user): + elif user.role.is_site_manager: return get_user_model().objects.filter(role__member__id__in=user.role.manager.all()).distinct() return get_user_model().objects.none() diff --git a/rdmo/core/assets/js/_bs53/app.js b/rdmo/core/assets/js/_bs53/app.js new file mode 100644 index 0000000000..e2204695ae --- /dev/null +++ b/rdmo/core/assets/js/_bs53/app.js @@ -0,0 +1 @@ +// This file produces an empty app.js script file, which can be overloaded in a theme. diff --git a/rdmo/core/assets/js/_bs53/base.js b/rdmo/core/assets/js/_bs53/base.js new file mode 100644 index 0000000000..96c7d6ad77 --- /dev/null +++ b/rdmo/core/assets/js/_bs53/base.js @@ -0,0 +1,14 @@ +window.loopHeaderBackgroundImage = (timeout) => { + const images = document.querySelectorAll('#header .header-image') + + let index = 0 + + const setHeaderBackgroundImage = () => { + images[index].classList.remove('header-image-visible') + index = (index == images.length - 1) ? 0 : index + 1 + images[index].classList.toggle('header-image-visible') + setTimeout(() => setHeaderBackgroundImage(), timeout) + } + + setTimeout(() => setHeaderBackgroundImage(), timeout) +} diff --git a/rdmo/core/assets/js/_bs53/bootstrap.js b/rdmo/core/assets/js/_bs53/bootstrap.js new file mode 100644 index 0000000000..ae9264d525 --- /dev/null +++ b/rdmo/core/assets/js/_bs53/bootstrap.js @@ -0,0 +1,2 @@ +import 'bootstrap' +window.bootstrap = require('bootstrap') diff --git a/rdmo/core/assets/js/_bs53/components/Modal.js b/rdmo/core/assets/js/_bs53/components/Modal.js new file mode 100644 index 0000000000..8f7b89d3b6 --- /dev/null +++ b/rdmo/core/assets/js/_bs53/components/Modal.js @@ -0,0 +1,86 @@ +import React, { useEffect, useRef } from 'react' +import PropTypes from 'prop-types' +import { Modal as BootstrapModal } from 'bootstrap' + +const Modal = ({ title, show, onClose, onSubmit, submitLabel, submitProps, children, modalProps = {}, size = '' }) => { + const modalRef = useRef(null) + + useEffect(() => { + const modalElement = modalRef.current + if (!modalElement) return + + const modal = BootstrapModal.getOrCreateInstance(modalElement, { + backdrop: 'static', + keyboard: true, + ...modalProps + }) + + const handleHide = () => { + if (show) { + onClose() + } + } + + modalElement.addEventListener('hide.bs.modal', handleHide) + + if (show) { + modal.show() + } + + return () => { + modalElement.removeEventListener('hide.bs.modal', handleHide) + modal.hide() + } + }, [show]) + + return ( +
    +
    +
    +
    +

    {title}

    + +
    + + { + children && ( +
    + {children} +
    + ) + } + +
    + { + onSubmit && ( + + ) + } + +
    +
    +
    +
    + ) +} + +Modal.propTypes = { + title: PropTypes.string.isRequired, + show: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + onSubmit: PropTypes.func, + submitLabel: PropTypes.string, + submitProps: PropTypes.object, + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node + ]), + modalProps: PropTypes.object, + size: PropTypes.string +} + +export default Modal diff --git a/rdmo/core/assets/js/_bs53/components/Tooltip.js b/rdmo/core/assets/js/_bs53/components/Tooltip.js new file mode 100644 index 0000000000..58aa80362e --- /dev/null +++ b/rdmo/core/assets/js/_bs53/components/Tooltip.js @@ -0,0 +1,33 @@ +import React, { useEffect, useRef } from 'react' +import PropTypes from 'prop-types' +import { renderToString } from 'react-dom/server' +import { Tooltip as BootstrapTooltip } from 'bootstrap' + +const Tooltip = ({ title, children, placement = 'bottom', tooltipProps = {} }) => { + const ref = useRef(null) + + useEffect(() => { + if (title) { + // console.log(renderToString(title)) + const t = new BootstrapTooltip(ref.current, { + title: renderToString(title), + placement, + html: true, + delay: 200, + ...tooltipProps + }) + return () => t.dispose() + } + }, [title]) + + return React.cloneElement(children, { ref }) +} + +Tooltip.propTypes = { + title: PropTypes.node.isRequired, + children: PropTypes.node.isRequired, + placement: PropTypes.string, + tooltipProps: PropTypes.object, +} + +export default Tooltip diff --git a/rdmo/core/assets/js/_bs53/components/index.js b/rdmo/core/assets/js/_bs53/components/index.js new file mode 100644 index 0000000000..8306153fc5 --- /dev/null +++ b/rdmo/core/assets/js/_bs53/components/index.js @@ -0,0 +1,2 @@ +export { default as Modal } from './Modal' +export { default as Tooltip } from './Tooltip' diff --git a/rdmo/core/assets/js/actions/actionTypes.js b/rdmo/core/assets/js/actions/actionTypes.js index 1f97c54f3d..16cc7203d5 100644 --- a/rdmo/core/assets/js/actions/actionTypes.js +++ b/rdmo/core/assets/js/actions/actionTypes.js @@ -15,3 +15,7 @@ export const FETCH_TEMPLATES_SUCCESS = 'FETCH_TEMPLATES_SUCCESS' export const FETCH_CURRENT_USER_ERROR = 'FETCH_CURRENT_USER_ERROR' export const FETCH_CURRENT_USER_INIT = 'FETCH_CURRENT_USER_INIT' export const FETCH_CURRENT_USER_SUCCESS = 'FETCH_CURRENT_USER_SUCCESS' + +export const FETCH_CATALOGS_ERROR = 'FETCH_CATALOGS_ERROR' +export const FETCH_CATALOGS_INIT = 'FETCH_CATALOGS_INIT' +export const FETCH_CATALOGS_SUCCESS = 'FETCH_CATALOGS_SUCCESS' diff --git a/rdmo/core/assets/js/actions/configActions.js b/rdmo/core/assets/js/actions/configActions.js index aefed9f305..b5f5fec0f8 100644 --- a/rdmo/core/assets/js/actions/configActions.js +++ b/rdmo/core/assets/js/actions/configActions.js @@ -1,4 +1,4 @@ -import { UPDATE_CONFIG, DELETE_CONFIG } from './actionTypes' +import { DELETE_CONFIG, UPDATE_CONFIG } from './actionTypes' export function updateConfig(path, value, ls = true) { return {type: UPDATE_CONFIG, path, value, ls} diff --git a/rdmo/core/assets/js/api/BaseApi.js b/rdmo/core/assets/js/api/BaseApi.js index df53e94b77..5bab58cbd4 100644 --- a/rdmo/core/assets/js/api/BaseApi.js +++ b/rdmo/core/assets/js/api/BaseApi.js @@ -168,6 +168,37 @@ class BaseApi { }) } + static download(url) { + let filename + + return fetch(baseUrl + url).catch(error => { + throw new ApiError(error.message) + }).then(response => { + if (response.ok) { + const contentDisposition = response.headers.get('Content-Disposition') + filename = contentDisposition?.match(/filename="?([^"]+)"?/)?.[1] ?? 'download' + return response.blob() + } else if (response.status === 400) { + return response.json().then(errors => { + throw new BadRequestError(response.statusText, response.status, errors) + }) + } else if (response.status === 404) { + return response.json().then(errors => { + throw new NotFoundError(response.statusText, response.status, errors) + }) + } else { + throw new ApiError(response.statusText, response.status) + } + }).then((blob) => { + const objectUrl = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = objectUrl + a.download = filename + a.click() + URL.revokeObjectURL(objectUrl) + }) + } + } export default BaseApi diff --git a/rdmo/core/assets/js/components/UploadDropZone.js b/rdmo/core/assets/js/components/Dropzone.js similarity index 60% rename from rdmo/core/assets/js/components/UploadDropZone.js rename to rdmo/core/assets/js/components/Dropzone.js index 4f8ebf17e2..aef53c600d 100644 --- a/rdmo/core/assets/js/components/UploadDropZone.js +++ b/rdmo/core/assets/js/components/Dropzone.js @@ -1,6 +1,7 @@ import React, { useState } from 'react' import PropTypes from 'prop-types' import { useDropzone } from 'react-dropzone' +import classNames from 'classnames' const UploadDropZone = ({ acceptedTypes, onImportFile }) => { const [errorMessage, setErrorMessage] = useState('') @@ -19,23 +20,21 @@ const UploadDropZone = ({ acceptedTypes, onImportFile }) => { }) return ( -
    -
    - - { - isDragActive ? ( -
    - {gettext('Drop the file here ...')} -
    - ) : ( -
    - {gettext('Drag and drop a file here or click to select a file')} -
    - ) - } - {errorMessage &&
    {errorMessage}
    } -
    -
    +
    + + { + isDragActive ? ( +
    + {gettext('Drop the file here ...')} +
    + ) : ( +
    + {gettext('Drag and drop a file here or click to select a file')} +
    + ) + } + {errorMessage &&
    {errorMessage}
    } +
    ) } diff --git a/rdmo/core/assets/js/components/Html.js b/rdmo/core/assets/js/components/Html.js index 097ec9a624..2b6b690fa6 100644 --- a/rdmo/core/assets/js/components/Html.js +++ b/rdmo/core/assets/js/components/Html.js @@ -1,11 +1,11 @@ -import React, { useRef, useLayoutEffect } from 'react' +import React, { useLayoutEffect, useRef } from 'react' import PropTypes from 'prop-types' import { isEmpty } from 'lodash' import { executeScriptTags } from 'rdmo/core/assets/js/utils/meta' -const Html = ({ id = null, html = '' }) => { +const Html = ({ id = null, className = '', html = '' }) => { const ref = useRef() // if html contains a {% endblock %} - {% block head %}{% endblock %} + {% block head %} + {% endblock %} diff --git a/rdmo/core/templates/core/base_navigation.html b/rdmo/core/templates/core/base_navigation.html index a29e63f15a..a4116cfdcc 100644 --- a/rdmo/core/templates/core/base_navigation.html +++ b/rdmo/core/templates/core/base_navigation.html @@ -105,7 +105,10 @@ {% if settings.ACCOUNT or settings.SOCIALACCOUNT %}
  • - {% include 'account/logout_form.html' %} +
    + {% csrf_token %} + +
  • {% else %}
  • diff --git a/rdmo/core/templates/core/base_navigation_account.html b/rdmo/core/templates/core/base_navigation_account.html index f56897a132..8959e11434 100644 --- a/rdmo/core/templates/core/base_navigation_account.html +++ b/rdmo/core/templates/core/base_navigation_account.html @@ -1,8 +1,12 @@ {% load i18n %} -
  • - {% trans 'Update e-mail' %} +
  • -
  • - {% trans 'Change password' %} +
  • diff --git a/rdmo/core/templates/core/base_navigation_socialaccount.html b/rdmo/core/templates/core/base_navigation_socialaccount.html index 9062432af2..0b081bda8c 100644 --- a/rdmo/core/templates/core/base_navigation_socialaccount.html +++ b/rdmo/core/templates/core/base_navigation_socialaccount.html @@ -1,5 +1,7 @@ {% load i18n %} -
  • - {% trans 'Account connections' %} +
  • diff --git a/rdmo/core/templates/core/bs53/base.html b/rdmo/core/templates/core/bs53/base.html new file mode 100644 index 0000000000..1d80eb9999 --- /dev/null +++ b/rdmo/core/templates/core/bs53/base.html @@ -0,0 +1,36 @@ +{% load static compress core_tags %} + + + {% include 'core/bs53/base_head.html' %} + + {% block css %} + + + + {% endblock %} + + {% block js %} + + + + + {% endblock %} + + {% block head %} + {% endblock %} + + + + +{% include 'core/bs53/base_navigation.html' %} + +
    + {% block content %}{% endblock %} +
    + +{% if not debug %} + {% include 'core/base_analytics.html' %} +{% endif %} + + + diff --git a/rdmo/core/templates/core/bs53/base_head.html b/rdmo/core/templates/core/bs53/base_head.html new file mode 100644 index 0000000000..d5e28ea95e --- /dev/null +++ b/rdmo/core/templates/core/bs53/base_head.html @@ -0,0 +1,18 @@ +{% load static %} +{% load i18n %} +{% get_current_language as LANGUAGE_CODE %} + + {{ request.site.name }} + + + + + + + + + + + + + diff --git a/rdmo/core/templates/core/bs53/base_navigation.html b/rdmo/core/templates/core/bs53/base_navigation.html new file mode 100644 index 0000000000..83632ef429 --- /dev/null +++ b/rdmo/core/templates/core/bs53/base_navigation.html @@ -0,0 +1,128 @@ +{% load i18n %} +{% load core_tags %} +{% load accounts_tags %} +{% load rules %} + + diff --git a/rdmo/core/templates/core/bs53/forms/bootstrap_checkbox.html b/rdmo/core/templates/core/bs53/forms/bootstrap_checkbox.html new file mode 100644 index 0000000000..a80744592a --- /dev/null +++ b/rdmo/core/templates/core/bs53/forms/bootstrap_checkbox.html @@ -0,0 +1,12 @@ +
    + + + + + {% for error in field.errors %} +
    {{ error }}
    + {% endfor %} +
    diff --git a/rdmo/core/templates/core/bs53/forms/bootstrap_input.html b/rdmo/core/templates/core/bs53/forms/bootstrap_input.html new file mode 100644 index 0000000000..733ad2d1f2 --- /dev/null +++ b/rdmo/core/templates/core/bs53/forms/bootstrap_input.html @@ -0,0 +1,17 @@ +
    + + + {% if field.help_text %} +

    + {{ field.help_text }} +

    + {% endif %} + + + + {% for error in field.errors %} +
    {{ error }}
    + {% endfor %} +
    diff --git a/rdmo/core/templates/core/bs53/forms/bootstrap_textarea.html b/rdmo/core/templates/core/bs53/forms/bootstrap_textarea.html new file mode 100644 index 0000000000..8f5b140148 --- /dev/null +++ b/rdmo/core/templates/core/bs53/forms/bootstrap_textarea.html @@ -0,0 +1,17 @@ +
    + + + {% if field.help_text %} +

    + {{ field.help_text }} +

    + {% endif %} + + + + {% for error in field.errors %} +
    {{ error }}
    + {% endfor %} +
    diff --git a/rdmo/core/templates/core/bs53/home.html b/rdmo/core/templates/core/bs53/home.html new file mode 100644 index 0000000000..d6aa512b31 --- /dev/null +++ b/rdmo/core/templates/core/bs53/home.html @@ -0,0 +1,14 @@ +{% extends 'core/bs53/base.html' %} +{% load static %} +{% load i18n %} + +{% block content %} + +{% get_current_language as lang %} +{% if lang == 'en' %} + {% include 'core/bs53/home_en.html' %} +{% elif lang == 'de' %} + {% include 'core/bs53/home_de.html' %} +{% endif %} + +{% endblock %} diff --git a/rdmo/core/templates/core/bs53/home_de.html b/rdmo/core/templates/core/bs53/home_de.html new file mode 100644 index 0000000000..20319453f3 --- /dev/null +++ b/rdmo/core/templates/core/bs53/home_de.html @@ -0,0 +1,100 @@ +{% load static %} + + + +
    +
    +
    +
    +
    +

    Lorem ipsum dolor sit amet

    + +

    + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. +

    + +

    + Lorem ipsum dolor sit amet: +

    + +

      +
    • consetetur sadipscing elitr
    • +
    • sed diam nonumy eirmod
    • +
    • et justo duo dolores et ea rebum
    • +
    +
    +
    +
    +
    + +
    +
    +
    +
    +

    Lorem ipsum dolor sit amet, consetetur sadipscing elitr?

    + +

    + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. +

    + + + orem ipsum dolor + +
    +
    +
    +
    + +
    +
    +
    +
    +

    + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. +

    +
    +
    +
    +
    +
    + + \ No newline at end of file diff --git a/rdmo/core/templates/core/bs53/home_en.html b/rdmo/core/templates/core/bs53/home_en.html new file mode 100644 index 0000000000..c095ed9be8 --- /dev/null +++ b/rdmo/core/templates/core/bs53/home_en.html @@ -0,0 +1,100 @@ +{% load static %} + + + +
    +
    +
    +
    +
    +

    Lorem ipsum dolor sit amet

    + +

    + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. +

    + +

    + Lorem ipsum dolor sit amet: +

    + +

      +
    • consetetur sadipscing elitr
    • +
    • sed diam nonumy eirmod
    • +
    • et justo duo dolores et ea rebum
    • +
    +
    +
    +
    +
    + +
    +
    +
    +
    +

    Lorem ipsum dolor sit amet, consetetur sadipscing elitr?

    + +

    + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. +

    + + + orem ipsum dolor + +
    +
    +
    +
    + +
    +
    +
    +
    +

    + Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est. Lorem ipsum dolor sit amet. +

    +
    +
    +
    +
    +
    + + diff --git a/rdmo/core/templates/core/bs53/home_images.html b/rdmo/core/templates/core/bs53/home_images.html new file mode 100644 index 0000000000..5319bdc959 --- /dev/null +++ b/rdmo/core/templates/core/bs53/home_images.html @@ -0,0 +1,13 @@ +{% load static %} +{% load core_tags %} + +{% for image in settings.HOME_IMAGES %} +
    + {{ image.alt }} +

    {{ image.attribution|markdown }}

    +
    +{% endfor %} + + diff --git a/rdmo/core/templates/core/bs53/home_login.html b/rdmo/core/templates/core/bs53/home_login.html new file mode 100644 index 0000000000..713933256a --- /dev/null +++ b/rdmo/core/templates/core/bs53/home_login.html @@ -0,0 +1,15 @@ +
    + {% if settings.LOGIN_FORM %} + {% include 'account/login_form_inline.html' %} + {% endif %} + + {% if settings.SHIBBOLETH %} + {% include 'account/login_shibboleth.html' %} + {% endif %} + + {% if settings.SOCIALACCOUNT %} +
    + {% include "socialaccount/snippets/provider_list.html" with process="login" button_class="btn-light" %} +
    + {% endif %} +
    diff --git a/rdmo/core/templates/core/bs53/main.html b/rdmo/core/templates/core/bs53/main.html new file mode 100644 index 0000000000..82558c023b --- /dev/null +++ b/rdmo/core/templates/core/bs53/main.html @@ -0,0 +1,11 @@ +{% extends 'core/bs53/base.html' %} + +{% block content %} + +
    +
    + {% block main %}{% endblock %} +
    +
    + +{% endblock %} diff --git a/rdmo/core/templates/core/bs53/page.html b/rdmo/core/templates/core/bs53/page.html new file mode 100644 index 0000000000..0e4f271396 --- /dev/null +++ b/rdmo/core/templates/core/bs53/page.html @@ -0,0 +1,16 @@ +{% extends 'core/bs53/base.html' %} + +{% block content %} + +
    +
    +
    + {% block page %}{% endblock %} +
    + +
    +
    + +{% endblock %} diff --git a/rdmo/core/templatetags/core_tags.py b/rdmo/core/templatetags/core_tags.py index 530c5fb157..495b802760 100644 --- a/rdmo/core/templatetags/core_tags.py +++ b/rdmo/core/templatetags/core_tags.py @@ -22,9 +22,9 @@ def i18n_switcher(): for language, language_string in settings.LANGUAGES: url = reverse('i18n_switcher', args=[language]) if language == translation.get_language(): - string += f"
  • {language_string}
  • " + string += f"
  • {language_string}
  • " else: - string += f"
  • {language_string}
  • " + string += f"
  • {language_string}
  • " return mark_safe(string) @@ -50,6 +50,18 @@ def render_lang_template(template_name, escape_html=False): return '' +@register.simple_tag() +def bootstrap_form_field(field, **kwargs): + context = { + 'field': field + } + + if field.widget_type in ['text', 'email', 'password']: + return render_to_string('core/bs53/forms/bootstrap_input.html', context) + else: + return render_to_string(f'core/bs53/forms/bootstrap_{field.widget_type}.html', context) + + @register.simple_tag(takes_context=True) def bootstrap_form(context, **kwargs): form_context = {} diff --git a/rdmo/core/tests/test_openapi.py b/rdmo/core/tests/test_openapi.py index a12f319eff..6b26055b89 100644 --- a/rdmo/core/tests/test_openapi.py +++ b/rdmo/core/tests/test_openapi.py @@ -10,6 +10,7 @@ 'anonymous' ) +n_path = 137 @pytest.mark.parametrize('username', users) def test_openapi_schema(db, client, login, settings, username): @@ -21,8 +22,7 @@ def test_openapi_schema(db, client, login, settings, username): assert response.status_code == 200 schema = yaml.safe_load(response.content) assert schema['openapi'] == '3.0.3' - assert len(schema['paths']) > 120 - assert len(schema['paths']) < 140 + assert len(schema['paths']) == 140 else: assert response.status_code == 302 diff --git a/rdmo/core/tests/test_package_status.py b/rdmo/core/tests/test_package_status.py index 2bd6ca4bf0..958c51a55e 100644 --- a/rdmo/core/tests/test_package_status.py +++ b/rdmo/core/tests/test_package_status.py @@ -1,58 +1,36 @@ import json -import pytest - from django.core.management import call_command import yaml from packaging.version import Version -def parse_js_version(version_str: str) -> Version: - stripped_version = version_str.lstrip("^~") - return Version(stripped_version) - def test_makemigrations_has_no_changes(db, capsys): call_command("makemigrations", check=True, dry_run=True) captured = capsys.readouterr() assert "No changes detected" in captured.out -@pytest.fixture(scope="session") -def package_json(): - with open("package.json") as f: - return json.load(f) -@pytest.fixture(scope="session") -def pre_commit_config(): +def test_package_json_and_pre_commit_versions_match(): with open(".pre-commit-config.yaml") as f: - return yaml.safe_load(f) - -@pytest.fixture(scope="session") -def package_json_versions(package_json): - versions = { - "eslint": parse_js_version(package_json["devDependencies"]["eslint"]), - "eslint-plugin-react": parse_js_version(package_json["devDependencies"]["eslint-plugin-react"]), - "react": parse_js_version(package_json["dependencies"]["react"]), - } - return versions - -@pytest.fixture(scope="session") -def pre_commit_config_versions(pre_commit_config): - mirrors_eslint = "https://github.com/pre-commit/mirrors-eslint" + pre_commit_config = yaml.safe_load(f) + + with open("package.json") as f: + package_json = json.load(f) + + mirrors_eslint_url = "https://github.com/pre-commit/mirrors-eslint" for repo in pre_commit_config["repos"]: - if repo["repo"] == mirrors_eslint: + if repo["repo"] == mirrors_eslint_url: eslint_config = repo["hooks"][0] break - versions = {} + + pre_commit_config_versions = {} for dependency in sorted(eslint_config["additional_dependencies"]): name, version = dependency.split("@") - versions[name] = parse_js_version(version) - return versions - -def test_package_json_and_pre_commit_versions_match(package_json_versions, pre_commit_config_versions): - eslint_plugin_react_package = package_json_versions.pop('eslint-plugin-react') - eslint_plugin_react_precommit = pre_commit_config_versions.pop('eslint-plugin-react') - assert eslint_plugin_react_package.major == eslint_plugin_react_precommit.major - assert abs(eslint_plugin_react_package.minor - eslint_plugin_react_precommit.minor) < 3 + pre_commit_config_versions[name] = Version(version.lstrip("^~")) - assert package_json_versions == pre_commit_config_versions + for name, version in package_json["devDependencies"].items(): + if name in pre_commit_config_versions: + assert pre_commit_config_versions[name].major == Version(version.strip('^~')).major, name + assert abs(pre_commit_config_versions[name].minor - Version(version.strip('^~')).minor) < 3 diff --git a/rdmo/core/tests/test_tags.py b/rdmo/core/tests/test_tags.py index 0aa74a73fa..e97728aff5 100644 --- a/rdmo/core/tests/test_tags.py +++ b/rdmo/core/tests/test_tags.py @@ -19,6 +19,6 @@ def test_i18n_switcher(rf): rendered_template = Template(template).render(context) for language in settings.LANGUAGES: if language == settings.LANGUAGES[0]: - assert '{}'.format(*language) in rendered_template + assert 'href="/i18n/{}/">{}'.format(*language) in rendered_template else: - assert'{}'.format(*language) in rendered_template + assert 'href="/i18n/{}/">{}'.format(*language) in rendered_template diff --git a/rdmo/core/tests/test_utils.py b/rdmo/core/tests/test_utils.py index 2c446d5192..eae862cbe7 100644 --- a/rdmo/core/tests/test_utils.py +++ b/rdmo/core/tests/test_utils.py @@ -42,7 +42,11 @@ ] invalid_date_strings = [ - ("2025-02-31","day is out of range for month"), + ("2025-02-31", ( + "day is out of range for month", # Python 3.10 + "day 31 must be in range 1..28 for month 2 in year 2025" # Python 3.14 + ) + ), ("2025-17-02", "month must be in 1..12"), ("99/99/9999", "Invalid date format"), ("abcd-ef-gh", "Invalid date format"), @@ -91,11 +95,14 @@ def test_parse_date_from_string_valid_formats(settings, locale, date_string, exp @pytest.mark.parametrize("invalid_date, error_msg", invalid_date_strings) def test_parse_date_from_string_invalid_formats(settings, invalid_date, error_msg): - if not isinstance(invalid_date,str): - with pytest.raises(TypeError, match=error_msg): + patterns = error_msg if isinstance(error_msg, (tuple, list)) else (error_msg,) + match = "|".join(f"(?:{pattern})" for pattern in patterns) + + if not isinstance(invalid_date, str): + with pytest.raises(TypeError, match=match): parse_date_from_string(invalid_date) else: - with pytest.raises(ValueError,match=error_msg): + with pytest.raises(ValueError, match=match): parse_date_from_string(invalid_date) diff --git a/rdmo/core/views.py b/rdmo/core/views.py index f8d9603866..a85c0b812a 100644 --- a/rdmo/core/views.py +++ b/rdmo/core/views.py @@ -33,15 +33,15 @@ def home(request): if settings.LOGIN_FORM: if settings.ACCOUNT or settings.SOCIALACCOUNT: from rdmo.accounts.account import LoginForm - return render(request, 'core/home.html', { + return render(request, 'core/bs53/home.html', { 'form': LoginForm(), 'signup_url': reverse("account_signup") }) else: from django.contrib.auth.forms import AuthenticationForm - return render(request, 'core/home.html', {'form': AuthenticationForm()}) + return render(request, 'core/bs53/home.html', {'form': AuthenticationForm()}) else: - return render(request, 'core/home.html') + return render(request, 'core/bs53/home.html') @login_required diff --git a/rdmo/management/assets/js/actions/actionTypes.js b/rdmo/management/assets/js/actions/actionTypes.js new file mode 100644 index 0000000000..79158ecf28 --- /dev/null +++ b/rdmo/management/assets/js/actions/actionTypes.js @@ -0,0 +1,37 @@ +export const FETCH_ELEMENTS_INIT = 'FETCH_ELEMENTS_INIT' +export const FETCH_ELEMENTS_SUCCESS = 'FETCH_ELEMENTS_SUCCESS' +export const FETCH_ELEMENTS_ERROR = 'FETCH_ELEMENTS_ERROR' + +export const FETCH_ELEMENT_INIT = 'FETCH_ELEMENT_INIT' +export const FETCH_ELEMENT_SUCCESS = 'FETCH_ELEMENT_SUCCESS' +export const FETCH_ELEMENT_ERROR = 'FETCH_ELEMENT_ERROR' + +export const STORE_ELEMENT_INIT = 'STORE_ELEMENT_INIT' +export const STORE_ELEMENT_SUCCESS = 'STORE_ELEMENT_SUCCESS' +export const STORE_ELEMENT_ERROR = 'STORE_ELEMENT_ERROR' + +export const CREATE_ELEMENT_INIT = 'CREATE_ELEMENT_INIT' +export const CREATE_ELEMENT_SUCCESS = 'CREATE_ELEMENT_SUCCESS' +export const CREATE_ELEMENT_ERROR = 'CREATE_ELEMENT_ERROR' + +export const DELETE_ELEMENT_INIT = 'DELETE_ELEMENT_INIT' +export const DELETE_ELEMENT_SUCCESS = 'DELETE_ELEMENT_SUCCESS' +export const DELETE_ELEMENT_ERROR = 'DELETE_ELEMENT_ERROR' + +export const UPDATE_ELEMENT = 'UPDATE_ELEMENT' + +export const UPLOAD_IMPORT_FILE_INIT = 'UPLOAD_FILE_INIT' +export const UPLOAD_IMPORT_FILE_SUCCESS = 'UPLOAD_FILE_SUCCESS' +export const UPLOAD_IMPORT_FILE_ERROR = 'UPLOAD_FILE_ERROR' + +export const IMPORT_ELEMENTS_INIT = 'IMPORT_ELEMENTS_INIT' +export const IMPORT_ELEMENTS_SUCCESS = 'IMPORT_ELEMENTS_SUCCESS' +export const IMPORT_ELEMENTS_ERROR = 'IMPORT_ELEMENTS_ERROR' + +export const UPDATE_IMPORT_ELEMENT = 'UPDATE_IMPORT_ELEMENT' +export const SELECT_IMPORT_ELEMENTS = 'SELECT_IMPORT_ELEMENTS' +export const SELECT_CHANGED_IMPORT_ELEMENTS = 'SELECT_CHANGED_IMPORT_ELEMENTS' +export const SHOW_IMPORT_ELEMENTS = 'SHOW_IMPORT_ELEMENTS' +export const SHOW_CHANGED_IMPORT_ELEMENTS = 'SHOW_CHANGED_IMPORT_ELEMENTS' +export const UPDATE_IMPORT_URI_PREFIX = 'UPDATE_IMPORT_URI_PREFIX' +export const RESET_IMPORT_ELEMENTS = 'RESET_IMPORT_ELEMENTS' diff --git a/rdmo/management/assets/js/actions/elementActions.js b/rdmo/management/assets/js/actions/elementActions.js index 26790ec026..d19ad4d315 100644 --- a/rdmo/management/assets/js/actions/elementActions.js +++ b/rdmo/management/assets/js/actions/elementActions.js @@ -1,16 +1,19 @@ import { get, isNil } from 'lodash' -import { addToPending, removeFromPending } from 'rdmo/core/assets/js/actions/pendingActions' import { updateConfig } from 'rdmo/core/assets/js/actions/configActions' +import { addToPending, removeFromPending } from 'rdmo/core/assets/js/actions/pendingActions' import { siteId } from 'rdmo/core/assets/js/utils/meta' +import { elementTypes } from '../constants/elements' +import { canMoveElement, findDescendants, moveElement, updateWarning } from '../utils/elements' +import { updateLocation } from '../utils/location' + import ConditionsApi from '../api/ConditionsApi' import DomainApi from '../api/DomainApi' import OptionsApi from '../api/OptionsApi' import QuestionsApi from '../api/QuestionsApi' import TasksApi from '../api/TasksApi' import ViewsApi from '../api/ViewsApi' - import ConditionsFactory from '../factories/ConditionsFactory' import DomainFactory from '../factories/DomainFactory' import OptionsFactory from '../factories/OptionsFactory' @@ -18,9 +21,7 @@ import QuestionsFactory from '../factories/QuestionsFactory' import TasksFactory from '../factories/TasksFactory' import ViewsFactory from '../factories/ViewsFactory' -import { elementTypes } from '../constants/elements' -import { updateLocation } from '../utils/location' -import { canMoveElement, findDescendants, moveElement, updateWarning } from '../utils/elements' +import * as actionTypes from './actionTypes' export function fetchElements(elementType) { const pendingId = `fetchElements/${elementType}` @@ -96,20 +97,20 @@ export function fetchElements(elementType) { } export function fetchElementsInit(elementType) { - return {type: 'elements/fetchElementsInit', elementType} + return {type: actionTypes.FETCH_ELEMENTS_INIT, elementType} } export function fetchElementsSuccess(elements) { - return {type: 'elements/fetchElementsSuccess', elements} + return {type: actionTypes.FETCH_ELEMENTS_SUCCESS, elements} } export function fetchElementsError(error) { - return {type: 'elements/fetchElementsError', error} + return {type: actionTypes.FETCH_ELEMENTS_ERROR, error} } // fetch element -export function fetchElement(elementType, elementId, elementAction=null) { +export function fetchElement(elementType, elementId, elementAction = null) { const pendingId = `fetchElement/${elementType}/${elementId}` + (isNil(elementAction) ? '' : `/${elementAction}`) return function(dispatch, getState) { @@ -167,7 +168,7 @@ export function fetchElement(elementType, elementId, elementAction=null) { QuestionsApi.fetchQuestionSets('index'), QuestionsApi.fetchQuestions('index') ]).then(([element, attributes, conditions, sections, - questionsets, questions]) => { + questionsets, questions]) => { if (elementAction == 'copy') { delete element.sections } @@ -192,14 +193,14 @@ export function fetchElement(elementType, elementId, elementAction=null) { QuestionsApi.fetchQuestionSets('index'), QuestionsApi.fetchQuestions('index') ]).then(([element, attributes, conditions, pages, - questionsets, questions]) => { + questionsets, questions]) => { if (elementAction == 'copy') { delete element.pages delete element.parents } return { - element, attributes, conditions, pages, questionsets, questions + element, attributes, conditions, pages, questionsets, questions } }) } @@ -219,7 +220,7 @@ export function fetchElement(elementType, elementId, elementAction=null) { QuestionsApi.fetchPages('index'), QuestionsApi.fetchQuestionSets('index') ]).then(([element, attributes, optionsets, options, conditions, - pages, questionsets]) => { + pages, questionsets]) => { if (elementAction == 'copy') { delete element.pages delete element.questionsets @@ -246,17 +247,17 @@ export function fetchElement(elementType, elementId, elementAction=null) { QuestionsApi.fetchQuestions('index'), TasksApi.fetchTasks('index'), ]).then(([element, attributes, conditions, pages, questionsets, - questions, tasks]) => { - if (elementAction == 'copy') { - delete element.conditions - delete element.pages - delete element.questionsets - delete element.questions - delete element.tasks - } + questions, tasks]) => { + if (elementAction == 'copy') { + delete element.conditions + delete element.pages + delete element.questionsets + delete element.questions + delete element.tasks + } return { - element, attributes, conditions, pages, questionsets, questions, tasks + element, attributes, conditions, pages, questionsets, questions, tasks } }) } @@ -310,17 +311,17 @@ export function fetchElement(elementType, elementId, elementAction=null) { QuestionsApi.fetchQuestions('index'), TasksApi.fetchTasks('index'), ]).then(([element, attributes, optionsets, options, - pages, questionsets, questions, tasks]) => { - if (elementAction == 'copy') { + pages, questionsets, questions, tasks]) => { + if (elementAction == 'copy') { delete element.optionsets delete element.pages delete element.questionsets delete element.questions delete element.tasks } - return { - element, attributes, optionsets, options, pages, questionsets, questions, tasks - } + return { + element, attributes, optionsets, options, pages, questionsets, questions, tasks + } }) break @@ -366,15 +367,15 @@ export function fetchElement(elementType, elementId, elementAction=null) { } export function fetchElementInit(elementType, elementId, elementAction) { - return {type: 'elements/fetchElementInit', elementType, elementId, elementAction} + return {type: actionTypes.FETCH_ELEMENT_INIT, elementType, elementId, elementAction} } export function fetchElementSuccess(elements) { - return {type: 'elements/fetchElementSuccess', elements} + return {type: actionTypes.FETCH_ELEMENT_SUCCESS, elements} } export function fetchElementError(error) { - return {type: 'elements/fetchElementError', error} + return {type: actionTypes.FETCH_ELEMENT_ERROR, error} } // store element @@ -450,20 +451,20 @@ export function storeElement(elementType, element, elementAction = null, back = } export function storeElementInit(element) { - return {type: 'elements/storeElementInit', element} + return {type: actionTypes.STORE_ELEMENT_INIT, element} } export function storeElementSuccess(element) { - return {type: 'elements/storeElementSuccess', element} + return {type: actionTypes.STORE_ELEMENT_SUCCESS, element} } export function storeElementError(element, error) { - return {type: 'elements/storeElementError', element, error} + return {type: actionTypes.STORE_ELEMENT_ERROR, element, error} } // createElement -export function createElement(elementType, parent={}) { +export function createElement(elementType, parent = {}) { const pendingId = `createElement/${elementType}` return function(dispatch, getState) { @@ -500,7 +501,7 @@ export function createElement(elementType, parent={}) { QuestionsApi.fetchQuestionSets('index'), QuestionsApi.fetchQuestions('index') ]).then(([element, attributes, conditions, - questionsets, questions]) => ({ + questionsets, questions]) => ({ element, parent, attributes, conditions, questionsets, questions })) break @@ -513,7 +514,7 @@ export function createElement(elementType, parent={}) { QuestionsApi.fetchQuestionSets('index'), QuestionsApi.fetchQuestions('index') ]).then(([element, attributes, conditions, - questionsets, questions]) => ({ + questionsets, questions]) => ({ element, parent, attributes, conditions, questionsets, questions })) break @@ -528,9 +529,9 @@ export function createElement(elementType, parent={}) { QuestionsApi.fetchWidgetTypes(), QuestionsApi.fetchValueTypes() ]).then(([element, attributes, optionsets, - options, conditions]) => ({ - element, parent, attributes, optionsets, options, conditions - })) + options, conditions]) => ({ + element, parent, attributes, optionsets, options, conditions + })) break case 'attributes': @@ -538,8 +539,8 @@ export function createElement(elementType, parent={}) { DomainFactory.createAttribute(getState().config, parent), DomainApi.fetchAttributes('index'), ]).then(([element, attributes]) => ({ - element, parent, attributes - })) + element, parent, attributes + })) break case 'optionsets': @@ -547,8 +548,8 @@ export function createElement(elementType, parent={}) { OptionsFactory.createOptionSet(getState().config, parent), OptionsApi.fetchOptions('index'), ]).then(([element, options]) => ({ - element, parent, options - })) + element, parent, options + })) break case 'options': @@ -556,8 +557,8 @@ export function createElement(elementType, parent={}) { OptionsFactory.createOption(getState().config, parent), OptionsApi.fetchOptionSets('index'), ]).then(([element, optionsets]) => ({ - element, parent, optionsets - })) + element, parent, optionsets + })) break case 'conditions': @@ -566,8 +567,8 @@ export function createElement(elementType, parent={}) { DomainApi.fetchAttributes('index'), OptionsApi.fetchOptions('index'), ]).then(([element, attributes, options]) => ({ - element, parent, attributes, options - })) + element, parent, attributes, options + })) break case 'tasks': @@ -577,8 +578,8 @@ export function createElement(elementType, parent={}) { ConditionsApi.fetchConditions('index'), QuestionsApi.fetchCatalogs('index') ]).then(([element, attributes, conditions, catalogs]) => ({ - element, attributes, conditions, catalogs - })) + element, attributes, conditions, catalogs + })) break case 'views': @@ -599,15 +600,15 @@ export function createElement(elementType, parent={}) { } export function createElementInit(elementType) { - return {type: 'elements/createElementInit', elementType} + return {type: actionTypes.CREATE_ELEMENT_INIT, elementType} } export function createElementSuccess(elements) { - return {type: 'elements/createElementSuccess', elements} + return {type: actionTypes.CREATE_ELEMENT_SUCCESS, elements} } export function createElementError(error) { - return {type: 'elements/createElementError', error} + return {type: actionTypes.CREATE_ELEMENT_ERROR, error} } // delete element @@ -677,21 +678,21 @@ export function deleteElement(elementType, element) { } export function deleteElementInit(element) { - return {type: 'elements/deleteElementInit', element} + return {type: actionTypes.DELETE_ELEMENT_INIT, element} } export function deleteElementSuccess(element) { - return {type: 'elements/deleteElementSuccess', element} + return {type: actionTypes.DELETE_ELEMENT_SUCCESS, element} } export function deleteElementError(element, error) { - return {type: 'elements/deleteElementError', element, error} + return {type: actionTypes.DELETE_ELEMENT_ERROR, element, error} } // update elements export function updateElement(element, values) { - return {type: 'elements/updateElement', element, values} + return {type: actionTypes.UPDATE_ELEMENT, element, values} } // move elements @@ -703,9 +704,9 @@ export function dropElement(dragElement, dropElement, mode) { const element = {...getState().elements.element} const { dragParent, dropParent } = moveElement(element, dragElement, dropElement, mode) - dispatch(storeElement(elementTypes[dragParent.model], dragParent)) - if (!isNil(dropParent)) { - dispatch(storeElement(elementTypes[dropParent.model], dropParent)) + dispatch(storeElement(elementTypes[dragParent.model], dragParent)) + if (!isNil(dropParent)) { + dispatch(storeElement(elementTypes[dropParent.model], dropParent)) } } } diff --git a/rdmo/management/assets/js/actions/importActions.js b/rdmo/management/assets/js/actions/importActions.js index 0240cddcc6..cea606f78c 100644 --- a/rdmo/management/assets/js/actions/importActions.js +++ b/rdmo/management/assets/js/actions/importActions.js @@ -1,10 +1,11 @@ import isNil from 'lodash/isNil' -import ManagementApi from '../api/ManagementApi' - import { addToPending, removeFromPending } from 'rdmo/core/assets/js/actions/pendingActions' -import { fetchElements, fetchElement } from './elementActions' +import ManagementApi from '../api/ManagementApi' + +import * as actionTypes from './actionTypes' +import { fetchElement, fetchElements } from './elementActions' // upload file @@ -24,15 +25,15 @@ export function uploadFile(file) { } export function uploadFileInit(file) { - return {type: 'import/uploadFileInit', file: file} + return {type: actionTypes.UPLOAD_IMPORT_FILE_INIT, file: file} } export function uploadFileSuccess(elements) { - return {type: 'import/uploadFileSuccess', elements} + return {type: actionTypes.UPLOAD_IMPORT_FILE_SUCCESS, elements} } export function uploadFileError(error) { - return {type: 'import/uploadFileError', error} + return {type: actionTypes.UPLOAD_IMPORT_FILE_ERROR, error} } // import elements @@ -54,39 +55,39 @@ export function importElements() { } export function importElementsInit() { - return {type: 'import/importElementsInit'} + return {type: actionTypes.IMPORT_ELEMENTS_INIT} } export function importElementsSuccess(elements) { - return {type: 'import/importElementsSuccess', elements} + return {type: actionTypes.IMPORT_ELEMENTS_SUCCESS, elements} } export function importElementsError(error) { - return {type: 'import/importElementsError', error} + return {type: actionTypes.IMPORT_ELEMENTS_ERROR, error} } // update elements export function updateElement(element, values) { - return {type: 'import/updateElement', element, values} + return {type: actionTypes.UPDATE_IMPORT_ELEMENT, element, values} } export function selectElements(value) { - return {type: 'import/selectElements', value} + return {type: actionTypes.SELECT_IMPORT_ELEMENTS, value} } export function selectChangedElements(value) { - return {type: 'import/selectChangedElements', value} + return {type: actionTypes.SELECT_CHANGED_IMPORT_ELEMENTS, value} } export function showElements(value) { - return {type: 'import/showElements', value} + return {type: actionTypes.SHOW_IMPORT_ELEMENTS, value} } export function showChangedElements(value) { - return {type: 'import/showChangedElements', value} + return {type: actionTypes.SHOW_CHANGED_IMPORT_ELEMENTS, value} } export function updateUriPrefix(uriPrefix) { - return {type: 'import/updateUriPrefix', uriPrefix} + return {type: actionTypes.UPDATE_IMPORT_URI_PREFIX, uriPrefix} } export function resetElements() { @@ -97,6 +98,6 @@ export function resetElements() { } else { dispatch(fetchElement(elementType, elementId, elementAction)) } - dispatch({type: 'import/resetElements'}) + dispatch({type: actionTypes.RESET_IMPORT_ELEMENTS}) } } diff --git a/rdmo/management/assets/js/components/Main.js b/rdmo/management/assets/js/components/Main.js new file mode 100644 index 0000000000..92ef3622d9 --- /dev/null +++ b/rdmo/management/assets/js/components/Main.js @@ -0,0 +1,63 @@ +import React from 'react' +import { useSelector } from 'react-redux' +import { get, isEmpty, isNil } from 'lodash' + +import { MainErrors } from '../components/common/Errors' +import Edit from '../components/edit/Edit' +import Elements from '../components/elements/Elements' +import Import from '../components/import/Import' +import Nested from '../components/nested/Nested' + +const Main = () => { + const config = useSelector((state) => state.config) + const elements = useSelector((state) => state.elements) + const imports = useSelector((state) => state.imports) + + const { element, elementType, elementId, elementAction } = elements + + const render = () => { + // check if anything was loaded yet + if (isNil(config.settings) || isNil(elementType)) { + return null + } + + // check if an error occurred + if (!isNil(elements.errors.api)) { + return + } else if (get(elements, 'element.errors.api')) { + return + } else if (!isNil(imports.errors.file)) { + return + } + + if (!isEmpty(imports.elements)) { + return + } + + // check if the nested components should be displayed + if (!isNil(element) && elementAction === 'nested') { + return + } + + // check if the edit components should be displayed + if (!isNil(element)) { + return + } + + // check if the list components should be displayed + if (isNil(elementId) && isNil(elementAction)) { + return + } + + // fetching the data is not complete yet, or no action was invoked yet + return null + } + + return ( +
    + {render()} +
    + ) +} + +export default Main diff --git a/rdmo/management/assets/js/components/Sidebar.js b/rdmo/management/assets/js/components/Sidebar.js new file mode 100644 index 0000000000..6123728851 --- /dev/null +++ b/rdmo/management/assets/js/components/Sidebar.js @@ -0,0 +1,24 @@ +import React from 'react' +import { useSelector } from 'react-redux' +import { isEmpty, isNil } from 'lodash' + +import ElementsSidebar from '../components/sidebar/ElementsSidebar' +import ImportSidebar from '../components/sidebar/ImportSidebar' + +const Sidebar = () => { + const config = useSelector((state) => state.config) + const imports = useSelector((state) => state.imports) + + // check if anything was loaded yet + if (isNil(config.settings)) { + return null + } + + if (isEmpty(imports.elements)) { + return + } else { + return + } +} + +export default Sidebar diff --git a/rdmo/management/assets/js/components/common/Buttons.js b/rdmo/management/assets/js/components/common/Buttons.js index 9b8afb9530..43458521f4 100644 --- a/rdmo/management/assets/js/components/common/Buttons.js +++ b/rdmo/management/assets/js/components/common/Buttons.js @@ -1,23 +1,41 @@ import React from 'react' import PropTypes from 'prop-types' +import classNames from 'classnames' -const BackButton = () => ( - ) -const SaveButton = ({ elementAction, onClick, disabled=false, back=false }) => { - let text, className = 'element-button btn btn-xs' +BackButton.propTypes = { + className: PropTypes.string +} + +const SaveButton = ({ elementAction, onClick, disabled = false, back = false }) => { + let text + let className + if (elementAction == 'create') { text = back ? gettext('Create') : gettext('Create and continue editing') - className += back ? ' btn-success' : ' btn-default' + className = classNames('btn btn-sm', { + 'btn-success': back, + 'btn-light border': !back + }) } else if (elementAction == 'copy') { text = back ? gettext('Copy') : gettext('Copy and continue editing') - className += back ? ' btn-info' : ' btn-default' + className = classNames('btn btn-sm', { + 'btn-info': back, + 'btn-light border': !back + }) } else { text = back ? gettext('Save') : gettext('Save and continue editing') - className += back ? ' btn-primary' : ' btn-default' + className = classNames('btn btn-sm', { + 'btn-primary': back, + 'btn-light border': !back + }) } return ( @@ -35,7 +53,7 @@ SaveButton.propTypes = { } const NewButton = ({ onClick }) => ( - ) @@ -44,8 +62,8 @@ NewButton.propTypes = { onClick: PropTypes.func.isRequired } -const DeleteButton = ({ onClick, disabled=false }) => ( - ) @@ -55,4 +73,4 @@ DeleteButton.propTypes = { disabled: PropTypes.bool } -export { BackButton, SaveButton, NewButton, DeleteButton } +export { BackButton, DeleteButton, NewButton, SaveButton } diff --git a/rdmo/management/assets/js/components/common/Checkboxes.js b/rdmo/management/assets/js/components/common/Checkboxes.js deleted file mode 100644 index 7265be5de0..0000000000 --- a/rdmo/management/assets/js/components/common/Checkboxes.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -const Checkbox = ({ label, value, onChange }) => { - const checked = [true, 'true'].includes(value) // values are stored as string in the local storage - - return ( - - - - ) -} - -Checkbox.propTypes = { - label: PropTypes.oneOfType([PropTypes.object, PropTypes.string]).isRequired, - value: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]).isRequired, - onChange: PropTypes.func.isRequired -} - -export { Checkbox } diff --git a/rdmo/management/assets/js/components/common/DragAndDrop.js b/rdmo/management/assets/js/components/common/DragAndDrop.js index 08e7d807a8..d7e17439d4 100644 --- a/rdmo/management/assets/js/components/common/DragAndDrop.js +++ b/rdmo/management/assets/js/components/common/DragAndDrop.js @@ -1,9 +1,12 @@ import React, { useRef } from 'react' -import { useDrag, useDrop } from 'react-dnd' import PropTypes from 'prop-types' +import { useDrag, useDrop } from 'react-dnd' +import { useDispatch } from 'react-redux' import classNames from 'classnames' -const Drag = ({ element, show=true }) => { +import { dropElement } from '../../actions/elementActions' + +const Drag = ({ element, show = true }) => { const dragRef = useRef(null) const [{}, drag] = useDrag(() => ({ @@ -13,9 +16,15 @@ const Drag = ({ element, show=true }) => { drag(dragRef) - return show && - - + const className = classNames('bi bi-arrows-move drag', { + 'disabled': element.read_only + }) + + return show && ( + + + + ) } Drag.propTypes = { @@ -23,7 +32,9 @@ Drag.propTypes = { show: PropTypes.bool } -const Drop = ({ element, elementActions, indent=0, mode='in', children=null }) => { +const Drop = ({ element, indent = 0, mode = 'in', children = null }) => { + const dispatch = useDispatch() + const dropRef = useRef(null) let accept @@ -49,7 +60,7 @@ const Drop = ({ element, elementActions, indent=0, mode='in', children=null }) = isOver: monitor.isOver() }), drop: (item) => { - elementActions.dropElement(item, element, mode) + dispatch(dropElement(item, element, mode)) }, }), [element]) @@ -65,13 +76,12 @@ const Drop = ({ element, elementActions, indent=0, mode='in', children=null }) = if (mode == 'in') { return
    {children}
    } else { - return
    + return
    } } Drop.propTypes = { element: PropTypes.object.isRequired, - elementActions: PropTypes.object.isRequired, mode: PropTypes.string, indent: PropTypes.number, children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]) diff --git a/rdmo/management/assets/js/components/common/Errors.js b/rdmo/management/assets/js/components/common/Errors.js index 0e63a37dfd..cb2e5ed998 100644 --- a/rdmo/management/assets/js/components/common/Errors.js +++ b/rdmo/management/assets/js/components/common/Errors.js @@ -1,6 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' -import isString from 'lodash/isString' +import { isEmpty, isString } from 'lodash' const MainErrors = ({ errors }) => { return ( @@ -11,7 +11,7 @@ const MainErrors = ({ errors }) => { {gettext('One or more errors occurred:')}

      - { errors.map((error, index) =>
    • {error}
    • ) } + {errors.map((error, index) =>
    • {error}
    • )}
    @@ -24,7 +24,7 @@ MainErrors.propTypes = { } const ElementErrors = ({ element }) => { - if (element.errors) { + if (!isEmpty(element.errors)) { const errorList = Object.values(element.errors).flat().reduce((acc, cur) => { if (isString(cur)) { acc.push(cur) @@ -37,7 +37,7 @@ const ElementErrors = ({ element }) => { return acc }, []) - return element.errors && ( + return (
      {errorList.map((error, index) =>
    • {error}
    • )}
    @@ -51,4 +51,4 @@ ElementErrors.propTypes = { element: PropTypes.object.isRequired } -export { MainErrors, ElementErrors } +export { ElementErrors, MainErrors } diff --git a/rdmo/management/assets/js/components/common/Filter.js b/rdmo/management/assets/js/components/common/Filter.js index 13119a9143..d659a3ab3a 100644 --- a/rdmo/management/assets/js/components/common/Filter.js +++ b/rdmo/management/assets/js/components/common/Filter.js @@ -3,17 +3,15 @@ import PropTypes from 'prop-types' const FilterString = ({ value, onChange, label }) => { return ( -
    -
    - onChange(e.target.value)}> - - - -
    +
    + onChange(e.target.value)}> +
    ) } @@ -26,9 +24,10 @@ FilterString.propTypes = { const FilterUriPrefix = ({ value, options, onChange }) => { return ( -
    - onChange(event.target.value)}> { options.map((option, index) => ) @@ -46,10 +45,11 @@ FilterUriPrefix.propTypes = { const FilterSite = ({ value, options, onChange, label = 'Filter sites', allLabel = 'All sites' }) => { return ( -
    - onChange(event.target.value)}> + { options.map((option, index) => ) } @@ -66,4 +66,4 @@ FilterSite.propTypes = { allLabel: PropTypes.string } -export { FilterString, FilterUriPrefix, FilterSite } +export { FilterSite, FilterString, FilterUriPrefix } diff --git a/rdmo/management/assets/js/components/common/Forms.js b/rdmo/management/assets/js/components/common/Forms.js deleted file mode 100644 index e95382e633..0000000000 --- a/rdmo/management/assets/js/components/common/Forms.js +++ /dev/null @@ -1,36 +0,0 @@ -import React, { useState } from 'react' -import PropTypes from 'prop-types' -import isNil from 'lodash/isNil' - -const UploadForm = ({ onSubmit }) => { - const [file, setFile] = useState(null) - - const handleSubmit = event => { - event.preventDefault() - onSubmit(file) - } - - return ( -
    -
    - setFile(event.target.files[0])} accept=".xml" /> -

    {file ? file.name : gettext('Select file')}

    -
    - -
    - -
    -
    - ) -} - -UploadForm.propTypes = { - onSubmit: PropTypes.func.isRequired -} - -export { UploadForm } diff --git a/rdmo/management/assets/js/components/common/Icons.js b/rdmo/management/assets/js/components/common/Icons.js index 162b1bec34..8210eab22f 100644 --- a/rdmo/management/assets/js/components/common/Icons.js +++ b/rdmo/management/assets/js/components/common/Icons.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types' const ReadOnlyIcon = ({ title, show }) => { return show && ( - + ) } diff --git a/rdmo/management/assets/js/components/common/Labels.js b/rdmo/management/assets/js/components/common/Labels.js deleted file mode 100644 index f0d3e3729e..0000000000 --- a/rdmo/management/assets/js/components/common/Labels.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -const Label = ({ text, type, onClick, show = true, className = '' }) => { - const labelClass = `label label-${type} ${className}` - return show && ( - - {text} - - ) -} - -Label.propTypes = { - text: PropTypes.string.isRequired, - type: PropTypes.string.isRequired, - onClick: PropTypes.func, - show: PropTypes.bool, - className: PropTypes.string, -} - -export default Label diff --git a/rdmo/management/assets/js/components/common/Links.js b/rdmo/management/assets/js/components/common/Links.js index c9454a91e0..ce044a3b51 100644 --- a/rdmo/management/assets/js/components/common/Links.js +++ b/rdmo/management/assets/js/components/common/Links.js @@ -7,8 +7,8 @@ import isUndefined from 'lodash/isUndefined' import Link from 'rdmo/core/assets/js/components/Link' import LinkButton from 'rdmo/core/assets/js/components/LinkButton' -const NestedLink = ({ href, title, onClick, show=true }) => { - return show && +const NestedLink = ({ href, title, onClick, show = true }) => { + return show && } NestedLink.propTypes = { @@ -18,8 +18,8 @@ NestedLink.propTypes = { show: PropTypes.bool } -const EditLink = ({ href, title, onClick, disabled= false }) => { - return +const EditLink = ({ href, title, onClick, disabled = false }) => { + return } EditLink.propTypes = { @@ -30,7 +30,7 @@ EditLink.propTypes = { } const CopyLink = ({ href, title, onClick }) => { - return + return } CopyLink.propTypes = { @@ -41,12 +41,13 @@ CopyLink.propTypes = { const AddLink = ({ title, altTitle, onClick, onAltClick, disabled }) => { if (isUndefined(onAltClick)) { - return + return } else { return ( -
    • @@ -71,13 +72,16 @@ AddLink.propTypes = { const AvailableLink = ({ available, locked, title, onClick, disabled }) => { const className = classNames({ - 'element-btn-link fa': true, - 'fa-toggle-on': available, - 'fa-toggle-off': !available + 'link bi': true, + 'bi-toggle-on': available, + 'bi-toggle-off': !available }) - return + return ( + + ) } AvailableLink.propTypes = { @@ -90,9 +94,9 @@ AvailableLink.propTypes = { const LockedLink = ({ locked, title, onClick, disabled }) => { const className = classNames({ - 'element-btn-link fa': true, - 'fa-lock text-danger': locked, - 'fa-unlock-alt': !locked + 'link bi': true, + 'bi-lock text-danger': locked, + 'bi-unlock': !locked }) return @@ -107,11 +111,11 @@ LockedLink.propTypes = { const ToggleCurrentSiteLink = ({ hasCurrentSite, onClick, show }) => { const className = classNames({ - 'element-btn-link fa': true, - 'fa-plus-square': !hasCurrentSite, - 'fa-minus-square': hasCurrentSite, + 'link bi': true, + 'bi-plus-square': !hasCurrentSite, + 'bi-dash-square': hasCurrentSite, }) - const title = hasCurrentSite ? gettext('Remove your site'): gettext('Add your site') + const title = hasCurrentSite ? gettext('Remove your site') : gettext('Add your site') return show && } @@ -125,9 +129,9 @@ ToggleCurrentSiteLink.propTypes = { const ShowElementsLink = ({ showElements, show, onClick }) => { const className = classNames({ - 'element-btn-link fa': true, - 'fa-chevron-down': showElements, - 'fa-chevron-up': !showElements + 'link bi': true, + 'bi-chevron-down': showElements, + 'bi-chevron-up': !showElements }) const title = showElements ? gettext('Hide elements') : gettext('Show elements') @@ -141,34 +145,50 @@ ShowElementsLink.propTypes = { onClick: PropTypes.func.isRequired } -const ExportLink = ({ exportUrl, title, exportFormats, csv=false, full=false }) => { +const ExportLink = ({ exportUrl, title, exportFormats, csv = false, full = false }) => { return ( - -
        -
      • {gettext('XML')}
      • + + @@ -184,14 +204,12 @@ ExportLink.propTypes = { } const ExtendLink = ({ extend, onClick }) => { - const className = classNames({ - 'element-link fa': true, - 'fa-chevron-up': extend, - 'fa-chevron-down': !extend + const className = classNames('element-link bi ms-1', { + 'bi-chevron-up': extend, + 'bi-chevron-down': !extend }) - const title = extend ? gettext('Show less') - : gettext('Show more') + const title = extend ? gettext('Show less') : gettext('Show more') return } @@ -201,21 +219,26 @@ ExtendLink.propTypes = { onClick: PropTypes.func.isRequired } -const CodeLink = ({ className, uri, href, onClick, order }) => { +const CodeLink = ({ className, type, uri, href, onClick, order }) => { return ( - <> + - {uri} + {uri} - {!isNil(order) ? ( - <>{' '}{order} - ) : null} - + { + !isNil(order) && ( + + {order} + + ) + } + ) } CodeLink.propTypes = { - className: PropTypes.string.isRequired, + className: PropTypes.string, + type: PropTypes.string.isRequired, uri: PropTypes.string.isRequired, href: PropTypes.string, onClick: PropTypes.func.isRequired, @@ -223,7 +246,7 @@ CodeLink.propTypes = { } const ErrorLink = ({ onClick }) => { - return + return } ErrorLink.propTypes = { @@ -231,7 +254,7 @@ ErrorLink.propTypes = { } const WarningLink = ({ onClick }) => { - return + return } WarningLink.propTypes = { @@ -241,9 +264,9 @@ WarningLink.propTypes = { const ShowLink = ({ show = false, onClick }) => { const title = show ? gettext('Hide') : gettext('Show') const className = classNames({ - 'element-link fa': true, - 'fa-chevron-down': !show, - 'fa-chevron-up': show + 'element-link bi': true, + 'bi-chevron-down': !show, + 'bi-chevron-up': show }) return @@ -254,5 +277,19 @@ ShowLink.propTypes = { onClick: PropTypes.func.isRequired } -export { EditLink, CopyLink, AddLink, AvailableLink, ToggleCurrentSiteLink, LockedLink, ShowElementsLink, - NestedLink, ExportLink, ExtendLink, CodeLink, ErrorLink, WarningLink, ShowLink } +export { + AddLink, + AvailableLink, + CodeLink, + CopyLink, + EditLink, + ErrorLink, + ExportLink, + ExtendLink, + LockedLink, + NestedLink, + ShowElementsLink, + ShowLink, + ToggleCurrentSiteLink, + WarningLink +} diff --git a/rdmo/management/assets/js/components/common/Modals.js b/rdmo/management/assets/js/components/common/Modals.js index 2dfa3c72ed..b8d2b2e740 100644 --- a/rdmo/management/assets/js/components/common/Modals.js +++ b/rdmo/management/assets/js/components/common/Modals.js @@ -9,7 +9,7 @@ const DeleteModal = ({ title, show, onClose, onDelete, children }) => {

        {title}

        - { children } + {children}