From a38a813865b0aad37ab28218416b9c6ca575478f Mon Sep 17 00:00:00 2001 From: Bernard Siebens Date: Sat, 3 Jan 2026 13:00:06 +0100 Subject: [PATCH] Set up `members` app with models, migrations, permissions, and rules integration; updated dependencies and settings --- TeamForge/settings.py | 39 +++++-------------- members/migrations/0001_initial.py | 48 ++++++++++++++++++++++++ members/migrations/0002_member_family.py | 23 ++++++++++++ members/models.py | 26 ++++++++++++- members/rules.py | 9 +++++ pyproject.toml | 4 ++ uv.lock | 11 ++++++ 7 files changed, 129 insertions(+), 31 deletions(-) create mode 100644 members/migrations/0001_initial.py create mode 100644 members/migrations/0002_member_family.py create mode 100644 members/rules.py diff --git a/TeamForge/settings.py b/TeamForge/settings.py index 0fb3bbf..9d1ca02 100644 --- a/TeamForge/settings.py +++ b/TeamForge/settings.py @@ -45,8 +45,9 @@ INSTALLED_APPS = [ "django.contrib.staticfiles", "constance", "tailwind", - "theme.apps.ThemeConfig", - "members.apps.MembersConfig", # Tailwind theme app + "rules.apps.AutodiscoverRulesConfig", + "theme.apps.ThemeConfig", # Tailwind theme app + "members.apps.MembersConfig", ] MIDDLEWARE = [ @@ -83,9 +84,7 @@ WSGI_APPLICATION = "TeamForge.wsgi.application" # https://docs.djangoproject.com/en/6.0/ref/settings/#databases DATABASES = { - "default": config( - "DJANGO_DATABASE_URL", default="sqlite:///db.sqlite3", cast=db_url - ), + "default": config("DJANGO_DATABASE_URL", default="sqlite:///db.sqlite3", cast=db_url), } @@ -132,31 +131,11 @@ MEDIA_ROOT = BASE_DIR / "media" CONSTANCE_BACKEND = "constance.backends.database.DatabaseBackend" CONSTANCE_CONFIG = { - "TF_CLUB_NAME": ( - config("TF_CLUB_NAME", default="TeamForge", cast=str), - "Club Name", - str, - ), - "TF_CLUB_HOME": ( - config("TF_CLUB_HOME", default="TeamForge", cast=str), - "Club Location", - str, - ), - "TF_DEFAULT_SEASON_MONTH": ( - config("TF_DEFAULT_SEASON_MONTH", default=8, cast=int), - "Default season start month", - int, - ), - "TF_DEFAULT_SEASON_DAY": ( - config("TF_DEFAULT_SEASON_DAY", default=1, cast=int), - "Default season start day", - int, - ), - "TF_DEFAULT_SEASON_DURATION": ( - config("TF_DEFAULT_SEASON_DURATION", default="1y", cast=str), - "Default season duration", - str, - ), + "TF_CLUB_NAME": (config("TF_CLUB_NAME", default="TeamForge", cast=str), "Club Name", str), + "TF_CLUB_HOME": (config("TF_CLUB_HOME", default="TeamForge", cast=str), "Club Location", str), + "TF_DEFAULT_SEASON_MONTH": (config("TF_DEFAULT_SEASON_MONTH", default=8, cast=int), "Default season start month", int), + "TF_DEFAULT_SEASON_DAY": (config("TF_DEFAULT_SEASON_DAY", default=1, cast=int), "Default season start day", int), + "TF_DEFAULT_SEASON_DURATION": (config("TF_DEFAULT_SEASON_DURATION", default="1y", cast=str), "Default season duration", str), } PHONENUMBER_DEFAULT_FORMAT = "INTERNATIONAL" diff --git a/members/migrations/0001_initial.py b/members/migrations/0001_initial.py new file mode 100644 index 0000000..2f47b5e --- /dev/null +++ b/members/migrations/0001_initial.py @@ -0,0 +1,48 @@ +# Generated by Django 6.0 on 2026-01-03 11:53 + +import django.db.models.deletion +import rules.contrib.models +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Member", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="member", + to=settings.AUTH_USER_MODEL, + verbose_name="user", + ), + ), + ], + options={ + "verbose_name": "member", + "verbose_name_plural": "members", + "ordering": ["user__last_name", "user__first_name"], + "permissions": [("member_manager", "Can manage members")], + }, + bases=(rules.contrib.models.RulesModelMixin, models.Model), + ), + ] diff --git a/members/migrations/0002_member_family.py b/members/migrations/0002_member_family.py new file mode 100644 index 0000000..f98ee47 --- /dev/null +++ b/members/migrations/0002_member_family.py @@ -0,0 +1,23 @@ +# Generated by Django 6.0 on 2026-01-03 11:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("members", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="member", + name="family", + field=models.ManyToManyField( + blank=True, + related_name="family", + to="members.member", + verbose_name="family", + ), + ), + ] diff --git a/members/models.py b/members/models.py index 71a8362..315ccb1 100644 --- a/members/models.py +++ b/members/models.py @@ -1,3 +1,27 @@ +from django.conf import settings from django.db import models +from django.utils.translation import gettext_lazy as _ +from rules import is_superuser +from rules.contrib.models import RulesModel -# Create your models here. +from members.rules import is_member_manager + + +class Member(RulesModel): + user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="member", verbose_name=_("user")) + family = models.ManyToManyField("self", symmetrical=True, blank=True, verbose_name=_("family")) + + class Meta: + verbose_name = _("member") + verbose_name_plural = _("members") + ordering = ["user__last_name", "user__first_name"] + permissions = [("member_manager", _("Can manage members"))] + rules_permissions = { + "add": is_superuser | is_member_manager, + "change": is_superuser | is_member_manager, + "delete": is_superuser | is_member_manager, + "view": is_superuser | is_member_manager, + } + + def __str__(self): + return self.user.get_full_name() diff --git a/members/rules.py b/members/rules.py new file mode 100644 index 0000000..565c622 --- /dev/null +++ b/members/rules.py @@ -0,0 +1,9 @@ +from typing import Optional + +import rules +from django.contrib.auth.models import AbstractUser + + +@rules.predicate +def is_member_manager(user: Optional[AbstractUser]) -> bool: + return user.has_perm('members.member_manager') \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e354229..a0ef2a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "django-tailwind[cookiecutter,honcho]>=4.4.2", "psycopg2-binary>=2.9.11", "python-decouple>=3.8", + "rules>=3.5", ] [dependency-groups] @@ -19,3 +20,6 @@ dev = [ "coverage>=7.13.1", "ruff>=0.14.10", ] + +[tool.ruff] +line-length = 250 diff --git a/uv.lock b/uv.lock index 5d33a10..dc91dad 100644 --- a/uv.lock +++ b/uv.lock @@ -567,6 +567,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, ] +[[package]] +name = "rules" +version = "3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/36/918cf4cc9fd0e38bb9310b2d1a13ae6ebb2b5732d56e7de6feb4a992a6ed/rules-3.5.tar.gz", hash = "sha256:f01336218f4561bab95f53672d22418b4168baea271423d50d9e8490d64cb27a", size = 55504, upload-time = "2024-09-02T16:01:46.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/33/16213dd62ca8ce8749985318a966ac1300ab55c977b2d66632a45b405c99/rules-3.5-py2.py3-none-any.whl", hash = "sha256:0f00fc9ee448b3f82e9aff9334ab0c56c76dce4dfa14f1598f57969f1022acc0", size = 25658, upload-time = "2024-09-02T16:01:44.844Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -598,6 +607,7 @@ dependencies = [ { name = "django-tailwind", extra = ["cookiecutter", "honcho"] }, { name = "psycopg2-binary" }, { name = "python-decouple" }, + { name = "rules" }, ] [package.dev-dependencies] @@ -616,6 +626,7 @@ requires-dist = [ { name = "django-tailwind", extras = ["cookiecutter", "honcho"], specifier = ">=4.4.2" }, { name = "psycopg2-binary", specifier = ">=2.9.11" }, { name = "python-decouple", specifier = ">=3.8" }, + { name = "rules", specifier = ">=3.5" }, ] [package.metadata.requires-dev]