diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 6edb75283..79c044510 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -38,7 +38,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: '3.11.0' # From file: '.python-version' + python-version: '3.11.2' # From file: '.python-version' - name: Install dependencies run: | diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 5a90df6ca..5094c5d4b 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,7 @@ { "recommendations": [ + "eeyore.yapf", + "ms-python.flake8", "eamodio.gitlens", "mikestead.dotenv", "ms-python.python", @@ -11,6 +13,11 @@ "ms-python.vscode-pylance", "editorconfig.editorconfig", "yzhang.markdown-all-in-one", + "visualstudioexptteam.vscodeintellicode", + "hediet.vscode-drawio", + "stylelint.vscode-stylelint", + "ms-python.mypy-type-checker", + "rvest.vs-code-prettier-eslint", "visualstudioexptteam.vscodeintellicode" ] } diff --git a/.vscode/launch.json b/.vscode/launch.json index b6b53b4a8..e7d3a5b2c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,6 +11,7 @@ "type": "python", "request": "attach", "django": true, + "justMyCode": false, "connect": { "host": "0.0.0.0", "port": 5678 @@ -28,6 +29,7 @@ "name": "Django (localhost)", "type": "python", "request": "attach", + "justMyCode": false, "django": true, "connect": { "host": "0.0.0.0", diff --git a/.vscode/settings.default.json b/.vscode/settings.default.json index 37e4dddfe..edd115cff 100644 --- a/.vscode/settings.default.json +++ b/.vscode/settings.default.json @@ -1,8 +1,85 @@ { + // These settings are required for the project. + + // ================================ + // Editor + // ================================ + "files.autoSave": "onFocusChange", "editor.formatOnSave": true, - "html.format.enable": false, - "python.linting.enabled": true, + + // ================================ + // Backend + // ================================ + // yapf (formatter) + "yapf.importStrategy": "fromEnvironment", + "yapf.args": ["--style", "backend/.style.yapf"], + // mypy (type checker, linter) + "mypy-type-checker.importStrategy": "fromEnvironment", + "mypy-type-checker.args": ["--config-file", "backend/mypy.ini", "--no-pretty"], + // flake8 (linter) + "flake8.importStrategy": "fromEnvironment", + "flake8.args": ["--config", "backend/.flake8"], + // pytest "python.testing.pytestEnabled": true, "python.testing.pytestArgs": ["-c", "backend/pytest.ini"], - "python.formatting.provider": "yapf" + // Other + "python.envFile": "backend/.env", + "[python]": { + "editor.codeActionsOnSave": { + "source.fixAll": false, + "source.organizeImports": false + }, + "editor.defaultFormatter": "eeyore.yapf" + }, + + // ================================ + // Frontend + // ================================ + "eslint.enable": true, + "eslint.format.enable": true, + "html.format.enable": false, + "prettier.configPath": "frontend/.prettierrc.js", + "prettier.ignorePath": "frontend/.prettierignore", + "editor.defaultFormatter": "esbenp.prettier-vscode", + "eslint.workingDirectories": ["frontend"], + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll": true, + "source.organizeImports": true + } + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll": false, + "source.organizeImports": true + } + }, + "[javascriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll": true, + "source.organizeImports": true + } + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.codeActionsOnSave": { + "source.fixAll": true, + "source.organizeImports": true + } + }, + "[scss]": { + "editor.defaultFormatter": "stylelint.vscode-stylelint" + }, + "[css]": { + "editor.defaultFormatter": "stylelint.vscode-stylelint" + }, + "css.validate": true, + "less.validate": true, + "scss.validate": true, + "stylelint.configFile": "frontend/.stylelintrc", + "stylelint.enable": true, + "stylelint.validate": ["css", "scss"] } diff --git a/.vscode/settings.emil.json b/.vscode/settings.emil.json index b3a241a11..dfc005f63 100644 --- a/.vscode/settings.emil.json +++ b/.vscode/settings.emil.json @@ -1,29 +1,56 @@ { - "files.autoSave": "onFocusChange", + // ================================ + // Editor + // ================================ "editor.formatOnSave": true, - "eslint.workingDirectories": ["frontend"], - "python.linting.mypyEnabled": true, - "python.linting.mypyArgs": ["--config-file", "backend/mypy.ini", "--no-pretty"], + "files.autoSave": "onFocusChange", + "diffEditor.codeLens": true, "window.title": "🟥 ${activeEditorShort}${separator}${rootName}", - "python.linting.enabled": true, - "python.linting.flake8Enabled": true, - "python.linting.flake8Args": ["--config", "backend/.flake8"], - "python.testing.pytestEnabled": true, - "python.testing.pytestArgs": ["-c", "backend/pytest.ini"], - "python.formatting.provider": "yapf", - "python.envFile": "backend/.env", "workbench.colorCustomizations": { "titleBar.activeForeground": "#fff", "titleBar.activeBackground": "#a03033", "titleBar.inactiveBackground": "#a03033", "editorCursor.foreground": "#ff2222" }, - "python.testing.unittestEnabled": false, + "workbench.iconTheme": "atom-icons", + "workbench.colorTheme": "Atom One Dark", + "workbench.tree.indent": 20, + + // ================================ + // Backend + // ================================ + // yapf (formatter) + "yapf.importStrategy": "fromEnvironment", + "yapf.args": ["--style", "backend/.style.yapf"], + // mypy (type checker, linter) + "mypy-type-checker.importStrategy": "fromEnvironment", + "mypy-type-checker.args": ["--config-file", "backend/mypy.ini", "--no-pretty"], + // flake8 (linter) + "flake8.importStrategy": "fromEnvironment", + "flake8.args": ["--config", "backend/.flake8"], + // pytest + "python.testing.pytestEnabled": true, + "python.testing.pytestArgs": ["-c", "backend/pytest.ini"], + // Other + "python.envFile": "backend/.env", + "[python]": { + "editor.codeActionsOnSave": { + "source.fixAll": false, + "source.organizeImports": false + }, + "editor.defaultFormatter": "eeyore.yapf" + }, + + // ================================ + // Frontend + // ================================ "eslint.enable": true, "eslint.format.enable": true, - "diffEditor.codeLens": true, + "html.format.enable": false, "prettier.configPath": "frontend/.prettierrc.js", "prettier.ignorePath": "frontend/.prettierignore", + "editor.defaultFormatter": "esbenp.prettier-vscode", + "eslint.workingDirectories": ["frontend"], "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { @@ -34,7 +61,7 @@ "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { - "source.fixAll": true, + "source.fixAll": false, "source.organizeImports": true } }, @@ -52,20 +79,21 @@ "source.organizeImports": true } }, - "[python]": { - "editor.codeActionsOnSave": { - "source.fixAll": false, - "source.organizeImports": false - } + "[scss]": { + "editor.defaultFormatter": "stylelint.vscode-stylelint" + }, + "[css]": { + "editor.defaultFormatter": "stylelint.vscode-stylelint" }, - // "[scss]": { - // "editor.defaultFormatter": "stylelint.vscode-stylelint" - // }, "css.validate": true, "less.validate": true, "scss.validate": true, "stylelint.configFile": "frontend/.stylelintrc", "stylelint.enable": true, "stylelint.validate": ["css", "scss"], + + // ================================ + // Other + // ================================ "svg.preview.background": "editor" } diff --git a/.vscode/settings.suggestions.json b/.vscode/settings.suggestions.json index 346d25f55..f906a345c 100644 --- a/.vscode/settings.suggestions.json +++ b/.vscode/settings.suggestions.json @@ -1,44 +1,21 @@ { - "editor.formatOnSave": true, - // Hide import regions automatically, e.g. (.js) + // These settings are not required for the project. + + // Hide import regions automatically, e.g. (.js). "editor.foldingImportsByDefault": true, + // Automatically set filetype when opening a new file. // This setting determines new filetype based on the current focused file. "files.defaultLanguage": "${activeEditorLanguage}", - // Change layout of the upper title + + // Change layout of the upper title. "window.title": "🦐 ${activeEditorShort}${separator}${rootName}", - "[typescript]": { - "editor.defaultFormatter": "rvest.vs-code-prettier-eslint", - "editor.codeActionsOnSave": { - "source.fixAll": true, - "source.organizeImports": true - } - }, - "[typescriptreact]": { - "editor.defaultFormatter": "rvest.vs-code-prettier-eslint", - "editor.codeActionsOnSave": { - "source.fixAll": true, - "source.organizeImports": true - } - }, - "[javascriptreact]": { - "editor.defaultFormatter": "rvest.vs-code-prettier-eslint", - "editor.codeActionsOnSave": { - "source.fixAll": true, - "source.organizeImports": true - } - }, - "[javascript]": { - "editor.defaultFormatter": "rvest.vs-code-prettier-eslint", - "editor.codeActionsOnSave": { - "source.fixAll": true, - "source.organizeImports": true - } - }, - "[python]": { - "editor.codeActionsOnSave": { - "source.fixAll": false, - "source.organizeImports": false - } + + // Change layout of editor. + "workbench.colorCustomizations": { + "titleBar.activeForeground": "#fff", + "titleBar.activeBackground": "#a03033", + "titleBar.inactiveBackground": "#a03033", + "editorCursor.foreground": "#ff2222" } -} \ No newline at end of file +} diff --git a/README.md b/README.md index f9254371d..19ee277ff 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ - [Useful Commands](/docs/useful-commands.md) - [Technologies used on Samf4 🤖](/docs/technical/Samf4Tech.md) - [Useful Docker aliases](/docs/docker-project-specific-commands.md) + ## Installation We have a script that handles all installation for you. To run the script, a Github Personal Access Token (PAT) is required. diff --git a/backend/.docker.example.env b/backend/.docker.example.env index 207c30f66..864c250bb 100644 --- a/backend/.docker.example.env +++ b/backend/.docker.example.env @@ -12,9 +12,9 @@ ENV=development ENABLE_DEBUGPY=yes # Automatically create local superuser -DJANGO_SUPERUSER_USERNAME=emilte +DJANGO_SUPERUSER_USERNAME=admin DJANGO_SUPERUSER_PASSWORD=Django123 -DJANGO_SUPERUSER_EMAIL=emil97@live.no +DJANGO_SUPERUSER_EMAIL=mg-web@samfundet.no DOMAIN=0.0.0.0 @@ -35,3 +35,6 @@ DB_PORT=5432 EMAIL_HOST=smtp.gmail.com EMAIL_HOST_USER= example@gmail.com EMAIL_HOST_PASSWORD=... + +# Github webhook secret. +WEBHOOK_SECRET=... diff --git a/backend/.env.example b/backend/.env.example index cb313c558..0bb59bef3 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -39,3 +39,6 @@ DB_PORT=5432 EMAIL_HOST=smtp.gmail.com EMAIL_HOST_USER=your_email@gmail.com EMAIL_HOST_PASSWORD=your_email_password + +# Github webhook secret. +WEBHOOK_SECRET=... diff --git a/backend/.flake8 b/backend/.flake8 index c78348bde..c50c850bd 100644 --- a/backend/.flake8 +++ b/backend/.flake8 @@ -1,60 +1,56 @@ [flake8] -; NOTES: -; Do not use inline comments (on the same line after some configuration). -; This is not supported +# NOTES: +# Do not use inline comments (on the same line after some configuration). +# This is not supported -; Version is downgraded from 6.0.0 to allow plugins to catch up with breaking changes. -; TODO: upgrade later. +# Version is downgraded from 6.0.0 to allow plugins to catch up with breaking changes. +# TODO: upgrade later. -; Same as yapf. +# Same as yapf. max-line-length = 160 -require-plugins = flake8-print, flake8-keyword-arguments, flake8-quotes, flake8-annotations +require-plugins = flake8-print, flake8-quotes, flake8-annotations +# require-plugins = flake8-print, flake8-keyword-arguments, flake8-quotes, flake8-annotations -# Ignore specific errors in specific modules/files (glob pattern). +# Ignore specific errors in specific files (glob pattern). # Format: : # Example: root/urls.py:T201 per-file-ignores = - ; Don't care about print statements in commands. + # Don't care about print statements in commands. **/management/commands/**:T201, - ; urls.py are allowed positional arguments because the alternative is cluttering. + # urls.py are allowed positional arguments because the alternative is cluttering. **/urls.py:FKA01, + +# Terminal and VSCode triggers flake8 differently. They ignore certain patterns in this list. +# Exclude both folder and all files within for expected results. +# Works for VSCode: **//** +# Doesn't work for VSCode: **//**/* extend-exclude= - .git, - .pytest_ignore, - __pycache__, - venv, - .venv, - tests, - migrations, + .git, **/.git/**, + .pytest_ignore, **/.pytest_ignore/**, + .history, **/.history/**, # Local History VSCode extensions. + venv, **/venv/**, + .venv, **/.venv/**, + migrations, **/migrations/**, root/scripts/testing.py, extend-ignore= E266, - ANN101, - ANN001, - ANN201, - ANN102, - ANN002, - ANN003, + ANN101, # Don't require typing of self. (flake8-annotations) + ANN001, # Covered by mypy. (flake8-annotations) + ANN201, # Covered by mypy. (flake8-annotations) + ANN102, # Don't require typing of cls. (flake8-annotations) + ANN002, # Don't require typing of *args. (flake8-annotations) + ANN003, # Don't require typing of **kwargs. (flake8-annotations) F841, E303, -; Don't use comments on the lists above. - -; ANN101: Don't require typing of self. (flake8-annotations) -; ANN001: Covered by mypy. (flake8-annotations) -; ANN201: Covered by mypy. (flake8-annotations) -; ANN102: Don't require typing of cls. (flake8-annotations) -; ANN002: Don't require typing of *args. (flake8-annotations) -; ANN00: Don't require typing of **kwargs. (flake8-annotations) - -; Disable individual lines with comment, example: "print(1) # noqa" +# Disable individual lines with comment, example: "print(1) # noqa" -; List of rules: -; https://lintlyci.github.io/Flake8Rules/ +# List of rules: +# https://lintlyci.github.io/Flake8Rules/ -[flake8-keyword-arguments] -max-pos-args = 0 +# [flake8-keyword-arguments] +# max-pos-args = 0 diff --git a/backend/.gitignore b/backend/.gitignore index 6da7f292a..fc5ff2dcd 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -13,3 +13,6 @@ __pycache__ # Script for debugging purposes. root/scripts/testing.py + +# File to trigger restart of uwsgi server in production environment. +reload diff --git a/backend/.python-version b/backend/.python-version index afad81866..1e3345683 100644 --- a/backend/.python-version +++ b/backend/.python-version @@ -1 +1 @@ -3.11.0 +3.11.2 diff --git a/backend/Dockerfile b/backend/Dockerfile index 8f4a162b8..242777841 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,44 +1,61 @@ # Use same version as defined in .python-version. -FROM python:3.11.0-slim-bullseye +FROM python:3.11.2-slim-bullseye +################################## +# Exposed ports # +################################## + +# Backend server. EXPOSE 8000 +# Debugpy session. EXPOSE 5678 -# Docker should configure env vars in '.docker.env'. +################################## +# Environment variables # +################################## +# Docker-compose should configure env vars in '.docker.env'. ENV PIPENV_DONT_LOAD_ENV=1 ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 ENV PIPENV_VENV_IN_PROJECT=1 -# Dependency for billig development database +################################## +# System dependencies # +################################## +# sqlite3: dependency for billig development database. +# gcc, libpq-dev: psycopg-dependencies. RUN apt-get update -RUN apt-get install sqlite3 +RUN apt-get install sqlite3 gcc libpq-dev -y +RUN export PATH=/usr/lib/postgresql/X.Y/bin/:$PATH -# Alias -RUN echo 'alias la="ls -la"' >> ~/.bashrc -# Useful alias shortcuts for django +################################## +# Alias # +################################## +RUN echo 'alias la="ls -la"' >> ~/.bashrc +# Useful alias shortcuts for django. RUN echo 'alias migrate="pipenv run python /app/manage.py migrate"' >> ~/.bashrc RUN echo 'alias makemigrations="pipenv run python /app/manage.py makemigrations"' >> ~/.bashrc RUN echo 'alias seed="pipenv run python /app/manage.py seed"' >> ~/.bashrc RUN echo 'alias collectstatic="pipenv run python /app/manage.py collectstatic --noinput"' >> ~/.bashrc RUN echo 'alias pipeline="pipenv run /app/run-pipeline.sh"' >> ~/.bashrc -# Make directories + +# Make directories. RUN mkdir /app WORKDIR /app -# Copy dependencies -COPY Pipfile Pipfile.lock ./ - -# Install virtual environment. +# Prepare virtual environment. RUN mkdir .venv RUN python -m pip install pipenv + +# Install venv dependecies. +COPY Pipfile Pipfile.lock ./ RUN python -m pipenv install --dev --deploy -# Copy remaining +# Copy remaining. COPY . /app -# Start +# Start. ENTRYPOINT ["/app/entrypoint.sh"] CMD ["pipenv", "run", "python", "manage.py", "runserver", "0.0.0.0:8000"] diff --git a/backend/Pipfile b/backend/Pipfile index e1abfc065..e2f42be6a 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -6,10 +6,10 @@ name = "pypi" [scripts] # See '/docs/pipenv.md'. # Doesn't work for powershell. - "pipenv:install" = "pipenv install" "pipenv:update" = "pipenv update" "pipenv:sync" = "bash -c \"pipenv clean; pipenv sync --dev\"" +"pipenv:sync-prod" = "pipenv sync" "pipenv:docker-install-dev" = "pipenv install --deploy --ignore-pipfile --dev" # 'deploy' means abort if outdated lock file. 'ignore-pipfile' means only install using the lock file. 'dev' means install dev dependencies. "pipenv:docker-install-prod" = "pipenv install --deploy --ignore-pipfile" # 'deploy' means abort if outdated lock file. 'ignore-pipfile' means only install using the lock file. "pipenv:outdated" = "pipenv update --outdated" # Show outdated dependencies. @@ -21,6 +21,7 @@ name = "pypi" "migrations:make" = "pipenv run python manage.py makemigrations --noinput" "migrations:verify" = "pipenv run python manage.py makemigrations --check --dry-run --noinput --verbosity 2" "migrations:apply" = "pipenv run python manage.py migrate" +"static:collect" = "pipenv run python manage.py collectstatic --noinput" "bandit:run" = "pipenv run bandit --recursive --ini .bandit ." "flake8:run" = "pipenv run flake8 --config=.flake8 ." "yapf:diff" = "pipenv run yapf --parallel --recursive --diff ." # Dry-run yapf on all files in the project. @@ -44,6 +45,7 @@ debugpy = "*" gunicorn = "*" django-admin-autocomplete-filter = "*" django-notifications-hq = "*" +psycopg = {extras = ["c"], version = "*"} [dev-packages] yapf = "*" @@ -52,10 +54,10 @@ bandit = "*" pillow = "*" flake8 = "~=5.0" flake8-annotations = "*" -flake8-keyword-arguments = "*" +# flake8-keyword-arguments = "*" flake8-print = "*" flake8-quotes = "*" requests = "*" [requires] -python_version = "3.11.0" # from .python-version +python_version = "3.11.2" # from .python-version diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index bf3451ed1..e6e85e367 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "fc3d1fb078ca2215b702cd3692e46a1b725aece789d9b183cb500a40cb21b37c" + "sha256": "b41f4c4e49ebaf4ae75888c7d1dab85f7d6f152710900f6227b31ae305e6941f" }, "pipfile-spec": 6, "requires": { - "python_version": "3.11.0" + "python_version": "3.11.2" }, "sources": [ { @@ -26,11 +26,12 @@ }, "dataclasses-json": { "hashes": [ - "sha256:1bd8418a61fe3d588bb0079214d7fb71d44937da40742b787256fd53b26b6c80", - "sha256:a53c220c35134ce08211a1057fd0e5bf76dc5331627c6b241cacbc570a89faae" + "sha256:1b934c1bd63e775880946b8361a902d7de86e894bab8098eab27c010f95724d1", + "sha256:71816ced3d0f55a2c5bc1a813ace1b8d4234e79a08744269a7cf84d6f7c06e99" ], "index": "pypi", - "version": "==0.6.1" + "markers": "python_version >= '3.7' and python_version < '4.0'", + "version": "==0.6.2" }, "debugpy": { "hashes": [ @@ -54,15 +55,17 @@ "sha256:ef9ab7df0b9a42ed9c878afd3eaaff471fce3fa73df96022e1f5c9f8f8c87ada" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==1.8.0" }, "django": { "hashes": [ - "sha256:08f41f468b63335aea0d904c5729e0250300f6a1907bf293a65499496cdbc68f", - "sha256:a64d2487cdb00ad7461434320ccc38e60af9c404773a2f95ab0093b4453a3215" + "sha256:8e0f1c2c2786b5c0e39fe1afce24c926040fad47c8ea8ad30aaf1188df29fc41", + "sha256:e1d37c51ad26186de355cbcec16613ebdabfa9689bbade9c538835205a8abbe9" ], "index": "pypi", - "version": "==4.2.6" + "markers": "python_version >= '3.8'", + "version": "==4.2.7" }, "django-admin-autocomplete-filter": { "hashes": [ @@ -74,11 +77,12 @@ }, "django-cors-headers": { "hashes": [ - "sha256:25aabc94d4837678c1edf442c7f68a5f5fd151f6767b0e0b01c61a2179d02711", - "sha256:bd36c7aea0d070e462f3383f0dc9ef717e5fdc2b10a99c98c285f16da84ffba2" + "sha256:0b1fd19297e37417fc9f835d39e45c8c642938ddba1acce0c1753d3edef04f36", + "sha256:0bf65ef45e606aff1994d35503e6b677c0b26cafff6506f8fd7187f3be840207" ], "index": "pypi", - "version": "==4.3.0" + "markers": "python_version >= '3.8'", + "version": "==4.3.1" }, "django-environ": { "hashes": [ @@ -86,6 +90,7 @@ "sha256:f32a87aa0899894c27d4e1776fa6b477e8164ed7f6b3e410a62a6d72caaf64be" ], "index": "pypi", + "markers": "python_version >= '3.6' and python_version < '4'", "version": "==0.11.2" }, "django-extensions": { @@ -94,6 +99,7 @@ "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==3.2.3" }, "django-guardian": { @@ -102,6 +108,7 @@ "sha256:c58a68ae76922d33e6bdc0e69af1892097838de56e93e78a8361090bcd9f89a0" ], "index": "pypi", + "markers": "python_version >= '3.5'", "version": "==2.4.0" }, "django-model-utils": { @@ -114,11 +121,10 @@ }, "django-notifications-hq": { "hashes": [ - "sha256:c3ca2fa300d438587882ff47a8ccefdde3c28d3f9103f482a692cfba21fb2c01", - "sha256:c7f0294d70be101a1291fdc69c86291ecd7be68ff95db0eb5f350b314bea3921" + "sha256:0f4b216bb382b7c7c4eef273eb211e59c1c6a0ea38cba6077415ac031d330725" ], "index": "pypi", - "version": "==1.8.2" + "version": "==1.8.3" }, "djangorestframework": { "hashes": [ @@ -126,6 +132,7 @@ "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==3.14.0" }, "gunicorn": { @@ -134,6 +141,7 @@ "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033" ], "index": "pypi", + "markers": "python_version >= '3.5'", "version": "==21.2.0" }, "iniconfig": { @@ -178,63 +186,64 @@ }, "pillow": { "hashes": [ - "sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff", - "sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f", - "sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21", - "sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635", - "sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a", - "sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f", - "sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1", - "sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d", - "sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db", - "sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849", - "sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7", - "sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876", - "sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3", - "sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317", - "sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91", - "sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d", - "sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b", - "sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd", - "sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed", - "sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500", - "sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7", - "sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a", - "sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a", - "sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0", - "sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf", - "sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f", - "sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1", - "sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088", - "sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971", - "sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a", - "sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205", - "sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54", - "sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08", - "sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21", - "sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d", - "sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08", - "sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e", - "sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf", - "sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b", - "sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145", - "sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2", - "sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d", - "sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d", - "sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf", - "sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad", - "sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d", - "sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1", - "sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4", - "sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2", - "sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19", - "sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37", - "sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4", - "sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68", - "sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1" + "sha256:00f438bb841382b15d7deb9a05cc946ee0f2c352653c7aa659e75e592f6fa17d", + "sha256:0248f86b3ea061e67817c47ecbe82c23f9dd5d5226200eb9090b3873d3ca32de", + "sha256:04f6f6149f266a100374ca3cc368b67fb27c4af9f1cc8cb6306d849dcdf12616", + "sha256:062a1610e3bc258bff2328ec43f34244fcec972ee0717200cb1425214fe5b839", + "sha256:0a026c188be3b443916179f5d04548092e253beb0c3e2ee0a4e2cdad72f66099", + "sha256:0f7c276c05a9767e877a0b4c5050c8bee6a6d960d7f0c11ebda6b99746068c2a", + "sha256:1a8413794b4ad9719346cd9306118450b7b00d9a15846451549314a58ac42219", + "sha256:1ab05f3db77e98f93964697c8efc49c7954b08dd61cff526b7f2531a22410106", + "sha256:1c3ac5423c8c1da5928aa12c6e258921956757d976405e9467c5f39d1d577a4b", + "sha256:1c41d960babf951e01a49c9746f92c5a7e0d939d1652d7ba30f6b3090f27e412", + "sha256:1fafabe50a6977ac70dfe829b2d5735fd54e190ab55259ec8aea4aaea412fa0b", + "sha256:1fb29c07478e6c06a46b867e43b0bcdb241b44cc52be9bc25ce5944eed4648e7", + "sha256:24fadc71218ad2b8ffe437b54876c9382b4a29e030a05a9879f615091f42ffc2", + "sha256:2cdc65a46e74514ce742c2013cd4a2d12e8553e3a2563c64879f7c7e4d28bce7", + "sha256:2ef6721c97894a7aa77723740a09547197533146fba8355e86d6d9a4a1056b14", + "sha256:3b834f4b16173e5b92ab6566f0473bfb09f939ba14b23b8da1f54fa63e4b623f", + "sha256:3d929a19f5469b3f4df33a3df2983db070ebb2088a1e145e18facbc28cae5b27", + "sha256:41f67248d92a5e0a2076d3517d8d4b1e41a97e2df10eb8f93106c89107f38b57", + "sha256:47e5bf85b80abc03be7455c95b6d6e4896a62f6541c1f2ce77a7d2bb832af262", + "sha256:4d0152565c6aa6ebbfb1e5d8624140a440f2b99bf7afaafbdbf6430426497f28", + "sha256:50d08cd0a2ecd2a8657bd3d82c71efd5a58edb04d9308185d66c3a5a5bed9610", + "sha256:61f1a9d247317fa08a308daaa8ee7b3f760ab1809ca2da14ecc88ae4257d6172", + "sha256:6932a7652464746fcb484f7fc3618e6503d2066d853f68a4bd97193a3996e273", + "sha256:7a7e3daa202beb61821c06d2517428e8e7c1aab08943e92ec9e5755c2fc9ba5e", + "sha256:7dbaa3c7de82ef37e7708521be41db5565004258ca76945ad74a8e998c30af8d", + "sha256:7df5608bc38bd37ef585ae9c38c9cd46d7c81498f086915b0f97255ea60c2818", + "sha256:806abdd8249ba3953c33742506fe414880bad78ac25cc9a9b1c6ae97bedd573f", + "sha256:883f216eac8712b83a63f41b76ddfb7b2afab1b74abbb413c5df6680f071a6b9", + "sha256:912e3812a1dbbc834da2b32299b124b5ddcb664ed354916fd1ed6f193f0e2d01", + "sha256:937bdc5a7f5343d1c97dc98149a0be7eb9704e937fe3dc7140e229ae4fc572a7", + "sha256:9882a7451c680c12f232a422730f986a1fcd808da0fd428f08b671237237d651", + "sha256:9a92109192b360634a4489c0c756364c0c3a2992906752165ecb50544c251312", + "sha256:9d7bc666bd8c5a4225e7ac71f2f9d12466ec555e89092728ea0f5c0c2422ea80", + "sha256:a5f63b5a68daedc54c7c3464508d8c12075e56dcfbd42f8c1bf40169061ae666", + "sha256:a646e48de237d860c36e0db37ecaecaa3619e6f3e9d5319e527ccbc8151df061", + "sha256:a89b8312d51715b510a4fe9fc13686283f376cfd5abca8cd1c65e4c76e21081b", + "sha256:a92386125e9ee90381c3369f57a2a50fa9e6aa8b1cf1d9c4b200d41a7dd8e992", + "sha256:ae88931f93214777c7a3aa0a8f92a683f83ecde27f65a45f95f22d289a69e593", + "sha256:afc8eef765d948543a4775f00b7b8c079b3321d6b675dde0d02afa2ee23000b4", + "sha256:b0eb01ca85b2361b09480784a7931fc648ed8b7836f01fb9241141b968feb1db", + "sha256:b1c25762197144e211efb5f4e8ad656f36c8d214d390585d1d21281f46d556ba", + "sha256:b4005fee46ed9be0b8fb42be0c20e79411533d1fd58edabebc0dd24626882cfd", + "sha256:b920e4d028f6442bea9a75b7491c063f0b9a3972520731ed26c83e254302eb1e", + "sha256:baada14941c83079bf84c037e2d8b7506ce201e92e3d2fa0d1303507a8538212", + "sha256:bb40c011447712d2e19cc261c82655f75f32cb724788df315ed992a4d65696bb", + "sha256:c0949b55eb607898e28eaccb525ab104b2d86542a85c74baf3a6dc24002edec2", + "sha256:c9aeea7b63edb7884b031a35305629a7593272b54f429a9869a4f63a1bf04c34", + "sha256:cfe96560c6ce2f4c07d6647af2d0f3c54cc33289894ebd88cfbb3bcd5391e256", + "sha256:d27b5997bdd2eb9fb199982bb7eb6164db0426904020dc38c10203187ae2ff2f", + "sha256:d921bc90b1defa55c9917ca6b6b71430e4286fc9e44c55ead78ca1a9f9eba5f2", + "sha256:e6bf8de6c36ed96c86ea3b6e1d5273c53f46ef518a062464cd7ef5dd2cf92e38", + "sha256:eaed6977fa73408b7b8a24e8b14e59e1668cfc0f4c40193ea7ced8e210adf996", + "sha256:fa1d323703cfdac2036af05191b969b910d8f115cf53093125e4058f62012c9a", + "sha256:fe1e26e1ffc38be097f0ba1d0d07fcade2bcfd1d023cda5b29935ae8052bd793" ], "index": "pypi", - "version": "==10.0.1" + "markers": "python_version >= '3.8'", + "version": "==10.1.0" }, "pluggy": { "hashes": [ @@ -244,21 +253,40 @@ "markers": "python_version >= '3.8'", "version": "==1.3.0" }, + "psycopg": { + "extras": [ + "c" + ], + "hashes": [ + "sha256:8ec5230d6a7eb654b4fb3cf2d3eda8871d68f24807b934790504467f1deee9f8", + "sha256:cec7ad2bc6a8510e56c45746c631cf9394148bdc8a9a11fd8cf8554ce129ae78" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.12" + }, + "psycopg-c": { + "hashes": [ + "sha256:81db07874c7c530482d07155d144b287b47260dd1782a0d2d3ca7ae2d4641686" + ], + "version": "==3.1.12" + }, "pytest": { "hashes": [ - "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002", - "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069" + "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", + "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" ], "index": "pypi", - "version": "==7.4.2" + "markers": "python_version >= '3.7'", + "version": "==7.4.3" }, "pytest-django": { "hashes": [ - "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e", - "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2" + "sha256:4e1c79d5261ade2dd58d91208017cd8f62cb4710b56e012ecd361d15d5d662a2", + "sha256:92d6fd46b1d79b54fb6b060bbb39428073396cec717d5f2e122a990d4b6aa5e8" ], "index": "pypi", - "version": "==4.5.2" + "markers": "python_version >= '3.8'", + "version": "==4.7.0" }, "pytz": { "hashes": [ @@ -313,6 +341,7 @@ "sha256:bdfc739baa03b880c2d15d0431b31c658ffc348e907fe197e54e0389dd59e11e" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==1.7.5" }, "certifi": { @@ -325,99 +354,99 @@ }, "charset-normalizer": { "hashes": [ - "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843", - "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786", - "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e", - "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8", - "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4", - "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa", - "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d", - "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82", - "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7", - "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895", - "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d", - "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a", - "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382", - "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678", - "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b", - "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e", - "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741", - "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4", - "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596", - "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9", - "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69", - "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c", - "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77", - "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13", - "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459", - "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e", - "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7", - "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908", - "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a", - "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f", - "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8", - "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482", - "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d", - "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d", - "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545", - "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34", - "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86", - "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6", - "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe", - "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e", - "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc", - "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7", - "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd", - "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c", - "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557", - "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a", - "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89", - "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078", - "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e", - "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4", - "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403", - "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0", - "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89", - "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115", - "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9", - "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05", - "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a", - "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec", - "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56", - "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38", - "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479", - "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c", - "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e", - "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd", - "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186", - "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455", - "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c", - "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65", - "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78", - "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287", - "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df", - "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43", - "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1", - "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7", - "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989", - "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a", - "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63", - "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884", - "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649", - "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810", - "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828", - "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4", - "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2", - "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd", - "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5", - "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe", - "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293", - "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e", - "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e", - "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8" + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.0" + "version": "==3.3.2" }, "flake8": { "hashes": [ @@ -425,6 +454,7 @@ "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248" ], "index": "pypi", + "markers": "python_full_version >= '3.6.1'", "version": "==5.0.4" }, "flake8-annotations": { @@ -433,22 +463,16 @@ "sha256:ff37375e71e3b83f2a5a04d443c41e2c407de557a884f3300a7fa32f3c41cb0a" ], "index": "pypi", + "markers": "python_full_version >= '3.8.1'", "version": "==3.0.1" }, - "flake8-keyword-arguments": { - "hashes": [ - "sha256:615977c41b8a1ced10c1ca723d4778b1ef33d401f8a997912052b6f60bb613b5", - "sha256:a15b6731713f37f2e5d1a60c5e2e04bc6c24569a4708b4054321580665e5f1bd" - ], - "index": "pypi", - "version": "==0.1.0" - }, "flake8-print": { "hashes": [ "sha256:76915a2a389cc1c0879636c219eb909c38501d3a43cc8dae542081c9ba48bdf9", "sha256:84a1a6ea10d7056b804221ac5e62b1cee1aefc897ce16f2e5c42d3046068f5d8" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==5.0.0" }, "flake8-quotes": { @@ -460,19 +484,19 @@ }, "gitdb": { "hashes": [ - "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a", - "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7" + "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4", + "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b" ], "markers": "python_version >= '3.7'", - "version": "==4.0.10" + "version": "==4.0.11" }, "gitpython": { "hashes": [ - "sha256:5f4c4187de49616d710a77e98ddf17b4782060a1788df441846bddefbb89ab33", - "sha256:f9b9ddc0761c125d5780eab2d64be4873fc6817c2899cbcb34b02344bdc7bc54" + "sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4", + "sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a" ], "markers": "python_version >= '3.7'", - "version": "==3.1.37" + "version": "==3.1.40" }, "idna": { "hashes": [ @@ -516,36 +540,37 @@ }, "mypy": { "hashes": [ - "sha256:091f53ff88cb093dcc33c29eee522c087a438df65eb92acd371161c1f4380ff0", - "sha256:1a69db3018b87b3e6e9dd28970f983ea6c933800c9edf8c503c3135b3274d5ad", - "sha256:24f3de8b9e7021cd794ad9dfbf2e9fe3f069ff5e28cb57af6f873ffec1cb0425", - "sha256:31eba8a7a71f0071f55227a8057468b8d2eb5bf578c8502c7f01abaec8141b2f", - "sha256:3c8835a07b8442da900db47ccfda76c92c69c3a575872a5b764332c4bacb5a0a", - "sha256:3df87094028e52766b0a59a3e46481bb98b27986ed6ded6a6cc35ecc75bb9182", - "sha256:49499cf1e464f533fc45be54d20a6351a312f96ae7892d8e9f1708140e27ce41", - "sha256:4c192445899c69f07874dabda7e931b0cc811ea055bf82c1ababf358b9b2a72c", - "sha256:4f3d27537abde1be6d5f2c96c29a454da333a2a271ae7d5bc7110e6d4b7beb3f", - "sha256:7469545380dddce5719e3656b80bdfbb217cfe8dbb1438532d6abc754b828fed", - "sha256:7807a2a61e636af9ca247ba8494031fb060a0a744b9fee7de3a54bed8a753323", - "sha256:856bad61ebc7d21dbc019b719e98303dc6256cec6dcc9ebb0b214b81d6901bd8", - "sha256:89513ddfda06b5c8ebd64f026d20a61ef264e89125dc82633f3c34eeb50e7d60", - "sha256:8e0db37ac4ebb2fee7702767dfc1b773c7365731c22787cb99f507285014fcaf", - "sha256:971104bcb180e4fed0d7bd85504c9036346ab44b7416c75dd93b5c8c6bb7e28f", - "sha256:9e1589ca150a51d9d00bb839bfeca2f7a04f32cd62fad87a847bc0818e15d7dc", - "sha256:9f8464ed410ada641c29f5de3e6716cbdd4f460b31cf755b2af52f2d5ea79ead", - "sha256:ab98b8f6fdf669711f3abe83a745f67f50e3cbaea3998b90e8608d2b459fd566", - "sha256:b19006055dde8a5425baa5f3b57a19fa79df621606540493e5e893500148c72f", - "sha256:c69051274762cccd13498b568ed2430f8d22baa4b179911ad0c1577d336ed849", - "sha256:d2dad072e01764823d4b2f06bc7365bb1d4b6c2f38c4d42fade3c8d45b0b4b67", - "sha256:dccd850a2e3863891871c9e16c54c742dba5470f5120ffed8152956e9e0a5e13", - "sha256:e28d7b221898c401494f3b77db3bac78a03ad0a0fff29a950317d87885c655d2", - "sha256:e4b7a99275a61aa22256bab5839c35fe8a6887781862471df82afb4b445daae6", - "sha256:eb7ff4007865833c470a601498ba30462b7374342580e2346bf7884557e40531", - "sha256:f8598307150b5722854f035d2e70a1ad9cc3c72d392c34fffd8c66d888c90f17", - "sha256:fea451a3125bf0bfe716e5d7ad4b92033c471e4b5b3e154c67525539d14dc15a" + "sha256:0e81ffd120ee24959b449b647c4b2fbfcf8acf3465e082b8d58fd6c4c2b27e46", + "sha256:185cff9b9a7fec1f9f7d8352dff8a4c713b2e3eea9c6c4b5ff7f0edf46b91e41", + "sha256:1e280b5697202efa698372d2f39e9a6713a0395a756b1c6bd48995f8d72690dc", + "sha256:1fe46e96ae319df21359c8db77e1aecac8e5949da4773c0274c0ef3d8d1268a9", + "sha256:2b53655a295c1ed1af9e96b462a736bf083adba7b314ae775563e3fb4e6795f5", + "sha256:551d4a0cdcbd1d2cccdcc7cb516bb4ae888794929f5b040bb51aae1846062901", + "sha256:55d28d7963bef00c330cb6461db80b0b72afe2f3c4e2963c99517cf06454e665", + "sha256:5da84d7bf257fd8f66b4f759a904fd2c5a765f70d8b52dde62b521972a0a2357", + "sha256:6cb8d5f6d0fcd9e708bb190b224089e45902cacef6f6915481806b0c77f7786d", + "sha256:7a7b1e399c47b18feb6f8ad4a3eef3813e28c1e871ea7d4ea5d444b2ac03c418", + "sha256:870bd1ffc8a5862e593185a4c169804f2744112b4a7c55b93eb50f48e7a77010", + "sha256:87c076c174e2c7ef8ab416c4e252d94c08cd4980a10967754f91571070bf5fbe", + "sha256:96650d9a4c651bc2a4991cf46f100973f656d69edc7faf91844e87fe627f7e96", + "sha256:a3637c03f4025f6405737570d6cbfa4f1400eb3c649317634d273687a09ffc2f", + "sha256:a79cdc12a02eb526d808a32a934c6fe6df07b05f3573d210e41808020aed8b5d", + "sha256:b633f188fc5ae1b6edca39dae566974d7ef4e9aaaae00bc36efe1f855e5173ac", + "sha256:bf7a2f0a6907f231d5e41adba1a82d7d88cf1f61a70335889412dec99feeb0f8", + "sha256:c1b06b4b109e342f7dccc9efda965fc3970a604db70f8560ddfdee7ef19afb05", + "sha256:cddee95dea7990e2215576fae95f6b78a8c12f4c089d7e4367564704e99118d3", + "sha256:d01921dbd691c4061a3e2ecdbfbfad029410c5c2b1ee88946bf45c62c6c91210", + "sha256:d0fa29919d2e720c8dbaf07d5578f93d7b313c3e9954c8ec05b6d83da592e5d9", + "sha256:d6ed9a3997b90c6f891138e3f83fb8f475c74db4ccaa942a1c7bf99e83a989a1", + "sha256:d93e76c2256aa50d9c82a88e2f569232e9862c9982095f6d54e13509f01222fc", + "sha256:df67fbeb666ee8828f675fee724cc2cbd2e4828cc3df56703e02fe6a421b7401", + "sha256:f29386804c3577c83d76520abf18cfcd7d68264c7e431c5907d250ab502658ee", + "sha256:f65f385a6f43211effe8c682e8ec3f55d79391f70a201575def73d08db68ead1", + "sha256:fc9fe455ad58a20ec68599139ed1113b21f977b536a91b42bef3ffed5cce7391" ], "index": "pypi", - "version": "==1.6.0" + "markers": "python_version >= '3.8'", + "version": "==1.7.0" }, "mypy-extensions": { "hashes": [ @@ -557,79 +582,80 @@ }, "pbr": { "hashes": [ - "sha256:567f09558bae2b3ab53cb3c1e2e33e726ff3338e7bae3db5dc954b3a44eef12b", - "sha256:aefc51675b0b533d56bb5fd1c8c6c0522fe31896679882e1c4c63d5e4a0fccb3" + "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda", + "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9" ], "markers": "python_version >= '2.6'", - "version": "==5.11.1" + "version": "==6.0.0" }, "pillow": { "hashes": [ - "sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff", - "sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f", - "sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21", - "sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635", - "sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a", - "sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f", - "sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1", - "sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d", - "sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db", - "sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849", - "sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7", - "sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876", - "sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3", - "sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317", - "sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91", - "sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d", - "sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b", - "sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd", - "sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed", - "sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500", - "sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7", - "sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a", - "sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a", - "sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0", - "sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf", - "sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f", - "sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1", - "sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088", - "sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971", - "sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a", - "sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205", - "sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54", - "sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08", - "sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21", - "sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d", - "sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08", - "sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e", - "sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf", - "sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b", - "sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145", - "sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2", - "sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d", - "sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d", - "sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf", - "sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad", - "sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d", - "sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1", - "sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4", - "sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2", - "sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19", - "sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37", - "sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4", - "sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68", - "sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1" + "sha256:00f438bb841382b15d7deb9a05cc946ee0f2c352653c7aa659e75e592f6fa17d", + "sha256:0248f86b3ea061e67817c47ecbe82c23f9dd5d5226200eb9090b3873d3ca32de", + "sha256:04f6f6149f266a100374ca3cc368b67fb27c4af9f1cc8cb6306d849dcdf12616", + "sha256:062a1610e3bc258bff2328ec43f34244fcec972ee0717200cb1425214fe5b839", + "sha256:0a026c188be3b443916179f5d04548092e253beb0c3e2ee0a4e2cdad72f66099", + "sha256:0f7c276c05a9767e877a0b4c5050c8bee6a6d960d7f0c11ebda6b99746068c2a", + "sha256:1a8413794b4ad9719346cd9306118450b7b00d9a15846451549314a58ac42219", + "sha256:1ab05f3db77e98f93964697c8efc49c7954b08dd61cff526b7f2531a22410106", + "sha256:1c3ac5423c8c1da5928aa12c6e258921956757d976405e9467c5f39d1d577a4b", + "sha256:1c41d960babf951e01a49c9746f92c5a7e0d939d1652d7ba30f6b3090f27e412", + "sha256:1fafabe50a6977ac70dfe829b2d5735fd54e190ab55259ec8aea4aaea412fa0b", + "sha256:1fb29c07478e6c06a46b867e43b0bcdb241b44cc52be9bc25ce5944eed4648e7", + "sha256:24fadc71218ad2b8ffe437b54876c9382b4a29e030a05a9879f615091f42ffc2", + "sha256:2cdc65a46e74514ce742c2013cd4a2d12e8553e3a2563c64879f7c7e4d28bce7", + "sha256:2ef6721c97894a7aa77723740a09547197533146fba8355e86d6d9a4a1056b14", + "sha256:3b834f4b16173e5b92ab6566f0473bfb09f939ba14b23b8da1f54fa63e4b623f", + "sha256:3d929a19f5469b3f4df33a3df2983db070ebb2088a1e145e18facbc28cae5b27", + "sha256:41f67248d92a5e0a2076d3517d8d4b1e41a97e2df10eb8f93106c89107f38b57", + "sha256:47e5bf85b80abc03be7455c95b6d6e4896a62f6541c1f2ce77a7d2bb832af262", + "sha256:4d0152565c6aa6ebbfb1e5d8624140a440f2b99bf7afaafbdbf6430426497f28", + "sha256:50d08cd0a2ecd2a8657bd3d82c71efd5a58edb04d9308185d66c3a5a5bed9610", + "sha256:61f1a9d247317fa08a308daaa8ee7b3f760ab1809ca2da14ecc88ae4257d6172", + "sha256:6932a7652464746fcb484f7fc3618e6503d2066d853f68a4bd97193a3996e273", + "sha256:7a7e3daa202beb61821c06d2517428e8e7c1aab08943e92ec9e5755c2fc9ba5e", + "sha256:7dbaa3c7de82ef37e7708521be41db5565004258ca76945ad74a8e998c30af8d", + "sha256:7df5608bc38bd37ef585ae9c38c9cd46d7c81498f086915b0f97255ea60c2818", + "sha256:806abdd8249ba3953c33742506fe414880bad78ac25cc9a9b1c6ae97bedd573f", + "sha256:883f216eac8712b83a63f41b76ddfb7b2afab1b74abbb413c5df6680f071a6b9", + "sha256:912e3812a1dbbc834da2b32299b124b5ddcb664ed354916fd1ed6f193f0e2d01", + "sha256:937bdc5a7f5343d1c97dc98149a0be7eb9704e937fe3dc7140e229ae4fc572a7", + "sha256:9882a7451c680c12f232a422730f986a1fcd808da0fd428f08b671237237d651", + "sha256:9a92109192b360634a4489c0c756364c0c3a2992906752165ecb50544c251312", + "sha256:9d7bc666bd8c5a4225e7ac71f2f9d12466ec555e89092728ea0f5c0c2422ea80", + "sha256:a5f63b5a68daedc54c7c3464508d8c12075e56dcfbd42f8c1bf40169061ae666", + "sha256:a646e48de237d860c36e0db37ecaecaa3619e6f3e9d5319e527ccbc8151df061", + "sha256:a89b8312d51715b510a4fe9fc13686283f376cfd5abca8cd1c65e4c76e21081b", + "sha256:a92386125e9ee90381c3369f57a2a50fa9e6aa8b1cf1d9c4b200d41a7dd8e992", + "sha256:ae88931f93214777c7a3aa0a8f92a683f83ecde27f65a45f95f22d289a69e593", + "sha256:afc8eef765d948543a4775f00b7b8c079b3321d6b675dde0d02afa2ee23000b4", + "sha256:b0eb01ca85b2361b09480784a7931fc648ed8b7836f01fb9241141b968feb1db", + "sha256:b1c25762197144e211efb5f4e8ad656f36c8d214d390585d1d21281f46d556ba", + "sha256:b4005fee46ed9be0b8fb42be0c20e79411533d1fd58edabebc0dd24626882cfd", + "sha256:b920e4d028f6442bea9a75b7491c063f0b9a3972520731ed26c83e254302eb1e", + "sha256:baada14941c83079bf84c037e2d8b7506ce201e92e3d2fa0d1303507a8538212", + "sha256:bb40c011447712d2e19cc261c82655f75f32cb724788df315ed992a4d65696bb", + "sha256:c0949b55eb607898e28eaccb525ab104b2d86542a85c74baf3a6dc24002edec2", + "sha256:c9aeea7b63edb7884b031a35305629a7593272b54f429a9869a4f63a1bf04c34", + "sha256:cfe96560c6ce2f4c07d6647af2d0f3c54cc33289894ebd88cfbb3bcd5391e256", + "sha256:d27b5997bdd2eb9fb199982bb7eb6164db0426904020dc38c10203187ae2ff2f", + "sha256:d921bc90b1defa55c9917ca6b6b71430e4286fc9e44c55ead78ca1a9f9eba5f2", + "sha256:e6bf8de6c36ed96c86ea3b6e1d5273c53f46ef518a062464cd7ef5dd2cf92e38", + "sha256:eaed6977fa73408b7b8a24e8b14e59e1668cfc0f4c40193ea7ced8e210adf996", + "sha256:fa1d323703cfdac2036af05191b969b910d8f115cf53093125e4058f62012c9a", + "sha256:fe1e26e1ffc38be097f0ba1d0d07fcade2bcfd1d023cda5b29935ae8052bd793" ], "index": "pypi", - "version": "==10.0.1" + "markers": "python_version >= '3.8'", + "version": "==10.1.0" }, "platformdirs": { "hashes": [ - "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3", - "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e" + "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b", + "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731" ], "markers": "python_version >= '3.7'", - "version": "==3.11.0" + "version": "==4.0.0" }, "pycodestyle": { "hashes": [ @@ -717,23 +743,16 @@ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.31.0" }, "rich": { "hashes": [ - "sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245", - "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef" + "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa", + "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235" ], "markers": "python_full_version >= '3.7.0'", - "version": "==13.6.0" - }, - "setuptools": { - "hashes": [ - "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87", - "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a" - ], - "markers": "python_version >= '3.8'", - "version": "==68.2.2" + "version": "==13.7.0" }, "smmap": { "hashes": [ @@ -769,11 +788,11 @@ }, "urllib3": { "hashes": [ - "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2", - "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564" + "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3", + "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54" ], - "markers": "python_version >= '3.7'", - "version": "==2.0.6" + "markers": "python_version >= '3.8'", + "version": "==2.1.0" }, "yapf": { "hashes": [ @@ -781,6 +800,7 @@ "sha256:adc8b5dd02c0143108878c499284205adb258aad6db6634e5b869e7ee2bd548b" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==0.40.2" }, "zipp": { diff --git a/backend/database/.gitignore b/backend/database/.gitignore index 7c9d611b5..06a3270ed 100644 --- a/backend/database/.gitignore +++ b/backend/database/.gitignore @@ -1,3 +1,5 @@ * !.gitignore !README.md +!recruitment_schema.drawio +!*.png diff --git a/backend/database/recruitment_schema.drawio b/backend/database/recruitment_schema.drawio new file mode 100644 index 000000000..206660bbc --- /dev/null +++ b/backend/database/recruitment_schema.drawio @@ -0,0 +1,685 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/database/recruitment_schema.png b/backend/database/recruitment_schema.png new file mode 100644 index 000000000..1a65c9c6b Binary files /dev/null and b/backend/database/recruitment_schema.png differ diff --git a/backend/logs/README.md b/backend/logs/README.md index 4a15e380c..f8903c07b 100644 --- a/backend/logs/README.md +++ b/backend/logs/README.md @@ -1 +1,7 @@ This folder is dedicated to contain log files configured in django. + +These filenames are reserved: + +- `.log`: server logs from django. +- `sql.log`: database logs from django server. +- `deploy____.log`: logs executed by [deploy.sh](../../../deploy.sh). diff --git a/backend/root/constants.py b/backend/root/constants.py index 4259b3de0..9d5fc53e8 100644 --- a/backend/root/constants.py +++ b/backend/root/constants.py @@ -25,6 +25,15 @@ class Environment: # Name of attribute set on response for requested impersonation of user_id. REQUESTED_IMPERSONATE_USER = 'requested_impersonate_user' +# Name of the http header from Github webhook to compare signature of payload. +# https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries +GITHUB_SIGNATURE_HEADER = 'HTTP_X_HUB_SIGNATURE_256' + +# Name of the http header from Github webhook to identify event type. +# (action can be found in the payload.) +# https://docs.github.com/en/webhooks/using-webhooks/best-practices-for-using-webhooks +GITHUB_EVENT_HEADER = 'X-GitHub-Event' + # This token can be imported anywhere to retrieve the values. request_contextvar: ContextVar[HttpRequest] = ContextVar('request_contextvar', default=None) diff --git a/backend/root/custom_classes/admin_classes.py b/backend/root/custom_classes/admin_classes.py index 15f7eee2b..78902849f 100644 --- a/backend/root/custom_classes/admin_classes.py +++ b/backend/root/custom_classes/admin_classes.py @@ -7,7 +7,6 @@ from django.urls import reverse from django.contrib import admin from django.db.models import QuerySet -from django.utils.safestring import mark_safe from django.contrib.auth.admin import UserAdmin, GroupAdmin from admin_auto_filters.filters import AutocompleteFilter @@ -45,7 +44,8 @@ def user_link(self, obj: Contact) -> str: """ if obj: href = get_admin_url(obj=obj) - return mark_safe(f'{obj}') # nosec mark_safe + + return f'{obj}' # nosec: B308, B703 return None diff --git a/backend/root/db_router.py b/backend/root/db_router.py index 2c64a4693..7d0f03d24 100644 --- a/backend/root/db_router.py +++ b/backend/root/db_router.py @@ -1,9 +1,7 @@ -# -# Handles routing for databases -# (which database should be used for which model) -# -# All models use the default database except billig models -# +""" +Handles routing for databases (which database should be used for which model). +All models use the default database except billig models. +""" from typing import Any, Type from django.db import models @@ -13,7 +11,7 @@ BilligTicketGroup, ) -# List of models routed to billig database +# List of models routed to billig database. BILLIG_MODELS: list[Type[models.Model]] = [ BilligEvent, BilligTicketGroup, diff --git a/backend/root/urls.py b/backend/root/urls.py index 25b63b85e..db7249314 100644 --- a/backend/root/urls.py +++ b/backend/root/urls.py @@ -14,6 +14,6 @@ path('', include('samfundet.urls')), # Put last. ] -# Setup static access and media upload +# Setup static access and media upload. urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/backend/root/utils/routes.py b/backend/root/utils/routes.py index 31abead04..48cab5507 100644 --- a/backend/root/utils/routes.py +++ b/backend/root/utils/routes.py @@ -5,7 +5,7 @@ DO NOT WRITE IN THIS FILE, AS IT WILL BE OVERWRITTEN ON NEXT UPDATE. THIS FILE WAS GENERATED BY: root.management.commands.generate_routes -LAST UPDATE: 2023-09-24 15:12:38.294245+00:00 +LAST UPDATE: 2023-11-02 18:37:45.368800+00:00 """ ############################################################ @@ -353,6 +353,15 @@ admin__samfundet_interviewroom_delete = 'admin:samfundet_interviewroom_delete' admin__samfundet_interviewroom_change = 'admin:samfundet_interviewroom_change' adminsamfundetinterviewroom__objectId = '' +admin__samfundet_interview_permissions = 'admin:samfundet_interview_permissions' +admin__samfundet_interview_permissions_manage_user = 'admin:samfundet_interview_permissions_manage_user' +admin__samfundet_interview_permissions_manage_group = 'admin:samfundet_interview_permissions_manage_group' +admin__samfundet_interview_changelist = 'admin:samfundet_interview_changelist' +admin__samfundet_interview_add = 'admin:samfundet_interview_add' +admin__samfundet_interview_history = 'admin:samfundet_interview_history' +admin__samfundet_interview_delete = 'admin:samfundet_interview_delete' +admin__samfundet_interview_change = 'admin:samfundet_interview_change' +adminsamfundetinterview__objectId = '' admin__samfundet_notification_changelist = 'admin:samfundet_notification_changelist' admin__samfundet_notification_add = 'admin:samfundet_notification_add' admin__samfundet_notification_history = 'admin:samfundet_notification_history' @@ -427,8 +436,12 @@ samfundet__recruitment_position_detail = 'samfundet:recruitment_position-detail' samfundet__recruitment_admissions_for_applicant_list = 'samfundet:recruitment_admissions_for_applicant-list' samfundet__recruitment_admissions_for_applicant_detail = 'samfundet:recruitment_admissions_for_applicant-detail' +samfundet__recruitment_admissions_for_group_list = 'samfundet:recruitment_admissions_for_group-list' +samfundet__recruitment_admissions_for_group_detail = 'samfundet:recruitment_admissions_for_group-detail' samfundet__recruitment_admissions_for_gang_list = 'samfundet:recruitment_admissions_for_gang-list' samfundet__recruitment_admissions_for_gang_detail = 'samfundet:recruitment_admissions_for_gang-detail' +samfundet__interview_list = 'samfundet:interview-list' +samfundet__interview_detail = 'samfundet:interview-detail' samfundet__api_root = 'samfundet:api-root' samfundet__csrf = 'samfundet:csrf' samfundet__login = 'samfundet:login' @@ -444,6 +457,7 @@ samfundet__home = 'samfundet:home' samfundet__assign_group = 'samfundet:assign_group' samfundet__recruitment_positions = 'samfundet:recruitment_positions' +samfundet__recruitment_positions_gang = 'samfundet:recruitment_positions_gang' samfundet__active_recruitment_positions = 'samfundet:active_recruitment_positions' samfundet__applicants_without_interviews = 'samfundet:applicants_without_interviews/' static__path = '' diff --git a/backend/samfundet/admin.py b/backend/samfundet/admin.py index f780483bf..2ee248dfc 100644 --- a/backend/samfundet/admin.py +++ b/backend/samfundet/admin.py @@ -20,6 +20,7 @@ RecruitmentAdmission, InterviewRoom, Interview, + Occupiedtimeslot, ) from .models.general import ( Tag, @@ -62,6 +63,8 @@ # Unregister User and Group to set new Admins. admin.site.unregister(Group) +# Just for testing TODO remove when done +admin.site.register(Occupiedtimeslot) @admin.register(User) diff --git a/backend/samfundet/migrations/0041_recruitmentposition_norwegian_applicants_only_and_more.py b/backend/samfundet/migrations/0041_recruitmentposition_norwegian_applicants_only_and_more.py new file mode 100644 index 000000000..75c96975c --- /dev/null +++ b/backend/samfundet/migrations/0041_recruitmentposition_norwegian_applicants_only_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.6 on 2023-11-16 12:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('samfundet', '0040_alter_notification_options_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='recruitmentposition', + name='norwegian_applicants_only', + field=models.BooleanField(default=False, help_text='Is this position only for Norwegian applicants?'), + ), + migrations.AlterField( + model_name='recruitmentposition', + name='default_admission_letter_en', + field=models.TextField(blank=True, help_text='Default admission letter for the position', null=True), + ), + migrations.AlterField( + model_name='recruitmentposition', + name='long_description_en', + field=models.TextField(blank=True, help_text='Long description of the position', null=True), + ), + migrations.AlterField( + model_name='recruitmentposition', + name='short_description_en', + field=models.CharField(blank=True, help_text='Short description of the position', max_length=100, null=True), + ), + ] diff --git a/backend/samfundet/migrations/0042_occupiedtimeslot.py b/backend/samfundet/migrations/0042_occupiedtimeslot.py new file mode 100644 index 000000000..d65e64621 --- /dev/null +++ b/backend/samfundet/migrations/0042_occupiedtimeslot.py @@ -0,0 +1,53 @@ +# Generated by Django 5.0 on 2023-12-17 21:25 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("samfundet", "0041_recruitmentposition_norwegian_applicants_only_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="Occupiedtimeslot", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "start_dt", + models.DateTimeField(help_text="The time of the interview"), + ), + ("end_dt", models.DateTimeField(help_text="The time of the interview")), + ( + "recruitment", + models.ForeignKey( + help_text="Occupied timeslots for the users for this recruitment", + on_delete=django.db.models.deletion.CASCADE, + to="samfundet.recruitment", + ), + ), + ( + "user", + models.ForeignKey( + help_text="Occupied timeslots for user", + on_delete=django.db.models.deletion.CASCADE, + related_name="occupied_timeslots", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/backend/samfundet/models/recruitment.py b/backend/samfundet/models/recruitment.py index 333ea7cc6..01e55360e 100644 --- a/backend/samfundet/models/recruitment.py +++ b/backend/samfundet/models/recruitment.py @@ -28,7 +28,20 @@ def is_active(self) -> bool: return self.visible_from < timezone.now() < self.actual_application_deadline def clean(self, *args: tuple, **kwargs: dict) -> None: - # All times should be in the future + super().clean() + + if not all( + [ + self.visible_from, + self.actual_application_deadline, + self.shown_application_deadline, + self.reprioritization_deadline_for_applicant, + self.reprioritization_deadline_for_groups, + ] + ): + raise ValidationError('Missing datetime') + + # All times should be in the future. now = timezone.now() if any( [ @@ -38,24 +51,18 @@ def clean(self, *args: tuple, **kwargs: dict) -> None: ): raise ValidationError('All times should be in the future') - # Deadline should be after visible from - if self.visible_from > self.actual_application_deadline: - raise ValidationError('Application deadline should be after visible from') + if self.actual_application_deadline < self.visible_from: + raise ValidationError('Visible from should be before application deadline') - # Shown deadline should be before the actual deadline - if self.shown_application_deadline > self.actual_application_deadline: + if self.actual_application_deadline < self.shown_application_deadline: raise ValidationError('Shown application deadline should be before the actual application deadline') - # Actual deadline should be before reprioritization deadline for applicants - if self.actual_application_deadline > self.reprioritization_deadline_for_applicant: + if self.reprioritization_deadline_for_applicant < self.actual_application_deadline: raise ValidationError('Actual application deadline should be before reprioritization deadline for applicants') - # Reprioritization deadline for applicants should be before reprioritization deadline for groups - if self.reprioritization_deadline_for_applicant > self.reprioritization_deadline_for_groups: + if self.reprioritization_deadline_for_groups < self.reprioritization_deadline_for_applicant: raise ValidationError('Reprioritization deadline for applicants should be before reprioritization deadline for groups') - super().clean() - def __str__(self) -> str: return f'Recruitment: {self.name_en} at {self.organization}' @@ -65,15 +72,17 @@ class RecruitmentPosition(FullCleanSaveMixin): name_en = models.CharField(max_length=100, help_text='Name of the position') short_description_nb = models.CharField(max_length=100, help_text='Short description of the position') - short_description_en = models.CharField(max_length=100, help_text='Short description of the position') + short_description_en = models.CharField(max_length=100, help_text='Short description of the position', null=True, blank=True) long_description_nb = models.TextField(help_text='Long description of the position') - long_description_en = models.TextField(help_text='Long description of the position') + long_description_en = models.TextField(help_text='Long description of the position', null=True, blank=True) is_funksjonaer_position = models.BooleanField(help_text='Is this a funksjonær position?') default_admission_letter_nb = models.TextField(help_text='Default admission letter for the position') - default_admission_letter_en = models.TextField(help_text='Default admission letter for the position') + default_admission_letter_en = models.TextField(help_text='Default admission letter for the position', null=True, blank=True) + + norwegian_applicants_only = models.BooleanField(help_text='Is this position only for Norwegian applicants?', default=False) gang = models.ForeignKey(to=Gang, on_delete=models.CASCADE, help_text='The gang that is recruiting') recruitment = models.ForeignKey( @@ -96,6 +105,14 @@ class RecruitmentPosition(FullCleanSaveMixin): def __str__(self) -> str: return f'Position: {self.name_en} in {self.recruitment}' + def save(self, *args: tuple, **kwargs: dict) -> None: + if self.norwegian_applicants_only: + self.name_en = 'Norwegian speaking applicants only' + self.short_description_en = 'This position only admits Norwegian speaking applicants' + self.long_description_en = 'No english applicants' + self.default_admission_letter_en = 'No english applicants' + super(RecruitmentPosition, self).save(*args, **kwargs) + class InterviewRoom(FullCleanSaveMixin): name = models.CharField(max_length=255, help_text='Name of the room') @@ -109,11 +126,11 @@ def __str__(self) -> str: return self.name def clean(self) -> None: + super().clean() + if self.start_time > self.end_time: raise ValidationError('Start time should be before end time') - super().clean() - class Interview(FullCleanSaveMixin): # User visible fields @@ -183,4 +200,22 @@ def save(self, *args: tuple, **kwargs: dict) -> None: # Create a new interview instance if needed self.interview = Interview.objects.create() - super(RecruitmentAdmission, self).save(*args, **kwargs) + super().save(*args, **kwargs) + + +class Occupiedtimeslot(FullCleanSaveMixin): + + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + null=False, + blank=False, + help_text='Occupied timeslots for user', + related_name='occupied_timeslots', + ) + # Mostly only used for deletion, and anonymization. + recruitment = models.ForeignKey(Recruitment, on_delete=models.CASCADE, help_text='Occupied timeslots for the users for this recruitment') + + # Start and end time of availability + start_dt = models.DateTimeField(help_text='The time of the interview', null=False, blank=False) + end_dt = models.DateTimeField(help_text='The time of the interview', null=False, blank=False) diff --git a/backend/samfundet/models/tests/test_recruitment.py b/backend/samfundet/models/tests/test_recruitment.py new file mode 100644 index 000000000..e090a8543 --- /dev/null +++ b/backend/samfundet/models/tests/test_recruitment.py @@ -0,0 +1,69 @@ +import pytest + +from django.utils import timezone +from django.core.exceptions import ValidationError + +from samfundet.models.recruitment import Recruitment, Organization + +datetime_fields_expecting_error = [ + # 'visible_from', # Allowed to be in the past. + 'actual_application_deadline', + 'shown_application_deadline', + 'reprioritization_deadline_for_applicant', + 'reprioritization_deadline_for_groups', +] + + +@pytest.fixture +def fixture_org(): + org = Organization.objects.create(name='Samf') + yield org + org.delete() + + +FUTURE_DAYS = 20 + + +def _create_recruitment_with_dt(*, overrides: dict[str, timezone.datetime]) -> Recruitment: + """Override fields with kwargs.""" + future = timezone.now() + timezone.timedelta(days=FUTURE_DAYS) + + fields = {field: future for field in datetime_fields_expecting_error} + fields['visible_from'] = future + fields.update(overrides) + + Recruitment.objects.create( + **fields, + name_nb='Navn', + name_en='Name', + organization=Organization.objects.get(name='Samf'), + ) + + +class TestRecruitmentClean: + + def test_all_datetimes_is_in_the_future(self, fixture_org): + error_msg = 'All times should be in the future' + past = timezone.now() - timezone.timedelta(days=2) + + for field in datetime_fields_expecting_error: + with pytest.raises(ValidationError, match=error_msg): + _create_recruitment_with_dt(overrides={field: past}) + + def test_visible_from_before_application_deadline(self, fixture_org): + error_msg = 'Visible from should be before application deadline' + future_more = timezone.now() + timezone.timedelta(days=FUTURE_DAYS + 2) + with pytest.raises(ValidationError, match=error_msg): + _create_recruitment_with_dt(overrides={'visible_from': future_more}) + + def test_application_deadline_before_reprioritization_deadline(self, fixture_org): + error_msg = 'Actual application deadline should be before reprioritization deadline for applicants' + future_more = timezone.now() + timezone.timedelta(days=FUTURE_DAYS + 2) + with pytest.raises(ValidationError, match=error_msg): + _create_recruitment_with_dt(overrides={'actual_application_deadline': future_more}) + + def test_reprioritization_deadline_for_applicant_before_reprioritization_deadline_for_groups(self, fixture_org): + error_msg = 'Reprioritization deadline for applicants should be before reprioritization deadline for groups' + future_more = timezone.now() + timezone.timedelta(days=FUTURE_DAYS + 2) + with pytest.raises(ValidationError, match=error_msg): + _create_recruitment_with_dt(overrides={'reprioritization_deadline_for_applicant': future_more}) diff --git a/backend/samfundet/serializers.py b/backend/samfundet/serializers.py index 2a1204f8d..0a347ea75 100644 --- a/backend/samfundet/serializers.py +++ b/backend/samfundet/serializers.py @@ -15,6 +15,7 @@ RecruitmentAdmission, InterviewRoom, Interview, + Occupiedtimeslot, ) from .models.event import (Event, EventGroup, EventCustomTicket) from .models.general import ( @@ -518,6 +519,7 @@ def get_recruitment_admission_ids(self, obj: User) -> list[int]: class RecruitmentPositionSerializer(serializers.ModelSerializer): + gang = GangSerializer(read_only=True) class Meta: model = RecruitmentPosition @@ -550,11 +552,19 @@ def create(self, validated_data: dict) -> RecruitmentAdmission: return recruitment_admission +class OccupiedtimeslotSerializer(serializers.ModelSerializer): + + class Meta: + model = Occupiedtimeslot + fields = '__all__' + + class ApplicantInfoSerializer(serializers.ModelSerializer): + occupied_timeslots = OccupiedtimeslotSerializer(many=True) class Meta: model = User - fields = ['id', 'first_name', 'last_name', 'email'] + fields = ['id', 'first_name', 'last_name', 'email', 'occupied_timeslots'] class InterviewRoomSerializer(serializers.ModelSerializer): diff --git a/backend/samfundet/tests/test_email.py b/backend/samfundet/tests/test_email.py index 67d2d71f1..fb52dc7df 100644 --- a/backend/samfundet/tests/test_email.py +++ b/backend/samfundet/tests/test_email.py @@ -1,4 +1,3 @@ -import pytest from django.core import mail diff --git a/backend/samfundet/tests/test_models.py b/backend/samfundet/tests/test_models.py index f9799ec0a..03cea629c 100644 --- a/backend/samfundet/tests/test_models.py +++ b/backend/samfundet/tests/test_models.py @@ -1,11 +1,10 @@ from datetime import datetime, timedelta import pytest + from django.core.exceptions import ValidationError -from django.utils import timezone -from samfundet.models.general import Booking, Organization -from samfundet.models.recruitment import Recruitment +from samfundet.models.general import Booking @pytest.mark.django_db @@ -23,29 +22,3 @@ def test_booking_duration_constraint_less_than_2_hours(): expected_msg = 'Duration cannot be longer than 2 hours.' with pytest.raises(ValidationError, match=expected_msg): booking.save() - - -@pytest.mark.django_db -def test_recruitment_time_constraints(fixture_recruitment: Recruitment): - now = timezone.now() - one_hour = timedelta(hours=1) - - # Change actual_application_deadline to the past, should fail. - fixture_recruitment.actual_application_deadline = now - one_hour - expected_msg = 'All times should be in the future' - with pytest.raises(ValidationError, match=expected_msg): - fixture_recruitment.clean() - - # Reset actual_application_deadline and change visible_from to be after actual_application_deadline, should fail. - fixture_recruitment.actual_application_deadline = now + 3 * one_hour - fixture_recruitment.visible_from = now + 4 * one_hour - expected_msg = 'Application deadline should be after visible from' - with pytest.raises(ValidationError, match=expected_msg): - fixture_recruitment.clean() - - # Reset visible_from and change shown_application_deadline to be after actual_application_deadline, should fail. - fixture_recruitment.visible_from = now - fixture_recruitment.shown_application_deadline = now + 4 * one_hour - expected_msg = 'Shown application deadline should be before the actual application deadline' - with pytest.raises(ValidationError, match=expected_msg): - fixture_recruitment.clean() diff --git a/backend/samfundet/tests/test_signals.py b/backend/samfundet/tests/test_signals.py index b41406cb4..bef9e661d 100644 --- a/backend/samfundet/tests/test_signals.py +++ b/backend/samfundet/tests/test_signals.py @@ -2,7 +2,6 @@ from django.contrib.auth.models import Group from samfundet.models import Event, Gang, Profile, User, UserPreference -from samfundet.permissions import SAMFUNDET_CHANGE_EVENT, SAMFUNDET_DELETE_EVENT class TestUserSignals: diff --git a/backend/samfundet/urls.py b/backend/samfundet/urls.py index 187455f24..cfb37f2b1 100644 --- a/backend/samfundet/urls.py +++ b/backend/samfundet/urls.py @@ -36,6 +36,7 @@ router.register('recruitment', views.RecruitmentView, 'recruitment') router.register('recruitment-position', views.RecruitmentPositionView, 'recruitment_position') router.register('recruitment-admisisons-for-applicant', views.RecruitmentAdmissionForApplicantView, 'recruitment_admissions_for_applicant') +router.register('recruitment-admisisons-for-group', views.RecruitmentAdmissionForGangView, 'recruitment_admissions_for_group') router.register('recruitment-admisisons-for-gang', views.RecruitmentAdmissionForGangView, 'recruitment_admissions_for_gang') router.register('interview', views.InterviewView, 'interview') @@ -56,9 +57,12 @@ path('isclosed/', views.IsClosedView().as_view(), name='isclosed'), path('home/', views.HomePageView().as_view(), name='home'), path('assign_group/', views.AssignGroupView.as_view(), name='assign_group'), + path('webhook/', views.WebhookView.as_view(), name='webhook'), ########## Recruitment ########## path('recruitment-positions/', views.RecruitmentPositionsPerRecruitmentView.as_view(), name='recruitment_positions'), + path('recruitment-positions-gang/', views.RecruitmentPositionsPerGangView.as_view(), name='recruitment_positions_gang'), path('active-recruitment-positions/', views.ActiveRecruitmentPositionsView.as_view(), name='active_recruitment_positions'), path('applicants-without-interviews/', views.ApplicantsWithoutInterviewsView.as_view(), name='applicants_without_interviews/'), + path('occupiedtimeslot/', views.OccupiedtimeslotView.as_view(), name='occupied_timeslots') ] diff --git a/backend/samfundet/utils.py b/backend/samfundet/utils.py index 2a124346a..e47674b2a 100644 --- a/backend/samfundet/utils.py +++ b/backend/samfundet/utils.py @@ -1,8 +1,8 @@ from __future__ import annotations +from django.http import QueryDict from django.db.models import Q from django.db.models.query import QuerySet -from django.http import QueryDict from .models.event import ( Event, diff --git a/backend/samfundet/views.py b/backend/samfundet/views.py index 84e012343..688d8ee38 100644 --- a/backend/samfundet/views.py +++ b/backend/samfundet/views.py @@ -1,37 +1,45 @@ -from typing import Type +import os +import hmac +import hashlib +from typing import Any, Type -from django.db.models import Count, Case, When +from django.utils import timezone +from django.shortcuts import get_object_or_404 +from django.db.models import Count, Case, When, QuerySet from django.contrib.auth import login, logout -from django.contrib.auth.models import Group -from django.db.models import QuerySet +from django.utils.encoding import force_bytes from django.middleware.csrf import get_token -from django.shortcuts import get_object_or_404 -from django.utils import timezone from django.utils.decorators import method_decorator +from django.contrib.auth.models import Group from django.views.decorators.csrf import csrf_protect, ensure_csrf_cookie + from guardian.shortcuts import get_objects_for_user + from rest_framework import status -from rest_framework.generics import ListAPIView -from rest_framework.permissions import AllowAny, IsAuthenticated, BasePermission, DjangoModelPermissionsOrAnonReadOnly +from rest_framework.views import APIView from rest_framework.request import Request from rest_framework.response import Response -from rest_framework.views import APIView +from rest_framework.generics import ListAPIView, ListCreateAPIView from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet +from rest_framework.exceptions import PermissionDenied +from rest_framework.permissions import AllowAny, IsAuthenticated, BasePermission, DjangoModelPermissionsOrAnonReadOnly from root.constants import ( XCSRFTOKEN, AUTH_BACKEND, + GITHUB_SIGNATURE_HEADER, REQUESTED_IMPERSONATE_USER, ) from .homepage import homepage from .models.event import Event, EventGroup from .models.recruitment import ( + Interview, Recruitment, + InterviewRoom, + Occupiedtimeslot, RecruitmentPosition, RecruitmentAdmission, - InterviewRoom, - Interview, ) from .models.general import ( Tag, @@ -48,8 +56,8 @@ GangType, TextItem, KeyValue, - Organization, BlogPost, + Organization, FoodCategory, Saksdokument, ClosedPeriod, @@ -76,8 +84,8 @@ KeyValueSerializer, MenuItemSerializer, GangTypeSerializer, - InterviewSerializer, BlogPostSerializer, + InterviewSerializer, EventGroupSerializer, RecruitmentSerializer, SaksdokumentSerializer, @@ -88,6 +96,7 @@ FoodPreferenceSerializer, UserPreferenceSerializer, InformationPageSerializer, + OccupiedtimeslotSerializer, UserForRecruitmentSerializer, RecruitmentPositionSerializer, RecruitmentAdmissionForGangSerializer, @@ -157,11 +166,11 @@ class EventPerDayView(APIView): permission_classes = [AllowAny] def get(self, request: Request) -> Response: - # Fetch and serialize events + # Fetch and serialize events. events = Event.objects.filter(start_dt__gt=timezone.now()).order_by('start_dt') serialized = EventSerializer(events, many=True).data - # Organize in date dictionary + # Organize in date dictionary. events_per_day: dict = {} for event, serial in zip(events, serialized): date = event.start_dt.strftime('%Y-%m-%d') @@ -414,6 +423,43 @@ class ProfileView(ModelViewSet): queryset = Profile.objects.all() +class WebhookView(APIView): + """ + https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries + https://simpleisbetterthancomplex.com/tutorial/2016/10/31/how-to-handle-github-webhooks-using-django.html + """ + permission_classes = [AllowAny] + + # TODO: Whitelist ip? https://docs.github.com/en/webhooks/using-webhooks/best-practices-for-using-webhooks#allow-githubs-ip-addresses + # TODO: Ensure unique delivery? # https://docs.github.com/en/webhooks/using-webhooks/best-practices-for-using-webhooks#use-the-x-github-delivery-header + + def post(self, request: Request) -> Response: + + WebhookView.verify_signature( + payload_body=request.stream.body, + secret_token=os.environ['WEBHOOK_SECRET'], + signature_header=request.META[GITHUB_SIGNATURE_HEADER], + ) + return Response() # Success. + + def verify_signature(*, payload_body: Any, secret_token: str, signature_header: str) -> None: + """Verify that the payload was sent from GitHub by validating SHA256. + + Raise and return 403 if not authorized. + + Args: + payload_body: original request body to verify (request.body()) + secret_token: GitHub app webhook token (WEBHOOK_SECRET) + signature_header: header received from GitHub (x-hub-signature-256) + """ + if not signature_header: + raise PermissionDenied(detail='x-hub-signature-256 header is missing!') + hash_object = hmac.new(key=force_bytes(secret_token), msg=force_bytes(payload_body), digestmod=hashlib.sha256) + expected_signature = 'sha256=' + hash_object.hexdigest() + if not hmac.compare_digest(force_bytes(expected_signature), force_bytes(signature_header)): + raise PermissionDenied(detail="Request signatures didn't match!") + + @method_decorator(ensure_csrf_cookie, 'dispatch') class AssignGroupView(APIView): """ @@ -515,6 +561,24 @@ def get_queryset(self) -> Response: return None +@method_decorator(ensure_csrf_cookie, 'dispatch') +class RecruitmentPositionsPerGangView(ListAPIView): + permission_classes = [AllowAny] + serializer_class = RecruitmentPositionSerializer + + def get_queryset(self) -> Response: + """ + Optionally restricts the returned positions to a given recruitment, + by filtering against a `recruitment` query parameter in the URL. + """ + recruitment = self.request.query_params.get('recruitment', None) + gang = self.request.query_params.get('gang', None) + if recruitment is not None and gang is not None: + return RecruitmentPosition.objects.filter(gang=gang, recruitment=recruitment) + else: + return None + + class ApplicantsWithoutInterviewsView(ListAPIView): permission_classes = [AllowAny] serializer_class = UserForRecruitmentSerializer @@ -637,3 +701,24 @@ class InterviewView(ModelViewSet): permission_classes = [AllowAny] serializer_class = InterviewSerializer queryset = Interview.objects.all() + + +class OccupiedtimeslotView(ListCreateAPIView): + model = Occupiedtimeslot + serializer_class = OccupiedtimeslotSerializer + + def get_queryset(self) -> QuerySet[Occupiedtimeslot]: + recruitment = self.request.query_params.get('recruitment', Recruitment.objects.order_by('-actual_application_deadline').first()) + return Occupiedtimeslot.objects.filter(recruitment=recruitment, user=self.request.user.id) + + def create(self, request: Request) -> Response: + for p in request.data: + p['user'] = request.user.id + # TODO Could maybe need a check for saving own, not allowing to save others to themselves + serializer = self.get_serializer(data=request.data, many=True) + if serializer.is_valid(): + serializer.save() + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 000000000..1b91e03d9 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +# Get date and timestamp to ensure unique filenames. +# https://man7.org/linux/man-pages/man1/date.1.html +# https://www.geeksforgeeks.org/date-command-linux-examples/ +# Structured like this: __ +# Outputs: ______ +# Example: 19_11_2023__23_02_57 +now="$(date +"%m_%d_%Y__%H_%M_%S")" +filename="backend/logs/deploy__$now.log" + +echo "Running deploy script..." +echo "See logs in $filename" + +# https://serverfault.com/questions/103501/how-can-i-fully-log-all-bash-scripts-actions +# Redirect all commands, results and errors to a log file. +exec 1> "$filename" 2>&1 + + +# https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html +# -e: Stop script on first failed command. +# -x: Trace executed commands. +set -ex + +# Fetch latest changes. +# -f: force pull, overwrite local changes. +git pull -f origin master + +################################## +# Frontend # +################################## + +cd frontend || exit +yarnpkg run ci:debian +yarnpkg run build +cd .. + +################################## +# Backend # +################################## + +cd backend || exit +pipenv run pipenv:sync-prod +pipenv run migrations:apply +pipenv run static:collect +touch reload # Trigger restart of uwsgi server. +cd .. diff --git a/docs/work-methodology.md b/docs/work-methodology.md index 69af5653d..1894e3f7c 100644 --- a/docs/work-methodology.md +++ b/docs/work-methodology.md @@ -5,17 +5,17 @@ 1. Always create issue first, preferably in English. 2. Assign developer on issue. 3. Create branch from issue (choose source). - - Desired branch name is on the format `-`. - - Example: `1-init-project`. + - Desired branch name is on the format `-`. + - Example: `1-init-project`. 4. Assign developer on branch. 5. Write code, commit, push. 6. Create PR (Pull Request) to intended source branch. 7. Request review. 8. Never resolve threads startet by others. Write e.g.: - - `Done` - - Reasons for disagreement. - - Reasons for not fixing it. - - Describe how you solved the issue in an alternative way. + - `Done` + - Reasons for disagreement. + - Reasons for not fixing it. + - Describe how you solved the issue in an alternative way. 9. Owner of thread hits `Resolve` when they are satisfied. 10. Update and/or fix conflicts branch with source. -11. Owner of the PR merges it, may use squash. +11. Owner of the PR merges it. diff --git a/frontend/package.json b/frontend/package.json index a8a18f7a4..7064d9907 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,6 +8,7 @@ "dev": "yarn run start", "build": "vite build", "ci": "yarn install --immutable", + "ci:debian": "yarnpkg install --immutable", "preview": "vite preview", "upgrade": "yarn upgrade", "outdated": "yarn upgrade-interactive", diff --git a/frontend/src/AppRoutes.tsx b/frontend/src/AppRoutes.tsx index 9570877ba..a958babad 100644 --- a/frontend/src/AppRoutes.tsx +++ b/frontend/src/AppRoutes.tsx @@ -15,6 +15,7 @@ import { LycheAboutPage, LycheContactPage, LycheHomePage, + LycheMenuPage, NotFoundPage, RecruitmentAdmissionFormPage, RecruitmentPage, @@ -232,6 +233,7 @@ export function AppRoutes() { }> } /> } /> + } /> } /> diff --git a/frontend/src/Components/Button/Button.tsx b/frontend/src/Components/Button/Button.tsx index db8797074..10785f90e 100644 --- a/frontend/src/Components/Button/Button.tsx +++ b/frontend/src/Components/Button/Button.tsx @@ -1,20 +1,9 @@ -import { default as classNames, default as classnames } from 'classnames'; +import { default as classnames } from 'classnames'; import { Link } from 'react-router-dom'; import { ButtonType, Children } from '~/types'; import styles from './Button.module.scss'; - -type ButtonTheme = - | 'basic' - | 'samf' - | 'secondary' - | 'success' - | 'outlined' - | 'blue' - | 'black' - | 'white' - | 'green' - | 'pure'; -type ButtonDisplay = 'basic' | 'pill' | 'block'; +import { ButtonDisplay, ButtonTheme } from './types'; +import { displayToStyleMap, themeToStyleMap } from './utils'; type ButtonProps = { name?: string; @@ -31,25 +20,6 @@ type ButtonProps = { onClick?: () => void; }; -const mapThemeToStyle: { [theme in ButtonTheme]: string } = { - basic: styles.button_basic, - pure: styles.pure, - samf: styles.button_samf, - secondary: styles.button_secondary, - success: styles.button_success, - outlined: classNames(styles.button_outlined, 'button_outlined'), // Must be globally available for theme-dark. - blue: styles.button_blue, - black: styles.button_black, - white: styles.button_white, - green: styles.button_green, -}; - -const mapDisplayToStyle: { [display in ButtonDisplay]: string } = { - basic: styles.display_basic, - pill: styles.display_pill, - block: styles.display_block, -}; - export function Button({ name, theme = 'basic', @@ -67,8 +37,8 @@ export function Button({ const classNames = classnames( !isPure && styles.button, - mapThemeToStyle[theme], - !isPure && mapDisplayToStyle[display], + themeToStyleMap[theme], + !isPure && displayToStyleMap[display], rounded && styles.rounded, className, ); diff --git a/frontend/src/Components/Button/index.ts b/frontend/src/Components/Button/index.ts index fe9c53c51..b8ca3aaac 100644 --- a/frontend/src/Components/Button/index.ts +++ b/frontend/src/Components/Button/index.ts @@ -1 +1,2 @@ export { Button } from './Button'; +export type { ButtonDisplay, ButtonTheme } from './types'; diff --git a/frontend/src/Components/Button/types.ts b/frontend/src/Components/Button/types.ts new file mode 100644 index 000000000..744a18a69 --- /dev/null +++ b/frontend/src/Components/Button/types.ts @@ -0,0 +1,5 @@ +import { displayToStyleMap, themeToStyleMap } from './utils'; + +export type ButtonTheme = keyof typeof themeToStyleMap; + +export type ButtonDisplay = keyof typeof displayToStyleMap; diff --git a/frontend/src/Components/Button/utils.ts b/frontend/src/Components/Button/utils.ts new file mode 100644 index 000000000..6733fe9ba --- /dev/null +++ b/frontend/src/Components/Button/utils.ts @@ -0,0 +1,26 @@ +import classNames from 'classnames'; +import styles from './Button.module.scss'; + +export const themeToStyleMap = { + basic: styles.button_basic, + pure: styles.pure, + samf: styles.button_samf, + secondary: styles.button_secondary, + success: styles.button_success, + outlined: classNames(styles.button_outlined, 'button_outlined'), // Must be globally available for theme-dark. + blue: styles.button_blue, + black: styles.button_black, + white: styles.button_white, + green: styles.button_green, +} as const; + +/** + * basic: Normal button wrapping text. + * pill: Rounded button. + * block: Utilises full width. + */ +export const displayToStyleMap = { + basic: styles.display_basic, + pill: styles.display_pill, + block: styles.display_block, +} as const; diff --git a/frontend/src/Components/Checkbox/Checkbox.module.scss b/frontend/src/Components/Checkbox/Checkbox.module.scss index 8cb52e187..55842e95e 100644 --- a/frontend/src/Components/Checkbox/Checkbox.module.scss +++ b/frontend/src/Components/Checkbox/Checkbox.module.scss @@ -4,6 +4,7 @@ $checkmark: '\2714'; /* ASCII "check"-symbol. */ /* Label wraper i checkbox.tsx. */ .checkbox { + position: relative; display: inline-flex; align-items: center; cursor: pointer; @@ -11,13 +12,17 @@ $checkmark: '\2714'; /* ASCII "check"-symbol. */ /* Selve input checkbox typen. */ .checkbox__input { - display: none; + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; } /* Div som styles for å representere en checkbox. */ .checkbox__box { - width: 1em; - height: 1em; + width: 1.2em; + height: 1.2em; border: 2px solid $grey-3; border-radius: 3px; display: flex; diff --git a/frontend/src/Components/Checkbox/Checkbox.tsx b/frontend/src/Components/Checkbox/Checkbox.tsx index 938098601..a719b7292 100644 --- a/frontend/src/Components/Checkbox/Checkbox.tsx +++ b/frontend/src/Components/Checkbox/Checkbox.tsx @@ -47,7 +47,7 @@ export function Checkbox({ name={name} onClick={handleChange} disabled={disabled} - checked={checked} + checked={isChecked} />
{alignment == 'right' && label} diff --git a/frontend/src/Components/ExpandableHeader/ExpandableHeader.module.scss b/frontend/src/Components/ExpandableHeader/ExpandableHeader.module.scss index e77458e9a..5be35d311 100644 --- a/frontend/src/Components/ExpandableHeader/ExpandableHeader.module.scss +++ b/frontend/src/Components/ExpandableHeader/ExpandableHeader.module.scss @@ -1,18 +1,38 @@ @import 'src/constants'; +@import 'src/mixins'; + + .container { width: 100%; - border: 1px solid $grey-3; + border: 2px solid $grey-3; overflow: hidden; + + @include theme-dark{ + border: 1px solid $grey-0; + } } .parent { - border-radius: 0.7em; + border-radius: 10px; + border-color: $grey-4; + background-color: $grey_4; + + @include theme-dark{ + background-color: #1d0809; + border-color: transparent; + } } .child { - border-radius: 0; - border: 0; + border: 1px solid transparent; + border-radius: 10px; + margin: 10px; + width: auto; + + @include theme-dark{ + border-color: transparent; + } } .extendable_header_wrapper { @@ -20,25 +40,39 @@ flex-direction: row; justify-content: space-between; align-items: center; - font-size: 1rem; - background: $red-samf; - padding: 0.5em 1.5em 0.5em 0.5em; - color: $white; + font-size: 20px; + font-weight: 400; + background: $grey_4; + padding: 0.5em 1.5em 0.5em 1em; + color: $black; cursor: pointer; border: 0; outline: none; text-align: left; + + @include theme-dark{ + background-color: #1d0809; + color: white; + } +} + +.extendable_header_wrapper:hover{ + background-color: #dbdbdb; + + @include theme-dark{ + background-color: #1d0809; + } } .extendable_header_title { display: block; - flex-basis: 75%; + flex-basis: 95%; padding: 0; margin: 0; } .expandable_header_arrow { - flex-basis: 25%; + flex-basis: 5%; height: 35px; width: 35px; pointer-events: none; @@ -46,7 +80,8 @@ display: flex; justify-content: center; align-items: center; - color: $white; + font-size: medium; + font-weight: 400; } .expandable_header_arrow.open { diff --git a/frontend/src/Components/ExpandableHeader/ExpandableHeader.tsx b/frontend/src/Components/ExpandableHeader/ExpandableHeader.tsx index 0be53a787..c065df2ac 100644 --- a/frontend/src/Components/ExpandableHeader/ExpandableHeader.tsx +++ b/frontend/src/Components/ExpandableHeader/ExpandableHeader.tsx @@ -30,10 +30,10 @@ export function ExpandableHeader({ return (
setShowChildren(!showChildren)}> +

{label}

-

{label}

{showChildren && children}
diff --git a/frontend/src/Components/Footer/Footer.module.scss b/frontend/src/Components/Footer/Footer.module.scss index 2573cc434..e8ef3077b 100644 --- a/frontend/src/Components/Footer/Footer.module.scss +++ b/frontend/src/Components/Footer/Footer.module.scss @@ -12,6 +12,12 @@ background-color: $red-samf; gap: 0.5rem; padding: 1em; + + @include theme-dark { + /* stylelint-disable-next-line function-no-unknown */ + background-color: darken($red-samf, 35%); + + } } .main_row { diff --git a/frontend/src/Components/ImageCard/ImageCard.module.scss b/frontend/src/Components/ImageCard/ImageCard.module.scss index 189a81c48..fa33bd757 100644 --- a/frontend/src/Components/ImageCard/ImageCard.module.scss +++ b/frontend/src/Components/ImageCard/ImageCard.module.scss @@ -3,10 +3,13 @@ @import 'src/constants'; $mobile-width: $primary-content-width-mobile; -$card-border-radius: 1em; +$card-border-radius: 0.5rem; $subtitle-max-height: 1rem; $card-gradient-overlay: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.05) 30%, rgba(0, 0, 0, 0.54) 80%, rgba(0, 0, 0, 0.6) 100%); $card-text-shadow: 1px 1px 8px rgba(0, 0, 0, 0.5); + +$card-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2); +$card-box-shadow-hover: 0 3px 8px 0 rgba(0, 0, 0, 0.4); // TODO color variables .container { @@ -28,7 +31,7 @@ $card-text-shadow: 1px 1px 8px rgba(0, 0, 0, 0.5); .card { border-radius: $card-border-radius; transition: 0.2s; - box-shadow: 0 0 5px 3px $black-t10; + box-shadow: $card-box-shadow; background-size: cover; background-position: center; min-height: 11em; @@ -131,7 +134,7 @@ $card-text-shadow: 1px 1px 8px rgba(0, 0, 0, 0.5); } .card { transform: translateY(-3px); - box-shadow: 0 3px 8px 2px $black-t25; + box-shadow: $card-box-shadow-hover; text-decoration: none; } @include for-tablet-up { diff --git a/frontend/src/Components/InputField/InputField.tsx b/frontend/src/Components/InputField/InputField.tsx index ec7fdf86d..6836693fa 100644 --- a/frontend/src/Components/InputField/InputField.tsx +++ b/frontend/src/Components/InputField/InputField.tsx @@ -12,7 +12,7 @@ type InputFieldProps = { inputClassName?: string; onChange?: (value: T) => void; onBlur?: (value: T) => void; - placeholder?: string | null; + placeholder?: string; type?: InputFieldType; disabled?: boolean; value?: string; @@ -27,7 +27,7 @@ export function InputField({ inputClassName, onChange, onBlur, - placeholder, + placeholder = '', disabled, value, error, @@ -53,7 +53,7 @@ export function InputField({ onChange={(e) => onChange?.(preprocessValue(e))} onBlur={(e) => onBlur?.(preprocessValue(e))} className={classNames(styles.input_field, inputClassName, error && styles.error)} - placeholder={placeholder || ''} + placeholder={placeholder} disabled={disabled} type={type} value={value} diff --git a/frontend/src/Components/LycheFrame/LycheFrame.module.scss b/frontend/src/Components/LycheFrame/LycheFrame.module.scss new file mode 100644 index 000000000..5af99a773 --- /dev/null +++ b/frontend/src/Components/LycheFrame/LycheFrame.module.scss @@ -0,0 +1,105 @@ +@import 'src/constants'; + +.lyche_menu { + max-width: 1200px; + margin: 0 auto; + color: white; + display: flex; + flex-direction: column; + align-items: center; + +} + +.lyche_header_border_top { + height: 32px; + width: 100%; + display: flex; + align-items: flex-end; + align-content: center; +} + +.lyche_menu_border { + border-left: 1px solid white; + border-right: 1px solid white; + width: calc(100% - 64px); +} + +.lyche_menu_inner_border { + margin-right: 14px; + margin-left: 14px; + border-left: 1px solid white; + border-right: 1px solid white; + padding-top: 3em; + padding-bottom: 3em; + display: block; +} + +.lyche_header_border_bottom { + height: 32px; + width: 100%; + display: flex; + align-items: flex-end; + align-content:center; +} + +.corner_right_top { + width: 32px; + height: 32px; + border: 1px solid white; + display: flex; + align-items: center; + transform: translateX(-16px); +} + +.corner_left_top { + width: 32px; + height: 32px; + border: 1px solid white; + align-items: center; + transform: translateX(16px); +} + +.corner_right_bottom { + width: 32px; + height: 32px; + border: 1px solid white; + display: flex; + align-items: center; + transform: translate(-17px,0); +} + +.corner_left_bottom { + width: 32px; + height: 32px; + border: 1px solid white; + display: flex; + align-items: center; + transform: translate(17px,0); +} + +.middle_top { + flex: 1; + height: 16px; + border: 1px solid white; + display: flex; + justify-content: space-between; +} + +.middle_bottom { + flex: 1; + height: 16px; + border: 1px solid white; + display: flex; + justify-content: space-between; + transform: translateY(-16px); +} + +.dot { + width: 4px; + height: 4px; + margin: 5px; + background-color: white; +} + + + diff --git a/frontend/src/Components/LycheFrame/LycheFrame.tsx b/frontend/src/Components/LycheFrame/LycheFrame.tsx new file mode 100644 index 000000000..c6c634876 --- /dev/null +++ b/frontend/src/Components/LycheFrame/LycheFrame.tsx @@ -0,0 +1,34 @@ +import { Children } from '~/types'; +import styles from './LycheFrame.module.scss'; + +type LycheFrameProps = { + children?: Children; +}; + +export function LycheFrame({ children }: LycheFrameProps) { + return ( +
+
+
+
+
+
+
+
+
+ +
+
{children}
+
+ +
+
+
+
+
+
+
+
+
+ ); +} diff --git a/frontend/src/Components/LycheFrame/index.ts b/frontend/src/Components/LycheFrame/index.ts new file mode 100644 index 000000000..c396cc68b --- /dev/null +++ b/frontend/src/Components/LycheFrame/index.ts @@ -0,0 +1 @@ +export { LycheFrame } from './LycheFrame'; diff --git a/frontend/src/Components/LycheMenuDivider/LycheMenuDivider.module.scss b/frontend/src/Components/LycheMenuDivider/LycheMenuDivider.module.scss new file mode 100644 index 000000000..0c77655ad --- /dev/null +++ b/frontend/src/Components/LycheMenuDivider/LycheMenuDivider.module.scss @@ -0,0 +1,39 @@ +@import 'src/constants'; + +.lyche_menu_divider { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + margin-bottom: 3em; + margin-top: 2em; + margin-left: 10px; + margin-right: 10px; +} + +.lyche_menu_divider_img { + height: 17px; + width: 128px; + background-size: contain; + background-repeat: no-repeat; +} + +.lyche_menu_divider_left { + @extend .lyche_menu_divider_img; + background-image: $lyche-menu-divider-left; +} + +.lyche_menu_divider_title { + font-family: $lyche-title-font; + font-size: 1.25em; + color: white; + text-transform: uppercase; + margin-left: 0.5em; + margin-right: 0.5em; + white-space: nowrap; +} + +.lyche_menu_divider_right { + @extend .lyche_menu_divider_img; + background-image: $lyche-menu-divider-right; +} diff --git a/frontend/src/Components/LycheMenuDivider/LycheMenuDivider.tsx b/frontend/src/Components/LycheMenuDivider/LycheMenuDivider.tsx new file mode 100644 index 000000000..50fef86f8 --- /dev/null +++ b/frontend/src/Components/LycheMenuDivider/LycheMenuDivider.tsx @@ -0,0 +1,15 @@ +import styles from './LycheMenuDivider.module.scss'; + +type LycheMenuDividerProps = { + title: string; +}; + +export function LycheMenuDivider({ title }: LycheMenuDividerProps) { + return ( +
+
+
{title}
+
+
+ ); +} diff --git a/frontend/src/Components/MenuItem/MenuItem.module.scss b/frontend/src/Components/MenuItem/MenuItem.module.scss new file mode 100644 index 000000000..03a069961 --- /dev/null +++ b/frontend/src/Components/MenuItem/MenuItem.module.scss @@ -0,0 +1,46 @@ +@import 'src/constants'; + +.menu_item { + padding: 5px; + padding-left: 20px; + padding-right: 20px; + max-width: 600px; + margin: 0 auto; + color: white; + +} + +.item_name { + color: white; + display: flex; + flex-wrap: wrap; + flex-direction: row; + justify-content: space-between; + font-size: 1.25em; + margin-bottom: 5px; + text-transform: uppercase; + font-family: 'LemonMilkLight', sans-serif; +} + +.item_price { + font-size: .75em; + margin-left: 20px; + margin-top: .25em; + font-family: 'Lora', sans-serif; + color: white; +} + +.item_description { + color: dimgray; + font-family: 'Lora', sans-serif; +} + +.item_allergens { + color: dimgray; + font-family: 'Lora', sans-serif; +} + +.item_recommendations { + color: dimgray; + font-family: 'Lora', sans-serif; +} diff --git a/frontend/src/Components/MenuItem/MenuItem.tsx b/frontend/src/Components/MenuItem/MenuItem.tsx new file mode 100644 index 000000000..b47eefb18 --- /dev/null +++ b/frontend/src/Components/MenuItem/MenuItem.tsx @@ -0,0 +1,25 @@ +import styles from './MenuItem.module.scss'; + +type MenuItemProps = { + dishTitle: string; + dishDescription: string; + allergens?: string; + recommendations?: string; + price: string; +}; + +export function MenuItem({ dishTitle, dishDescription, allergens, recommendations, price }: MenuItemProps) { + return ( +
+
+ {dishTitle} +
{price}
+
+
{dishDescription}
+

+ {allergens &&
{allergens}
} +

+ {recommendations &&
{recommendations}
} +
+ ); +} diff --git a/frontend/src/Components/MenuItem/index.ts b/frontend/src/Components/MenuItem/index.ts new file mode 100644 index 000000000..42973643d --- /dev/null +++ b/frontend/src/Components/MenuItem/index.ts @@ -0,0 +1 @@ +export { MenuItem } from './MenuItem'; diff --git a/frontend/src/Components/MultiSelect/MultiSelect.module.scss b/frontend/src/Components/MultiSelect/MultiSelect.module.scss new file mode 100644 index 000000000..d2eaf2d62 --- /dev/null +++ b/frontend/src/Components/MultiSelect/MultiSelect.module.scss @@ -0,0 +1,30 @@ +@import 'src/constants'; + +@import 'src/mixins'; + +.row { + display: flex; + gap: 10px; + flex: 1; + + @include for-mobile-down { + flex-direction: column; + } +} + +.col { + flex-grow: 1; + min-height: 300px; + padding: 7px; + background-color: #dddddd; + @include rounded; + + @include theme-dark { + background-color: $grey-3; + + } +} + +.item { + margin-bottom: 5px; +} diff --git a/frontend/src/Components/MultiSelect/MultiSelect.stories.tsx b/frontend/src/Components/MultiSelect/MultiSelect.stories.tsx new file mode 100644 index 000000000..6cff326d5 --- /dev/null +++ b/frontend/src/Components/MultiSelect/MultiSelect.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { MultiSelect } from './MultiSelect'; + +const meta: Meta = { + title: 'Components/MultiSelect', + component: MultiSelect, +}; + +export default meta; +type Story = StoryObj>; + +export const Basic: Story = { + args: { + options: [ + { + label: '1', + value: 1, + }, + { + label: '2', + value: 2, + }, + { + label: '3', + value: 3, + }, + ], + }, +}; diff --git a/frontend/src/Components/MultiSelect/MultiSelect.tsx b/frontend/src/Components/MultiSelect/MultiSelect.tsx new file mode 100644 index 000000000..5bea36724 --- /dev/null +++ b/frontend/src/Components/MultiSelect/MultiSelect.tsx @@ -0,0 +1,109 @@ +import { useMemo, useState } from 'react'; +import { InputField } from '~/Components/InputField'; +import { Button } from '../Button'; +import { DropDownOption } from '../Dropdown/Dropdown'; +import styles from './MultiSelect.module.scss'; +import { exists, searchFilter } from './utils'; + +type MultiSelectProps = { + label?: string; + optionsLabel?: string; + selectedLabel?: string; + selectAllBtnTxt?: string; + unselectAllBtnTxt?: string; + selected?: DropDownOption[]; + options?: DropDownOption[]; + onChange?: (values: T[]) => void; + className?: string; +}; + +/** + * `options`: All possible options that can be selected. + * `selected`: Selected values if state is managed outside this component. + */ +export function MultiSelect({ + label, + optionsLabel, + selectedLabel, + selectAllBtnTxt = '+', + unselectAllBtnTxt = '-', + className, + selected: initialValues = [], + options = [], + onChange, +}: MultiSelectProps) { + const [searchUnselected, setSearchUnselected] = useState(''); + const [searchSelected, setSearchSelected] = useState(''); + const [selected, setSelected] = useState[]>(initialValues); + + const filteredOptions = useMemo( + () => options.filter((item) => searchFilter(item, searchUnselected)).filter((item) => !exists(item, selected)), + [options, searchUnselected, selected], + ); + + const filteredSelected = useMemo( + () => selected.filter((item) => searchFilter(item, searchSelected)), + [searchSelected, selected], + ); + + function selectItem(item: DropDownOption) { + const updatedSelected = [...selected, item]; + setSelected(updatedSelected); + onChange?.(updatedSelected.map((_item) => _item.value)); + } + + function unselectItem(item: DropDownOption) { + const updatedSelected = selected.filter((_item) => _item !== item); + setSelected(updatedSelected); + onChange?.(updatedSelected.map((_item) => _item.value)); + } + + return ( + + ); +} diff --git a/frontend/src/Components/MultiSelect/index.ts b/frontend/src/Components/MultiSelect/index.ts new file mode 100644 index 000000000..7140f36c6 --- /dev/null +++ b/frontend/src/Components/MultiSelect/index.ts @@ -0,0 +1 @@ +export { MultiSelect } from './MultiSelect'; diff --git a/frontend/src/Components/MultiSelect/utils.ts b/frontend/src/Components/MultiSelect/utils.ts new file mode 100644 index 000000000..d27906d19 --- /dev/null +++ b/frontend/src/Components/MultiSelect/utils.ts @@ -0,0 +1,9 @@ +import { DropDownOption } from '../Dropdown/Dropdown'; + +export function searchFilter(item: DropDownOption, q: string): boolean { + return item.label.toLowerCase().includes(q.toLowerCase()); +} + +export function exists(item: DropDownOption, items: DropDownOption[]): boolean { + return items.some((_item) => _item.value === item.value); +} diff --git a/frontend/src/Components/SamfundetLogoSpinner/SamfundetLogoSpinner.module.scss b/frontend/src/Components/SamfundetLogoSpinner/SamfundetLogoSpinner.module.scss index 22022f375..f44f7955f 100644 --- a/frontend/src/Components/SamfundetLogoSpinner/SamfundetLogoSpinner.module.scss +++ b/frontend/src/Components/SamfundetLogoSpinner/SamfundetLogoSpinner.module.scss @@ -57,8 +57,7 @@ $time: 1.75s; /* The element to apply the animation to */ .spinning_logo { - /* stylelint-disable-next-line function-no-unknown */ - color: lighten($grey-3, 25%); + color: $grey-3; max-width: 5em; max-height: 5em; opacity: 0; @@ -66,7 +65,7 @@ $time: 1.75s; @include theme-dark { /* stylelint-disable-next-line function-no-unknown */ - color: darken($grey-1, 25%); + color: $black-t90; } // These aren't actually global in the context of the module parent class. diff --git a/frontend/src/Components/ToggleSwitch/ToggleSwitch.module.scss b/frontend/src/Components/ToggleSwitch/ToggleSwitch.module.scss index 16898d7d2..c791364ad 100644 --- a/frontend/src/Components/ToggleSwitch/ToggleSwitch.module.scss +++ b/frontend/src/Components/ToggleSwitch/ToggleSwitch.module.scss @@ -8,13 +8,20 @@ box-sizing: border-box; } -$switch-width: 60px; -$switch-height: 30px; +$switch-width: 2.5rem; +$switch-height: 1.5rem; + $switch-color: $grey-2; $switch-color-checked: $red-samf; +$focus-box-shadow: 0 0 0 2px rgba($switch-color-checked, 0.5); $toggle-transition: 0.2s ease; -$ball-size: 25px; $ball-color: $white; +$ball-size: calc($switch-height - (0.125rem * 2)); +$track-padding: calc(($switch-height - $ball-size) / 2); + +.label { + margin-bottom: 0; // TODO: Can be removed once label's margin-bottom is removed from global.scss +} .toggle_switch { opacity: 0; @@ -22,45 +29,41 @@ $ball-color: $white; cursor: pointer; } -.label { +.track { cursor: pointer; width: $switch-width; height: $switch-height; - display: flex; background-color: $switch-color; - border-radius: 50px; - align-items: center; - justify-content: space-between; - padding: 5px; + border-radius: $switch-height; position: relative; transition: background-color $toggle-transition; } -// Styling on label when toggle_switch is checked. -.label:has(.toggle_switch:checked) { - background-color: $switch-color-checked; -} - .ball { width: $ball-size; height: $ball-size; background-color: $ball-color; position: absolute; - top: calc(($switch-height - $ball-size) / 2); // Align center vertically. - left: 2px; + top: $track-padding; + left: $track-padding; border-radius: 50%; transition: transform $toggle-transition; } -// Styling on ball when toggle_switch is checked. -.toggle_switch:checked ~ .ball { - transform: translateX(30px); // Move all the way to the end. +// Styling on track when toggle_switch is checked. +.toggle_switch:checked + .track { + background-color: $switch-color-checked; + + /* stylelint-disable-next-line */ + .ball { + transform: translateX($switch-width - $ball-size - ($track-padding * 2)); + } } -.off_icon { - margin-left: -2px; +.toggle_switch:focus + .track { + box-shadow: $focus-box-shadow; } -.on_icon { - margin-right: -2px; +.toggle_switch:disabled + .track { + cursor: not-allowed; } diff --git a/frontend/src/Components/ToggleSwitch/ToggleSwitch.stories.tsx b/frontend/src/Components/ToggleSwitch/ToggleSwitch.stories.tsx index dc6c2006f..ff883bf54 100644 --- a/frontend/src/Components/ToggleSwitch/ToggleSwitch.stories.tsx +++ b/frontend/src/Components/ToggleSwitch/ToggleSwitch.stories.tsx @@ -16,6 +16,3 @@ Basic.args = {}; export const Disabled = Template.bind({}); Disabled.args = { disabled: true }; - -export const WithIcons = Template.bind({}); -WithIcons.args = { offIcon: '0', onIcon: '1' }; diff --git a/frontend/src/Components/ToggleSwitch/ToggleSwitch.tsx b/frontend/src/Components/ToggleSwitch/ToggleSwitch.tsx index 9ccb11ef6..42e3931d3 100644 --- a/frontend/src/Components/ToggleSwitch/ToggleSwitch.tsx +++ b/frontend/src/Components/ToggleSwitch/ToggleSwitch.tsx @@ -1,16 +1,13 @@ -import { ReactNode } from 'react'; import styles from './ToggleSwitch.module.scss'; type ToggleSwitchProps = { className?: string; checked?: boolean; - offIcon?: ReactNode; - onIcon?: ReactNode; disabled?: boolean; onChange?: () => void; }; -export function ToggleSwitch({ className, checked, onChange, disabled, offIcon, onIcon }: ToggleSwitchProps) { +export function ToggleSwitch({ className, checked, onChange, disabled }: ToggleSwitchProps) { return (
); diff --git a/frontend/src/Forms/SamfFormField.tsx b/frontend/src/Forms/SamfFormField.tsx index 41aaf3ce7..c70a3c504 100644 --- a/frontend/src/Forms/SamfFormField.tsx +++ b/frontend/src/Forms/SamfFormField.tsx @@ -96,6 +96,7 @@ type SamfFormFieldProps = { // Dropdown options?: DropDownOption[]; defaultOption?: DropDownOption; + onChange?: (value: U) => void; }; export function SamfFormField({ @@ -107,6 +108,7 @@ export function SamfFormField({ options, defaultOption, validator, + onChange, }: SamfFormFieldProps) { // Validate on init context const { validateOnInit, validateOn } = useContext(SamfFormConfigContext); @@ -134,6 +136,7 @@ export function SamfFormField({ if (validateOn === 'change' && initialUpdate !== true) { setShowError(true); } + onChange && onChange(newValue as U); } // Enable show error for validate on submit diff --git a/frontend/src/Forms/SamfFormFieldTypes.tsx b/frontend/src/Forms/SamfFormFieldTypes.tsx index 29a5b2a52..12ceaac5b 100644 --- a/frontend/src/Forms/SamfFormFieldTypes.tsx +++ b/frontend/src/Forms/SamfFormFieldTypes.tsx @@ -162,6 +162,6 @@ function makeCheckboxInput(args: SamfFormFieldArgs) { className={styles.input_element} onChange={args.onChange} error={args.error} - > + /> ); } diff --git a/frontend/src/Pages/AdminPage/AdminPage.tsx b/frontend/src/Pages/AdminPage/AdminPage.tsx index 3acca02d1..09c690445 100644 --- a/frontend/src/Pages/AdminPage/AdminPage.tsx +++ b/frontend/src/Pages/AdminPage/AdminPage.tsx @@ -7,8 +7,11 @@ import styles from './AdminPage.module.scss'; import { WISEWORDS } from './data'; import { ROUTES } from '~/routes'; import { Link } from 'react-router-dom'; +import { KEY } from '~/i18n/constants'; +import { useTranslation } from 'react-i18next'; export function AdminPage() { + const { t } = useTranslation(); const { user } = useAuthContext(); const randomWisewordIndex = Math.floor(Math.random() * WISEWORDS.length); @@ -38,7 +41,7 @@ export function AdminPage() {

- + diff --git a/frontend/src/Pages/ApiTestingPage/ApiTestingPage.tsx b/frontend/src/Pages/ApiTestingPage/ApiTestingPage.tsx index e458d46c7..49484e778 100644 --- a/frontend/src/Pages/ApiTestingPage/ApiTestingPage.tsx +++ b/frontend/src/Pages/ApiTestingPage/ApiTestingPage.tsx @@ -31,7 +31,7 @@ export function ApiTestingPage() { diff --git a/frontend/src/Pages/ComponentPage/ComponentPage.tsx b/frontend/src/Pages/ComponentPage/ComponentPage.tsx index 4274bed4b..cb7b8f2c4 100644 --- a/frontend/src/Pages/ComponentPage/ComponentPage.tsx +++ b/frontend/src/Pages/ComponentPage/ComponentPage.tsx @@ -2,6 +2,7 @@ import { Button, Countdown, InputField, ProgressBar, RadioButton } from '~/Compo import { Checkbox } from '~/Components/Checkbox'; import { Link } from '~/Components/Link'; import { List } from '~/Components/List'; +import { MultiSelect } from '~/Components/MultiSelect'; import { SnowflakesOverlay } from '~/Components/SnowflakesOverlay/SnowflakesOverlay'; import { norwegianFlag } from '~/assets'; import { HOUR_MILLIS } from '~/constants'; @@ -14,6 +15,50 @@ import styles from './ComponentPage.module.scss'; export function ComponentPage() { return (
+ +
+
+
+

Components:

Buttons:

diff --git a/frontend/src/Pages/HomePage/components/Splash/Splash.tsx b/frontend/src/Pages/HomePage/components/Splash/Splash.tsx index 083a6a7f8..c6432d8b4 100644 --- a/frontend/src/Pages/HomePage/components/Splash/Splash.tsx +++ b/frontend/src/Pages/HomePage/components/Splash/Splash.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { BACKEND_DOMAIN } from '~/constants'; import { EventDto } from '~/dto'; import styles from '../../HomePage.module.scss'; -import { dbT } from '~/utils'; +import { dbT, lowerCapitalize } from '~/utils'; import { Button } from '~/Components'; import { useTranslation } from 'react-i18next'; import { KEY } from '~/i18n/constants'; @@ -37,7 +37,7 @@ export function Splash({ events, showInfo }: SplashProps) { const ticketButton = isPaid ? ( ) : ( <> diff --git a/frontend/src/Pages/InformationPage/InformationPage.tsx b/frontend/src/Pages/InformationPage/InformationPage.tsx index 691ae023c..084eb9a59 100644 --- a/frontend/src/Pages/InformationPage/InformationPage.tsx +++ b/frontend/src/Pages/InformationPage/InformationPage.tsx @@ -14,7 +14,7 @@ import { toast } from 'react-toastify'; import { useAuthContext } from '~/AuthContext'; import { SamfMarkdown } from '~/Components/SamfMarkdown'; import { PERM } from '~/permissions'; -import { dbT, hasPerm } from '~/utils'; +import { dbT, hasPerm, lowerCapitalize } from '~/utils'; import styles from './InformationPage.module.scss'; /** @@ -67,7 +67,7 @@ export function InformationPage() { <>

diff --git a/frontend/src/Pages/LycheMenuPage/LycheMenuPage.tsx b/frontend/src/Pages/LycheMenuPage/LycheMenuPage.tsx new file mode 100644 index 000000000..bcb823ca4 --- /dev/null +++ b/frontend/src/Pages/LycheMenuPage/LycheMenuPage.tsx @@ -0,0 +1,48 @@ +import { MenuItem } from '~/Components/MenuItem'; +import { SultenPage } from '~/Components/SultenPage'; + +export function LycheMenuPage() { + return ( + + + +

+

+ + + +

+

+ + + +

+ + +
+ ); +} diff --git a/frontend/src/Pages/LycheMenuPage/index.ts b/frontend/src/Pages/LycheMenuPage/index.ts new file mode 100644 index 000000000..736c5b456 --- /dev/null +++ b/frontend/src/Pages/LycheMenuPage/index.ts @@ -0,0 +1 @@ +export { LycheMenuPage } from './LycheMenuPage'; diff --git a/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.module.scss b/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.module.scss index abde53a3a..f17e4ee73 100644 --- a/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.module.scss +++ b/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.module.scss @@ -1,3 +1,49 @@ +@import 'src/constants'; + +@import 'src/mixins'; + .container { display: flex; + flex-direction: column; + padding: 2em; + @mixin for-tablet-only { + width: 100%; + } +} + +.row { + display: flex; + flex-direction: row; +} + +.textcontainer { + flex: 5; +} + +.otherposition { + flex: 1; + padding: 2em; +} + +.header { + font-weight: 700; + font-size: 2em; +} + +.subheader { + font-weight: 700; + font-size: 1.5em; + margin-bottom: 0.5em; +} + +.text { + margin-bottom: 2em; +} + +.formContainer { + background-color: $red-samf; +} + +.formLabel { + margin-bottom: 0.5em; } diff --git a/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.tsx b/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.tsx index 96d37cf58..8d6a99371 100644 --- a/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.tsx +++ b/frontend/src/Pages/RecruitmentAdmissionFormPage/RecruitmentAdmissionFormPage.tsx @@ -2,10 +2,11 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { toast } from 'react-toastify'; -import { Page, SamfundetLogoSpinner } from '~/Components'; +import { reverse } from '~/named-urls'; +import { Page, SamfundetLogoSpinner, Link, Button } from '~/Components'; import { SamfForm } from '~/Forms/SamfForm'; import { SamfFormField } from '~/Forms/SamfFormField'; -import { getRecruitmentPosition, postRecruitmentAdmission } from '~/api'; +import { getRecruitmentPosition, postRecruitmentAdmission, getRecruitmentPositionsGang } from '~/api'; import { RecruitmentAdmissionDto, RecruitmentPositionDto } from '~/dto'; import { useCustomNavigate } from '~/hooks'; import { KEY } from '~/i18n/constants'; @@ -18,15 +19,25 @@ export function RecruitmentAdmissionFormPage() { const { t } = useTranslation(); const [recruitmentPosition, setRecruitmentPosition] = useState(); + const [recruitmentPositionsForGang, setRecruitmentPositionsForGang] = useState(); + const [loading, setLoading] = useState(true); const { positionID, id } = useParams(); useEffect(() => { - getRecruitmentPosition('1').then((res) => { + getRecruitmentPosition(positionID as string).then((res) => { setRecruitmentPosition(res.data); setLoading(false); }); - }, []); + }, [positionID]); + + useEffect(() => { + getRecruitmentPositionsGang(recruitmentPosition?.recruitment as string, recruitmentPosition?.gang.id).then( + (res) => { + setRecruitmentPositionsForGang(res.data); + }, + ); + }, [recruitmentPosition]); function handleOnSubmit(data: RecruitmentAdmissionDto) { data.recruitment_position = positionID ? +positionID : 1; @@ -64,14 +75,60 @@ export function RecruitmentAdmissionFormPage() { return (
-

{dbT(recruitmentPosition, 'name')}

-

{dbT(recruitmentPosition, 'long_description')}

+
+
+

{dbT(recruitmentPosition, 'name')}

+

+ {t(KEY.recruitment_volunteerfor)}{' '} + + {recruitmentPosition?.is_funksjonaer_position + ? t(KEY.recruitment_funksjonaer) + : t(KEY.recruitment_gangmember)} + {' '} + + {dbT(recruitmentPosition?.gang, 'name')} + +

+

{dbT(recruitmentPosition, 'long_description')}

+

{t(KEY.recruitment_applyfor)}

+

{t(KEY.recruitment_applyforhelp)}

+
+ +
+

+ {t(KEY.recruitment_otherpositions)} {dbT(recruitmentPosition?.gang, 'name')} +

+ {recruitmentPositionsForGang?.map((pos, index) => { + if (pos.id !== recruitmentPosition?.id) { + return ( + + ); + } + })} +
+
- {' '} +

{t(KEY.recruitment_admission)}

+ {' '}
diff --git a/frontend/src/Pages/RecruitmentPage/Components/GangPosition/GangPosition.module.scss b/frontend/src/Pages/RecruitmentPage/Components/GangPosition/GangPosition.module.scss index 07aa1fc6e..b31bef1d3 100644 --- a/frontend/src/Pages/RecruitmentPage/Components/GangPosition/GangPosition.module.scss +++ b/frontend/src/Pages/RecruitmentPage/Components/GangPosition/GangPosition.module.scss @@ -7,7 +7,7 @@ flex-direction: column; align-items: center; justify-content: center; - gap: 2em; + gap: 1em; } .video { @@ -20,8 +20,20 @@ } .gang_header { - border-bottom: 1px solid $grey-3; - background-color: $blue-deep; + background-color: $red_samf; + color: white; + font-size: 17px; + font-weight: 400; + @include theme-dark{ + background-color: #590000; + } +} + +.gang_header:hover{ + background-color: $red_samf_hover; + @include theme-dark{ + background-color: #400000; + } } .position_item { @@ -29,18 +41,47 @@ flex-direction: row; align-items: center; justify-content: space-between; - gap: 1em; width: 100%; background-color: $white; color: $black; - padding: 1em; - border-bottom: 1px solid $grey-3; + border-top: 1px solid $grey-5; + @include theme-dark{ + background-color: transparent; + border-top: 1px solid #300e10; + } +} + +.position_item:hover{ + background-color: $grey-5; + @include theme-dark{ + background-color: #300e10; + } } .position_name { flex: 1; + color: black; + padding: 1em; + font-weight: 400; + @include theme-dark{ + color: white; + } } -.position_short_desc { +.position_name:hover { + text-decoration: none; +} + +.position_short_desc{ flex: 1; + color: $black; + padding: 1em; + @include theme-dark{ + color: $grey-4; + } } + +.position_short_desc:hover{ + text-decoration: none; +} + diff --git a/frontend/src/Pages/RecruitmentPage/Components/GangPosition/GangPosition.tsx b/frontend/src/Pages/RecruitmentPage/Components/GangPosition/GangPosition.tsx index 12ab63ace..9ee89f3de 100644 --- a/frontend/src/Pages/RecruitmentPage/Components/GangPosition/GangPosition.tsx +++ b/frontend/src/Pages/RecruitmentPage/Components/GangPosition/GangPosition.tsx @@ -13,7 +13,7 @@ type GangItemProps = { export function GangPosition({ type, recruitmentPositions }: GangItemProps) { const filteredGangs = type.gangs .map((gang) => { - const filteredPositions = recruitmentPositions?.filter((pos) => pos.gang == `${gang.id}`); + const filteredPositions = recruitmentPositions?.filter((pos) => pos.gang.id === gang.id); if (filteredPositions && filteredPositions.length > 0) { return ( {dbT(pos, 'name')} - {dbT(pos, 'short_description')} + + {dbT(pos, 'short_description')} +
))} diff --git a/frontend/src/Pages/index.ts b/frontend/src/Pages/index.ts index 9811b99e8..e2ec012a4 100644 --- a/frontend/src/Pages/index.ts +++ b/frontend/src/Pages/index.ts @@ -13,6 +13,7 @@ export { LoginPage } from './LoginPage'; export { LycheContactPage } from './LycheContactPage'; export { LycheAboutPage } from './LycheAboutPage'; export { LycheHomePage } from './LycheHomePage'; +export { LycheMenuPage } from './LycheMenuPage'; export { NotFoundPage } from './NotFoundPage'; export { RecruitmentAdmissionFormPage } from './RecruitmentAdmissionFormPage'; export { RecruitmentPage } from './RecruitmentPage'; diff --git a/frontend/src/PagesAdmin/AdminLayout/AdminLayout.module.scss b/frontend/src/PagesAdmin/AdminLayout/AdminLayout.module.scss index 5e84f0549..77fe0b70f 100644 --- a/frontend/src/PagesAdmin/AdminLayout/AdminLayout.module.scss +++ b/frontend/src/PagesAdmin/AdminLayout/AdminLayout.module.scss @@ -2,7 +2,7 @@ @import 'src/mixins'; -$panel-width: 14em; +$panel-width: 15em; $panel-light-bg: #f6f6f6; $panel-light-bg-hover: #e8e8e8; $panel-light-border: #d7d7d7; @@ -13,6 +13,11 @@ $panel-dark-bg: #333333; flex-direction: row; max-height: 100vh; height: 100vh; + margin-top: $navbar-height; + + @include for-mobile-down { + flex-direction: column; + } } .panel { @@ -24,12 +29,16 @@ $panel-dark-bg: #333333; border-right: 1px solid $panel-light-border; display: flex; flex-direction: column; - margin-top: $navbar-height; gap: .2em; - overflow-y: scroll; + overflow-y: auto; + z-index: 10; + opacity: 1; + transition: transform 0.2s ease-in-out; @include for-mobile-only { - display: none; + box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.1); + width: 80%; + max-width: initial; } @include theme-dark { @@ -38,6 +47,55 @@ $panel-dark-bg: #333333; } } +.mobile_panel_closed { + transform: translateX(-100%); +} + +.mobile_header { + padding: 1rem; +} + +.mobile_panel_close_btn { + position: absolute; + right: 0.5rem; + top: 0.5rem; + padding: 0.5rem; + margin: 0; + background: none; + border: none; + border-radius: 0; + cursor: pointer; + color: $grey-1; + + &:hover { + color: $black; + } + + @include theme-dark { + color: $grey-3; + + &:hover { + color: $grey-4; + } + } +} + +.mobile_backdrop { + width: 100%; + height: 100%; + position: fixed; + z-index: 5; + opacity: 0; + transition: opacity 0.2s ease-in-out; + pointer-events: none; + backdrop-filter: blur(2px); +} + +.mobile_backdrop_open { + opacity: 1; + pointer-events: initial; +} + .panel_header { @include flex-row; font-weight: 700; @@ -50,7 +108,6 @@ $panel-dark-bg: #333333; @include theme-dark { border-color: $grey-1; } - } .category_header { @@ -77,7 +134,7 @@ $panel-dark-bg: #333333; @include theme-dark { color: $grey-4; } - + &:hover { text-decoration: none; background-color: $panel-light-bg-hover; @@ -89,13 +146,14 @@ $panel-dark-bg: #333333; } .selected { - background-color: $blue; + background-color: $red-samf; color: white; @include theme-dark { - background-color: $blue; + background-color: $red-samf; } + &:hover { - background-color: $blue; + background-color: $red-samf; color: white; } } @@ -103,11 +161,10 @@ $panel-dark-bg: #333333; .content_wrapper { flex: 1; height: calc(100vh - $navbar-height); - margin-top: $navbar-height; margin-left: calc($panel-width); max-width: calc(100vw - $panel-width); width: calc(100vw - $panel-width); - + @include for-mobile-only { width: auto; max-width: 100%; diff --git a/frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx b/frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx index f46042e34..02138322f 100644 --- a/frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx +++ b/frontend/src/PagesAdmin/AdminLayout/AdminLayout.tsx @@ -1,15 +1,16 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Icon } from '@iconify/react'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; import { Outlet } from 'react-router-dom'; -import { Link, Navbar } from '~/Components'; +import { Button, Link, Navbar } from '~/Components'; import { Applet } from '~/Components/AdminBox/types'; import { appletCategories } from '~/Pages/AdminPage/applets'; import { KEY } from '~/i18n/constants'; import { ROUTES_FRONTEND } from '~/routes/frontend'; import { dbT } from '~/utils'; import styles from './AdminLayout.module.scss'; +import { useMobile } from '~/hooks'; /** * Wraps admin routes with the standard navbar and a side panel with common links @@ -18,6 +19,8 @@ import styles from './AdminLayout.module.scss'; */ export function AdminLayout() { const { t } = useTranslation(); + const [mobilePanelOpen, setMobilePanelOpen] = useState(false); + const isMobile = useMobile(); function makeAppletShortcut(applet: Applet, index: number) { // No default url, dont show in navmenu @@ -30,6 +33,7 @@ export function AdminLayout() { key={index} className={classNames(styles.panel_item, selected && styles.selected)} url={applet.url} + onAfterClick={() => mobilePanelOpen && setMobilePanelOpen(false)} plain={true} > @@ -40,35 +44,70 @@ export function AdminLayout() { const selectedIndex = window.location.href.endsWith(ROUTES_FRONTEND.admin); + useEffect(() => { + if (!isMobile) { + setMobilePanelOpen(false); + } + }, [isMobile]); + + const panel = ( +
+ {isMobile && ( + + )} + + {/* Header */} +
{t(KEY.control_panel_title)}
+ {/* Index */} + mobilePanelOpen && setMobilePanelOpen(false)} + > + + {t(KEY.common_profile)} + +

+ {/* Applets */} + {appletCategories.map((category) => { + return ( + +
{dbT(category, 'title')}
+ {category.applets.map((applet, index) => makeAppletShortcut(applet, index))} +
+ ); + })} +

+ {/* TODO help/faq */} + + + {t(KEY.control_panel_faq)} + +
+ ); + + const mobileHeader = ( + <> +
+ +
+
setMobilePanelOpen(false)} + >
+ + ); + return (
-
- {/* Header */} -
{t(KEY.control_panel_title)}
- {/* Index */} - - - Profil {/* TODO translate */} - -

- {/* Applets */} - {appletCategories.map((category) => { - return ( - -
{dbT(category, 'title')}
- {category.applets.map((applet, index) => makeAppletShortcut(applet, index))} -
- ); - })} -

- {/* TODO help/faq */} - - - {t(KEY.control_panel_faq)} - -
+ {isMobile && mobileHeader} + {panel} {/* Content */}
diff --git a/frontend/src/PagesAdmin/AdminPageLayout/AdminPageLayout.module.scss b/frontend/src/PagesAdmin/AdminPageLayout/AdminPageLayout.module.scss index 3c28457ca..93cb6803b 100644 --- a/frontend/src/PagesAdmin/AdminPageLayout/AdminPageLayout.module.scss +++ b/frontend/src/PagesAdmin/AdminPageLayout/AdminPageLayout.module.scss @@ -6,14 +6,6 @@ $page-padding: 2em; $header-bg-dark: #222222; -.page { - display: flex; - flex-direction: column; - max-height: 100%; - max-width: 100%; - margin-bottom: 0; -} - .header { padding-top: 1em; padding-left: $page-padding; diff --git a/frontend/src/PagesAdmin/AdminPageLayout/AdminPageLayout.tsx b/frontend/src/PagesAdmin/AdminPageLayout/AdminPageLayout.tsx index 6fdeca577..c300f47d4 100644 --- a/frontend/src/PagesAdmin/AdminPageLayout/AdminPageLayout.tsx +++ b/frontend/src/PagesAdmin/AdminPageLayout/AdminPageLayout.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from 'react'; +import { ReactNode, useEffect } from 'react'; import { IconButton, SamfundetLogoSpinner } from '~/Components'; import { COLORS } from '~/types'; import styles from './AdminPageLayout.module.scss'; @@ -15,8 +15,13 @@ type AdminPageLayoutProps = { * Simple wrapper for admin pages to keep them consistent. */ export function AdminPageLayout({ title, backendUrl, header, loading, children }: AdminPageLayoutProps) { + useEffect(() => { + // Scroll to top on page change + window.scrollTo(0, 0); + }, []); + return ( -
+ <>
{title}
@@ -41,6 +46,6 @@ export function AdminPageLayout({ title, backendUrl, header, loading, children } )} {!loading && children}
-
+ ); } diff --git a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx index fc0bb4301..1c263c32f 100644 --- a/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx +++ b/frontend/src/PagesAdmin/EventCreatorAdminPage/EventCreatorAdminPage.tsx @@ -1,5 +1,4 @@ import { Button, ImageCard } from '~/Components'; - import { Icon } from '@iconify/react'; import classNames from 'classnames'; import { t } from 'i18next'; @@ -287,7 +286,7 @@ export function EventCreatorAdminPage() { ); - const title = `${t(KEY.common_create)} ${t(KEY.common_event)}`; + const title = lowerCapitalize(`${t(KEY.common_create)} ${t(KEY.common_event)}`); return ( { // TODO custom modal confirm - if (window.confirm(`${t(KEY.form_confirm)} ${t(KEY.common_delete)} ${dbT(event, 'title')}`)) { + const msg = lowerCapitalize(`${t(KEY.form_confirm)} ${t(KEY.common_delete)}`); + if (window.confirm(`${msg} ${dbT(event, 'title')}`)) { // TODO toast component? A bit too easy to delete events deleteSelectedEvent(event.id); } @@ -105,12 +106,12 @@ export function EventsAdminPage() { ]; }); - const title = `${t(KEY.common_edit)} ${t(KEY.common_event).toLowerCase()}`; + const title = lowerCapitalize(`${t(KEY.common_edit)} ${t(KEY.common_event)}`); const backendUrl = ROUTES.backend.admin__samfundet_event_changelist; const header = ( <> ); diff --git a/frontend/src/PagesAdmin/GangsFormAdminPage/GangsFormAdminPage.tsx b/frontend/src/PagesAdmin/GangsFormAdminPage/GangsFormAdminPage.tsx index 1d72816fe..a40d9ad6a 100644 --- a/frontend/src/PagesAdmin/GangsFormAdminPage/GangsFormAdminPage.tsx +++ b/frontend/src/PagesAdmin/GangsFormAdminPage/GangsFormAdminPage.tsx @@ -12,6 +12,7 @@ import { KEY } from '~/i18n/constants'; import { ROUTES } from '~/routes'; import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout'; import styles from './GangsFormAdminPage.module.scss'; +import { lowerCapitalize } from '~/utils'; export function GangsFormAdminPage() { const navigate = useCustomNavigate(); @@ -53,8 +54,8 @@ export function GangsFormAdminPage() { console.log(JSON.stringify(data)); } - const submitText = id ? t(KEY.common_save) : `${t(KEY.common_create)} ${t(KEY.common_gang)}`; - const title = id ? t(KEY.common_edit) : `${t(KEY.common_create)} ${t(KEY.common_gang)}`; + const submitText = id ? t(KEY.common_save) : lowerCapitalize(`${t(KEY.common_create)} ${t(KEY.common_gang)}`); + const title = id ? t(KEY.common_edit) : lowerCapitalize(`${t(KEY.common_create)} ${t(KEY.common_gang)}`); return ( @@ -66,12 +67,28 @@ export function GangsFormAdminPage() { devMode={false} >
- - + +
- - + +
{/* TODO fetch options */} {/* */} diff --git a/frontend/src/PagesAdmin/ImageAdminPage/ImageAdminPage.tsx b/frontend/src/PagesAdmin/ImageAdminPage/ImageAdminPage.tsx index 6dd0ad3bc..d32ff2173 100644 --- a/frontend/src/PagesAdmin/ImageAdminPage/ImageAdminPage.tsx +++ b/frontend/src/PagesAdmin/ImageAdminPage/ImageAdminPage.tsx @@ -8,6 +8,7 @@ import { ROUTES } from '~/routes'; import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout'; import styles from './ImageAdminPage.module.scss'; import { AdminImage } from './components'; +import { lowerCapitalize } from '~/utils'; export function ImageAdminPage() { const [images, setImages] = useState([]); @@ -31,7 +32,7 @@ export function ImageAdminPage() { const backendUrl = ROUTES.backend.admin__samfundet_image_changelist; const header = ( ); diff --git a/frontend/src/PagesAdmin/ImageFormAdminPage/ImageFormAdminPage.tsx b/frontend/src/PagesAdmin/ImageFormAdminPage/ImageFormAdminPage.tsx index 6a31cd572..004e31e68 100644 --- a/frontend/src/PagesAdmin/ImageFormAdminPage/ImageFormAdminPage.tsx +++ b/frontend/src/PagesAdmin/ImageFormAdminPage/ImageFormAdminPage.tsx @@ -11,6 +11,7 @@ import { STATUS } from '~/http_status_codes'; import { KEY } from '~/i18n/constants'; import { ROUTES } from '~/routes'; import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout'; +import { lowerCapitalize } from '~/utils'; export function ImageFormAdminPage() { const navigate = useCustomNavigate(); @@ -64,8 +65,8 @@ export function ImageFormAdminPage() { } } - const submitText = id ? t(KEY.common_save) : `${t(KEY.common_create)} ${t(KEY.common_image)}`; - const title = id ? `${t(KEY.common_edit)} ${t(KEY.common_image)}` : t(KEY.admin_images_create); + const submitText = id ? t(KEY.common_save) : lowerCapitalize(`${t(KEY.common_create)} ${t(KEY.common_image)}`); + const title = id ? lowerCapitalize(`${t(KEY.common_edit)} ${t(KEY.common_image)}`) : t(KEY.admin_images_create); return ( @@ -74,7 +75,11 @@ export function ImageFormAdminPage() { {/* TODO helpText "Merkelapper må være separert med ', ', f.ex 'lapp1, lapp2, lapp3'" */} {/* TODO create file picker input type */} - +

{JSON.stringify(image.file)} {image.file?.name} diff --git a/frontend/src/PagesAdmin/InformationAdminPage/InformationAdminPage.tsx b/frontend/src/PagesAdmin/InformationAdminPage/InformationAdminPage.tsx index ca640e1f5..de2055305 100644 --- a/frontend/src/PagesAdmin/InformationAdminPage/InformationAdminPage.tsx +++ b/frontend/src/PagesAdmin/InformationAdminPage/InformationAdminPage.tsx @@ -10,7 +10,7 @@ import { useCustomNavigate } from '~/hooks'; import { KEY } from '~/i18n/constants'; import { reverse } from '~/named-urls'; import { ROUTES } from '~/routes'; -import { dbT } from '~/utils'; +import { dbT, lowerCapitalize } from '~/utils'; import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout'; export function InformationAdminPage() { @@ -99,7 +99,7 @@ export function InformationAdminPage() { const backendUrl = ROUTES.backend.admin__samfundet_informationpage_changelist; const header = ( ); diff --git a/frontend/src/PagesAdmin/InformationFormAdminPage/InformationFormAdminPage.tsx b/frontend/src/PagesAdmin/InformationFormAdminPage/InformationFormAdminPage.tsx index 80b349989..0d06c0e95 100644 --- a/frontend/src/PagesAdmin/InformationFormAdminPage/InformationFormAdminPage.tsx +++ b/frontend/src/PagesAdmin/InformationFormAdminPage/InformationFormAdminPage.tsx @@ -13,6 +13,7 @@ import { STATUS } from '~/http_status_codes'; import { KEY } from '~/i18n/constants'; import { ROUTES } from '~/routes'; import styles from './InformationFormAdminPage.module.scss'; +import { lowerCapitalize } from '~/utils'; export function InformationFormAdminPage() { const { t } = useTranslation(); @@ -129,7 +130,7 @@ export function InformationFormAdminPage() { {/* Header tools */}

- {slugField ? t(KEY.common_edit) : t(KEY.common_create)} {t(KEY.information_page_short)} + {slugField ? t(KEY.common_edit) : lowerCapitalize(`${t(KEY.common_create)} ${t(KEY.information_page_short)}`)}
); diff --git a/frontend/src/PagesAdmin/RecruitmentGangAdminPage/RecruitmentGangAdminPage.tsx b/frontend/src/PagesAdmin/RecruitmentGangAdminPage/RecruitmentGangAdminPage.tsx index 133133ec5..c2f4644c6 100644 --- a/frontend/src/PagesAdmin/RecruitmentGangAdminPage/RecruitmentGangAdminPage.tsx +++ b/frontend/src/PagesAdmin/RecruitmentGangAdminPage/RecruitmentGangAdminPage.tsx @@ -8,7 +8,7 @@ import { RecruitmentPositionDto } from '~/dto'; import { KEY } from '~/i18n/constants'; import { reverse } from '~/named-urls'; import { ROUTES } from '~/routes'; -import { dbT } from '~/utils'; +import { dbT, lowerCapitalize } from '~/utils'; import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout'; export function RecruitmentGangAdminPage() { @@ -23,7 +23,7 @@ export function RecruitmentGangAdminPage() { recruitmentId && getRecruitmentPositions(recruitmentId).then((data) => { // TODO: Make this filtering happen on the backend - setRecruitmentPositions(data.data.filter((recruitment) => recruitment.gang == gangId)); + setRecruitmentPositions(data.data.filter((recruitment) => recruitment.gang.id.toString() == gangId)); setShowSpinner(false); }); }, [recruitmentId, gangId]); @@ -77,7 +77,7 @@ export function RecruitmentGangAdminPage() { }, })} > - {t(KEY.common_create)} {t(KEY.recruitment_position)} + {lowerCapitalize(`${t(KEY.common_create)} ${t(KEY.recruitment_position)}`)} ); diff --git a/frontend/src/PagesAdmin/RecruitmentPositionFormAdminPage/RecruitmentPositionFormAdminPage.tsx b/frontend/src/PagesAdmin/RecruitmentPositionFormAdminPage/RecruitmentPositionFormAdminPage.tsx index 33731a014..f4dd8b578 100644 --- a/frontend/src/PagesAdmin/RecruitmentPositionFormAdminPage/RecruitmentPositionFormAdminPage.tsx +++ b/frontend/src/PagesAdmin/RecruitmentPositionFormAdminPage/RecruitmentPositionFormAdminPage.tsx @@ -24,6 +24,7 @@ export function RecruitmentPositionFormAdminPage() { name_nb: 'Ny stilling', name_en: 'New position', }); + const [norwegianApplicantsOnly, setNorwegianApplicantsOnly] = useState(false); // Fetch data if edit mode. useEffect(() => { @@ -57,6 +58,8 @@ export function RecruitmentPositionFormAdminPage() { long_description_nb: position?.long_description_nb, long_description_en: position?.long_description_en, + norwegian_applicants_only: position?.norwegian_applicants_only || false, + default_admission_letter_nb: position?.default_admission_letter_nb, default_admission_letter_en: position?.default_admission_letter_en, is_funksjonaer_position: position?.is_funksjonaer_position || false, @@ -81,7 +84,7 @@ export function RecruitmentPositionFormAdminPage() { function handleOnSubmit(data: RecruitmentPositionDto) { const updatedPosition = data; - updatedPosition.gang = gangId ?? ''; + updatedPosition.gang.id = parseInt(gangId ?? ''); updatedPosition.recruitment = recruitmentId ?? ''; updatedPosition.interviewers = []; if (positionId) { @@ -127,6 +130,16 @@ export function RecruitmentPositionFormAdminPage() {
+
+ { + setNorwegianApplicantsOnly(value); + }} + /> +
@@ -164,6 +181,8 @@ export function RecruitmentPositionFormAdminPage() { field="default_admission_letter_en" type="text-long" label={t(KEY.recrutment_default_admission_letter) + ' ' + t(KEY.common_english)} + hidden={norwegianApplicantsOnly} + required={!norwegianApplicantsOnly} />
diff --git a/frontend/src/PagesAdmin/SaksdokumentAdminPage/SaksdokumentAdminPage.tsx b/frontend/src/PagesAdmin/SaksdokumentAdminPage/SaksdokumentAdminPage.tsx index 26a7a82b7..07b09cef8 100644 --- a/frontend/src/PagesAdmin/SaksdokumentAdminPage/SaksdokumentAdminPage.tsx +++ b/frontend/src/PagesAdmin/SaksdokumentAdminPage/SaksdokumentAdminPage.tsx @@ -11,7 +11,7 @@ import { SaksdokumentDto } from '~/dto'; import { KEY } from '~/i18n/constants'; import { reverse } from '~/named-urls'; import { ROUTES } from '~/routes'; -import { dbT } from '~/utils'; +import { dbT, lowerCapitalize } from '~/utils'; import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout'; import styles from './SaksdokumentAdminPage.module.scss'; @@ -96,7 +96,7 @@ export function SaksdokumentAdminPage() { const backendUrl = ROUTES.backend.admin__samfundet_saksdokument_changelist; const header = ( ); diff --git a/frontend/src/PagesAdmin/SaksdokumentFormAdminPage/SaksdokumentFormAdminPage.tsx b/frontend/src/PagesAdmin/SaksdokumentFormAdminPage/SaksdokumentFormAdminPage.tsx index 0be34d68e..afac80b11 100644 --- a/frontend/src/PagesAdmin/SaksdokumentFormAdminPage/SaksdokumentFormAdminPage.tsx +++ b/frontend/src/PagesAdmin/SaksdokumentFormAdminPage/SaksdokumentFormAdminPage.tsx @@ -11,7 +11,7 @@ import { SaksdokumentDto } from '~/dto'; import { STATUS } from '~/http_status_codes'; import { KEY } from '~/i18n/constants'; import { ROUTES } from '~/routes'; -import { utcTimestampToLocal } from '~/utils'; +import { lowerCapitalize, utcTimestampToLocal } from '~/utils'; import { AdminPageLayout } from '../AdminPageLayout/AdminPageLayout'; import styles from './SaksdokumentFormAdminPage.module.scss'; @@ -95,8 +95,8 @@ export function SaksdokumentFormAdminPage() { } } - const submitText = id ? t(KEY.common_save) : `${t(KEY.common_create)} ${t(KEY.admin_saksdokument)}`; - const title = id ? t(KEY.common_edit) : `${t(KEY.common_create)} ${t(KEY.admin_saksdokument)}`; + const submitText = id ? t(KEY.common_save) : lowerCapitalize(`${t(KEY.common_create)} ${t(KEY.admin_saksdokument)}`); + const title = id ? t(KEY.common_edit) : lowerCapitalize(`${t(KEY.common_create)} ${t(KEY.admin_saksdokument)}`); return ( {/* Document form */} diff --git a/frontend/src/_constants.scss b/frontend/src/_constants.scss index 2f7e89dfb..ec1d1f4bf 100644 --- a/frontend/src/_constants.scss +++ b/frontend/src/_constants.scss @@ -1,6 +1,7 @@ // Colors: // When adding new colors, types.ts must be updated to reflect all colors $red-samf: #a03033; +$red_samf_hover: #732225; $red-samf-faded: #fac7c8; $red-lighter: #fff2f0; $red-light: #ffcfca; @@ -77,3 +78,12 @@ $primary-content-width-mobile: calc(100vw - 2em); $theme-key: 'data-theme'; // Must match THEME_KEY $theme-dark: 'theme-dark'; // Must match THEME.DARK $theme-light: 'theme-light'; // Must match THEM.LIGHT + +//Lyche +$lyche-title-font:'LemonMilkLight', sans-serif; + +//Assets +$lyche-menu-divider-left: url('~/assets/lyche/menu-detail-left.png'); +$lyche-menu-divider-right: url('~/assets/lyche/menu-detail-right.png'); + + diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 1200aab1c..e2fb79646 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -541,6 +541,21 @@ export async function getRecruitmentPositions(recruitmentId: string): Promise> { + const url = + BACKEND_DOMAIN + + reverse({ + pattern: ROUTES.backend.samfundet__recruitment_positions_gang, + queryParams: { recruitment: recruitmentId, gang: gangId }, + }); + const response = await axios.get(url, { withCredentials: true }); + + return response; +} + export async function getRecruitmentPosition(positionId: string): Promise> { const url = BACKEND_DOMAIN + diff --git a/frontend/src/assets/lyche/menu-detail-left.png b/frontend/src/assets/lyche/menu-detail-left.png new file mode 100644 index 000000000..a78db8c5a Binary files /dev/null and b/frontend/src/assets/lyche/menu-detail-left.png differ diff --git a/frontend/src/assets/lyche/menu-detail-right.png b/frontend/src/assets/lyche/menu-detail-right.png new file mode 100644 index 000000000..8fd98b4e5 Binary files /dev/null and b/frontend/src/assets/lyche/menu-detail-right.png differ diff --git a/frontend/src/dto.ts b/frontend/src/dto.ts index 68bd9e9f1..ed57f10ff 100644 --- a/frontend/src/dto.ts +++ b/frontend/src/dto.ts @@ -330,10 +330,12 @@ export type RecruitmentPositionDto = { is_funksjonaer_position: boolean; + norwegian_applicants_only: boolean; + default_admission_letter_nb: string; default_admission_letter_en: string; - gang: string; + gang: GangDto; recruitment: string; tags: string; diff --git a/frontend/src/global.scss b/frontend/src/global.scss index a372a4d4b..6763b18f5 100644 --- a/frontend/src/global.scss +++ b/frontend/src/global.scss @@ -100,8 +100,11 @@ label { // THEME.DARK [#{$theme-key}='#{$theme-dark}'] { - background-color: $theme-dark-bg; color: $theme-dark-color; + /* stylelint-disable-next-line function-no-unknown */ + background: linear-gradient(180deg, darken($red-samf, 21%) 0%, darken($red-samf, 35%) 100%); + background-repeat: no-repeat; + background-attachment: fixed; form { background-color: $form-bg-dark; diff --git a/frontend/src/i18n/constants.ts b/frontend/src/i18n/constants.ts index 7f33c16cf..09e4c8a46 100644 --- a/frontend/src/i18n/constants.ts +++ b/frontend/src/i18n/constants.ts @@ -61,6 +61,7 @@ export const KEY = { common_tags: 'common_tags', common_gang: 'common_gang', common_next: 'common_next', + common_open: 'common_open', common_send: 'common_send', common_event: 'common_event', common_login: 'common_login', @@ -78,6 +79,7 @@ export const KEY = { common_choose: 'common_choose', common_missing: 'common_missing', common_delete: 'common_delete', + common_profile: 'common_profile', common_message: 'common_message', common_english: 'common_english', common_whatsup: 'common_whatsup', @@ -174,7 +176,12 @@ export const KEY = { recruitment_duration: 'recruitment_duration', recruitment_admission: 'recruitment_admission', recruitment_funksjonaer: 'recruitment_funksjonaer', + recruitment_gangmember: 'recruitment_gangmember', recruitment_organization: 'recruitment_organization', + recruitment_applyfor: 'recruitment_applyfor', + recruitment_applyforhelp: 'recruitment_applyforhelp', + recruitment_volunteerfor: 'recruitment_volunteerfor', + recruitment_otherpositions: 'KEY.recruitment_otherpositions', recruitment_visible_from: 'recruitment_visible_from', recruitment_administrate: 'recruitment_administrate', shown_application_deadline: 'shown_application_deadline', @@ -182,6 +189,7 @@ export const KEY = { recruitment_number_of_applications: 'recruitment_number_of_applications', recrutment_default_admission_letter: 'recrutment_default_admission_letter', reprioritization_deadline_for_groups: 'reprioritization_deadline_for_groups', + recruitment_norwegian_applicants_only: 'recruitment_norwegian_applicants_only', reprioritization_deadline_for_applicant: 'reprioritization_deadline_for_applicant', recruitment_show_unprocessed_applicants: 'recruitment_show_unprocessed_applicants', recruitment_show_applicants_without_interview: 'recruitment_show_applicants_without_interview', @@ -191,6 +199,7 @@ export const KEY = { admin_saksdokument: 'admin_saksdokument', admin_images_title: 'admin_images_title', admin_images_create: 'admin_images_create', + admin_steal_identity: 'admin_steal_identity', adminpage_gangs_title: 'admin_gangs_title', adminpage_gangs_create: 'admin_gangs_create', admin_opening_hours_hint: 'admin_opening_hours_hint', diff --git a/frontend/src/i18n/translations.ts b/frontend/src/i18n/translations.ts index 8351aba80..2aff4d741 100644 --- a/frontend/src/i18n/translations.ts +++ b/frontend/src/i18n/translations.ts @@ -41,6 +41,7 @@ export const nb: Record = { [KEY.common_show]: 'Vis', [KEY.common_date]: 'Dato', [KEY.common_send]: 'Send', + [KEY.common_open]: 'Åpne', [KEY.common_menu]: 'Meny', [KEY.common_name]: 'Navn', [KEY.common_next]: 'Neste', @@ -68,6 +69,7 @@ export const nb: Record = { [KEY.common_missing]: 'Mangler', [KEY.common_contact]: 'Kontakt', [KEY.common_english]: 'Engelsk', + [KEY.common_profile]: 'Profil', [KEY.common_message]: 'Melding', [KEY.common_whatsup]: 'Hva skjer?', [KEY.common_sponsor]: 'Sponsorer', @@ -160,6 +162,12 @@ export const nb: Record = { [KEY.recruitment_duration]: 'Varighet', [KEY.recruitment_admission]: 'Søknad', [KEY.recruitment_funksjonaer]: 'Funksjonær', + [KEY.recruitment_gangmember]: 'Gjengmedlem', + [KEY.recruitment_applyfor]: 'Søk på dette vervet', + [KEY.recruitment_applyforhelp]: + 'Klar til å søke? \n Skriv litt om deg selv, din motivasjon for å søke og dine kvalifikasjoner for vervet. Du trenger ikke legge inn full CV og referanser - en kort tekst er nok. Vi gleder oss til å høre fra deg! Etter at du er registrert som søker, kan du logge inn for å se status på dine søknader. Hvis du har søkt flere verv, må du også prioritere hvilket du ønsker mest. Du vil kun få tilbud om ett verv.', + [KEY.recruitment_volunteerfor]: 'Verv som', + [KEY.recruitment_otherpositions]: 'Andre verv i', [KEY.recruitment_visible_from]: 'Synlig fra', [KEY.recruitment_organization]: 'Organisasjon', [KEY.recruitment_administrate]: 'Administrer opptak', @@ -168,6 +176,7 @@ export const nb: Record = { [KEY.recruitment_number_of_applications]: 'Antall søknader', [KEY.recrutment_default_admission_letter]: 'Standard søknadstekst', [KEY.reprioritization_deadline_for_groups]: 'Flaggefrist', + [KEY.recruitment_norwegian_applicants_only]: 'Kun norsktalende søkere', [KEY.reprioritization_deadline_for_applicant]: 'Omprioriteringsfrist', [KEY.recruitment_show_unprocessed_applicants]: 'Vis ubehandlede søkere', [KEY.recruitment_show_applicants_without_interview]: 'Vis søkere uten et intervju', @@ -177,6 +186,7 @@ export const nb: Record = { [KEY.admin_saksdokument]: 'Saksdokument', [KEY.admin_images_title]: 'Bildearkiv', [KEY.admin_images_create]: 'Nytt Bilde', + [KEY.admin_steal_identity]: 'Stjel identitet', [KEY.admin_opening_hours_hint]: 'Endringer lagres automatisk!', [KEY.admin_closed_period_title]: 'Planlagte perioder Samfundet skal holde stengt', [KEY.admin_saksdokumenter_title]: 'Administrer Saksdokumenter', @@ -274,6 +284,7 @@ export const en: Record = { [KEY.common_from]: 'From', [KEY.common_date]: 'Date', [KEY.common_send]: 'Send', + [KEY.common_open]: 'Open', [KEY.common_edit]: 'Edit', [KEY.common_show]: 'Show', [KEY.common_table]: 'Table', @@ -303,6 +314,7 @@ export const en: Record = { [KEY.common_sponsor]: 'Sponsors', [KEY.common_whatsup]: "what's up?", [KEY.common_english]: 'English', + [KEY.common_profile]: 'Profile', [KEY.common_contact]: 'Contact', [KEY.common_register]: 'Register', [KEY.common_lastname]: 'Last name', @@ -393,6 +405,12 @@ export const en: Record = { [KEY.recruitment_duration]: 'Duration', [KEY.recruitment_admission]: 'Admission', [KEY.recruitment_funksjonaer]: 'Functionary', + [KEY.recruitment_gangmember]: 'Gangmember', + [KEY.recruitment_applyfor]: 'Apply for this position', + [KEY.recruitment_applyforhelp]: + ' Ready to apply? \nWrite a little bit about yourself, your motivation for applying and your qualifications for this job. You don’t have to send in a full CV or references from previous employers - a short text is enough. We look forward to hearing from you! When you have registered as an applicant, you can log in and check the status of your applications. If you apply for more than one job, you must prioritize which one you want the most. Note that you will only be offered one job.', + [KEY.recruitment_volunteerfor]: 'Position as', + [KEY.recruitment_otherpositions]: 'Other positions in', [KEY.recruitment_organization]: 'Organization', [KEY.recruitment_visible_from]: 'Visible from', [KEY.recruitment_administrate]: 'Administrate recruitment', @@ -403,6 +421,7 @@ export const en: Record = { [KEY.reprioritization_deadline_for_groups]: 'Group reprioritization deadline', [KEY.reprioritization_deadline_for_applicant]: 'Reprioritization deadline', [KEY.recruitment_show_unprocessed_applicants]: 'Show unprocessed applicants', + [KEY.recruitment_norwegian_applicants_only]: 'Norwegian speaking applicants only', [KEY.recruitment_show_applicants_without_interview]: 'Show applicants without an interview', // Admin: @@ -410,6 +429,7 @@ export const en: Record = { [KEY.admin_saksdokument]: 'case document', [KEY.admin_images_title]: 'Image Archive', [KEY.admin_images_create]: 'New Image', + [KEY.admin_steal_identity]: 'Steal identity', [KEY.adminpage_gangs_title]: 'Groups administration', [KEY.adminpage_gangs_create]: 'Create group', [KEY.admin_opening_hours_hint]: 'Changes are saved automatically!', diff --git a/frontend/src/routes/backend.ts b/frontend/src/routes/backend.ts index 17b5d56f9..b4b2ceba1 100644 --- a/frontend/src/routes/backend.ts +++ b/frontend/src/routes/backend.ts @@ -4,7 +4,7 @@ THIS FILE IS AUTOGENERATED. DO NOT WRITE IN THIS FILE, AS IT WILL BE OVERWRITTEN ON NEXT UPDATE. THIS FILE WAS GENERATED BY: root.management.commands.generate_routes -LAST UPDATE: 2023-09-24 15:12:38.294245+00:00 +LAST UPDATE: 2023-11-02 18:37:45.368800+00:00 """ */ // ############################################################ @@ -352,6 +352,15 @@ export const ROUTES_BACKEND = { admin__samfundet_interviewroom_delete: '/admin/samfundet/interviewroom/:objectId/delete/', admin__samfundet_interviewroom_change: '/admin/samfundet/interviewroom/:objectId/change/', adminsamfundetinterviewroom__objectId: '/admin/samfundet/interviewroom/:objectId/', + admin__samfundet_interview_permissions: '/admin/samfundet/interview/:objectPk/permissions/', + admin__samfundet_interview_permissions_manage_user: '/admin/samfundet/interview/:objectPk/permissions/user-manage/:userId/', + admin__samfundet_interview_permissions_manage_group: '/admin/samfundet/interview/:objectPk/permissions/group-manage/:groupId/', + admin__samfundet_interview_changelist: '/admin/samfundet/interview/', + admin__samfundet_interview_add: '/admin/samfundet/interview/add/', + admin__samfundet_interview_history: '/admin/samfundet/interview/:objectId/history/', + admin__samfundet_interview_delete: '/admin/samfundet/interview/:objectId/delete/', + admin__samfundet_interview_change: '/admin/samfundet/interview/:objectId/change/', + adminsamfundetinterview__objectId: '/admin/samfundet/interview/:objectId/', admin__samfundet_notification_changelist: '/admin/samfundet/notification/', admin__samfundet_notification_add: '/admin/samfundet/notification/add/', admin__samfundet_notification_history: '/admin/samfundet/notification/:objectId/history/', @@ -426,8 +435,12 @@ export const ROUTES_BACKEND = { samfundet__recruitment_position_detail: '/api/recruitment-position/:pk/', samfundet__recruitment_admissions_for_applicant_list: '/api/recruitment-admisisons-for-applicant/', samfundet__recruitment_admissions_for_applicant_detail: '/api/recruitment-admisisons-for-applicant/:pk/', + samfundet__recruitment_admissions_for_group_list: '/api/recruitment-admisisons-for-group/', + samfundet__recruitment_admissions_for_group_detail: '/api/recruitment-admisisons-for-group/:pk/', samfundet__recruitment_admissions_for_gang_list: '/api/recruitment-admisisons-for-gang/', samfundet__recruitment_admissions_for_gang_detail: '/api/recruitment-admisisons-for-gang/:pk/', + samfundet__interview_list: '/api/interview/', + samfundet__interview_detail: '/api/interview/:pk/', samfundet__api_root: '/api/', samfundet__csrf: '/csrf/', samfundet__login: '/login/', @@ -443,6 +456,7 @@ export const ROUTES_BACKEND = { samfundet__home: '/home/', samfundet__assign_group: '/assign_group/', samfundet__recruitment_positions: '/recruitment-positions/', + samfundet__recruitment_positions_gang: '/recruitment-positions-gang/', samfundet__active_recruitment_positions: '/active-recruitment-positions/', samfundet__applicants_without_interviews: '/applicants-without-interviews/', static__path: '/static/:path', diff --git a/frontend/src/types.ts b/frontend/src/types.ts index b7a4a83b1..f1c4ae1d6 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -20,6 +20,7 @@ export type Children = ReactNode; /**Duplicate of colors and hex from _constants.scss */ export const COLORS = { red_samf: '#a03033', + red_samf_hover: '#732225', red_samf_faded: '#fac7c8', blue: '#337ab7', blue_lighter: '#e3f2ff', diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index cddefaf8e..dc1c3ede0 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -242,3 +242,14 @@ export function createDot(e: MouseEvent): HTMLDivElement { dot.style.top = e.clientY + window.pageYOffset + 'px'; return dot; } + +/** + * Lowercases the string, then capitalizes the first word. + * Example: 'lorem ipsum Dolor' becomes 'Lorem ipsum dolor' + */ +export function lowerCapitalize(s: string): string { + if (s.length < 2) { + return s.toUpperCase(); + } + return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase(); +}