feat(core): add cross-club scope validator
Add clubmanager.base.validate_club_scope(instance, owning_club_id, ...): a shared model-clean() helper that rejects FKs leaking across clubs. Club-scoped FKs must share the owning club; Member FKs must have a ClubMembership in it. Unset FKs are skipped. Nothing enforced tenant consistency on the FKs between club-scoped rows, so an order could reference another club's product, an event another club's season, and so on. The following commits wire this into each app's clean(). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from club.tenancy import require_current_club
|
||||
|
||||
@@ -10,6 +12,30 @@ if TYPE_CHECKING:
|
||||
from club.models import Club
|
||||
|
||||
|
||||
def validate_club_scope(instance, owning_club_id, *, same_club_fields=(), member_fields=()):
|
||||
"""Reject FKs that leak across clubs.
|
||||
|
||||
``same_club_fields`` are FKs to club-scoped models that must share
|
||||
``owning_club_id``; ``member_fields`` are Member FKs whose target must have
|
||||
a ClubMembership in that club. Unset (None) FKs are skipped. Call from a
|
||||
model's ``clean()``.
|
||||
"""
|
||||
errors = {}
|
||||
for field in same_club_fields:
|
||||
if getattr(instance, f"{field}_id") is not None and getattr(instance, field).club_id != owning_club_id:
|
||||
errors[field] = _("Must belong to the same club.")
|
||||
|
||||
if member_fields:
|
||||
from club.models import ClubMembership
|
||||
|
||||
for field in member_fields:
|
||||
if getattr(instance, f"{field}_id") is not None and not ClubMembership.objects.filter(club_id=owning_club_id, member=getattr(instance, field)).exists():
|
||||
errors[field] = _("Must be a member of this club.")
|
||||
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
|
||||
def unique_slugify(instance, value, *, slug_field="slug", scope=None):
|
||||
"""Return a slug derived from ``value``, unique within ``scope``.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user