Compare commits
7 Commits
c48cc3b347
...
535543f82f
Author | SHA1 | Date |
---|---|---|
Víðir Valberg Guðmundsson | 535543f82f | |
Víðir Valberg Guðmundsson | cf3c84b8d9 | |
Víðir Valberg Guðmundsson | 1b65558608 | |
Halfdan Mouritzen | 4112069cac | |
Víðir Valberg Guðmundsson | bdc2d8717c | |
Víðir Valberg Guðmundsson | f99c7ee698 | |
Víðir Valberg Guðmundsson | a098a0b032 |
|
@ -1,5 +1,5 @@
|
|||
default_language_version:
|
||||
python: python3.11
|
||||
python: python3.12
|
||||
exclude: ^.*\b(migrations)\b.*$
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
|
@ -16,45 +16,22 @@ repos:
|
|||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: 'v0.1.11'
|
||||
rev: 'v0.3.0'
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
- --fix
|
||||
- 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/
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.15.0
|
||||
rev: v3.15.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args:
|
||||
- --py311-plus
|
||||
exclude: migrations/
|
||||
- repo: https://github.com/adamchainz/django-upgrade
|
||||
rev: 1.15.0
|
||||
rev: 1.16.0
|
||||
hooks:
|
||||
- id: django-upgrade
|
||||
args:
|
||||
- --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
|
||||
- --target-version=5.0
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.11-slim-bullseye
|
||||
FROM python:3.12-slim-bullseye
|
||||
|
||||
ENV PYTHONFAULTHANDLER=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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";
|
||||
};
|
||||
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
inputs:
|
||||
nixpkgs:
|
||||
url: github:NixOS/nixpkgs/nixpkgs-unstable
|
||||
nixpkgs-python:
|
||||
url: github:cachix/nixpkgs-python
|
|
@ -104,3 +104,26 @@ follow_imports = "normal"
|
|||
[[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
|
||||
|
|
|
@ -7,7 +7,6 @@ from django.db import models
|
|||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
|
@ -99,9 +98,7 @@ class Migration(migrations.Migration):
|
|||
),
|
||||
(
|
||||
"vat",
|
||||
djmoney.models.fields.MoneyField(
|
||||
decimal_places=2, max_digits=16, verbose_name="VAT"
|
||||
),
|
||||
djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16, verbose_name="VAT"),
|
||||
),
|
||||
("is_paid", models.BooleanField(default=False, verbose_name="is paid")),
|
||||
(
|
||||
|
|
|
@ -17,8 +17,7 @@ class CreatedModifiedAbstract(models.Model):
|
|||
|
||||
|
||||
class Account(CreatedModifiedAbstract):
|
||||
"""
|
||||
This is the model where we can give access to several users, such that they
|
||||
"""This is the model where we can give access to several users, such that they
|
||||
can decide which account to use to pay for something.
|
||||
"""
|
||||
|
||||
|
@ -30,8 +29,7 @@ class Account(CreatedModifiedAbstract):
|
|||
|
||||
|
||||
class Transaction(CreatedModifiedAbstract):
|
||||
"""
|
||||
Tracks in and outgoing events of an account. When an order is received, an
|
||||
"""Tracks in and outgoing events of an account. When an order is received, an
|
||||
amount is subtracted, when a payment is received, an amount is added.
|
||||
"""
|
||||
|
||||
|
@ -50,8 +48,7 @@ class Transaction(CreatedModifiedAbstract):
|
|||
|
||||
|
||||
class Order(CreatedModifiedAbstract):
|
||||
"""
|
||||
Scoped out: Contents of invoices will have to be tracked either here or in
|
||||
"""Scoped out: Contents of invoices will have to be tracked either here or in
|
||||
a separate Invoice model. This is undecided because we are not generating
|
||||
invoices at the moment.
|
||||
"""
|
||||
|
@ -91,7 +88,7 @@ class Order(CreatedModifiedAbstract):
|
|||
verbose_name = pgettext_lazy("accounting term", "Order")
|
||||
verbose_name_plural = pgettext_lazy("accounting term", "Orders")
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"Order ID {self.display_id}"
|
||||
|
||||
|
||||
|
@ -116,7 +113,7 @@ class Payment(CreatedModifiedAbstract):
|
|||
description=order.description,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"Payment ID {self.display_id}"
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -8,8 +8,8 @@ from . import models
|
|||
# do stuff
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_balance():
|
||||
@pytest.mark.django_db()
|
||||
def test_balance() -> None:
|
||||
user = User.objects.create_user("test", "lala@adas.com", "1234")
|
||||
account = models.Account.objects.create(owner=user)
|
||||
assert account.balance == 0
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
"""
|
||||
Membership application
|
||||
"""Membership application.
|
||||
======================
|
||||
|
||||
This application's domain relate to organizational structures and
|
||||
|
|
|
@ -5,7 +5,7 @@ from django.db.models.signals import post_migrate
|
|||
class MembershipConfig(AppConfig):
|
||||
name = "membership"
|
||||
|
||||
def ready(self):
|
||||
def ready(self) -> None:
|
||||
from .permissions import persist_permissions
|
||||
|
||||
post_migrate.connect(persist_permissions, sender=self)
|
||||
|
|
|
@ -7,7 +7,6 @@ from django.db import models
|
|||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
import django.contrib.postgres.constraints
|
||||
import django.contrib.postgres.fields.ranges
|
||||
from django.db import migrations, models
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("membership", "0001_initial"),
|
||||
]
|
||||
|
@ -34,9 +34,7 @@ class Migration(migrations.Migration):
|
|||
),
|
||||
(
|
||||
"period",
|
||||
django.contrib.postgres.fields.ranges.DateRangeField(
|
||||
verbose_name="period"
|
||||
),
|
||||
django.contrib.postgres.fields.ranges.DateRangeField(verbose_name="period"),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
# Generated by Django 4.1 on 2023-01-02 21:05
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("membership", "0002_subscriptionperiod_remove_membership_period_and_more"),
|
||||
]
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
# Generated by Django 4.1 on 2023-01-02 21:06
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("membership", "0003_membership_period"),
|
||||
]
|
||||
|
|
|
@ -4,22 +4,20 @@ from django.db import migrations
|
|||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
('membership', '0004_alter_membership_period'),
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
("membership", "0004_alter_membership_period"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Member',
|
||||
fields=[
|
||||
],
|
||||
name="Member",
|
||||
fields=[],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
'constraints': [],
|
||||
"proxy": True,
|
||||
"indexes": [],
|
||||
"constraints": [],
|
||||
},
|
||||
bases=('auth.user',),
|
||||
bases=("auth.user",),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -36,9 +36,7 @@ class Member(User):
|
|||
|
||||
|
||||
class SubscriptionPeriod(CreatedModifiedAbstract):
|
||||
"""
|
||||
Denotes a period for which members should pay their membership fee for.
|
||||
"""
|
||||
"""Denotes a period for which members should pay their membership fee for."""
|
||||
|
||||
period = DateRangeField(verbose_name=_("period"))
|
||||
|
||||
|
@ -52,16 +50,12 @@ class SubscriptionPeriod(CreatedModifiedAbstract):
|
|||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"{self.period.lower} - {self.period.upper or _('next general assembly')}"
|
||||
)
|
||||
def __str__(self) -> str:
|
||||
return f"{self.period.lower} - {self.period.upper or _('next general assembly')}"
|
||||
|
||||
|
||||
class Membership(CreatedModifiedAbstract):
|
||||
"""
|
||||
Tracks that a user has membership of a given type for a given period.
|
||||
"""
|
||||
"""Tracks that a user has membership of a given type for a given period."""
|
||||
|
||||
class QuerySet(models.QuerySet):
|
||||
def for_member(self, member: Member):
|
||||
|
@ -102,13 +96,12 @@ class Membership(CreatedModifiedAbstract):
|
|||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"{self.user} - {self.period}"
|
||||
|
||||
|
||||
class MembershipType(CreatedModifiedAbstract):
|
||||
"""
|
||||
Models membership types. Currently only a name, but will in the future
|
||||
"""Models membership types. Currently only a name, but will in the future
|
||||
possibly contain more information like fees.
|
||||
"""
|
||||
|
||||
|
@ -118,7 +111,7 @@ class MembershipType(CreatedModifiedAbstract):
|
|||
|
||||
name = models.CharField(verbose_name=_("name"), max_length=64)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
PERMISSIONS = []
|
||||
|
||||
|
||||
def persist_permissions(sender, **kwargs):
|
||||
def persist_permissions(sender, **kwargs) -> None:
|
||||
for permission in PERMISSIONS:
|
||||
permission.persist_permission()
|
||||
|
||||
|
@ -23,10 +23,10 @@ class Permission:
|
|||
PERMISSIONS.append(self)
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
def path(self) -> str:
|
||||
return f"{self.app_label}.{self.codename}"
|
||||
|
||||
def persist_permission(self):
|
||||
def persist_permission(self) -> None:
|
||||
content_type, _ = ContentType.objects.get_or_create(
|
||||
app_label=self.app_label,
|
||||
model=self.model,
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
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
|
||||
from utils.view_utils import render
|
||||
from utils.view_utils import render_list
|
||||
from utils.view_utils import RowAction
|
||||
|
||||
|
||||
member_view = namespaced_decorator_factory(namespace="member", base_path="membership")
|
||||
|
||||
|
|
|
@ -156,6 +156,26 @@ ACCOUNT_EMAIL_REQUIRED = True
|
|||
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False
|
||||
ACCOUNT_USERNAME_REQUIRED = False
|
||||
|
||||
# Logging
|
||||
# We want to log everything to stdout in docker
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"handlers": {
|
||||
"console": {
|
||||
"level": "DEBUG",
|
||||
"class": "logging.StreamHandler",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"": {
|
||||
"handlers": ["console"],
|
||||
"level": "DEBUG",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if DEBUG:
|
||||
INSTALLED_APPS += ["debug_toolbar", "django_browser_reload"]
|
||||
MIDDLEWARE += [
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,56 @@
|
|||
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);
|
||||
}
|
|
@ -1,348 +1,573 @@
|
|||
/* Reset */
|
||||
*, *::before, *::after {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: 1.5;
|
||||
}
|
||||
img, picture, video, canvas, svg {
|
||||
|
||||
img,
|
||||
picture,
|
||||
video,
|
||||
canvas,
|
||||
svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
input, button, textarea, select {
|
||||
|
||||
input,
|
||||
button,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
p, h1, h2, h3, h4, h5, h6 {
|
||||
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
#root, #__next {
|
||||
|
||||
#root,
|
||||
#__next {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
/* Variables */
|
||||
:root {
|
||||
/* Colors */
|
||||
--light : #fff;
|
||||
--light-dust : #f6f6f6;
|
||||
--dust : #f1f1f1;
|
||||
--medium-dust : #dadada;
|
||||
--dark-dust : #bfbfbf;
|
||||
--fade : #878787;
|
||||
--twilight : #4a4a4a;
|
||||
--dark : #2a2a2a;
|
||||
--custard : #f0dcac;
|
||||
--water : #a8f3f4;
|
||||
--splash : #4b3aba;
|
||||
--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);
|
||||
--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%;
|
||||
--outer-space: 15%;
|
||||
}
|
||||
}
|
||||
|
||||
html, body {
|
||||
height : 100%;
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
font-size: 1.05em;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight : 600;
|
||||
color : var(--twilight);
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 600;
|
||||
color: var(--twilight);
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight : 500;
|
||||
color : var(--splash);
|
||||
text-decoration : none;
|
||||
<<<<<<< HEAD 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);
|
||||
=======font-weight: 500;
|
||||
color: var(--splash);
|
||||
text-decoration: none;
|
||||
>>>>>>>bdc2d8717cbcab1795b1b2dc4f08f83242e4a4ca
|
||||
}
|
||||
|
||||
body {
|
||||
margin : 0;
|
||||
padding : 0;
|
||||
background : var(--custard);
|
||||
font-family : Inter;
|
||||
font-weight : 400;
|
||||
line-height : 1.6;
|
||||
color : var(--dark);
|
||||
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;
|
||||
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;
|
||||
header>h1 {
|
||||
font-size: 1.44em;
|
||||
}
|
||||
|
||||
header > a.logout {
|
||||
padding : 6px 12px;
|
||||
border-radius : 6px;
|
||||
background : var(--twilight);
|
||||
text-decoration : none;
|
||||
color : var(--dust);
|
||||
#switch-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin: 0 var(--space);
|
||||
top: -2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
header > a.logout:hover {
|
||||
background : var(--splash);
|
||||
color : var(--light);
|
||||
#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 : var(--double-space) var(--outer-space);
|
||||
background : var(--light);
|
||||
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 {
|
||||
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>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>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 {
|
||||
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);
|
||||
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-dust);
|
||||
background : var(--light);
|
||||
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;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
nav ol li {
|
||||
margin : 0;
|
||||
padding : 0;
|
||||
float : left;
|
||||
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>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:first-child>a {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
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 {
|
||||
font-weight : bold;
|
||||
border-color : var(--splash);
|
||||
color : var(--splash);
|
||||
font-weight: bold;
|
||||
border-color: var(--splash);
|
||||
color: var(--splash);
|
||||
}
|
||||
|
||||
article {
|
||||
padding : var(--double-space) var(--outer-space);
|
||||
padding: var(--double-space) var(--outer-space);
|
||||
}
|
||||
|
||||
article > div {
|
||||
background : var(--dust);
|
||||
padding : var(--double-space);
|
||||
margin-bottom : var(--double-space);
|
||||
article>div.content-view {
|
||||
background: var(--dust);
|
||||
padding: var(--double-space);
|
||||
margin-bottom: var(--space);
|
||||
}
|
||||
|
||||
div.content-view > h2 {
|
||||
margin : 0 0 var(--double-space) 0;
|
||||
div.content-view>h2 {
|
||||
margin: 0 0 var(--space) 0;
|
||||
}
|
||||
|
||||
div.services {
|
||||
display : flex;
|
||||
justify-content : start;
|
||||
gap : var(--double-space);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--double-space);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
div.services > div,
|
||||
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 : 3px;
|
||||
opacity : 0.9;
|
||||
cursor : pointer;
|
||||
text-align : center;
|
||||
border : 0;
|
||||
font-weight : 600;
|
||||
letter-spacing : 0.03em;
|
||||
text-decoration : none;
|
||||
}
|
||||
|
||||
div.services > div > a:hover,
|
||||
a.button:hover,
|
||||
button:hover {
|
||||
opacity : 1.0;
|
||||
}
|
||||
|
||||
article table {
|
||||
width : 100%;
|
||||
border-spacing : 0;
|
||||
margin : var(--space) 0;
|
||||
}
|
||||
|
||||
article table thead th {
|
||||
text-align : left;
|
||||
}
|
||||
|
||||
article table tbody tr:nth-child(odd) {
|
||||
background : var(--medium-dust);
|
||||
}
|
||||
|
||||
article table thead th,
|
||||
article table tbody td {
|
||||
padding : var(--half-space);
|
||||
}
|
||||
|
||||
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"] {
|
||||
border : 2px solid var(--twilight);
|
||||
border-radius : 6px;
|
||||
padding : 8px;
|
||||
background : var(--light-dust);
|
||||
width : 100%;
|
||||
}
|
||||
|
||||
#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);
|
||||
<<<<<<< HEAD background: var(--light);
|
||||
padding: var(--double-space);
|
||||
border-radius: 6px;
|
||||
flex: 240px;
|
||||
max-width: 420px;
|
||||
=======background: var(--light);
|
||||
padding: var(--double-space);
|
||||
border-radius: 6px;
|
||||
flex: 240px;
|
||||
max-width: 420px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#loginbox > div.signup {
|
||||
background : var(--water);
|
||||
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: 3px;
|
||||
opacity: 0.9;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
border: 0;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div.services>div>a:hover,
|
||||
a.button:hover,
|
||||
button:hover {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
article table {
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
margin: var(--space) 0;
|
||||
}
|
||||
|
||||
article table thead th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
article table tbody tr:nth-child(odd) {
|
||||
background: var(--medium-dust);
|
||||
}
|
||||
|
||||
article table thead th,
|
||||
article table tbody td {
|
||||
padding: var(--half-space);
|
||||
}
|
||||
|
||||
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"] {
|
||||
border: 2px solid var(--twilight);
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
background: var(--light-dust);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#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);
|
||||
>>>>>>>bdc2d8717cbcab1795b1b2dc4f08f83242e4a4ca 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
#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: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 {
|
||||
border-radius: 0 var(--half-space) var(--half-space) 0;
|
||||
}
|
||||
|
||||
#loginbox > div:last-child > * {
|
||||
flex : 1;
|
||||
#loginbox>div:last-child>* {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#loginbox div.new_here {
|
||||
margin-top : var(--double-space);
|
||||
margin-top: var(--double-space);
|
||||
}
|
||||
|
||||
#loginbox div.new_here h2 {
|
||||
|
@ -350,35 +575,73 @@ form > div > input[type="password"] {
|
|||
}
|
||||
|
||||
#loginbox button {
|
||||
width : 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#loginbox img {
|
||||
padding : 0 var(--double-space);
|
||||
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;
|
||||
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);
|
||||
color: var(--fade);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display : flex;
|
||||
justify-content : center;
|
||||
list-style : none;
|
||||
padding : 0;
|
||||
margin : 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
list-style: none;
|
||||
padding: var(--half-space) 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pagination > li {
|
||||
margin : 0 6px;
|
||||
.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;
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -2,115 +2,95 @@
|
|||
|
||||
{% load i18n %}
|
||||
|
||||
{% block head_title %}{% trans "E-mail Addresses" %}{% endblock %}
|
||||
|
||||
|
||||
{% block head_title %}{% trans "Email Addresses" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="content-view">
|
||||
<h2>{% trans "Email Addresses" %}</h2>
|
||||
<p>{% trans 'The following email addresses are associated with your account:' %}</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<hr />
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h4>{% trans "E-mail Addresses" %}</h4>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% if user.emailaddress_set.all %}
|
||||
<form action="{% url 'account_email' %}" class="email_list" method="post">
|
||||
{% csrf_token %}
|
||||
<fieldset class="blockLabels">
|
||||
<div class="buttonHolder">
|
||||
<button class="small" name="action_add" style="float:right">Add Email …</button>
|
||||
<button class="small" disabled type="submit" id="action_primary" name="action_primary">Make Primary</button>
|
||||
<button class="small" type="submit" name="action_send">Re-send Verification</button>
|
||||
<button class="small" type="submit" name="action_remove">Remove</button>
|
||||
</div>
|
||||
<table class="table" id="user_email_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% trans "Address" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Primary" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for emailaddress in user.emailaddress_set.all %}
|
||||
<tr>
|
||||
<label for="email_radio_{{ forloop.counter }}"
|
||||
class="{% if emailaddress.primary %}primary_email{% endif %}">
|
||||
<td>
|
||||
<input
|
||||
id="email_radio_{{ forloop.counter }}"
|
||||
type="radio"
|
||||
name="email"
|
||||
value="{{ emailaddress.email }}"
|
||||
{% if emailaddress.primary or user.emailaddress_set.count == 1 %}
|
||||
checked="checked"
|
||||
{% endif %}
|
||||
class="{% if emailaddress.primary %}primary_email{% endif %}"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{{ emailaddress.email }}
|
||||
</td>
|
||||
<td>
|
||||
{% if emailaddress.verified %}
|
||||
<span class="label label-success">{% trans "Verified" %}</span>
|
||||
{% else %}
|
||||
<span class="label label-danger">{% trans "Unverified" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if emailaddress.primary %}
|
||||
<span class="label label-primary">{% trans "Primary" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</label>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{% if user.emailaddress_set.all %}
|
||||
<p>{% trans 'The following e-mail addresses are associated with your account:' %}</p>
|
||||
<form action="{% url 'account_email' %}" class="email_list"
|
||||
method="post">
|
||||
{% csrf_token %}
|
||||
<fieldset class="blockLabels">
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{% trans "Address" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Primary" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{% for emailaddress in user.emailaddress_set.all %}
|
||||
<tr class="ctrlHolder">
|
||||
<label for="email_radio_{{ forloop.counter }}"
|
||||
class="{% if emailaddress.primary %}primary_email{% endif %}">
|
||||
<td>
|
||||
<input
|
||||
id="email_radio_{{ forloop.counter }}"
|
||||
type="radio"
|
||||
name="email"
|
||||
value="{{ emailaddress.email }}"
|
||||
{% if emailaddress.primary or user.emailaddress_set.count == 1 %}
|
||||
checked="checked"
|
||||
{% endif %}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{{ emailaddress.email }}
|
||||
</td>
|
||||
<td>
|
||||
{% if emailaddress.verified %}
|
||||
<span class="label label-success">Verified</span>
|
||||
{% else %}
|
||||
<span class="label label-danger">Unverified</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if emailaddress.primary %}
|
||||
<span class="label label-primary">Primary</span>{% endif %}
|
||||
</td>
|
||||
</label>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="buttonHolder">
|
||||
<button class="btn btn-success" type="submit"
|
||||
name="action_primary">Make Primary
|
||||
</button>
|
||||
<button class="btn btn-primary" type="submit"
|
||||
name="action_send">Re-send Verification
|
||||
</button>
|
||||
<button class="btn btn-danger" type="submit"
|
||||
name="action_remove">Remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>
|
||||
<strong>{% trans 'Warning:' %}</strong>
|
||||
{% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h4>{% trans "Add E-mail" %}</h4>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form method="post" action="{% url 'account_email' %}"
|
||||
class="add_email">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button name="action_add" class="btn btn-success" type="submit">
|
||||
{% trans "Add E-mail" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</tbody>
|
||||
</table>
|
||||
</fieldset>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>
|
||||
<strong>{% trans 'Warning:' %}</strong>
|
||||
{% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<!--
|
||||
<hr />
|
||||
|
||||
<h3>{% trans "Add E-mail" %}</h3>
|
||||
<div class="panel-body">
|
||||
<form method="post" action="{% url 'account_email' %}"
|
||||
class="add_email">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button name="action_add" class="btn btn-success" type="submit">
|
||||
{% trans "Add E-mail" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
@ -124,6 +104,15 @@
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
let radio_actions = document.getElementsByName('email');
|
||||
if (radio_actions.length) {
|
||||
for (radio of radio_actions) {
|
||||
radio.addEventListener("change", function (e) {
|
||||
document.getElementById('action_primary').disabled = e.target.classList.contains('primary_email')
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
|
|
@ -13,20 +13,20 @@
|
|||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<h2>Log in</h2>
|
||||
<h2>Login</h2>
|
||||
|
||||
<form method="post" action="">
|
||||
{% csrf_token %}
|
||||
<div>
|
||||
<label for="id_username"
|
||||
class="visually-hidden">
|
||||
{% trans "E-mail" %}
|
||||
{% trans "Email" %}
|
||||
</label>
|
||||
<input type="text"
|
||||
id="id_username"
|
||||
name="login"
|
||||
class="form-control mb-lg-2"
|
||||
placeholder="{% trans "E-mail" %}"
|
||||
placeholder="{% trans "Email" %}"
|
||||
required
|
||||
autofocus>
|
||||
</div>
|
||||
|
@ -42,7 +42,7 @@
|
|||
required>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit">{% trans "Sign in" %}</button>
|
||||
<button type="submit">{% trans "Login" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
<div>
|
||||
|
@ -55,7 +55,7 @@
|
|||
<div class="signup">
|
||||
<img src="https://data.coop/static/img/logo_da.svg" alt="data.coop logo">
|
||||
<div class="new_here">
|
||||
<h2> Are you new here? </h2>
|
||||
<h2>{% trans "Are you new here?" %}</h2>
|
||||
<a class="button" href="{% url "account_signup" %}">{% trans "Become a member" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -10,11 +10,39 @@
|
|||
<title>{% block head_title %}{% endblock %} – {{ site.name }}</title>
|
||||
<link rel="stylesheet" href="{% static "fonts/inter.css" %}">
|
||||
<link rel="stylesheet" href="{% static "css/style.css" %}">
|
||||
<link rel="stylesheet" href="{% static "css/dark-style.css" %}">
|
||||
<script>
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
|
||||
if (savedTheme === "dark" || (savedTheme == null && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
|
||||
document.querySelector('html').classList.add('dark');
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1> data.coop membersystem </h1>
|
||||
<a class="logout" href="{% url "account_logout" %}">Log out</a>
|
||||
<div>
|
||||
<a id="theme-switcher" title="Switch theme">
|
||||
<svg id="switch-icon"
|
||||
width="20.00033mm"
|
||||
height="20.000334mm"
|
||||
viewBox="0 0 20.00033 20.000334"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g id="layer1">
|
||||
<path id="path1"
|
||||
style="fill-opacity:1;stroke:none;stroke-width:0.697782;stroke-linecap:round"
|
||||
d="M 9.999906,20.000334 A 10,10 0 0 0 20.000329,9.999911 10,10 0 0 0 9.999906,0 10,10 0 0 0 0,9.999911 10,10 0 0 0 9.999906,20.000334 Z m 0,-2.00039 V 1.99988 a 8,8 0 0 1 8.000023,8.000031 8,8 0 0 1 -8.000023,8.000033 z" />
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
<a id="logout" href="{% url "account_logout" %}">Log out</a>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<aside>
|
||||
|
@ -78,5 +106,14 @@
|
|||
<footer>
|
||||
data.coop membersystem version 0.0.1
|
||||
</footer>
|
||||
<script>
|
||||
const themeSwitcher = document.getElementById('theme-switcher');
|
||||
themeSwitcher.addEventListener('click', function() {
|
||||
themeSwitcher.classList.toggle('active')
|
||||
let isDark = document.querySelector('html').classList.toggle('dark');
|
||||
|
||||
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
"""URLs for the membersystem"""
|
||||
"""URLs for the membersystem."""
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.urls import include
|
||||
|
@ -15,4 +15,5 @@ if settings.DEBUG:
|
|||
urlpatterns = [
|
||||
path("__debug__/", include("debug_toolbar.urls")),
|
||||
path("__reload__/", include("django_browser_reload.urls")),
|
||||
] + urlpatterns
|
||||
*urlpatterns,
|
||||
]
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from django_view_decorator import view
|
||||
|
||||
from utils.view_utils import render
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
"""
|
||||
WSGI config for membersystem project.
|
||||
"""WSGI config for membersystem project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ register = template.Library()
|
|||
@register.simple_tag(takes_context=True)
|
||||
def active_path(context, path_name, class_name) -> str | None:
|
||||
"""Return the given class name if the current path matches the given path name."""
|
||||
|
||||
path = reverse(path_name)
|
||||
request_path = context.get("request").path
|
||||
|
||||
|
@ -19,3 +18,4 @@ def active_path(context, path_name, class_name) -> str | None:
|
|||
|
||||
if is_path or is_base_path:
|
||||
return class_name
|
||||
return None
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
import contextlib
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Any
|
||||
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
from django.core.exceptions import FieldError
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Model
|
||||
from django.http import HttpRequest
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse
|
||||
from zen_queries import queries_disabled
|
||||
from zen_queries import render as zen_queries_render
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.db.models import Model
|
||||
|
||||
|
||||
@dataclass
|
||||
class Row:
|
||||
"""
|
||||
A row in a table.
|
||||
"""
|
||||
"""A row in a table."""
|
||||
|
||||
data: dict[str, str]
|
||||
actions: list[dict[str, str]]
|
||||
|
@ -25,18 +26,14 @@ class Row:
|
|||
|
||||
@dataclass
|
||||
class RowAction:
|
||||
"""
|
||||
An action that can be performed on a row in a table.
|
||||
"""
|
||||
"""An action that can be performed on a row in a table."""
|
||||
|
||||
label: str
|
||||
url_name: str
|
||||
url_kwargs: dict[str, str]
|
||||
|
||||
def render(self, obj) -> dict[str, str]:
|
||||
"""
|
||||
Render the action as a dictionary for the given object.
|
||||
"""
|
||||
"""Render the action as a dictionary for the given object."""
|
||||
url = reverse(
|
||||
self.url_name,
|
||||
kwargs={key: getattr(obj, value) for key, value in self.url_kwargs.items()},
|
||||
|
@ -50,14 +47,11 @@ def render_list(
|
|||
entity_name_plural: str,
|
||||
objects: list["Model"],
|
||||
columns: list[tuple[str, str]],
|
||||
row_actions: list[RowAction] = None,
|
||||
list_actions: list[tuple[str, str]] = None,
|
||||
paginate_by: int = None,
|
||||
row_actions: list[RowAction] | None = None,
|
||||
list_actions: list[tuple[str, str]] | None = None,
|
||||
paginate_by: int | None = None,
|
||||
) -> HttpResponse:
|
||||
"""
|
||||
Render a list of objects with a table.
|
||||
"""
|
||||
|
||||
"""Render a list of objects with a table."""
|
||||
# TODO: List actions
|
||||
|
||||
total_count = len(objects)
|
||||
|
@ -107,17 +101,18 @@ def render_list(
|
|||
|
||||
|
||||
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)}
|
||||
|
||||
|
||||
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:
|
||||
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)
|
||||
|
|
Loading…
Reference in New Issue