Merge branch 'main' into services
This commit is contained in:
commit
535543f82f
|
@ -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 \
|
||||
|
|
241
devenv.lock
Normal file
241
devenv.lock
Normal file
|
@ -0,0 +1,241 @@
|
|||
{
|
||||
"nodes": {
|
||||
"devenv": {
|
||||
"locked": {
|
||||
"dir": "src/modules",
|
||||
"lastModified": 1707004164,
|
||||
"narHash": "sha256-9Hr8onWtvLk5A8vCEkaE9kxA0D7PR62povFokM1oL5Q=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "0e68853bb27981a4ffd7a7225b59ed84f7180fc7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"dir": "src/modules",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat_2": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1705309234,
|
||||
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "flake-utils",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701680307,
|
||||
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"pre-commit-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1703887061,
|
||||
"narHash": "sha256-gGPa9qWNc6eCXT/+Z5/zMkyYOuRZqeFZBDbopNZQkuY=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "43e1aa1308018f37118e34d3a9cb4f5e75dc11d5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1707451808,
|
||||
"narHash": "sha256-UwDBUNHNRsYKFJzyTMVMTF5qS4xeJlWoeyJf+6vvamU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "442d407992384ed9c0e6d352de75b69079904e4e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-python": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1707114737,
|
||||
"narHash": "sha256-ZXqv2epXAjDjfWbYn+yy4VOmW+C7SuUBoiZkkDoSqA4=",
|
||||
"owner": "cachix",
|
||||
"repo": "nixpkgs-python",
|
||||
"rev": "f34ed02276bc08fe1c91c1bf0ef3589d68028878",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "nixpkgs-python",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-stable": {
|
||||
"locked": {
|
||||
"lastModified": 1704874635,
|
||||
"narHash": "sha256-YWuCrtsty5vVZvu+7BchAxmcYzTMfolSPP5io8+WYCg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3dc440faeee9e889fe2d1b4d25ad0f430d449356",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-23.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1707347730,
|
||||
"narHash": "sha256-0etC/exQIaqC9vliKhc3eZE2Mm2wgLa0tj93ZF/egvM=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6832d0d99649db3d65a0e15fa51471537b2c56a6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-23.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"pre-commit-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat_2",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-stable": "nixpkgs-stable"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1707297608,
|
||||
"narHash": "sha256-ADjo/5VySGlvtCW3qR+vdFF4xM9kJFlRDqcC9ZGI8EA=",
|
||||
"owner": "cachix",
|
||||
"repo": "pre-commit-hooks.nix",
|
||||
"rev": "0db2e67ee49910adfa13010e7f012149660af7f0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "pre-commit-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devenv": "devenv",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nixpkgs-python": "nixpkgs-python",
|
||||
"pre-commit-hooks": "pre-commit-hooks"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
19
devenv.nix
Normal file
19
devenv.nix
Normal file
|
@ -0,0 +1,19 @@
|
|||
{ pkgs, ... }:
|
||||
|
||||
{
|
||||
languages.python.enable = true;
|
||||
languages.python.version = "3.12";
|
||||
|
||||
services.postgres = {
|
||||
enable = true;
|
||||
package = pkgs.postgresql_15;
|
||||
initialDatabases = [ {"name" = "postgres";} ];
|
||||
listen_addresses = "localhost";
|
||||
initialScript = "create user postgres with password 'postgres' superuser;";
|
||||
};
|
||||
|
||||
processes = {
|
||||
app.exec = "while ! pg_isready -d postgres -h localhost -U postgres 2>/dev/null; do sleep 1; done; hatch run server";
|
||||
};
|
||||
|
||||
}
|
5
devenv.yaml
Normal file
5
devenv.yaml
Normal file
|
@ -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 += [
|
||||
|
|
2018
src/project/static/css/bootstrap-icons.css
vendored
2018
src/project/static/css/bootstrap-icons.css
vendored
File diff suppressed because it is too large
Load diff
7
src/project/static/css/bootstrap.min.css
vendored
7
src/project/static/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
56
src/project/static/css/dark-style.css
Normal file
56
src/project/static/css/dark-style.css
Normal file
|
@ -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,29 +2,26 @@
|
|||
|
||||
{% 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">
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h4>{% trans "E-mail Addresses" %}</h4>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<hr />
|
||||
|
||||
{% 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">
|
||||
<form action="{% url 'account_email' %}" class="email_list" method="post">
|
||||
{% csrf_token %}
|
||||
<fieldset class="blockLabels">
|
||||
|
||||
<table class="table">
|
||||
<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>
|
||||
|
@ -34,9 +31,8 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{% for emailaddress in user.emailaddress_set.all %}
|
||||
<tr class="ctrlHolder">
|
||||
<tr>
|
||||
<label for="email_radio_{{ forloop.counter }}"
|
||||
class="{% if emailaddress.primary %}primary_email{% endif %}">
|
||||
<td>
|
||||
|
@ -48,6 +44,7 @@
|
|||
{% if emailaddress.primary or user.emailaddress_set.count == 1 %}
|
||||
checked="checked"
|
||||
{% endif %}
|
||||
class="{% if emailaddress.primary %}primary_email{% endif %}"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -55,14 +52,15 @@
|
|||
</td>
|
||||
<td>
|
||||
{% if emailaddress.verified %}
|
||||
<span class="label label-success">Verified</span>
|
||||
<span class="label label-success">{% trans "Verified" %}</span>
|
||||
{% else %}
|
||||
<span class="label label-danger">Unverified</span>
|
||||
<span class="label label-danger">{% trans "Unverified" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if emailaddress.primary %}
|
||||
<span class="label label-primary">Primary</span>{% endif %}
|
||||
<span class="label label-primary">{% trans "Primary" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</label>
|
||||
</tr>
|
||||
|
@ -70,19 +68,6 @@
|
|||
|
||||
</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 %}
|
||||
|
@ -91,13 +76,10 @@
|
|||
{% 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>
|
||||
<!--
|
||||
<hr />
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h4>{% trans "Add E-mail" %}</h4>
|
||||
</div>
|
||||
<h3>{% trans "Add E-mail" %}</h3>
|
||||
<div class="panel-body">
|
||||
<form method="post" action="{% url 'account_email' %}"
|
||||
class="add_email">
|
||||
|
@ -108,9 +90,7 @@
|
|||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</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 a new issue