Add accounts app: custom User, Member, families & guardianship
Introduce the foundational accounts app: - Custom email-as-username User (AbstractBaseUser + PermissionsMixin) set as AUTH_USER_MODEL, decoupled from membership so children can be members without a login. - Member model holding personal/roster data (names, contact email, phone + emergency phone via django-phonenumber-field, license number, DOB) with an optional link to a User. - Family household grouping and directional Guardianship (guardian -> child) with uniqueness and no-self-guardian constraints. - Custom UserAdmin plus Member/Family admin with inlines and autocomplete. - Settings: register apps, AUTH_USER_MODEL, phonenumber defaults (BE/E164). - Add CLAUDE.md and a tracked static/ directory. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
52
CLAUDE.md
Normal file
52
CLAUDE.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## What this is
|
||||
|
||||
ClubManager is a **single-club** sport club management app + public website, built on **Django 6.0** (Python 3.14+). It is deliberately *not* multi-tenant — there is no `club_id` tenancy; the app manages one club.
|
||||
|
||||
The repo is currently an early **skeleton**: a stock `django-admin startproject` layout with only Django's built-in apps installed. None of the domain apps exist yet — see "Planned architecture" below for the intended shape (encoded in `pyproject.toml`, not yet on disk). Verify against the actual tree before assuming a module exists.
|
||||
|
||||
## Commands
|
||||
|
||||
Dependencies and the virtualenv are managed with **uv** (`pyproject.toml` at repo root, `uv.lock` committed). Run Django/tools through `uv run` so the project venv is used.
|
||||
|
||||
```bash
|
||||
uv sync # install deps (incl. dev group) into .venv
|
||||
uv run python manage.py runserver # dev server
|
||||
uv run python manage.py migrate # apply migrations
|
||||
uv run python manage.py makemigrations
|
||||
uv run python manage.py createsuperuser
|
||||
uv run python manage.py shell
|
||||
|
||||
uv run python manage.py test # run all tests (Django test runner)
|
||||
uv run python manage.py test <app> # one app
|
||||
uv run python manage.py test <app>.tests.<Case> # one TestCase
|
||||
uv run python manage.py test <app>.tests.<Case>.<method> # one test
|
||||
|
||||
uv run ruff check . # lint
|
||||
uv run ruff check --fix . # lint + autofix
|
||||
uv run ruff format . # format
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Settings live in a single `clubmanager/settings.py` and read from the environment via **python-decouple** (`config(...)`), with a local `.env` file for dev. Key vars: `DJANGO_SECRET_KEY` (required), `DJANGO_DEBUG`, `DJANGO_ALLOWED_HOSTS`, `DJANGO_CSRF_TRUSTED_ORIGINS`, `DJANGO_DATABASE_URL`, `DJANGO_TIME_ZONE`.
|
||||
|
||||
The database is configured through a single `DJANGO_DATABASE_URL` (parsed by **dj-database-url**), defaulting to `sqlite:///db.sqlite3` for dev; production is intended to point at PostgreSQL via that URL. Don't hardcode DB settings — go through the env var.
|
||||
|
||||
## Planned architecture
|
||||
|
||||
`pyproject.toml`'s isort `known-first-party` list is the intended app decomposition — treat it as the roadmap when adding domain code:
|
||||
`accounts`, `club`, `members`, `teams`, `events`, `news`, `pages`, `home`, `search`.
|
||||
|
||||
Domain notes (drive modeling decisions):
|
||||
- **Season** is the central organizing concept. Team rosters, events, and attendance are season-scoped — model them with a FK to a season, not as global state.
|
||||
- A **Member** can play on one or more **Teams**, each with a position + jersey number, always tied to a specific season.
|
||||
- Three access tiers, implemented via Django groups/permissions: public site / members + parents / coaches + team managers.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Ruff config anticipates a Wagtail-style codebase (`DJ` Django rules; `RUF012`/`RUF005` ignored for framework idioms; `line-length = 250`). Migrations are excluded from linting — don't hand-edit them to satisfy ruff.
|
||||
- Settings files are exempt from `F403/F405/E501` (star imports allowed) under `clubmanager/settings/*` — note the config expects a settings *package*, though the current code is a single `settings.py`. If you split settings, match that path.
|
||||
0
accounts/__init__.py
Normal file
0
accounts/__init__.py
Normal file
61
accounts/admin.py
Normal file
61
accounts/admin.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
|
||||
from .forms import UserChangeForm, UserCreationForm
|
||||
from .models import Family, Guardianship, Member, User
|
||||
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(BaseUserAdmin):
|
||||
add_form = UserCreationForm
|
||||
form = UserChangeForm
|
||||
model = User
|
||||
|
||||
list_display = ("email", "is_staff", "is_superuser", "is_active")
|
||||
list_filter = ("is_staff", "is_superuser", "is_active", "groups")
|
||||
search_fields = ("email",)
|
||||
ordering = ("email",)
|
||||
|
||||
fieldsets = (
|
||||
(None, {"fields": ("email", "password")}),
|
||||
("Permissions", {"fields": ("is_active", "is_staff", "is_superuser", "groups", "user_permissions")}),
|
||||
("Important dates", {"fields": ("last_login", "date_joined")}),
|
||||
)
|
||||
add_fieldsets = (
|
||||
(None, {
|
||||
"classes": ("wide",),
|
||||
"fields": ("email", "usable_password", "password1", "password2"),
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
class GuardianshipInline(admin.TabularInline):
|
||||
"""Guardians of a member (edit from the child's page)."""
|
||||
|
||||
model = Guardianship
|
||||
fk_name = "child"
|
||||
extra = 0
|
||||
autocomplete_fields = ("guardian",)
|
||||
|
||||
|
||||
class MemberInline(admin.TabularInline):
|
||||
model = Member
|
||||
extra = 0
|
||||
fields = ("first_name", "last_name", "date_of_birth", "license_number")
|
||||
show_change_link = True
|
||||
|
||||
|
||||
@admin.register(Member)
|
||||
class MemberAdmin(admin.ModelAdmin):
|
||||
list_display = ("last_name", "first_name", "date_of_birth", "license_number", "family", "user")
|
||||
list_filter = ("family",)
|
||||
search_fields = ("first_name", "last_name", "email", "license_number")
|
||||
autocomplete_fields = ("user", "family")
|
||||
inlines = (GuardianshipInline,)
|
||||
|
||||
|
||||
@admin.register(Family)
|
||||
class FamilyAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "address")
|
||||
search_fields = ("name",)
|
||||
inlines = (MemberInline,)
|
||||
6
accounts/apps.py
Normal file
6
accounts/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "accounts"
|
||||
16
accounts/forms.py
Normal file
16
accounts/forms.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.contrib.auth.forms import BaseUserCreationForm
|
||||
from django.contrib.auth.forms import UserChangeForm as BaseUserChangeForm
|
||||
|
||||
from .models import User
|
||||
|
||||
|
||||
class UserCreationForm(BaseUserCreationForm):
|
||||
class Meta(BaseUserCreationForm.Meta):
|
||||
model = User
|
||||
fields = ("email",)
|
||||
|
||||
|
||||
class UserChangeForm(BaseUserChangeForm):
|
||||
class Meta(BaseUserChangeForm.Meta):
|
||||
model = User
|
||||
fields = "__all__"
|
||||
32
accounts/managers.py
Normal file
32
accounts/managers.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from django.contrib.auth.base_user import BaseUserManager
|
||||
|
||||
|
||||
class UserManager(BaseUserManager):
|
||||
"""Manager for the email-based custom User model."""
|
||||
|
||||
use_in_migrations = True
|
||||
|
||||
def _create_user(self, email, password, **extra_fields):
|
||||
if not email:
|
||||
raise ValueError("Users must have an email address.")
|
||||
email = self.normalize_email(email)
|
||||
user = self.model(email=email, **extra_fields)
|
||||
user.set_password(password)
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
|
||||
def create_user(self, email, password=None, **extra_fields):
|
||||
extra_fields.setdefault("is_staff", False)
|
||||
extra_fields.setdefault("is_superuser", False)
|
||||
return self._create_user(email, password, **extra_fields)
|
||||
|
||||
def create_superuser(self, email, password=None, **extra_fields):
|
||||
extra_fields.setdefault("is_staff", True)
|
||||
extra_fields.setdefault("is_superuser", True)
|
||||
|
||||
if extra_fields.get("is_staff") is not True:
|
||||
raise ValueError("Superuser must have is_staff=True.")
|
||||
if extra_fields.get("is_superuser") is not True:
|
||||
raise ValueError("Superuser must have is_superuser=True.")
|
||||
|
||||
return self._create_user(email, password, **extra_fields)
|
||||
97
accounts/migrations/0001_initial.py
Normal file
97
accounts/migrations/0001_initial.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# Generated by Django 6.0.6 on 2026-07-01 22:04
|
||||
|
||||
import accounts.managers
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import phonenumber_field.modelfields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Family',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text='e.g. "The Smiths"', max_length=150)),
|
||||
('address', models.CharField(blank=True, max_length=255)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'families',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Guardianship',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('relationship', models.CharField(choices=[('parent', 'Parent'), ('guardian', 'Guardian'), ('other', 'Other')], default='parent', max_length=20)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('email', models.EmailField(max_length=254, unique=True)),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Whether the user can log into the admin site.')),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
managers=[
|
||||
('objects', accounts.managers.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Member',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('first_name', models.CharField(max_length=150)),
|
||||
('last_name', models.CharField(max_length=150)),
|
||||
('email', models.EmailField(blank=True, max_length=254)),
|
||||
('phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, region=None)),
|
||||
('emergency_phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, region=None)),
|
||||
('license_number', models.CharField(blank=True, max_length=50)),
|
||||
('date_of_birth', models.DateField()),
|
||||
('family', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='accounts.family')),
|
||||
('guardians', models.ManyToManyField(related_name='dependents', through='accounts.Guardianship', through_fields=('child', 'guardian'), to='accounts.member')),
|
||||
('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='member', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['last_name', 'first_name'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='guardianship',
|
||||
name='child',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='child_links', to='accounts.member'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='guardianship',
|
||||
name='guardian',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='guardian_links', to='accounts.member'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='guardianship',
|
||||
constraint=models.UniqueConstraint(fields=('guardian', 'child'), name='unique_guardianship'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='guardianship',
|
||||
constraint=models.CheckConstraint(condition=models.Q(('guardian', models.F('child')), _negated=True), name='guardian_not_self'),
|
||||
),
|
||||
]
|
||||
0
accounts/migrations/__init__.py
Normal file
0
accounts/migrations/__init__.py
Normal file
140
accounts/models.py
Normal file
140
accounts/models.py
Normal file
@@ -0,0 +1,140 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
|
||||
from .managers import UserManager
|
||||
|
||||
|
||||
class User(AbstractBaseUser, PermissionsMixin):
|
||||
"""Login identity. Only people who actually sign in get a User.
|
||||
|
||||
Personal/roster data lives on :class:`Member`; this model carries just the
|
||||
authentication identity. ``PermissionsMixin`` provides ``is_superuser``,
|
||||
``groups`` and ``user_permissions`` (the basis for the access tiers).
|
||||
"""
|
||||
|
||||
email = models.EmailField(unique=True)
|
||||
is_staff = models.BooleanField(
|
||||
default=False,
|
||||
help_text="Whether the user can log into the admin site.",
|
||||
)
|
||||
is_active = models.BooleanField(default=True)
|
||||
date_joined = models.DateTimeField(default=timezone.now)
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
USERNAME_FIELD = "email"
|
||||
REQUIRED_FIELDS = []
|
||||
|
||||
def __str__(self):
|
||||
return self.get_full_name()
|
||||
|
||||
def get_full_name(self):
|
||||
member = getattr(self, "member", None)
|
||||
if member is not None:
|
||||
return f"{member.first_name} {member.last_name}".strip()
|
||||
return self.email
|
||||
|
||||
def get_short_name(self):
|
||||
member = getattr(self, "member", None)
|
||||
if member is not None:
|
||||
return member.first_name
|
||||
return self.email
|
||||
|
||||
|
||||
class Family(models.Model):
|
||||
"""A household grouping members together."""
|
||||
|
||||
name = models.CharField(max_length=150, help_text='e.g. "The Smiths"')
|
||||
address = models.CharField(max_length=255, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["name"]
|
||||
verbose_name_plural = "families"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Member(models.Model):
|
||||
"""A person in the club. May or may not have a login (:attr:`user`)."""
|
||||
|
||||
user = models.OneToOneField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="member",
|
||||
)
|
||||
|
||||
first_name = models.CharField(max_length=150)
|
||||
last_name = models.CharField(max_length=150)
|
||||
# Contact email — optional. Children may have none; a login email lives on User.
|
||||
email = models.EmailField(blank=True)
|
||||
phone = PhoneNumberField(blank=True)
|
||||
emergency_phone = PhoneNumberField(blank=True)
|
||||
license_number = models.CharField(max_length=50, blank=True)
|
||||
date_of_birth = models.DateField()
|
||||
|
||||
family = models.ForeignKey(
|
||||
Family,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="members",
|
||||
)
|
||||
guardians = models.ManyToManyField(
|
||||
"self",
|
||||
through="Guardianship",
|
||||
through_fields=("child", "guardian"),
|
||||
symmetrical=False,
|
||||
related_name="dependents",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ["last_name", "first_name"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
|
||||
|
||||
class Guardianship(models.Model):
|
||||
"""Directional guardian -> child relationship between two members."""
|
||||
|
||||
class Relationship(models.TextChoices):
|
||||
PARENT = "parent", "Parent"
|
||||
GUARDIAN = "guardian", "Guardian"
|
||||
OTHER = "other", "Other"
|
||||
|
||||
guardian = models.ForeignKey(
|
||||
Member,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="guardian_links",
|
||||
)
|
||||
child = models.ForeignKey(
|
||||
Member,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="child_links",
|
||||
)
|
||||
relationship = models.CharField(
|
||||
max_length=20,
|
||||
choices=Relationship.choices,
|
||||
default=Relationship.PARENT,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["guardian", "child"],
|
||||
name="unique_guardianship",
|
||||
),
|
||||
models.CheckConstraint(
|
||||
condition=~models.Q(guardian=models.F("child")),
|
||||
name="guardian_not_self",
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.guardian} → {self.child} ({self.get_relationship_display()})"
|
||||
87
accounts/tests.py
Normal file
87
accounts/tests.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.test import TestCase
|
||||
|
||||
from .models import Family, Guardianship, Member, User
|
||||
|
||||
|
||||
class UserManagerTests(TestCase):
|
||||
def test_create_user_with_email(self):
|
||||
user = User.objects.create_user(email="Parent@Example.com", password="pw")
|
||||
self.assertEqual(user.email, "Parent@example.com") # domain normalized
|
||||
self.assertTrue(user.check_password("pw"))
|
||||
self.assertFalse(user.is_staff)
|
||||
self.assertFalse(user.is_superuser)
|
||||
|
||||
def test_create_superuser(self):
|
||||
admin = User.objects.create_superuser(email="admin@example.com", password="pw")
|
||||
self.assertTrue(admin.is_staff)
|
||||
self.assertTrue(admin.is_superuser)
|
||||
|
||||
def test_create_user_requires_email(self):
|
||||
with self.assertRaises(ValueError):
|
||||
User.objects.create_user(email="", password="pw")
|
||||
|
||||
def test_create_superuser_rejects_non_superuser_flag(self):
|
||||
with self.assertRaises(ValueError):
|
||||
User.objects.create_superuser(email="a@b.com", password="pw", is_superuser=False)
|
||||
|
||||
|
||||
class MemberTests(TestCase):
|
||||
def test_member_without_login_or_optional_fields(self):
|
||||
member = Member.objects.create(
|
||||
first_name="Kid",
|
||||
last_name="Smith",
|
||||
date_of_birth="2015-05-01",
|
||||
)
|
||||
self.assertIsNone(member.user)
|
||||
self.assertEqual(member.email, "")
|
||||
self.assertEqual(member.phone, "")
|
||||
self.assertEqual(member.license_number, "")
|
||||
|
||||
def test_valid_phone_accepted(self):
|
||||
member = Member(
|
||||
first_name="Ann",
|
||||
last_name="Smith",
|
||||
date_of_birth="1980-01-01",
|
||||
phone="+32470123456",
|
||||
)
|
||||
member.full_clean() # should not raise
|
||||
|
||||
def test_invalid_phone_rejected(self):
|
||||
member = Member(
|
||||
first_name="Ann",
|
||||
last_name="Smith",
|
||||
date_of_birth="1980-01-01",
|
||||
phone="not-a-number",
|
||||
)
|
||||
with self.assertRaises(ValidationError):
|
||||
member.full_clean()
|
||||
|
||||
|
||||
class FamilyAndGuardianshipTests(TestCase):
|
||||
def setUp(self):
|
||||
self.family = Family.objects.create(name="The Smiths")
|
||||
self.parent = Member.objects.create(
|
||||
first_name="Ann", last_name="Smith", date_of_birth="1980-01-01", family=self.family
|
||||
)
|
||||
self.child = Member.objects.create(
|
||||
first_name="Kid", last_name="Smith", date_of_birth="2015-05-01", family=self.family
|
||||
)
|
||||
|
||||
def test_family_groups_members(self):
|
||||
self.assertEqual(self.family.members.count(), 2)
|
||||
|
||||
def test_guardianship_relations_resolve(self):
|
||||
Guardianship.objects.create(guardian=self.parent, child=self.child)
|
||||
self.assertIn(self.parent, self.child.guardians.all())
|
||||
self.assertIn(self.child, self.parent.dependents.all())
|
||||
|
||||
def test_no_self_guardianship(self):
|
||||
with transaction.atomic(), self.assertRaises(IntegrityError):
|
||||
Guardianship.objects.create(guardian=self.parent, child=self.parent)
|
||||
|
||||
def test_guardianship_is_unique(self):
|
||||
Guardianship.objects.create(guardian=self.parent, child=self.child)
|
||||
with transaction.atomic(), self.assertRaises(IntegrityError):
|
||||
Guardianship.objects.create(guardian=self.parent, child=self.child)
|
||||
@@ -43,8 +43,12 @@ INSTALLED_APPS = [
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"phonenumber_field",
|
||||
"accounts",
|
||||
]
|
||||
|
||||
AUTH_USER_MODEL = "accounts.User"
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
@@ -123,3 +127,9 @@ STATICFILES_DIRS = [BASE_DIR / "static"]
|
||||
|
||||
MEDIA_URL = "media/"
|
||||
MEDIA_ROOT = BASE_DIR / "media"
|
||||
|
||||
|
||||
# Phone numbers (django-phonenumber-field)
|
||||
|
||||
PHONENUMBER_DEFAULT_REGION = "BE"
|
||||
PHONENUMBER_DB_FORMAT = "E164"
|
||||
|
||||
@@ -5,6 +5,7 @@ requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
"dj-database-url>=3.1.2",
|
||||
"django>=6.0.6",
|
||||
"django-phonenumber-field[phonenumbers]>=8.4.0",
|
||||
"python-decouple>=3.8",
|
||||
]
|
||||
|
||||
|
||||
0
static/.gitkeep
Normal file
0
static/.gitkeep
Normal file
28
uv.lock
generated
28
uv.lock
generated
@@ -18,6 +18,7 @@ source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "dj-database-url" },
|
||||
{ name = "django" },
|
||||
{ name = "django-phonenumber-field", extra = ["phonenumbers"] },
|
||||
{ name = "python-decouple" },
|
||||
]
|
||||
|
||||
@@ -30,6 +31,7 @@ dev = [
|
||||
requires-dist = [
|
||||
{ name = "dj-database-url", specifier = ">=3.1.2" },
|
||||
{ name = "django", specifier = ">=6.0.6" },
|
||||
{ name = "django-phonenumber-field", extras = ["phonenumbers"], specifier = ">=8.4.0" },
|
||||
{ name = "python-decouple", specifier = ">=3.8" },
|
||||
]
|
||||
|
||||
@@ -62,6 +64,32 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/50/23f9dc45483419a3cc2085b498b25adfbf10642b2941c73e6d2dfaffc9ab/django-6.0.6-py3-none-any.whl", hash = "sha256:25148b1194c47c2e685e5f5e9c5d59c78b075dfd282cb9618861ba6c1708f4d2", size = 8373354, upload-time = "2026-06-03T13:02:41.72Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django-phonenumber-field"
|
||||
version = "8.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/87/bf/8aa60c9834773b955dff1ddea842e361e8daaf6b0945d5bfc29fc66d53ab/django_phonenumber_field-8.4.0.tar.gz", hash = "sha256:2b83e843dac35eec6a69880a166487235b737a71a1e38c9a52e5ad67d6996083", size = 45512, upload-time = "2025-11-24T15:09:51.904Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/a3/f6b85a9246e22cf719752ad83f5e95aa9ba12419b2f9eff70f20d30e55df/django_phonenumber_field-8.4.0-py3-none-any.whl", hash = "sha256:7a1cb3a6456edb54d879f11ffa0acb227ded08c93b587035d0f28093f0e46511", size = 69528, upload-time = "2025-11-24T15:09:45.479Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
phonenumbers = [
|
||||
{ name = "phonenumbers" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phonenumbers"
|
||||
version = "9.0.33"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/75/37/dfc4cf24169f1a7169ebaedaf896c818f0add8603409d1e748e3085ccdc0/phonenumbers-9.0.33.tar.gz", hash = "sha256:9ab8a02b940b90c64f3866c0b25a30e567ddf7bb9836a3e11efdb0478f65fc1c", size = 2306756, upload-time = "2026-06-22T10:23:33.428Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/29/f7e30e3dbd3c7e3d9c4a55006112c04ee62b4765a31f21bcc28c253ac3f1/phonenumbers-9.0.33-py2.py3-none-any.whl", hash = "sha256:ba1d0da52711d5fdda6b2b673b2621fe80774fc5d1b2e5a6ef783396b0343186", size = 2595422, upload-time = "2026-06-22T10:23:29.925Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-decouple"
|
||||
version = "3.8"
|
||||
|
||||
Reference in New Issue
Block a user