Compare commits

...

31 Commits
stripe ... main

Author SHA1 Message Date
Víðir Valberg Guðmundsson d31f62ebb4 Upd.
continuous-integration/drone/push Build is passing Details
2024-05-31 21:26:20 +02:00
Víðir Valberg Guðmundsson 712c50fac7 Use hatch for installing.
continuous-integration/drone/push Build is passing Details
2024-03-03 11:05:21 +01:00
Halfdan Mouritzen fedfca25a5 Adding flow to add new emails to an account
continuous-integration/drone/push Build is passing Details
2024-03-02 17:43:16 +01:00
Halfdan Mouritzen 89d7c9c9d5 CSS should be merged correctly now
continuous-integration/drone/push Build is passing Details
2024-03-02 14:49:50 +01:00
Halfdan Mouritzen f5acfbedd4 Cleaning 2024-03-02 12:15:33 +01:00
Víðir Valberg Guðmundsson cf3c84b8d9 More linting.
continuous-integration/drone/push Build is passing Details
2024-02-29 21:30:36 +01:00
Víðir Valberg Guðmundsson 1b65558608 Lint galore. Also use python 3.12. 2024-02-29 21:28:17 +01:00
Halfdan Mouritzen 4112069cac feature/css (#29)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #29
Co-authored-by: Halfdan Mouritzen <halfdan@robothangarskib.dk>
Co-committed-by: Halfdan Mouritzen <halfdan@robothangarskib.dk>
2024-02-29 20:02:39 +00:00
Víðir Valberg Guðmundsson bdc2d8717c Add devenv files.
continuous-integration/drone/push Build is passing Details
2024-02-10 10:55:38 +01:00
Víðir Valberg Guðmundsson f99c7ee698 Fetch permissions before rendering templates.
continuous-integration/drone/push Build is passing Details
2024-02-10 10:50:23 +01:00
Víðir Valberg Guðmundsson a098a0b032 Add logging.
continuous-integration/drone/push Build is passing Details
2024-02-09 22:04:30 +01:00
Víðir Valberg Guðmundsson f31cd62351 Implement django-view-decorator
continuous-integration/drone/push Build is passing Details
2024-01-14 12:27:36 +01:00
Víðir Valberg Guðmundsson b8a970d5fe Add missing migration 2024-01-14 12:14:51 +01:00
Halfdan Mouritzen 9dda7670df Minimal CSS for tables (#26)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #26
Co-authored-by: Halfdan Mouritzen <halfdan@robothangarskib.dk>
Co-committed-by: Halfdan Mouritzen <halfdan@robothangarskib.dk>
2024-01-14 11:10:55 +00:00
Halfdan Mouritzen 02907a7684 Minimal CSS for tables
continuous-integration/drone/pr Build is failing Details
2024-01-13 22:14:23 +01:00
valberg 33f5e7a285 ui-overhaul (#24)
continuous-integration/drone/push Build is passing Details
Co-authored-by: Halfdan <halm@itu.dk>
Co-authored-by: Halfdan Mouritzen <halfdan@robothangarskib.dk>
Reviewed-on: #24
2024-01-13 20:07:36 +00:00
Víðir Valberg Guðmundsson 2ed5df5241 Add pyproject.toml file. 2024-01-13 15:39:31 +01:00
Víðir Valberg Guðmundsson 9b52cd4270 Update stuff. 2024-01-07 15:04:08 +01:00
Víðir Valberg Guðmundsson 25445e59cf Add i18n to builtin template tags.
continuous-integration/drone/push Build was killed Details
2023-10-02 21:19:25 +02:00
Víðir Valberg Guðmundsson f42101c476 Adding some titles
continuous-integration/drone/push Build is passing Details
2023-10-02 20:50:39 +02:00
Víðir Valberg Guðmundsson 88e15974dc Fixing some stuff with README
continuous-integration/drone/push Build is passing Details
2023-10-02 20:10:24 +02:00
Víðir Valberg Guðmundsson 81ff8d0177 Upgrade pre-commit hooks.
continuous-integration/drone/push Build is passing Details
2023-10-02 19:56:38 +02:00
Víðir Valberg Guðmundsson 6894c57aa0 Upgrade dependencies. 2023-10-02 19:56:09 +02:00
Víðir Valberg Guðmundsson d2e58d396f Small adjustments.
continuous-integration/drone/push Build is passing Details
2023-09-30 08:14:02 +02:00
Víðir Valberg Guðmundsson 770c1c81b6 Fix member list.
continuous-integration/drone/push Build is passing Details
2023-09-18 21:14:47 +02:00
Víðir Valberg Guðmundsson f1c328a10c Upgrading stuff.
continuous-integration/drone/push Build is passing Details
2023-09-18 20:58:30 +02:00
Víðir Valberg Guðmundsson beabd49976 Fix membership page
continuous-integration/drone/push Build is passing Details
2023-09-16 16:27:15 +02:00
Víðir Valberg Guðmundsson bab951afb8 Add missing migration
continuous-integration/drone/push Build is passing Details
2023-09-16 16:19:51 +02:00
Víðir Valberg Guðmundsson 390954ebaf Use docker compose, not docker-compose.
continuous-integration/drone/push Build is passing Details
2023-09-16 15:39:13 +02:00
Víðir Valberg Guðmundsson 175f0438d4 Update logo.
continuous-integration/drone/push Build is passing Details
2023-09-16 15:28:42 +02:00
Víðir Valberg Guðmundsson e6661c1b5a Minor detail.
continuous-integration/drone/push Build is passing Details
2023-02-08 08:19:58 +01:00
99 changed files with 2147 additions and 2806 deletions

View File

@ -5,3 +5,5 @@
!src/
!requirements/
!entrypoint.sh
!pyproject.toml
!README.md

View File

@ -3,5 +3,7 @@ POSTGRES_HOST=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_PORT=5432
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
DJANGO_ENV=development

3
.gitignore vendored
View File

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

View File

@ -1,9 +1,9 @@
default_language_version:
python: python3.11
python: python3.12
exclude: ^.*\b(migrations)\b.*$
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v4.6.0
hooks:
- id: check-ast
- id: check-merge-conflict
@ -16,48 +16,22 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: 'v0.0.209'
rev: 'v0.4.4'
hooks:
- id: ruff
args:
- --force-exclude
- --fix
- repo: https://github.com/asottile/reorder_python_imports
rev: v3.9.0
hooks:
- id: reorder-python-imports
args:
- --py310-plus
- --application-directories=.:src
exclude: migrations/
- id: ruff-format
- repo: https://github.com/asottile/pyupgrade
rev: v3.3.1
rev: v3.15.2
hooks:
- id: pyupgrade
args:
- --py311-plus
exclude: migrations/
- repo: https://github.com/adamchainz/django-upgrade
rev: 1.12.0
rev: 1.17.0
hooks:
- id: django-upgrade
args:
- --target-version=4.1
- repo: https://github.com/asottile/yesqa
rev: v1.4.0
hooks:
- id: yesqa
- repo: https://github.com/asottile/add-trailing-comma
rev: v2.4.0
hooks:
- id: add-trailing-comma
args:
- --py36-plus
- repo: https://github.com/hadialqattan/pycln
rev: v2.1.2
hooks:
- id: pycln
- repo: https://github.com/psf/black
rev: 22.12.0
hooks:
- id: black
- --target-version=5.0

View File

@ -1,4 +1,4 @@
FROM python:3.11-slim-bullseye
FROM python:3.12-slim-bullseye
ENV PYTHONFAULTHANDLER=1 \
PYTHONUNBUFFERED=1 \
@ -11,7 +11,13 @@ ARG DJANGO_ENV
ARG BUILD
ENV BUILD ${BUILD}
RUN apt-get update \
WORKDIR /app
RUN groupadd -g 1000 www && useradd -u 1000 -ms /bin/bash -g www www
COPY --chown=www:www . .
RUN mkdir /app/src/static \
&& chown www:www /app/src/static \
&& apt-get update \
&& apt-get install -y \
binutils \
libpq-dev \
@ -23,15 +29,9 @@ RUN apt-get update \
libgdk-pixbuf2.0-0 \
libffi-dev \
shared-mime-info \
gettext
WORKDIR /app
RUN groupadd -g 1000 www && useradd -u 1000 -ms /bin/bash -g www www
COPY --chown=www:www . /app/
RUN mkdir /app/src/static && chown www:www /app/src/static
RUN pip install -r requirements/$([ "$DJANGO_ENV" = "production" ] && echo "base.txt" || echo "dev.txt") &&\
django-admin compilemessages
gettext \
&& pip install . \
&& django-admin compilemessages
ENTRYPOINT ["./entrypoint.sh"]

View File

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

View File

@ -1,12 +1,22 @@
# member.data.coop
## Development requirements
## Development
### Setup environment
Copy over the .env.example file to .env and adjust DATABASE_URL accordingly
$ cp .env.example .env
### Docker
#### Requirements
- Docker
- Docker compose
- pre-commit (preferred for contributions)
## Start local server
#### Setup
Given that the requirements above are installed, it should be as easy as:
@ -33,3 +43,29 @@ Make messages:
Running tests:
$ 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

241
devenv.lock Normal file
View File

@ -0,0 +1,241 @@
{
"nodes": {
"devenv": {
"locked": {
"dir": "src/modules",
"lastModified": 1707004164,
"narHash": "sha256-9Hr8onWtvLk5A8vCEkaE9kxA0D7PR62povFokM1oL5Q=",
"owner": "cachix",
"repo": "devenv",
"rev": "0e68853bb27981a4ffd7a7225b59ed84f7180fc7",
"type": "github"
},
"original": {
"dir": "src/modules",
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_2": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github"
},
"original": {
"id": "flake-utils",
"type": "indirect"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1703887061,
"narHash": "sha256-gGPa9qWNc6eCXT/+Z5/zMkyYOuRZqeFZBDbopNZQkuY=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "43e1aa1308018f37118e34d3a9cb4f5e75dc11d5",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1707451808,
"narHash": "sha256-UwDBUNHNRsYKFJzyTMVMTF5qS4xeJlWoeyJf+6vvamU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "442d407992384ed9c0e6d352de75b69079904e4e",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-python": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1707114737,
"narHash": "sha256-ZXqv2epXAjDjfWbYn+yy4VOmW+C7SuUBoiZkkDoSqA4=",
"owner": "cachix",
"repo": "nixpkgs-python",
"rev": "f34ed02276bc08fe1c91c1bf0ef3589d68028878",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "nixpkgs-python",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1704874635,
"narHash": "sha256-YWuCrtsty5vVZvu+7BchAxmcYzTMfolSPP5io8+WYCg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3dc440faeee9e889fe2d1b4d25ad0f430d449356",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1707347730,
"narHash": "sha256-0etC/exQIaqC9vliKhc3eZE2Mm2wgLa0tj93ZF/egvM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6832d0d99649db3d65a0e15fa51471537b2c56a6",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": "flake-compat_2",
"flake-utils": "flake-utils_2",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1707297608,
"narHash": "sha256-ADjo/5VySGlvtCW3qR+vdFF4xM9kJFlRDqcC9ZGI8EA=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "0db2e67ee49910adfa13010e7f012149660af7f0",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"nixpkgs": "nixpkgs",
"nixpkgs-python": "nixpkgs-python",
"pre-commit-hooks": "pre-commit-hooks"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

19
devenv.nix Normal file
View File

@ -0,0 +1,19 @@
{ pkgs, ... }:
{
languages.python.enable = true;
languages.python.version = "3.12";
services.postgres = {
enable = true;
package = pkgs.postgresql_15;
initialDatabases = [ {"name" = "postgres";} ];
listen_addresses = "localhost";
initialScript = "create user postgres with password 'postgres' superuser;";
};
processes = {
app.exec = "while ! pg_isready -d postgres -h localhost -U postgres 2>/dev/null; do sleep 1; done; hatch run server";
};
}

5
devenv.yaml Normal file
View File

@ -0,0 +1,5 @@
inputs:
nixpkgs:
url: github:NixOS/nixpkgs/nixpkgs-unstable
nixpkgs-python:
url: github:cachix/nixpkgs-python

View File

@ -15,7 +15,7 @@ services:
depends_on:
- postgres
env_file:
- env
- .env
postgres:
image: postgres:13-alpine
@ -24,7 +24,7 @@ services:
ports:
- 5432:5432
env_file:
- env
- .env
volumes:
postgres_data:

130
pyproject.toml Normal file
View File

@ -0,0 +1,130 @@
[build-system]
requires = ["hatchling"]
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.6",
"django-money==3.4.1",
"django-allauth==0.63.3",
"psycopg[binary]==3.1.19",
"environs[django]==11.0.0",
"uvicorn==0.30.0",
"whitenoise==6.6.0",
"django-zen-queries==2.1.0",
"django-registries==0.0.3",
"django-view-decorator==0.0.4",
"django-oauth-toolkit==2.4.0",
]
version = "0.0.1"
[tool.hatch.build.targets.wheel]
packages = ["src"]
[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 0.0.0.0:8000"
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
[tool.ruff]
target-version = "py312"
extend-exclude = [
".git",
"__pycache__",
]
line-length = 120
[tool.ruff.lint]
select = ["ALL"]
ignore = [
"G004", # Logging statement uses f-string
"ANN101", # Missing type annotation for `self` in method
"ANN102", # Missing type annotation for `cls` in classmethod
"EM101", # Exception must not use a string literal, assign to variable first
"EM102", # Exception must not use a f-string literal, assign to variable first
"COM812", # missing-trailing-comma (https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules)
"ISC001", # single-line-implicit-string-concatenation (https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules)
]
[tool.ruff.lint.isort]
force-single-line = true

View File

@ -1,8 +1,8 @@
Django==4.1.5
django-money==1.3
django-allauth==0.46
psycopg2-binary==2.9.5
environs[django]==9.3
uvicorn==0.13
whitenoise==5.2
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,6 @@ from django.db import models
class Migration(migrations.Migration):
initial = True
dependencies = [
@ -99,9 +98,7 @@ class Migration(migrations.Migration):
),
(
"vat",
djmoney.models.fields.MoneyField(
decimal_places=2, max_digits=16, verbose_name="VAT"
),
djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16, verbose_name="VAT"),
),
("is_paid", models.BooleanField(default=False, verbose_name="is paid")),
(

View File

@ -0,0 +1,41 @@
# Generated by Django 5.0.1 on 2024-01-14 11:14
import djmoney.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("accounting", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="order",
name="price_currency",
field=djmoney.models.fields.CurrencyField(
choices=[("DKK", "DKK")], default=None, editable=False, max_length=3
),
),
migrations.AlterField(
model_name="order",
name="vat_currency",
field=djmoney.models.fields.CurrencyField(
choices=[("DKK", "DKK")], default=None, editable=False, max_length=3
),
),
migrations.AlterField(
model_name="payment",
name="amount_currency",
field=djmoney.models.fields.CurrencyField(
choices=[("DKK", "DKK")], default=None, editable=False, max_length=3
),
),
migrations.AlterField(
model_name="transaction",
name="amount_currency",
field=djmoney.models.fields.CurrencyField(
choices=[("DKK", "DKK")], default=None, editable=False, max_length=3
),
),
]

View File

@ -9,7 +9,6 @@ from djmoney.models.fields import MoneyField
class CreatedModifiedAbstract(models.Model):
modified = models.DateTimeField(auto_now=True, verbose_name=_("modified"))
created = models.DateTimeField(auto_now_add=True, verbose_name=_("created"))
@ -18,8 +17,7 @@ class CreatedModifiedAbstract(models.Model):
class Account(CreatedModifiedAbstract):
"""
This is the model where we can give access to several users, such that they
"""This is the model where we can give access to several users, such that they
can decide which account to use to pay for something.
"""
@ -31,8 +29,7 @@ class Account(CreatedModifiedAbstract):
class Transaction(CreatedModifiedAbstract):
"""
Tracks in and outgoing events of an account. When an order is received, an
"""Tracks in and outgoing events of an account. When an order is received, an
amount is subtracted, when a payment is received, an amount is added.
"""
@ -51,8 +48,7 @@ class Transaction(CreatedModifiedAbstract):
class Order(CreatedModifiedAbstract):
"""
Scoped out: Contents of invoices will have to be tracked either here or in
"""Scoped out: Contents of invoices will have to be tracked either here or in
a separate Invoice model. This is undecided because we are not generating
invoices at the moment.
"""
@ -92,12 +88,11 @@ class Order(CreatedModifiedAbstract):
verbose_name = pgettext_lazy("accounting term", "Order")
verbose_name_plural = pgettext_lazy("accounting term", "Orders")
def __str__(self):
def __str__(self) -> str:
return f"Order ID {self.display_id}"
class Payment(CreatedModifiedAbstract):
amount = MoneyField(max_digits=16, decimal_places=2)
order = models.ForeignKey(Order, on_delete=models.PROTECT)
@ -118,7 +113,7 @@ class Payment(CreatedModifiedAbstract):
description=order.description,
)
def __str__(self):
def __str__(self) -> str:
return f"Payment ID {self.display_id}"
class Meta:

View File

@ -8,8 +8,8 @@ from . import models
# do stuff
@pytest.mark.django_db
def test_balance():
@pytest.mark.django_db()
def test_balance() -> None:
user = User.objects.create_user("test", "lala@adas.com", "1234")
account = models.Account.objects.create(owner=user)
assert account.balance == 0

View File

@ -1,5 +1,4 @@
"""
Membership application
"""Membership application.
======================
This application's domain relate to organizational structures and

View File

@ -5,7 +5,7 @@ from django.db.models.signals import post_migrate
class MembershipConfig(AppConfig):
name = "membership"
def ready(self):
def ready(self) -> None:
from .permissions import persist_permissions
post_migrate.connect(persist_permissions, sender=self)

View File

@ -7,7 +7,6 @@ from django.db import models
class Migration(migrations.Migration):
initial = True
dependencies = [

View File

@ -2,11 +2,11 @@
import django.contrib.postgres.constraints
import django.contrib.postgres.fields.ranges
from django.db import migrations, models
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("membership", "0001_initial"),
]
@ -34,9 +34,7 @@ class Migration(migrations.Migration):
),
(
"period",
django.contrib.postgres.fields.ranges.DateRangeField(
verbose_name="period"
),
django.contrib.postgres.fields.ranges.DateRangeField(verbose_name="period"),
),
],
),

View File

@ -1,11 +1,11 @@
# Generated by Django 4.1 on 2023-01-02 21:05
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("membership", "0002_subscriptionperiod_remove_membership_period_and_more"),
]

View File

@ -1,11 +1,11 @@
# Generated by Django 4.1 on 2023-01-02 21:06
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("membership", "0003_membership_period"),
]

View File

@ -0,0 +1,23 @@
# 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

@ -5,20 +5,24 @@ from django.contrib.postgres.fields import RangeOperators
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext as _
from utils.mixins import CreatedModifiedAbstract
class Member(User):
class QuerySet(models.QuerySet):
def annotate_membership(self):
from membership.selectors import get_current_subscription_period
from .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(
active_membership=models.Exists(
Membership.objects.filter(
user=models.OuterRef("pk"),
period=get_current_subscription_period().id,
period=current_subscription_period.id,
),
),
)
@ -30,9 +34,7 @@ class Member(User):
class SubscriptionPeriod(CreatedModifiedAbstract):
"""
Denotes a period for which members should pay their membership fee for.
"""
"""Denotes a period for which members should pay their membership fee for."""
period = DateRangeField(verbose_name=_("period"))
@ -46,16 +48,12 @@ class SubscriptionPeriod(CreatedModifiedAbstract):
),
]
def __str__(self):
return (
f"{self.period.lower} - {self.period.upper or _('next general assembly')}"
)
def __str__(self) -> str:
return f"{self.period.lower} - {self.period.upper or _('next general assembly')}"
class Membership(CreatedModifiedAbstract):
"""
Tracks that a user has membership of a given type for a given period.
"""
"""Tracks that a user has membership of a given type for a given period."""
class QuerySet(models.QuerySet):
def for_member(self, member: Member):
@ -96,13 +94,12 @@ class Membership(CreatedModifiedAbstract):
on_delete=models.PROTECT,
)
def __str__(self):
def __str__(self) -> str:
return f"{self.user} - {self.period}"
class MembershipType(CreatedModifiedAbstract):
"""
Models membership types. Currently only a name, but will in the future
"""Models membership types. Currently only a name, but will in the future
possibly contain more information like fees.
"""
@ -112,5 +109,5 @@ class MembershipType(CreatedModifiedAbstract):
name = models.CharField(verbose_name=_("name"), max_length=64)
def __str__(self):
def __str__(self) -> str:
return self.name

View File

@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _
PERMISSIONS = []
def persist_permissions(sender, **kwargs):
def persist_permissions(sender, **kwargs) -> None:
for permission in PERMISSIONS:
permission.persist_permission()
@ -23,11 +23,10 @@ class Permission:
PERMISSIONS.append(self)
@property
def path(self):
def path(self) -> str:
return f"{self.app_label}.{self.codename}"
def persist_permission(self):
def persist_permission(self) -> None:
content_type, _ = ContentType.objects.get_or_create(
app_label=self.app_label,
model=self.model,

View File

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

View File

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

View File

@ -1,8 +1,14 @@
{% extends "base.html" %}
{% load i18n %}
{% block head_title %}
{% trans "Membership" %}
{% endblock %}
{% block content %}
<div class="content-view">
<h2>Membership settings</h2>
{% if not current_membership %}
<p>{% trans "You do not have an active membership!" %}</p>
@ -20,4 +26,38 @@
<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>
{% endif %}
{% endblock %}
</div>
<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,25 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.decorators import permission_required
from django.utils.translation import gettext_lazy as _
from zen_queries import render
from django_view_decorator import namespaced_decorator_factory
from utils.view_utils import RowAction
from utils.view_utils import render
from utils.view_utils import render_list
from .permissions import ADMINISTRATE_MEMBERS
from .selectors import get_member
from .selectors import get_members
from .selectors import get_memberships
from .selectors import get_subscription_periods
from utils.view_utils import render_list
from utils.view_utils import RowAction
member_view = namespaced_decorator_factory(namespace="member", base_path="membership")
@login_required
@member_view(
paths="",
name="membership-overview",
login_required=True,
)
def membership_overview(request):
memberships = get_memberships(user=request.user)
memberships = get_memberships(member=request.user)
current_membership = memberships.current()
previous_memberships = memberships.previous()
@ -33,12 +38,24 @@ def membership_overview(request):
)
@login_required
@permission_required(ADMINISTRATE_MEMBERS.path)
admin_members_view = namespaced_decorator_factory(
namespace="admin-members",
base_path="admin",
)
@admin_members_view(
paths="members/",
name="list",
login_required=True,
permissions=[ADMINISTRATE_MEMBERS.path],
)
def members_admin(request):
users = get_members()
return render_list(
entity_name="member",
entity_name_plural="members",
request=request,
paginate_by=20,
objects=users,
@ -52,15 +69,19 @@ def members_admin(request):
row_actions=[
RowAction(
label=_("View"),
url_name="admin-members-detail",
url_name="admin-members:detail",
url_kwargs={"member_id": "id"},
),
],
)
@login_required
@permission_required(ADMINISTRATE_MEMBERS.path)
@admin_members_view(
paths="<int:member_id>/",
name="detail",
login_required=True,
permissions=[ADMINISTRATE_MEMBERS.path],
)
def members_admin_detail(request, member_id):
member = get_member(member_id=member_id)
subscription_periods = get_subscription_periods(member=member)
@ -68,6 +89,7 @@ def members_admin_detail(request, member_id):
context = {
"member": member,
"subscription_periods": subscription_periods,
"base_path": "admin-members:list",
}
return render(

View File

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

View File

@ -40,6 +40,7 @@ DJANGO_APPS = [
THIRD_PARTY_APPS = [
"allauth",
"allauth.account",
"django_view_decorator",
]
LOCAL_APPS = [
@ -65,6 +66,7 @@ MIDDLEWARE = [
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"allauth.account.middleware.AccountMiddleware",
]
ROOT_URLCONF = "project.urls"
@ -80,7 +82,9 @@ TEMPLATES = [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"project.context_processors.current_site",
],
"builtins": [
"django.templatetags.i18n",
],
},
},
@ -150,6 +154,26 @@ ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False
ACCOUNT_USERNAME_REQUIRED = False
# Logging
# We want to log everything to stdout in docker
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
},
},
"loggers": {
"": {
"handlers": ["console"],
"level": "DEBUG",
},
},
}
if DEBUG:
INSTALLED_APPS += ["debug_toolbar", "django_browser_reload"]
MIDDLEWARE += [

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,67 @@
html.dark body {
--splash: #5b47e0;
background: var(--dark-dark);
color: var(--medium-dust)
}
html.dark h1,
html.dark h2,
html.dark h3,
html.dark h4,
html.dark h5,
html.dark h6,
html.dark footer,
html.dark nav ol li a {
color: var(--medium-dust);
}
html.dark nav ol li a:not(.current):hover {
border-color: var(--medium-dust);
}
html.dark header,
html.dark main aside,
html.dark nav {
background: #1d1d1d;
}
html.dark nav {
border: none;
}
html.dark hr {
border-color: var(--twilight);
}
html.dark main aside div,
html.dark article div.content-view {
background: var(--dark-twilight);
}
html.dark article table tbody {
background: var(--dark-twilight);
}
html.dark article table tbody tr:nth-child(2n+1) {
background: var(--dark);
}
html.dark article table tbody tr:nth-child(2n+1) td {
border-top: 1px solid var(--dark-dark);
border-bottom: 1px solid var(--dark-dark);
}
html.dark article table tbody tr:last-child td {
border-bottom: var(--half-space) solid var(--twilight);
}
html.dark form>div>input[type="text"],
html.dark form>div>input[type="password"],
html.dark input[type="email"] {
border: 2px solid var(--twilight);
border-radius: 6px;
padding: 8px;
background: var(--dark-dark);
width: 100%;
color: var(--light-dust);
}

View File

@ -0,0 +1,559 @@
/* 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: #ffffff;
--light-dust: #fefef9;
--dust: #f4f1ef;
--medium-dust: #dadada;
--dark-dust: #bfbfbf;
--fade: #878787;
--twilight: #4a4a4a;
--dark-twilight: #2f2f2f;
--dark: #2a2a2a;
--dark-dark: #121212;
--light-custard: #eee7d5;
--custard: #f0dcac;
--dark-custard: #d4c7a9;
--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%;
font-size: 1.05em;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 600;
color: var(--twilight);
}
a {
font-weight: 500;
color: var(--splash);
text-decoration: none;
cursor: pointer;
}
hr {
margin: var(--double-space) 0;
height: 0;
border: 0;
border-bottom: 1px solid var(--dark-custard);
}
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;
}
#switch-icon {
width: 30px;
height: 30px;
display: inline-block;
vertical-align: middle;
margin: 0 var(--space);
top: -2px;
position: relative;
}
#switch-icon #layer1 path {
fill: var(--twilight);
}
header>div>a#logout {
padding: 6px 12px;
border-radius: 6px;
background: var(--twilight);
text-decoration: none;
color: var(--dust);
transition: background 0.2s;
}
header>div>a#logout:hover {
background: var(--splash);
color: var(--light);
}
aside {
padding: 0 var(--outer-space) 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-custard);
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.content-view {
background: var(--dust);
padding: var(--double-space);
margin-bottom: var(--space);
}
div.content-view>h2 {
margin: 0 0 var(--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: var(--quarter-space);
opacity: 0.85;
cursor: pointer;
text-align: center;
border: 0;
font-weight: 600;
text-decoration: none;
transition: opacity 0.15s;
}
button.small {
font-size: 0.78em;
padding: var(--half-space) var(--space);
}
div.services>div>a:hover,
a.button:hover,
button:hover {
opacity: 1.0;
}
button:disabled {
opacity: 0.6;
background: var(--twilight);
cursor: default;
}
button.secondary {
background: var(--twilight);
}
article table {
width: 100%;
border-spacing: 0;
margin: var(--space) 0;
}
article table thead th {
background: var(--twilight);
color: var(--medium-dust);
}
article table thead th a {
color: var(--light);
}
article table thead th:first-child {
border-radius: var(--half-space) 0 0 0;
}
article table thead th:last-child {
border-radius: 0 var(--half-space) 0 0;
}
article table tbody {
background: var(--light-dust);
}
article table tbody tr:nth-child(odd) {
background: var(--light-custard);
}
article table tbody tr:nth-child(odd) td {
border-top: 1px solid var(--dark-custard);
border-bottom: 1px solid var(--dark-custard);
}
article table tbody tr:last-child td {
border-bottom: var(--half-space) solid var(--twilight);
}
article table thead th,
article table tbody td {
padding: var(--space);
text-align: left;
}
article table#user_email_table tbody tr td:first-child {
text-align: center;
}
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"],
input[type="email"] {
border: 2px solid var(--twilight);
border-radius: 6px;
padding: 8px;
background: var(--light-dust);
width: 100%;
color: var(--dark);
}
form fieldset {
border: 0;
padding: 0;
margin: 0;
}
form div.buttonHolder button {
display: inline-block;
}
#email-add-overlay {
display: none;
position: fixed;
left: 0;
top: 0;
bottom: 0;
right: 0;
background: rgba(0, 0, 0, 0.8);
align-items: center;
justify-content: center;
z-index: 1000;
}
#email-add-overlay .content-view {
width: 600px;
padding: var(--double-space);
}
#email-add-overlay .content-view p {
margin: var(--double-space) 0;
}
#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: var(--half-space) 0;
margin: 0;
}
.pagination>li {
margin: 0 var(--half-space);
}
.pagination>li:first-child {
margin-right: var(--double-space);
}
.pagination>li:last-child {
margin-left: var(--double-space);
}
.pagination .page-item {
border: 1px solid var(--fade);
padding: var(--quarter-space) var(--half-space);
border-radius: var(--half-space);
background: var(--light-dust);
font-size: 0.78em;
}
.pagination .page-link {
padding: var(--half-space);
color: var(--twilight);
}
.pagination .page-item.active {
background: var(--twilight);
}
.pagination .page-item.active .page-link {
color: var(--light-dust);
font-weight: bold;
}
.pagination .page-item.disabled {
opacity: 0.6;
}
.pagination .page-item.disabled .page-link {
cursor: default;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,200 @@
@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");
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,114 +2,95 @@
{% load i18n %}
{% block head_title %}{% trans "E-mail Addresses" %}{% endblock %}
{% block head_title %}{% trans "Email Addresses" %}{% endblock %}
{% block content %}
<div class="content-view">
<h2>{% trans "Email Addresses" %}</h2>
<p>{% trans 'The following email addresses are associated with your account:' %}</p>
<div class="row">
<div class="col-md-12">
<hr />
<div class="panel panel-default">
<div class="panel-heading">
<h4>{% trans "E-mail Addresses" %}</h4>
</div>
<div class="panel-body">
{% if user.emailaddress_set.all %}
<form action="{% url 'account_email' %}" class="email_list" method="post">
{% csrf_token %}
<fieldset class="blockLabels">
<div class="buttonHolder">
<button class="small" name="action_add_open" style="float:right">Add Email …</button>
<button class="small" disabled type="submit" id="action_primary" name="action_primary">Make Primary</button>
<button class="small" type="submit" name="action_send">Re-send Verification</button>
<button class="small" type="submit" name="action_remove">Remove</button>
</div>
<table class="table" id="user_email_table">
<thead>
<tr>
<th></th>
<th>{% trans "Address" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Primary" %}</th>
</tr>
</thead>
<tbody>
{% for emailaddress in user.emailaddress_set.all %}
<tr>
<label for="email_radio_{{ forloop.counter }}"
class="{% if emailaddress.primary %}primary_email{% endif %}">
<td>
<input
id="email_radio_{{ forloop.counter }}"
type="radio"
name="email"
value="{{ emailaddress.email }}"
{% if emailaddress.primary or user.emailaddress_set.count == 1 %}
checked="checked"
{% endif %}
class="{% if emailaddress.primary %}primary_email{% endif %}"
/>
</td>
<td>
{{ emailaddress.email }}
</td>
<td>
{% if emailaddress.verified %}
<span class="label label-success">{% trans "Verified" %}</span>
{% else %}
<span class="label label-danger">{% trans "Unverified" %}</span>
{% endif %}
</td>
<td>
{% if emailaddress.primary %}
<span class="label label-primary">{% trans "Primary" %}</span>
{% endif %}
</td>
</label>
</tr>
{% endfor %}
{% if user.emailaddress_set.all %}
<p>{% trans 'The following e-mail addresses are associated with your account:' %}</p>
<form action="{% url 'account_email' %}" class="email_list"
method="post">
{% csrf_token %}
<fieldset class="blockLabels">
<table class="table">
<thead>
<tr>
<th></th>
<th>{% trans "Address" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Primary" %}</th>
</tr>
</thead>
<tbody>
{% for emailaddress in user.emailaddress_set.all %}
<tr class="ctrlHolder">
<label for="email_radio_{{ forloop.counter }}"
class="{% if emailaddress.primary %}primary_email{% endif %}">
<td>
<input
id="email_radio_{{ forloop.counter }}"
type="radio"
name="email"
value="{{ emailaddress.email }}"
{% if emailaddress.primary or user.emailaddress_set.count == 1 %}
checked="checked"
{% endif %}
/>
</td>
<td>
{{ emailaddress.email }}
</td>
<td>
{% if emailaddress.verified %}
<span class="label label-success">Verified</span>
{% else %}
<span class="label label-danger">Unverified</span>
{% endif %}
</td>
<td>
{% if emailaddress.primary %}
<span class="label label-primary">Primary</span>{% endif %}
</td>
</label>
</tr>
{% endfor %}
</tbody>
</table>
<div class="buttonHolder">
<button class="btn btn-success" type="submit"
name="action_primary">Make Primary
</button>
<button class="btn btn-primary" type="submit"
name="action_send">Re-send Verification
</button>
<button class="btn btn-danger" type="submit"
name="action_remove">Remove
</button>
</div>
</fieldset>
</form>
{% else %}
<p>
<strong>{% trans 'Warning:' %}</strong>
{% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}
</p>
{% endif %}
</div>
</tbody>
</table>
</fieldset>
</form>
{% else %}
<p>
<strong>{% trans 'Warning:' %}</strong>
{% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}
</p>
{% endif %}
</div>
<div id="email-add-overlay">
<div class="content-view">
<h3>{% trans "Add E-mail" %}</h3>
<div class="panel-body">
<form method="post" action="{% url 'account_email' %}"
class="add_email">
{% csrf_token %}
{{ form.as_p }}
<button name="action_add" style="float:right" type="submit">
{% trans "Add E-mail" %}
</button>
<button id="overlay-close-button" class="secondary">Cancel</button>
</form>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4>{% trans "Add E-mail" %}</h4>
</div>
<div class="panel-body">
<form method="post" action="{% url 'account_email' %}"
class="add_email">
{% csrf_token %}
{{ form.as_p }}
<button name="action_add" class="btn btn-success" type="submit">
{% trans "Add E-mail" %}
</button>
</form>
</div>
</div>
</div>
</div>
@ -124,6 +105,33 @@
}
});
}
let addEmail = document.getElementsByName('action_add_open')[0];
addEmail.addEventListener('click', function(e) {
e.preventDefault();
let overlay = document.getElementById('email-add-overlay')
overlay.style.display = 'flex'
window.addEventListener('keydown', function(e) {
if (e.key == 'Escape') {
overlay.style.display = 'none'
}
})
document.getElementById('overlay-close-button').addEventListener('click', function() {
overlay.style.display = 'none'
})
})
let radio_actions = document.getElementsByName('email');
if (radio_actions.length) {
for (radio of radio_actions) {
radio.addEventListener("change", function (e) {
document.getElementById('action_primary').disabled = e.target.classList.contains('primary_email')
});
}
}
})();
</script>

View File

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

View File

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

View File

@ -7,71 +7,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<title>Login {{ site.name }}</title>
<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>
<link rel="stylesheet" href="{% static "/fonts/inter.css" %}">
<link rel="stylesheet" href="{% static "/css/style.css" %}">
</head>
<body class="text-center">
<main class="form-signin">
<img class="mb-4" src="https://new.data.coop/static/img/logo_da.svg" alt=""
width="260" height="160">
{% block non_login_content %}
{% endblock %}
</main>
<body>
<main id="login">
{% block non_login_content %}
{% endblock %}
</main>
</body>
</html>

View File

@ -3,211 +3,119 @@
{% load static %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<title>{% block head_title %}{% endblock %} {{ site.name }}</title>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<title>{% block head_title %}{% endblock %} {{ site.name }}</title>
<link rel="stylesheet" href="{% static "fonts/inter.css" %}">
<link rel="stylesheet" href="{% static "css/style.css" %}">
<link rel="stylesheet" href="{% static "css/dark-style.css" %}">
<script>
const savedTheme = localStorage.getItem('theme');
<link href="{% static "/css/bootstrap.min.css" %}" rel="stylesheet"
crossorigin="anonymous">
<link href="{% static "/css/bootstrap-icons.css" %}" rel="stylesheet"
crossorigin="anonymous">
<style>
body {
font-size: .875rem;
}
/*
* Sidebar
*/
.sidebar {
position: fixed;
top: 0;
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>
</li>
</ul>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>{% trans "Profile" %}</span>
</h6>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="#">
{% trans "Details" %}
</a>
</li>
<li class="nav-item">
<a class="nav-link {% active_path "account_email" "active" %}"
aria-current="page" href="{% url "account_email" %}">
{% trans "Emails" %}
</a>
</li>
</ul>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>{% trans "Membership" %}</span>
</h6>
<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>
</li>
</ul>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>{% trans "Services" %}</span>
</h6>
<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 %}
if (savedTheme === "dark" || (savedTheme == null && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
document.querySelector('html').classList.add('dark');
}
</script>
</head>
<body>
<header>
<h1> data.coop membersystem </h1>
<div>
<a id="theme-switcher" title="Switch theme">
<svg id="switch-icon"
width="20.00033mm"
height="20.000334mm"
viewBox="0 0 20.00033 20.000334"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g id="layer1">
<path id="path1"
style="fill-opacity:1;stroke:none;stroke-width:0.697782;stroke-linecap:round"
d="M 9.999906,20.000334 A 10,10 0 0 0 20.000329,9.999911 10,10 0 0 0 9.999906,0 10,10 0 0 0 0,9.999911 10,10 0 0 0 9.999906,20.000334 Z m 0,-2.00039 V 1.99988 a 8,8 0 0 1 8.000023,8.000031 8,8 0 0 1 -8.000023,8.000033 z" />
</g>
</svg>
</a>
<a id="logout" href="{% url "account_logout" %}">Log out</a>
</div>
</nav>
</header>
<main>
<aside>
<div>
<figure></figure>
<h2>{{ user }}</h2>
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 p-4">
{% block content %}
{% endblock %}
{% if current_membership %}
<dl>
<dt>Membership</dt>
<dd>
Active
</dd>
<dt>Period</dt>
<dd>
Until {{ current_period.upper }} <span class="time_remaining">({{ current_period.upper|timeuntil }})</span>
</dd>
<dt>Membership type</dt>
<dd>Normal member</dd>
</dl>
{% else %}
Your membership status will be displayed here in the future.
{% endif %}
</div>
</aside>
<nav>
<ol>
<li>
<a href="/" class="{% active_path "index" "current" %}">
Dashboard
</a>
</li>
{% comment %}
<li>
<a href="/services" class="{% active_path "services" "current" %}">
Services
</a>
</li>
{% endcomment %}
<li>
<a href="{% url "account_email" %}" class="{% active_path "account_email" "current" %}">
Email
</a>
</li>
{% if perms.membership.administrate_memberships %}
<li>
<a href="{% url "admin-members:list" %}" class="{% active_path "admin-members:list" "current" %}">
Admin
</a>
</li>
{% endif %}
</ol>
</nav>
<article>
{% block content %}{% endblock %}
</article>
</main>
</div>
</div>
</body>
<footer>
data.coop membersystem version 0.0.1
</footer>
<script>
const themeSwitcher = document.getElementById('theme-switcher');
themeSwitcher.addEventListener('click', function() {
themeSwitcher.classList.toggle('active')
let isDark = document.querySelector('html').classList.toggle('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
</script>
</body>
</html>

View File

@ -1 +1,28 @@
{% 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,10 +1,62 @@
{% extends "base.html" %}
{% block content %}
<p>
Services and signup to these will be
</p>
<p>
This is yet to be implemented.
</p>
<div class="content-view">
Coming soon!
</div>
{% comment %}
<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 %}

View File

@ -1,26 +1,12 @@
"""URLs for the membersystem"""
"""URLs for the membersystem."""
from django.conf import settings
from django.contrib import admin
from django.contrib.auth.decorators import login_required
from django.urls import include
from django.urls import path
from .views import index
from .views import services_overview
from membership.views import members_admin
from membership.views import members_admin_detail
from membership.views import membership_overview
from django_view_decorator import include_view_urls
urlpatterns = [
path("", login_required(index), name="index"),
path("services/", login_required(services_overview), name="services-overview"),
path("membership/", membership_overview, name="membership-overview"),
path("admin/members/", members_admin, name="admin-members"),
path(
"admin/members/<int:member_id>/",
members_admin_detail,
name="admin-members-detail",
),
path("", include_view_urls(extra_modules=["project.views"])),
path("accounts/", include("allauth.urls")),
path("_admin/", admin.site.urls),
]
@ -29,4 +15,5 @@ if settings.DEBUG:
urlpatterns = [
path("__debug__/", include("debug_toolbar.urls")),
path("__reload__/", include("django_browser_reload.urls")),
] + urlpatterns
*urlpatterns,
]

View File

@ -1,9 +1,20 @@
from django.shortcuts import render
from django_view_decorator import view
from utils.view_utils import render
@view(
paths="",
name="index",
login_required=True,
)
def index(request):
return render(request, "index.html")
@view(
paths="services/",
name="services",
login_required=True,
)
def services_overview(request):
return render(request, "services_overview.html")

View File

@ -1,5 +1,4 @@
"""
WSGI config for membersystem project.
"""WSGI config for membersystem project.
It exposes the WSGI callable as a module-level variable named ``application``.

View File

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

View File

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

View File

@ -5,9 +5,17 @@ register = template.Library()
@register.simple_tag(takes_context=True)
def active_path(context, path_name, class_name):
def active_path(context, path_name, class_name) -> str | None:
"""Return the given class name if the current path matches the given path name."""
path = reverse(path_name)
request_path = context.get("request").path
if path == request_path or ("basepath" in context and context["basepath"] == path):
# Check if the current path matches the given path name.
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 None

View File

@ -1,20 +1,24 @@
import contextlib
from dataclasses import dataclass
from typing import TYPE_CHECKING
from typing import Any
from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import FieldError
from django.core.paginator import Paginator
from django.db.models import Model
from django.http import HttpRequest
from django.http import HttpResponse
from django.urls import reverse
from zen_queries import render
from zen_queries import queries_disabled
from zen_queries import render as zen_queries_render
if TYPE_CHECKING:
from django.db.models import Model
@dataclass
class Row:
"""
A row in a table.
"""
"""A row in a table."""
data: dict[str, str]
actions: list[dict[str, str]]
@ -22,18 +26,14 @@ class Row:
@dataclass
class RowAction:
"""
An action that can be performed on a row in a table.
"""
"""An action that can be performed on a row in a table."""
label: str
url_name: str
url_kwargs: dict[str, str]
def render(self, obj) -> dict[str, str]:
"""
Render the action as a dictionary for the given object.
"""
"""Render the action as a dictionary for the given object."""
url = reverse(
self.url_name,
kwargs={key: getattr(obj, value) for key, value in self.url_kwargs.items()},
@ -43,16 +43,15 @@ class RowAction:
def render_list(
request: HttpRequest,
entity_name: str,
entity_name_plural: str,
objects: list["Model"],
columns: list[tuple[str, str]],
row_actions: list[RowAction] = None,
list_actions: list[tuple[str, str]] = None,
paginate_by: int = None,
row_actions: list[RowAction] | None = None,
list_actions: list[tuple[str, str]] | None = None,
paginate_by: int | None = None,
) -> HttpResponse:
"""
Render a list of objects with a table.
"""
"""Render a list of objects with a table."""
# TODO: List actions
total_count = len(objects)
@ -70,11 +69,12 @@ def render_list(
rows = []
for obj in objects:
row = Row(
data={column: getattr(obj, column[0]) for column in columns},
actions=[action.render(obj) for action in row_actions],
)
rows.append(row)
with queries_disabled():
row = Row(
data={column: getattr(obj, column[0]) for column in columns},
actions=[action.render(obj) for action in row_actions],
)
rows.append(row)
context = {
"rows": rows,
@ -83,6 +83,8 @@ def render_list(
"list_actions": list_actions,
"total_count": total_count,
"order_by": order_by,
"entity_name": entity_name,
"entity_name_plural": entity_name_plural,
}
if paginate_by:
@ -96,3 +98,21 @@ def render_list(
template_name="utils/list.html",
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
# Make sure to fetch all permissions before rendering the template
# otherwise django-zen-queries will complain about database queries.
request.user.get_all_permissions()
return zen_queries_render(request, template_name, context)