New WaitingListEntry #33
|
@ -3,6 +3,7 @@
|
||||||
*/.*
|
*/.*
|
||||||
|
|
||||||
!src/
|
!src/
|
||||||
|
!requirements.txt
|
||||||
!requirements/
|
!requirements/
|
||||||
!entrypoint.sh
|
!entrypoint.sh
|
||||||
!pyproject.toml
|
!pyproject.toml
|
||||||
|
|
|
@ -23,14 +23,14 @@ repos:
|
||||||
- --fix
|
- --fix
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v3.15.2
|
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.17.0
|
rev: 1.19.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: django-upgrade
|
- id: django-upgrade
|
||||||
args:
|
args:
|
||||||
|
|
12
Dockerfile
12
Dockerfile
|
@ -9,12 +9,16 @@ ENV PYTHONFAULTHANDLER=1 \
|
||||||
PIP_DEFAULT_TIMEOUT=100
|
PIP_DEFAULT_TIMEOUT=100
|
||||||
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
|
||||||
&& apt-get update \
|
COPY --chown=www:www . .
|
||||||
&& apt-get install -y \
|
RUN mkdir /app/src/static && \
|
||||||
|
chown www:www /app/src/static && \
|
||||||
|
apt-get update && \
|
||||||
|
apt-get install -y \
|
||||||
binutils \
|
binutils \
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
build-essential \
|
build-essential \
|
||||||
|
@ -32,7 +36,7 @@ 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
|
||||||
|
|
||||||
RUN pip install . \
|
RUN pip install --no-cache-dir -r $REQUIREMENTS_FILE && \
|
||||||
&& django-admin compilemessages
|
&& django-admin compilemessages
|
||||||
|
|
||||||
ENTRYPOINT ["./entrypoint.sh"]
|
ENTRYPOINT ["./entrypoint.sh"]
|
||||||
|
|
4
Makefile
4
Makefile
|
@ -2,9 +2,7 @@ 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_CONTAINER_NAME = backend
|
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
|
||||||
|
|
82
README.md
82
README.md
|
@ -1,71 +1,73 @@
|
||||||
# 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
|
### Using Docker Compose
|
||||||
|
|
||||||
### Docker
|
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 .
|
```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,9 +1,9 @@
|
||||||
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:
|
||||||
|
@ -34,3 +34,4 @@ services:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
...
|
||||||
|
|
|
@ -12,13 +12,13 @@ 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.6",
|
"Django==5.0.7",
|
||||||
"django-money==3.4.1",
|
"django-money==3.5.2",
|
||||||
"django-allauth==0.63.3",
|
"django-allauth==0.63.6",
|
||||||
"psycopg[binary]==3.1.19",
|
"psycopg[binary]==3.2.1",
|
||||||
"environs[django]==11.0.0",
|
"environs[django]==11.0.0",
|
||||||
"uvicorn==0.30.0",
|
"uvicorn==0.30.1",
|
||||||
"whitenoise==6.6.0",
|
"whitenoise==6.7.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",
|
||||||
|
@ -29,7 +29,14 @@ 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.6
|
||||||
|
# - django-money==3.5.2
|
||||||
|
# - 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.7
|
||||||
|
# - environs[django]==11.0.0
|
||||||
|
# - psycopg[binary]==3.2.1
|
||||||
|
# - uvicorn==0.30.1
|
||||||
|
# - whitenoise==6.7.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.7
|
||||||
|
# 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.6
|
||||||
|
# via hatch.envs.default
|
||||||
|
django-cache-url==3.4.5
|
||||||
|
# via environs
|
||||||
|
django-money==3.5.2
|
||||||
|
# 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.2.1
|
||||||
|
# via hatch.envs.default
|
||||||
|
psycopg-binary==3.2.1
|
||||||
|
# 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.1
|
||||||
|
# via django
|
||||||
|
typing-extensions==4.12.2
|
||||||
|
# via
|
||||||
|
# dj-database-url
|
||||||
|
# jwcrypto
|
||||||
|
# psycopg
|
||||||
|
# py-moneyed
|
||||||
|
urllib3==2.2.2
|
||||||
|
# via requests
|
||||||
|
uvicorn==0.30.1
|
||||||
|
# via hatch.envs.default
|
||||||
|
whitenoise==6.7.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.6
|
||||||
|
# - django-money==3.5.2
|
||||||
|
# - 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.7
|
||||||
|
# - environs[django]==11.0.0
|
||||||
|
# - psycopg[binary]==3.2.1
|
||||||
|
# - uvicorn==0.30.1
|
||||||
|
# - whitenoise==6.7.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.7
|
||||||
|
# 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.6
|
||||||
|
# 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.5.2
|
||||||
|
# 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.2.1
|
||||||
|
# via
|
||||||
|
# hatch.envs.dev
|
||||||
|
# psycopg
|
||||||
|
psycopg-binary==3.2.1
|
||||||
|
# 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.1
|
||||||
|
# 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.1
|
||||||
|
# via hatch.envs.dev
|
||||||
|
wheel==0.43.0
|
||||||
|
# via pip-tools
|
||||||
|
whitenoise==6.7.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,3 +1,5 @@
|
||||||
|
"""Admin configuration for membership app."""
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
benjaoming marked this conversation as resolved
Outdated
|
|||||||
|
@ -5,19 +7,23 @@ from . import models
|
||||||
|
|
||||||
@admin.register(models.Membership)
|
@admin.register(models.Membership)
|
||||||
class MembershipAdmin(admin.ModelAdmin):
|
class MembershipAdmin(admin.ModelAdmin):
|
||||||
pass
|
"""Admin for Membership model."""
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.MembershipType)
|
@admin.register(models.MembershipType)
|
||||||
class MembershipTypeAdmin(admin.ModelAdmin):
|
class MembershipTypeAdmin(admin.ModelAdmin):
|
||||||
pass
|
"""Admin for MembershipType model."""
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.SubscriptionPeriod)
|
@admin.register(models.SubscriptionPeriod)
|
||||||
class SubscriptionPeriodAdmin(admin.ModelAdmin):
|
class SubscriptionPeriodAdmin(admin.ModelAdmin):
|
||||||
|
"""Admin for SubscriptionPeriod model."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.WaitingListEntry)
|
@admin.register(models.WaitingListEntry)
|
||||||
class WaitingListEntryAdmin(admin.ModelAdmin):
|
class WaitingListEntryAdmin(admin.ModelAdmin):
|
||||||
|
"""Admin for WaitingList 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)
|
||||||
|
|
|
@ -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
|
||||||
|
@ -8,15 +13,24 @@ from django.utils.translation import gettext as _
|
||||||
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 +48,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 +70,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 +102,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,21 +116,27 @@ 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
|
||||||
|
|
||||||
|
|
|
@ -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 _
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,13 +1,24 @@
|
||||||
|
"""Project views."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from django_view_decorator import view
|
from django_view_decorator import view
|
||||||
from utils.view_utils import render
|
from utils.view_utils import render
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,5 +27,6 @@ def index(request):
|
||||||
name="services",
|
name="services",
|
||||||
login_required=True,
|
login_required=True,
|
||||||
)
|
)
|
||||||
def services_overview(request):
|
def services_overview(request: HttpRequest) -> HttpResponse:
|
||||||
|
"""View to show the services overview."""
|
||||||
return render(request, "services_overview.html")
|
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,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
I'm against importing whole modules.
Don't we import whole modules everywhere all the time?
The idea is that the Django app is a domain-specific app so importing the models into the admin makes sense because almost all Models from that models module will have an admin. To me, this makes the code more readable than having lots of import statements.
Well, yes. I mostly use for instance
from django.db import models
in my ownmodels
modules, because it is a standard - but I actually would love to be more explicit everywhere.When push comes to shove it is all about preference. My preference is to explicitly import what is being used, to have a clear "in this module we are using these external things".
I also use
from django.db import models
inapp.models
but inapp.*
I usefrom app import models
because it's so convenient and it targets the domain-specific ideal of the app. As in, if you are import some othermodels
module, then something is wrong.I think having explicit imports is more readable and gives me an overview of what the module is interfacing with.
I think we can debate this forever :P
I would say that we should go with "do not change it just for the sake of changing it" and keep it as it was (but that is also in favorable for me ATM)
Alright you stubborn one ❤️