feat: add events app + "multi-tenants" aka multi-events
This commit is contained in:
@@ -25,11 +25,13 @@ from .schemas import NoitaSubmissionOut
|
||||
router = Router()
|
||||
NOITA_APP_ID = 881100
|
||||
|
||||
PREFIX = "/events/{event_path}/noita"
|
||||
|
||||
@router.get("results", response=ResultsOut)
|
||||
|
||||
@router.get(f"{PREFIX}/results", response=ResultsOut)
|
||||
@decorate_view(require_game_enabled(NOITA_APP_ID))
|
||||
def get_results(request: HttpRequest):
|
||||
cache_key = f"api:noita:results:{request.user.id}"
|
||||
def get_results(request: HttpRequest, event_path: str):
|
||||
cache_key = f"api:{event_path}:noita:results:{request.user.id}"
|
||||
cached_data = cache.get(cache_key)
|
||||
|
||||
if cached_data is not None:
|
||||
@@ -42,7 +44,7 @@ def get_results(request: HttpRequest):
|
||||
"""
|
||||
# Group objectives by objectiv_id and count occurrences
|
||||
user_objectives = (
|
||||
Objectiv.objects.filter(user=request.user)
|
||||
Objectiv.objects.filter(user=request.user, submission__event=request.event)
|
||||
.values("objectiv_id")
|
||||
.annotate(count=Count("id"))
|
||||
)
|
||||
@@ -118,7 +120,7 @@ def get_results(request: HttpRequest):
|
||||
total_score += points
|
||||
|
||||
# Count deaths for the user
|
||||
deaths_count = DeathCounter.objects.filter(user=request.user).count()
|
||||
deaths_count = DeathCounter.for_event.filter(user=request.user).count()
|
||||
|
||||
data = {
|
||||
"total_score": total_score,
|
||||
@@ -126,19 +128,19 @@ def get_results(request: HttpRequest):
|
||||
"objectives": list(with_points.values()),
|
||||
}
|
||||
|
||||
cache.set(f"api:noita:results:{request.user.id}", data, 300)
|
||||
cache.set(cache_key, data, 300)
|
||||
return data
|
||||
|
||||
|
||||
@router.get("leaderboard", response=LeaderboardOut)
|
||||
@router.get(f"{PREFIX}/leaderboard", response=LeaderboardOut)
|
||||
@decorate_view(require_game_enabled(NOITA_APP_ID))
|
||||
def get_leaderboard(request: HttpRequest):
|
||||
def get_leaderboard(request: HttpRequest, event_path: str):
|
||||
"""
|
||||
Get the global leaderboard for all users ranked by total score.
|
||||
|
||||
Uses Window functions to rank users by their total score in descending order.
|
||||
"""
|
||||
cache_key = "api:noita:leaderboard"
|
||||
cache_key = f"api:{event_path}:noita:leaderboard"
|
||||
cached_data = cache.get(cache_key)
|
||||
|
||||
if cached_data is not None:
|
||||
@@ -150,7 +152,8 @@ def get_leaderboard(request: HttpRequest):
|
||||
|
||||
# Get all objectives with calculated points (grouped by objectiv_id and user)
|
||||
all_objectives = (
|
||||
Objectiv.objects.values("user", "objectiv_id")
|
||||
Objectiv.objects.filter(submission__event=request.event)
|
||||
.values("user", "objectiv_id")
|
||||
.annotate(count=Count("id"))
|
||||
.annotate(
|
||||
# Fetch points from ObjectivPoint using Subquery
|
||||
@@ -201,12 +204,12 @@ def get_leaderboard(request: HttpRequest):
|
||||
for user_id, total_score in user_totals_dict.items():
|
||||
user = User.objects.get(id=user_id)
|
||||
objectives_count = (
|
||||
Objectiv.objects.filter(user_id=user_id)
|
||||
Objectiv.objects.filter(user_id=user_id, submission__event=request.event)
|
||||
.values("objectiv_id")
|
||||
.distinct()
|
||||
.count()
|
||||
)
|
||||
deaths_count = DeathCounter.objects.filter(user_id=user_id).count()
|
||||
deaths_count = DeathCounter.for_event.filter(user_id=user_id).count()
|
||||
users_with_scores.append(
|
||||
{
|
||||
"user_id": user_id,
|
||||
@@ -232,13 +235,15 @@ def get_leaderboard(request: HttpRequest):
|
||||
]
|
||||
|
||||
data = {"leaderboard": leaderboard}
|
||||
cache.set("api:noita:leaderboard", data, 300)
|
||||
cache.set(cache_key, data, 300)
|
||||
return data
|
||||
|
||||
|
||||
@router.post("submit", response={200: NoitaSubmissionOut, 400: dict})
|
||||
@router.post(f"{PREFIX}/submit", response={200: NoitaSubmissionOut, 400: dict})
|
||||
@decorate_view(require_game_enabled(NOITA_APP_ID))
|
||||
def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)):
|
||||
def submit_log_file(
|
||||
request: HttpRequest, event_path: str, file: UploadedFile = File(...)
|
||||
):
|
||||
"""
|
||||
Submit a Noita run file (log file, screenshot, or video).
|
||||
|
||||
@@ -267,6 +272,7 @@ def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)):
|
||||
try:
|
||||
# Create submission
|
||||
submission = LogfileSubmission.objects.create(
|
||||
event=request.event,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
content_type=file.content_type,
|
||||
file_size=file.size,
|
||||
@@ -284,8 +290,8 @@ def submit_log_file(request: HttpRequest, file: UploadedFile = File(...)):
|
||||
|
||||
# Invalidate caches on successful submission
|
||||
if submission.user:
|
||||
cache.delete(f"api:noita:results:{submission.user.id}")
|
||||
cache.delete("api:noita:leaderboard")
|
||||
cache.delete(f"api:{event_path}:noita:results:{submission.user.id}")
|
||||
cache.delete(f"api:{event_path}:noita:leaderboard")
|
||||
|
||||
return {
|
||||
"id": str(submission.id),
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 6.0.5 on 2026-09-15 21:10
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("events", "0002_seed_polylan_43"),
|
||||
("noita", "0012_deathcounter_unique_death_per_seen_at"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="deathcounter",
|
||||
name="event",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="death_counters",
|
||||
to="events.event",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="logfilesubmission",
|
||||
name="event",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="logfile_submissions",
|
||||
to="events.event",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,27 @@
|
||||
from django.db import migrations
|
||||
|
||||
EVENT_PATH = "polylan-43"
|
||||
|
||||
|
||||
def backfill_event(apps, schema_editor):
|
||||
Event = apps.get_model("events", "Event")
|
||||
LogfileSubmission = apps.get_model("noita", "LogfileSubmission")
|
||||
DeathCounter = apps.get_model("noita", "DeathCounter")
|
||||
event = Event.objects.get(path=EVENT_PATH)
|
||||
LogfileSubmission.objects.filter(event__isnull=True).update(event=event)
|
||||
DeathCounter.objects.filter(event__isnull=True).update(event=event)
|
||||
|
||||
|
||||
def noop(apps, schema_editor):
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("noita", "0013_deathcounter_event_logfilesubmission_event"),
|
||||
("events", "0002_seed_polylan_43"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(backfill_event, noop),
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("noita", "0014_backfill_event"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="deathcounter",
|
||||
name="event",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="death_counters",
|
||||
to="events.event",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="logfilesubmission",
|
||||
name="event",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="logfile_submissions",
|
||||
to="events.event",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -4,6 +4,9 @@ from django.utils import timezone
|
||||
|
||||
import uuid
|
||||
|
||||
from events.models import Event
|
||||
from events.scoping import EventScopedManager
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@@ -21,6 +24,12 @@ class LogfileSubmission(models.Model):
|
||||
# Identification
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="logfile_submissions",
|
||||
)
|
||||
|
||||
content_type = models.CharField(max_length=100, help_text="MIME type of the file")
|
||||
file_size = models.PositiveIntegerField(help_text="File size in bytes")
|
||||
file = models.FileField(
|
||||
@@ -44,6 +53,9 @@ class LogfileSubmission(models.Model):
|
||||
|
||||
processed = models.BooleanField(default=False)
|
||||
|
||||
objects = models.Manager()
|
||||
for_event = EventScopedManager()
|
||||
|
||||
|
||||
class Objectiv(models.Model):
|
||||
objectiv_id = models.CharField(max_length=64)
|
||||
@@ -71,10 +83,18 @@ class ObjectivPoint(models.Model):
|
||||
|
||||
|
||||
class DeathCounter(models.Model):
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="death_counters",
|
||||
)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
seed = models.CharField(max_length=32)
|
||||
seen_at = models.DateTimeField()
|
||||
|
||||
objects = models.Manager()
|
||||
for_event = EventScopedManager()
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
|
||||
@@ -31,7 +31,11 @@ def parse_objectives_and_store(logfile: LogfileSubmission) -> None:
|
||||
continue
|
||||
|
||||
if idx == "DEATH":
|
||||
deaths.append(DeathCounter(user=logfile.user, seed=seed, seen_at=ts))
|
||||
deaths.append(
|
||||
DeathCounter(
|
||||
event=logfile.event, user=logfile.user, seed=seed, seen_at=ts
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
objectives.append(
|
||||
|
||||
Reference in New Issue
Block a user