Compare commits

..

1 commit

Author SHA1 Message Date
Víðir Valberg Guðmundsson a9a88c9290 Keycloak integration 2023-01-22 10:20:41 +01:00
77 changed files with 558 additions and 1300 deletions

3
.gitignore vendored
View file

@ -5,6 +5,3 @@ db.sqlite3
.pytest_cache .pytest_cache
.idea/ .idea/
*.mo *.mo
.env
venv/
.venv/

View file

@ -3,7 +3,7 @@ default_language_version:
exclude: ^.*\b(migrations)\b.*$ exclude: ^.*\b(migrations)\b.*$
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0 rev: v4.4.0
hooks: hooks:
- id: check-ast - id: check-ast
- id: check-merge-conflict - id: check-merge-conflict
@ -16,13 +16,14 @@ repos:
- id: end-of-file-fixer - id: end-of-file-fixer
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
rev: 'v0.1.11' rev: 'v0.0.209'
hooks: hooks:
- id: ruff - id: ruff
args: args:
- --force-exclude
- --fix - --fix
- repo: https://github.com/asottile/reorder_python_imports - repo: https://github.com/asottile/reorder_python_imports
rev: v3.12.0 rev: v3.9.0
hooks: hooks:
- id: reorder-python-imports - id: reorder-python-imports
args: args:
@ -30,31 +31,33 @@ repos:
- --application-directories=.:src - --application-directories=.:src
exclude: migrations/ exclude: migrations/
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.15.0 rev: v3.3.1
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: args:
- --py311-plus - --py311-plus
exclude: migrations/ exclude: migrations/
- repo: https://github.com/adamchainz/django-upgrade - repo: https://github.com/adamchainz/django-upgrade
rev: 1.15.0 rev: 1.12.0
hooks: hooks:
- id: django-upgrade - id: django-upgrade
args: args:
- --target-version=4.1 - --target-version=4.1
- repo: https://github.com/asottile/yesqa - repo: https://github.com/asottile/yesqa
rev: v1.5.0 rev: v1.4.0
hooks: hooks:
- id: yesqa - id: yesqa
- repo: https://github.com/asottile/add-trailing-comma - repo: https://github.com/asottile/add-trailing-comma
rev: v3.1.0 rev: v2.4.0
hooks: hooks:
- id: add-trailing-comma - id: add-trailing-comma
args:
- --py36-plus
- repo: https://github.com/hadialqattan/pycln - repo: https://github.com/hadialqattan/pycln
rev: v2.4.0 rev: v2.1.2
hooks: hooks:
- id: pycln - id: pycln
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 23.12.1 rev: 22.12.0
hooks: hooks:
- id: black - id: black

View file

@ -1,4 +1,4 @@
DOCKER_COMPOSE = COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker compose DOCKER_COMPOSE = COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 venv/bin/docker-compose
DOCKER_RUN = ${DOCKER_COMPOSE} run -u `id -u` DOCKER_RUN = ${DOCKER_COMPOSE} run -u `id -u`
DOCKER_BUILD = DOCKER_BUILDKIT=1 docker build DOCKER_BUILD = DOCKER_BUILDKIT=1 docker build
DOCKER_CONTAINER_NAME = backend DOCKER_CONTAINER_NAME = backend
@ -14,7 +14,7 @@ setup_venv:
rm -rf venv rm -rf venv
python3.11 -m venv venv; python3.11 -m venv venv;
venv/bin/python -m pip install wheel setuptools; venv/bin/python -m pip install wheel setuptools;
venv/bin/python -m pip install pre-commit boto3 pip-tools; venv/bin/python -m pip install docker-compose pre-commit boto3 pip-tools;
pre_commit_install: pre_commit_install:
venv/bin/pre-commit install venv/bin/pre-commit install

View file

@ -1,22 +1,12 @@
# member.data.coop # member.data.coop
## Development ## Development requirements
### Setup environment
Copy over the .env.example file to .env and adjust DATABASE_URL accordingly
$ cp .env.example .env
### Docker
#### Requirements
- Docker - Docker
- Docker compose - Docker compose
- pre-commit (preferred for contributions) - pre-commit (preferred for contributions)
#### Setup ## Start local server
Given that the requirements above are installed, it should be as easy as: Given that the requirements above are installed, it should be as easy as:
@ -43,29 +33,3 @@ Make messages:
Running tests: Running tests:
$ make test $ make test
### Non-docker
Create a venv
$ python3 -m venv venv
Activate the venv
$ source venv/bin/activate
Install requirements
$ pip install -r requirements/dev.txt
Run migrations
$ ./src/manage.py migrate
Create a superuser
$ ./src/manage.py createsuperuser
Run the server
$ ./src/manage.py runserver

View file

@ -14,8 +14,9 @@ services:
- ./:/app/ - ./:/app/
depends_on: depends_on:
- postgres - postgres
- keycloak
env_file: env_file:
- .env - env
postgres: postgres:
image: postgres:13-alpine image: postgres:13-alpine
@ -24,7 +25,27 @@ services:
ports: ports:
- 5432:5432 - 5432:5432
env_file: env_file:
- .env - env
keycloak_db:
image: postgres:13-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file:
- env
keycloak:
image: "quay.io/keycloak/keycloak:20.0"
restart: "unless-stopped"
command:
- "start-dev"
- "--db=postgres"
- "--db-url=jdbc:postgresql://keycloak_db:5432/postgres"
- "--db-username=postgres"
- "--db-password=postgres"
- "--hostname=localhost"
- "--http-relative-path=/auth"
volumes: volumes:
postgres_data: postgres_data:

View file

@ -3,7 +3,5 @@ POSTGRES_HOST=postgres
POSTGRES_PASSWORD=postgres POSTGRES_PASSWORD=postgres
POSTGRES_PORT=5432 POSTGRES_PORT=5432
DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres
# Use something along the the following if you are not using docker
# DATABASE_URL=postgres://postgres:postgres@localhost:5432/datacoop_membersystem
DEBUG=True DEBUG=True
DJANGO_ENV=development DJANGO_ENV=development

View file

@ -1,105 +0,0 @@
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[project]
name = "membersystem"
description = ''
readme = "README.md"
requires-python = ">=3.11"
keywords = []
authors = [
{ name = "Víðir Valberg Guðmundsson", email = "valberg@orn.li" },
]
dependencies = [
"Django==5.0.1",
"django-money==3.4.1",
"django-allauth==0.60.0",
"psycopg[binary]==3.1.16",
"environs[django]==10.0.0",
"uvicorn==0.25.0",
"whitenoise==6.6.0",
"django-zen-queries==2.1.0",
"django-registries==0.0.3",
]
dynamic = ["version"]
[tool.hatch.version]
source = "vcs"
[tool.hatch.envs.default]
dependencies = [
"coverage[toml]==7.3.0",
"pytest==7.2.2",
"pytest-cov",
"pytest-django==4.5.2",
"mypy==1.1.1",
"django-stubs==1.16.0",
"pip-tools==7.3.0",
"django-debug-toolbar==4.2.0",
"django-browser-reload==1.7.0",
"model-bakery==1.17.0",
]
[[tool.hatch.envs.tests.matrix]]
python = ["3.12"]
django = ["4.2", "5.0"]
[tool.hatch.envs.tests.overrides]
matrix.django.dependencies = [
{ value = "django~={matrix:django}" },
]
matrix.python.dependencies = [
{ value = "typing_extensions==4.5.0", if = ["3.10"]},
]
[tool.hatch.envs.default.scripts]
cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=src --cov=tests --cov=append {args}"
no-cov = "cov --no-cov {args}"
typecheck = "mypy --config-file=pyproject.toml ."
requirements = "pip-compile --output-file requirements/base.txt pyproject.toml"
server = "./src/manage.py runserver"
migrate = "./src/manage.py migrate"
makemigrations = "./src/manage.py makemigrations"
createsuperuser = "./src/manage.py createsuperuser"
shell = "./src/manage.py shell"
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE="tests.settings"
addopts = "--reuse-db"
norecursedirs = "build dist docs .eggs/* *.egg-info htmlcov .git"
python_files = "test*.py"
testpaths = "tests"
pythonpath = ". tests"
[tool.coverage.run]
branch = true
parallel = true
[tool.coverage.report]
exclude_lines = [
"no cov",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]
[tool.mypy]
mypy_path = "src/"
exclude = [
"venv/",
"dist/",
"docs/",
]
namespace_packages = false
show_error_codes = true
strict = true
warn_unreachable = true
follow_imports = "normal"
#plugins = ["mypy_django_plugin.main"]
[tool.django-stubs]
#django_settings_module = "tests.settings"
[[tool.mypy.overrides]]
module = "tests.*"
allow_untyped_defs = true

View file

@ -1,8 +1,8 @@
Django==5.0.1 Django==4.1.5
django-money==3.4.1 django-money==1.3
django-allauth==0.60.0 django-allauth==0.46
psycopg[binary]==3.1.16 psycopg2-binary==2.9.5
environs[django]==10.0.0 environs[django]==9.3
uvicorn==0.25.0 uvicorn==0.13
whitenoise==6.6.0 whitenoise==5.2
django-zen-queries==2.1.0 django-zen-queries==2.1.0

View file

@ -1,98 +1,82 @@
# #
# This file is autogenerated by pip-compile with Python 3.11 # This file is autogenerated by pip-compile with Python 3.10
# by the following command: # by the following command:
# #
# pip-compile --output-file=requirements/base.txt pyproject.toml # pip-compile --output-file=requirements/base.txt requirements/base.in
# #
asgiref==3.7.2 asgiref==3.5.2
# via django # via django
babel==2.14.0 certifi==2022.9.24
# via py-moneyed
certifi==2023.11.17
# via requests # via requests
cffi==1.16.0 cffi==1.15.1
# via cryptography # via cryptography
charset-normalizer==3.3.2 charset-normalizer==2.1.1
# via requests # via requests
click==8.1.7 click==7.1.2
# via uvicorn # via uvicorn
cryptography==41.0.7 cryptography==38.0.3
# via pyjwt # via pyjwt
defusedxml==0.7.1 defusedxml==0.7.1
# via python3-openid # via python3-openid
dj-database-url==2.1.0 dj-database-url==1.0.0
# via environs # via environs
dj-email-url==1.0.6 dj-email-url==1.0.6
# via environs # via environs
django==5.0.1 django==4.1.5
# via # via
# -r requirements/base.in
# dj-database-url # dj-database-url
# django-allauth # django-allauth
# django-money # django-money
# django-registries
# django-zen-queries # django-zen-queries
# membersystem (pyproject.toml) django-allauth==0.46
django-allauth==0.60.0 # via -r requirements/base.in
# via membersystem (pyproject.toml) django-cache-url==3.4.2
django-cache-url==3.4.5
# via environs # via environs
django-money==3.4.1 django-money==1.3
# via membersystem (pyproject.toml) # via -r requirements/base.in
django-registries==0.0.3
# via membersystem (pyproject.toml)
django-zen-queries==2.1.0 django-zen-queries==2.1.0
# via membersystem (pyproject.toml) # via -r requirements/base.in
environs[django]==10.0.0 environs[django]==9.3
# via # via -r requirements/base.in
# environs
# membersystem (pyproject.toml)
h11==0.14.0 h11==0.14.0
# via uvicorn # via uvicorn
idna==3.6 idna==3.4
# via requests # via requests
marshmallow==3.20.1 marshmallow==3.19.0
# via environs # via environs
oauthlib==3.2.2 oauthlib==3.2.2
# via requests-oauthlib # via requests-oauthlib
packaging==23.2 packaging==21.3
# via marshmallow # via marshmallow
psycopg[binary]==3.1.16 psycopg2-binary==2.9.5
# via # via -r requirements/base.in
# membersystem (pyproject.toml) py-moneyed==0.8.0
# psycopg
psycopg-binary==3.1.16
# via psycopg
py-moneyed==3.0
# via django-money # via django-money
pycparser==2.21 pycparser==2.21
# via cffi # via cffi
pyjwt[crypto]==2.8.0 pyjwt[crypto]==2.6.0
# via # via django-allauth
# django-allauth pyparsing==3.0.9
# pyjwt # via packaging
python-dotenv==1.0.0 python-dotenv==0.21.0
# via environs # via environs
python3-openid==3.2.0 python3-openid==3.2.0
# via django-allauth # via django-allauth
requests==2.31.0 requests==2.28.1
# via # via
# django-allauth # django-allauth
# requests-oauthlib # requests-oauthlib
requests-oauthlib==1.3.1 requests-oauthlib==1.3.1
# via django-allauth # via django-allauth
sqlparse==0.4.4 sqlparse==0.4.3
# via django # via django
typing-extensions==4.9.0 urllib3==1.26.12
# via
# dj-database-url
# psycopg
# py-moneyed
urllib3==2.1.0
# via requests # via requests
uvicorn==0.25.0 uvicorn==0.13
# via membersystem (pyproject.toml) # via -r requirements/base.in
whitenoise==6.6.0 whitenoise==5.2
# via membersystem (pyproject.toml) # via -r requirements/base.in
# The following packages are considered to be unsafe in a requirements file: # The following packages are considered to be unsafe in a requirements file:
# setuptools # setuptools

View file

@ -1,8 +1,8 @@
-r test.txt -r test.txt
django-browser-reload==1.12.1 django-browser-reload==1.6.0
django-debug-toolbar==4.2.0 django-debug-toolbar==3.7.0
django-extensions==3.2.3 django-extensions==3.2.1
django-stubs==4.2.7 django-stubs==1.12.0
ipython==8.19.0 ipython==8.6.0
mypy==1.8.0 mypy==0.990

View file

@ -1,39 +1,36 @@
# #
# This file is autogenerated by pip-compile with Python 3.11 # This file is autogenerated by pip-compile with Python 3.10
# by the following command: # by the following command:
# #
# pip-compile --output-file=requirements/dev.txt requirements/dev.in # pip-compile --output-file=requirements/dev.txt requirements/dev.in
# #
asgiref==3.7.2 asgiref==3.5.2
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# django # django
# django-browser-reload asttokens==2.1.0
asttokens==2.4.1
# via stack-data # via stack-data
babel==2.14.0 backcall==0.2.0
# via # via ipython
# -r requirements/test.txt certifi==2022.9.24
# py-moneyed
certifi==2023.11.17
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# requests # requests
cffi==1.16.0 cffi==1.15.1
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# cryptography # cryptography
charset-normalizer==3.3.2 charset-normalizer==2.1.1
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# requests # requests
click==8.1.7 click==7.1.2
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# uvicorn # uvicorn
coverage==7.4.0 coverage==6.5.0
# via -r requirements/test.txt # via -r requirements/test.txt
cryptography==41.0.7 cryptography==38.0.3
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# pyjwt # pyjwt
@ -43,7 +40,7 @@ defusedxml==0.7.1
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# python3-openid # python3-openid
dj-database-url==2.1.0 dj-database-url==1.0.0
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# environs # environs
@ -51,7 +48,7 @@ dj-email-url==1.0.6
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# environs # environs
django==5.0.1 django==4.1.5
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# dj-database-url # dj-database-url
@ -63,81 +60,81 @@ django==5.0.1
# django-stubs # django-stubs
# django-stubs-ext # django-stubs-ext
# django-zen-queries # django-zen-queries
django-allauth==0.60.0 django-allauth==0.46
# via -r requirements/test.txt # via -r requirements/test.txt
django-browser-reload==1.12.1 django-browser-reload==1.6.0
# via -r requirements/dev.in # via -r requirements/dev.in
django-cache-url==3.4.5 django-cache-url==3.4.2
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# environs # environs
django-debug-toolbar==4.2.0 django-debug-toolbar==3.7.0
# via -r requirements/dev.in # via -r requirements/dev.in
django-extensions==3.2.3 django-extensions==3.2.1
# via -r requirements/dev.in # via -r requirements/dev.in
django-money==3.4.1 django-money==1.3
# via -r requirements/test.txt # via -r requirements/test.txt
django-stubs==4.2.7 django-stubs==1.12.0
# via -r requirements/dev.in # via -r requirements/dev.in
django-stubs-ext==4.2.7 django-stubs-ext==0.7.0
# via django-stubs # via django-stubs
django-zen-queries==2.1.0 django-zen-queries==2.1.0
# via -r requirements/test.txt # via -r requirements/test.txt
environs[django]==10.0.0 environs[django]==9.3
# via -r requirements/test.txt # via -r requirements/test.txt
executing==2.0.1 executing==1.2.0
# via stack-data # via stack-data
h11==0.14.0 h11==0.14.0
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# uvicorn # uvicorn
idna==3.6 idna==3.4
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# requests # requests
ipython==8.19.0 ipython==8.6.0
# via -r requirements/dev.in # via -r requirements/dev.in
jedi==0.19.1 jedi==0.18.1
# via ipython # via ipython
lxml==5.0.1 lxml==4.9.1
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# unittest-xml-reporting # unittest-xml-reporting
marshmallow==3.20.1 marshmallow==3.19.0
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# environs # environs
matplotlib-inline==0.1.6 matplotlib-inline==0.1.6
# via ipython # via ipython
mypy==1.8.0 mypy==0.990
# via -r requirements/dev.in # via
mypy-extensions==1.0.0 # -r requirements/dev.in
# django-stubs
mypy-extensions==0.4.3
# via mypy # via mypy
oauthlib==3.2.2 oauthlib==3.2.2
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# requests-oauthlib # requests-oauthlib
packaging==23.2 packaging==21.3
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# marshmallow # marshmallow
parso==0.8.3 parso==0.8.3
# via jedi # via jedi
pexpect==4.9.0 pexpect==4.8.0
# via ipython # via ipython
prompt-toolkit==3.0.43 pickleshare==0.7.5
# via ipython # via ipython
psycopg[binary]==3.1.16 prompt-toolkit==3.0.32
# via ipython
psycopg2-binary==2.9.5
# via -r requirements/test.txt # via -r requirements/test.txt
psycopg-binary==3.1.16
# via
# -r requirements/test.txt
# psycopg
ptyprocess==0.7.0 ptyprocess==0.7.0
# via pexpect # via pexpect
pure-eval==0.2.2 pure-eval==0.2.2
# via stack-data # via stack-data
py-moneyed==3.0 py-moneyed==0.8.0
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# django-money # django-money
@ -145,13 +142,17 @@ pycparser==2.21
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# cffi # cffi
pygments==2.17.2 pygments==2.13.0
# via ipython # via ipython
pyjwt[crypto]==2.8.0 pyjwt[crypto]==2.6.0
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# django-allauth # django-allauth
python-dotenv==1.0.0 pyparsing==3.0.9
# via
# -r requirements/test.txt
# packaging
python-dotenv==0.21.0
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# environs # environs
@ -159,7 +160,7 @@ python3-openid==3.2.0
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# django-allauth # django-allauth
requests==2.31.0 requests==2.28.1
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# django-allauth # django-allauth
@ -170,43 +171,43 @@ requests-oauthlib==1.3.1
# django-allauth # django-allauth
six==1.16.0 six==1.16.0
# via asttokens # via asttokens
sqlparse==0.4.4 sqlparse==0.4.3
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# django # django
# django-debug-toolbar # django-debug-toolbar
stack-data==0.6.3 stack-data==0.6.1
# via ipython # via ipython
tblib==3.0.0 tblib==1.7.0
# via -r requirements/test.txt # via -r requirements/test.txt
traitlets==5.14.1 tomli==2.0.1
# via
# django-stubs
# mypy
traitlets==5.5.0
# via # via
# ipython # ipython
# matplotlib-inline # matplotlib-inline
types-pytz==2023.3.1.1 types-pytz==2022.6.0.1
# via django-stubs # via django-stubs
types-pyyaml==6.0.12.12 types-pyyaml==6.0.12.2
# via django-stubs # via django-stubs
typing-extensions==4.9.0 typing-extensions==4.4.0
# via # via
# -r requirements/test.txt
# dj-database-url
# django-stubs # django-stubs
# django-stubs-ext # django-stubs-ext
# mypy # mypy
# psycopg
# py-moneyed
unittest-xml-reporting==3.2.0 unittest-xml-reporting==3.2.0
# via -r requirements/test.txt # via -r requirements/test.txt
urllib3==2.1.0 urllib3==1.26.12
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# requests # requests
uvicorn==0.25.0 uvicorn==0.13
# via -r requirements/test.txt # via -r requirements/test.txt
wcwidth==0.2.13 wcwidth==0.2.5
# via prompt-toolkit # via prompt-toolkit
whitenoise==6.6.0 whitenoise==5.2
# via -r requirements/test.txt # via -r requirements/test.txt
# The following packages are considered to be unsafe in a requirements file: # The following packages are considered to be unsafe in a requirements file:

View file

@ -1,5 +1,5 @@
-r base.txt -r base.txt
coverage==7.4.0 coverage==6.5.0
tblib==3.0.0 tblib==1.7.0
unittest-xml-reporting==3.2.0 unittest-xml-reporting==3.2.0

View file

@ -1,36 +1,32 @@
# #
# This file is autogenerated by pip-compile with Python 3.11 # This file is autogenerated by pip-compile with Python 3.10
# by the following command: # by the following command:
# #
# pip-compile --output-file=requirements/test.txt requirements/test.in # pip-compile --output-file=requirements/test.txt requirements/test.in
# #
asgiref==3.7.2 asgiref==3.5.2
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# django # django
babel==2.14.0 certifi==2022.9.24
# via
# -r requirements/base.txt
# py-moneyed
certifi==2023.11.17
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# requests # requests
cffi==1.16.0 cffi==1.15.1
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# cryptography # cryptography
charset-normalizer==3.3.2 charset-normalizer==2.1.1
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# requests # requests
click==8.1.7 click==7.1.2
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# uvicorn # uvicorn
coverage==7.4.0 coverage==6.5.0
# via -r requirements/test.in # via -r requirements/test.in
cryptography==41.0.7 cryptography==38.0.3
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# pyjwt # pyjwt
@ -38,7 +34,7 @@ defusedxml==0.7.1
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# python3-openid # python3-openid
dj-database-url==2.1.0 dj-database-url==1.0.0
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# environs # environs
@ -46,36 +42,36 @@ dj-email-url==1.0.6
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# environs # environs
django==5.0.1 django==4.1.5
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# dj-database-url # dj-database-url
# django-allauth # django-allauth
# django-money # django-money
# django-zen-queries # django-zen-queries
django-allauth==0.60.0 django-allauth==0.46
# via -r requirements/base.txt # via -r requirements/base.txt
django-cache-url==3.4.5 django-cache-url==3.4.2
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# environs # environs
django-money==3.4.1 django-money==1.3
# via -r requirements/base.txt # via -r requirements/base.txt
django-zen-queries==2.1.0 django-zen-queries==2.1.0
# via -r requirements/base.txt # via -r requirements/base.txt
environs[django]==10.0.0 environs[django]==9.3
# via -r requirements/base.txt # via -r requirements/base.txt
h11==0.14.0 h11==0.14.0
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# uvicorn # uvicorn
idna==3.6 idna==3.4
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# requests # requests
lxml==5.0.1 lxml==4.9.1
# via unittest-xml-reporting # via unittest-xml-reporting
marshmallow==3.20.1 marshmallow==3.19.0
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# environs # environs
@ -83,17 +79,13 @@ oauthlib==3.2.2
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# requests-oauthlib # requests-oauthlib
packaging==23.2 packaging==21.3
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# marshmallow # marshmallow
psycopg[binary]==3.1.16 psycopg2-binary==2.9.5
# via -r requirements/base.txt # via -r requirements/base.txt
psycopg-binary==3.1.16 py-moneyed==0.8.0
# via
# -r requirements/base.txt
# psycopg
py-moneyed==3.0
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# django-money # django-money
@ -101,11 +93,15 @@ pycparser==2.21
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# cffi # cffi
pyjwt[crypto]==2.8.0 pyjwt[crypto]==2.6.0
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# django-allauth # django-allauth
python-dotenv==1.0.0 pyparsing==3.0.9
# via
# -r requirements/base.txt
# packaging
python-dotenv==0.21.0
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# environs # environs
@ -113,7 +109,7 @@ python3-openid==3.2.0
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# django-allauth # django-allauth
requests==2.31.0 requests==2.28.1
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# django-allauth # django-allauth
@ -122,27 +118,21 @@ requests-oauthlib==1.3.1
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# django-allauth # django-allauth
sqlparse==0.4.4 sqlparse==0.4.3
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# django # django
tblib==3.0.0 tblib==1.7.0
# via -r requirements/test.in # via -r requirements/test.in
typing-extensions==4.9.0
# via
# -r requirements/base.txt
# dj-database-url
# psycopg
# py-moneyed
unittest-xml-reporting==3.2.0 unittest-xml-reporting==3.2.0
# via -r requirements/test.in # via -r requirements/test.in
urllib3==2.1.0 urllib3==1.26.12
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# requests # requests
uvicorn==0.25.0 uvicorn==0.13
# via -r requirements/base.txt # via -r requirements/base.txt
whitenoise==6.6.0 whitenoise==5.2
# via -r requirements/base.txt # via -r requirements/base.txt
# The following packages are considered to be unsafe in a requirements file: # The following packages are considered to be unsafe in a requirements file:

View file

@ -6,6 +6,7 @@ from . import models
@admin.register(models.Order) @admin.register(models.Order)
class OrderAdmin(admin.ModelAdmin): class OrderAdmin(admin.ModelAdmin):
list_display = ("who", "description", "created", "is_paid") list_display = ("who", "description", "created", "is_paid")
@admin.display(description=_("Customer")) @admin.display(description=_("Customer"))
@ -15,6 +16,7 @@ class OrderAdmin(admin.ModelAdmin):
@admin.register(models.Payment) @admin.register(models.Payment)
class PaymentAdmin(admin.ModelAdmin): class PaymentAdmin(admin.ModelAdmin):
list_display = ("who", "description", "order_id", "created") list_display = ("who", "description", "order_id", "created")
@admin.display(description=_("Customer")) @admin.display(description=_("Customer"))

View file

@ -9,6 +9,7 @@ from djmoney.models.fields import MoneyField
class CreatedModifiedAbstract(models.Model): class CreatedModifiedAbstract(models.Model):
modified = models.DateTimeField(auto_now=True, verbose_name=_("modified")) modified = models.DateTimeField(auto_now=True, verbose_name=_("modified"))
created = models.DateTimeField(auto_now_add=True, verbose_name=_("created")) created = models.DateTimeField(auto_now_add=True, verbose_name=_("created"))
@ -96,6 +97,7 @@ class Order(CreatedModifiedAbstract):
class Payment(CreatedModifiedAbstract): class Payment(CreatedModifiedAbstract):
amount = MoneyField(max_digits=16, decimal_places=2) amount = MoneyField(max_digits=16, decimal_places=2)
order = models.ForeignKey(Order, on_delete=models.PROTECT) order = models.ForeignKey(Order, on_delete=models.PROTECT)

View file

@ -1,25 +0,0 @@
# Generated by Django 4.1.5 on 2023-09-16 14:09
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('membership', '0004_alter_membership_period'),
]
operations = [
migrations.CreateModel(
name='Member',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('auth.user',),
),
]

View file

@ -12,18 +12,13 @@ from utils.mixins import CreatedModifiedAbstract
class Member(User): class Member(User):
class QuerySet(models.QuerySet): class QuerySet(models.QuerySet):
def annotate_membership(self): def annotate_membership(self):
from .selectors import get_current_subscription_period from membership.selectors import get_current_subscription_period
current_subscription_period = get_current_subscription_period()
if not current_subscription_period:
raise ValueError("No current subscription period found")
return self.annotate( return self.annotate(
active_membership=models.Exists( active_membership=models.Exists(
Membership.objects.filter( Membership.objects.filter(
user=models.OuterRef("pk"), user=models.OuterRef("pk"),
period=current_subscription_period.id, period=get_current_subscription_period().id,
), ),
), ),
) )

View file

@ -27,6 +27,7 @@ class Permission:
return f"{self.app_label}.{self.codename}" return f"{self.app_label}.{self.codename}"
def persist_permission(self): def persist_permission(self):
content_type, _ = ContentType.objects.get_or_create( content_type, _ = ContentType.objects.get_or_create(
app_label=self.app_label, app_label=self.app_label,
model=self.model, model=self.model,

View file

@ -23,7 +23,7 @@ def get_subscription_periods(member: Member | None = None) -> list[SubscriptionP
period=OuterRef("pk"), period=OuterRef("pk"),
), ),
), ),
).filter(membership_exists=True) )
return list(subscription_periods) return list(subscription_periods)

View file

@ -1,13 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n %} {% load i18n %}
{% block head_title %}
{% trans "Member detail" %}
{% endblock %}
{% block content %} {% block content %}
<div class="content-view">
<h1> <h1>
{{ member.username }} {{ member.username }}
</h1> </h1>
@ -16,30 +12,28 @@
<h3>{% trans "Membership" %}</h3> <h3>{% trans "Membership" %}</h3>
{% if subscription_periods %} <table class="table">
<table class="table"> <thead>
<thead> <tr>
<tr> <th>{% trans "Start" %}</th>
<th>{% trans "Start" %}</th> <th>{% trans "End" %}</th>
<th>{% trans "End" %}</th> <th>{% trans "Has membership" %}</th>
<th>{% trans "Has membership" %}</th> <th>{% trans "Actions" %}</th>
<th>{% trans "Actions" %}</th> </tr>
</thead>
<tbody>
{% for period in subscription_periods %}
<tr {% if not period.period.upper %}class="table-active"{% endif %}>
<td>{{ period.period.lower }}</td>
<td>{{ period.period.upper }}</td>
<td>{{ period.membership_exists }}</td>
<td></td>
</tr> </tr>
</thead> {% endfor %}
<tbody> </tbody>
{% for period in subscription_periods %} </table>
<tr {% if not period.period.upper %}class="table-active"{% endif %}>
<td>{{ period.period.lower }}</td>
<td>{{ period.period.upper }}</td>
<td>{{ period.membership_exists }}</td>
<td></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
{% trans "No memberships" %}
{% endif %}
</div>
{% endblock %} {% endblock %}

View file

@ -1,14 +1,8 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n %} {% load i18n %}
{% block head_title %}
{% trans "Membership" %}
{% endblock %}
{% block content %} {% block content %}
<div class="content-view">
<h2>Membership settings</h2>
{% if not current_membership %} {% if not current_membership %}
<p>{% trans "You do not have an active membership!" %}</p> <p>{% trans "You do not have an active membership!" %}</p>
@ -26,38 +20,4 @@
<p>{% trans "Period" %}: {{ current_period.lower|date:"SHORT_DATE_FORMAT" }} to {{ current_period.upper|date:"SHORT_DATE_FORMAT"|default:next_general_assembly }}</p> <p>{% trans "Period" %}: {{ current_period.lower|date:"SHORT_DATE_FORMAT" }} to {{ current_period.upper|date:"SHORT_DATE_FORMAT"|default:next_general_assembly }}</p>
<p>{% trans "Type" %}: {{ current_membership.membership_type }}</p> <p>{% trans "Type" %}: {{ current_membership.membership_type }}</p>
{% endif %} {% endif %}
</div> {% endblock %}
<div class="content-view">
<h2>Profile settings</h2>
<form>
<div>
<label for="username">
Username
</label>
<input id="username" type="text" value="{{user}}" />
</div>
<div>
<label for="first_name">
First name
</label>
<input id="first_name" type="text" value="{{user.first_name}}" />
</div>
<div>
<label for="last_name">
Last name
</label>
<input id="last_name" type="text" value="{{user.last_name}}" />
</div>
<button>Update Profile</button>
</form>
</div>
<div class="view-list">
<h2>Email settings</h2>
<button>Update Email</button>
</div>
{% endblock %}

View file

@ -1,20 +1,20 @@
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from zen_queries import render
from .permissions import ADMINISTRATE_MEMBERS from .permissions import ADMINISTRATE_MEMBERS
from .selectors import get_member from .selectors import get_member
from .selectors import get_members from .selectors import get_members
from .selectors import get_memberships from .selectors import get_memberships
from .selectors import get_subscription_periods from .selectors import get_subscription_periods
from utils.view_utils import render
from utils.view_utils import render_list from utils.view_utils import render_list
from utils.view_utils import RowAction from utils.view_utils import RowAction
@login_required @login_required
def membership_overview(request): def membership_overview(request):
memberships = get_memberships(member=request.user) memberships = get_memberships(user=request.user)
current_membership = memberships.current() current_membership = memberships.current()
previous_memberships = memberships.previous() previous_memberships = memberships.previous()
@ -39,8 +39,6 @@ def members_admin(request):
users = get_members() users = get_members()
return render_list( return render_list(
entity_name="member",
entity_name_plural="members",
request=request, request=request,
paginate_by=20, paginate_by=20,
objects=users, objects=users,
@ -70,7 +68,6 @@ def members_admin_detail(request, member_id):
context = { context = {
"member": member, "member": member,
"subscription_periods": subscription_periods, "subscription_periods": subscription_periods,
"base_path": "admin-members",
} }
return render( return render(

View file

@ -0,0 +1,5 @@
from django.contrib.sites.shortcuts import get_current_site
def current_site(request):
return {"site": get_current_site(request)}

View file

@ -65,7 +65,6 @@ MIDDLEWARE = [
"django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
"allauth.account.middleware.AccountMiddleware",
] ]
ROOT_URLCONF = "project.urls" ROOT_URLCONF = "project.urls"
@ -81,9 +80,7 @@ TEMPLATES = [
"django.template.context_processors.request", "django.template.context_processors.request",
"django.contrib.auth.context_processors.auth", "django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages", "django.contrib.messages.context_processors.messages",
], "project.context_processors.current_site",
"builtins": [
"django.templatetags.i18n",
], ],
}, },
}, },

View file

@ -1,384 +0,0 @@
/* Reset */
*, *::before, *::after {
box-sizing: border-box;
}
* {
margin: 0;
}
body {
line-height: 1.5;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
#root, #__next {
isolation: isolate;
}
/* Variables */
:root {
/* Colors */
--light : #fff;
--light-dust : #f6f6f6;
--dust : #f1f1f1;
--medium-dust : #dadada;
--dark-dust : #bfbfbf;
--fade : #878787;
--twilight : #4a4a4a;
--dark : #2a2a2a;
--custard : #f0dcac;
--water : #a8f3f4;
--splash : #4b3aba;
/* Sizes */
--space : 12px;
--double-space : calc(var(--space) * 2);
--half-space : calc(var(--space) / 2);
--quarter-space : calc(var(--space) / 4);
--outer-space : var(--double-space);
}
@media (min-width: 1380px) {
:root {
--outer-space : 15%;
}
}
html, body {
height : 100%;
}
h1, h2, h3, h4, h5, h6 {
font-weight : 600;
color : var(--twilight);
}
a {
font-weight : 500;
color : var(--splash);
text-decoration : none;
}
body {
margin : 0;
padding : 0;
background : var(--custard);
font-family : Inter;
font-weight : 400;
line-height : 1.6;
color : var(--dark);
}
header {
display : flex;
padding : var(--double-space) var(--outer-space);
background : var(--light);
justify-content : space-between;
align-items : center;
}
header > h1 {
font-size : 1.44em;
}
header > a.logout {
padding : 6px 12px;
border-radius : 6px;
background : var(--twilight);
text-decoration : none;
color : var(--dust);
}
header > a.logout:hover {
background : var(--splash);
color : var(--light);
}
aside {
padding : var(--double-space) var(--outer-space);
background : var(--light);
}
aside > div {
background : var(--dust);
padding : var(--double-space);
border-radius : var(--quarter-space);
overflow : hidden;
}
aside > div > h2 {
font-size : 1.22em;
margin : 0 0 6px 0;
}
aside > div > figure {
width : 100px;
height : 100px;
border : 1px solid var(--dark-dust);
float : left;
margin-right : var(--double-space);
}
aside > div > dl {
overflow : hidden;
}
aside > div > dl > dt {
float : left;
clear : left;
margin : 0 var(--double-space) 0 0;
width : 180px;
font-weight : 600;
color : var(--twilight);
}
nav {
display : block;
border-bottom : 1px solid var(--dark-dust);
background : var(--light);
}
nav ol {
margin: 0 calc(var(--outer-space));
padding : 0;
list-style-type : none;
overflow : hidden;
}
nav ol li {
margin : 0;
padding : 0;
float : left;
}
nav > ol > li > a {
display : block;
padding : var(--half-space) var(--half-space) var(--quarter-space);
margin : 0 var(--space);
border-bottom : var(--half-space) solid transparent;
text-decoration : none;
color : var(--dark);
cursor : pointer;
font-weight : 500;
letter-spacing : 0.04em;
}
nav > ol > li:first-child > a {
margin-left : 0;
}
nav ol li a:hover {
border-color : rgba(0,0,0,0.6);
}
nav ol li a.current {
font-weight : bold;
border-color : var(--splash);
color : var(--splash);
}
article {
padding : var(--double-space) var(--outer-space);
}
article > div {
background : var(--dust);
padding : var(--double-space);
margin-bottom : var(--double-space);
}
div.content-view > h2 {
margin : 0 0 var(--double-space) 0;
}
div.services {
display : flex;
justify-content : space-between;
gap : var(--double-space);
flex-wrap: wrap;
}
div.services > div,
div.infobox {
background : var(--light);
padding : var(--double-space);
border-radius : 6px;
flex : 240px;
max-width : 420px;
display : flex;
flex-flow : column;
justify-content : space-between;
}
div.infobox button {
margin-top : var(--double-space);
}
div.services > div > div.description {
margin-bottom : var(--double-space);
}
div.services > div > div.description > p {
margin-top : var(--half-space);
}
div.services > div > a,
a.button,
button {
display : block;
color : var(--light);
background : var(--splash);
padding : var(--space) var(--double-space);
border-radius : 3px;
opacity : 0.9;
cursor : pointer;
text-align : center;
border : 0;
font-weight : 600;
letter-spacing : 0.03em;
text-decoration : none;
}
div.services > div > a:hover,
a.button:hover,
button:hover {
opacity : 1.0;
}
article table {
width : 100%;
border-spacing : 0;
margin : var(--space) 0;
}
article table thead th {
text-align : left;
}
article table tbody tr:nth-child(odd) {
background : var(--medium-dust);
}
article table thead th,
article table tbody td {
padding : var(--half-space);
}
form > div {
margin : 0 0 var(--double-space);
}
form > div >label {
display : block;
margin : 0 0 6px;
}
form > div > input[type="text"],
form > div > input[type="password"] {
border : 2px solid var(--twilight);
border-radius : 6px;
padding : 8px;
background : var(--light-dust);
width : 100%;
}
#login {
height : 100%;
display : flex;
align-items : center;
justify-content : center;
}
#loginbox {
border-radius : var(--space);
border : 6px solid white;
width : 800px;
height : 500px;
display : flex;
flex-flow : row;
}
#loginbox > div {
padding : var(--double-space);
flex : 1;
}
#loginbox label {
color : var(--twilight);
}
#loginbox > div.login {
background : var(--light-dust);
display: flex;
flex-flow: column;
justify-content: space-between;
}
#loginbox > div.signup {
background : var(--water);
display: flex;
flex-flow: column;
align-items: center;
}
#loginbox > div:first-child {
border-radius : var(--half-space) 0 0 var(--half-space);
}
#loginbox > div:last-child {
border-radius : 0 var(--half-space) var(--half-space) 0;
}
#loginbox > div:last-child > * {
flex : 1;
}
#loginbox div.new_here {
margin-top : var(--double-space);
}
#loginbox div.new_here h2 {
margin: var(--double-space) 0;
}
#loginbox button {
width : 100%;
}
#loginbox img {
padding : 0 var(--double-space);
}
footer {
margin : var(--space) var(--outer-space);
padding : var(--space);
border-radius : var(--quarter-space);
background : var(--dark);
color : var(--dust);
font-size : 0.78em;
opacity : 0.8;
}
span.time_remaining {
color : var(--fade);
}
.pagination {
display : flex;
justify-content : center;
list-style : none;
padding : 0;
margin : 0;
}
.pagination > li {
margin : 0 6px;
}

View file

@ -1,200 +0,0 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100;
font-display: swap;
src: url("Inter-Thin.woff2?v=3.19") format("woff2"),
url("Inter-Thin.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 100;
font-display: swap;
src: url("Inter-ThinItalic.woff2?v=3.19") format("woff2"),
url("Inter-ThinItalic.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 200;
font-display: swap;
src: url("Inter-ExtraLight.woff2?v=3.19") format("woff2"),
url("Inter-ExtraLight.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 200;
font-display: swap;
src: url("Inter-ExtraLightItalic.woff2?v=3.19") format("woff2"),
url("Inter-ExtraLightItalic.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url("Inter-Light.woff2?v=3.19") format("woff2"),
url("Inter-Light.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url("Inter-LightItalic.woff2?v=3.19") format("woff2"),
url("Inter-LightItalic.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("Inter-Regular.woff2?v=3.19") format("woff2"),
url("Inter-Regular.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url("Inter-Italic.woff2?v=3.19") format("woff2"),
url("Inter-Italic.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("Inter-Medium.woff2?v=3.19") format("woff2"),
url("Inter-Medium.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url("Inter-MediumItalic.woff2?v=3.19") format("woff2"),
url("Inter-MediumItalic.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("Inter-SemiBold.woff2?v=3.19") format("woff2"),
url("Inter-SemiBold.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url("Inter-SemiBoldItalic.woff2?v=3.19") format("woff2"),
url("Inter-SemiBoldItalic.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("Inter-Bold.woff2?v=3.19") format("woff2"),
url("Inter-Bold.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url("Inter-BoldItalic.woff2?v=3.19") format("woff2"),
url("Inter-BoldItalic.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url("Inter-ExtraBold.woff2?v=3.19") format("woff2"),
url("Inter-ExtraBold.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 800;
font-display: swap;
src: url("Inter-ExtraBoldItalic.woff2?v=3.19") format("woff2"),
url("Inter-ExtraBoldItalic.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url("Inter-Black.woff2?v=3.19") format("woff2"),
url("Inter-Black.woff?v=3.19") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 900;
font-display: swap;
src: url("Inter-BlackItalic.woff2?v=3.19") format("woff2"),
url("Inter-BlackItalic.woff?v=3.19") format("woff");
}
/* -------------------------------------------------------
Variable font.
Usage:
html { font-family: 'Inter', sans-serif; }
@supports (font-variation-settings: normal) {
html { font-family: 'Inter var', sans-serif; }
}
*/
@font-face {
font-family: 'Inter var';
font-weight: 100 900;
font-display: swap;
font-style: normal;
font-named-instance: 'Regular';
src: url("Inter-roman.var.woff2?v=3.19") format("woff2");
}
@font-face {
font-family: 'Inter var';
font-weight: 100 900;
font-display: swap;
font-style: italic;
font-named-instance: 'Italic';
src: url("Inter-italic.var.woff2?v=3.19") format("woff2");
}
/* --------------------------------------------------------------------------
[EXPERIMENTAL] Multi-axis, single variable font.
Slant axis is not yet widely supported (as of February 2019) and thus this
multi-axis single variable font is opt-in rather than the default.
When using this, you will probably need to set font-variation-settings
explicitly, e.g.
* { font-variation-settings: "slnt" 0deg }
.italic { font-variation-settings: "slnt" 10deg }
*/
@font-face {
font-family: 'Inter var experimental';
font-weight: 100 900;
font-display: swap;
font-style: oblique 0deg 10deg;
src: url("Inter.var.woff2?v=3.19") format("woff2");
}

View file

@ -4,61 +4,53 @@
{% block non_login_content %} {% block non_login_content %}
<div id="loginbox"> {% if form.non_field_errors %}
<div class="login"> {% for error in form.non_field_errors %}
{% if form.non_field_errors %} <div class="alert alert-danger" role="alert">
{% for error in form.non_field_errors %} {{ error }}
<div class="alert alert-danger" role="alert"> </div>
{{ error }} {% endfor %}
</div> {% endif %}
{% endfor %}
{% endif %}
<h2>Log in</h2>
<form method="post" action=""> <form method="post" action="">
{% csrf_token %} {% csrf_token %}
<div>
<label for="id_username" <label for="id_username"
class="visually-hidden"> class="visually-hidden">
{% trans "E-mail" %} {% trans "E-mail" %}
</label> </label>
<input type="text" <input type="text"
id="id_username" id="id_username"
name="login" name="login"
class="form-control mb-lg-2" class="form-control mb-lg-2"
placeholder="{% trans "E-mail" %}" placeholder="{% trans "E-mail" %}"
required required
autofocus> autofocus>
</div>
<div> <label for="id_password" class="visually-hidden">
<label for="id_password" class="visually-hidden"> {% trans "Password" %}
{% trans "Password" %} </label>
</label> <input type="password"
<input type="password" id="id_password"
id="id_password" name="password"
name="password" class="form-control mb-lg-2"
class="form-control mb-lg-2" placeholder="{% trans "Password" %}"
placeholder="{% trans "Password" %}" required>
required>
</div> <button class="w-100 mb-lg-2 btn btn-lg btn-primary"
<div> type="submit">{% trans "Sign in" %}</button>
<button type="submit">{% trans "Sign in" %}</button> </form>
</div> <a href="{% url "account_reset_password" %}"
</form> class="w-100 btn btn-lg btn-outline-info">
<div> {% trans "Forgot password?" %}
<a href="{% url "account_reset_password" %}" </a>
class="w-100 btn btn-lg btn-outline-info">
{% trans "Forgot password?" %} <hr class="hr-text" data-content="{% trans "Or"|upper %}">
</a>
</div> <a class="w-100 btn btn-lg btn-outline-success"
</div> type="submit"
<div class="signup"> href="{% url "account_signup" %}">
<img src="https://data.coop/static/img/logo_da.svg" alt="data.coop logo"> {% trans "Become a member" %}
<div class="new_here"> </a>
<h2> Are you new here? </h2>
<a class="button" href="{% url "account_signup" %}">{% trans "Become a member" %}</a>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View file

@ -5,18 +5,17 @@
{% block head_title %}{% trans "Sign Out" %}{% endblock %} {% block head_title %}{% trans "Sign Out" %}{% endblock %}
{% block content %} {% block content %}
<div class="content-view"> <h1>{% trans "Sign Out" %}</h1>
<h2>{% trans "Sign Out" %}</h2>
<p>{% trans 'Are you sure you want to sign out?' %}</p> <p>{% trans 'Are you sure you want to sign out?' %}</p>
<form method="post" action="{% url 'account_logout' %}"> <form method="post" action="{% url 'account_logout' %}">
{% csrf_token %} {% csrf_token %}
{% if redirect_field_value %} {% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}"/> <input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}"/>
{% endif %} {% endif %}
<button type="submit">{% trans 'Sign Out' %}</button> <button type="submit">{% trans 'Sign Out' %}</button>
</form> </form>
</div>
{% endblock %} {% endblock %}

View file

@ -7,13 +7,71 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content=""> <meta name="description" content="">
<title>Login {{ site.name }}</title> <title>Login {{ site.name }}</title>
<link rel="stylesheet" href="{% static "/fonts/inter.css" %}">
<link rel="stylesheet" href="{% static "/css/style.css" %}"> <link href="{% static "/css/bootstrap.min.css" %}" rel="stylesheet"
crossorigin="anonymous">
<link href="{% static "/css/bootstrap-icons.css" %}" rel="stylesheet"
crossorigin="anonymous">
<style>
html,
body {
height: 100%;
}
body {
display: flex;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #a8f3f4;
}
.form-signin {
width: 100%;
max-width: 330px;
padding: 15px;
margin: auto;
}
.form-signin .checkbox {
font-weight: 400;
}
.form-signin .form-control {
position: relative;
box-sizing: border-box;
height: auto;
padding: 10px;
font-size: 16px;
}
.form-signin .form-control:focus {
z-index: 2;
}
.hr-text:after {
content: attr(data-content);
padding: 0 4px;
position: relative;
top: -13px;
background-color: #a8f3f4;
}
</style>
</head> </head>
<body> <body class="text-center">
<main id="login">
{% block non_login_content %} <main class="form-signin">
{% endblock %} <img class="mb-4" src="https://new.data.coop/static/img/logo_da.svg" alt=""
</main> width="260" height="160">
{% block non_login_content %}
{% endblock %}
</main>
</body> </body>
</html> </html>

View file

@ -3,82 +3,211 @@
{% load static %} {% load static %}
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content=""> <meta name="description" content="">
<title>{% block head_title %}{% endblock %} {{ site.name }}</title> <title>{% block head_title %}{% endblock %} {{ site.name }}</title>
<link rel="stylesheet" href="{% static "fonts/inter.css" %}">
<link rel="stylesheet" href="{% static "css/style.css" %}">
</head>
<body>
<header>
<h1> data.coop membersystem </h1>
<a class="logout" href="{% url "account_logout" %}">Log out</a>
</header>
<main>
<aside>
<div>
<figure></figure>
<h2>{{ user }}</h2>
{% if current_membership %} <link href="{% static "/css/bootstrap.min.css" %}" rel="stylesheet"
<dl> crossorigin="anonymous">
<dt>Membership</dt>
<dd>
Active
</dd>
<dt>Period</dt> <link href="{% static "/css/bootstrap-icons.css" %}" rel="stylesheet"
<dd> crossorigin="anonymous">
Until {{ current_period.upper }} <span class="time_remaining">({{ current_period.upper|timeuntil }})</span>
</dd>
<dt>Membership type</dt> <style>
<dd>Normal member</dd> body {
</dl> font-size: .875rem;
{% else %} }
Your membership status will be displayed here in the future.
{% endif %} /*
</div> * Sidebar
</aside> */
<nav>
<ol> .sidebar {
<li> position: fixed;
<a href="/" class="{% active_path "index" "current" %}"> top: 0;
Dashboard bottom: 0;
left: 0;
z-index: 100; /* Behind the navbar */
padding: 48px 0 0; /* Height of navbar */
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
}
@media (max-width: 767.98px) {
.sidebar {
top: 5rem;
}
}
.sidebar-sticky {
position: relative;
top: 0;
height: calc(100vh - 48px);
padding-top: .5rem;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
}
.sidebar .nav-link {
font-weight: 500;
color: #333;
}
.sidebar .nav-link .feather {
margin-right: 4px;
color: #727272;
}
.sidebar .nav-link.active {
color: #007bff;
}
.sidebar .nav-link:hover .feather,
.sidebar .nav-link.active .feather {
color: inherit;
}
.sidebar-heading {
font-size: .75rem;
text-transform: uppercase;
}
/*
* Navbar
*/
.navbar-brand {
padding-top: .75rem;
padding-bottom: .75rem;
font-size: 1rem;
background-color: rgba(0, 0, 0, .25);
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
}
.navbar .navbar-toggler {
top: .25rem;
right: 1rem;
}
.navbar .form-control {
padding: .75rem 1rem;
border-width: 0;
border-radius: 0;
}
.form-control-dark {
color: #fff;
background-color: rgba(255, 255, 255, .1);
border-color: rgba(255, 255, 255, .1);
}
.form-control-dark:focus {
border-color: transparent;
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
} </style>
<script src="{% static "js/bootstrap.bundle.min.js" %}"
crossorigin="anonymous"></script>
</head>
<body>
<header class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
<a class="navbar-brand col-md-3 col-lg-2 me-0 px-3" href="#">{{ site.name }}</a>
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button"
data-bs-toggle="collapse" data-bs-target="#sidebarMenu"
aria-controls="sidebarMenu" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<ul class="navbar-nav px-3">
<li class="nav-item text-nowrap">
<a class="nav-link" href="{% url "account_logout" %}">
<i class="bi bi-person-circle"></i>
Sign out</a>
</li>
</ul>
</header>
<div class="container-fluid">
<div class="row">
<nav id="sidebarMenu"
class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
<div class="position-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link {% active_path "index" "active" %}"
href="{% url "index" %}">
{% trans "Dashboard" %}
</a> </a>
</li> </li>
</ul>
{% comment %} <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<li> <span>{% trans "Profile" %}</span>
<a href="/services" class="{% active_path "services" "current" %}"> </h6>
Services
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="#">
{% trans "Details" %}
</a> </a>
</li> </li>
{% endcomment %} <li class="nav-item">
<a class="nav-link {% active_path "account_email" "active" %}"
<li> aria-current="page" href="{% url "account_email" %}">
<a href="{% url "account_email" %}" class="{% active_path "account_email" "current" %}"> {% trans "Emails" %}
Email
</a> </a>
</li> </li>
</ul>
{% if perms.membership.administrate_memberships %} <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<li> <span>{% trans "Membership" %}</span>
<a href="{% url "admin-members" %}" class="{% active_path "admin-members" "current" %}"> </h6>
Admin
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link {% active_path "membership-overview" "active" %}"
aria-current="page" href="{% url "membership-overview" %}">
{% trans "Overview" %}
</a> </a>
</li> </li>
{% endif %} </ul>
</ol>
</nav> <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<article> <span>{% trans "Services" %}</span>
{% block content %}{% endblock %} </h6>
</article>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link {% active_path "services-overview" "active" %}"
href="{% url "services-overview" %}">
{% trans "Overview" %}
</a>
</li>
</ul>
{% if perms.membership.administrate_memberships %}
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>{% trans "Admin" %}</span>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item {% active_path "admin-members" "active" %}">
<a class="nav-link" href="{% url "admin-members" %}">
<span data-feather="file-text"></span>
{% trans "Members" %}
</a>
</li>
</ul>
{% endif %}
</div>
</nav>
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 p-4">
{% block content %}
{% endblock %}
</main> </main>
<footer> </div>
data.coop membersystem version 0.0.1 </div>
</footer> </body>
</body>
</html> </html>

View file

@ -1,28 +1 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block head_title %}
{% trans "Home" %}
{% endblock %}
{% block content %}
<div class="content-view">
<h2>Welcome {{ user }}!</h2>
<p>
This is the new member area.
</p>
<p>
It is very much under construction.
</p>
{% comment %}
<hr>
<br>
<div class="infobox">
<p>
To get started we need you to verify your email!
</p>
<button>Verify email</button>
</div>
{% endcomment %}
</div>
{% endblock %}

View file

@ -1,62 +1,10 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<p>
<div class="content-view"> Services and signup to these will be
Coming soon! </p>
</div> <p>
This is yet to be implemented.
{% comment %} </p>
<div class="content-view">
<h2>Services you subscribe to</h2>
<div class="services">
<div>
<div class="description">
<h3>Passit</h3>
<p>Passit is a service that blabla</p>
<a href="#">Read more &hellip;</a>
</div>
<a>Unsubscribe</a>
</div>
</div>
</div>
<div class="content-view">
<h2>Available services</h2>
<div class="services">
<div>
<div class="description">
<h3>Forgejo</h3>
<p>Forgejo is a service that blabla</p>
<a href="#">Read more &hellip;</a>
</div>
<a>Subscribe</a>
</div>
<div>
<div class="description">
<h3>Mastodon</h3>
<p>Mastodon is a service where you can write things to people around the world.</p>
<a href="#">Read more &hellip;</a>
</div>
<a>Subscribe</a>
</div>
<div>
<div class="description">
<h3>Matrix</h3>
<p>Matrix is a service that blabla</p>
<a href="#">Read more &hellip;</a>
</div>
<a>Subscribe</a>
</div>
<div>
<div class="description">
<h3>NextCloud</h3>
<p>NextCloud is a service that blabla</p>
<a href="#">Read more &hellip;</a>
</div>
<a>Subscribe</a>
</div>
</div>
</div>
{% endcomment %}
{% endblock %} {% endblock %}

View file

@ -13,7 +13,7 @@ from membership.views import membership_overview
urlpatterns = [ urlpatterns = [
path("", login_required(index), name="index"), path("", login_required(index), name="index"),
path("services/", login_required(services_overview), name="services"), path("services/", login_required(services_overview), name="services-overview"),
path("membership/", membership_overview, name="membership-overview"), path("membership/", membership_overview, name="membership-overview"),
path("admin/members/", members_admin, name="admin-members"), path("admin/members/", members_admin, name="admin-members"),
path( path(

View file

@ -1,4 +1,4 @@
from utils.view_utils import render from django.shortcuts import render
def index(request): def index(request):

View file

@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _
class CreatedModifiedAbstract(models.Model): class CreatedModifiedAbstract(models.Model):
modified = models.DateTimeField(auto_now=True, verbose_name=_("modified")) modified = models.DateTimeField(auto_now=True, verbose_name=_("modified"))
created = models.DateTimeField(auto_now_add=True, verbose_name=_("created")) created = models.DateTimeField(auto_now_add=True, verbose_name=_("created"))

View file

@ -1,16 +1,10 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n %} {% load i18n %}
{% block head_title %}
{{ entity_name_plural|capfirst }}
{% endblock %}
{% block content %} {% block content %}
<div class="content-view">
<h1> <h1>
{{ entity_name_plural|capfirst }} <small class="text-muted">{{ total_count }}</small> Users <small class="text-muted">{{ total_count }}</small>
</h1> </h1>
<table class="table table-striped"> <table class="table table-striped">
@ -50,6 +44,7 @@
</table> </table>
{% if is_paginated %} {% if is_paginated %}
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">
{% if not page.has_previous %} {% if not page.has_previous %}
@ -102,9 +97,8 @@
{% endif %} {% endif %}
</ul> </ul>
</nav>
{% endif %} {% endif %}
</div>
{% endblock %} {% endblock %}

View file

@ -5,17 +5,9 @@ register = template.Library()
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def active_path(context, path_name, class_name) -> str | None: def active_path(context, path_name, class_name):
"""Return the given class name if the current path matches the given path name."""
path = reverse(path_name) path = reverse(path_name)
request_path = context.get("request").path request_path = context.get("request").path
# Check if the current path matches the given path name. if path == request_path or ("basepath" in context and context["basepath"] == path):
is_path = path == request_path
# Check if the current path is a sub-path of the given path name.
is_base_path = "base_path" in context and reverse(context["base_path"]) == path
if is_path or is_base_path:
return class_name return class_name

View file

@ -1,16 +1,13 @@
import contextlib import contextlib
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models import Model from django.db.models import Model
from django.http import HttpRequest from django.http import HttpRequest
from django.http import HttpResponse from django.http import HttpResponse
from django.urls import reverse from django.urls import reverse
from zen_queries import queries_disabled from zen_queries import render
from zen_queries import render as zen_queries_render
@dataclass @dataclass
@ -46,8 +43,6 @@ class RowAction:
def render_list( def render_list(
request: HttpRequest, request: HttpRequest,
entity_name: str,
entity_name_plural: str,
objects: list["Model"], objects: list["Model"],
columns: list[tuple[str, str]], columns: list[tuple[str, str]],
row_actions: list[RowAction] = None, row_actions: list[RowAction] = None,
@ -75,12 +70,11 @@ def render_list(
rows = [] rows = []
for obj in objects: for obj in objects:
with queries_disabled(): row = Row(
row = Row( data={column: getattr(obj, column[0]) for column in columns},
data={column: getattr(obj, column[0]) for column in columns}, actions=[action.render(obj) for action in row_actions],
actions=[action.render(obj) for action in row_actions], )
) rows.append(row)
rows.append(row)
context = { context = {
"rows": rows, "rows": rows,
@ -89,8 +83,6 @@ def render_list(
"list_actions": list_actions, "list_actions": list_actions,
"total_count": total_count, "total_count": total_count,
"order_by": order_by, "order_by": order_by,
"entity_name": entity_name,
"entity_name_plural": entity_name_plural,
} }
if paginate_by: if paginate_by:
@ -104,20 +96,3 @@ def render_list(
template_name="utils/list.html", template_name="utils/list.html",
context=context, context=context,
) )
def base_context(request: HttpRequest) -> dict[str, Any]:
"""
Return a base context for all views.
"""
return {"site": get_current_site(request)}
def render(request, template_name, context=None):
"""
Render a template with a base context.
"""
if context is None:
context = {}
context = base_context(request) | context
return zen_queries_render(request, template_name, context)