Spaces:
Sleeping
Sleeping
| """Per-category completion tracker — powers certificate eligibility. | |
| Idempotent via ``UNIQUE(user_id, category, challenge_id)`` so duplicate | |
| ``/api/training/solved`` calls never double-count. | |
| """ | |
| from typing import Optional | |
| import httpx | |
| from app.core.config import SUPABASE_URL, SUPABASE_ANON_KEY | |
| from app.services.supabase_service import supabase_headers | |
| async def record_user_completion( | |
| user_id: Optional[str], | |
| team_role: str, | |
| category: str, | |
| module: str, | |
| challenge_id: str, | |
| xp_awarded: int, | |
| ): | |
| """Insert (or ignore) a row in ``user_completions``.""" | |
| if not user_id: | |
| return | |
| if not (SUPABASE_URL and SUPABASE_ANON_KEY): | |
| return | |
| headers = supabase_headers(content_type=True) | |
| headers["Prefer"] = "return=minimal,resolution=ignore-duplicates" | |
| payload = { | |
| "user_id": user_id, | |
| "category": category, | |
| "module": module, | |
| "challenge_id": challenge_id, | |
| "xp_awarded": int(xp_awarded or 0), | |
| } | |
| try: | |
| async with httpx.AsyncClient(timeout=10) as client: | |
| r = await client.post( | |
| f"{SUPABASE_URL}/rest/v1/user_completions", | |
| json=payload, | |
| headers=headers, | |
| ) | |
| if r.status_code not in (200, 201): | |
| print(f"[completions] insert failed {r.status_code}: {r.text[:200]}") | |
| except Exception as e: | |
| print(f"[completions] insert exception: {e}") | |
| async def count_completions(user_id: str, category: str) -> int: | |
| if not (SUPABASE_URL and SUPABASE_ANON_KEY): | |
| return 0 | |
| url = ( | |
| f"{SUPABASE_URL}/rest/v1/user_completions" | |
| f"?user_id=eq.{user_id}&category=eq.{category}&select=id" | |
| ) | |
| try: | |
| async with httpx.AsyncClient(timeout=10) as client: | |
| r = await client.get(url, headers=supabase_headers()) | |
| if r.status_code == 200: | |
| return len(r.json()) | |
| except Exception as e: | |
| print(f"[completions] count exception: {e}") | |
| return 0 | |