Compare commits
4 Commits
3e77b1edb8
...
769f18dac8
| Author | SHA1 | Date | |
|---|---|---|---|
| 769f18dac8 | |||
| a38a813865 | |||
| 63ad906557 | |||
| 8f566b2e5e |
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
2
Procfile.tailwind
Normal file
2
Procfile.tailwind
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
django: python manage.py runserver
|
||||||
|
tailwind: python manage.py tailwind start
|
||||||
@@ -45,7 +45,9 @@ INSTALLED_APPS = [
|
|||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
"constance",
|
"constance",
|
||||||
"tailwind",
|
"tailwind",
|
||||||
|
"rules.apps.AutodiscoverRulesConfig",
|
||||||
"theme.apps.ThemeConfig", # Tailwind theme app
|
"theme.apps.ThemeConfig", # Tailwind theme app
|
||||||
|
"members.apps.MembersConfig",
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@@ -82,9 +84,7 @@ WSGI_APPLICATION = "TeamForge.wsgi.application"
|
|||||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": config(
|
"default": config("DJANGO_DATABASE_URL", default="sqlite:///db.sqlite3", cast=db_url),
|
||||||
"DJANGO_DATABASE_URL", default="sqlite:///db.sqlite3", cast=db_url
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -131,31 +131,11 @@ MEDIA_ROOT = BASE_DIR / "media"
|
|||||||
|
|
||||||
CONSTANCE_BACKEND = "constance.backends.database.DatabaseBackend"
|
CONSTANCE_BACKEND = "constance.backends.database.DatabaseBackend"
|
||||||
CONSTANCE_CONFIG = {
|
CONSTANCE_CONFIG = {
|
||||||
"TF_CLUB_NAME": (
|
"TF_CLUB_NAME": (config("TF_CLUB_NAME", default="TeamForge", cast=str), "Club Name", str),
|
||||||
config("TF_CLUB_NAME", default="TeamForge", cast=str),
|
"TF_CLUB_HOME": (config("TF_CLUB_HOME", default="TeamForge Home", cast=str), "Club Location", str),
|
||||||
"Club Name",
|
"TF_DEFAULT_SEASON_MONTH": (config("TF_DEFAULT_SEASON_MONTH", default=8, cast=int), "Default season start month", int),
|
||||||
str,
|
"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_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"
|
PHONENUMBER_DEFAULT_FORMAT = "INTERNATIONAL"
|
||||||
|
|||||||
0
members/__init__.py
Normal file
0
members/__init__.py
Normal file
28
members/admin.py
Normal file
28
members/admin.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from .models import Member
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Member)
|
||||||
|
class MemberAdmin(admin.ModelAdmin):
|
||||||
|
def family_member_count(self, obj):
|
||||||
|
return obj.family_members.count()
|
||||||
|
|
||||||
|
def show_user_is_active(self, obj):
|
||||||
|
return obj.user.is_active
|
||||||
|
|
||||||
|
show_user_is_active.boolean = True
|
||||||
|
show_user_is_active.short_description = _("active?")
|
||||||
|
|
||||||
|
list_display = ["__str__", "user__email", "show_user_is_active", "family_member_count", "birthday", "created", "updated"]
|
||||||
|
date_hierarchy = "birthday"
|
||||||
|
list_filter = ["user__is_superuser", "user__is_active"]
|
||||||
|
readonly_fields = ["created", "updated", "access_token"]
|
||||||
|
filter_horizontal = ["family_members"]
|
||||||
|
raw_id_fields = ["user"]
|
||||||
|
fieldsets = [
|
||||||
|
("GENERAL INFORMATION", {"fields": ["user", "family_members", "birthday", "license", "access_token"]}),
|
||||||
|
("CONTACT_INFORMATION", {"fields": ["phone_number", "emergency_phone_number"]}),
|
||||||
|
("METADATA", {"fields": ["created", "updated"]}),
|
||||||
|
]
|
||||||
5
members/apps.py
Normal file
5
members/apps.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class MembersConfig(AppConfig):
|
||||||
|
name = "members"
|
||||||
48
members/migrations/0001_initial.py
Normal file
48
members/migrations/0001_initial.py
Normal file
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
23
members/migrations/0002_member_family.py
Normal file
23
members/migrations/0002_member_family.py
Normal file
@@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Generated by Django 6.0 on 2026-01-03 12:00
|
||||||
|
|
||||||
|
import django.utils.timezone
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("members", "0002_member_family"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="member",
|
||||||
|
name="created",
|
||||||
|
field=models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
default=django.utils.timezone.now,
|
||||||
|
verbose_name="created",
|
||||||
|
),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="member",
|
||||||
|
name="updated",
|
||||||
|
field=models.DateTimeField(auto_now=True, verbose_name="updated"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="member",
|
||||||
|
name="family",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, to="members.member", verbose_name="family"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Generated by Django 6.0 on 2026-01-03 12:12
|
||||||
|
|
||||||
|
import phonenumber_field.modelfields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("members", "0003_member_created_member_updated_alter_member_family"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="member",
|
||||||
|
name="access_token",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, max_length=255, null=True, verbose_name="access token"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="member",
|
||||||
|
name="birthday",
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name="birthday"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="member",
|
||||||
|
name="emergency_phone_number",
|
||||||
|
field=phonenumber_field.modelfields.PhoneNumberField(
|
||||||
|
blank=True,
|
||||||
|
max_length=128,
|
||||||
|
null=True,
|
||||||
|
region=None,
|
||||||
|
verbose_name="emergency phone number",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="member",
|
||||||
|
name="license",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, max_length=20, null=True, verbose_name="license"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="member",
|
||||||
|
name="phone_number",
|
||||||
|
field=phonenumber_field.modelfields.PhoneNumberField(
|
||||||
|
blank=True,
|
||||||
|
max_length=128,
|
||||||
|
null=True,
|
||||||
|
region=None,
|
||||||
|
verbose_name="phone number",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 6.0 on 2026-01-03 12:20
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("members", "0004_member_access_token_member_birthday_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="member",
|
||||||
|
name="family",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="member",
|
||||||
|
name="family_members",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, to="members.member", verbose_name="family members"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
members/migrations/__init__.py
Normal file
0
members/migrations/__init__.py
Normal file
46
members/models.py
Normal file
46
members/models.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from phonenumber_field.modelfields import PhoneNumberField
|
||||||
|
from rules import is_superuser
|
||||||
|
from rules.contrib.models import RulesModel
|
||||||
|
|
||||||
|
from members.rules import is_member_manager
|
||||||
|
|
||||||
|
|
||||||
|
class MemberManager(models.Manager):
|
||||||
|
def get_queryset(self) -> models.QuerySet:
|
||||||
|
return super().get_queryset().select_related("user")
|
||||||
|
|
||||||
|
|
||||||
|
class Member(RulesModel):
|
||||||
|
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="member", verbose_name=_("user"))
|
||||||
|
family_members = models.ManyToManyField("self", symmetrical=True, blank=True, verbose_name=_("family members"))
|
||||||
|
|
||||||
|
birthday = models.DateField(_("birthday"), blank=True, null=True)
|
||||||
|
license = models.CharField(_("license"), max_length=20, blank=True, null=True)
|
||||||
|
|
||||||
|
phone_number = PhoneNumberField(_("phone number"), blank=True, null=True)
|
||||||
|
emergency_phone_number = PhoneNumberField(_("emergency phone number"), blank=True, null=True)
|
||||||
|
|
||||||
|
access_token = models.CharField(_("access token"), max_length=255, blank=True, null=True)
|
||||||
|
|
||||||
|
created = models.DateTimeField(auto_now_add=True, verbose_name=_("created"))
|
||||||
|
updated = models.DateTimeField(auto_now=True, verbose_name=_("updated"))
|
||||||
|
|
||||||
|
objects = MemberManager()
|
||||||
|
|
||||||
|
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()
|
||||||
9
members/rules.py
Normal file
9
members/rules.py
Normal file
@@ -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')
|
||||||
3
members/tests.py
Normal file
3
members/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
3
members/views.py
Normal file
3
members/views.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
@@ -12,6 +12,7 @@ dependencies = [
|
|||||||
"django-tailwind[cookiecutter,honcho]>=4.4.2",
|
"django-tailwind[cookiecutter,honcho]>=4.4.2",
|
||||||
"psycopg2-binary>=2.9.11",
|
"psycopg2-binary>=2.9.11",
|
||||||
"python-decouple>=3.8",
|
"python-decouple>=3.8",
|
||||||
|
"rules>=3.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
@@ -19,3 +20,6 @@ dev = [
|
|||||||
"coverage>=7.13.1",
|
"coverage>=7.13.1",
|
||||||
"ruff>=0.14.10",
|
"ruff>=0.14.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 250
|
||||||
|
|||||||
BIN
static/teamforge/logo.png
Normal file
BIN
static/teamforge/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
11
uv.lock
generated
11
uv.lock
generated
@@ -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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "six"
|
name = "six"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
@@ -598,6 +607,7 @@ dependencies = [
|
|||||||
{ name = "django-tailwind", extra = ["cookiecutter", "honcho"] },
|
{ name = "django-tailwind", extra = ["cookiecutter", "honcho"] },
|
||||||
{ name = "psycopg2-binary" },
|
{ name = "psycopg2-binary" },
|
||||||
{ name = "python-decouple" },
|
{ name = "python-decouple" },
|
||||||
|
{ name = "rules" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
@@ -616,6 +626,7 @@ requires-dist = [
|
|||||||
{ name = "django-tailwind", extras = ["cookiecutter", "honcho"], specifier = ">=4.4.2" },
|
{ name = "django-tailwind", extras = ["cookiecutter", "honcho"], specifier = ">=4.4.2" },
|
||||||
{ name = "psycopg2-binary", specifier = ">=2.9.11" },
|
{ name = "psycopg2-binary", specifier = ">=2.9.11" },
|
||||||
{ name = "python-decouple", specifier = ">=3.8" },
|
{ name = "python-decouple", specifier = ">=3.8" },
|
||||||
|
{ name = "rules", specifier = ">=3.5" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
|
|||||||
Reference in New Issue
Block a user