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 @@

{% trans "API token" %}

{% csrf_token %} - + +

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 "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..3a90c9abdc 100644 --- a/rdmo/accounts/templates/account/email_confirm.html +++ b/rdmo/accounts/templates/account/email_confirm.html @@ -1,4 +1,4 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} {% load account %} @@ -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..4d30561981 100644 --- a/rdmo/accounts/templates/account/login.html +++ b/rdmo/accounts/templates/account/login.html @@ -1,10 +1,24 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} {% block page %}

{% 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 %} +
+ {% trans 'Forgot your password?' %} +
+ {% 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..0ce41378a6 100644 --- a/rdmo/accounts/templates/account/logout.html +++ b/rdmo/accounts/templates/account/logout.html @@ -1,4 +1,4 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} {% block page %} @@ -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..a39659522c 100644 --- a/rdmo/accounts/templates/account/password_change.html +++ b/rdmo/accounts/templates/account/password_change.html @@ -1,5 +1,6 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} +{% load core_tags %} {% block page %} @@ -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..c75ac6ad91 100644 --- a/rdmo/accounts/templates/account/password_reset.html +++ b/rdmo/accounts/templates/account/password_reset.html @@ -1,5 +1,6 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} +{% load core_tags %} {% block page %} @@ -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..e965196545 100644 --- a/rdmo/accounts/templates/account/password_reset_done.html +++ b/rdmo/accounts/templates/account/password_reset_done.html @@ -1,4 +1,4 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} {% block page %} diff --git a/rdmo/accounts/templates/account/password_reset_from_key.html b/rdmo/accounts/templates/account/password_reset_from_key.html index 8bddbe38d0..b38945e22d 100644 --- a/rdmo/accounts/templates/account/password_reset_from_key.html +++ b/rdmo/accounts/templates/account/password_reset_from_key.html @@ -1,5 +1,6 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} +{% load core_tags %} {% block page %} @@ -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..91ba493c0f 100644 --- a/rdmo/accounts/templates/account/password_reset_from_key_done.html +++ b/rdmo/accounts/templates/account/password_reset_from_key_done.html @@ -1,4 +1,4 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} {% load core_tags %} @@ -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..6b2d4c3611 100644 --- a/rdmo/accounts/templates/account/password_set.html +++ b/rdmo/accounts/templates/account/password_set.html @@ -1,4 +1,4 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} {% block page %} @@ -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..5fa20a1d54 100644 --- a/rdmo/accounts/templates/account/signup.html +++ b/rdmo/accounts/templates/account/signup.html @@ -1,9 +1,9 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} {% block page %} -

{% 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..e58ffc1adb 100644 --- a/rdmo/accounts/templates/account/signup_closed.html +++ b/rdmo/accounts/templates/account/signup_closed.html @@ -3,7 +3,7 @@ {% block page %} -

{% 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..92e6111c92 100644 --- a/rdmo/accounts/templates/account/terms_of_use.html +++ b/rdmo/accounts/templates/account/terms_of_use.html @@ -1,4 +1,4 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} {% block page %} 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..a13ccec6d8 --- /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 page %} + + {{ 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..3f6d07a1e4 100644 --- a/rdmo/accounts/templates/account/verification_sent.html +++ b/rdmo/accounts/templates/account/verification_sent.html @@ -1,4 +1,4 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} {% block page %} diff --git a/rdmo/accounts/templates/account/verified_email_required.html b/rdmo/accounts/templates/account/verified_email_required.html index d011aedd4d..0f06d179f3 100644 --- a/rdmo/accounts/templates/account/verified_email_required.html +++ b/rdmo/accounts/templates/account/verified_email_required.html @@ -1,4 +1,4 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.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..a120efb0a5 100644 --- a/rdmo/accounts/templates/profile/profile_remove_closed.html +++ b/rdmo/accounts/templates/profile/profile_remove_closed.html @@ -1,9 +1,9 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} {% block page %} -

{% 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..b22c800a04 100644 --- a/rdmo/accounts/templates/profile/profile_remove_failed.html +++ b/rdmo/accounts/templates/profile/profile_remove_failed.html @@ -1,9 +1,9 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} {% block page %} -

{% 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..e6cdf502a5 100644 --- a/rdmo/accounts/templates/profile/profile_remove_form.html +++ b/rdmo/accounts/templates/profile/profile_remove_form.html @@ -1,4 +1,4 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} {% load core_tags %} @@ -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..eb6cbe6ab9 100644 --- a/rdmo/accounts/templates/profile/profile_remove_success.html +++ b/rdmo/accounts/templates/profile/profile_remove_success.html @@ -1,9 +1,9 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} {% block page %} -

{% 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..1fe9f2d0d6 100644 --- a/rdmo/accounts/templates/profile/profile_update_closed.html +++ b/rdmo/accounts/templates/profile/profile_update_closed.html @@ -1,9 +1,9 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} {% block page %} -

{% 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..a0e083a197 100644 --- a/rdmo/accounts/templates/profile/profile_update_form.html +++ b/rdmo/accounts/templates/profile/profile_update_form.html @@ -1,5 +1,6 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} +{% load core_tags %} {% block page %} @@ -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..69ad9f0954 100644 --- a/rdmo/accounts/templates/socialaccount/authentication_error.html +++ b/rdmo/accounts/templates/socialaccount/authentication_error.html @@ -1,4 +1,4 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} {% load account %} {% load socialaccount %} diff --git a/rdmo/accounts/templates/socialaccount/connections.html b/rdmo/accounts/templates/socialaccount/connections.html index 1f39855b24..71aa001540 100644 --- a/rdmo/accounts/templates/socialaccount/connections.html +++ b/rdmo/accounts/templates/socialaccount/connections.html @@ -1,78 +1,76 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} -{% load static %} -{% load accounts_tags %} {% block page %}

{% 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..34e14b82b4 100644 --- a/rdmo/accounts/templates/socialaccount/login.html +++ b/rdmo/accounts/templates/socialaccount/login.html @@ -1,21 +1,45 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} -{% block head_title %}{% trans "Sign In" %}{% endblock %} +{% block head_title %} +{% trans "Sign In" %} +{% endblock %} {% 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..25e766d2c8 100644 --- a/rdmo/accounts/templates/socialaccount/login_cancelled.html +++ b/rdmo/accounts/templates/socialaccount/login_cancelled.html @@ -1,4 +1,4 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} {% load account %} {% load socialaccount %} diff --git a/rdmo/accounts/templates/socialaccount/signup.html b/rdmo/accounts/templates/socialaccount/signup.html index 3eccb49c7d..fd43dcd644 100644 --- a/rdmo/accounts/templates/socialaccount/signup.html +++ b/rdmo/accounts/templates/socialaccount/signup.html @@ -1,59 +1,20 @@ -{% extends 'core/page.html' %} +{% extends 'core/bs53/page.html' %} {% load i18n %} {% block page %} -

{% 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 acddb03df6..f6078a2f44 100644 --- a/rdmo/accounts/templatetags/accounts_tags.py +++ b/rdmo/accounts/templatetags/accounts_tags.py @@ -26,13 +26,13 @@ def user_data_as_dl(user): @register.simple_tag(takes_context=True) -def get_inactive_providers(context={}): - from allauth.socialaccount.templatetags.socialaccount import get_providers - if 'form' in context: - accounts = context['form'].accounts - providers = [account.provider for account in accounts] - return [ - provider - for provider in get_providers(context) - if provider.id not in providers - ] +def get_current_provider_ids(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 new file mode 100644 index 0000000000..916dce32f7 --- /dev/null +++ b/rdmo/accounts/tests/test_models.py @@ -0,0 +1,25 @@ +import pytest + +from django.contrib.auth import get_user_model + +normal_users = ( + ('user', 'user', 'user@example.com'), +) + +site_managers = ( + ('site', 'site', 'site@example.com'), +) + + +@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 4916a136c7..1f0a898814 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..32fcfd27ee 100644 --- a/rdmo/accounts/utils.py +++ b/rdmo/accounts/utils.py @@ -1,12 +1,12 @@ 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 django.utils.crypto import get_random_string +from django.utils.text import slugify -from .models import Role from .settings import GROUPS log = logging.getLogger(__name__) @@ -19,19 +19,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) @@ -93,3 +80,59 @@ def get_user_from_db_or_none(username: str, email: str): except ObjectDoesNotExist: log.error('Retrieval of user "%s" with email "%s" failed, user does not exist', username, email) return None + + +def find_user(user_id=None, username="", email=""): + username = (username or "").strip() + email = (email or "").strip().lower() + + if user_id: + user = get_user_model().objects.filter(pk=user_id).first() + if user: + return user + + if username: + user = get_user_model().objects.filter(username=username).first() + if user: + return user + + if email: + return get_user_model().objects.filter(email__iexact=email).first() + + return None + + +def make_unique_username(seed: str) -> str: + base = slugify(seed) or "user" + user_model = get_user_model() + for suffix in range(0, 8): + candidate = base if suffix == 0 else f"{base}_{suffix}" + if not user_model.objects.filter(username=candidate).exists(): + return candidate + # fallback + return f"{base}_{get_random_string(8)}" + + +def create_user_from_fields(username, email, first_name, last_name): + username = (username or "").strip() + email = (email or "").strip().lower() + first_name = (first_name or "").strip() + last_name = (last_name or "").strip() + + base = username or (email.split("@")[0] if email else "") or "imported" + unique = make_unique_username(base) + + user = get_user_model().objects.create_user( + username=unique, + email=email, + first_name=first_name, + last_name=last_name, + is_active=True, + ) + user.set_unusable_password() + user.save(update_fields=["password"]) + + if unique != base: + log.info("Username '%s' taken, created unique name '%s'.", base, unique) + + return user diff --git a/rdmo/accounts/views.py b/rdmo/accounts/views.py index e785155826..2088e607c7 100644 --- a/rdmo/accounts/views.py +++ b/rdmo/accounts/views.py @@ -4,7 +4,7 @@ from django.conf import settings from django.contrib.auth import logout from django.contrib.auth.decorators import login_required -from django.http import HttpResponseNotAllowed, HttpResponseRedirect +from django.http import HttpResponseRedirect from django.shortcuts import redirect, render from django.urls import reverse @@ -26,14 +26,9 @@ def profile_update(request): form = ProfileForm(request.POST or None, instance=request.user) - if request.method == 'POST': - if 'cancel' in request.POST: - log.debug('User %s update cancelled', request.user.username) - return HttpResponseRedirect(get_next(request)) - - if form.is_valid(): - form.save() - return HttpResponseRedirect(get_next(request)) + if request.method == 'POST' and form.is_valid(): + form.save() + return HttpResponseRedirect(get_next(request)) return render(request, 'profile/profile_update_form.html', { 'form': form, @@ -45,22 +40,12 @@ def profile_update(request): @login_required() def remove_user(request): - if not settings.PROFILE_DELETE: - log.info('Remove user form is disabled in settings PROFILE_DELETE') - return render(request, 'profile/profile_remove_closed.html') - form = RemoveForm(request.POST or None, request=request) - log.debug('Remove user form initialized for "%s"', request.user.username) - - if request.method == 'POST': - if 'cancel' in request.POST: - log.info('User %s removal cancelled', str(request.user)) + if settings.PROFILE_DELETE: + log.debug('Remove user %s', request.user.username) - if settings.PROFILE_UPDATE: - return HttpResponseRedirect('/account') - else: - return HttpResponseRedirect('/') + form = RemoveForm(request.POST or None, user=request.user) - if form.is_valid(): + if request.method == 'POST' and form.is_valid(): user_is_deleted = delete_user(user=request.user, email=request.POST['email'], password=request.POST.get('password', None)) @@ -72,10 +57,12 @@ def remove_user(request): log.info('Remove user, deletion failed for %s', request.user.username) return render(request, 'profile/profile_remove_failed.html') - return render(request, 'profile/profile_remove_form.html', { - 'form': form, - 'next': get_referer_path_info(request, default='/') - }) + return render(request, 'profile/profile_remove_form.html', { + 'form': form, + 'next': get_referer_path_info(request, default='/') + }) + else: + return render(request, 'profile/profile_remove_closed.html') def terms_of_use(request): @@ -113,30 +100,19 @@ def shibboleth_logout(request): def terms_of_use_accept(request): - if not request.user.is_authenticated: return redirect("account_login") + # Use the form to handle both update and delete actions + form = AcceptConsentForm(request.POST or None, user=request.user) + if request.method == "POST": - # Use the form to handle both update and delete actions - form = AcceptConsentForm(request.POST, user=request.user) if form.is_valid(): consent_saved = form.save(request.session) # saves the consent and sets the session key if consent_saved: return redirect("home") - # If consent was not saved, re-render the form with an error - return render(request, - "account/terms_of_use_accept_form.html", - {"form": form}, - ) - - elif request.method == "GET": - has_consented = ConsentFieldValue.objects.filter(user=request.user).exists() - return render( - request, - "account/terms_of_use_accept_form.html", - {"has_consented": has_consented}, - ) - - return HttpResponseNotAllowed(["GET", "POST"]) + return render(request, "account/terms_of_use_accept.html", { + "form": form, + "has_consented": ConsentFieldValue.objects.filter(user=request.user).exists() + }) diff --git a/rdmo/accounts/viewsets.py b/rdmo/accounts/viewsets.py index 09604f146f..da01040c96 100644 --- a/rdmo/accounts/viewsets.py +++ b/rdmo/accounts/viewsets.py @@ -10,7 +10,6 @@ from rdmo.core.permissions import HasModelPermission, HasObjectPermission from .serializers.v1 import UserSerializer -from .utils import is_site_manager class UserViewSetMixin: @@ -19,7 +18,7 @@ def get_users_for_user(self, user): if user.is_authenticated: if user.has_perm('auth.view_user'): return get_user_model().objects.all() - elif is_site_manager(user): + elif user.role.is_site_manager: return get_user_model().objects.filter(role__member__id__in=user.role.manager.all()).distinct() return get_user_model().objects.none() diff --git a/rdmo/core/assets/js/_bs53/app.js b/rdmo/core/assets/js/_bs53/app.js new file mode 100644 index 0000000000..e2204695ae --- /dev/null +++ b/rdmo/core/assets/js/_bs53/app.js @@ -0,0 +1 @@ +// This file produces an empty app.js script file, which can be overloaded in a theme. diff --git a/rdmo/core/assets/js/_bs53/base.js b/rdmo/core/assets/js/_bs53/base.js new file mode 100644 index 0000000000..ee9a4d14c4 --- /dev/null +++ b/rdmo/core/assets/js/_bs53/base.js @@ -0,0 +1,14 @@ +window.loopHeaderBackgroundImage = (timeout) => { + const images = document.querySelectorAll('#header .header-image') + + let index = 0 + + const setHeaderBackgroundImage = () => { + images[index].classList.remove('header-image-visible') + index = (index == images.length - 1) ? 0 : index + 1 + images[index].classList.toggle('header-image-visible') + setTimeout(() => setHeaderBackgroundImage(), timeout) + } + + setTimeout(() => setHeaderBackgroundImage(), timeout) +} diff --git a/rdmo/core/assets/js/_bs53/bootstrap.js b/rdmo/core/assets/js/_bs53/bootstrap.js new file mode 100644 index 0000000000..ae9264d525 --- /dev/null +++ b/rdmo/core/assets/js/_bs53/bootstrap.js @@ -0,0 +1,2 @@ +import 'bootstrap' +window.bootstrap = require('bootstrap') diff --git a/rdmo/core/assets/js/_bs53/components/Modal.js b/rdmo/core/assets/js/_bs53/components/Modal.js new file mode 100644 index 0000000000..e3045198a0 --- /dev/null +++ b/rdmo/core/assets/js/_bs53/components/Modal.js @@ -0,0 +1,73 @@ +import React, { useEffect, useRef } from 'react' +import PropTypes from 'prop-types' +import { Modal as BootstrapModal } from 'bootstrap' + +const Modal = ({ title, show, onClose, onSubmit, submitLabel, submitProps, children, modalProps = {}, size = '' }) => { + const modalRef = useRef(null) + + useEffect(() => { + const modalElement = modalRef.current + if (!modalElement) return + + const modal = BootstrapModal.getOrCreateInstance(modalElement, { + backdrop: 'static', + keyboard: true, + ...modalProps + }) + if (show) { + modal.show() + } + return () => modal.hide() + }, [show]) + + return ( +
    +
    +
    +
    +

    {title}

    + +
    + + { + children && ( +
    + {children} +
    + ) + } + +
    + { + onSubmit && ( + + ) + } + +
    +
    +
    +
    + ) +} + +Modal.propTypes = { + title: PropTypes.string.isRequired, + show: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + onSubmit: PropTypes.func, + submitLabel: PropTypes.string, + submitProps: PropTypes.object, + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node + ]), + modalProps: PropTypes.object, + size: PropTypes.string +} + +export default Modal diff --git a/rdmo/core/assets/js/_bs53/components/Tooltip.js b/rdmo/core/assets/js/_bs53/components/Tooltip.js new file mode 100644 index 0000000000..d4711fb279 --- /dev/null +++ b/rdmo/core/assets/js/_bs53/components/Tooltip.js @@ -0,0 +1,33 @@ +import React, { useEffect, useRef } from 'react' +import { renderToString } from 'react-dom/server' +import PropTypes from 'prop-types' +import { Tooltip as BootstrapTooltip } from 'bootstrap' + +const Tooltip = ({ title, children, placement = 'bottom', tooltipProps = {} }) => { + const ref = useRef(null) + + useEffect(() => { + if (title) { + // console.log(renderToString(title)) + const t = new BootstrapTooltip(ref.current, { + title: renderToString(title), + placement, + html: true, + delay: 200, + ...tooltipProps + }) + return () => t.dispose() + } + }, [title]) + + return React.cloneElement(children, { ref }) +} + +Tooltip.propTypes = { + title: PropTypes.node.isRequired, + children: PropTypes.node.isRequired, + placement: PropTypes.string, + tooltipProps: PropTypes.object, +} + +export default Tooltip diff --git a/rdmo/core/assets/js/_bs53/components/index.js b/rdmo/core/assets/js/_bs53/components/index.js new file mode 100644 index 0000000000..8306153fc5 --- /dev/null +++ b/rdmo/core/assets/js/_bs53/components/index.js @@ -0,0 +1,2 @@ +export { default as Modal } from './Modal' +export { default as Tooltip } from './Tooltip' diff --git a/rdmo/core/assets/js/actions/actionTypes.js b/rdmo/core/assets/js/actions/actionTypes.js index 1f97c54f3d..16cc7203d5 100644 --- a/rdmo/core/assets/js/actions/actionTypes.js +++ b/rdmo/core/assets/js/actions/actionTypes.js @@ -15,3 +15,7 @@ export const FETCH_TEMPLATES_SUCCESS = 'FETCH_TEMPLATES_SUCCESS' export const FETCH_CURRENT_USER_ERROR = 'FETCH_CURRENT_USER_ERROR' export const FETCH_CURRENT_USER_INIT = 'FETCH_CURRENT_USER_INIT' export const FETCH_CURRENT_USER_SUCCESS = 'FETCH_CURRENT_USER_SUCCESS' + +export const FETCH_CATALOGS_ERROR = 'FETCH_CATALOGS_ERROR' +export const FETCH_CATALOGS_INIT = 'FETCH_CATALOGS_INIT' +export const FETCH_CATALOGS_SUCCESS = 'FETCH_CATALOGS_SUCCESS' diff --git a/rdmo/core/assets/js/components/Select.js b/rdmo/core/assets/js/components/Select.js index ef65c0f7fb..22961c3f02 100644 --- a/rdmo/core/assets/js/components/Select.js +++ b/rdmo/core/assets/js/components/Select.js @@ -2,37 +2,35 @@ import React from 'react' import PropTypes from 'prop-types' import ReactSelect from 'react-select' -const Select = ({ options, onChange, placeholder, value }) => { +const Select = ({ options, onChange, value, ...props }) => { const selectedOption = options.find(option => option.value === value) || null + const handleChange = (selected) => { onChange(selected ? selected.value : null) } return ( -
    - -
    + ) } Select.propTypes = { - value: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), options: PropTypes.arrayOf( PropTypes.shape({ - value: PropTypes.string.isRequired, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired }) ).isRequired, onChange: PropTypes.func, - placeholder: PropTypes.string } export default Select diff --git a/rdmo/core/assets/js/components/forms/Input.js b/rdmo/core/assets/js/components/forms/Input.js new file mode 100644 index 0000000000..9562901987 --- /dev/null +++ b/rdmo/core/assets/js/components/forms/Input.js @@ -0,0 +1,70 @@ +import React, { useEffect, useState } from 'react' +import PropTypes from 'prop-types' +import classNames from 'classnames' +import { isEmpty, isNil, uniqueId } from 'lodash' + +import { useDebouncedCallback } from 'use-debounce' + +const Input = ({ type = 'text', className, debounce, label, placeholder, help, isDisabled, errors, value, onChange }) => { + const id = uniqueId('input-') + + // store the value in a local state (only when debouncing) + const [inputValue, setInputValue] = isNil(debounce) ? [value, () => {}] : useState(value) + + // use the debounce hook on the onChange callback (only when debouncing) + const callOnChange = isNil(debounce) ? (value) => onChange(value) + : useDebouncedCallback((value) => onChange(value), debounce) + + // update the local state if the value prop changes (only when debouncing) + useEffect(() => setInputValue(value), [value]) + + const handleChange = (event) => { + const value = event.target.value + + setInputValue(value) // will update the local state (only when debouncing) + callOnChange(value) // will call onChange (with or without debouncing) + } + + return ( +
    + + + + { + errors && ( +
    + {errors.map((error, index) =>
    {error}
    )} +
    + ) + } + { + help &&
    {help}
    + } +
    + ) +} + +Input.propTypes = { + type: PropTypes.string, + className: PropTypes.string, + debounce: PropTypes.number, + label: PropTypes.string, + placeholder: PropTypes.string, + help: PropTypes.string, + isDisabled: PropTypes.bool, + errors: PropTypes.array, + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired +} + +export default Input diff --git a/rdmo/core/assets/js/components/forms/Select.js b/rdmo/core/assets/js/components/forms/Select.js new file mode 100644 index 0000000000..dafcdf19e5 --- /dev/null +++ b/rdmo/core/assets/js/components/forms/Select.js @@ -0,0 +1,75 @@ +import React from 'react' +import ReactSelect from 'react-select' +import PropTypes from 'prop-types' +import classNames from 'classnames' +import { isArray, isEmpty, isNil, uniqueId } from 'lodash' + +const Select = ({ className, label, help, placeholder, isClearable, isDisabled, isMulti, options, errors, value, onChange }) => { + const id = uniqueId('select-') + + // lookup value(s) in the options array + const getValue = () => ( + isArray(value) ? options.filter(option => (value.includes(option.value))) + : options.find(option => (option.value == value)) + ) + + const handleChange = (option) => { + if (isNil(option)) { + onChange(null) + } else if (isArray(option)) { + onChange(option.map(option => option.value)) + } else { + onChange(option.value) + } + } + + return ( +
    + + + classNames('form-select') + }} + placeholder={placeholder} + isClearable={isClearable} + isDisabled={isDisabled} + isMulti={isMulti} + options={options} + value={getValue()} + onChange={handleChange} + /> + + { + errors && ( +
    + {errors.map((error, index) =>
    {error}
    )} +
    + ) + } + { + help &&
    {help}
    + } +
    + ) +} + +Select.propTypes = { + className: PropTypes.string, + label: PropTypes.string, + help: PropTypes.string, + placeholder: PropTypes.string, + isClearable: PropTypes.bool, + isDisabled: PropTypes.bool, + isMulti: PropTypes.bool, + options: PropTypes.array, + errors: PropTypes.array, + value: PropTypes.oneOfType([PropTypes.array, PropTypes.string]), + onChange: PropTypes.func.isRequired +} + +export default Select diff --git a/rdmo/core/assets/js/components/forms/Textarea.js b/rdmo/core/assets/js/components/forms/Textarea.js new file mode 100644 index 0000000000..66219cd093 --- /dev/null +++ b/rdmo/core/assets/js/components/forms/Textarea.js @@ -0,0 +1,70 @@ +import React, { useEffect, useState } from 'react' +import PropTypes from 'prop-types' +import classNames from 'classnames' +import { isEmpty, isNil, uniqueId } from 'lodash' + +import { useDebouncedCallback } from 'use-debounce' + +const Textarea = ({ rows, className, debounce, label, placeholder, help, isDisabled, errors, value, onChange }) => { + const id = uniqueId('textarea-') + + // store the value in a local state (only when debouncing) + const [textareaValue, setTextareaValue] = isNil(debounce) ? [value, () => {}] : useState(value) + + // use the debounce hook on the onChange callback (only when debouncing) + const callOnChange = isNil(debounce) ? (value) => onChange(value) + : useDebouncedCallback((value) => onChange(value), debounce) + + // update the local state if the value prop changes (only when debouncing) + useEffect(() => setTextareaValue(value), [value]) + + const handleChange = (event) => { + const value = event.target.value + + setTextareaValue(value) // will update the local state (only when debouncing) + callOnChange(value) // will call onChange (with or without debouncing) + } + + return ( +
    + + + + + {% for error in field.errors %} +
    {{ error }}
    + {% endfor %} +
    diff --git a/rdmo/core/templates/core/bs53/home.html b/rdmo/core/templates/core/bs53/home.html new file mode 100644 index 0000000000..d6aa512b31 --- /dev/null +++ b/rdmo/core/templates/core/bs53/home.html @@ -0,0 +1,14 @@ +{% extends 'core/bs53/base.html' %} +{% load static %} +{% load i18n %} + +{% block content %} + +{% get_current_language as lang %} +{% if lang == 'en' %} + {% include 'core/bs53/home_en.html' %} +{% elif lang == 'de' %} + {% include 'core/bs53/home_de.html' %} +{% endif %} + +{% endblock %} diff --git a/rdmo/core/templates/core/bs53/home_de.html b/rdmo/core/templates/core/bs53/home_de.html new file mode 100644 index 0000000000..20319453f3 --- /dev/null +++ b/rdmo/core/templates/core/bs53/home_de.html @@ -0,0 +1,100 @@ +{% load static %} + + + +
    +
    +
    +
    +
    +

    Lorem ipsum dolor sit amet

    + +

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

    + +

    + Lorem ipsum dolor sit amet: +

    + +

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

    Lorem ipsum dolor sit amet, consetetur sadipscing elitr?

    + +

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

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

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

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

    Lorem ipsum dolor sit amet

    + +

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

    + +

    + Lorem ipsum dolor sit amet: +

    + +

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

    Lorem ipsum dolor sit amet, consetetur sadipscing elitr?

    + +

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

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

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

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

    {{ image.attribution|markdown }}

    +
    +{% endfor %} + + diff --git a/rdmo/core/templates/core/bs53/home_login.html b/rdmo/core/templates/core/bs53/home_login.html new file mode 100644 index 0000000000..713933256a --- /dev/null +++ b/rdmo/core/templates/core/bs53/home_login.html @@ -0,0 +1,15 @@ +
    + {% if settings.LOGIN_FORM %} + {% include 'account/login_form_inline.html' %} + {% endif %} + + {% if settings.SHIBBOLETH %} + {% include 'account/login_shibboleth.html' %} + {% endif %} + + {% if settings.SOCIALACCOUNT %} +
    + {% include "socialaccount/snippets/provider_list.html" with process="login" button_class="btn-light" %} +
    + {% endif %} +
    diff --git a/rdmo/core/templates/core/bs53/page.html b/rdmo/core/templates/core/bs53/page.html new file mode 100644 index 0000000000..0e4f271396 --- /dev/null +++ b/rdmo/core/templates/core/bs53/page.html @@ -0,0 +1,16 @@ +{% extends 'core/bs53/base.html' %} + +{% block content %} + +
    +
    +
    + {% block page %}{% endblock %} +
    + +
    +
    + +{% endblock %} diff --git a/rdmo/core/templatetags/core_tags.py b/rdmo/core/templatetags/core_tags.py index 530c5fb157..0427d1a8de 100644 --- a/rdmo/core/templatetags/core_tags.py +++ b/rdmo/core/templatetags/core_tags.py @@ -22,9 +22,9 @@ def i18n_switcher(): for language, language_string in settings.LANGUAGES: url = reverse('i18n_switcher', args=[language]) if language == translation.get_language(): - string += f"
  • {language_string}
  • " + string += f"
  • {language_string}
  • " else: - string += f"
  • {language_string}
  • " + string += f"
  • {language_string}
  • " return mark_safe(string) @@ -50,6 +50,18 @@ def render_lang_template(template_name, escape_html=False): return '' +@register.simple_tag() +def bootstrap_form_field(field, **kwargs): + context = { + 'field': field + } + + if field.widget_type in ['text', 'password']: + return render_to_string('core/bs53/forms/bootstrap_input.html', context) + else: + return render_to_string(f'core/bs53/forms/bootstrap_{field.widget_type}.html', context) + + @register.simple_tag(takes_context=True) def bootstrap_form(context, **kwargs): form_context = {} diff --git a/rdmo/core/tests/test_openapi.py b/rdmo/core/tests/test_openapi.py index 51dfd154a6..5447105878 100644 --- a/rdmo/core/tests/test_openapi.py +++ b/rdmo/core/tests/test_openapi.py @@ -10,6 +10,7 @@ 'anonymous' ) +n_path = 143 @pytest.mark.parametrize('username', users) def test_openapi_schema(db, client, login, settings, username): @@ -21,7 +22,7 @@ def test_openapi_schema(db, client, login, settings, username): assert response.status_code == 200 schema = yaml.safe_load(response.content) assert schema['openapi'] == '3.0.3' - assert len(schema['paths']) == 124 + assert len(schema['paths']) == n_path else: assert response.status_code == 302 diff --git a/rdmo/core/tests/test_tags.py b/rdmo/core/tests/test_tags.py index 0aa74a73fa..e97728aff5 100644 --- a/rdmo/core/tests/test_tags.py +++ b/rdmo/core/tests/test_tags.py @@ -19,6 +19,6 @@ def test_i18n_switcher(rf): rendered_template = Template(template).render(context) for language in settings.LANGUAGES: if language == settings.LANGUAGES[0]: - assert '{}'.format(*language) in rendered_template + assert 'href="/i18n/{}/">{}'.format(*language) in rendered_template else: - assert'{}'.format(*language) in rendered_template + assert 'href="/i18n/{}/">{}'.format(*language) in rendered_template diff --git a/rdmo/core/views.py b/rdmo/core/views.py index f8d9603866..a85c0b812a 100644 --- a/rdmo/core/views.py +++ b/rdmo/core/views.py @@ -33,15 +33,15 @@ def home(request): if settings.LOGIN_FORM: if settings.ACCOUNT or settings.SOCIALACCOUNT: from rdmo.accounts.account import LoginForm - return render(request, 'core/home.html', { + return render(request, 'core/bs53/home.html', { 'form': LoginForm(), 'signup_url': reverse("account_signup") }) else: from django.contrib.auth.forms import AuthenticationForm - return render(request, 'core/home.html', {'form': AuthenticationForm()}) + return render(request, 'core/bs53/home.html', {'form': AuthenticationForm()}) else: - return render(request, 'core/home.html') + return render(request, 'core/bs53/home.html') @login_required diff --git a/rdmo/management/viewsets.py b/rdmo/management/viewsets.py index 2b5ec2d295..ed73e40d7a 100644 --- a/rdmo/management/viewsets.py +++ b/rdmo/management/viewsets.py @@ -9,7 +9,7 @@ from rest_framework.response import Response from rest_framework.serializers import ValidationError -from rdmo.core.imports import handle_uploaded_file +from rdmo.core.imports import store_temp_file from rdmo.core.permissions import CanToggleElementCurrentSite from rdmo.core.utils import get_model_field_meta, is_truthy from rdmo.core.xml import parse_xml_to_elements @@ -40,7 +40,7 @@ def create(self, request, *args, **kwargs): except KeyError as e: raise ValidationError({'file': [_('This field may not be blank.')]}) from e else: - import_tmpfile_name = handle_uploaded_file(uploaded_file) + import_tmpfile_name = store_temp_file(uploaded_file) try: # step 1.1: initialize parse_xml_to_elements # step 2-6: parse xml, validate and convert to diff --git a/rdmo/projects/assets/js/common/api/CatalogsApi.js b/rdmo/projects/assets/js/common/api/CatalogsApi.js new file mode 100644 index 0000000000..e4efa22b92 --- /dev/null +++ b/rdmo/projects/assets/js/common/api/CatalogsApi.js @@ -0,0 +1,15 @@ +import BaseApi from 'rdmo/core/assets/js/api/BaseApi' + +class CatalogsApi extends BaseApi { + static fetchCatalogs() { + return fetch('/api/v1/projects/catalogs/').then(response => { + if (response.ok) { + return response.json() + } else { + throw new Error(response.statusText) + } + }) + } +} + +export default CatalogsApi diff --git a/rdmo/projects/assets/js/common/constants/roles.js b/rdmo/projects/assets/js/common/constants/roles.js new file mode 100644 index 0000000000..4f9a6da8b3 --- /dev/null +++ b/rdmo/projects/assets/js/common/constants/roles.js @@ -0,0 +1,6 @@ +export const roleOptions = [ + { value: 'owner', label: gettext('Owner') }, + { value: 'manager', label: gettext('Manager') }, + { value: 'author', label: gettext('Author') }, + { value: 'guest', label: gettext('Guest') } +] diff --git a/rdmo/projects/assets/js/project.js b/rdmo/projects/assets/js/project.js new file mode 100644 index 0000000000..6f5390751c --- /dev/null +++ b/rdmo/projects/assets/js/project.js @@ -0,0 +1,20 @@ +import React from 'react' +import { createRoot } from 'react-dom/client' +import { Provider } from 'react-redux' + +import configureStore from './project/store/configureStore' + +import { DndProvider } from 'react-dnd' +import { HTML5Backend } from 'react-dnd-html5-backend' + +import Project from './project/components/Project' + +const store = configureStore() + +createRoot(document.getElementById('main')).render( + + + + + +) diff --git a/rdmo/projects/assets/js/project/actions/actionTypes.js b/rdmo/projects/assets/js/project/actions/actionTypes.js new file mode 100644 index 0000000000..aee5aa9754 --- /dev/null +++ b/rdmo/projects/assets/js/project/actions/actionTypes.js @@ -0,0 +1,34 @@ +export const FETCH_PROJECT_INIT = 'FETCH_PROJECT_INIT' +export const FETCH_PROJECT_SUCCESS = 'FETCH_PROJECT_SUCCESS' +export const FETCH_PROJECT_ERROR = 'FETCH_PROJECT_ERROR' +export const UPDATE_PROJECT_INIT = 'UPDATE_PROJECT_INIT' +export const UPDATE_PROJECT_SUCCESS = 'UPDATE_PROJECT_SUCCESS' +export const UPDATE_PROJECT_ERROR = 'UPDATE_PROJECT_ERROR' +export const DELETE_PROJECT_INIT = 'DELETE_PROJECT_INIT' +export const DELETE_PROJECT_SUCCESS = 'DELETE_PROJECT_SUCCESS' +export const DELETE_PROJECT_ERROR = 'DELETE_PROJECT_ERROR' +export const FETCH_PROJECT_INVITES_INIT = 'FETCH_PROJECT_INVITES_INIT' +export const FETCH_PROJECT_INVITES_SUCCESS = 'FETCH_PROJECT_INVITES_SUCCESS' +export const FETCH_PROJECT_INVITES_ERROR = 'FETCH_PROJECT_INVITES_ERROR' +export const CREATE_PROJECT_MEMBER_INIT = 'CREATE_PROJECT_MEMBER_INIT' +export const CREATE_PROJECT_MEMBER_SUCCESS = 'CREATE_PROJECT_MEMBER_SUCCESS' +export const CREATE_PROJECT_MEMBER_ERROR = 'CREATE_PROJECT_MEMBER_ERROR' +export const SEND_INVITE_INIT = 'SEND_INVITE_INIT' +export const SEND_INVITE_SUCCESS = 'SEND_INVITE_SUCCESS' +export const SEND_INVITE_ERROR = 'SEND_INVITE_ERROR' +export const UPDATE_PROJECT_MEMBER_INIT = 'UPDATE_PROJECT_MEMBER_INIT' +export const UPDATE_PROJECT_MEMBER_SUCCESS = 'UPDATE_PROJECT_MEMBER_SUCCESS' +export const UPDATE_PROJECT_MEMBER_ERROR = 'UPDATE_PROJECT_MEMBER_ERROR' +export const DELETE_PROJECT_MEMBER_INIT = 'DELETE_PROJECT_MEMBER_INIT' +export const DELETE_PROJECT_MEMBER_SUCCESS = 'DELETE_PROJECT_MEMBER_SUCCESS' +export const DELETE_PROJECT_MEMBER_ERROR = 'DELETE_PROJECT_MEMBER_ERROR' +export const UPDATE_PROJECT_INVITE_INIT = 'UPDATE_PROJECT_INVITE_INIT' +export const UPDATE_PROJECT_INVITE_SUCCESS = 'UPDATE_PROJECT_INVITE_SUCCESS' +export const UPDATE_PROJECT_INVITE_ERROR = 'UPDATE_PROJECT_INVITE_ERROR' +export const DELETE_PROJECT_INVITE_INIT = 'DELETE_PROJECT_INVITE_INIT' +export const DELETE_PROJECT_INVITE_SUCCESS = 'DELETE_PROJECT_INVITE_SUCCESS' +export const DELETE_PROJECT_INVITE_ERROR = 'DELETE_PROJECT_INVITE_ERROR' +export const LEAVE_PROJECT_INIT = 'LEAVE_PROJECT_INIT' +export const LEAVE_PROJECT_SUCCESS = 'LEAVE_PROJECT_SUCCESS' +export const LEAVE_PROJECT_ERROR = 'LEAVE_PROJECT_ERROR' +export const CLEAR_PROJECT_ERRORS = 'CLEAR_PROJECT_ERRORS' diff --git a/rdmo/projects/assets/js/project/actions/projectActions.js b/rdmo/projects/assets/js/project/actions/projectActions.js new file mode 100644 index 0000000000..773674c6ed --- /dev/null +++ b/rdmo/projects/assets/js/project/actions/projectActions.js @@ -0,0 +1,417 @@ +import CatalogsApi from 'rdmo/projects/assets/js/common/api/CatalogsApi' + +import { addToPending, removeFromPending } from 'rdmo/core/assets/js/actions/pendingActions' +import { updateConfig } from 'rdmo/core/assets/js/actions/configActions' +import { baseUrl } from 'rdmo/core/assets/js/utils/meta' + +import { projectId } from '../utils/meta' +import { updateLocation } from '../utils/location' + +import ProjectApi from '../api/ProjectApi' + +import * as actionTypes from './actionTypes' + + +export function setPage(page) { + return function(dispatch) { + dispatch(updateConfig('page', page)) + updateLocation(page) + } +} + +export function fetchProject() { + return function(dispatch) { + dispatch(addToPending('fetchProject')) + dispatch(fetchProjectInit()) + + return Promise.all([ + ProjectApi.fetchProject(projectId), + ProjectApi.fetchProjectHierarchy(projectId), + ProjectApi.fetchProjectSnapshots(projectId), + ProjectApi.fetchProjectTasks(projectId), + ProjectApi.fetchProjectMemberships(projectId), + ProjectApi.fetchProjectMembershipHierarchy(projectId), + CatalogsApi.fetchCatalogs() + ]) + .then(([project, hierarchy, snapshots, tasks, memberships, membershipHierarchy, catalogs]) => { + const projectData = { + project: project, + hierarchy: hierarchy, + snapshots: snapshots, + tasks: tasks, + memberships: [...memberships, ...membershipHierarchy], + catalogs: catalogs + } + + dispatch(removeFromPending('fetchProject')) + dispatch(fetchProjectSuccess(projectData)) + }) + .catch(error => { + dispatch(removeFromPending('fetchProject')) + dispatch(fetchProjectError(error)) + throw error + }) + } +} + +export function fetchProjectInit() { + return { type: actionTypes.FETCH_PROJECT_INIT } +} + +export function fetchProjectSuccess(project) { + return { type: actionTypes.FETCH_PROJECT_SUCCESS, project } +} + +export function fetchProjectError(error) { + return { type: actionTypes.FETCH_PROJECT_ERROR, error } +} + +export function updateProject(data) { + return function(dispatch, getState) { + const state = getState() + const currentBundle = state.project.project + const id = currentBundle?.project?.id + + if (!id) { + console.warn('No project ID available for update.') + return + } + + dispatch(addToPending('updateProject')) + dispatch(updateProjectInit()) + + return ProjectApi.updateProject(id, data) + .then(() => + Promise.all([ + ProjectApi.fetchProject(id), + ProjectApi.fetchProjectHierarchy(id), + ]) + ) + .then(([project, hierarchy]) => { + const updatedBundle = { + ...currentBundle, + // only these two are refreshed from server: + project, + hierarchy, + // everything else stays untouched: + // snapshots: currentBundle.snapshots, + // tasks: currentBundle.tasks, + // memberships: currentBundle.memberships, + // catalogs: currentBundle.catalogs, + } + + dispatch(removeFromPending('updateProject')) + dispatch(updateProjectSuccess(updatedBundle)) + }) + .catch((error) => { + dispatch(removeFromPending('updateProject')) + dispatch(updateProjectError(error)) + throw error + }) + } +} + +export function updateProjectInit() { + return { type: actionTypes.UPDATE_PROJECT_INIT } +} + +export function updateProjectSuccess(project) { + return { type: actionTypes.UPDATE_PROJECT_SUCCESS, project } +} + +export function updateProjectError(error) { + return { type: actionTypes.UPDATE_PROJECT_ERROR, error } +} + +export function deleteProject() { + return function(dispatch) { + dispatch(addToPending('deleteProject')) + dispatch(deleteProjectInit()) + + return ProjectApi.deleteProject(projectId) + .then(() => { + dispatch(removeFromPending('deleteProject')) + dispatch(deleteProjectSuccess(projectId)) + + window.location.href = `${baseUrl}/projects/` + }) + .catch((error) => { + dispatch(removeFromPending('deleteProject')) + dispatch(deleteProjectError(error)) + throw error + }) + } +} + +export function deleteProjectInit() { + return { type: actionTypes.DELETE_PROJECT_INIT } +} + +export function deleteProjectSuccess(projectId) { + return { type: actionTypes.DELETE_PROJECT_SUCCESS, projectId } +} + +export function deleteProjectError(error) { + return { type: actionTypes.DELETE_PROJECT_ERROR, error } +} + +export function fetchProjectInvites() { + return function(dispatch) { + dispatch(addToPending('fetchProjectInvites')) + dispatch(fetchProjectInvitesInit()) + + return ProjectApi.fetchProjectInvites(projectId) + .then(invites => { + dispatch(removeFromPending('fetchProjectInvites')) + dispatch(fetchProjectInvitesSuccess(invites)) + }) + .catch(error => { + dispatch(removeFromPending('fetchProjectInvites')) + dispatch(fetchProjectInvitesError(error)) + }) + } +} + +export function fetchProjectInvitesInit() { + return { type: actionTypes.FETCH_PROJECT_INVITES_INIT } +} + +export function fetchProjectInvitesSuccess(invites) { + return { type: actionTypes.FETCH_PROJECT_INVITES_SUCCESS, invites } +} + +export function fetchProjectInvitesError(error) { + return { type: actionTypes.FETCH_PROJECT_INVITES_ERROR, error } +} + +export function createProjectMember(data) { + return function(dispatch) { + dispatch(addToPending('createProjectMember')) + dispatch(createProjectMemberInit()) + + return ProjectApi.createMember(projectId, data) + .then(member => { + dispatch(removeFromPending('createProjectMember')) + dispatch(createProjectMemberSuccess(member)) + }) + .catch(error => { + dispatch(removeFromPending('createProjectMember')) + dispatch(createProjectMemberError(error)) + throw error + }) + } +} + +export function createProjectMemberInit() { + return { type: actionTypes.CREATE_PROJECT_MEMBER_INIT } +} + +export function createProjectMemberSuccess(member) { + return { type: actionTypes.CREATE_PROJECT_MEMBER_SUCCESS, member } +} + +export function createProjectMemberError(error) { + return { type: actionTypes.CREATE_PROJECT_MEMBER_ERROR, error } +} + +export function updateProjectMember(membershipId, data) { + return function(dispatch, getState) { + dispatch(addToPending('updateProjectMember')) + dispatch(updateProjectMemberInit()) + + return ProjectApi.updateMember(projectId, membershipId, data) + .then(member => { + dispatch(updateProjectMemberSuccess({ ...member, id: membershipId })) + + // membership updates can lead to a permission change for owner <-> last owner cases + // project with permissions needs to be fetched + const state = getState() + const currentBundle = state.project.project + return ProjectApi.fetchProject(projectId).then(project => ({ project, currentBundle })) + }) + .then(({ project, currentBundle }) => { + const updatedBundle = { + ...currentBundle, + project + } + + dispatch(removeFromPending('updateProjectMember')) + dispatch(updateProjectSuccess(updatedBundle)) + }) + .catch(error => { + dispatch(removeFromPending('updateProjectMember')) + dispatch(updateProjectMemberError(error)) + throw error + }) + } +} + +export function updateProjectMemberInit() { + return { type: actionTypes.UPDATE_PROJECT_MEMBER_INIT } +} + +export function updateProjectMemberSuccess(member) { + return { type: actionTypes.UPDATE_PROJECT_MEMBER_SUCCESS, member } +} + +export function updateProjectMemberError(error) { + return { type: actionTypes.UPDATE_PROJECT_MEMBER_ERROR, error } +} + +export function deleteProjectMember(membershipId) { + return function(dispatch) { + dispatch(addToPending('deleteProjectMember')) + dispatch(deleteProjectMemberInit()) + + return ProjectApi.deleteMember(projectId, membershipId) + .then(() => { + dispatch(removeFromPending('deleteProjectMember')) + dispatch(deleteProjectMemberSuccess(membershipId)) + }) + .catch(error => { + dispatch(removeFromPending('deleteProjectMember')) + dispatch(deleteProjectMemberError(error)) + throw error + }) + } +} + +export function deleteProjectMemberInit() { + return { type: actionTypes.DELETE_PROJECT_MEMBER_INIT } +} + +export function deleteProjectMemberSuccess(membershipId) { + return { type: actionTypes.DELETE_PROJECT_MEMBER_SUCCESS, membershipId } +} + +export function deleteProjectMemberError(error) { + return { type: actionTypes.DELETE_PROJECT_MEMBER_ERROR, error } +} + +export function sendProjectInvite(data) { + return function(dispatch) { + dispatch(addToPending('sendInvite')) + dispatch(sendProjectInviteInit()) + + return ProjectApi.sendInvite(projectId, data) + .then(invite => { + dispatch(removeFromPending('sendInvite')) + dispatch(sendProjectInviteSuccess(invite)) + }) + .catch(error => { + dispatch(removeFromPending('sendInvite')) + dispatch(sendProjectInviteError(error)) + throw error + }) + } +} + +export function sendProjectInviteInit() { + return { type: actionTypes.SEND_INVITE_INIT } +} + +export function sendProjectInviteSuccess(invite) { + return { type: actionTypes.SEND_INVITE_SUCCESS, invite } +} + +export function sendProjectInviteError(error) { + return { type: actionTypes.SEND_INVITE_ERROR, error } +} + +export function updateProjectInvite(inviteId, data) { + return function(dispatch) { + dispatch(addToPending('updateProjectInvite')) + dispatch(updateProjectInviteInit()) + + return ProjectApi.updateInvite(projectId, inviteId, data) + .then(invite => { + dispatch(removeFromPending('updateProjectInvite')) + dispatch(updateProjectInviteSuccess({...invite, id: inviteId})) + }) + .catch(error => { + dispatch(removeFromPending('updateProjectInvite')) + dispatch(updateProjectInviteError(error)) + throw error + }) + } +} + +export function updateProjectInviteInit() { + return { type: actionTypes.UPDATE_PROJECT_INVITE_INIT } +} + +export function updateProjectInviteSuccess(invite) { + return { type: actionTypes.UPDATE_PROJECT_INVITE_SUCCESS, invite } +} + +export function updateProjectInviteError(error) { + return { type: actionTypes.UPDATE_PROJECT_INVITE_ERROR, error } +} + +export function deleteProjectInvite(inviteId) { + return function(dispatch) { + dispatch(addToPending('deleteProjectInvite')) + dispatch(deleteProjectInviteInit()) + + return ProjectApi.deleteInvite(projectId, inviteId) + .then(() => { + dispatch(removeFromPending('deleteProjectInvite')) + dispatch(deleteProjectInviteSuccess(inviteId)) + }) + .catch(error => { + dispatch(removeFromPending('deleteProjectInvite')) + dispatch(deleteProjectInviteError(error)) + throw error + }) + } +} + +export function deleteProjectInviteInit() { + return { type: actionTypes.DELETE_PROJECT_INVITE_INIT } +} + +export function deleteProjectInviteSuccess(inviteId) { + return { type: actionTypes.DELETE_PROJECT_INVITE_SUCCESS, inviteId } +} + +export function deleteProjectInviteError(error) { + return { type: actionTypes.DELETE_PROJECT_INVITE_ERROR, error } +} + +export function leaveProject(membershipId, { redirect = false } = {}) { + return function(dispatch) { + dispatch(addToPending('leaveProject')) + dispatch(leaveProjectInit()) + + return ProjectApi.leaveProject(projectId) + .then(() => { + dispatch(removeFromPending('leaveProject')) + dispatch(leaveProjectSuccess(membershipId)) + if (redirect) { + window.location.href = `${baseUrl}/projects/` + return + } + }) + .catch(error => { + dispatch(removeFromPending('leaveProject')) + dispatch(leaveProjectError(error)) + throw error + }) + } +} + +export function leaveProjectInit() { + return { type: actionTypes.LEAVE_PROJECT_INIT } +} + +export function leaveProjectSuccess(membershipId) { + return { type: actionTypes.LEAVE_PROJECT_SUCCESS, membershipId } +} + +export function leaveProjectError(error) { + return { type: actionTypes.LEAVE_PROJECT_ERROR, error } +} + +export function clearProjectErrors() { + return { type: actionTypes.CLEAR_PROJECT_ERRORS } +} diff --git a/rdmo/projects/assets/js/project/api/ProjectApi.js b/rdmo/projects/assets/js/project/api/ProjectApi.js new file mode 100644 index 0000000000..9919f7cbce --- /dev/null +++ b/rdmo/projects/assets/js/project/api/ProjectApi.js @@ -0,0 +1,78 @@ +import { encodeParams } from 'rdmo/core/assets/js/utils/api' + +import BaseApi from 'rdmo/core/assets/js/api/BaseApi' + +export default class ProjectApi extends BaseApi { + + static fetchProject(projectId) { + return this.get(`/api/v1/projects/projects/${projectId}/`) + } + + static fetchProjectHierarchy(projectId) { + return this.get(`/api/v1/projects/projects/${projectId}/hierarchy/`) + } + + static fetchProjectSnapshots(projectId) { + return this.get(`/api/v1/projects/projects/${projectId}/snapshots/`) + } + + static fetchProjectTasks(projectId) { + return this.get(`/api/v1/projects/projects/${projectId}/issues/`) + } + + static fetchProjectMemberships(projectId) { + return this.get(`/api/v1/projects/projects/${projectId}/memberships/`) + } + + static fetchProjectMembershipHierarchy(projectId) { + return this.get(`/api/v1/projects/projects/${projectId}/memberships/hierarchy/`) + } + + static fetchProjectInvites(projectId) { + return this.get(`/api/v1/projects/projects/${projectId}/invites/`) + } + + static fetchViews() { + return this.get('/api/v1/projects/views/views/') + } + + static fetchProjects(params) { + return this.get(`/api/v1/projects/projects/?${encodeParams(params)}`) + } + + static updateProject(projectId, data) { + return this.put(`/api/v1/projects/projects/${projectId}/`, data) + } + + static deleteProject(projectId) { + return this.delete(`/api/v1/projects/projects/${projectId}/`) + } + + static createMember(projectId, data) { + return this.post(`/api/v1/projects/projects/${projectId}/memberships/`, data) + } + + static updateMember(projectId, membershipId, data) { + return this.put(`/api/v1/projects/projects/${projectId}/memberships/${membershipId}/`, data) + } + + static deleteMember(projectId, membershipId) { + return this.delete(`/api/v1/projects/projects/${projectId}/memberships/${membershipId}/`) + } + + static leaveProject(projectId) { + return this.delete(`/api/v1/projects/projects/${projectId}/memberships/leave/`) + } + + static sendInvite(projectId, data) { + return this.post(`/api/v1/projects/projects/${projectId}/invites/`, data) + } + + static updateInvite(projectId, inviteId, data) { + return this.put(`/api/v1/projects/projects/${projectId}/invites/${inviteId}/`, data) + } + + static deleteInvite(projectId, inviteId) { + return this.delete(`/api/v1/projects/projects/${projectId}/invites/${inviteId}/`) + } +} diff --git a/rdmo/projects/assets/js/project/components/Project.js b/rdmo/projects/assets/js/project/components/Project.js new file mode 100644 index 0000000000..ae26f204f9 --- /dev/null +++ b/rdmo/projects/assets/js/project/components/Project.js @@ -0,0 +1,41 @@ +import React from 'react' +import ProjectSidebar from './ProjectSidebar' +import ProjectPage from './ProjectPage' + +const Project = () => { + const menuItems = [ + { + title: '', + items: [{ id: '', name: gettext('Dashboard'), icon: 'bi-grid' }], + }, + { + title: gettext('DATA MANAGEMENT PLAN'), + items: [ + { id: 'interview', name: gettext('Interview'), icon: 'bi-clipboard-check' }, + { id: 'documents', name: gettext('Documents'), icon: 'bi-file-text' }, + { id: 'snapshots', name: gettext('Snapshots'), icon: 'bi-stack' }, + ], + }, + { + title: gettext('PROJECT MANAGEMENT'), + items: [ + { id: 'project-information', name: gettext('Project data'), icon: 'bi-info-square' }, + { id: 'membership', name: gettext('Membership'), icon: 'bi-people' }, + { id: 'plugins', name: gettext('Plugins'), icon: 'bi-wrench' }, + ], + }, + ] + + return ( +
    + +
    +
    + +
    +
    +
    + ) +} + +export default Project diff --git a/rdmo/projects/assets/js/project/components/ProjectPage.js b/rdmo/projects/assets/js/project/components/ProjectPage.js new file mode 100644 index 0000000000..a8451a1ce4 --- /dev/null +++ b/rdmo/projects/assets/js/project/components/ProjectPage.js @@ -0,0 +1,51 @@ +import React from 'react' +import { useSelector } from 'react-redux' + +import Dashboard from './pages/Dashboard' +// import Interview from '../pages/Interview' +// import Documents from '../pages/Documents' +// import Snapshots from '../pages/Snapshots' +import Membership from './pages/Membership' +import ProjectData from './pages/ProjectData' + +const ProjectPage = () => { + // Get Redux state + // const config = useSelector((state) => state.config) + // const settings = useSelector((state) => state.settings) + // const templates = useSelector((state) => state.templates) + // const currentUser = useSelector((state) => state.user.currentUser) + // const project = useSelector((state) => state.project.project) + // const catalogs = useSelector((state) => state.catalogs) + + // console.log('Project:', project) + // console.log('User:', currentUser) + // console.log('Settings: ', settings) + // console.log('Config:', config) + // console.log('Catalogs:', catalogs) + // console.log('settings', settings) + // console.log('templates', templates) + // console.log('currentUser', currentUser) + + // const dispatch = useDispatch() + + const page = useSelector((state) => state.config.page) + + switch (page) { + case '': + return + // case 'interview': + // return + // case 'documents': + // return + // case 'snapshots': + // return + case 'project-information': + return + case 'membership': + return + default: + return

    Page Not Found

    + } +} + +export default ProjectPage diff --git a/rdmo/projects/assets/js/project/components/ProjectSidebar.js b/rdmo/projects/assets/js/project/components/ProjectSidebar.js new file mode 100644 index 0000000000..1d742ad32a --- /dev/null +++ b/rdmo/projects/assets/js/project/components/ProjectSidebar.js @@ -0,0 +1,67 @@ +import React from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' +import { useDispatch, useSelector } from 'react-redux' + +import { baseUrl } from 'rdmo/core/assets/js/utils/meta' + +import { setPage } from '../actions/projectActions' + +import ProjectBadge from './helper/ProjectBadge' + +const ProjectSidebar = ({ menuItems }) => { + const page = useSelector((state) => state.config.page) + const dispatch = useDispatch() + + return ( +
    + + +
      + {menuItems.map((section) => ( +
      + {section.title &&
      {section.title}
      } + + {section.items.map((item) => ( +
    • + +
    • + ))} +
      + ))} +
    + + +
    + ) +} + +ProjectSidebar.propTypes = { + menuItems: PropTypes.arrayOf( + PropTypes.shape({ + title: PropTypes.string, + items: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + icon: PropTypes.string, + }) + ).isRequired, + }) + ).isRequired +} + +export default ProjectSidebar diff --git a/rdmo/projects/assets/js/project/components/TestForm.js b/rdmo/projects/assets/js/project/components/TestForm.js new file mode 100644 index 0000000000..57ccf4874c --- /dev/null +++ b/rdmo/projects/assets/js/project/components/TestForm.js @@ -0,0 +1,132 @@ +import React, { useState } from 'react' + +import Input from 'rdmo/core/assets/js/components/forms/Input' +import Select from 'rdmo/core/assets/js/components/forms/Select' +import Textarea from 'rdmo/core/assets/js/components/forms/Textarea' + +const TestForm = ({ }) => { + + const [values, setValues] = useState({text: '1', textDebounced: '2'}) + const [errors, setErrors] = useState({}) + + const handleSubmit = (event) => { + event.preventDefault() + console.log('submit!') + } + + const fakeErrors = () => { + setErrors({ + text: ['There is something wrong here...'], + textDebounced: ['There is something wrong here...'], + textarea: ['There is something wrong here...'], + textareaDebounced: ['There is something wrong here...'], + select: ['There is something wrong here...'], + selectClearable: ['There is something wrong here...'], + selectMulti: ['There is something wrong here...'] + }) + } + + const reset = () => { + setValues({}) + setErrors({}) + } + + const options = [ + {value: 'a', label: 'a'}, + {value: 'b', label: 'b'}, + {value: 'c', label: 'c'} + ] + + return ( +
    + setValues({ ...values, text })} + /> + + setValues({ ...values, textDebounced })} + /> + +