diff --git a/.gitignore b/.gitignore index ec05075999..50da21c10b 100644 --- a/.gitignore +++ b/.gitignore @@ -36,16 +36,28 @@ dist rdmo/management/static +rdmo/core/static/core/css/app-bs53.css +rdmo/core/static/core/js/app-bs53.js rdmo/core/static/core/js/base.js rdmo/core/static/core/js/base.js.LICENSE.txt +rdmo/core/static/core/js/base-bs53.js +rdmo/core/static/core/js/base-bs53.js.LICENSE.txt +rdmo/core/static/core/js/bootstrap-bs53.js +rdmo/core/static/core/js/bootstrap-bs53.js.LICENSE.txt rdmo/core/static/core/css/base.css +rdmo/core/static/core/css/base-bs53.css +rdmo/core/static/core/css/bootstrap.css +rdmo/core/static/core/css/bootstrap-bs53.css rdmo/core/static/core/fonts rdmo/projects/static/projects/css/interview.css rdmo/projects/static/projects/css/projects.css +rdmo/projects/static/projects/css/project.css rdmo/projects/static/projects/fonts/ rdmo/projects/static/projects/js/interview.js rdmo/projects/static/projects/js/interview.js.LICENSE.txt rdmo/projects/static/projects/js/projects.js rdmo/projects/static/projects/js/projects.js.LICENSE.txt +rdmo/projects/static/projects/js/project.js +rdmo/projects/static/projects/js/project.js.LICENSE.txt screenshots diff --git a/conftest.py b/conftest.py index 01fad1f47f..eab9ac0181 100644 --- a/conftest.py +++ b/conftest.py @@ -8,6 +8,8 @@ from django.contrib.auth.models import User from django.core.management import call_command +from rest_framework.test import APIClient + from rdmo.accounts.utils import set_group_permissions @@ -75,3 +77,7 @@ def delete_all(*models): for model in models: model.objects.all().delete() return delete_all + +@pytest.fixture +def api_client(): + return APIClient() diff --git a/package-lock.json b/package-lock.json index 0be895f0ad..05bcc2b627 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.23.0", + "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.21", @@ -23,7 +26,7 @@ "prop-types": "^15.7.2", "react": "^18.3.1", "react-bootstrap": "0.33.1", - "react-datepicker": "7.5.0", + "react-datepicker": "7.3.0", "react-diff-viewer-continued": "^3.4.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", @@ -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.27.0", @@ -45,7 +48,7 @@ "copy-webpack-plugin": "^13.0.0", "css-loader": "^7.1.1", "eslint": "~8.56.0", - "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react": "^7.35.0", "file-loader": "^6.2.0", "mini-css-extract-plugin": "^2.9.0", "sass": "^1.89.1", @@ -2143,6 +2146,22 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" }, + "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", @@ -2632,6 +2651,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", @@ -2647,18 +2676,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", @@ -3414,6 +3431,39 @@ "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", @@ -3844,9 +3894,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" @@ -3879,14 +3929,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", @@ -3972,57 +4014,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.146", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.146.tgz", @@ -4052,17 +4043,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", @@ -5129,39 +5109,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", @@ -5812,14 +5759,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", @@ -6255,18 +6194,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", @@ -6307,14 +6234,6 @@ "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", @@ -6591,29 +6510,21 @@ } }, "node_modules/react-datepicker": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-7.5.0.tgz", - "integrity": "sha512-6MzeamV8cWSOcduwePHfGqY40acuGlS1cG//ePHT6bVbLxWyqngaStenfH03n1wbzOibFggF66kWaBTb1SbTtQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-7.3.0.tgz", + "integrity": "sha512-EqRKLAtLZUTztiq6a+tjSjQX9ES0Xd229JPckAtyZZ4GoY3rtvNWAzkYZnQUf6zTWT50Ki0+t+W9VRQIkSJLfg==", "dependencies": { - "@floating-ui/react": "^0.26.23", - "clsx": "^2.1.1", - "date-fns": "^3.6.0", - "prop-types": "^15.8.1" + "@floating-ui/react": "^0.26.2", + "clsx": "^2.1.0", + "date-fns": "^3.3.1", + "prop-types": "^15.7.2", + "react-onclickoutside": "^6.13.0" }, "peerDependencies": { "react": "^16.9.0 || ^17 || ^18", "react-dom": "^16.9.0 || ^17 || ^18" } }, - "node_modules/react-datepicker/node_modules/date-fns": { - "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" - } - }, "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", @@ -6708,6 +6619,19 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "node_modules/react-onclickoutside": { + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.2.tgz", + "integrity": "sha512-h6Hbf1c8b7tIYY4u90mDdBLY4+AGQVMFtIE89HgC0DtVCh/JfKl477gYqUtGLmjZBKK3MJxomP/lFiLbz4sq9A==", + "funding": { + "type": "individual", + "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md" + }, + "peerDependencies": { + "react": "^15.5.x || ^16.x || ^17.x || ^18.x", + "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x" + } + }, "node_modules/react-overlays": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.9.3.tgz", @@ -7291,17 +7215,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", @@ -7942,9 +7855,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" }, @@ -9675,6 +9588,16 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" }, + "@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", @@ -9954,6 +9877,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", @@ -9969,15 +9898,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", @@ -10559,6 +10479,17 @@ "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", @@ -10848,9 +10779,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.3.4", @@ -10871,11 +10802,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", @@ -10937,39 +10863,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.146", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.146.tgz", @@ -10992,11 +10885,6 @@ "tapable": "^2.2.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", @@ -11784,29 +11672,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", @@ -12252,11 +12117,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", @@ -12577,15 +12437,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", @@ -12614,11 +12465,6 @@ "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", @@ -12804,21 +12650,15 @@ } }, "react-datepicker": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-7.5.0.tgz", - "integrity": "sha512-6MzeamV8cWSOcduwePHfGqY40acuGlS1cG//ePHT6bVbLxWyqngaStenfH03n1wbzOibFggF66kWaBTb1SbTtQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-7.3.0.tgz", + "integrity": "sha512-EqRKLAtLZUTztiq6a+tjSjQX9ES0Xd229JPckAtyZZ4GoY3rtvNWAzkYZnQUf6zTWT50Ki0+t+W9VRQIkSJLfg==", "requires": { - "@floating-ui/react": "^0.26.23", - "clsx": "^2.1.1", - "date-fns": "^3.6.0", - "prop-types": "^15.8.1" - }, - "dependencies": { - "date-fns": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", - "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==" - } + "@floating-ui/react": "^0.26.2", + "clsx": "^2.1.0", + "date-fns": "^3.3.1", + "prop-types": "^15.7.2", + "react-onclickoutside": "^6.13.0" } }, "react-diff-viewer-continued": { @@ -12882,6 +12722,12 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-onclickoutside": { + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.2.tgz", + "integrity": "sha512-h6Hbf1c8b7tIYY4u90mDdBLY4+AGQVMFtIE89HgC0DtVCh/JfKl477gYqUtGLmjZBKK3MJxomP/lFiLbz4sq9A==", + "requires": {} + }, "react-overlays": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.9.3.tgz", @@ -13279,14 +13125,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", @@ -13723,9 +13561,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": { diff --git a/package.json b/package.json index 331c0b4d13..37f4f10728 100644 --- a/package.json +++ b/package.json @@ -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.23.0", + "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.21", @@ -33,7 +36,7 @@ "prop-types": "^15.7.2", "react": "^18.3.1", "react-bootstrap": "0.33.1", - "react-datepicker": "7.5.0", + "react-datepicker": "7.3.0", "react-diff-viewer-continued": "^3.4.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", @@ -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.27.0", @@ -55,7 +58,7 @@ "copy-webpack-plugin": "^13.0.0", "css-loader": "^7.1.1", "eslint": "~8.56.0", - "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react": "^7.35.0", "file-loader": "^6.2.0", "mini-css-extract-plugin": "^2.9.0", "sass": "^1.89.1", diff --git a/pyproject.toml b/pyproject.toml index 16540a6a04..65de1d3c8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -236,3 +236,4 @@ default.extend-ignore-re = [ "(?Rm)^.*(#|//)\\s*spellchecker:disable-line$", # for .py files "(?Rm)^.*$", # for .html files ] +files.extend-exclude = ["rdmo/core/templates/core/bs53/home.html"] 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 634a809d02..96814ee4f1 100644 --- a/rdmo/accounts/models.py +++ b/rdmo/accounts/models.py @@ -3,6 +3,7 @@ from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver +from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from rdmo.core.models import Model as RDMOTimeStampedModel @@ -205,6 +206,10 @@ class Meta: def __str__(self): return self.user.username + @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..23eab15608 100644 --- a/rdmo/accounts/templates/account/account_token.html +++ b/rdmo/accounts/templates/account/account_token.html @@ -1,4 +1,4 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} {% block page %} @@ -9,7 +9,10 @@
diff --git a/rdmo/accounts/templates/account/email.html b/rdmo/accounts/templates/account/email.html index 6fb9e86652..5423d264ea 100644 --- a/rdmo/accounts/templates/account/email.html +++ b/rdmo/accounts/templates/account/email.html @@ -1,4 +1,4 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} {% block page %} @@ -9,74 +9,114 @@
{% trans 'The following e-mail addresses are associated with your account:' %}
- + ) +} + +ProjectForm.propTypes = { + disabled: PropTypes.bool +} + +export default ProjectForm diff --git a/rdmo/projects/assets/js/project/hooks/useFieldErrors.js b/rdmo/projects/assets/js/project/hooks/useFieldErrors.js new file mode 100644 index 0000000000..4e192599c7 --- /dev/null +++ b/rdmo/projects/assets/js/project/hooks/useFieldErrors.js @@ -0,0 +1,11 @@ +import { useSelector } from 'react-redux' + +export const useFieldErrors = () => { + const errors = useSelector((state) => state.project.errors?.[0]?.errors ?? {}) + return Object.fromEntries( + Object.entries(errors).map(([field, err]) => [ + field, + Array.isArray(err) ? err : err != null ? [err] : [] + ]) + ) +} diff --git a/rdmo/projects/assets/js/project/reducers/projectReducer.js b/rdmo/projects/assets/js/project/reducers/projectReducer.js new file mode 100644 index 0000000000..d48342b8d2 --- /dev/null +++ b/rdmo/projects/assets/js/project/reducers/projectReducer.js @@ -0,0 +1,133 @@ +import * as actionTypes from '../actions/actionTypes' + +const initialState = { + project: null, + invites: null, + errors: [] +} + +export default function projectReducer(state = initialState, action) { + switch(action.type) { + case actionTypes.FETCH_PROJECT_SUCCESS: + return { ...state, project: action.project} + case actionTypes.FETCH_PROJECT_INIT: + return { ...state, errors: [] } + case actionTypes.FETCH_PROJECT_ERROR: + return { ...state, errors: [...state.errors, { actionType: action.type, ...action.error }] } + case actionTypes.UPDATE_PROJECT_SUCCESS: + return { ...state, project: action.project } + case actionTypes.UPDATE_PROJECT_INIT: + return { ...state, errors: [] } + case actionTypes.UPDATE_PROJECT_ERROR: + return { + ...state, + errors: [...state.errors, { actionType: action.type, ...action.error }] + } + case actionTypes.DELETE_PROJECT_SUCCESS: + return { ...state, project: null } + case actionTypes.DELETE_PROJECT_INIT: + return { ...state, errors: [] } + case actionTypes.DELETE_PROJECT_ERROR: + return { + ...state, + errors: [...state.errors, { actionType: action.type, ...action.error }] + } + case actionTypes.FETCH_PROJECT_INVITES_SUCCESS: + return { ...state, invites: action.invites } + case actionTypes.FETCH_PROJECT_INVITES_INIT: + return { ...state, errors: [] } + case actionTypes.FETCH_PROJECT_INVITES_ERROR: + return { ...state, errors: [...state.errors, { actionType: action.type, ...action.error }] } + case actionTypes.CREATE_PROJECT_MEMBER_SUCCESS: + return { ...state, project: { ...state.project, memberships: [ ...(state.project?.memberships || []), action.member ] } } + case actionTypes.CREATE_PROJECT_MEMBER_INIT: + return { ...state, errors: [] } + case actionTypes.CREATE_PROJECT_MEMBER_ERROR: + return { + ...state, + errors: [...state.errors, { actionType: action.type, ...action.error }] + } + case actionTypes.UPDATE_PROJECT_MEMBER_INIT: + return { ...state, errors: [] } + case actionTypes.UPDATE_PROJECT_MEMBER_SUCCESS: + return { + ...state, + project: { + ...state.project, + memberships: state.project?.memberships.map(m => (m.id == action.member.id ? { ...m, role: action.member.role } : m)) + } + } + case actionTypes.UPDATE_PROJECT_MEMBER_ERROR: + return { + ...state, + errors: [...state.errors, { actionType: action.type, ...action.error }] + } + case actionTypes.DELETE_PROJECT_MEMBER_INIT: + return { ...state, errors: [] } + case actionTypes.DELETE_PROJECT_MEMBER_SUCCESS: { + return { + ...state, + project: { + ...state.project, + memberships: state.project.memberships?.filter(m => m.id !== action.membershipId) + } + } + } + case actionTypes.DELETE_PROJECT_MEMBER_ERROR: + return { + ...state, + errors: [...state.errors, { actionType: action.type, ...action.error }] + } + case actionTypes.SEND_INVITE_SUCCESS: + return { ...state, invites: [...state.invites, action.invite] } + case actionTypes.SEND_INVITE_INIT: + return { ...state, errors: [] } + case actionTypes.SEND_INVITE_ERROR: + return { + ...state, + errors: [...state.errors, { actionType: action.type, ...action.error }] + } + case actionTypes.UPDATE_PROJECT_INVITE_INIT: + return { ...state, errors: [] } + case actionTypes.UPDATE_PROJECT_INVITE_SUCCESS: + return { + ...state, + invites: state.invites?.map(i => (i.id == action.invite.id ? { ...i, role: action.invite.role } : i)) + } + case actionTypes.UPDATE_PROJECT_INVITE_ERROR: + return { + ...state, + errors: [...state.errors, { actionType: action.type, ...action.error }] + } + case actionTypes.DELETE_PROJECT_INVITE_INIT: + return { ...state, errors: [] } + case actionTypes.DELETE_PROJECT_INVITE_SUCCESS: { + return { ...state, invites: state.invites.filter(i => i.id !== action.inviteId) } + } + case actionTypes.DELETE_PROJECT_INVITE_ERROR: + return { + ...state, + errors: [...state.errors, { actionType: action.type, ...action.error }] + } + case actionTypes.LEAVE_PROJECT_INIT: + return { ...state, errors: [] } + case actionTypes.LEAVE_PROJECT_SUCCESS: { + return { + ...state, + project: { + ...state.project, + memberships: state.project.memberships?.filter(m => m.id !== action.membershipId) + } + } + } + case actionTypes.LEAVE_PROJECT_ERROR: + return { + ...state, + errors: [...state.errors, { actionType: action.type, ...action.error }] + } + case actionTypes.CLEAR_PROJECT_ERRORS: + return { ...state, errors: [] } + default: + return state + } +} diff --git a/rdmo/projects/assets/js/project/store/configureStore.js b/rdmo/projects/assets/js/project/store/configureStore.js new file mode 100644 index 0000000000..4e77a423f1 --- /dev/null +++ b/rdmo/projects/assets/js/project/store/configureStore.js @@ -0,0 +1,92 @@ +import { applyMiddleware, createStore, combineReducers } from 'redux' +import thunk from 'redux-thunk' + +import { checkStoreId } from 'rdmo/core/assets/js/utils/store' +import { getConfigFromLocalStorage } from 'rdmo/core/assets/js/utils/config' + +import configReducer from 'rdmo/core/assets/js/reducers/configReducer' +import pendingReducer from 'rdmo/core/assets/js/reducers/pendingReducer' +import settingsReducer from 'rdmo/core/assets/js/reducers/settingsReducer' +import templateReducer from 'rdmo/core/assets/js/reducers/templateReducer' +import userReducer from 'rdmo/core/assets/js/reducers/userReducer' + +import projectReducer from '../reducers/projectReducer' + +import * as configActions from 'rdmo/core/assets/js/actions/configActions' +import * as settingsActions from 'rdmo/core/assets/js/actions/settingsActions' +import * as templateActions from 'rdmo/core/assets/js/actions/templateActions' +import * as userActions from 'rdmo/core/assets/js/actions/userActions' + +import { parseLocation } from '../utils/location' +import { projectId } from '../utils/meta' + +import * as projectActions from '../actions/projectActions' + +export default function configureStore() { + // empty localStorage in new session + checkStoreId() + + const middlewares = [thunk] + + if (process.env.NODE_ENV === 'development') { + const { logger } = require('redux-logger') + middlewares.push(logger) + } + + const rootReducer = combineReducers({ + config: configReducer, + pending: pendingReducer, + project: projectReducer, + settings: settingsReducer, + templates: templateReducer, + user: userReducer, + }) + + const initialState = { + config: { + prefix: 'rdmo.project' + } + } + + const store = createStore( + rootReducer, + initialState, + applyMiddleware(...middlewares) + ) + + const getConfigFromLocation = () => { + const { page, itemId, itemAction } = parseLocation() + + store.dispatch(configActions.updateConfig('page', page, false)) + store.dispatch(configActions.updateConfig('itemId', itemId, false)) + store.dispatch(configActions.updateConfig('itemAction', itemAction, false)) + } + + // this event is triggered when the page first loads + window.addEventListener('load', () => { + getConfigFromLocalStorage('rdmo.project').forEach(([path, value]) => { + store.dispatch(configActions.updateConfig(path, value)) + }) + + getConfigFromLocation() + + store.dispatch(settingsActions.fetchSettings()) + store.dispatch(templateActions.fetchTemplates()) + store.dispatch(userActions.fetchCurrentUser()) + + store.dispatch(projectActions.fetchProject()).then(() => { + const { project: projectObj } = store.getState() + const permissions = projectObj.project.project.permissions || {} + if (permissions.can_view_invite) { + store.dispatch(projectActions.fetchProjectInvites(projectId)) + } + }) + }) + + // this event is triggered when when the forward/back buttons are used + window.addEventListener('popstate', () => { + getConfigFromLocation() + }) + + return store +} diff --git a/rdmo/projects/assets/js/project/utils/location.js b/rdmo/projects/assets/js/project/utils/location.js new file mode 100644 index 0000000000..d5014d9242 --- /dev/null +++ b/rdmo/projects/assets/js/project/utils/location.js @@ -0,0 +1,51 @@ +import { baseUrl } from 'rdmo/core/assets/js/utils/meta' +import { projectId } from '../utils/meta' +import { isEmpty } from 'lodash' + +export const parseLocation = () => { + const pathname = window.location.pathname + + const m1 = pathname.match(/\/projects\/\d+\/(?{rolesString}
@@ -159,8 +175,7 @@ const Projects = ({ config, configActions, currentUserObject, projectsActions, p actions: (_content, row) => { const rowUrl = `${baseUrl}/projects/${row.id}` const params = `?next=${window.location.pathname}` - const { isProjectManager, isProjectOwner } = getUserRoles(row, currentUserId, ['managers', 'owners']) - + const perms = row.permissions || {} return (| {% full_name membership.user %} - {% include 'projects/project_detail_memberships_socialaccounts.html' %} + {% include 'projects/old/project_detail_memberships_socialaccounts.html' %} |
{{ membership.user.email }}
diff --git a/rdmo/projects/templates/projects/project_detail_memberships_help.html b/rdmo/projects/templates/projects/old/project_detail_memberships_help.html
similarity index 100%
rename from rdmo/projects/templates/projects/project_detail_memberships_help.html
rename to rdmo/projects/templates/projects/old/project_detail_memberships_help.html
diff --git a/rdmo/projects/templates/projects/project_detail_memberships_socialaccounts.html b/rdmo/projects/templates/projects/old/project_detail_memberships_socialaccounts.html
similarity index 100%
rename from rdmo/projects/templates/projects/project_detail_memberships_socialaccounts.html
rename to rdmo/projects/templates/projects/old/project_detail_memberships_socialaccounts.html
diff --git a/rdmo/projects/templates/projects/project_detail_sidebar.html b/rdmo/projects/templates/projects/old/project_detail_sidebar.html
similarity index 100%
rename from rdmo/projects/templates/projects/project_detail_sidebar.html
rename to rdmo/projects/templates/projects/old/project_detail_sidebar.html
diff --git a/rdmo/projects/templates/projects/project_detail_sidebar_parent_import.html b/rdmo/projects/templates/projects/old/project_detail_sidebar_parent_import.html
similarity index 100%
rename from rdmo/projects/templates/projects/project_detail_sidebar_parent_import.html
rename to rdmo/projects/templates/projects/old/project_detail_sidebar_parent_import.html
diff --git a/rdmo/projects/templates/projects/project_detail_snapshots.html b/rdmo/projects/templates/projects/old/project_detail_snapshots.html
similarity index 98%
rename from rdmo/projects/templates/projects/project_detail_snapshots.html
rename to rdmo/projects/templates/projects/old/project_detail_snapshots.html
index c825276b97..c1353c4c3c 100644
--- a/rdmo/projects/templates/projects/project_detail_snapshots.html
+++ b/rdmo/projects/templates/projects/old/project_detail_snapshots.html
@@ -11,7 +11,7 @@
{% trans 'Snapshots' %}- {% include 'projects/project_detail_snapshots_help.html' %} + {% include 'projects/old/project_detail_snapshots_help.html' %} {% if snapshots %} diff --git a/rdmo/projects/templates/projects/project_detail_snapshots_help.html b/rdmo/projects/templates/projects/old/project_detail_snapshots_help.html similarity index 100% rename from rdmo/projects/templates/projects/project_detail_snapshots_help.html rename to rdmo/projects/templates/projects/old/project_detail_snapshots_help.html diff --git a/rdmo/projects/templates/projects/project_detail_views.html b/rdmo/projects/templates/projects/old/project_detail_views.html similarity index 97% rename from rdmo/projects/templates/projects/project_detail_views.html rename to rdmo/projects/templates/projects/old/project_detail_views.html index 88563582a4..71e15fc71d 100644 --- a/rdmo/projects/templates/projects/project_detail_views.html +++ b/rdmo/projects/templates/projects/old/project_detail_views.html @@ -10,7 +10,7 @@{% trans 'Views' %}- {% include 'projects/project_detail_views_help.html' %} + {% include 'projects/old/project_detail_views_help.html' %} {% if views %} diff --git a/rdmo/projects/templates/projects/project_detail_views_help.html b/rdmo/projects/templates/projects/old/project_detail_views_help.html similarity index 100% rename from rdmo/projects/templates/projects/project_detail_views_help.html rename to rdmo/projects/templates/projects/old/project_detail_views_help.html diff --git a/rdmo/projects/templates/projects/project_answers.html b/rdmo/projects/templates/projects/project_answers.html index ca3a6b8dd3..8172b6550b 100644 --- a/rdmo/projects/templates/projects/project_answers.html +++ b/rdmo/projects/templates/projects/project_answers.html @@ -1,88 +1,8 @@ -{% extends 'core/page.html' %} {% load i18n %} -{% load core_tags %} -{% block sidebar %} +{% blocktrans with title=project.title %}Answers for {{ title }}{% endblocktrans %}++ {% trans 'In the following, we have summarized the information about the project as given by you and your collaborators.' %} + - {% if snapshots %} - -{% trans 'Snapshots' %}-
{% trans 'Options' %}- - -{% trans 'Export' %}-
{% trans 'Attachments' %}-
{% blocktrans with title=project.title %}Answers for {{ title }}{% endblocktrans %}-- {% trans 'In the following, we have summarized the information about the project as given by you and your collaborators.' %} - - - {% include 'projects/project_answers_tree.html' %} - - {% endif %} - - -{% endblock %} +{% include 'projects/project_answers_tree.html' %} diff --git a/rdmo/projects/templates/projects/project_detail.html b/rdmo/projects/templates/projects/project_detail.html index 3d21b98044..60af4152e8 100644 --- a/rdmo/projects/templates/projects/project_detail.html +++ b/rdmo/projects/templates/projects/project_detail.html @@ -1,51 +1,25 @@ -{% extends 'core/page.html' %} -{% load i18n %} +{% extends 'core/bs53/base.html' %} {% load static %} -{% load compress %} -{% load core_tags %} {% block head %} - {% compress css %} - - - {% endcompress %} - {% compress js %} - - {% endcompress %} - + {% endblock %} -{% block sidebar %} - - {% include 'projects/project_detail_sidebar.html' %} - +{% block css %} + + + + {{ block.super }} {% endblock %} -{% block page %} - - {% include 'projects/project_detail_header.html' %} - {% include 'projects/project_detail_issues.html' %} - {% include 'projects/project_detail_views.html' %} - {% include 'projects/project_detail_memberships.html' %} - {% include 'projects/project_detail_invites.html' %} - {% include 'projects/project_detail_snapshots.html' %} - {% include 'projects/project_detail_integrations.html' %} +{% block js %} + + + +{% endblock %} - +{% block content %} - {% render_lang_template 'projects/overlays/project_project_questions' %} - {% render_lang_template 'projects/overlays/project_project_catalog' %} - {% render_lang_template 'projects/overlays/project_project_issues' %} - {% render_lang_template 'projects/overlays/project_project_views' %} - {% render_lang_template 'projects/overlays/project_project_memberships' %} - {% render_lang_template 'projects/overlays/project_project_snapshots' %} - {% render_lang_template 'projects/overlays/project_export_project' %} - {% render_lang_template 'projects/overlays/project_import_project' %} - {% render_lang_template 'projects/overlays/project_support_info' %} + {% endblock %} diff --git a/rdmo/projects/templates/projects/project_view_author_info.html b/rdmo/projects/templates/projects/project_view_author_info.html new file mode 100644 index 0000000000..0b2592246d --- /dev/null +++ b/rdmo/projects/templates/projects/project_view_author_info.html @@ -0,0 +1,7 @@ +{% load i18n %} + ++ {% blocktrans trimmed %} + Like guest, but can edit datasets and questionnaires + {% endblocktrans %} + diff --git a/rdmo/projects/templates/projects/project_view_export.html b/rdmo/projects/templates/projects/project_view_export.html index 2c4b1b8d97..6a4361c64e 100644 --- a/rdmo/projects/templates/projects/project_view_export.html +++ b/rdmo/projects/templates/projects/project_view_export.html @@ -3,6 +3,6 @@ {% block body %} -{{ rendered_view }} +{{ html }} {% endblock %} diff --git a/rdmo/projects/templates/projects/project_view_guest_info.html b/rdmo/projects/templates/projects/project_view_guest_info.html new file mode 100644 index 0000000000..417b227c87 --- /dev/null +++ b/rdmo/projects/templates/projects/project_view_guest_info.html @@ -0,0 +1,7 @@ +{% load i18n %} + ++ {% blocktrans trimmed %} + Can view datasets, questionnaire, and documents + {% endblocktrans %} + diff --git a/rdmo/projects/templates/projects/project_view_invite_member.html b/rdmo/projects/templates/projects/project_view_invite_member.html new file mode 100644 index 0000000000..e1d240903e --- /dev/null +++ b/rdmo/projects/templates/projects/project_view_invite_member.html @@ -0,0 +1,13 @@ +{% load i18n %} + ++ {% blocktrans trimmed %} + Users can be invited by their username (if they already have an account here), or by their e-mail address. + {% endblocktrans %} + + ++ {% blocktrans trimmed %} + Users will receive an e-mail with a link to join the project with the assigned role. + {% endblocktrans %} + diff --git a/rdmo/projects/templates/projects/project_view_invite_member_silently_help.html b/rdmo/projects/templates/projects/project_view_invite_member_silently_help.html new file mode 100644 index 0000000000..c48178e3ad --- /dev/null +++ b/rdmo/projects/templates/projects/project_view_invite_member_silently_help.html @@ -0,0 +1,7 @@ +{% load i18n %} + ++ {% blocktrans trimmed %} + As site manager or admin, you can directly add users without notifying them via e-mail, when you check the following checkbox. + {% endblocktrans %} + diff --git a/rdmo/projects/templates/projects/project_view_invite_member_user_help.html b/rdmo/projects/templates/projects/project_view_invite_member_user_help.html new file mode 100644 index 0000000000..9aa1055876 --- /dev/null +++ b/rdmo/projects/templates/projects/project_view_invite_member_user_help.html @@ -0,0 +1,7 @@ +{% load i18n %} + ++ {% blocktrans trimmed %} + The username or e-mail of the new user. + {% endblocktrans %} + diff --git a/rdmo/projects/templates/projects/project_view_manager_info.html b/rdmo/projects/templates/projects/project_view_manager_info.html new file mode 100644 index 0000000000..96551a1c92 --- /dev/null +++ b/rdmo/projects/templates/projects/project_view_manager_info.html @@ -0,0 +1,7 @@ +{% load i18n %} + ++ {% blocktrans trimmed %} + Like author, but can edit snapshots, project data, and the project team + {% endblocktrans %} + diff --git a/rdmo/projects/templates/projects/project_view_owner_info.html b/rdmo/projects/templates/projects/project_view_owner_info.html new file mode 100644 index 0000000000..6af9f335af --- /dev/null +++ b/rdmo/projects/templates/projects/project_view_owner_info.html @@ -0,0 +1,7 @@ +{% load i18n %} + ++ {% blocktrans trimmed %} + Has full rights + {% endblocktrans %} + diff --git a/rdmo/projects/templates/projects/project_view_parent_help.html b/rdmo/projects/templates/projects/project_view_parent_help.html new file mode 100644 index 0000000000..27ec46cfb8 --- /dev/null +++ b/rdmo/projects/templates/projects/project_view_parent_help.html @@ -0,0 +1,7 @@ +{% load i18n %} + ++ {% blocktrans trimmed %} + By linking to a parent project, you can transfer settings and data from that project. + {% endblocktrans %} + diff --git a/rdmo/projects/tests/conftest.py b/rdmo/projects/tests/conftest.py index 1c99a862de..a5b4f07400 100644 --- a/rdmo/projects/tests/conftest.py +++ b/rdmo/projects/tests/conftest.py @@ -1,3 +1,5 @@ +from pathlib import Path + import pytest from django.apps import apps @@ -13,3 +15,15 @@ def enable_project_views_sync(settings): def enable_project_tasks_sync(settings): settings.PROJECT_TASKS_SYNC = True apps.get_app_config('projects').ready() + +@pytest.fixture +def xml_path_project(settings): + xml_file = Path(settings.BASE_DIR) / 'xml' / 'project.xml' + assert xml_file.exists(), f"Missing test XML at {xml_file}" + return xml_file + +@pytest.fixture +def xml_path_error(settings): + xml_file = Path(settings.BASE_DIR) / 'xml' / 'error.xml' + assert xml_file.exists(), f"Missing test XML at {xml_file}" + return xml_file diff --git a/rdmo/projects/tests/e2e/test_frontend_project_detail.py b/rdmo/projects/tests/e2e/test_frontend_project_detail.py index 565a3b16d7..6ce15166c5 100644 --- a/rdmo/projects/tests/e2e/test_frontend_project_detail.py +++ b/rdmo/projects/tests/e2e/test_frontend_project_detail.py @@ -18,5 +18,5 @@ def test_project_detail_page(page: Page): page.screenshot(path="screenshots/projects/project-detail.png", full_page=True) # Assert project detail page - expect(page.get_by_role("heading", name="Test")).to_be_visible() - expect(page.get_by_role("link", name="Answer questions")).to_be_visible() + expect(page.get_by_text("Test")).to_be_visible() + expect(page.get_by_text("Interview")).to_be_visible() diff --git a/rdmo/projects/tests/helpers/xml.py b/rdmo/projects/tests/helpers/xml.py new file mode 100644 index 0000000000..6b32d3b2a7 --- /dev/null +++ b/rdmo/projects/tests/helpers/xml.py @@ -0,0 +1,39 @@ +import io +import xml.etree.ElementTree as ET +from xml.sax.saxutils import XMLGenerator + +from rdmo.projects.renderers import XMLRenderer + + +def add_memberships_to_xml(xml_path: str, members: list[dict]) -> None: + """ + Replace or add |