Compare commits
11 commits
ad6564b510
...
6e2ed21f67
Author | SHA1 | Date | |
---|---|---|---|
Víðir Valberg Guðmundsson | 6e2ed21f67 | ||
Víðir Valberg Guðmundsson | 4d0062f600 | ||
Víðir Valberg Guðmundsson | 0508d7f2a4 | ||
Víðir Valberg Guðmundsson | 78fb74160b | ||
Halfdan Mouritzen | cfe44d675e | ||
Víðir Valberg Guðmundsson | dae974660d | ||
Víðir Valberg Guðmundsson | 3d1b51f99b | ||
Víðir Valberg Guðmundsson | f18469833a | ||
Víðir Valberg Guðmundsson | 480eecca12 | ||
Víðir Valberg Guðmundsson | b39b114e30 | ||
Víðir Valberg Guðmundsson | d31f62ebb4 |
|
@ -3,6 +3,7 @@
|
||||||
*/.*
|
*/.*
|
||||||
|
|
||||||
!src/
|
!src/
|
||||||
|
!requirements.txt
|
||||||
!requirements/
|
!requirements/
|
||||||
!entrypoint.sh
|
!entrypoint.sh
|
||||||
!pyproject.toml
|
!pyproject.toml
|
||||||
|
|
|
@ -7,7 +7,6 @@ steps:
|
||||||
- name: docker
|
- name: docker
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
environment:
|
environment:
|
||||||
DJANGO_ENV: production
|
|
||||||
BUILD: "${DRONE_COMMIT_SHA}"
|
BUILD: "${DRONE_COMMIT_SHA}"
|
||||||
settings:
|
settings:
|
||||||
repo: docker.data.coop/membersystem
|
repo: docker.data.coop/membersystem
|
||||||
|
@ -17,7 +16,6 @@ steps:
|
||||||
password:
|
password:
|
||||||
from_secret: DOCKER_PASSWORD
|
from_secret: DOCKER_PASSWORD
|
||||||
build_args_from_env:
|
build_args_from_env:
|
||||||
- DJANGO_ENV
|
|
||||||
- BUILD
|
- BUILD
|
||||||
tags:
|
tags:
|
||||||
- "${DRONE_BUILD_NUMBER}"
|
- "${DRONE_BUILD_NUMBER}"
|
||||||
|
|
|
@ -6,4 +6,3 @@ DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres
|
||||||
# Use something along the the following if you are not using docker
|
# Use something along the the following if you are not using docker
|
||||||
# DATABASE_URL=postgres://postgres:postgres@localhost:5432/datacoop_membersystem
|
# DATABASE_URL=postgres://postgres:postgres@localhost:5432/datacoop_membersystem
|
||||||
DEBUG=True
|
DEBUG=True
|
||||||
DJANGO_ENV=development
|
|
||||||
|
|
|
@ -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.6.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-ast
|
- id: check-ast
|
||||||
- id: check-merge-conflict
|
- id: check-merge-conflict
|
||||||
|
@ -15,22 +15,22 @@ repos:
|
||||||
- id: check-toml
|
- id: check-toml
|
||||||
- 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/astral-sh/ruff-pre-commit
|
||||||
rev: 'v0.3.0'
|
rev: 'v0.5.2'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args:
|
args:
|
||||||
- --fix
|
- --fix
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v3.15.1
|
rev: v3.16.0
|
||||||
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.16.0
|
rev: 1.19.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: django-upgrade
|
- id: django-upgrade
|
||||||
args:
|
args:
|
||||||
|
|
16
Dockerfile
16
Dockerfile
|
@ -7,18 +7,18 @@ ENV PYTHONFAULTHANDLER=1 \
|
||||||
PIP_NO_CACHE_DIR=off \
|
PIP_NO_CACHE_DIR=off \
|
||||||
PIP_DISABLE_PIP_VERSION_CHECK=on \
|
PIP_DISABLE_PIP_VERSION_CHECK=on \
|
||||||
PIP_DEFAULT_TIMEOUT=100
|
PIP_DEFAULT_TIMEOUT=100
|
||||||
ARG DJANGO_ENV
|
|
||||||
ARG BUILD
|
ARG BUILD
|
||||||
ENV BUILD ${BUILD}
|
ENV BUILD ${BUILD}
|
||||||
|
ARG REQUIREMENTS_FILE=requirements.txt
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN groupadd -g 1000 www && useradd -u 1000 -ms /bin/bash -g www www
|
RUN groupadd -g 1000 www && useradd -u 1000 -ms /bin/bash -g www www
|
||||||
COPY --chown=www:www . .
|
COPY --chown=www:www . .
|
||||||
RUN mkdir /app/src/static \
|
RUN mkdir /app/src/static && \
|
||||||
&& chown www:www /app/src/static \
|
chown www:www /app/src/static && \
|
||||||
&& apt-get update \
|
apt-get update && \
|
||||||
&& apt-get install -y \
|
apt-get install -y \
|
||||||
binutils \
|
binutils \
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
build-essential \
|
build-essential \
|
||||||
|
@ -29,9 +29,9 @@ RUN mkdir /app/src/static \
|
||||||
libgdk-pixbuf2.0-0 \
|
libgdk-pixbuf2.0-0 \
|
||||||
libffi-dev \
|
libffi-dev \
|
||||||
shared-mime-info \
|
shared-mime-info \
|
||||||
gettext \
|
gettext && \
|
||||||
&& pip install . \
|
pip install --no-cache-dir -r $REQUIREMENTS_FILE && \
|
||||||
&& django-admin compilemessages
|
django-admin compilemessages
|
||||||
|
|
||||||
ENTRYPOINT ["./entrypoint.sh"]
|
ENTRYPOINT ["./entrypoint.sh"]
|
||||||
|
|
||||||
|
|
37
Makefile
37
Makefile
|
@ -1,27 +1,12 @@
|
||||||
DOCKER_COMPOSE = COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker compose
|
DOCKER_COMPOSE = COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 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
|
|
||||||
MANAGE_EXEC = python /app/src/manage.py
|
MANAGE_EXEC = python /app/src/manage.py
|
||||||
MANAGE_COMMAND = ${DOCKER_RUN} ${DOCKER_CONTAINER_NAME} ${MANAGE_EXEC}
|
MANAGE_COMMAND = ${DOCKER_RUN} app ${MANAGE_EXEC}
|
||||||
|
|
||||||
init: setup_venv pre_commit_install migrate
|
|
||||||
|
|
||||||
run:
|
run:
|
||||||
${DOCKER_COMPOSE} up
|
${DOCKER_COMPOSE} up
|
||||||
|
|
||||||
setup_venv:
|
|
||||||
rm -rf venv
|
|
||||||
python3.11 -m venv venv;
|
|
||||||
venv/bin/python -m pip install wheel setuptools;
|
|
||||||
venv/bin/python -m pip install pre-commit boto3 pip-tools;
|
|
||||||
|
|
||||||
pre_commit_install:
|
|
||||||
venv/bin/pre-commit install
|
|
||||||
|
|
||||||
pre_commit_run_all:
|
|
||||||
venv/bin/pre-commit run --all-files
|
|
||||||
|
|
||||||
makemigrations:
|
makemigrations:
|
||||||
${MANAGE_COMMAND} makemigrations ${ARGS}
|
${MANAGE_COMMAND} makemigrations ${ARGS}
|
||||||
|
|
||||||
|
@ -36,23 +21,3 @@ shell:
|
||||||
|
|
||||||
manage_command:
|
manage_command:
|
||||||
${MANAGE_COMMAND} ${ARGS}
|
${MANAGE_COMMAND} ${ARGS}
|
||||||
|
|
||||||
add_dependency:
|
|
||||||
${DOCKER_RUN} ${DOCKER_CONTAINER_NAME} poetry add --lock ${DEPENDENCY}
|
|
||||||
|
|
||||||
add_dev_dependency:
|
|
||||||
${DOCKER_RUN} ${DOCKER_CONTAINER_NAME} poetry add -D --lock ${DEPENDENCY}
|
|
||||||
|
|
||||||
poetry_lock:
|
|
||||||
${DOCKER_RUN} ${DOCKER_CONTAINER_NAME} poetry lock --no-update
|
|
||||||
|
|
||||||
poetry_command:
|
|
||||||
${DOCKER_RUN} ${DOCKER_CONTAINER_NAME} poetry ${COMMAND}
|
|
||||||
|
|
||||||
build_dev_docker_image: compile_requirements
|
|
||||||
${DOCKER_COMPOSE} build ${DOCKER_CONTAINER_NAME}
|
|
||||||
|
|
||||||
compile_requirements:
|
|
||||||
./venv/bin/pip-compile --output-file requirements/base.txt requirements/base.in
|
|
||||||
./venv/bin/pip-compile --output-file requirements/test.txt requirements/test.in
|
|
||||||
./venv/bin/pip-compile --output-file requirements/dev.txt requirements/dev.in
|
|
||||||
|
|
83
README.md
83
README.md
|
@ -1,71 +1,74 @@
|
||||||
# member.data.coop
|
# data.coop member system
|
||||||
|
|
||||||
## Development
|
## Development setup
|
||||||
|
|
||||||
### Setup environment
|
There are two ways to setup the development environment.
|
||||||
|
|
||||||
Copy over the .env.example file to .env and adjust DATABASE_URL accordingly
|
- Using the Docker Compose setup provided in this repository.
|
||||||
|
- Using [hatch](https://hatch.pypa.io/) in your host OS.
|
||||||
|
|
||||||
$ cp .env.example .env
|
|
||||||
|
|
||||||
### Docker
|
### Using Docker Compose
|
||||||
|
|
||||||
|
Working with the Docker Compose setup is made easy with the `Makefile` provided in the repository.
|
||||||
|
|
||||||
#### Requirements
|
#### Requirements
|
||||||
|
|
||||||
- Docker
|
- Docker
|
||||||
- Docker compose
|
- docker compose plugin
|
||||||
- pre-commit (preferred for contributions)
|
|
||||||
|
|
||||||
#### Setup
|
#### Setup
|
||||||
|
|
||||||
Given that the requirements above are installed, it should be as easy as:
|
1. Setup .env file
|
||||||
|
|
||||||
$ make migrate
|
An example .env file is provided in the repository. You can copy it to .env file using the following command:
|
||||||
|
|
||||||
This will setup the database. Next run:
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
$ make run
|
The default values in the .env file are suitable for the docker-compose setup.
|
||||||
|
|
||||||
This will build the docker image and start the member system on http://localhost:8000.
|
2. Migrate
|
||||||
|
|
||||||
You can create a superuser by running:
|
```bash
|
||||||
|
make migrate
|
||||||
|
```
|
||||||
|
|
||||||
$ make createsuperuser
|
3. Run the development server
|
||||||
|
|
||||||
Make migrations:
|
```bash
|
||||||
|
make run
|
||||||
|
```
|
||||||
|
|
||||||
$ make makemigrations
|
### Using hatch
|
||||||
|
|
||||||
Make messages:
|
#### Requirements
|
||||||
|
|
||||||
$ make makemessages
|
- Python 3.12 or higher
|
||||||
|
- [hatch](https://hatch.pypa.io/) (Recommended way to install is using `pipx install hatch`)
|
||||||
|
- A running PostgreSQL server
|
||||||
|
|
||||||
Running tests:
|
#### Setup
|
||||||
|
|
||||||
$ make test
|
1. Setup .env file
|
||||||
|
|
||||||
### Non-docker
|
An example .env file is provided in the repository. You can copy it to .env file using the following command:
|
||||||
|
|
||||||
Create a venv
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
$ python3 -m venv venv
|
Edit the .env file and set the values for the environment variables, especially the database variables.
|
||||||
|
|
||||||
Activate the venv
|
2. Run migrate
|
||||||
|
|
||||||
$ source venv/bin/activate
|
```bash
|
||||||
|
hatch run dev:migrate
|
||||||
|
```
|
||||||
|
|
||||||
Install requirements
|
3. Run the development server
|
||||||
|
|
||||||
$ pip install -r requirements/dev.txt
|
```bash
|
||||||
|
hatch run dev:server
|
||||||
Run migrations
|
```
|
||||||
|
|
||||||
$ ./src/manage.py migrate
|
|
||||||
|
|
||||||
Create a superuser
|
|
||||||
|
|
||||||
$ ./src/manage.py createsuperuser
|
|
||||||
|
|
||||||
Run the server
|
|
||||||
|
|
||||||
$ ./src/manage.py runserver
|
|
||||||
|
|
241
devenv.lock
241
devenv.lock
|
@ -1,241 +0,0 @@
|
||||||
{
|
|
||||||
"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
19
devenv.nix
|
@ -1,19 +0,0 @@
|
||||||
{ 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";
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
inputs:
|
|
||||||
nixpkgs:
|
|
||||||
url: github:NixOS/nixpkgs/nixpkgs-unstable
|
|
||||||
nixpkgs-python:
|
|
||||||
url: github:cachix/nixpkgs-python
|
|
|
@ -1,11 +1,10 @@
|
||||||
version: '3.7'
|
---
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
app:
|
||||||
backend:
|
|
||||||
image: data_coop_membersystem:dev
|
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
|
args:
|
||||||
|
- REQUIREMENTS_FILE=requirements/requirements-dev.txt
|
||||||
command: python /app/src/manage.py runserver 0.0.0.0:8000
|
command: python /app/src/manage.py runserver 0.0.0.0:8000
|
||||||
tty: true
|
tty: true
|
||||||
ports:
|
ports:
|
||||||
|
@ -28,3 +27,4 @@ services:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
...
|
||||||
|
|
|
@ -12,24 +12,31 @@ authors = [
|
||||||
{ name = "Víðir Valberg Guðmundsson", email = "valberg@orn.li" },
|
{ name = "Víðir Valberg Guðmundsson", email = "valberg@orn.li" },
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"Django==5.0.1",
|
"Django==5.0.6",
|
||||||
"django-money==3.4.1",
|
"django-money==3.4.1",
|
||||||
"django-allauth==0.60.0",
|
"django-allauth==0.63.3",
|
||||||
"psycopg[binary]==3.1.16",
|
"psycopg[binary]==3.1.19",
|
||||||
"environs[django]==10.0.0",
|
"environs[django]==11.0.0",
|
||||||
"uvicorn==0.25.0",
|
"uvicorn==0.30.0",
|
||||||
"whitenoise==6.6.0",
|
"whitenoise==6.6.0",
|
||||||
"django-zen-queries==2.1.0",
|
"django-zen-queries==2.1.0",
|
||||||
"django-registries==0.0.3",
|
"django-registries==0.0.3",
|
||||||
"django-view-decorator==0.0.4",
|
"django-view-decorator==0.0.4",
|
||||||
"django-oauth-toolkit==2.3.0",
|
"django-oauth-toolkit==2.4.0",
|
||||||
]
|
]
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["src"]
|
packages = ["src"]
|
||||||
|
|
||||||
|
[tool.hatch.env]
|
||||||
|
requires = ["hatch-pip-compile"]
|
||||||
|
|
||||||
[tool.hatch.envs.default]
|
[tool.hatch.envs.default]
|
||||||
|
type = "pip-compile"
|
||||||
|
|
||||||
|
[tool.hatch.envs.dev]
|
||||||
|
type = "pip-compile"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"coverage[toml]==7.3.0",
|
"coverage[toml]==7.3.0",
|
||||||
"pytest==7.2.2",
|
"pytest==7.2.2",
|
||||||
|
@ -45,7 +52,7 @@ dependencies = [
|
||||||
|
|
||||||
[[tool.hatch.envs.tests.matrix]]
|
[[tool.hatch.envs.tests.matrix]]
|
||||||
python = ["3.12"]
|
python = ["3.12"]
|
||||||
django = ["4.2", "5.0"]
|
django = ["5.0"]
|
||||||
|
|
||||||
[tool.hatch.envs.tests.overrides]
|
[tool.hatch.envs.tests.overrides]
|
||||||
matrix.django.dependencies = [
|
matrix.django.dependencies = [
|
||||||
|
@ -55,7 +62,7 @@ matrix.python.dependencies = [
|
||||||
{ value = "typing_extensions==4.5.0", if = ["3.10"]},
|
{ value = "typing_extensions==4.5.0", if = ["3.10"]},
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.hatch.envs.default.scripts]
|
[tool.hatch.envs.dev.scripts]
|
||||||
cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=src --cov=tests --cov=append {args}"
|
cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=src --cov=tests --cov=append {args}"
|
||||||
no-cov = "cov --no-cov {args}"
|
no-cov = "cov --no-cov {args}"
|
||||||
typecheck = "mypy --config-file=pyproject.toml ."
|
typecheck = "mypy --config-file=pyproject.toml ."
|
||||||
|
@ -111,6 +118,9 @@ target-version = "py312"
|
||||||
extend-exclude = [
|
extend-exclude = [
|
||||||
".git",
|
".git",
|
||||||
"__pycache__",
|
"__pycache__",
|
||||||
|
"manage.py",
|
||||||
|
"asgi.py",
|
||||||
|
"wsgi.py",
|
||||||
]
|
]
|
||||||
line-length = 120
|
line-length = 120
|
||||||
|
|
||||||
|
@ -124,7 +134,21 @@ ignore = [
|
||||||
"EM102", # Exception must not use a f-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)
|
"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)
|
"ISC001", # single-line-implicit-string-concatenation (https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules)
|
||||||
|
"D105", # Missing docstring in magic method
|
||||||
|
"D106", # Missing docstring in public nested class
|
||||||
|
"FIX", # TODO, FIXME, XXX
|
||||||
|
"TD", # TODO, FIXME, XXX
|
||||||
|
"ANN002", # Missing type annotation for `*args`
|
||||||
|
"ANN003", # Missing type annotation for `**kwargs`
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff.lint.isort]
|
[tool.ruff.lint.isort]
|
||||||
force-single-line = true
|
force-single-line = true
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
"tests.py" = [
|
||||||
|
"S101", # Use of assert
|
||||||
|
"SLF001", # Private member access
|
||||||
|
"D100", # Docstrings
|
||||||
|
"D103", # Docstrings
|
||||||
|
]
|
||||||
|
|
103
requirements.txt
Normal file
103
requirements.txt
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
#
|
||||||
|
# This file is autogenerated by hatch-pip-compile with Python 3.12
|
||||||
|
#
|
||||||
|
# - django-allauth==0.63.3
|
||||||
|
# - django-money==3.4.1
|
||||||
|
# - django-oauth-toolkit==2.4.0
|
||||||
|
# - django-registries==0.0.3
|
||||||
|
# - django-view-decorator==0.0.4
|
||||||
|
# - django-zen-queries==2.1.0
|
||||||
|
# - django==5.0.6
|
||||||
|
# - environs[django]==11.0.0
|
||||||
|
# - psycopg[binary]==3.1.19
|
||||||
|
# - uvicorn==0.30.0
|
||||||
|
# - whitenoise==6.6.0
|
||||||
|
#
|
||||||
|
|
||||||
|
asgiref==3.8.1
|
||||||
|
# via django
|
||||||
|
babel==2.15.0
|
||||||
|
# via py-moneyed
|
||||||
|
certifi==2024.7.4
|
||||||
|
# via requests
|
||||||
|
cffi==1.16.0
|
||||||
|
# via cryptography
|
||||||
|
charset-normalizer==3.3.2
|
||||||
|
# via requests
|
||||||
|
click==8.1.7
|
||||||
|
# via uvicorn
|
||||||
|
cryptography==42.0.8
|
||||||
|
# via jwcrypto
|
||||||
|
dj-database-url==2.2.0
|
||||||
|
# via environs
|
||||||
|
dj-email-url==1.0.6
|
||||||
|
# via environs
|
||||||
|
django==5.0.6
|
||||||
|
# via
|
||||||
|
# hatch.envs.default
|
||||||
|
# dj-database-url
|
||||||
|
# django-allauth
|
||||||
|
# django-money
|
||||||
|
# django-oauth-toolkit
|
||||||
|
# django-registries
|
||||||
|
# django-view-decorator
|
||||||
|
# django-zen-queries
|
||||||
|
django-allauth==0.63.3
|
||||||
|
# via hatch.envs.default
|
||||||
|
django-cache-url==3.4.5
|
||||||
|
# via environs
|
||||||
|
django-money==3.4.1
|
||||||
|
# via hatch.envs.default
|
||||||
|
django-oauth-toolkit==2.4.0
|
||||||
|
# via hatch.envs.default
|
||||||
|
django-registries==0.0.3
|
||||||
|
# via hatch.envs.default
|
||||||
|
django-view-decorator==0.0.4
|
||||||
|
# via hatch.envs.default
|
||||||
|
django-zen-queries==2.1.0
|
||||||
|
# via hatch.envs.default
|
||||||
|
environs==11.0.0
|
||||||
|
# via hatch.envs.default
|
||||||
|
h11==0.14.0
|
||||||
|
# via uvicorn
|
||||||
|
idna==3.7
|
||||||
|
# via requests
|
||||||
|
jwcrypto==1.5.6
|
||||||
|
# via django-oauth-toolkit
|
||||||
|
marshmallow==3.21.3
|
||||||
|
# via environs
|
||||||
|
oauthlib==3.2.2
|
||||||
|
# via django-oauth-toolkit
|
||||||
|
packaging==24.1
|
||||||
|
# via marshmallow
|
||||||
|
psycopg==3.1.19
|
||||||
|
# via hatch.envs.default
|
||||||
|
psycopg-binary==3.1.19
|
||||||
|
# via psycopg
|
||||||
|
py-moneyed==3.0
|
||||||
|
# via django-money
|
||||||
|
pycparser==2.22
|
||||||
|
# via cffi
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
# via environs
|
||||||
|
pytz==2024.1
|
||||||
|
# via django-oauth-toolkit
|
||||||
|
requests==2.32.3
|
||||||
|
# via django-oauth-toolkit
|
||||||
|
sqlparse==0.5.0
|
||||||
|
# via django
|
||||||
|
typing-extensions==4.12.2
|
||||||
|
# via
|
||||||
|
# dj-database-url
|
||||||
|
# jwcrypto
|
||||||
|
# psycopg
|
||||||
|
# py-moneyed
|
||||||
|
urllib3==2.2.2
|
||||||
|
# via requests
|
||||||
|
uvicorn==0.30.0
|
||||||
|
# via hatch.envs.default
|
||||||
|
whitenoise==6.6.0
|
||||||
|
# via hatch.envs.default
|
||||||
|
|
||||||
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
|
# setuptools
|
|
@ -1,8 +0,0 @@
|
||||||
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
|
|
|
@ -1,101 +0,0 @@
|
||||||
#
|
|
||||||
# This file is autogenerated by pip-compile with Python 3.11
|
|
||||||
# by the following command:
|
|
||||||
#
|
|
||||||
# pip-compile --output-file=requirements/base.txt pyproject.toml
|
|
||||||
#
|
|
||||||
asgiref==3.7.2
|
|
||||||
# via django
|
|
||||||
babel==2.14.0
|
|
||||||
# via py-moneyed
|
|
||||||
certifi==2023.11.17
|
|
||||||
# via requests
|
|
||||||
cffi==1.16.0
|
|
||||||
# via cryptography
|
|
||||||
charset-normalizer==3.3.2
|
|
||||||
# via requests
|
|
||||||
click==8.1.7
|
|
||||||
# via uvicorn
|
|
||||||
cryptography==41.0.7
|
|
||||||
# via pyjwt
|
|
||||||
defusedxml==0.7.1
|
|
||||||
# via python3-openid
|
|
||||||
dj-database-url==2.1.0
|
|
||||||
# via environs
|
|
||||||
dj-email-url==1.0.6
|
|
||||||
# via environs
|
|
||||||
django==5.0.1
|
|
||||||
# via
|
|
||||||
# dj-database-url
|
|
||||||
# django-allauth
|
|
||||||
# django-money
|
|
||||||
# django-registries
|
|
||||||
# django-view-decorator
|
|
||||||
# django-zen-queries
|
|
||||||
# membersystem (pyproject.toml)
|
|
||||||
django-allauth==0.60.0
|
|
||||||
# via membersystem (pyproject.toml)
|
|
||||||
django-cache-url==3.4.5
|
|
||||||
# via environs
|
|
||||||
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 membersystem (pyproject.toml)
|
|
||||||
environs[django]==10.0.0
|
|
||||||
# via
|
|
||||||
# environs
|
|
||||||
# membersystem (pyproject.toml)
|
|
||||||
h11==0.14.0
|
|
||||||
# via uvicorn
|
|
||||||
idna==3.6
|
|
||||||
# via requests
|
|
||||||
marshmallow==3.20.1
|
|
||||||
# via environs
|
|
||||||
oauthlib==3.2.2
|
|
||||||
# via requests-oauthlib
|
|
||||||
packaging==23.2
|
|
||||||
# via marshmallow
|
|
||||||
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.8.0
|
|
||||||
# via
|
|
||||||
# django-allauth
|
|
||||||
# pyjwt
|
|
||||||
python-dotenv==1.0.0
|
|
||||||
# via environs
|
|
||||||
python3-openid==3.2.0
|
|
||||||
# via django-allauth
|
|
||||||
requests==2.31.0
|
|
||||||
# via
|
|
||||||
# django-allauth
|
|
||||||
# requests-oauthlib
|
|
||||||
requests-oauthlib==1.3.1
|
|
||||||
# via django-allauth
|
|
||||||
sqlparse==0.4.4
|
|
||||||
# via django
|
|
||||||
typing-extensions==4.9.0
|
|
||||||
# via
|
|
||||||
# dj-database-url
|
|
||||||
# psycopg
|
|
||||||
# py-moneyed
|
|
||||||
urllib3==2.1.0
|
|
||||||
# via requests
|
|
||||||
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
|
|
|
@ -1,8 +0,0 @@
|
||||||
-r test.txt
|
|
||||||
|
|
||||||
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
|
|
|
@ -1,213 +0,0 @@
|
||||||
#
|
|
||||||
# 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.7.2
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# django
|
|
||||||
# django-browser-reload
|
|
||||||
asttokens==2.4.1
|
|
||||||
# via stack-data
|
|
||||||
babel==2.14.0
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# py-moneyed
|
|
||||||
certifi==2023.11.17
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# requests
|
|
||||||
cffi==1.16.0
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# cryptography
|
|
||||||
charset-normalizer==3.3.2
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# requests
|
|
||||||
click==8.1.7
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# uvicorn
|
|
||||||
coverage==7.4.0
|
|
||||||
# via -r requirements/test.txt
|
|
||||||
cryptography==41.0.7
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# pyjwt
|
|
||||||
decorator==5.1.1
|
|
||||||
# via ipython
|
|
||||||
defusedxml==0.7.1
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# python3-openid
|
|
||||||
dj-database-url==2.1.0
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# environs
|
|
||||||
dj-email-url==1.0.6
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# environs
|
|
||||||
django==5.0.1
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# dj-database-url
|
|
||||||
# django-allauth
|
|
||||||
# django-browser-reload
|
|
||||||
# django-debug-toolbar
|
|
||||||
# django-extensions
|
|
||||||
# django-money
|
|
||||||
# django-stubs
|
|
||||||
# django-stubs-ext
|
|
||||||
# django-zen-queries
|
|
||||||
django-allauth==0.60.0
|
|
||||||
# via -r requirements/test.txt
|
|
||||||
django-browser-reload==1.12.1
|
|
||||||
# via -r requirements/dev.in
|
|
||||||
django-cache-url==3.4.5
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# environs
|
|
||||||
django-debug-toolbar==4.2.0
|
|
||||||
# via -r requirements/dev.in
|
|
||||||
django-extensions==3.2.3
|
|
||||||
# via -r requirements/dev.in
|
|
||||||
django-money==3.4.1
|
|
||||||
# via -r requirements/test.txt
|
|
||||||
django-stubs==4.2.7
|
|
||||||
# via -r requirements/dev.in
|
|
||||||
django-stubs-ext==4.2.7
|
|
||||||
# via django-stubs
|
|
||||||
django-zen-queries==2.1.0
|
|
||||||
# via -r requirements/test.txt
|
|
||||||
environs[django]==10.0.0
|
|
||||||
# via -r requirements/test.txt
|
|
||||||
executing==2.0.1
|
|
||||||
# via stack-data
|
|
||||||
h11==0.14.0
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# uvicorn
|
|
||||||
idna==3.6
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# requests
|
|
||||||
ipython==8.19.0
|
|
||||||
# via -r requirements/dev.in
|
|
||||||
jedi==0.19.1
|
|
||||||
# via ipython
|
|
||||||
lxml==5.0.1
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# unittest-xml-reporting
|
|
||||||
marshmallow==3.20.1
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# environs
|
|
||||||
matplotlib-inline==0.1.6
|
|
||||||
# via ipython
|
|
||||||
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==23.2
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# marshmallow
|
|
||||||
parso==0.8.3
|
|
||||||
# via jedi
|
|
||||||
pexpect==4.9.0
|
|
||||||
# via ipython
|
|
||||||
prompt-toolkit==3.0.43
|
|
||||||
# via ipython
|
|
||||||
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==3.0
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# django-money
|
|
||||||
pycparser==2.21
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# cffi
|
|
||||||
pygments==2.17.2
|
|
||||||
# via ipython
|
|
||||||
pyjwt[crypto]==2.8.0
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# django-allauth
|
|
||||||
python-dotenv==1.0.0
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# environs
|
|
||||||
python3-openid==3.2.0
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# django-allauth
|
|
||||||
requests==2.31.0
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# django-allauth
|
|
||||||
# requests-oauthlib
|
|
||||||
requests-oauthlib==1.3.1
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# django-allauth
|
|
||||||
six==1.16.0
|
|
||||||
# via asttokens
|
|
||||||
sqlparse==0.4.4
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# django
|
|
||||||
# django-debug-toolbar
|
|
||||||
stack-data==0.6.3
|
|
||||||
# via ipython
|
|
||||||
tblib==3.0.0
|
|
||||||
# via -r requirements/test.txt
|
|
||||||
traitlets==5.14.1
|
|
||||||
# via
|
|
||||||
# ipython
|
|
||||||
# matplotlib-inline
|
|
||||||
types-pytz==2023.3.1.1
|
|
||||||
# via django-stubs
|
|
||||||
types-pyyaml==6.0.12.12
|
|
||||||
# via django-stubs
|
|
||||||
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==2.1.0
|
|
||||||
# via
|
|
||||||
# -r requirements/test.txt
|
|
||||||
# requests
|
|
||||||
uvicorn==0.25.0
|
|
||||||
# via -r requirements/test.txt
|
|
||||||
wcwidth==0.2.13
|
|
||||||
# via prompt-toolkit
|
|
||||||
whitenoise==6.6.0
|
|
||||||
# via -r requirements/test.txt
|
|
||||||
|
|
||||||
# The following packages are considered to be unsafe in a requirements file:
|
|
||||||
# setuptools
|
|
183
requirements/requirements-dev.txt
Normal file
183
requirements/requirements-dev.txt
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
#
|
||||||
|
# This file is autogenerated by hatch-pip-compile with Python 3.12
|
||||||
|
#
|
||||||
|
# - 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
|
||||||
|
# - django-allauth==0.63.3
|
||||||
|
# - django-money==3.4.1
|
||||||
|
# - django-oauth-toolkit==2.4.0
|
||||||
|
# - django-registries==0.0.3
|
||||||
|
# - django-view-decorator==0.0.4
|
||||||
|
# - django-zen-queries==2.1.0
|
||||||
|
# - django==5.0.6
|
||||||
|
# - environs[django]==11.0.0
|
||||||
|
# - psycopg[binary]==3.1.19
|
||||||
|
# - uvicorn==0.30.0
|
||||||
|
# - whitenoise==6.6.0
|
||||||
|
#
|
||||||
|
|
||||||
|
asgiref==3.8.1
|
||||||
|
# via django
|
||||||
|
attrs==23.2.0
|
||||||
|
# via pytest
|
||||||
|
babel==2.15.0
|
||||||
|
# via py-moneyed
|
||||||
|
build==1.2.1
|
||||||
|
# via pip-tools
|
||||||
|
certifi==2024.7.4
|
||||||
|
# via requests
|
||||||
|
cffi==1.16.0
|
||||||
|
# via cryptography
|
||||||
|
charset-normalizer==3.3.2
|
||||||
|
# via requests
|
||||||
|
click==8.1.7
|
||||||
|
# via
|
||||||
|
# pip-tools
|
||||||
|
# uvicorn
|
||||||
|
coverage==7.3.0
|
||||||
|
# via
|
||||||
|
# hatch.envs.dev
|
||||||
|
# coverage
|
||||||
|
# pytest-cov
|
||||||
|
cryptography==42.0.8
|
||||||
|
# via jwcrypto
|
||||||
|
dj-database-url==2.2.0
|
||||||
|
# via environs
|
||||||
|
dj-email-url==1.0.6
|
||||||
|
# via environs
|
||||||
|
django==5.0.6
|
||||||
|
# via
|
||||||
|
# hatch.envs.dev
|
||||||
|
# dj-database-url
|
||||||
|
# django-allauth
|
||||||
|
# django-browser-reload
|
||||||
|
# django-debug-toolbar
|
||||||
|
# django-money
|
||||||
|
# django-oauth-toolkit
|
||||||
|
# django-registries
|
||||||
|
# django-stubs
|
||||||
|
# django-stubs-ext
|
||||||
|
# django-view-decorator
|
||||||
|
# django-zen-queries
|
||||||
|
# model-bakery
|
||||||
|
django-allauth==0.63.3
|
||||||
|
# via hatch.envs.dev
|
||||||
|
django-browser-reload==1.7.0
|
||||||
|
# via hatch.envs.dev
|
||||||
|
django-cache-url==3.4.5
|
||||||
|
# via environs
|
||||||
|
django-debug-toolbar==4.2.0
|
||||||
|
# via hatch.envs.dev
|
||||||
|
django-money==3.4.1
|
||||||
|
# via hatch.envs.dev
|
||||||
|
django-oauth-toolkit==2.4.0
|
||||||
|
# via hatch.envs.dev
|
||||||
|
django-registries==0.0.3
|
||||||
|
# via hatch.envs.dev
|
||||||
|
django-stubs==1.16.0
|
||||||
|
# via hatch.envs.dev
|
||||||
|
django-stubs-ext==5.0.2
|
||||||
|
# via django-stubs
|
||||||
|
django-view-decorator==0.0.4
|
||||||
|
# via hatch.envs.dev
|
||||||
|
django-zen-queries==2.1.0
|
||||||
|
# via hatch.envs.dev
|
||||||
|
environs==11.0.0
|
||||||
|
# via
|
||||||
|
# hatch.envs.dev
|
||||||
|
# environs
|
||||||
|
h11==0.14.0
|
||||||
|
# via uvicorn
|
||||||
|
idna==3.7
|
||||||
|
# via requests
|
||||||
|
iniconfig==2.0.0
|
||||||
|
# via pytest
|
||||||
|
jwcrypto==1.5.6
|
||||||
|
# via django-oauth-toolkit
|
||||||
|
marshmallow==3.21.3
|
||||||
|
# via environs
|
||||||
|
model-bakery==1.17.0
|
||||||
|
# via hatch.envs.dev
|
||||||
|
mypy==1.1.1
|
||||||
|
# via
|
||||||
|
# hatch.envs.dev
|
||||||
|
# django-stubs
|
||||||
|
mypy-extensions==1.0.0
|
||||||
|
# via mypy
|
||||||
|
oauthlib==3.2.2
|
||||||
|
# via django-oauth-toolkit
|
||||||
|
packaging==24.1
|
||||||
|
# via
|
||||||
|
# build
|
||||||
|
# marshmallow
|
||||||
|
# pytest
|
||||||
|
pip-tools==7.3.0
|
||||||
|
# via hatch.envs.dev
|
||||||
|
pluggy==1.5.0
|
||||||
|
# via pytest
|
||||||
|
psycopg==3.1.19
|
||||||
|
# via
|
||||||
|
# hatch.envs.dev
|
||||||
|
# psycopg
|
||||||
|
psycopg-binary==3.1.19
|
||||||
|
# via psycopg
|
||||||
|
py-moneyed==3.0
|
||||||
|
# via django-money
|
||||||
|
pycparser==2.22
|
||||||
|
# via cffi
|
||||||
|
pyproject-hooks==1.1.0
|
||||||
|
# via build
|
||||||
|
pytest==7.2.2
|
||||||
|
# via
|
||||||
|
# hatch.envs.dev
|
||||||
|
# pytest-cov
|
||||||
|
# pytest-django
|
||||||
|
pytest-cov==5.0.0
|
||||||
|
# via hatch.envs.dev
|
||||||
|
pytest-django==4.5.2
|
||||||
|
# via hatch.envs.dev
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
# via environs
|
||||||
|
pytz==2024.1
|
||||||
|
# via django-oauth-toolkit
|
||||||
|
requests==2.32.3
|
||||||
|
# via django-oauth-toolkit
|
||||||
|
sqlparse==0.5.0
|
||||||
|
# via
|
||||||
|
# django
|
||||||
|
# django-debug-toolbar
|
||||||
|
tomli==2.0.1
|
||||||
|
# via django-stubs
|
||||||
|
types-pytz==2024.1.0.20240417
|
||||||
|
# via django-stubs
|
||||||
|
types-pyyaml==6.0.12.20240311
|
||||||
|
# via django-stubs
|
||||||
|
typing-extensions==4.12.2
|
||||||
|
# via
|
||||||
|
# dj-database-url
|
||||||
|
# django-stubs
|
||||||
|
# django-stubs-ext
|
||||||
|
# jwcrypto
|
||||||
|
# mypy
|
||||||
|
# psycopg
|
||||||
|
# py-moneyed
|
||||||
|
urllib3==2.2.2
|
||||||
|
# via requests
|
||||||
|
uvicorn==0.30.0
|
||||||
|
# via hatch.envs.dev
|
||||||
|
wheel==0.43.0
|
||||||
|
# via pip-tools
|
||||||
|
whitenoise==6.6.0
|
||||||
|
# via hatch.envs.dev
|
||||||
|
|
||||||
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
|
# pip
|
||||||
|
# setuptools
|
|
@ -1,5 +0,0 @@
|
||||||
-r base.txt
|
|
||||||
|
|
||||||
coverage==7.4.0
|
|
||||||
tblib==3.0.0
|
|
||||||
unittest-xml-reporting==3.2.0
|
|
|
@ -1,149 +0,0 @@
|
||||||
#
|
|
||||||
# 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.7.2
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# django
|
|
||||||
babel==2.14.0
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# py-moneyed
|
|
||||||
certifi==2023.11.17
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# requests
|
|
||||||
cffi==1.16.0
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# cryptography
|
|
||||||
charset-normalizer==3.3.2
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# requests
|
|
||||||
click==8.1.7
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# uvicorn
|
|
||||||
coverage==7.4.0
|
|
||||||
# via -r requirements/test.in
|
|
||||||
cryptography==41.0.7
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# pyjwt
|
|
||||||
defusedxml==0.7.1
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# python3-openid
|
|
||||||
dj-database-url==2.1.0
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# environs
|
|
||||||
dj-email-url==1.0.6
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# environs
|
|
||||||
django==5.0.1
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# dj-database-url
|
|
||||||
# django-allauth
|
|
||||||
# django-money
|
|
||||||
# django-zen-queries
|
|
||||||
django-allauth==0.60.0
|
|
||||||
# via -r requirements/base.txt
|
|
||||||
django-cache-url==3.4.5
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# environs
|
|
||||||
django-money==3.4.1
|
|
||||||
# via -r requirements/base.txt
|
|
||||||
django-zen-queries==2.1.0
|
|
||||||
# via -r requirements/base.txt
|
|
||||||
environs[django]==10.0.0
|
|
||||||
# via -r requirements/base.txt
|
|
||||||
h11==0.14.0
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# uvicorn
|
|
||||||
idna==3.6
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# requests
|
|
||||||
lxml==5.0.1
|
|
||||||
# via unittest-xml-reporting
|
|
||||||
marshmallow==3.20.1
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# environs
|
|
||||||
oauthlib==3.2.2
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# requests-oauthlib
|
|
||||||
packaging==23.2
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# marshmallow
|
|
||||||
psycopg[binary]==3.1.16
|
|
||||||
# via -r requirements/base.txt
|
|
||||||
psycopg-binary==3.1.16
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# psycopg
|
|
||||||
py-moneyed==3.0
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# django-money
|
|
||||||
pycparser==2.21
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# cffi
|
|
||||||
pyjwt[crypto]==2.8.0
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# django-allauth
|
|
||||||
python-dotenv==1.0.0
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# environs
|
|
||||||
python3-openid==3.2.0
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# django-allauth
|
|
||||||
requests==2.31.0
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# django-allauth
|
|
||||||
# requests-oauthlib
|
|
||||||
requests-oauthlib==1.3.1
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# django-allauth
|
|
||||||
sqlparse==0.4.4
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# django
|
|
||||||
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==2.1.0
|
|
||||||
# via
|
|
||||||
# -r requirements/base.txt
|
|
||||||
# requests
|
|
||||||
uvicorn==0.25.0
|
|
||||||
# via -r requirements/base.txt
|
|
||||||
whitenoise==6.6.0
|
|
||||||
# via -r requirements/base.txt
|
|
||||||
|
|
||||||
# The following packages are considered to be unsafe in a requirements file:
|
|
||||||
# setuptools
|
|
|
@ -0,0 +1 @@
|
||||||
|
"""Accounting app."""
|
|
@ -1,26 +1,36 @@
|
||||||
|
"""Admin for the accounting app."""
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from . import models
|
from .models import Order
|
||||||
|
from .models import Payment
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Order)
|
@admin.register(Order)
|
||||||
class OrderAdmin(admin.ModelAdmin):
|
class OrderAdmin(admin.ModelAdmin):
|
||||||
|
"""Admin for the Order model."""
|
||||||
|
|
||||||
list_display = ("who", "description", "created", "is_paid")
|
list_display = ("who", "description", "created", "is_paid")
|
||||||
|
|
||||||
@admin.display(description=_("Customer"))
|
@admin.display(description=_("Customer"))
|
||||||
def who(self, instance):
|
def who(self, instance: Order) -> str:
|
||||||
|
"""Return the full name of the user who made the order."""
|
||||||
return instance.user.get_full_name()
|
return instance.user.get_full_name()
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Payment)
|
@admin.register(Payment)
|
||||||
class PaymentAdmin(admin.ModelAdmin):
|
class PaymentAdmin(admin.ModelAdmin):
|
||||||
|
"""Admin for the Payment model."""
|
||||||
|
|
||||||
list_display = ("who", "description", "order_id", "created")
|
list_display = ("who", "description", "order_id", "created")
|
||||||
|
|
||||||
@admin.display(description=_("Customer"))
|
@admin.display(description=_("Customer"))
|
||||||
def who(self, instance):
|
def who(self, instance: Payment) -> str:
|
||||||
|
"""Return the full name of the user who made the payment."""
|
||||||
return instance.order.user.get_full_name()
|
return instance.order.user.get_full_name()
|
||||||
|
|
||||||
@admin.display(description=_("Order ID"))
|
@admin.display(description=_("Order ID"))
|
||||||
def order_id(self, instance):
|
def order_id(self, instance: Payment) -> int:
|
||||||
|
"""Return the ID of the order."""
|
||||||
return instance.order.id
|
return instance.order.id
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
|
"""Accounting app configuration."""
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class AccountingConfig(AppConfig):
|
class AccountingConfig(AppConfig):
|
||||||
|
"""Accounting app config."""
|
||||||
|
|
||||||
name = "accounting"
|
name = "accounting"
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-07-14 22:16
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounting', '0002_alter_order_price_currency_alter_order_vat_currency_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='payment',
|
||||||
|
name='stripe_charge_id',
|
||||||
|
field=models.CharField(blank=True, default='', max_length=255),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,4 +1,7 @@
|
||||||
|
"""Models for the accounting app."""
|
||||||
|
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -6,9 +9,12 @@ from django.db.models.aggregates import Sum
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.utils.translation import pgettext_lazy
|
from django.utils.translation import pgettext_lazy
|
||||||
from djmoney.models.fields import MoneyField
|
from djmoney.models.fields import MoneyField
|
||||||
|
from djmoney.money import Money
|
||||||
|
|
||||||
|
|
||||||
class CreatedModifiedAbstract(models.Model):
|
class CreatedModifiedAbstract(models.Model):
|
||||||
|
"""Abstract model to track creation and modification of objects."""
|
||||||
|
|
||||||
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"))
|
||||||
|
|
||||||
|
@ -17,19 +23,27 @@ class CreatedModifiedAbstract(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Account(CreatedModifiedAbstract):
|
class Account(CreatedModifiedAbstract):
|
||||||
"""This is the model where we can give access to several users, such that they
|
"""An account for a user.
|
||||||
|
|
||||||
|
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.
|
can decide which account to use to pay for something.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
owner = models.ForeignKey("auth.User", on_delete=models.PROTECT)
|
owner = models.ForeignKey("auth.User", on_delete=models.PROTECT)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"Account of {self.owner.get_full_name()}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def balance(self):
|
def balance(self) -> Money:
|
||||||
|
"""Return the balance of the account."""
|
||||||
return self.transactions.all().aggregate(Sum("amount")).get("amount", 0)
|
return self.transactions.all().aggregate(Sum("amount")).get("amount", 0)
|
||||||
|
|
||||||
|
|
||||||
class Transaction(CreatedModifiedAbstract):
|
class Transaction(CreatedModifiedAbstract):
|
||||||
"""Tracks in and outgoing events of an account. When an order is received, an
|
"""A transaction.
|
||||||
|
|
||||||
|
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.
|
amount is subtracted, when a payment is received, an amount is added.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -46,9 +60,14 @@ class Transaction(CreatedModifiedAbstract):
|
||||||
)
|
)
|
||||||
description = models.CharField(max_length=1024, verbose_name=_("description"))
|
description = models.CharField(max_length=1024, verbose_name=_("description"))
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"Transaction of {self.amount} for {self.account}"
|
||||||
|
|
||||||
|
|
||||||
class Order(CreatedModifiedAbstract):
|
class Order(CreatedModifiedAbstract):
|
||||||
"""Scoped out: Contents of invoices will have to be tracked either here or in
|
"""An order.
|
||||||
|
|
||||||
|
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
|
a separate Invoice model. This is undecided because we are not generating
|
||||||
invoices at the moment.
|
invoices at the moment.
|
||||||
"""
|
"""
|
||||||
|
@ -67,23 +86,6 @@ class Order(CreatedModifiedAbstract):
|
||||||
|
|
||||||
is_paid = models.BooleanField(default=False, verbose_name=_("is paid"))
|
is_paid = models.BooleanField(default=False, verbose_name=_("is paid"))
|
||||||
|
|
||||||
@property
|
|
||||||
def total(self):
|
|
||||||
return self.price + self.vat
|
|
||||||
|
|
||||||
@property
|
|
||||||
def display_id(self):
|
|
||||||
return str(self.id).zfill(6)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def payment_token(self):
|
|
||||||
pk = str(self.pk).encode("utf-8")
|
|
||||||
x = md5()
|
|
||||||
x.update(pk)
|
|
||||||
extra_hash = (settings.SECRET_KEY + "blah").encode("utf-8")
|
|
||||||
x.update(extra_hash)
|
|
||||||
return x.hexdigest()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = pgettext_lazy("accounting term", "Order")
|
verbose_name = pgettext_lazy("accounting term", "Order")
|
||||||
verbose_name_plural = pgettext_lazy("accounting term", "Orders")
|
verbose_name_plural = pgettext_lazy("accounting term", "Orders")
|
||||||
|
@ -91,31 +93,55 @@ class Order(CreatedModifiedAbstract):
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"Order ID {self.display_id}"
|
return f"Order ID {self.display_id}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total(self) -> Money:
|
||||||
|
"""Return the total price of the order."""
|
||||||
|
return self.price + self.vat
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_id(self) -> str:
|
||||||
|
"""Return an id for the order."""
|
||||||
|
return str(self.id).zfill(6)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def payment_token(self) -> str:
|
||||||
|
"""Return a token for the payment."""
|
||||||
|
pk = str(self.pk).encode("utf-8")
|
||||||
|
x = md5() # noqa: S324
|
||||||
|
x.update(pk)
|
||||||
|
extra_hash = (settings.SECRET_KEY + "blah").encode("utf-8")
|
||||||
|
x.update(extra_hash)
|
||||||
|
return x.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
class Payment(CreatedModifiedAbstract):
|
class Payment(CreatedModifiedAbstract):
|
||||||
|
"""A payment is a transaction that is made to pay for an order."""
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
description = models.CharField(max_length=1024, verbose_name=_("description"))
|
description = models.CharField(max_length=1024, verbose_name=_("description"))
|
||||||
|
|
||||||
stripe_charge_id = models.CharField(max_length=255, null=True, blank=True)
|
stripe_charge_id = models.CharField(max_length=255, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("payment")
|
||||||
|
verbose_name_plural = _("payments")
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"Payment ID {self.display_id}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def display_id(self):
|
def display_id(self) -> str:
|
||||||
|
"""Return an id for the payment."""
|
||||||
return str(self.id).zfill(6)
|
return str(self.id).zfill(6)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_order(cls, order):
|
def from_order(cls, order: Order) -> Self:
|
||||||
|
"""Create a payment from an order."""
|
||||||
return cls.objects.create(
|
return cls.objects.create(
|
||||||
order=order,
|
order=order,
|
||||||
user=order.user,
|
user=order.user,
|
||||||
amount=order.total,
|
amount=order.total,
|
||||||
description=order.description,
|
description=order.description,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"Payment ID {self.display_id}"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("payment")
|
|
||||||
verbose_name_plural = _("payments")
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
"""Membership application.
|
"""Membership application.
|
||||||
======================
|
|
||||||
|
|
||||||
This application's domain relate to organizational structures and
|
This application's domain relate to organizational structures and
|
||||||
implementation of statutes, policies etc.
|
implementation of statutes, policies etc.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,20 +1,29 @@
|
||||||
|
"""Admin configuration for membership app."""
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import Membership
|
from .models import Membership
|
||||||
from .models import MembershipType
|
from .models import MembershipType
|
||||||
|
from .models import ServiceAccess
|
||||||
from .models import SubscriptionPeriod
|
from .models import SubscriptionPeriod
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Membership)
|
@admin.register(Membership)
|
||||||
class MembershipAdmin(admin.ModelAdmin):
|
class MembershipAdmin(admin.ModelAdmin):
|
||||||
pass
|
"""Admin for Membership model."""
|
||||||
|
|
||||||
|
|
||||||
@admin.register(MembershipType)
|
@admin.register(MembershipType)
|
||||||
class MembershipTypeAdmin(admin.ModelAdmin):
|
class MembershipTypeAdmin(admin.ModelAdmin):
|
||||||
pass
|
"""Admin for MembershipType model."""
|
||||||
|
|
||||||
|
|
||||||
@admin.register(SubscriptionPeriod)
|
@admin.register(SubscriptionPeriod)
|
||||||
class SubscriptionPeriodAdmin(admin.ModelAdmin):
|
class SubscriptionPeriodAdmin(admin.ModelAdmin):
|
||||||
|
"""Admin for SubscriptionPeriod model."""
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ServiceAccess)
|
||||||
|
class ServiceAccessAdmin(admin.ModelAdmin):
|
||||||
|
"""Admin for ServiceAccess model."""
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
|
"""Membership app configuration."""
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.db.models.signals import post_migrate
|
from django.db.models.signals import post_migrate
|
||||||
|
|
||||||
|
|
||||||
class MembershipConfig(AppConfig):
|
class MembershipConfig(AppConfig):
|
||||||
|
"""Membership app config."""
|
||||||
|
|
||||||
name = "membership"
|
name = "membership"
|
||||||
|
|
||||||
def ready(self) -> None:
|
def ready(self) -> None:
|
||||||
|
"""Ready method."""
|
||||||
from .permissions import persist_permissions
|
from .permissions import persist_permissions
|
||||||
|
|
||||||
post_migrate.connect(persist_permissions, sender=self)
|
post_migrate.connect(persist_permissions, sender=self)
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Generated by Django 5.0.1 on 2024-01-13 19:20
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django_registries.registry
|
||||||
|
import services.registry
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("membership", "0005_member"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ServiceAccess",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"modified",
|
||||||
|
models.DateTimeField(auto_now=True, verbose_name="modified"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created",
|
||||||
|
models.DateTimeField(auto_now_add=True, verbose_name="created"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"service",
|
||||||
|
django_registries.registry.ChoicesField(
|
||||||
|
choices=[],
|
||||||
|
registry=services.registry.ServiceRegistry,
|
||||||
|
verbose_name="service",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "service access",
|
||||||
|
"verbose_name_plural": "service accesses",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name="serviceaccess",
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=("user", "service"), name="unique_user_service"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,3 +1,8 @@
|
||||||
|
"""Models for the membership app."""
|
||||||
|
|
||||||
|
from typing import ClassVar
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.postgres.constraints import ExclusionConstraint
|
from django.contrib.postgres.constraints import ExclusionConstraint
|
||||||
from django.contrib.postgres.fields import DateRangeField
|
from django.contrib.postgres.fields import DateRangeField
|
||||||
|
@ -5,18 +10,29 @@ from django.contrib.postgres.fields import RangeOperators
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from services.registry import ServiceRegistry
|
||||||
from utils.mixins import CreatedModifiedAbstract
|
from utils.mixins import CreatedModifiedAbstract
|
||||||
|
|
||||||
|
|
||||||
|
class NoSubscriptionPeriodFoundError(Exception):
|
||||||
|
"""Raised when no subscription period is found."""
|
||||||
|
|
||||||
|
|
||||||
class Member(User):
|
class Member(User):
|
||||||
|
"""Proxy model for the User model to add some convenience methods."""
|
||||||
|
|
||||||
class QuerySet(models.QuerySet):
|
class QuerySet(models.QuerySet):
|
||||||
def annotate_membership(self):
|
"""QuerySet for the Member model."""
|
||||||
|
|
||||||
|
def annotate_membership(self) -> Self:
|
||||||
|
"""Annotate whether the user has an active membership."""
|
||||||
from .selectors import get_current_subscription_period
|
from .selectors import get_current_subscription_period
|
||||||
|
|
||||||
current_subscription_period = get_current_subscription_period()
|
current_subscription_period = get_current_subscription_period()
|
||||||
|
|
||||||
if not current_subscription_period:
|
if not current_subscription_period:
|
||||||
raise ValueError("No current subscription period found")
|
raise NoSubscriptionPeriodFoundError
|
||||||
|
|
||||||
return self.annotate(
|
return self.annotate(
|
||||||
active_membership=models.Exists(
|
active_membership=models.Exists(
|
||||||
|
@ -34,12 +50,15 @@ class Member(User):
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionPeriod(CreatedModifiedAbstract):
|
class SubscriptionPeriod(CreatedModifiedAbstract):
|
||||||
"""Denotes a period for which members should pay their membership fee for."""
|
"""A subscription period.
|
||||||
|
|
||||||
|
Denotes a period for which members should pay their membership fee for.
|
||||||
|
"""
|
||||||
|
|
||||||
period = DateRangeField(verbose_name=_("period"))
|
period = DateRangeField(verbose_name=_("period"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
constraints = [
|
constraints: ClassVar = [
|
||||||
ExclusionConstraint(
|
ExclusionConstraint(
|
||||||
name="exclude_overlapping_periods",
|
name="exclude_overlapping_periods",
|
||||||
expressions=[
|
expressions=[
|
||||||
|
@ -53,22 +72,31 @@ class SubscriptionPeriod(CreatedModifiedAbstract):
|
||||||
|
|
||||||
|
|
||||||
class Membership(CreatedModifiedAbstract):
|
class Membership(CreatedModifiedAbstract):
|
||||||
"""Tracks that a user has membership of a given type for a given period."""
|
"""A membership.
|
||||||
|
|
||||||
|
Tracks that a user has membership of a given type for a given period.
|
||||||
|
"""
|
||||||
|
|
||||||
class QuerySet(models.QuerySet):
|
class QuerySet(models.QuerySet):
|
||||||
def for_member(self, member: Member):
|
"""QuerySet for the Membership model."""
|
||||||
|
|
||||||
|
def for_member(self, member: Member) -> Self:
|
||||||
|
"""Filter memberships for a given member."""
|
||||||
return self.filter(user=member)
|
return self.filter(user=member)
|
||||||
|
|
||||||
def _current(self):
|
def _current(self) -> Self:
|
||||||
|
"""Filter memberships for the current period."""
|
||||||
return self.filter(period__period__contains=timezone.now())
|
return self.filter(period__period__contains=timezone.now())
|
||||||
|
|
||||||
def current(self) -> "Membership | None":
|
def current(self) -> "Membership | None":
|
||||||
|
"""Get the current membership."""
|
||||||
try:
|
try:
|
||||||
return self._current().get()
|
return self._current().get()
|
||||||
except self.model.DoesNotExist:
|
except self.model.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def previous(self) -> list["Membership"]:
|
def previous(self) -> list["Membership"]:
|
||||||
|
"""Get previous memberships."""
|
||||||
# A naïve way to get previous by just excluding the current. This
|
# A naïve way to get previous by just excluding the current. This
|
||||||
# means that there must be some protection against "future"
|
# means that there must be some protection against "future"
|
||||||
# memberships.
|
# memberships.
|
||||||
|
@ -76,10 +104,6 @@ class Membership(CreatedModifiedAbstract):
|
||||||
|
|
||||||
objects = QuerySet.as_manager()
|
objects = QuerySet.as_manager()
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("membership")
|
|
||||||
verbose_name_plural = _("memberships")
|
|
||||||
|
|
||||||
user = models.ForeignKey("auth.User", on_delete=models.PROTECT)
|
user = models.ForeignKey("auth.User", on_delete=models.PROTECT)
|
||||||
|
|
||||||
membership_type = models.ForeignKey(
|
membership_type = models.ForeignKey(
|
||||||
|
@ -94,20 +118,45 @@ class Membership(CreatedModifiedAbstract):
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("membership")
|
||||||
|
verbose_name_plural = _("memberships")
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.user} - {self.period}"
|
return f"{self.user} - {self.period}"
|
||||||
|
|
||||||
|
|
||||||
class MembershipType(CreatedModifiedAbstract):
|
class MembershipType(CreatedModifiedAbstract):
|
||||||
"""Models membership types. Currently only a name, but will in the future
|
"""A membership type.
|
||||||
|
|
||||||
|
Models membership types. Currently only a name, but will in the future
|
||||||
possibly contain more information like fees.
|
possibly contain more information like fees.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
name = models.CharField(verbose_name=_("name"), max_length=64)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("membership type")
|
verbose_name = _("membership type")
|
||||||
verbose_name_plural = _("membership types")
|
verbose_name_plural = _("membership types")
|
||||||
|
|
||||||
name = models.CharField(verbose_name=_("name"), max_length=64)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceAccess(CreatedModifiedAbstract):
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("service access")
|
||||||
|
verbose_name_plural = _("service accesses")
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["user", "service"],
|
||||||
|
name="unique_user_service",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
user = models.ForeignKey("auth.User", on_delete=models.PROTECT)
|
||||||
|
|
||||||
|
service = ServiceRegistry.choices_field(verbose_name=_("service"))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user} - {self.service}"
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"""Permissions for the membership app."""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from django.contrib.auth.models import Permission as DjangoPermission
|
from django.contrib.auth.models import Permission as DjangoPermission
|
||||||
|
@ -7,26 +9,32 @@ from django.utils.translation import gettext_lazy as _
|
||||||
PERMISSIONS = []
|
PERMISSIONS = []
|
||||||
|
|
||||||
|
|
||||||
def persist_permissions(sender, **kwargs) -> None:
|
def persist_permissions(*args, **kwargs) -> None: # type: ignore[no-untyped-def] # noqa: ARG001
|
||||||
|
"""Persist all permissions."""
|
||||||
for permission in PERMISSIONS:
|
for permission in PERMISSIONS:
|
||||||
permission.persist_permission()
|
permission.persist_permission()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Permission:
|
class Permission:
|
||||||
|
"""Dataclass to define a permission."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
codename: str
|
codename: str
|
||||||
app_label: str
|
app_label: str
|
||||||
model: str
|
model: str
|
||||||
|
|
||||||
def __post_init__(self, *args, **kwargs):
|
def __post_init__(self, *args, **kwargs) -> None:
|
||||||
|
"""Post init method."""
|
||||||
PERMISSIONS.append(self)
|
PERMISSIONS.append(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path(self) -> str:
|
def path(self) -> str:
|
||||||
|
"""Return the path of the permission."""
|
||||||
return f"{self.app_label}.{self.codename}"
|
return f"{self.app_label}.{self.codename}"
|
||||||
|
|
||||||
def persist_permission(self) -> None:
|
def persist_permission(self) -> None:
|
||||||
|
"""Persist the permission."""
|
||||||
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,
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
|
"""Selectors for the membership app."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from django.db.models import Exists
|
from django.db.models import Exists
|
||||||
from django.db.models import OuterRef
|
from django.db.models import OuterRef
|
||||||
|
@ -8,8 +13,12 @@ from membership.models import Member
|
||||||
from membership.models import Membership
|
from membership.models import Membership
|
||||||
from membership.models import SubscriptionPeriod
|
from membership.models import SubscriptionPeriod
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from django.db.models.query import QuerySet
|
||||||
|
|
||||||
|
|
||||||
def get_subscription_periods(member: Member | None = None) -> list[SubscriptionPeriod]:
|
def get_subscription_periods(member: Member | None = None) -> list[SubscriptionPeriod]:
|
||||||
|
"""Get all subscription periods."""
|
||||||
subscription_periods = SubscriptionPeriod.objects.prefetch_related(
|
subscription_periods = SubscriptionPeriod.objects.prefetch_related(
|
||||||
"membership_set",
|
"membership_set",
|
||||||
"membership_set__user",
|
"membership_set__user",
|
||||||
|
@ -29,6 +38,7 @@ def get_subscription_periods(member: Member | None = None) -> list[SubscriptionP
|
||||||
|
|
||||||
|
|
||||||
def get_current_subscription_period() -> SubscriptionPeriod | None:
|
def get_current_subscription_period() -> SubscriptionPeriod | None:
|
||||||
|
"""Get the current subscription period."""
|
||||||
with contextlib.suppress(SubscriptionPeriod.DoesNotExist):
|
with contextlib.suppress(SubscriptionPeriod.DoesNotExist):
|
||||||
return SubscriptionPeriod.objects.prefetch_related(
|
return SubscriptionPeriod.objects.prefetch_related(
|
||||||
"membership_set",
|
"membership_set",
|
||||||
|
@ -41,6 +51,7 @@ def get_memberships(
|
||||||
member: Member | None = None,
|
member: Member | None = None,
|
||||||
period: SubscriptionPeriod | None = None,
|
period: SubscriptionPeriod | None = None,
|
||||||
) -> Membership.QuerySet:
|
) -> Membership.QuerySet:
|
||||||
|
"""Get memberships."""
|
||||||
memberships = Membership.objects.select_related("membership_type").all()
|
memberships = Membership.objects.select_related("membership_type").all()
|
||||||
|
|
||||||
if member:
|
if member:
|
||||||
|
@ -52,9 +63,11 @@ def get_memberships(
|
||||||
return memberships
|
return memberships
|
||||||
|
|
||||||
|
|
||||||
def get_members():
|
def get_members() -> QuerySet[Member]:
|
||||||
|
"""Get all members."""
|
||||||
return Member.objects.all().annotate_membership().order_by("username")
|
return Member.objects.all().annotate_membership().order_by("username")
|
||||||
|
|
||||||
|
|
||||||
def get_member(*, member_id: int) -> Member:
|
def get_member(*, member_id: int) -> Member:
|
||||||
|
"""Get a member by id."""
|
||||||
return get_members().get(id=member_id)
|
return get_members().get(id=member_id)
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
|
"""Views for the membership app."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_view_decorator import namespaced_decorator_factory
|
from django_view_decorator import namespaced_decorator_factory
|
||||||
|
from utils.view_utils import RenderConfig
|
||||||
from utils.view_utils import RowAction
|
from utils.view_utils import RowAction
|
||||||
from utils.view_utils import render
|
from utils.view_utils import render
|
||||||
from utils.view_utils import render_list
|
|
||||||
|
|
||||||
from .permissions import ADMINISTRATE_MEMBERS
|
from .permissions import ADMINISTRATE_MEMBERS
|
||||||
from .selectors import get_member
|
from .selectors import get_member
|
||||||
|
@ -10,6 +16,10 @@ 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
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from django.http import HttpRequest
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
member_view = namespaced_decorator_factory(namespace="member", base_path="membership")
|
member_view = namespaced_decorator_factory(namespace="member", base_path="membership")
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,7 +28,8 @@ member_view = namespaced_decorator_factory(namespace="member", base_path="member
|
||||||
name="membership-overview",
|
name="membership-overview",
|
||||||
login_required=True,
|
login_required=True,
|
||||||
)
|
)
|
||||||
def membership_overview(request):
|
def membership_overview(request: HttpRequest) -> HttpResponse:
|
||||||
|
"""View to show the membership overview."""
|
||||||
memberships = get_memberships(member=request.user)
|
memberships = get_memberships(member=request.user)
|
||||||
current_membership = memberships.current()
|
current_membership = memberships.current()
|
||||||
previous_memberships = memberships.previous()
|
previous_memberships = memberships.previous()
|
||||||
|
@ -50,13 +61,13 @@ admin_members_view = namespaced_decorator_factory(
|
||||||
login_required=True,
|
login_required=True,
|
||||||
permissions=[ADMINISTRATE_MEMBERS.path],
|
permissions=[ADMINISTRATE_MEMBERS.path],
|
||||||
)
|
)
|
||||||
def members_admin(request):
|
def members_admin(request: HttpRequest) -> HttpResponse:
|
||||||
|
"""View to list all members."""
|
||||||
users = get_members()
|
users = get_members()
|
||||||
|
|
||||||
return render_list(
|
render_config = RenderConfig(
|
||||||
entity_name="member",
|
entity_name="member",
|
||||||
entity_name_plural="members",
|
entity_name_plural="members",
|
||||||
request=request,
|
|
||||||
paginate_by=20,
|
paginate_by=20,
|
||||||
objects=users,
|
objects=users,
|
||||||
columns=[
|
columns=[
|
||||||
|
@ -75,6 +86,10 @@ def members_admin(request):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return render_config.render_list(
|
||||||
|
request=request,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin_members_view(
|
@admin_members_view(
|
||||||
paths="<int:member_id>/",
|
paths="<int:member_id>/",
|
||||||
|
@ -82,7 +97,8 @@ def members_admin(request):
|
||||||
login_required=True,
|
login_required=True,
|
||||||
permissions=[ADMINISTRATE_MEMBERS.path],
|
permissions=[ADMINISTRATE_MEMBERS.path],
|
||||||
)
|
)
|
||||||
def members_admin_detail(request, member_id):
|
def members_admin_detail(request: HttpRequest, member_id: int) -> HttpResponse:
|
||||||
|
"""View to show the details of a member."""
|
||||||
member = get_member(member_id=member_id)
|
member = get_member(member_id=member_id)
|
||||||
subscription_periods = get_subscription_periods(member=member)
|
subscription_periods = get_subscription_periods(member=member)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
"""data.coop member system."""
|
|
@ -1,3 +1,5 @@
|
||||||
|
"""Settings for the project."""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
@ -41,12 +43,15 @@ THIRD_PARTY_APPS = [
|
||||||
"allauth",
|
"allauth",
|
||||||
"allauth.account",
|
"allauth.account",
|
||||||
"django_view_decorator",
|
"django_view_decorator",
|
||||||
|
"django_registries",
|
||||||
|
"oauth2_provider",
|
||||||
]
|
]
|
||||||
|
|
||||||
LOCAL_APPS = [
|
LOCAL_APPS = [
|
||||||
"utils",
|
"utils",
|
||||||
"accounting",
|
"accounting",
|
||||||
"membership",
|
"membership",
|
||||||
|
"services",
|
||||||
]
|
]
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
@ -154,6 +159,16 @@ ACCOUNT_EMAIL_REQUIRED = True
|
||||||
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False
|
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False
|
||||||
ACCOUNT_USERNAME_REQUIRED = False
|
ACCOUNT_USERNAME_REQUIRED = False
|
||||||
|
|
||||||
|
# OAuth2 configuration
|
||||||
|
OAUTH2_PROVIDER = {
|
||||||
|
"OIDC_ENABLED": True,
|
||||||
|
"SCOPES": {
|
||||||
|
"openid": "OpenID Connect scope",
|
||||||
|
"profile": "Profile Information",
|
||||||
|
},
|
||||||
|
"PKCE_REQUIRED": False, # this can be a callable - https://github.com/jazzband/django-oauth-toolkit/issues/711#issuecomment-497073038
|
||||||
|
}
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
# We want to log everything to stdout in docker
|
# We want to log everything to stdout in docker
|
||||||
LOGGING = {
|
LOGGING = {
|
||||||
|
|
|
@ -64,4 +64,4 @@ html.dark input[type="email"] {
|
||||||
background: var(--dark-dark);
|
background: var(--dark-dark);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: var(--light-dust);
|
color: var(--light-dust);
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,7 @@ h6 {
|
||||||
--light-dust: #fefef9;
|
--light-dust: #fefef9;
|
||||||
--dust: #f4f1ef;
|
--dust: #f4f1ef;
|
||||||
--medium-dust: #dadada;
|
--medium-dust: #dadada;
|
||||||
|
--medium-dust : #dadada;
|
||||||
--dark-dust: #bfbfbf;
|
--dark-dust: #bfbfbf;
|
||||||
--fade: #878787;
|
--fade: #878787;
|
||||||
--twilight: #4a4a4a;
|
--twilight: #4a4a4a;
|
||||||
|
@ -256,7 +257,7 @@ div.content-view>h2 {
|
||||||
|
|
||||||
div.services {
|
div.services {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: start;
|
||||||
gap: var(--double-space);
|
gap: var(--double-space);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
@ -556,4 +557,4 @@ span.time_remaining {
|
||||||
|
|
||||||
.pagination .page-item.disabled .page-link {
|
.pagination .page-item.disabled .page-link {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,7 +110,7 @@
|
||||||
|
|
||||||
addEmail.addEventListener('click', function(e) {
|
addEmail.addEventListener('click', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let overlay = document.getElementById('email-add-overlay')
|
let overlay = document.getElementById('email-add-overlay')
|
||||||
overlay.style.display = 'flex'
|
overlay.style.display = 'flex'
|
||||||
|
|
||||||
window.addEventListener('keydown', function(e) {
|
window.addEventListener('keydown', function(e) {
|
||||||
|
|
|
@ -78,13 +78,11 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{% comment %}
|
|
||||||
<li>
|
<li>
|
||||||
<a href="/services" class="{% active_path "services" "current" %}">
|
<a href="{% url "services:list" %}" class="{% active_path "services:list" "current" %}">
|
||||||
Services
|
Services
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endcomment %}
|
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="{% url "account_email" %}" class="{% active_path "account_email" "current" %}">
|
<a href="{% url "account_email" %}" class="{% active_path "account_email" "current" %}">
|
||||||
|
@ -113,7 +111,7 @@
|
||||||
themeSwitcher.addEventListener('click', function() {
|
themeSwitcher.addEventListener('click', function() {
|
||||||
themeSwitcher.classList.toggle('active')
|
themeSwitcher.classList.toggle('active')
|
||||||
let isDark = document.querySelector('html').classList.toggle('dark');
|
let isDark = document.querySelector('html').classList.toggle('dark');
|
||||||
|
|
||||||
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<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 …</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 …</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 …</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 …</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 …</a>
|
|
||||||
</div>
|
|
||||||
<a>Subscribe</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endcomment %}
|
|
||||||
{% endblock %}
|
|
|
@ -1,4 +1,5 @@
|
||||||
"""URLs for the membersystem."""
|
"""URLs for the membersystem."""
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import include
|
from django.urls import include
|
||||||
|
@ -7,6 +8,7 @@ from django_view_decorator import include_view_urls
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", include_view_urls(extra_modules=["project.views"])),
|
path("", include_view_urls(extra_modules=["project.views"])),
|
||||||
|
path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
|
||||||
path("accounts/", include("allauth.urls")),
|
path("accounts/", include("allauth.urls")),
|
||||||
path("_admin/", admin.site.urls),
|
path("_admin/", admin.site.urls),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,20 +1,23 @@
|
||||||
from django_view_decorator import view
|
"""Project views."""
|
||||||
|
from __future__ import annotations
|
||||||
|
from membership.models import ServiceAccess
|
||||||
|
from services.registry import ServiceRegistry
|
||||||
from utils.view_utils import render
|
from utils.view_utils import render
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from django_view_decorator import view
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from django.http import HttpRequest
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
|
||||||
@view(
|
@view(
|
||||||
paths="",
|
paths="",
|
||||||
name="index",
|
name="index",
|
||||||
login_required=True,
|
login_required=True,
|
||||||
)
|
)
|
||||||
def index(request):
|
def index(request: HttpRequest) -> HttpResponse:
|
||||||
|
"""View to show the index page."""
|
||||||
return render(request, "index.html")
|
return render(request, "index.html")
|
||||||
|
|
||||||
|
|
||||||
@view(
|
|
||||||
paths="services/",
|
|
||||||
name="services",
|
|
||||||
login_required=True,
|
|
||||||
)
|
|
||||||
def services_overview(request):
|
|
||||||
return render(request, "services_overview.html")
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
For more information on this file, see
|
For more information on this file, see
|
||||||
https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
|
https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
0
src/services/__init__.py
Normal file
0
src/services/__init__.py
Normal file
1
src/services/admin.py
Normal file
1
src/services/admin.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Register your models here.
|
6
src/services/apps.py
Normal file
6
src/services/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ServicesConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "services"
|
0
src/services/migrations/__init__.py
Normal file
0
src/services/migrations/__init__.py
Normal file
1
src/services/models.py
Normal file
1
src/services/models.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Create your models here.
|
40
src/services/registry.py
Normal file
40
src/services/registry.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
from django import forms
|
||||||
|
from django_registries.registry import Interface
|
||||||
|
from django_registries.registry import Registry
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceRegistry(Registry):
|
||||||
|
"""Registry for services"""
|
||||||
|
|
||||||
|
implementations_module = "services"
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceInterface(Interface):
|
||||||
|
"""Interface for services"""
|
||||||
|
|
||||||
|
registry = ServiceRegistry
|
||||||
|
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
url: str
|
||||||
|
|
||||||
|
public: bool = False
|
||||||
|
|
||||||
|
# TODO: add a way to add a something which defines the required fields for a service
|
||||||
|
# - maybe a list of tuples with the field name and the type of the field
|
||||||
|
# this could be used to generate a form for the service, and also to validate
|
||||||
|
# the data saved in a JSONField on the ServiceAccess model
|
||||||
|
|
||||||
|
subscribe_fields: list[tuple[str, forms.Field]] = []
|
||||||
|
|
||||||
|
def get_form(self) -> type:
|
||||||
|
"""Get the form for the service"""
|
||||||
|
print(self.subscribe_fields)
|
||||||
|
return type(
|
||||||
|
"ServiceForm",
|
||||||
|
(forms.Form,),
|
||||||
|
{
|
||||||
|
field_name: field_type
|
||||||
|
for field_name, field_type in self.subscribe_fields
|
||||||
|
},
|
||||||
|
)()
|
51
src/services/services.py
Normal file
51
src/services/services.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from .registry import ServiceInterface
|
||||||
|
|
||||||
|
|
||||||
|
class MailService(ServiceInterface):
|
||||||
|
slug = "mail"
|
||||||
|
name = "Mail"
|
||||||
|
url = "https://mail.data.coop"
|
||||||
|
description = "Mail service for data.coop"
|
||||||
|
|
||||||
|
|
||||||
|
class MatrixService(ServiceInterface):
|
||||||
|
slug = "matrix"
|
||||||
|
name = "Matrix"
|
||||||
|
url = "https://matrix.data.coop"
|
||||||
|
description = "Matrix service for data.coop"
|
||||||
|
|
||||||
|
subscribe_fields = [
|
||||||
|
("username", forms.CharField()),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class MastodonService(ServiceInterface):
|
||||||
|
slug = "mastodon"
|
||||||
|
name = "Mastodon"
|
||||||
|
url = "https://social.data.coop"
|
||||||
|
description = "Mastodon service for data.coop"
|
||||||
|
|
||||||
|
|
||||||
|
class NextcloudService(ServiceInterface):
|
||||||
|
slug = "nextcloud"
|
||||||
|
name = "Nextcloud"
|
||||||
|
url = "https://cloud.data.coop"
|
||||||
|
description = "Nextcloud service for data.coop"
|
||||||
|
|
||||||
|
|
||||||
|
class HedgeDocService(ServiceInterface):
|
||||||
|
slug = "hedgedoc"
|
||||||
|
name = "HedgeDoc"
|
||||||
|
url = "https://pad.data.coop"
|
||||||
|
public = True
|
||||||
|
description = "HedgeDoc service for data.coop"
|
||||||
|
|
||||||
|
|
||||||
|
class RalllyService(ServiceInterface):
|
||||||
|
slug = "rallly"
|
||||||
|
name = "Rallly"
|
||||||
|
url = "https://when.data.coop"
|
||||||
|
public = True
|
||||||
|
description = "Rallly service for data.coop"
|
9
src/services/templates/services/service_detail.html
Normal file
9
src/services/templates/services/service_detail.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="content-view">
|
||||||
|
<h2>{{ service.name }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
18
src/services/templates/services/service_subscribe.html
Normal file
18
src/services/templates/services/service_subscribe.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="content-view">
|
||||||
|
<h2>Subscribe to {{ service.name }}</h2>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
{{ form }}
|
||||||
|
|
||||||
|
<button type="submit">
|
||||||
|
Subscribe
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
45
src/services/templates/services/services_overview.html
Normal file
45
src/services/templates/services/services_overview.html
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="content-view">
|
||||||
|
<h2>Services you subscribe to</h2>
|
||||||
|
<div class="services">
|
||||||
|
{% for service in active_services %}
|
||||||
|
<div>
|
||||||
|
<div class="description">
|
||||||
|
<h3>{{ service.name }}</h3>
|
||||||
|
<p>...</p>
|
||||||
|
<a href="#">Read more …</a>
|
||||||
|
</div>
|
||||||
|
<a>Unsubscribe</a>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<p>You are not subscribed to any service.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-view">
|
||||||
|
<h2>Available services</h2>
|
||||||
|
<div class="services">
|
||||||
|
{% for service in non_active_services %}
|
||||||
|
<div>
|
||||||
|
<div class="description">
|
||||||
|
<h3>{{ service.name }}</h3>
|
||||||
|
<p>{{ service.description }}</p>
|
||||||
|
|
||||||
|
<a href="{% url "services:detail" service_slug=service.slug %}">
|
||||||
|
Read more
|
||||||
|
</a>
|
||||||
|
|
|
||||||
|
<a href="{{ service.url }}" target="_blank">
|
||||||
|
Visit
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<a href="{% url "services:subscribe" service_slug=service.slug %}">Subscribe</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
1
src/services/tests.py
Normal file
1
src/services/tests.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Create your tests here.
|
86
src/services/views.py
Normal file
86
src/services/views.py
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
# Create your views here.
|
||||||
|
from django_view_decorator import namespaced_decorator_factory
|
||||||
|
|
||||||
|
from membership.models import ServiceAccess
|
||||||
|
from services.registry import ServiceRegistry
|
||||||
|
from utils.view_utils import render
|
||||||
|
|
||||||
|
|
||||||
|
services_view = namespaced_decorator_factory(
|
||||||
|
namespace="services",
|
||||||
|
base_path="services",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@services_view(
|
||||||
|
paths="",
|
||||||
|
name="list",
|
||||||
|
login_required=True,
|
||||||
|
)
|
||||||
|
def services_overview(request):
|
||||||
|
active_services = [
|
||||||
|
access.service_implementation
|
||||||
|
for access in ServiceAccess.objects.filter(
|
||||||
|
user=request.user,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
active_service_classes = [service.__class__ for service in active_services]
|
||||||
|
|
||||||
|
services = [
|
||||||
|
service
|
||||||
|
for _, service in ServiceRegistry.get_items()
|
||||||
|
if service not in active_service_classes
|
||||||
|
]
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"non_active_services": services,
|
||||||
|
"active_services": active_services,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request=request,
|
||||||
|
template_name="services/services_overview.html",
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@services_view(
|
||||||
|
paths="<str:service_slug>/",
|
||||||
|
name="detail",
|
||||||
|
login_required=True,
|
||||||
|
)
|
||||||
|
def service_detail(request, service_slug):
|
||||||
|
service = ServiceRegistry.get(slug=service_slug)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"service": service,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request=request,
|
||||||
|
template_name="services/service_detail.html",
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@services_view(
|
||||||
|
paths="<str:service_slug>/subscribe/",
|
||||||
|
name="subscribe",
|
||||||
|
login_required=True,
|
||||||
|
)
|
||||||
|
def service_subscribe(request, service_slug):
|
||||||
|
service = ServiceRegistry.get(slug=service_slug)
|
||||||
|
|
||||||
|
# TODO: add a form to subscribe to the service
|
||||||
|
context = {
|
||||||
|
"service": service,
|
||||||
|
"base_path": "services:list",
|
||||||
|
"form": service.get_form(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request=request,
|
||||||
|
template_name="services/service_subscribe.html",
|
||||||
|
context=context,
|
||||||
|
)
|
|
@ -0,0 +1 @@
|
||||||
|
"""Utility functions for the project."""
|
|
@ -1,8 +1,12 @@
|
||||||
|
"""Mixins for models."""
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class CreatedModifiedAbstract(models.Model):
|
class CreatedModifiedAbstract(models.Model):
|
||||||
|
"""Abstract model to track creation and modification of objects."""
|
||||||
|
|
||||||
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"))
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
"""Utility template tags for the project."""
|
|
@ -1,3 +1,7 @@
|
||||||
|
"""Custom template tags for the project."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
@ -5,7 +9,7 @@ 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: dict[str, Any], path_name: str, class_name: str) -> str | None:
|
||||||
"""Return the given class name if the current path matches the given path 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
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
"""Utility views for rendering lists of objects."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
@ -6,14 +10,15 @@ from typing import Any
|
||||||
from django.contrib.sites.shortcuts import get_current_site
|
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.http import HttpRequest
|
|
||||||
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 queries_disabled
|
||||||
from zen_queries import render as zen_queries_render
|
from zen_queries import render as zen_queries_render
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
|
from django.db.models import QuerySet
|
||||||
|
from django.http import HttpRequest
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -32,7 +37,7 @@ class RowAction:
|
||||||
url_name: str
|
url_name: str
|
||||||
url_kwargs: dict[str, str]
|
url_kwargs: dict[str, str]
|
||||||
|
|
||||||
def render(self, obj) -> dict[str, str]:
|
def render(self, obj: Model) -> 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(
|
url = reverse(
|
||||||
self.url_name,
|
self.url_name,
|
||||||
|
@ -41,63 +46,77 @@ class RowAction:
|
||||||
return {"label": self.label, "url": url}
|
return {"label": self.label, "url": url}
|
||||||
|
|
||||||
|
|
||||||
def render_list(
|
@dataclass(kw_only=True)
|
||||||
request: HttpRequest,
|
class RenderConfig:
|
||||||
entity_name: str,
|
"""Configuration for rendering a list of objects."""
|
||||||
entity_name_plural: str,
|
|
||||||
objects: list["Model"],
|
|
||||||
columns: list[tuple[str, str]],
|
|
||||||
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."""
|
|
||||||
# TODO: List actions
|
|
||||||
|
|
||||||
total_count = len(objects)
|
entity_name: str
|
||||||
|
entity_name_plural: str
|
||||||
|
objects: QuerySet
|
||||||
|
columns: list[tuple[str, str]]
|
||||||
|
row_actions: list[RowAction] | None = None
|
||||||
|
list_actions: list[tuple[str, str]] | None = None
|
||||||
|
paginate_by: int | None = None
|
||||||
|
|
||||||
order_by = request.GET.get("order_by")
|
def render_list(
|
||||||
|
self,
|
||||||
|
request: HttpRequest,
|
||||||
|
) -> HttpResponse:
|
||||||
|
"""Render a list of objects with a table."""
|
||||||
|
# TODO: List actions
|
||||||
|
|
||||||
if order_by:
|
entity_name = self.entity_name
|
||||||
with contextlib.suppress(FieldError):
|
entity_name_plural = self.entity_name_plural
|
||||||
objects = objects.order_by(order_by)
|
objects = self.objects
|
||||||
|
columns = self.columns
|
||||||
|
row_actions = self.row_actions or []
|
||||||
|
list_actions = self.list_actions or []
|
||||||
|
paginate_by = self.paginate_by
|
||||||
|
|
||||||
if paginate_by:
|
total_count = len(objects)
|
||||||
paginator = Paginator(object_list=objects, per_page=paginate_by)
|
|
||||||
page = paginator.get_page(request.GET.get("page"))
|
|
||||||
objects = page.object_list
|
|
||||||
|
|
||||||
rows = []
|
order_by = request.GET.get("order_by")
|
||||||
for obj in objects:
|
|
||||||
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 = {
|
if order_by:
|
||||||
"rows": rows,
|
with contextlib.suppress(FieldError):
|
||||||
"columns": columns,
|
objects = objects.order_by(order_by)
|
||||||
"row_actions": row_actions,
|
|
||||||
"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:
|
if paginate_by:
|
||||||
context |= {
|
paginator = Paginator(object_list=objects, per_page=paginate_by)
|
||||||
"page": page,
|
page = paginator.get_page(request.GET.get("page"))
|
||||||
"is_paginated": True,
|
objects = page.object_list
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for obj in objects:
|
||||||
|
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,
|
||||||
|
"columns": columns,
|
||||||
|
"row_actions": row_actions,
|
||||||
|
"list_actions": list_actions,
|
||||||
|
"total_count": total_count,
|
||||||
|
"order_by": order_by,
|
||||||
|
"entity_name": entity_name,
|
||||||
|
"entity_name_plural": entity_name_plural,
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(
|
if paginate_by:
|
||||||
request=request,
|
context |= {
|
||||||
template_name="utils/list.html",
|
"page": page,
|
||||||
context=context,
|
"is_paginated": True,
|
||||||
)
|
}
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request=request,
|
||||||
|
template_name="utils/list.html",
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def base_context(request: HttpRequest) -> dict[str, Any]:
|
def base_context(request: HttpRequest) -> dict[str, Any]:
|
||||||
|
@ -105,7 +124,7 @@ def base_context(request: HttpRequest) -> dict[str, Any]:
|
||||||
return {"site": get_current_site(request)}
|
return {"site": get_current_site(request)}
|
||||||
|
|
||||||
|
|
||||||
def render(request, template_name, context=None):
|
def render(request: HttpRequest, template_name: str, context: dict[str, Any] | None = None) -> HttpResponse:
|
||||||
"""Render a template with a base context."""
|
"""Render a template with a base context."""
|
||||||
if context is None:
|
if context is None:
|
||||||
context = {}
|
context = {}
|
||||||
|
|
Loading…
Reference in a new issue