Files
polylan-submitter/polylan_submitter/submissions/services/aandt_sync.py
T

132 lines
4.1 KiB
Python

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