Compare commits

...

94 Commits
master ... main

Author SHA1 Message Date
Víðir Valberg Guðmundsson 712c50fac7 Use hatch for installing.
continuous-integration/drone/push Build is passing Details
2024-03-03 11:05:21 +01:00
Halfdan Mouritzen fedfca25a5 Adding flow to add new emails to an account
continuous-integration/drone/push Build is passing Details
2024-03-02 17:43:16 +01:00
Halfdan Mouritzen 89d7c9c9d5 CSS should be merged correctly now
continuous-integration/drone/push Build is passing Details
2024-03-02 14:49:50 +01:00
Halfdan Mouritzen f5acfbedd4 Cleaning 2024-03-02 12:15:33 +01:00
Víðir Valberg Guðmundsson cf3c84b8d9 More linting.
continuous-integration/drone/push Build is passing Details
2024-02-29 21:30:36 +01:00
Víðir Valberg Guðmundsson 1b65558608 Lint galore. Also use python 3.12. 2024-02-29 21:28:17 +01:00
Halfdan Mouritzen 4112069cac feature/css (#29)
continuous-integration/drone/push Build is passing Details
Reviewed-on: data.coop/membersystem#29
Co-authored-by: Halfdan Mouritzen <halfdan@robothangarskib.dk>
Co-committed-by: Halfdan Mouritzen <halfdan@robothangarskib.dk>
2024-02-29 20:02:39 +00:00
Víðir Valberg Guðmundsson bdc2d8717c Add devenv files.
continuous-integration/drone/push Build is passing Details
2024-02-10 10:55:38 +01:00
Víðir Valberg Guðmundsson f99c7ee698 Fetch permissions before rendering templates.
continuous-integration/drone/push Build is passing Details
2024-02-10 10:50:23 +01:00
Víðir Valberg Guðmundsson a098a0b032 Add logging.
continuous-integration/drone/push Build is passing Details
2024-02-09 22:04:30 +01:00
Víðir Valberg Guðmundsson f31cd62351 Implement django-view-decorator
continuous-integration/drone/push Build is passing Details
2024-01-14 12:27:36 +01:00
Víðir Valberg Guðmundsson b8a970d5fe Add missing migration 2024-01-14 12:14:51 +01:00
Halfdan Mouritzen 9dda7670df Minimal CSS for tables (#26)
continuous-integration/drone/push Build is passing Details
Reviewed-on: data.coop/membersystem#26
Co-authored-by: Halfdan Mouritzen <halfdan@robothangarskib.dk>
Co-committed-by: Halfdan Mouritzen <halfdan@robothangarskib.dk>
2024-01-14 11:10:55 +00:00
Halfdan Mouritzen 02907a7684 Minimal CSS for tables
continuous-integration/drone/pr Build is failing Details
2024-01-13 22:14:23 +01:00
valberg 33f5e7a285 ui-overhaul (#24)
continuous-integration/drone/push Build is passing Details
Co-authored-by: Halfdan <halm@itu.dk>
Co-authored-by: Halfdan Mouritzen <halfdan@robothangarskib.dk>
Reviewed-on: data.coop/membersystem#24
2024-01-13 20:07:36 +00:00
Víðir Valberg Guðmundsson 2ed5df5241 Add pyproject.toml file. 2024-01-13 15:39:31 +01:00
Víðir Valberg Guðmundsson 9b52cd4270 Update stuff. 2024-01-07 15:04:08 +01:00
Víðir Valberg Guðmundsson 25445e59cf Add i18n to builtin template tags.
continuous-integration/drone/push Build was killed Details
2023-10-02 21:19:25 +02:00
Víðir Valberg Guðmundsson f42101c476 Adding some titles
continuous-integration/drone/push Build is passing Details
2023-10-02 20:50:39 +02:00
Víðir Valberg Guðmundsson 88e15974dc Fixing some stuff with README
continuous-integration/drone/push Build is passing Details
2023-10-02 20:10:24 +02:00
Víðir Valberg Guðmundsson 81ff8d0177 Upgrade pre-commit hooks.
continuous-integration/drone/push Build is passing Details
2023-10-02 19:56:38 +02:00
Víðir Valberg Guðmundsson 6894c57aa0 Upgrade dependencies. 2023-10-02 19:56:09 +02:00
Víðir Valberg Guðmundsson d2e58d396f Small adjustments.
continuous-integration/drone/push Build is passing Details
2023-09-30 08:14:02 +02:00
Víðir Valberg Guðmundsson 770c1c81b6 Fix member list.
continuous-integration/drone/push Build is passing Details
2023-09-18 21:14:47 +02:00
Víðir Valberg Guðmundsson f1c328a10c Upgrading stuff.
continuous-integration/drone/push Build is passing Details
2023-09-18 20:58:30 +02:00
Víðir Valberg Guðmundsson beabd49976 Fix membership page
continuous-integration/drone/push Build is passing Details
2023-09-16 16:27:15 +02:00
Víðir Valberg Guðmundsson bab951afb8 Add missing migration
continuous-integration/drone/push Build is passing Details
2023-09-16 16:19:51 +02:00
Víðir Valberg Guðmundsson 390954ebaf Use docker compose, not docker-compose.
continuous-integration/drone/push Build is passing Details
2023-09-16 15:39:13 +02:00
Víðir Valberg Guðmundsson 175f0438d4 Update logo.
continuous-integration/drone/push Build is passing Details
2023-09-16 15:28:42 +02:00
Víðir Valberg Guðmundsson e6661c1b5a Minor detail.
continuous-integration/drone/push Build is passing Details
2023-02-08 08:19:58 +01:00
Víðir Valberg Guðmundsson b9f77251f1 Fix site context variable with a context processor.
continuous-integration/drone/push Build is passing Details
2023-01-21 16:20:50 +01:00
Víðir Valberg Guðmundsson 64ba5cc6f4 Begin making member admin page more detailed.
continuous-integration/drone/push Build is passing Details
2023-01-14 23:33:58 +01:00
Víðir Valberg Guðmundsson 659577aaac Pagination and ordering.
continuous-integration/drone/push Build is passing Details
2023-01-12 17:31:34 +01:00
Víðir Valberg Guðmundsson 26e06dfaea Upgrade to python 3.11. Update pre-commit. Small settings adjustments.
continuous-integration/drone/push Build is passing Details
2023-01-11 21:55:58 +01:00
Víðir Valberg Guðmundsson e38bd56549 Pre-commit autoupdate and run on --all-files.
continuous-integration/drone/push Build is passing Details
2023-01-03 21:39:40 +01:00
Víðir Valberg Guðmundsson 704b196128 Implement a generic way to adding actions to rows in a list. 2023-01-03 21:36:34 +01:00
Víðir Valberg Guðmundsson cf99c3f40e We want a list of users, not memberships.
continuous-integration/drone/push Build is passing Details
2023-01-03 17:00:07 +01:00
Víðir Valberg Guðmundsson bc00b32c46 Add the start for administration of memberships.
continuous-integration/drone/push Build is passing Details
2023-01-02 23:06:00 +01:00
Víðir Valberg Guðmundsson 01715a7704 Introduce subscription periods.
continuous-integration/drone/push Build is passing Details
2023-01-02 22:13:59 +01:00
Víðir Valberg Guðmundsson 282b500a5e Doh...
continuous-integration/drone/push Build is passing Details
2022-11-25 00:06:19 +01:00
Víðir Valberg Guðmundsson a3643ca602 Push to a non-dotted repo.
continuous-integration/drone/push Build is passing Details
2022-11-24 23:55:01 +01:00
Víðir Valberg Guðmundsson 17cd950a4e Clean .drone.yml (aka trigger build).
continuous-integration/drone/push Build is passing Details
2022-11-24 23:42:44 +01:00
Víðir Valberg Guðmundsson 3e8c773826 Forgot the bootstrap icon css.
continuous-integration/drone/push Build is passing Details
2022-11-22 21:36:31 +01:00
Víðir Valberg Guðmundsson c946cabd39 Include entrypoint.sh in the docker image.
continuous-integration/drone/push Build is passing Details
2022-11-22 17:12:06 +01:00
Víðir Valberg Guðmundsson 3ba2038d2c Upgrade bootstrap to 5.2.3.
continuous-integration/drone/push Build is passing Details
2022-11-22 17:02:05 +01:00
Víðir Valberg Guðmundsson 4c5803545d Switch out poetry with pip-tools.
continuous-integration/drone/push Build is passing Details
2022-11-21 20:51:09 +01:00
Víðir Valberg Guðmundsson 3dde14614f Fixes and cleanup.
continuous-integration/drone/push Build is passing Details
2022-08-09 19:29:42 +02:00
Víðir Valberg Guðmundsson 4c5d0e30ab Poetry lock. Bump django to 4.1.
continuous-integration/drone/push Build is passing Details
2022-08-09 19:17:29 +02:00
Víðir Valberg Guðmundsson f8688de23a Debug toolbar is only available when DEBUG is True.
continuous-integration/drone/push Build is failing Details
2022-08-09 16:33:56 +02:00
Víðir Valberg Guðmundsson 5d3f924873 Get build args from env.
continuous-integration/drone/push Build is passing Details
2022-08-09 16:26:31 +02:00
Víðir Valberg Guðmundsson a0afe20c54 Set BUILD environment.
continuous-integration/drone/push Build is passing Details
2022-08-09 15:50:38 +02:00
Víðir Valberg Guðmundsson 916e3694c6 Get CSRF_TRUSTED_ORIGINS from env.
continuous-integration/drone/push Build is passing Details
2022-08-09 14:58:51 +02:00
Víðir Valberg Guðmundsson a8b37005ed Have a latest tag.
continuous-integration/drone/push Build is passing Details
2022-08-09 14:46:05 +02:00
Víðir Valberg Guðmundsson 1c2854c517 Pre-commit update and run.
continuous-integration/drone/push Build is passing Details
2022-08-09 14:39:02 +02:00
valberg 4b999e886e Update '.drone.yml' 2022-08-09 11:53:39 +00:00
Víðir Valberg Guðmundsson 0278b2b48e Upgrade django, python, etc.
continuous-integration/drone/push Build is passing Details
2021-12-13 20:44:09 +01:00
valberg 50e2ba642f Merge pull request 'Refactoring things and doing stuff in a MVP way.' (#15) from vidir_refactor into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: data.coop/membersystem#15
2021-03-12 16:20:50 +00:00
Víðir Valberg Guðmundsson 16db4ee143 Setting base template for password reset flow. Some translations.
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2021-03-12 17:16:13 +01:00
Víðir Valberg Guðmundsson 141019ed75 Add DEFAULT_FROM_EMAIL.
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2021-03-03 18:06:36 +01:00
Víðir Valberg Guðmundsson f585299388 It should be a touple.
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2021-03-03 17:46:19 +01:00
Víðir Valberg Guðmundsson e725729bb3 Fix admins settings.
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2021-03-03 17:32:34 +01:00
Víðir Valberg Guðmundsson ba7fabf5c7 Add ADMINS settings.
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2021-03-03 15:18:57 +01:00
Víðir Valberg Guðmundsson 79f8903c2c Add email configuration.
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2021-03-03 14:58:46 +01:00
Víðir Valberg Guðmundsson 5b7b95d426 Add a link to password reset view. Fixup that template.
continuous-integration/drone/push Build is running Details
continuous-integration/drone/pr Build is passing Details
2021-03-03 10:36:18 +01:00
Víðir Valberg Guðmundsson e45f83838b Fix static file integrity.
continuous-integration/drone/push Build is running Details
continuous-integration/drone/pr Build is passing Details
2021-03-01 23:20:59 +01:00
Víðir Valberg Guðmundsson 55fd4cdd6c Expose port 8000 in the image.
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2021-03-01 21:44:57 +01:00
Víðir Valberg Guðmundsson 72c4020d96 Add whitenoise.
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build encountered an error Details
2021-03-01 20:34:51 +01:00
Víðir Valberg Guðmundsson cceaf25c6b Uvicorn has --app-dir, which might do the trick.
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2021-03-01 17:49:37 +01:00
Víðir Valberg Guðmundsson cbdbd7f7cf Set WORKDIR to /app/src before setting CMD.
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is running Details
2021-03-01 17:35:43 +01:00
Víðir Valberg Guðmundsson fb00df7274 Fix docker CMD. Add asgi.py. Fix static root.
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2021-03-01 17:13:15 +01:00
Víðir Valberg Guðmundsson ea3039a279 Fix entrypoint paths. Add uvicorn. Set correct port.
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2021-03-01 14:13:19 +01:00
Víðir Valberg Guðmundsson ace2e46dd4 Add drone config.
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2021-03-01 13:46:16 +01:00
Víðir Valberg Guðmundsson f35a31f259 Enable buildkit when building. 2021-03-01 00:20:59 +01:00
Víðir Valberg Guðmundsson 0bad2a599c Update readme. Make Makefile more nice. 2021-03-01 00:15:01 +01:00
Víðir Valberg Guðmundsson cf33b95201 Getting ready for some translations. 2021-02-28 23:55:01 +01:00
Víðir Valberg Guðmundsson 76478c5ae3 Ran pre-commit hooks. 2021-02-28 23:03:32 +01:00
Víðir Valberg Guðmundsson e41eae340b Revamp Membership models to be very simple and do some view/template work for them. 2021-02-28 23:01:24 +01:00
Víðir Valberg Guðmundsson 85e2fd76c0 Some template work. 2021-02-28 12:41:10 +01:00
Víðir Valberg Guðmundsson aac3de1bb3 Starting allauth template work. 2021-02-27 23:18:27 +01:00
Víðir Valberg Guðmundsson 4c75991fcb Refactoring things and doing stuff WIP way. 2021-02-27 21:07:48 +01:00
valberg 2be2755cb6 Merge pull request 'docker: Add entrypoint.sh' (#14) from laumann/membersystem:docker-entrypoint into master
Reviewed-on: data.coop/membersystem#14
2021-02-10 06:45:45 +00:00
valberg ecea9c7b3f Merge pull request 'Add psycopg2 as dependency' (#13) from laumann/membersystem:add-psycopg2-dep into master
Reviewed-on: data.coop/membersystem#13
2021-02-10 06:43:59 +00:00
Thomas Jespersen 6857bc69b5
docker: Add entrypoint.sh
Apparently I forgot to add this file in previous commits - the initial
shape is provided by @valberg, I added some defaults for
POSTGRES_{HOST,PORT}.
2021-02-09 23:01:15 +01:00
Thomas Jespersen 10f23d1ad8
Add psycopg2 as dependency 2021-02-09 22:36:34 +01:00
valberg 9093b393f1 Merge pull request 'Add initial Dockerfile' (#12) from laumann/membersystem:dockerize into master
Reviewed-on: data.coop/membersystem#12
2021-02-09 21:10:48 +00:00
Thomas Jespersen c6eb8d884b Add initial Dockerfile
Based on python3.9-slim-buster.
2021-02-09 22:04:02 +01:00
valberg ca5f2d41ba Merge pull request 'Upgrade various packages to most recent versions' (#11) from feature/package-upgrades into master
Reviewed-on: data.coop/membersystem#11
2021-01-26 19:44:33 +00:00
Mikkel Munch Mortensen 1a66dbff3a
Upgrade various packages to most recent versions
* Django
 * django-money
 * pre-commit

Also fix a few minor issues:
 * Front page was broken, as `render_to_response` is no longer in
   Django, but replaced by `render`.
 * Base template had a link to a "change e-mail" page that no longer
   exists. Linking to "change password" instead. At least for now.
2021-01-26 20:37:50 +01:00
benjaoming 5acff9e382 Merge branch 'use_poetry' of data.coop/membersystem into master 2019-09-02 11:58:18 +00:00
Víðir Valberg Guðmundsson 07170082e3 Remove setup.cfg, move relevant flake8 arguments into pre-commit config. Update pre-commit hooks. 2019-09-01 09:45:45 +02:00
Víðir Valberg Guðmundsson c4596c25c6 Run linting (aka the blackening) 2019-09-01 00:27:36 +02:00
Víðir Valberg Guðmundsson a08d6e044b Use poetry to control the projects dependencies. Also setup black for pre-commit. 2019-09-01 00:25:25 +02:00
benjaoming 90cbd74212 Merge branch 'master' of benjaoming/membersystem into master 2019-08-31 18:56:19 +00:00
benjaoming c429f922b2 Merge branch 'master' of benjaoming/membersystem into master 2019-08-31 17:42:30 +00:00
174 changed files with 4811 additions and 1220 deletions

9
.dockerignore Normal file
View File

@ -0,0 +1,9 @@
*
.*
*/.*
!src/
!requirements/
!entrypoint.sh
!pyproject.toml
!README.md

27
.drone.yml Normal file
View File

@ -0,0 +1,27 @@
---
kind: pipeline
type: docker
name: default
steps:
- name: docker
image: plugins/docker
environment:
DJANGO_ENV: production
BUILD: "${DRONE_COMMIT_SHA}"
settings:
repo: docker.data.coop/membersystem
registry: docker.data.coop
username:
from_secret: DOCKER_USERNAME
password:
from_secret: DOCKER_PASSWORD
build_args_from_env:
- DJANGO_ENV
- BUILD
tags:
- "${DRONE_BUILD_NUMBER}"
- "latest"
when:
branch:
- main

9
.env.example Normal file
View File

@ -0,0 +1,9 @@
SECRET_KEY=something-very-random
POSTGRES_HOST=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_PORT=5432
DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres
# Use something along the the following if you are not using docker
# DATABASE_URL=postgres://postgres:postgres@localhost:5432/datacoop_membersystem
DEBUG=True
DJANGO_ENV=development

6
.gitignore vendored
View File

@ -2,5 +2,9 @@ __pycache__/
*.pyc
*.sw*
db.sqlite3
project/settings/local.py
.pytest_cache
.idea/
*.mo
.env
venv/
.venv/

View File

@ -1,14 +1,37 @@
default_language_version:
python: python3.12
exclude: ^.*\b(migrations)\b.*$
repos:
- repo: git://github.com/pre-commit/pre-commit-hooks
rev: v1.3.0
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: flake8
- id: check-yaml
- id: check-added-large-files
- id: debug-statements
- id: end-of-file-fixer
- repo: https://github.com/asottile/reorder_python_imports
rev: v1.0.1
- id: check-ast
- id: check-merge-conflict
- id: check-case-conflict
- id: detect-private-key
- id: check-added-large-files
- id: check-json
- id: check-symlinks
- id: check-toml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: 'v0.3.0'
hooks:
- id: reorder-python-imports
- id: ruff
args:
- --fix
- id: ruff-format
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.1
hooks:
- id: pyupgrade
args:
- --py311-plus
exclude: migrations/
- repo: https://github.com/adamchainz/django-upgrade
rev: 1.16.0
hooks:
- id: django-upgrade
args:
- --target-version=5.0

40
Dockerfile Normal file
View File

@ -0,0 +1,40 @@
FROM python:3.12-slim-bullseye
ENV PYTHONFAULTHANDLER=1 \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONHASHSEED=random \
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100
ARG DJANGO_ENV
ARG BUILD
ENV BUILD ${BUILD}
WORKDIR /app
RUN groupadd -g 1000 www && useradd -u 1000 -ms /bin/bash -g www www
COPY --chown=www:www . .
RUN mkdir /app/src/static \
&& chown www:www /app/src/static \
&& apt-get update \
&& apt-get install -y \
binutils \
libpq-dev \
build-essential \
netcat-openbsd \
libcairo2 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libgdk-pixbuf2.0-0 \
libffi-dev \
shared-mime-info \
gettext \
&& pip install . \
&& django-admin compilemessages
ENTRYPOINT ["./entrypoint.sh"]
EXPOSE 8000
CMD ["uvicorn", "project.asgi:application", "--host", "0.0.0.0", "--port", "8000", "--workers", "3", "--lifespan", "off", "--app-dir", "/app/src"]

View File

@ -1,15 +1,58 @@
# These are just some make targets, expressing how things
# are supposed to be run, but feel free to change them!
DOCKER_COMPOSE = COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker compose
DOCKER_RUN = ${DOCKER_COMPOSE} run -u `id -u`
DOCKER_BUILD = DOCKER_BUILDKIT=1 docker build
DOCKER_CONTAINER_NAME = backend
MANAGE_EXEC = python /app/src/manage.py
MANAGE_COMMAND = ${DOCKER_RUN} ${DOCKER_CONTAINER_NAME} ${MANAGE_EXEC}
dev-setup:
pip install -r requirements_dev.txt --upgrade
pip install -r requirements.txt --upgrade
pre-commit install
python manage.py migrate
python manage.py createsuperuser
init: setup_venv pre_commit_install migrate
lint:
pre-commit run --all
run:
${DOCKER_COMPOSE} up
test:
pytest
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:
${MANAGE_COMMAND} makemigrations ${ARGS}
migrate:
${MANAGE_COMMAND} migrate ${ARGS}
createsuperuser:
${MANAGE_COMMAND} createsuperuser
shell:
${MANAGE_COMMAND} shell
manage_command:
${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

71
README.md Normal file
View File

@ -0,0 +1,71 @@
# member.data.coop
## Development
### Setup environment
Copy over the .env.example file to .env and adjust DATABASE_URL accordingly
$ cp .env.example .env
### Docker
#### Requirements
- Docker
- Docker compose
- pre-commit (preferred for contributions)
#### Setup
Given that the requirements above are installed, it should be as easy as:
$ make migrate
This will setup the database. Next run:
$ make run
This will build the docker image and start the member system on http://localhost:8000.
You can create a superuser by running:
$ make createsuperuser
Make migrations:
$ make makemigrations
Make messages:
$ make makemessages
Running tests:
$ make test
### Non-docker
Create a venv
$ python3 -m venv venv
Activate the venv
$ source venv/bin/activate
Install requirements
$ pip install -r requirements/dev.txt
Run migrations
$ ./src/manage.py migrate
Create a superuser
$ ./src/manage.py createsuperuser
Run the server
$ ./src/manage.py runserver

View File

@ -1,19 +0,0 @@
member.data.coop
================
To start developing:
# Create a virtualenv with python 3
$ mkvirtualenv -p python3 datacoop
# Run this make target, which installs all the requirements and sets up a
# development database.
$ make dev-setup
To run the Django development server:
$ python manage.py runserver
Before you push your stuff, run tests:
$ make test

View File

@ -1,84 +0,0 @@
# Generated by Django 2.0.6 on 2018-06-23 19:51
from decimal import Decimal
import django.db.models.deletion
import djmoney.models.fields
from django.conf import settings
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Account',
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')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Order',
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')),
('description', models.CharField(max_length=1024, verbose_name='description')),
('price_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default='XYZ', editable=False, max_length=3)),
('price', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0.0'), max_digits=16, verbose_name='price (excl. VAT)')),
('vat_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default='XYZ', editable=False, max_length=3)),
('vat', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0.0'), max_digits=16, verbose_name='VAT')),
('is_paid', models.BooleanField(default=False, verbose_name='is paid')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='accounting.Account')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Order',
'verbose_name_plural': 'Orders',
},
),
migrations.CreateModel(
name='Payment',
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')),
('amount_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default='XYZ', editable=False, max_length=3)),
('amount', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0.0'), max_digits=16)),
('description', models.CharField(max_length=1024, verbose_name='description')),
('stripe_charge_id', models.CharField(blank=True, max_length=255, null=True)),
('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='accounting.Order')),
],
options={
'verbose_name': 'payment',
'verbose_name_plural': 'payments',
},
),
migrations.CreateModel(
name='Transaction',
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')),
('amount_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default='XYZ', editable=False, max_length=3)),
('amount', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0.0'), help_text='This will include VAT', max_digits=16, verbose_name='amount')),
('description', models.CharField(max_length=1024, verbose_name='description')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transactions', to='accounting.Account')),
],
options={
'abstract': False,
},
),
]

241
devenv.lock Normal file
View File

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

19
devenv.nix Normal file
View File

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

5
devenv.yaml Normal file
View File

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

30
docker-compose.yml Normal file
View File

@ -0,0 +1,30 @@
version: '3.7'
services:
backend:
image: data_coop_membersystem:dev
build:
context: .
command: python /app/src/manage.py runserver 0.0.0.0:8000
tty: true
ports:
- "8000:8000"
volumes:
- ./:/app/
depends_on:
- postgres
env_file:
- .env
postgres:
image: postgres:13-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
ports:
- 5432:5432
env_file:
- .env
volumes:
postgres_data:

21
entrypoint.sh Executable file
View File

@ -0,0 +1,21 @@
#!/bin/sh
echo "Waiting for postgres..."
POSTGRES_PORT=${POSTGRES_PORT:-5432}
POSTGRES_HOST=${POSTGRES_HOST:-localhost}
while ! nc -z "$POSTGRES_HOST" "$POSTGRES_PORT"; do
sleep 0.1
done
echo "PostgreSQL started"
# Only migrate, collectstatic and compilemessages if we are NOT in development
if [ -z "$DEBUG" ]; then
python src/manage.py migrate;
python src/manage.py collectstatic --no-input;
python src/manage.py compilemessages;
fi
exec "$@"

View File

@ -1,8 +0,0 @@
from django.contrib import admin
from . import models
@admin.register(models.Membership)
class MembershipAdmin(admin.ModelAdmin):
pass

View File

@ -1,5 +0,0 @@
from django.apps import AppConfig
class MembershipConfig(AppConfig):
name = 'membership'

View File

@ -1,101 +0,0 @@
# Generated by Django 2.0.6 on 2018-06-23 19:07
from decimal import Decimal
import django.db.models.deletion
import djmoney.models.fields
from django.conf import settings
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Membership',
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')),
('can_vote', models.BooleanField(default=False, help_text='Indicates that the user has a democratic membership of the organization.', verbose_name='can vote')),
],
options={
'verbose_name': 'membership',
'verbose_name_plural': 'memberships',
},
),
migrations.CreateModel(
name='Organization',
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')),
('name', models.CharField(max_length=64, verbose_name='name')),
],
options={
'verbose_name': 'organization',
'verbose_name_plural': 'organizations',
},
),
migrations.CreateModel(
name='Subscription',
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')),
('active', models.BooleanField(default=False, help_text='Automatically set by payment system.', verbose_name='active')),
('starts', models.DateField()),
('ends', models.DateField()),
('renewed_subscription', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='membership.Subscription', verbose_name='renewed subscription')),
],
options={
'verbose_name': 'subscription',
'verbose_name_plural': 'subscriptions',
},
),
migrations.CreateModel(
name='SubscriptionType',
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')),
('name', models.CharField(max_length=64, verbose_name='name')),
('fee_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default='XYZ', editable=False, max_length=3)),
('fee', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0.0'), max_digits=16)),
('fee_vat_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default='XYZ', editable=False, max_length=3)),
('fee_vat', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0'), max_digits=16)),
('duration', models.PositiveSmallIntegerField(choices=[(1, 'annual')], default=1, verbose_name='duration')),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='membership.Organization')),
],
options={
'verbose_name': 'subscription type',
'verbose_name_plural': 'subscription types',
},
),
migrations.AddField(
model_name='subscription',
name='subscription_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='memberships', to='membership.SubscriptionType', verbose_name='subscription type'),
),
migrations.AddField(
model_name='subscription',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='membership',
name='organization',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='membership.Organization'),
),
migrations.AddField(
model_name='membership',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,144 +0,0 @@
from django.contrib.auth import get_user_model
from django.db import models
from django.utils.translation import gettext as _
from djmoney.models.fields import MoneyField
class CreatedModifiedAbstract(models.Model):
modified = models.DateTimeField(
auto_now=True,
verbose_name=_("modified"),
)
created = models.DateTimeField(
auto_now_add=True,
verbose_name=_("created"),
)
class Meta:
abstract = True
class Organization(CreatedModifiedAbstract):
"""
This holds the data of the organization that someone is a member of. It is
possible that we'll create more advanced features here.
"""
name = models.CharField(
verbose_name=_("name"),
max_length=64,
)
def __str__(self):
return self.name
class Meta:
verbose_name = _("organization")
verbose_name_plural = _("organizations")
class Membership(CreatedModifiedAbstract):
"""
A user remains a member of an organization even though the subscription is
unpaid or renewed. This just changes the status/permissions etc. of the
membership, thus we need to track subscription creation, expiry, renewals
etc. and ensure that the membership is modified accordingly.
This expresses some
"""
organization = models.ForeignKey(Organization, on_delete=models.PROTECT)
user = models.ForeignKey(get_user_model(), on_delete=models.PROTECT)
can_vote = models.BooleanField(
default=False,
verbose_name=_("can vote"),
help_text=_(
"Indicates that the user has a democratic membership of the "
"organization."
)
)
def __str__(self):
return _("{} is a member of {}").format(
self.user.get_full_name(),
self.organization.name,
)
class Meta:
verbose_name = _("membership")
verbose_name_plural = _("memberships")
class SubscriptionType(CreatedModifiedAbstract):
"""
Properties of subscriptions are stored here. Should of course not be edited
after subscriptions are created.
"""
organization = models.ForeignKey(Organization, on_delete=models.PROTECT)
name = models.CharField(
verbose_name=_("name"),
max_length=64,
)
fee = MoneyField(
max_digits=16,
decimal_places=2,
)
fee_vat = MoneyField(
max_digits=16,
decimal_places=2,
default=0,
)
duration = models.PositiveSmallIntegerField(
default=1,
choices=[(1, _("annual"))],
verbose_name=_("duration"),
)
class Meta:
verbose_name = _("subscription type")
verbose_name_plural = _("subscription types")
class Subscription(CreatedModifiedAbstract):
"""
To not confuse other types of subscriptions, one can be a *subscribed*
member of an organization, meaning that they are paying etc.
A subscription does not track payment, this is done in the accounting app.
"""
subscription_type = models.ForeignKey(
SubscriptionType,
related_name='memberships',
verbose_name=_("subscription type"),
on_delete=models.PROTECT,
)
user = models.ForeignKey(get_user_model(), on_delete=models.PROTECT)
active = models.BooleanField(
default=False,
verbose_name=_("active"),
help_text=_("Automatically set by payment system.")
)
starts = models.DateField()
ends = models.DateField()
renewed_subscription = models.ForeignKey(
'self',
null=True,
blank=True,
verbose_name=_("renewed subscription"),
on_delete=models.PROTECT,
)
class Meta:
verbose_name = _("subscription")
verbose_name_plural = _("subscriptions")

View File

@ -1,9 +0,0 @@
"""Context processors for the membersystem app."""
from django.contrib.sites.shortcuts import get_current_site
def current_site(request):
"""Include the current site in the context."""
return {
'site': get_current_site(request)
}

View File

@ -1,14 +0,0 @@
import warnings
from .base import * # noqa
try:
from .local import * # noqa
except ImportError:
warnings.warn(
"No settings.local, using a default SECRET_KEY 'hest'. You should "
"write a custom local.py with this setting."
)
SECRET_KEY = "hest"
DEBUG = True
pass

View File

@ -1,137 +0,0 @@
"""
Django settings for membersystem project.
Generated by 'django-admin startproject' using Django 2.0.4.
For more information on this file, see
https://docs.djangoproject.com/en/2.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.0/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'users',
'accounting',
'membership',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'project.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join("project", "templates")],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'project.context_processors.current_site',
],
},
},
]
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
)
WSGI_APPLICATION = 'project.wsgi.application'
# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', # noqa
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', # noqa
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', # noqa
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', # noqa
},
]
AUTH_USER_MODEL = 'users.User'
# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
SITE_ID = 1
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
CURRENCIES = ('DKK',)
CURRENCY_CHOICES = [('DKK', 'DKK')]

View File

@ -1,79 +0,0 @@
/* General styles */
html
{
margin: 0;
padding: 0;
font-family: sans-serif;
font-size: 2.5vmin;
background: #f8f8f8;
}
body
{
background: #fff;
color: #000;
margin: 1em auto;
max-width: 50em;
padding: 0 1em;
box-shadow: 0 0 2.5em rgba(0, 0, 0, 20%);
}
header,
footer
{
background: #eee;
padding: .5em;
margin: 0 -1em;
}
footer
{
margin-top: 2em;
}
header h1
{
font-size: 1em;
float: left;
padding: .5em .5em;
margin: 0;
}
header ul,
footer ul
{
list-style-type: none;
padding: 0;
margin: 0;
text-align: right;
}
header ul li,
footer ul li
{
display: inline;
}
header ul li a,
footer ul li a
{
display: inline-block;
margin: 0;
padding: .5em .5em;
}
/* Forms */
label
{
display: block;
padding: .5em 0;
}
button,
input,
textarea
{
font-size: inherit;
}

View File

@ -1,49 +0,0 @@
<!DOCTYPE html>
{% load static %}
<html>
<head>
<title>{% block head_title %}{% endblock %} {{ site.name }}</title>
{% block extra_head %}{% endblock %}
<link rel="stylesheet" href="{% static '/css/membersystem.css' %}" type="text/css" />
</head>
<body>
<header>
<h1>
<a href="/">{{ site.name }}</a>
</h1>
<ul>
{% if user.is_authenticated %}
<li><a href="{% url 'users:email' %}">Change e-mail</a></li>
<li><a href="{% url 'users:logout' %}">Sign out</a></li>
{% else %}
<li><a href="{% url 'users:login' %}">Sign in</a></li>
<li><a href="{% url 'users:signup' %}">Sign up</a></li>
{% endif %}
</ul>
</header>
{% block body %}
{% if messages %}
<ul id="messages">
{% for message in messages %}
<li>{{message}}</li>
{% endfor %}
</ul>
{% endif %}
{% block content %}
{% endblock %}
{% endblock %}
{% block extra_body %}
{% endblock %}
<footer>
<ul>
<li>
<a href="https://data.coop">data.coop</a>
</li>
<li>
<a href="https://git.data.coop/data.coop/membersystem">source code</a>
</li>
</ul>
</footer>
</body>
</html>

View File

@ -1,12 +0,0 @@
"""URLs for the membersystem"""
from django.contrib import admin
from django.urls import include
from django.urls import path
from . import views
urlpatterns = [
path('', views.index),
path("users/", include("users.urls")),
path('admin/', admin.site.urls),
]

View File

@ -1,5 +0,0 @@
from django.shortcuts import render_to_response
def index(request):
return render_to_response("index.html")

130
pyproject.toml Normal file
View File

@ -0,0 +1,130 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "membersystem"
description = ''
readme = "README.md"
requires-python = ">=3.11"
keywords = []
authors = [
{ name = "Víðir Valberg Guðmundsson", email = "valberg@orn.li" },
]
dependencies = [
"Django==5.0.1",
"django-money==3.4.1",
"django-allauth==0.60.0",
"psycopg[binary]==3.1.16",
"environs[django]==10.0.0",
"uvicorn==0.25.0",
"whitenoise==6.6.0",
"django-zen-queries==2.1.0",
"django-registries==0.0.3",
"django-view-decorator==0.0.4",
"django-oauth-toolkit==2.3.0",
]
version = "0.0.1"
[tool.hatch.build.targets.wheel]
packages = ["src"]
[tool.hatch.envs.default]
dependencies = [
"coverage[toml]==7.3.0",
"pytest==7.2.2",
"pytest-cov",
"pytest-django==4.5.2",
"mypy==1.1.1",
"django-stubs==1.16.0",
"pip-tools==7.3.0",
"django-debug-toolbar==4.2.0",
"django-browser-reload==1.7.0",
"model-bakery==1.17.0",
]
[[tool.hatch.envs.tests.matrix]]
python = ["3.12"]
django = ["4.2", "5.0"]
[tool.hatch.envs.tests.overrides]
matrix.django.dependencies = [
{ value = "django~={matrix:django}" },
]
matrix.python.dependencies = [
{ value = "typing_extensions==4.5.0", if = ["3.10"]},
]
[tool.hatch.envs.default.scripts]
cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=src --cov=tests --cov=append {args}"
no-cov = "cov --no-cov {args}"
typecheck = "mypy --config-file=pyproject.toml ."
requirements = "pip-compile --output-file requirements/base.txt pyproject.toml"
server = "./src/manage.py runserver 0.0.0.0:8000"
migrate = "./src/manage.py migrate"
makemigrations = "./src/manage.py makemigrations"
createsuperuser = "./src/manage.py createsuperuser"
shell = "./src/manage.py shell"
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE="tests.settings"
addopts = "--reuse-db"
norecursedirs = "build dist docs .eggs/* *.egg-info htmlcov .git"
python_files = "test*.py"
testpaths = "tests"
pythonpath = ". tests"
[tool.coverage.run]
branch = true
parallel = true
[tool.coverage.report]
exclude_lines = [
"no cov",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]
[tool.mypy]
mypy_path = "src/"
exclude = [
"venv/",
"dist/",
"docs/",
]
namespace_packages = false
show_error_codes = true
strict = true
warn_unreachable = true
follow_imports = "normal"
#plugins = ["mypy_django_plugin.main"]
[tool.django-stubs]
#django_settings_module = "tests.settings"
[[tool.mypy.overrides]]
module = "tests.*"
allow_untyped_defs = true
[tool.ruff]
target-version = "py312"
extend-exclude = [
".git",
"__pycache__",
]
line-length = 120
[tool.ruff.lint]
select = ["ALL"]
ignore = [
"G004", # Logging statement uses f-string
"ANN101", # Missing type annotation for `self` in method
"ANN102", # Missing type annotation for `cls` in classmethod
"EM101", # Exception must not use a string literal, assign to variable first
"EM102", # Exception must not use a f-string literal, assign to variable first
"COM812", # missing-trailing-comma (https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules)
"ISC001", # single-line-implicit-string-concatenation (https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules)
]
[tool.ruff.lint.isort]
force-single-line = true

View File

@ -1,2 +0,0 @@
Django>=2.2,<2.3
django-money==0.15

8
requirements/base.in Normal file
View File

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

101
requirements/base.txt Normal file
View File

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

8
requirements/dev.in Normal file
View File

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

213
requirements/dev.txt Normal file
View File

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

5
requirements/test.in Normal file
View File

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

149
requirements/test.txt Normal file
View File

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

View File

@ -1,3 +0,0 @@
pytest
pytest-django
pre-commit

View File

@ -1,13 +0,0 @@
[flake8]
ignore = E226,E302,E41
max-line-length = 160
max-complexity = 10
exclude = */migrations/*
[isort]
atomic = true
multi_line_output = 5
line_length = 160
indent = ' '
combine_as_imports = true
skip = wsgi.py,.eggs,setup.py

View File

@ -6,23 +6,21 @@ from . import models
@admin.register(models.Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ("who", "description", "created", "is_paid")
list_display = ('who', 'description', 'created', 'is_paid',)
@admin.display(description=_("Customer"))
def who(self, instance):
return instance.user.get_full_name()
who.short_description = _("Customer")
@admin.register(models.Payment)
class PaymentAdmin(admin.ModelAdmin):
list_display = ("who", "description", "order_id", "created")
list_display = ('who', 'description', 'order_id', 'created',)
@admin.display(description=_("Customer"))
def who(self, instance):
return instance.order.user.get_full_name()
who.short_description = _("Customer")
@admin.display(description=_("Order ID"))
def order_id(self, instance):
return instance.order.id
order_id.short_description = _("Order ID")

View File

@ -2,4 +2,4 @@ from django.apps import AppConfig
class AccountingConfig(AppConfig):
name = 'accounting'
name = "accounting"

View File

@ -0,0 +1,233 @@
# Generated by Django 3.1.7 on 2021-02-27 20:06
import django.db.models.deletion
import djmoney.models.fields
from django.conf import settings
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Account",
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="oprettet"),
),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="Order",
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="oprettet"),
),
(
"description",
models.CharField(max_length=1024, verbose_name="description"),
),
(
"price_currency",
djmoney.models.fields.CurrencyField(
choices=[("DKK", "DKK")],
default="XYZ",
editable=False,
max_length=3,
),
),
(
"price",
djmoney.models.fields.MoneyField(
decimal_places=2,
max_digits=16,
verbose_name="price (excl. VAT)",
),
),
(
"vat_currency",
djmoney.models.fields.CurrencyField(
choices=[("DKK", "DKK")],
default="XYZ",
editable=False,
max_length=3,
),
),
(
"vat",
djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16, verbose_name="VAT"),
),
("is_paid", models.BooleanField(default=False, verbose_name="is paid")),
(
"account",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="accounting.account",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Order",
"verbose_name_plural": "Orders",
},
),
migrations.CreateModel(
name="Transaction",
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="oprettet"),
),
(
"amount_currency",
djmoney.models.fields.CurrencyField(
choices=[("DKK", "DKK")],
default="XYZ",
editable=False,
max_length=3,
),
),
(
"amount",
djmoney.models.fields.MoneyField(
decimal_places=2,
help_text="This will include VAT",
max_digits=16,
verbose_name="amount",
),
),
(
"description",
models.CharField(max_length=1024, verbose_name="description"),
),
(
"account",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="transactions",
to="accounting.account",
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="Payment",
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="oprettet"),
),
(
"amount_currency",
djmoney.models.fields.CurrencyField(
choices=[("DKK", "DKK")],
default="XYZ",
editable=False,
max_length=3,
),
),
(
"amount",
djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16),
),
(
"description",
models.CharField(max_length=1024, verbose_name="description"),
),
(
"stripe_charge_id",
models.CharField(blank=True, max_length=255, null=True),
),
(
"order",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="accounting.order",
),
),
],
options={
"verbose_name": "payment",
"verbose_name_plural": "payments",
},
),
]

View File

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

View File

@ -1,7 +1,6 @@
from hashlib import md5
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import models
from django.db.models.aggregates import Sum
from django.utils.translation import gettext as _
@ -10,36 +9,27 @@ from djmoney.models.fields import MoneyField
class CreatedModifiedAbstract(models.Model):
modified = models.DateTimeField(
auto_now=True,
verbose_name=_("modified"),
)
created = models.DateTimeField(
auto_now_add=True,
verbose_name=_("created"),
)
modified = models.DateTimeField(auto_now=True, verbose_name=_("modified"))
created = models.DateTimeField(auto_now_add=True, verbose_name=_("created"))
class Meta:
abstract = True
class Account(CreatedModifiedAbstract):
"""
This is the model where we can give access to several users, such that they
"""This is the model where we can give access to several users, such that they
can decide which account to use to pay for something.
"""
owner = models.ForeignKey(get_user_model(), on_delete=models.PROTECT)
owner = models.ForeignKey("auth.User", on_delete=models.PROTECT)
@property
def balance(self):
return self.transactions.all().aggregate(Sum('amount')).get('amount', 0)
return self.transactions.all().aggregate(Sum("amount")).get("amount", 0)
class Transaction(CreatedModifiedAbstract):
"""
Tracks in and outgoing events of an account. When an order is received, an
"""Tracks in and outgoing events of an account. When an order is received, an
amount is subtracted, when a payment is received, an amount is added.
"""
@ -52,45 +42,30 @@ class Transaction(CreatedModifiedAbstract):
verbose_name=_("amount"),
max_digits=16,
decimal_places=2,
help_text=_("This will include VAT")
)
description = models.CharField(
max_length=1024,
verbose_name=_("description")
help_text=_("This will include VAT"),
)
description = models.CharField(max_length=1024, verbose_name=_("description"))
class Order(CreatedModifiedAbstract):
"""
Scoped out: Contents of invoices will have to be tracked either here or in
"""Scoped out: Contents of invoices will have to be tracked either here or in
a separate Invoice model. This is undecided because we are not generating
invoices at the moment.
"""
user = models.ForeignKey(get_user_model(), on_delete=models.PROTECT)
user = models.ForeignKey("auth.User", on_delete=models.PROTECT)
account = models.ForeignKey(Account, on_delete=models.PROTECT)
is_paid = models.BooleanField(default=False)
description = models.CharField(
max_length=1024,
verbose_name=_("description")
)
description = models.CharField(max_length=1024, verbose_name=_("description"))
price = MoneyField(
verbose_name=_("price (excl. VAT)"),
max_digits=16,
decimal_places=2,
)
vat = MoneyField(
verbose_name=_("VAT"),
max_digits=16,
decimal_places=2,
)
vat = MoneyField(verbose_name=_("VAT"), max_digits=16, decimal_places=2)
is_paid = models.BooleanField(
default=False,
verbose_name=_("is paid"),
)
is_paid = models.BooleanField(default=False, verbose_name=_("is paid"))
@property
def total(self):
@ -105,7 +80,7 @@ class Order(CreatedModifiedAbstract):
pk = str(self.pk).encode("utf-8")
x = md5()
x.update(pk)
extra_hash = (settings.SECRET_KEY + 'blah').encode('utf-8')
extra_hash = (settings.SECRET_KEY + "blah").encode("utf-8")
x.update(extra_hash)
return x.hexdigest()
@ -113,28 +88,17 @@ class Order(CreatedModifiedAbstract):
verbose_name = pgettext_lazy("accounting term", "Order")
verbose_name_plural = pgettext_lazy("accounting term", "Orders")
def __str__(self):
return "Order ID {id}".format(id=self.display_id)
def __str__(self) -> str:
return f"Order ID {self.display_id}"
class Payment(CreatedModifiedAbstract):
amount = MoneyField(
max_digits=16,
decimal_places=2,
)
amount = MoneyField(max_digits=16, decimal_places=2)
order = models.ForeignKey(Order, on_delete=models.PROTECT)
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, null=True, blank=True)
@property
def display_id(self):
@ -149,8 +113,8 @@ class Payment(CreatedModifiedAbstract):
description=order.description,
)
def __str__(self):
return "Payment ID {id}".format(id=self.display_id)
def __str__(self) -> str:
return f"Payment ID {self.display_id}"
class Meta:
verbose_name = _("payment")

View File

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

View File

@ -6,10 +6,10 @@ if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
except ImportError:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
"forget to activate a virtual environment?",
)
execute_from_command_line(sys.argv)

View File

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

20
src/membership/admin.py Normal file
View File

@ -0,0 +1,20 @@
from django.contrib import admin
from .models import Membership
from .models import MembershipType
from .models import SubscriptionPeriod
@admin.register(Membership)
class MembershipAdmin(admin.ModelAdmin):
pass
@admin.register(MembershipType)
class MembershipTypeAdmin(admin.ModelAdmin):
pass
@admin.register(SubscriptionPeriod)
class SubscriptionPeriodAdmin(admin.ModelAdmin):
pass

11
src/membership/apps.py Normal file
View File

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

View File

@ -0,0 +1,92 @@
# Generated by Django 3.1.7 on 2021-02-28 21:09
import django.contrib.postgres.fields.ranges
import django.db.models.deletion
from django.conf import settings
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="MembershipType",
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="oprettet"),
),
("name", models.CharField(max_length=64, verbose_name="navn")),
],
options={
"verbose_name": "membership type",
"verbose_name_plural": "membership types",
},
),
migrations.CreateModel(
name="Membership",
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="oprettet"),
),
(
"period",
django.contrib.postgres.fields.ranges.DateTimeRangeField(
help_text="The duration this subscription is for. "
),
),
(
"membership_type",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="memberships",
to="membership.membershiptype",
verbose_name="subscription type",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "membership",
"verbose_name_plural": "memberships",
},
),
]

View File

@ -0,0 +1,61 @@
# Generated by Django 4.1 on 2023-01-02 21:05
import django.contrib.postgres.constraints
import django.contrib.postgres.fields.ranges
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("membership", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="SubscriptionPeriod",
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"),
),
(
"period",
django.contrib.postgres.fields.ranges.DateRangeField(verbose_name="period"),
),
],
),
migrations.RemoveField(
model_name="membership",
name="period",
),
migrations.AlterField(
model_name="membership",
name="created",
field=models.DateTimeField(auto_now_add=True, verbose_name="created"),
),
migrations.AlterField(
model_name="membershiptype",
name="created",
field=models.DateTimeField(auto_now_add=True, verbose_name="created"),
),
migrations.AddConstraint(
model_name="subscriptionperiod",
constraint=django.contrib.postgres.constraints.ExclusionConstraint(
expressions=[("period", "&&")], name="exclude_overlapping_periods"
),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.1 on 2023-01-02 21:05
import django.db.models.deletion
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("membership", "0002_subscriptionperiod_remove_membership_period_and_more"),
]
operations = [
migrations.AddField(
model_name="membership",
name="period",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="membership.subscriptionperiod",
),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 4.1 on 2023-01-02 21:06
import django.db.models.deletion
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("membership", "0003_membership_period"),
]
operations = [
migrations.AlterField(
model_name="membership",
name="period",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="membership.subscriptionperiod",
),
),
]

View File

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

113
src/membership/models.py Normal file
View File

@ -0,0 +1,113 @@
from django.contrib.auth.models import User
from django.contrib.postgres.constraints import ExclusionConstraint
from django.contrib.postgres.fields import DateRangeField
from django.contrib.postgres.fields import RangeOperators
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext as _
from utils.mixins import CreatedModifiedAbstract
class Member(User):
class QuerySet(models.QuerySet):
def annotate_membership(self):
from .selectors import get_current_subscription_period
current_subscription_period = get_current_subscription_period()
if not current_subscription_period:
raise ValueError("No current subscription period found")
return self.annotate(
active_membership=models.Exists(
Membership.objects.filter(
user=models.OuterRef("pk"),
period=current_subscription_period.id,
),
),
)
objects = QuerySet.as_manager()
class Meta:
proxy = True
class SubscriptionPeriod(CreatedModifiedAbstract):
"""Denotes a period for which members should pay their membership fee for."""
period = DateRangeField(verbose_name=_("period"))
class Meta:
constraints = [
ExclusionConstraint(
name="exclude_overlapping_periods",
expressions=[
("period", RangeOperators.OVERLAPS),
],
),
]
def __str__(self) -> str:
return f"{self.period.lower} - {self.period.upper or _('next general assembly')}"
class Membership(CreatedModifiedAbstract):
"""Tracks that a user has membership of a given type for a given period."""
class QuerySet(models.QuerySet):
def for_member(self, member: Member):
return self.filter(user=member)
def _current(self):
return self.filter(period__period__contains=timezone.now())
def current(self) -> "Membership | None":
try:
return self._current().get()
except self.model.DoesNotExist:
return None
def previous(self) -> list["Membership"]:
# A naïve way to get previous by just excluding the current. This
# means that there must be some protection against "future"
# memberships.
return list(self.all().difference(self._current()))
objects = QuerySet.as_manager()
class Meta:
verbose_name = _("membership")
verbose_name_plural = _("memberships")
user = models.ForeignKey("auth.User", on_delete=models.PROTECT)
membership_type = models.ForeignKey(
"membership.MembershipType",
related_name="memberships",
verbose_name=_("subscription type"),
on_delete=models.PROTECT,
)
period = models.ForeignKey(
"membership.SubscriptionPeriod",
on_delete=models.PROTECT,
)
def __str__(self) -> str:
return f"{self.user} - {self.period}"
class MembershipType(CreatedModifiedAbstract):
"""Models membership types. Currently only a name, but will in the future
possibly contain more information like fees.
"""
class Meta:
verbose_name = _("membership type")
verbose_name_plural = _("membership types")
name = models.CharField(verbose_name=_("name"), max_length=64)
def __str__(self) -> str:
return self.name

View File

@ -0,0 +1,46 @@
from dataclasses import dataclass
from django.contrib.auth.models import Permission as DjangoPermission
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
PERMISSIONS = []
def persist_permissions(sender, **kwargs) -> None:
for permission in PERMISSIONS:
permission.persist_permission()
@dataclass
class Permission:
name: str
codename: str
app_label: str
model: str
def __post_init__(self, *args, **kwargs):
PERMISSIONS.append(self)
@property
def path(self) -> str:
return f"{self.app_label}.{self.codename}"
def persist_permission(self) -> None:
content_type, _ = ContentType.objects.get_or_create(
app_label=self.app_label,
model=self.model,
)
DjangoPermission.objects.get_or_create(
content_type=content_type,
codename=self.codename,
defaults={"name": self.name},
)
ADMINISTRATE_MEMBERS = Permission(
name=_("Can administrate members"),
codename="administrate_members",
app_label="membership",
model="membership",
)

View File

@ -0,0 +1,60 @@
import contextlib
from django.db.models import Exists
from django.db.models import OuterRef
from django.utils import timezone
from membership.models import Member
from membership.models import Membership
from membership.models import SubscriptionPeriod
def get_subscription_periods(member: Member | None = None) -> list[SubscriptionPeriod]:
subscription_periods = SubscriptionPeriod.objects.prefetch_related(
"membership_set",
"membership_set__user",
).all()
if member:
subscription_periods = subscription_periods.annotate(
membership_exists=Exists(
Membership.objects.filter(
user=member,
period=OuterRef("pk"),
),
),
).filter(membership_exists=True)
return list(subscription_periods)
def get_current_subscription_period() -> SubscriptionPeriod | None:
with contextlib.suppress(SubscriptionPeriod.DoesNotExist):
return SubscriptionPeriod.objects.prefetch_related(
"membership_set",
"membership_set__user",
).get(period__contains=timezone.now())
def get_memberships(
*,
member: Member | None = None,
period: SubscriptionPeriod | None = None,
) -> Membership.QuerySet:
memberships = Membership.objects.select_related("membership_type").all()
if member:
memberships = memberships.for_member(member=member)
if period:
memberships = memberships.filter(period=period)
return memberships
def get_members():
return Member.objects.all().annotate_membership().order_by("username")
def get_member(*, member_id: int) -> Member:
return get_members().get(id=member_id)

View File

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

View File

@ -0,0 +1,63 @@
{% extends "base.html" %}
{% load i18n %}
{% block head_title %}
{% trans "Membership" %}
{% endblock %}
{% block content %}
<div class="content-view">
<h2>Membership settings</h2>
{% if not current_membership %}
<p>{% trans "You do not have an active membership!" %}</p>
<p>{% trans "You can become a member by depositing the membership fee to our bank account." %}</p>
<ul>
<li>Reg. 8401 (Merkur)</li>
<li>Kontonr. 1016866</li>
<li>Tekst på overførslen: Your email</li>
</ul>
{% else %}
<p>{% trans "You are a member!" %}</p>
{% trans "next general assembly" as next_general_assembly %}
<p>{% trans "Period" %}: {{ current_period.lower|date:"SHORT_DATE_FORMAT" }} to {{ current_period.upper|date:"SHORT_DATE_FORMAT"|default:next_general_assembly }}</p>
<p>{% trans "Type" %}: {{ current_membership.membership_type }}</p>
{% endif %}
</div>
<div class="content-view">
<h2>Profile settings</h2>
<form>
<div>
<label for="username">
Username
</label>
<input id="username" type="text" value="{{user}}" />
</div>
<div>
<label for="first_name">
First name
</label>
<input id="first_name" type="text" value="{{user.first_name}}" />
</div>
<div>
<label for="last_name">
Last name
</label>
<input id="last_name" type="text" value="{{user.last_name}}" />
</div>
<button>Update Profile</button>
</form>
</div>
<div class="view-list">
<h2>Email settings</h2>
<button>Update Email</button>
</div>
{% endblock %}

99
src/membership/views.py Normal file
View File

@ -0,0 +1,99 @@
from django.utils.translation import gettext_lazy as _
from django_view_decorator import namespaced_decorator_factory
from utils.view_utils import RowAction
from utils.view_utils import render
from utils.view_utils import render_list
from .permissions import ADMINISTRATE_MEMBERS
from .selectors import get_member
from .selectors import get_members
from .selectors import get_memberships
from .selectors import get_subscription_periods
member_view = namespaced_decorator_factory(namespace="member", base_path="membership")
@member_view(
paths="",
name="membership-overview",
login_required=True,
)
def membership_overview(request):
memberships = get_memberships(member=request.user)
current_membership = memberships.current()
previous_memberships = memberships.previous()
current_period = current_membership.period.period if current_membership else None
context = {
"current_membership": current_membership,
"current_period": current_period,
"previous_memberships": previous_memberships,
}
return render(
request=request,
template_name="membership/membership_overview.html",
context=context,
)
admin_members_view = namespaced_decorator_factory(
namespace="admin-members",
base_path="admin",
)
@admin_members_view(
paths="members/",
name="list",
login_required=True,
permissions=[ADMINISTRATE_MEMBERS.path],
)
def members_admin(request):
users = get_members()
return render_list(
entity_name="member",
entity_name_plural="members",
request=request,
paginate_by=20,
objects=users,
columns=[
("username", _("Username")),
("first_name", _("First name")),
("last_name", _("Last name")),
("email", _("Email")),
("active_membership", _("Active membership")),
],
row_actions=[
RowAction(
label=_("View"),
url_name="admin-members:detail",
url_kwargs={"member_id": "id"},
),
],
)
@admin_members_view(
paths="<int:member_id>/",
name="detail",
login_required=True,
permissions=[ADMINISTRATE_MEMBERS.path],
)
def members_admin_detail(request, member_id):
member = get_member(member_id=member_id)
subscription_periods = get_subscription_periods(member=member)
context = {
"member": member,
"subscription_periods": subscription_periods,
"base_path": "admin-members:list",
}
return render(
request=request,
template_name="membership/members_admin_detail.html",
context=context,
)

7
src/project/asgi.py Normal file
View File

@ -0,0 +1,7 @@
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
application = get_asgi_application()

View File

@ -0,0 +1,519 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-03-04 09:06+0100\n"
"PO-Revision-Date: 2021-03-04 09:06+0100\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: da\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 2.4.1\n"
#: src/accounting/admin.py:15 src/accounting/admin.py:26
msgid "Customer"
msgstr ""
#: src/accounting/admin.py:31
msgid "Order ID"
msgstr ""
#: src/accounting/models.py:13 src/membership/models.py:11
msgid "modified"
msgstr ""
#: src/accounting/models.py:14 src/membership/models.py:12
msgid "created"
msgstr ""
#: src/accounting/models.py:43
msgid "amount"
msgstr ""
#: src/accounting/models.py:46
msgid "This will include VAT"
msgstr ""
#: src/accounting/models.py:48 src/accounting/models.py:61
#: src/accounting/models.py:100
msgid "description"
msgstr ""
#: src/accounting/models.py:64
msgid "price (excl. VAT)"
msgstr ""
#: src/accounting/models.py:66
msgid "VAT"
msgstr ""
#: src/accounting/models.py:68
msgid "is paid"
msgstr ""
#: src/accounting/models.py:88
msgctxt "accounting term"
msgid "Order"
msgstr ""
#: src/accounting/models.py:89
msgctxt "accounting term"
msgid "Orders"
msgstr ""
#: src/accounting/models.py:121
msgid "payment"
msgstr ""
#: src/accounting/models.py:122
msgid "payments"
msgstr ""
#: src/membership/models.py:45
msgid "membership"
msgstr "medlemskab"
#: src/membership/models.py:46
msgid "memberships"
msgstr "medlemskaber"
#: src/membership/models.py:53
msgid "subscription type"
msgstr ""
#: src/membership/models.py:57
msgid "The duration this subscription is for. "
msgstr ""
#: src/membership/models.py:70
msgid "membership type"
msgstr ""
#: src/membership/models.py:71
msgid "membership types"
msgstr ""
#: src/membership/models.py:73
msgid "name"
msgstr "navn"
#: src/membership/templates/membership_overview.html:7
msgid "You do not have an active membership!"
msgstr "Du har ikke et aktivt medlemskab!"
#: src/membership/templates/membership_overview.html:9
msgid ""
"You can become a member by depositing the membership fee to our bank account."
msgstr ""
#: src/membership/templates/membership_overview.html:17
msgid "You are a member!"
msgstr "Du er medlem!"
#: src/membership/templates/membership_overview.html:19
msgid "Period"
msgstr "Periode"
#: src/membership/templates/membership_overview.html:20
msgid "Type"
msgstr "Type"
#: src/project/settings.py:131
msgid "Danish"
msgstr "Dansk"
#: src/project/settings.py:132
msgid "English"
msgstr "Engelsk"
#: src/project/templates/account/account_inactive.html:5
#: src/project/templates/account/account_inactive.html:8
msgid "Account Inactive"
msgstr "Inaktiv konto"
#: src/project/templates/account/account_inactive.html:10
msgid "This account is inactive."
msgstr "Denne konto er inaktiv."
#: src/project/templates/account/email.html:5
#: src/project/templates/account/email.html:16
msgid "E-mail Addresses"
msgstr "E-mail adresser"
#: src/project/templates/account/email.html:21
msgid "The following e-mail addresses are associated with your account:"
msgstr "De følgende e-mail adresser er tilknyttet din konto:"
#: src/project/templates/account/email.html:31
msgid "Address"
msgstr "Adresse"
#: src/project/templates/account/email.html:32
msgid "Status"
msgstr "Status"
#: src/project/templates/account/email.html:33
msgid "Primary"
msgstr "Primær"
#: src/project/templates/account/email.html:90
msgid "Warning:"
msgstr "Advarsel:"
#: src/project/templates/account/email.html:91
msgid ""
"You currently do not have any e-mail address set up. You should really add "
"an e-mail address so you can receive notifications, reset your password, etc."
msgstr ""
"Du har lige nu ingen e-mail adresse tilknyttet. Du burde virkelig tilføje en "
"e-mail adresse så du kan modtage notifikationer, nulstille dit kodeord, osv."
#: src/project/templates/account/email.html:99
#: src/project/templates/account/email.html:107
msgid "Add E-mail"
msgstr "Tilføj e-mail"
#: src/project/templates/account/email.html:118
msgid "Do you really want to remove the selected e-mail address?"
msgstr "Vil du virkelig fjerne den valgte e-mail?"
#: src/project/templates/account/email/base_message.txt:1
#, python-format
msgid "Hello from %(site_name)s!"
msgstr "Hej fra %(site_name)s!"
#: src/project/templates/account/email/base_message.txt:5
#, python-format
msgid ""
"Thank you for using %(site_name)s!\n"
"%(site_domain)s"
msgstr ""
#: src/project/templates/account/email/email_confirmation_message.txt:5
#, python-format
msgid ""
"You're receiving this e-mail because user %(user_display)s has given your e-"
"mail address to register an account on %(site_domain)s.\n"
"\n"
"To confirm this is correct, go to %(activate_url)s"
msgstr ""
#: src/project/templates/account/email/email_confirmation_subject.txt:3
msgid "Please Confirm Your E-mail Address"
msgstr "Venligst bekræft din e-mail adresse"
#: src/project/templates/account/email/password_reset_key_message.txt:4
msgid ""
"You're receiving this e-mail because you or someone else has requested a "
"password for your user account.\n"
"It can be safely ignored if you did not request a password reset. Click the "
"link below to reset your password."
msgstr ""
"Du modtager denne e-mail fordi du, eller nogen anden, har anmodet om "
"nulstilling af dit kodeord.\n"
"Du kan trygt ignorere dette hvis det ikke var dig der anmodede om "
"nulstillingen. Klik på linket herunder for at nulstille dit kodeord."
#: src/project/templates/account/email/password_reset_key_message.txt:9
#, python-format
msgid "In case you forgot, your username is %(username)s."
msgstr ""
#: src/project/templates/account/email/password_reset_key_subject.txt:3
msgid "Password Reset E-mail"
msgstr "Nulstilling af password"
#: src/project/templates/account/email_confirm.html:6
#: src/project/templates/account/email_confirm.html:10
msgid "Confirm E-mail Address"
msgstr "Bekræft e-mail adresse"
#: src/project/templates/account/email_confirm.html:16
#, python-format
msgid ""
"Please confirm that <a href=\"mailto:%(email)s\">%(email)s</a> is an e-mail "
"address for user %(user_display)s."
msgstr ""
#: src/project/templates/account/email_confirm.html:20
msgid "Confirm"
msgstr "Bekræft"
#: src/project/templates/account/email_confirm.html:27
#, python-format
msgid ""
"This e-mail confirmation link expired or is invalid. Please <a href="
"\"%(email_url)s\">issue a new e-mail confirmation request</a>."
msgstr ""
#: src/project/templates/account/login.html:20
#: src/project/templates/account/login.html:26
#: src/project/templates/account/signup.html:26
#: src/project/templates/account/signup.html:32
msgid "E-mail"
msgstr "E-mail"
#: src/project/templates/account/login.html:31
#: src/project/templates/account/login.html:37
#: src/project/templates/account/signup.html:44
#: src/project/templates/account/signup.html:50
msgid "Password"
msgstr "Kodeord"
#: src/project/templates/account/login.html:41
msgid "Sign in"
msgstr "Log ind"
#: src/project/templates/account/login.html:45
msgid "Forgot password?"
msgstr "Glemt kodeord?"
#: src/project/templates/account/login.html:48
msgid "Or"
msgstr "Eller"
#: src/project/templates/account/login.html:53
#: src/project/templates/account/signup.html:6
msgid "Become a member"
msgstr "Bliv medlem"
#: src/project/templates/account/logout.html:5
#: src/project/templates/account/logout.html:8
#: src/project/templates/account/logout.html:17
msgid "Sign Out"
msgstr "Log ud"
#: src/project/templates/account/logout.html:10
msgid "Are you sure you want to sign out?"
msgstr "Er du sikker på at du vil logge ind?"
#: src/project/templates/account/messages/cannot_delete_primary_email.txt:2
#, python-format
msgid "You cannot remove your primary e-mail address (%(email)s)."
msgstr ""
#: src/project/templates/account/messages/email_confirmation_sent.txt:2
#, python-format
msgid "Confirmation e-mail sent to %(email)s."
msgstr ""
#: src/project/templates/account/messages/email_confirmed.txt:2
#, python-format
msgid "You have confirmed %(email)s."
msgstr ""
#: src/project/templates/account/messages/email_deleted.txt:2
#, python-format
msgid "Removed e-mail address %(email)s."
msgstr ""
#: src/project/templates/account/messages/logged_in.txt:4
#, python-format
msgid "Successfully signed in as %(name)s."
msgstr ""
#: src/project/templates/account/messages/logged_out.txt:2
msgid "You have signed out."
msgstr "Du er nu logget ud."
#: src/project/templates/account/messages/password_changed.txt:2
msgid "Password successfully changed."
msgstr ""
#: src/project/templates/account/messages/password_set.txt:2
msgid "Password successfully set."
msgstr ""
#: src/project/templates/account/messages/primary_email_set.txt:2
msgid "Primary e-mail address set."
msgstr ""
#: src/project/templates/account/messages/unverified_primary_email.txt:2
msgid "Your primary e-mail address must be verified."
msgstr ""
#: src/project/templates/account/password_change.html:5
#: src/project/templates/account/password_change.html:8
#: src/project/templates/account/password_change.html:13
#: src/project/templates/account/password_reset_from_key.html:4
#: src/project/templates/account/password_reset_from_key.html:7
#: src/project/templates/account/password_reset_from_key_done.html:4
#: src/project/templates/account/password_reset_from_key_done.html:7
msgid "Change Password"
msgstr "Skift kodeord"
#: src/project/templates/account/password_change.html:14
msgid "Forgot Password?"
msgstr "Glemt kodeord?"
#: src/project/templates/account/password_reset.html:6
#: src/project/templates/account/password_reset.html:10
#: src/project/templates/account/password_reset_done.html:6
#: src/project/templates/account/password_reset_done.html:9
msgid "Password Reset"
msgstr ""
#: src/project/templates/account/password_reset.html:16
msgid ""
"Forgotten your password? Enter your e-mail address below, and we'll send you "
"an e-mail allowing you to reset it."
msgstr ""
#: src/project/templates/account/password_reset.html:21
#: src/project/templates/account/password_reset.html:27
msgid "E-mail address"
msgstr "E-mail adresser"
#: src/project/templates/account/password_reset.html:38
msgid "Reset My Password"
msgstr ""
#: src/project/templates/account/password_reset.html:41
msgid "Please contact us if you have any trouble resetting your password."
msgstr ""
#: src/project/templates/account/password_reset.html:47
#: src/project/templates/account/signup.html:72
msgid "To login"
msgstr "Til login"
#: src/project/templates/account/password_reset_done.html:15
msgid ""
"We have sent you an e-mail. Please contact us if you do not receive it "
"within a few minutes."
msgstr ""
#: src/project/templates/account/password_reset_from_key.html:7
msgid "Bad Token"
msgstr ""
#: src/project/templates/account/password_reset_from_key.html:11
#, python-format
msgid ""
"The password reset link was invalid, possibly because it has already been "
"used. Please request a <a href=\"%(passwd_reset_url)s\">new password reset</"
"a>."
msgstr ""
#: src/project/templates/account/password_reset_from_key.html:17
msgid "change password"
msgstr ""
#: src/project/templates/account/password_reset_from_key.html:20
#: src/project/templates/account/password_reset_from_key_done.html:8
msgid "Your password is now changed."
msgstr ""
#: src/project/templates/account/password_set.html:5
#: src/project/templates/account/password_set.html:8
#: src/project/templates/account/password_set.html:13
msgid "Set Password"
msgstr "Sæt kodeord"
#: src/project/templates/account/signup.html:19
msgid "To become a member, you need to have an account. Create one here."
msgstr ""
#: src/project/templates/account/signup.html:66
msgid "Sign up"
msgstr "Bliv medlem"
#: src/project/templates/account/signup_closed.html:5
#: src/project/templates/account/signup_closed.html:8
msgid "Sign Up Closed"
msgstr ""
#: src/project/templates/account/signup_closed.html:10
msgid "We are sorry, but the sign up is currently closed."
msgstr ""
#: src/project/templates/account/snippets/already_logged_in.html:5
msgid "Note"
msgstr ""
#: src/project/templates/account/snippets/already_logged_in.html:5
#, python-format
msgid "you are already logged in as %(user_display)s."
msgstr ""
#: src/project/templates/account/verification_sent.html:5
#: src/project/templates/account/verification_sent.html:8
#: src/project/templates/account/verified_email_required.html:5
#: src/project/templates/account/verified_email_required.html:8
msgid "Verify Your E-mail Address"
msgstr "Verificér din e-mail adresse"
#: src/project/templates/account/verification_sent.html:10
msgid ""
"We have sent an e-mail to you for verification. Follow the link provided to "
"finalize the signup process. Please contact us if you do not receive it "
"within a few minutes."
msgstr ""
#: src/project/templates/account/verified_email_required.html:12
msgid ""
"This part of the site requires us to verify that\n"
"you are who you claim to be. For this purpose, we require that you\n"
"verify ownership of your e-mail address. "
msgstr ""
#: src/project/templates/account/verified_email_required.html:16
msgid ""
"We have sent an e-mail to you for\n"
"verification. Please click on the link inside this e-mail. Please\n"
"contact us if you do not receive it within a few minutes."
msgstr ""
#: src/project/templates/account/verified_email_required.html:20
#, python-format
msgid ""
"<strong>Note:</strong> you can still <a href=\"%(email_url)s\">change your e-"
"mail address</a>."
msgstr ""
#: src/project/templates/base.html:140
msgid "Dashboard"
msgstr ""
#: src/project/templates/base.html:146
msgid "Profile"
msgstr "Profil"
#: src/project/templates/base.html:152
msgid "Details"
msgstr "Detaljer"
#: src/project/templates/base.html:158
msgid "Emails"
msgstr "Emails"
#: src/project/templates/base.html:164
msgid "Membership"
msgstr "Medlemskab"
#: src/project/templates/base.html:171 src/project/templates/base.html:184
msgid "Overview"
msgstr "Oversigt"
#: src/project/templates/base.html:177
msgid "Services"
msgstr "Tjenester"
#: src/project/templates/base.html:191
msgid "Admin"
msgstr "Admin"
#: src/project/templates/base.html:197
msgid "Members"
msgstr "Medlemmer"
#~ msgid "OR"
#~ msgstr "Eller"

188
src/project/settings.py Normal file
View File

@ -0,0 +1,188 @@
from pathlib import Path
from django.utils.translation import gettext_lazy as _
from environs import Env
env = Env()
env.read_env()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
PROJECT_DIR = BASE_DIR / "project"
SECRET_KEY = env.str("SECRET_KEY", default="something-very-secret")
DEBUG = env.bool("DEBUG", default=False)
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=["*"])
CSRF_TRUSTED_ORIGINS = env.list(
"CSRF_TRUSTED_ORIGINS",
default=["http://localhost:8000"],
)
ADMINS = [tuple(x.split(":")) for x in env.list("DJANGO_ADMINS", default=[])]
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Application definition
DJANGO_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.sites",
]
THIRD_PARTY_APPS = [
"allauth",
"allauth.account",
"django_view_decorator",
]
LOCAL_APPS = [
"utils",
"accounting",
"membership",
]
INSTALLED_APPS = [
*DJANGO_APPS,
*THIRD_PARTY_APPS,
*LOCAL_APPS,
]
DATABASES = {"default": env.dj_db_url("DATABASE_URL")}
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"allauth.account.middleware.AccountMiddleware",
]
ROOT_URLCONF = "project.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [PROJECT_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
"builtins": [
"django.templatetags.i18n",
],
},
},
]
AUTHENTICATION_BACKENDS = (
"django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend",
)
WSGI_APPLICATION = "project.wsgi.application"
AUTH_PASSWORD_VALIDATORS = []
LANGUAGE_CODE = "da-dk"
TIME_ZONE = "Europe/Copenhagen"
USE_I18N = True
USE_TZ = True
STATIC_URL = "/static/"
STATICFILES_DIRS = [PROJECT_DIR / "static"]
STATIC_ROOT = BASE_DIR / "static"
SITE_ID = 1
LOGIN_REDIRECT_URL = "/"
EMAIL_BACKEND = env.str(
"EMAIL_BACKEND",
default="django.core.mail.backends.console.EmailBackend",
)
DEFAULT_FROM_EMAIL = env.str("DEFAULT_FROM_EMAIL", default="")
# Parse email URLs, e.g. "smtp://"
email = env.dj_email_url("EMAIL_URL", default="smtp://")
EMAIL_HOST = email["EMAIL_HOST"]
EMAIL_PORT = email["EMAIL_PORT"]
EMAIL_HOST_PASSWORD = email["EMAIL_HOST_PASSWORD"]
EMAIL_HOST_USER = email["EMAIL_HOST_USER"]
EMAIL_USE_TLS = email["EMAIL_USE_TLS"]
# Always show DDT in development for any IP, not just 127.0.0.1 or
# settings.INTERNAL_IPS. This is useful in a docker setup where the
# requesting IP isn't static.
DEBUG_TOOLBAR_CONFIG = {
"SHOW_TOOLBAR_CALLBACK": lambda _x: DEBUG,
}
CURRENCIES = ("DKK",)
CURRENCY_CHOICES = [("DKK", "DKK")]
LANGUAGES = [
("da", _("Danish")),
("en", _("English")),
]
# We store all translations in one location
LOCALE_PATHS = [PROJECT_DIR / "locale"]
# Allauth configuration
ACCOUNT_AUTHENTICATION_METHOD = "email"
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False
ACCOUNT_USERNAME_REQUIRED = False
# Logging
# We want to log everything to stdout in docker
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
},
},
"loggers": {
"": {
"handlers": ["console"],
"level": "DEBUG",
},
},
}
if DEBUG:
INSTALLED_APPS += ["debug_toolbar", "django_browser_reload"]
MIDDLEWARE += [
"debug_toolbar.middleware.DebugToolbarMiddleware",
"django_browser_reload.middleware.BrowserReloadMiddleware",
]
# Always show DDT in development for any IP, not just 127.0.0.1 or
# settings.INTERNAL_IPS. This is useful in a docker setup where the
# requesting IP isn't static.
DEBUG_TOOLBAR_CONFIG = {
"SHOW_TOOLBAR_CALLBACK": lambda _x: DEBUG,
}

View File

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

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,559 @@
/* Reset */
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
}
body {
line-height: 1.5;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
input,
button,
textarea,
select {
font: inherit;
}
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}
#root,
#__next {
isolation: isolate;
}
/* Variables */
:root {
/* Colors */
--light: #ffffff;
--light-dust: #fefef9;
--dust: #f4f1ef;
--medium-dust: #dadada;
--dark-dust: #bfbfbf;
--fade: #878787;
--twilight: #4a4a4a;
--dark-twilight: #2f2f2f;
--dark: #2a2a2a;
--dark-dark: #121212;
--light-custard: #eee7d5;
--custard: #f0dcac;
--dark-custard: #d4c7a9;
--water: #a8f3f4;
--splash: #4b3aba;
/* Sizes */
--space: 12px;
--double-space: calc(var(--space) * 2);
--half-space: calc(var(--space) / 2);
--quarter-space: calc(var(--space) / 4);
--outer-space: var(--double-space);
}
@media (min-width: 1380px) {
:root {
--outer-space: 15%;
}
}
html,
body {
height: 100%;
font-size: 1.05em;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 600;
color: var(--twilight);
}
a {
font-weight: 500;
color: var(--splash);
text-decoration: none;
cursor: pointer;
}
hr {
margin: var(--double-space) 0;
height: 0;
border: 0;
border-bottom: 1px solid var(--dark-custard);
}
body {
margin: 0;
padding: 0;
background: var(--custard);
font-family: Inter;
font-weight: 400;
line-height: 1.6;
color: var(--dark);
}
header {
display: flex;
padding: var(--double-space) var(--outer-space);
background: var(--light);
justify-content: space-between;
align-items: center;
}
header>h1 {
font-size: 1.44em;
}
#switch-icon {
width: 30px;
height: 30px;
display: inline-block;
vertical-align: middle;
margin: 0 var(--space);
top: -2px;
position: relative;
}
#switch-icon #layer1 path {
fill: var(--twilight);
}
header>div>a#logout {
padding: 6px 12px;
border-radius: 6px;
background: var(--twilight);
text-decoration: none;
color: var(--dust);
transition: background 0.2s;
}
header>div>a#logout:hover {
background: var(--splash);
color: var(--light);
}
aside {
padding: 0 var(--outer-space) var(--double-space) var(--outer-space);
background: var(--light);
}
aside>div {
background: var(--dust);
padding: var(--double-space);
border-radius: var(--quarter-space);
overflow: hidden;
}
aside>div>h2 {
font-size: 1.22em;
margin: 0 0 6px 0;
}
aside>div>figure {
width: 100px;
height: 100px;
border: 1px solid var(--dark-dust);
float: left;
margin-right: var(--double-space);
}
aside>div>dl {
overflow: hidden;
}
aside>div>dl>dt {
float: left;
clear: left;
margin: 0 var(--double-space) 0 0;
width: 180px;
font-weight: 600;
color: var(--twilight);
}
nav {
display: block;
border-bottom: 1px solid var(--dark-custard);
background: var(--light);
}
nav ol {
margin: 0 calc(var(--outer-space));
padding: 0;
list-style-type: none;
overflow: hidden;
}
nav ol li {
margin: 0;
padding: 0;
float: left;
}
nav>ol>li>a {
display: block;
padding: var(--half-space) var(--half-space) var(--quarter-space);
margin: 0 var(--space);
border-bottom: var(--half-space) solid transparent;
text-decoration: none;
color: var(--dark);
cursor: pointer;
font-weight: 500;
letter-spacing: 0.04em;
}
nav>ol>li:first-child>a {
margin-left: 0;
}
nav ol li a:hover {
border-color: rgba(0, 0, 0, 0.6);
}
nav ol li a.current {
font-weight: bold;
border-color: var(--splash);
color: var(--splash);
}
article {
padding: var(--double-space) var(--outer-space);
}
article div.content-view {
background: var(--dust);
padding: var(--double-space);
margin-bottom: var(--space);
}
div.content-view>h2 {
margin: 0 0 var(--space) 0;
}
div.services {
display: flex;
justify-content: space-between;
gap: var(--double-space);
flex-wrap: wrap;
}
div.services>div,
div.infobox {
background: var(--light);
padding: var(--double-space);
border-radius: 6px;
flex: 240px;
max-width: 420px;
display: flex;
flex-flow: column;
justify-content: space-between;
}
div.infobox button {
margin-top: var(--double-space);
}
div.services>div>div.description {
margin-bottom: var(--double-space);
}
div.services>div>div.description>p {
margin-top: var(--half-space);
}
div.services>div>a,
a.button,
button {
display: block;
color: var(--light);
background: var(--splash);
padding: var(--space) var(--double-space);
border-radius: var(--quarter-space);
opacity: 0.85;
cursor: pointer;
text-align: center;
border: 0;
font-weight: 600;
text-decoration: none;
transition: opacity 0.15s;
}
button.small {
font-size: 0.78em;
padding: var(--half-space) var(--space);
}
div.services>div>a:hover,
a.button:hover,
button:hover {
opacity: 1.0;
}
button:disabled {
opacity: 0.6;
background: var(--twilight);
cursor: default;
}
button.secondary {
background: var(--twilight);
}
article table {
width: 100%;
border-spacing: 0;
margin: var(--space) 0;
}
article table thead th {
background: var(--twilight);
color: var(--medium-dust);
}
article table thead th a {
color: var(--light);
}
article table thead th:first-child {
border-radius: var(--half-space) 0 0 0;
}
article table thead th:last-child {
border-radius: 0 var(--half-space) 0 0;
}
article table tbody {
background: var(--light-dust);
}
article table tbody tr:nth-child(odd) {
background: var(--light-custard);
}
article table tbody tr:nth-child(odd) td {
border-top: 1px solid var(--dark-custard);
border-bottom: 1px solid var(--dark-custard);
}
article table tbody tr:last-child td {
border-bottom: var(--half-space) solid var(--twilight);
}
article table thead th,
article table tbody td {
padding: var(--space);
text-align: left;
}
article table#user_email_table tbody tr td:first-child {
text-align: center;
}
form>div {
margin: 0 0 var(--double-space);
}
form>div>label {
display: block;
margin: 0 0 6px;
}
form>div>input[type="text"],
form>div>input[type="password"],
input[type="email"] {
border: 2px solid var(--twilight);
border-radius: 6px;
padding: 8px;
background: var(--light-dust);
width: 100%;
color: var(--dark);
}
form fieldset {
border: 0;
padding: 0;
margin: 0;
}
form div.buttonHolder button {
display: inline-block;
}
#email-add-overlay {
display: none;
position: fixed;
left: 0;
top: 0;
bottom: 0;
right: 0;
background: rgba(0, 0, 0, 0.8);
align-items: center;
justify-content: center;
z-index: 1000;
}
#email-add-overlay .content-view {
width: 600px;
padding: var(--double-space);
}
#email-add-overlay .content-view p {
margin: var(--double-space) 0;
}
#login {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
#loginbox {
border-radius: var(--space);
border: 6px solid white;
width: 800px;
height: 500px;
display: flex;
flex-flow: row;
}
#loginbox>div {
padding: var(--double-space);
flex: 1;
}
#loginbox label {
color: var(--twilight);
}
#loginbox>div.login {
background: var(--light-dust);
display: flex;
flex-flow: column;
justify-content: space-between;
}
#loginbox>div.signup {
background: var(--water);
display: flex;
flex-flow: column;
align-items: center;
}
#loginbox>div:first-child {
border-radius: var(--half-space) 0 0 var(--half-space);
}
#loginbox>div:last-child {
border-radius: 0 var(--half-space) var(--half-space) 0;
}
#loginbox>div:last-child>* {
flex: 1;
}
#loginbox div.new_here {
margin-top: var(--double-space);
}
#loginbox div.new_here h2 {
margin: var(--double-space) 0;
}
#loginbox button {
width: 100%;
}
#loginbox img {
padding: 0 var(--double-space);
}
footer {
margin: var(--space) var(--outer-space);
padding: var(--space);
border-radius: var(--quarter-space);
background: var(--dark);
color: var(--dust);
font-size: 0.78em;
opacity: 0.8;
}
span.time_remaining {
color: var(--fade);
}
.pagination {
display: flex;
justify-content: center;
list-style: none;
padding: var(--half-space) 0;
margin: 0;
}
.pagination>li {
margin: 0 var(--half-space);
}
.pagination>li:first-child {
margin-right: var(--double-space);
}
.pagination>li:last-child {
margin-left: var(--double-space);
}
.pagination .page-item {
border: 1px solid var(--fade);
padding: var(--quarter-space) var(--half-space);
border-radius: var(--half-space);
background: var(--light-dust);
font-size: 0.78em;
}
.pagination .page-link {
padding: var(--half-space);
color: var(--twilight);
}
.pagination .page-item.active {
background: var(--twilight);
}
.pagination .page-item.active .page-link {
color: var(--light-dust);
font-weight: bold;
}
.pagination .page-item.disabled {
opacity: 0.6;
}
.pagination .page-item.disabled .page-link {
cursor: default;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More