132 lines
4.1 KiB
Python
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
|