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..69293b7b06 100644 --- a/.gitignore +++ b/.gitignore @@ -34,18 +34,8 @@ dist .ruff_cache .on-save.json +rdmo/core/static/ rdmo/management/static +rdmo/projects/static/ -rdmo/core/static/core/js/base.js -rdmo/core/static/core/js/base.js.LICENSE.txt -rdmo/core/static/core/css/base.css -rdmo/core/static/core/fonts - -rdmo/projects/static/projects/css/interview.css -rdmo/projects/static/projects/css/projects.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 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..841fb65bce 100644 --- a/conftest.py +++ b/conftest.py @@ -19,7 +19,6 @@ def fixtures(): 'domain', 'groups', 'options', - 'overlays', 'projects', 'questions', 'sites', @@ -39,6 +38,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..5b1523d572 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,12 +42,10 @@ 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", "django-filter>=23.2,<26.0", - "django-libsass>=0.9,<1.0", "django-mathfilters>=1.0,<2.0", "django-mptt>=0.16.0,<1.0.0", "django-settings-export>=1.2,<2.0", @@ -203,15 +202,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 +229,7 @@ default.extend-ignore-re = [ "(?Rm)^.*(#|//)\\s*spellchecker:disable-line$", # for .py files "(?Rm)^.*$", # for .html files ] +files.extend-exclude = ["rdmo/core/templates/core/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..35d1941cc5 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/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..12f06afe71 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/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..16a2e0f0cf 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/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..7a19171a32 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/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..4993690cb0 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/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..6577a9a71e 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/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..6c9cff6b6f 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/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..5e1797c561 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/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..b516598ad0 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/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..0483009994 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/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..37fcdaf291 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/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..e82f0333c9 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/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..6ecf658363 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/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..5f41a39e00 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/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..8fd043160d 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/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..3e37cefa62 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/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..d164876d70 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/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..07b2389543 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/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..d81256496e 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/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..acff246b2a 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/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..5aba4b9508 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/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..58cdb917f1 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/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..33330cbbbf 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/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..90c97cb446 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/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..5ceca0781c 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/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..56a7435b6f 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/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/img/document.png b/rdmo/core/assets/img/document.png new file mode 100644 index 0000000000..9187ff5cd3 Binary files /dev/null and b/rdmo/core/assets/img/document.png differ diff --git a/rdmo/core/static/core/img/header/collection.jpg b/rdmo/core/assets/img/header/collection.jpg similarity index 100% rename from rdmo/core/static/core/img/header/collection.jpg rename to rdmo/core/assets/img/header/collection.jpg diff --git a/rdmo/core/static/core/img/header/library.jpg b/rdmo/core/assets/img/header/library.jpg similarity index 100% rename from rdmo/core/static/core/img/header/library.jpg rename to rdmo/core/assets/img/header/library.jpg diff --git a/rdmo/core/static/core/img/header/supercomputer.jpg b/rdmo/core/assets/img/header/supercomputer.jpg similarity index 100% rename from rdmo/core/static/core/img/header/supercomputer.jpg rename to rdmo/core/assets/img/header/supercomputer.jpg diff --git a/rdmo/core/static/core/img/rdmo-logo.png b/rdmo/core/assets/img/rdmo-logo.png similarity index 100% rename from rdmo/core/static/core/img/rdmo-logo.png rename to rdmo/core/assets/img/rdmo-logo.png 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/app.js b/rdmo/core/assets/js/app.js new file mode 100644 index 0000000000..e2204695ae --- /dev/null +++ b/rdmo/core/assets/js/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/base.js b/rdmo/core/assets/js/base.js index 1653c3c47c..d4c0cde622 100644 --- a/rdmo/core/assets/js/base.js +++ b/rdmo/core/assets/js/base.js @@ -1,4 +1 @@ -import 'bootstrap-sass' - -window.$ = require('jquery') -window.Cookies = require('js-cookie') +// This file is intentionally left empty. diff --git a/rdmo/core/assets/js/bootstrap.js b/rdmo/core/assets/js/bootstrap.js new file mode 100644 index 0000000000..ae9264d525 --- /dev/null +++ b/rdmo/core/assets/js/bootstrap.js @@ -0,0 +1,2 @@ +import 'bootstrap' +window.bootstrap = require('bootstrap') 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 %} - {% include 'core/base_navigation.html' %} - -
    {% block content %}{% endblock %}
    - - - -{% include 'core/base_footer.html' %} - {% if not debug %} - -{% include 'core/base_analytics.html' %} - + {% include 'core/base_analytics.html' %} {% endif %} -{% block extra_body %}{% endblock %} - diff --git a/rdmo/core/templates/core/base_footer.html b/rdmo/core/templates/core/base_footer.html deleted file mode 100644 index a2f083eb53..0000000000 --- a/rdmo/core/templates/core/base_footer.html +++ /dev/null @@ -1,7 +0,0 @@ -{% load i18n core_tags %} - - diff --git a/rdmo/core/templates/core/base_navigation.html b/rdmo/core/templates/core/base_navigation.html index a29e63f15a..ea8627ac45 100644 --- a/rdmo/core/templates/core/base_navigation.html +++ b/rdmo/core/templates/core/base_navigation.html @@ -3,60 +3,59 @@ {% load accounts_tags %} {% load rules %} -