Compare commits

..

5 commits

37 changed files with 2463 additions and 947 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[project] [project]
@ -12,22 +12,20 @@ authors = [
{ name = "Víðir Valberg Guðmundsson", email = "valberg@orn.li" }, { name = "Víðir Valberg Guðmundsson", email = "valberg@orn.li" },
] ]
dependencies = [ dependencies = [
"Django==5.0.6", "Django==5.0.1",
"django-money==3.4.1", "django-money==3.4.1",
"django-allauth==0.63.3", "django-allauth==0.60.0",
"psycopg[binary]==3.1.19", "psycopg[binary]==3.1.16",
"environs[django]==11.0.0", "environs[django]==10.0.0",
"uvicorn==0.30.0", "uvicorn==0.25.0",
"whitenoise==6.6.0", "whitenoise==6.6.0",
"django-zen-queries==2.1.0", "django-zen-queries==2.1.0",
"django-registries==0.0.3", "django-registries==0.0.3",
"django-view-decorator==0.0.4",
"django-oauth-toolkit==2.4.0",
] ]
version = "0.0.1" dynamic = ["version"]
[tool.hatch.build.targets.wheel] [tool.hatch.version]
packages = ["src"] source = "vcs"
[tool.hatch.envs.default] [tool.hatch.envs.default]
dependencies = [ dependencies = [
@ -60,7 +58,7 @@ cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=src --
no-cov = "cov --no-cov {args}" no-cov = "cov --no-cov {args}"
typecheck = "mypy --config-file=pyproject.toml ." typecheck = "mypy --config-file=pyproject.toml ."
requirements = "pip-compile --output-file requirements/base.txt pyproject.toml" requirements = "pip-compile --output-file requirements/base.txt pyproject.toml"
server = "./src/manage.py runserver 0.0.0.0:8000" server = "./src/manage.py runserver"
migrate = "./src/manage.py migrate" migrate = "./src/manage.py migrate"
makemigrations = "./src/manage.py makemigrations" makemigrations = "./src/manage.py makemigrations"
createsuperuser = "./src/manage.py createsuperuser" createsuperuser = "./src/manage.py createsuperuser"
@ -105,26 +103,3 @@ follow_imports = "normal"
[[tool.mypy.overrides]] [[tool.mypy.overrides]]
module = "tests.*" module = "tests.*"
allow_untyped_defs = true 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

@ -30,7 +30,6 @@ django==5.0.1
# django-allauth # django-allauth
# django-money # django-money
# django-registries # django-registries
# django-view-decorator
# django-zen-queries # django-zen-queries
# membersystem (pyproject.toml) # membersystem (pyproject.toml)
django-allauth==0.60.0 django-allauth==0.60.0
@ -41,8 +40,6 @@ django-money==3.4.1
# via membersystem (pyproject.toml) # via membersystem (pyproject.toml)
django-registries==0.0.3 django-registries==0.0.3
# via membersystem (pyproject.toml) # via membersystem (pyproject.toml)
django-view-decorator==0.0.4
# via membersystem (pyproject.toml)
django-zen-queries==2.1.0 django-zen-queries==2.1.0
# via membersystem (pyproject.toml) # via membersystem (pyproject.toml)
environs[django]==10.0.0 environs[django]==10.0.0

View file

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

View file

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

@ -17,7 +17,8 @@ class CreatedModifiedAbstract(models.Model):
class Account(CreatedModifiedAbstract): class Account(CreatedModifiedAbstract):
"""This is the model where we can give access to several users, such that they """
This is the model where we can give access to several users, such that they
can decide which account to use to pay for something. can decide which account to use to pay for something.
""" """
@ -29,7 +30,8 @@ class Account(CreatedModifiedAbstract):
class Transaction(CreatedModifiedAbstract): class Transaction(CreatedModifiedAbstract):
"""Tracks in and outgoing events of an account. When an order is received, an """
Tracks in and outgoing events of an account. When an order is received, an
amount is subtracted, when a payment is received, an amount is added. amount is subtracted, when a payment is received, an amount is added.
""" """
@ -48,7 +50,8 @@ class Transaction(CreatedModifiedAbstract):
class Order(CreatedModifiedAbstract): class Order(CreatedModifiedAbstract):
"""Scoped out: Contents of invoices will have to be tracked either here or in """
Scoped out: Contents of invoices will have to be tracked either here or in
a separate Invoice model. This is undecided because we are not generating a separate Invoice model. This is undecided because we are not generating
invoices at the moment. invoices at the moment.
""" """
@ -88,7 +91,7 @@ class Order(CreatedModifiedAbstract):
verbose_name = pgettext_lazy("accounting term", "Order") verbose_name = pgettext_lazy("accounting term", "Order")
verbose_name_plural = pgettext_lazy("accounting term", "Orders") verbose_name_plural = pgettext_lazy("accounting term", "Orders")
def __str__(self) -> str: def __str__(self):
return f"Order ID {self.display_id}" return f"Order ID {self.display_id}"
@ -113,7 +116,7 @@ class Payment(CreatedModifiedAbstract):
description=order.description, description=order.description,
) )
def __str__(self) -> str: def __str__(self):
return f"Payment ID {self.display_id}" return f"Payment ID {self.display_id}"
class Meta: class Meta:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,20 +4,22 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("auth", "0012_alter_user_first_name_max_length"), ('auth', '0012_alter_user_first_name_max_length'),
("membership", "0004_alter_membership_period"), ('membership', '0004_alter_membership_period'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name="Member", name='Member',
fields=[], fields=[
],
options={ options={
"proxy": True, 'proxy': True,
"indexes": [], 'indexes': [],
"constraints": [], 'constraints': [],
}, },
bases=("auth.user",), bases=('auth.user',),
), ),
] ]

View file

@ -5,6 +5,7 @@ from django.contrib.postgres.fields import RangeOperators
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from utils.mixins import CreatedModifiedAbstract from utils.mixins import CreatedModifiedAbstract
@ -34,7 +35,9 @@ class Member(User):
class SubscriptionPeriod(CreatedModifiedAbstract): class SubscriptionPeriod(CreatedModifiedAbstract):
"""Denotes a period for which members should pay their membership fee for.""" """
Denotes a period for which members should pay their membership fee for.
"""
period = DateRangeField(verbose_name=_("period")) period = DateRangeField(verbose_name=_("period"))
@ -48,12 +51,16 @@ class SubscriptionPeriod(CreatedModifiedAbstract):
), ),
] ]
def __str__(self) -> str: def __str__(self):
return f"{self.period.lower} - {self.period.upper or _('next general assembly')}" return (
f"{self.period.lower} - {self.period.upper or _('next general assembly')}"
)
class Membership(CreatedModifiedAbstract): class Membership(CreatedModifiedAbstract):
"""Tracks that a user has membership of a given type for a given period.""" """
Tracks that a user has membership of a given type for a given period.
"""
class QuerySet(models.QuerySet): class QuerySet(models.QuerySet):
def for_member(self, member: Member): def for_member(self, member: Member):
@ -94,12 +101,13 @@ class Membership(CreatedModifiedAbstract):
on_delete=models.PROTECT, on_delete=models.PROTECT,
) )
def __str__(self) -> str: def __str__(self):
return f"{self.user} - {self.period}" return f"{self.user} - {self.period}"
class MembershipType(CreatedModifiedAbstract): class MembershipType(CreatedModifiedAbstract):
"""Models membership types. Currently only a name, but will in the future """
Models membership types. Currently only a name, but will in the future
possibly contain more information like fees. possibly contain more information like fees.
""" """
@ -109,5 +117,5 @@ class MembershipType(CreatedModifiedAbstract):
name = models.CharField(verbose_name=_("name"), max_length=64) name = models.CharField(verbose_name=_("name"), max_length=64)
def __str__(self) -> str: def __str__(self):
return self.name return self.name

View file

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

View file

@ -1,23 +1,18 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.decorators import permission_required
from django.utils.translation import gettext_lazy as _ from 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 .permissions import ADMINISTRATE_MEMBERS
from .selectors import get_member from .selectors import get_member
from .selectors import get_members from .selectors import get_members
from .selectors import get_memberships from .selectors import get_memberships
from .selectors import get_subscription_periods from .selectors import get_subscription_periods
from utils.view_utils import render
member_view = namespaced_decorator_factory(namespace="member", base_path="membership") from utils.view_utils import render_list
from utils.view_utils import RowAction
@member_view( @login_required
paths="",
name="membership-overview",
login_required=True,
)
def membership_overview(request): def membership_overview(request):
memberships = get_memberships(member=request.user) memberships = get_memberships(member=request.user)
current_membership = memberships.current() current_membership = memberships.current()
@ -38,18 +33,8 @@ def membership_overview(request):
) )
admin_members_view = namespaced_decorator_factory( @login_required
namespace="admin-members", @permission_required(ADMINISTRATE_MEMBERS.path)
base_path="admin",
)
@admin_members_view(
paths="members/",
name="list",
login_required=True,
permissions=[ADMINISTRATE_MEMBERS.path],
)
def members_admin(request): def members_admin(request):
users = get_members() users = get_members()
@ -69,19 +54,15 @@ def members_admin(request):
row_actions=[ row_actions=[
RowAction( RowAction(
label=_("View"), label=_("View"),
url_name="admin-members:detail", url_name="admin-members-detail",
url_kwargs={"member_id": "id"}, url_kwargs={"member_id": "id"},
), ),
], ],
) )
@admin_members_view( @login_required
paths="<int:member_id>/", @permission_required(ADMINISTRATE_MEMBERS.path)
name="detail",
login_required=True,
permissions=[ADMINISTRATE_MEMBERS.path],
)
def members_admin_detail(request, member_id): def members_admin_detail(request, member_id):
member = get_member(member_id=member_id) member = get_member(member_id=member_id)
subscription_periods = get_subscription_periods(member=member) subscription_periods = get_subscription_periods(member=member)
@ -89,7 +70,7 @@ def members_admin_detail(request, member_id):
context = { context = {
"member": member, "member": member,
"subscription_periods": subscription_periods, "subscription_periods": subscription_periods,
"base_path": "admin-members:list", "base_path": "admin-members",
} }
return render( return render(

View file

@ -40,7 +40,6 @@ DJANGO_APPS = [
THIRD_PARTY_APPS = [ THIRD_PARTY_APPS = [
"allauth", "allauth",
"allauth.account", "allauth.account",
"django_view_decorator",
] ]
LOCAL_APPS = [ LOCAL_APPS = [
@ -154,26 +153,6 @@ ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False
ACCOUNT_USERNAME_REQUIRED = 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: if DEBUG:
INSTALLED_APPS += ["debug_toolbar", "django_browser_reload"] INSTALLED_APPS += ["debug_toolbar", "django_browser_reload"]
MIDDLEWARE += [ MIDDLEWARE += [

2018
src/project/static/css/bootstrap-icons.css vendored Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

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

View file

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -7,6 +7,7 @@
{% block content %} {% block content %}
<div class="content-view"> <div class="content-view">
<h2>{% trans "Email Addresses" %}</h2> <h2>{% trans "Email Addresses" %}</h2>
<p>Her kan du tilføje og ændre emailadresser tilknyttet din konto.</p>
<p>{% trans 'The following email addresses are associated with your account:' %}</p> <p>{% trans 'The following email addresses are associated with your account:' %}</p>
<hr /> <hr />
@ -16,7 +17,7 @@
{% csrf_token %} {% csrf_token %}
<fieldset class="blockLabels"> <fieldset class="blockLabels">
<div class="buttonHolder"> <div class="buttonHolder">
<button class="small" name="action_add_open" style="float:right">Add Email</button> <button class="small" type="submit" name="action_primary" style="float:right">+ Add Email</button>
<button class="small" disabled type="submit" id="action_primary" name="action_primary">Make Primary</button> <button class="small" disabled type="submit" id="action_primary" name="action_primary">Make Primary</button>
<button class="small" type="submit" name="action_send">Re-send Verification</button> <button class="small" type="submit" name="action_send">Re-send Verification</button>
<button class="small" type="submit" name="action_remove">Remove</button> <button class="small" type="submit" name="action_remove">Remove</button>
@ -76,22 +77,21 @@
{% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %} {% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}
</p> </p>
{% endif %} {% endif %}
</div> <!--
<div id="email-add-overlay"> <hr />
<div class="content-view">
<h3>{% trans "Add E-mail" %}</h3> <h3>{% trans "Add E-mail" %}</h3>
<div class="panel-body"> <div class="panel-body">
<form method="post" action="{% url 'account_email' %}" <form method="post" action="{% url 'account_email' %}"
class="add_email"> class="add_email">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} {{ form.as_p }}
<button name="action_add" style="float:right" type="submit"> <button name="action_add" class="btn btn-success" type="submit">
{% trans "Add E-mail" %} {% trans "Add E-mail" %}
</button> </button>
<button id="overlay-close-button" class="secondary">Cancel</button> </form>
</form>
</div>
</div> </div>
-->
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
@ -106,24 +106,6 @@
}); });
} }
let addEmail = document.getElementsByName('action_add_open')[0];
addEmail.addEventListener('click', function(e) {
e.preventDefault();
let overlay = document.getElementById('email-add-overlay')
overlay.style.display = 'flex'
window.addEventListener('keydown', function(e) {
if (e.key == 'Escape') {
overlay.style.display = 'none'
}
})
document.getElementById('overlay-close-button').addEventListener('click', function() {
overlay.style.display = 'none'
})
})
let radio_actions = document.getElementsByName('email'); let radio_actions = document.getElementsByName('email');
if (radio_actions.length) { if (radio_actions.length) {
for (radio of radio_actions) { for (radio of radio_actions) {

View file

@ -10,39 +10,11 @@
<title>{% block head_title %}{% endblock %} {{ site.name }}</title> <title>{% block head_title %}{% endblock %} {{ site.name }}</title>
<link rel="stylesheet" href="{% static "fonts/inter.css" %}"> <link rel="stylesheet" href="{% static "fonts/inter.css" %}">
<link rel="stylesheet" href="{% static "css/style.css" %}"> <link rel="stylesheet" href="{% static "css/style.css" %}">
<link rel="stylesheet" href="{% static "css/dark-style.css" %}">
<script>
const savedTheme = localStorage.getItem('theme');
if (savedTheme === "dark" || (savedTheme == null && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
document.querySelector('html').classList.add('dark');
}
</script>
</head> </head>
<body> <body>
<header> <header>
<h1> data.coop membersystem </h1> <h1> data.coop membersystem </h1>
<div> <a class="logout" href="{% url "account_logout" %}">Log out</a>
<a id="theme-switcher" title="Switch theme">
<svg id="switch-icon"
width="20.00033mm"
height="20.000334mm"
viewBox="0 0 20.00033 20.000334"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g id="layer1">
<path id="path1"
style="fill-opacity:1;stroke:none;stroke-width:0.697782;stroke-linecap:round"
d="M 9.999906,20.000334 A 10,10 0 0 0 20.000329,9.999911 10,10 0 0 0 9.999906,0 10,10 0 0 0 0,9.999911 10,10 0 0 0 9.999906,20.000334 Z m 0,-2.00039 V 1.99988 a 8,8 0 0 1 8.000023,8.000031 8,8 0 0 1 -8.000023,8.000033 z" />
</g>
</svg>
</a>
<a id="logout" href="{% url "account_logout" %}">Log out</a>
</div>
</header> </header>
<main> <main>
<aside> <aside>
@ -94,7 +66,7 @@
{% if perms.membership.administrate_memberships %} {% if perms.membership.administrate_memberships %}
<li> <li>
<a href="{% url "admin-members:list" %}" class="{% active_path "admin-members:list" "current" %}"> <a href="{% url "admin-members" %}" class="{% active_path "admin-members" "current" %}">
Admin Admin
</a> </a>
</li> </li>
@ -108,14 +80,5 @@
<footer> <footer>
data.coop membersystem version 0.0.1 data.coop membersystem version 0.0.1
</footer> </footer>
<script>
const themeSwitcher = document.getElementById('theme-switcher');
themeSwitcher.addEventListener('click', function() {
themeSwitcher.classList.toggle('active')
let isDark = document.querySelector('html').classList.toggle('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
</script>
</body> </body>
</html> </html>

View file

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

View file

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

View file

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

View file

@ -7,6 +7,7 @@ register = template.Library()
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def active_path(context, path_name, class_name) -> str | None: def active_path(context, path_name, class_name) -> str | None:
"""Return the given class name if the current path matches the given path name.""" """Return the given class name if the current path matches the given path name."""
path = reverse(path_name) path = reverse(path_name)
request_path = context.get("request").path request_path = context.get("request").path
@ -18,4 +19,3 @@ def active_path(context, path_name, class_name) -> str | None:
if is_path or is_base_path: if is_path or is_base_path:
return class_name return class_name
return None

View file

@ -1,24 +1,23 @@
import contextlib import contextlib
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING
from typing import Any from typing import Any
from django.contrib.sites.shortcuts import get_current_site from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models import Model
from django.http import HttpRequest from django.http import HttpRequest
from django.http import HttpResponse from django.http import HttpResponse
from django.urls import reverse from django.urls import reverse
from zen_queries import queries_disabled from zen_queries import queries_disabled
from zen_queries import render as zen_queries_render from zen_queries import render as zen_queries_render
if TYPE_CHECKING:
from django.db.models import Model
@dataclass @dataclass
class Row: class Row:
"""A row in a table.""" """
A row in a table.
"""
data: dict[str, str] data: dict[str, str]
actions: list[dict[str, str]] actions: list[dict[str, str]]
@ -26,14 +25,18 @@ class Row:
@dataclass @dataclass
class RowAction: class RowAction:
"""An action that can be performed on a row in a table.""" """
An action that can be performed on a row in a table.
"""
label: str label: str
url_name: str url_name: str
url_kwargs: dict[str, str] url_kwargs: dict[str, str]
def render(self, obj) -> dict[str, str]: def render(self, obj) -> dict[str, str]:
"""Render the action as a dictionary for the given object.""" """
Render the action as a dictionary for the given object.
"""
url = reverse( url = reverse(
self.url_name, self.url_name,
kwargs={key: getattr(obj, value) for key, value in self.url_kwargs.items()}, kwargs={key: getattr(obj, value) for key, value in self.url_kwargs.items()},
@ -47,11 +50,14 @@ def render_list(
entity_name_plural: str, entity_name_plural: str,
objects: list["Model"], objects: list["Model"],
columns: list[tuple[str, str]], columns: list[tuple[str, str]],
row_actions: list[RowAction] | None = None, row_actions: list[RowAction] = None,
list_actions: list[tuple[str, str]] | None = None, list_actions: list[tuple[str, str]] = None,
paginate_by: int | None = None, paginate_by: int = None,
) -> HttpResponse: ) -> HttpResponse:
"""Render a list of objects with a table.""" """
Render a list of objects with a table.
"""
# TODO: List actions # TODO: List actions
total_count = len(objects) total_count = len(objects)
@ -101,18 +107,17 @@ def render_list(
def base_context(request: HttpRequest) -> dict[str, Any]: def base_context(request: HttpRequest) -> dict[str, Any]:
"""Return a base context for all views.""" """
Return a base context for all views.
"""
return {"site": get_current_site(request)} return {"site": get_current_site(request)}
def render(request, template_name, context=None): def render(request, template_name, context=None):
"""Render a template with a base context.""" """
Render a template with a base context.
"""
if context is None: if context is None:
context = {} context = {}
context = base_context(request) | context context = base_context(request) | context
# Make sure to fetch all permissions before rendering the template
# otherwise django-zen-queries will complain about database queries.
request.user.get_all_permissions()
return zen_queries_render(request, template_name, context) return zen_queries_render(request, template_name, context)