feat(submissions): scoped submissions per user/matchs, sync from polylan
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from accounts.models import CustomUser
|
||||
from games.models import EventGame
|
||||
|
||||
from ..models import Scope
|
||||
from .aandt_client import AAndTAPIError, AAndTClient
|
||||
|
||||
|
||||
class ScopeSyncError(Exception):
|
||||
"""Raised when an EventGame cannot be synced against the AAndT API."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScopeSyncResult:
|
||||
created: int = 0
|
||||
updated: int = 0
|
||||
warnings: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def _submission_url(scope: Scope, /) -> str:
|
||||
path = reverse(
|
||||
"submissions.view",
|
||||
kwargs={
|
||||
"event_path": scope.event_game.event.path,
|
||||
"game_path": scope.event_game.game.path,
|
||||
"scope_id": scope.id,
|
||||
},
|
||||
)
|
||||
return f"{settings.SUBMITTER_BASE_URL.rstrip('/')}{path}"
|
||||
|
||||
|
||||
def _userid_by_members(members: list[dict]) -> dict[int, set[int]]:
|
||||
userids_by_members: dict[int, set[int]] = {}
|
||||
|
||||
for member in members:
|
||||
if "team" in member:
|
||||
userids = {u["id"] for u in member["team"]["members"]}
|
||||
else:
|
||||
userids = {member["user"]["id"]}
|
||||
userids_by_members[member["id"]] = userids
|
||||
|
||||
return userids_by_members
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def sync_event_game_scopes(event_game: EventGame, /) -> ScopeSyncResult:
|
||||
"""Fetch matches and members from the legacy AAndT API and upsert local Scopes.
|
||||
|
||||
Each remote match becomes one Scope (matched on `external_id`), with
|
||||
`assigned_users` set to every local account tied to either side of the
|
||||
match, resolved by username. Active scopes get their submission page's
|
||||
URL pushed back as the match's player-facing message, so participants
|
||||
browsing the legacy site get a direct link here.
|
||||
|
||||
Args:
|
||||
event_game: The EventGame to sync; must have `apibot_token` set.
|
||||
|
||||
Returns:
|
||||
A summary of created/updated scopes and any unresolved participants.
|
||||
|
||||
Raises:
|
||||
ScopeSyncError: When the EventGame has no token, or the API call fails.
|
||||
"""
|
||||
if not event_game.apibot_token:
|
||||
raise ScopeSyncError(f"{event_game} has no AAndT token configured")
|
||||
|
||||
client = AAndTClient(settings.AANDT_API_BASE_URL, event_game.apibot_token)
|
||||
|
||||
try:
|
||||
userids_by_members = _userid_by_members(client.get_members())
|
||||
matches = client.get_matches()
|
||||
except AAndTAPIError as exc:
|
||||
raise ScopeSyncError(str(exc)) from exc
|
||||
|
||||
result = ScopeSyncResult()
|
||||
now = timezone.now()
|
||||
|
||||
for match in matches:
|
||||
userids: set[int] = set()
|
||||
for member_id in match.get("players", {}):
|
||||
userids.update(userids_by_members.get(int(member_id), set()))
|
||||
|
||||
users = list(CustomUser.objects.filter(cas_user_id__in=userids))
|
||||
|
||||
scope, created = Scope.objects.update_or_create(
|
||||
event_game=event_game,
|
||||
external_id=match["id"],
|
||||
defaults={
|
||||
"name": match.get("title") or f"Match {match['id']}",
|
||||
"is_active": match.get("status") == "playing",
|
||||
"synced_at": now,
|
||||
},
|
||||
)
|
||||
scope.assigned_users.set(users)
|
||||
|
||||
result.created += created
|
||||
result.updated += not created
|
||||
|
||||
matched_ids = {
|
||||
int(u.cas_user_id)
|
||||
for u in users
|
||||
if u.cas_user_id and u.cas_user_id.isdigit()
|
||||
}
|
||||
missing = userids - matched_ids
|
||||
if missing:
|
||||
result.warnings.append(
|
||||
f"Match {match['id']}: no local account for {', '.join(sorted(map(str, missing)))}"
|
||||
)
|
||||
|
||||
if scope.is_active:
|
||||
try:
|
||||
client.set_match_message(
|
||||
match["id"],
|
||||
players=(
|
||||
"<h3>Submit your result</h3>"
|
||||
f'<a href="{_submission_url(scope)}" target="_blank" rel="noopener">'
|
||||
"Submit your proof here</a>"
|
||||
),
|
||||
)
|
||||
except AAndTAPIError as exc:
|
||||
result.warnings.append(
|
||||
f"Match {match['id']}: failed to push submission link ({exc})"
|
||||
)
|
||||
|
||||
return result
|
||||
Reference in New Issue
Block a user