Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/mcpt/ctf
Browse files Browse the repository at this point in the history
  • Loading branch information
JasonLovesDoggo committed Apr 5, 2024
2 parents 6f7ff12 + 002619f commit 7f8d1c6
Show file tree
Hide file tree
Showing 12 changed files with 104 additions and 44 deletions.
78 changes: 61 additions & 17 deletions gameserver/api/routes.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,75 @@
from django.db.models import F, OuterRef, Subquery
from django.db.models import F, OuterRef, Subquery, Case, When, Q
from django.shortcuts import get_object_or_404

from gameserver.models.cache import ContestScore
from gameserver.models.contest import ContestProblem, ContestSubmission
from gameserver.models.contest import ContestProblem, ContestSubmission, Contest
from ninja import NinjaAPI, Schema
from typing import List, Any


def unicode_safe(string):
return string.encode("unicode_escape").decode()



api = NinjaAPI()


class CTFSchema(Schema):
pos: int
team: str
team: Any = None
score: int
lastAccept: Any

lastAccept: Any = None

@staticmethod
def resolve_lastAccept(obj) -> int:
"""Turns a datetime object into a timestamp."""
if obj["lastAccept"] is None:
return 0
return int(obj["lastAccept"].timestamp())

@staticmethod
def resolve_team(obj):
return unicode_safe(obj["team"])


class CTFTimeSchema(Schema):
standings: List[CTFSchema]
tasks: List[str]

@api.get("/ctftime", response=CTFTimeSchema)
def add(request, contest_id: int):
last_sub_time = ContestSubmission.objects.filter(
participation=OuterRef("pk"), submission__is_correct=True
).values("submission__date_created")
standings = ContestScore.ranks(contest=contest_id).annotate(pos=F("rank"), score=F("points"), team=F("participation__team__name"), lastAccept=Subquery(last_sub_time))
# .only("pos", "team", "score")
task_names = ContestProblem.objects.filter(contest_id=contest_id).prefetch_related("problem__name").values_list("problem__name")

return {"standings": standings, "tasks": task_names}


@api.get("/ctftime/{contest_name}", response=CTFTimeSchema)
def ctftime_standings(request, contest_name: str):
contest_id = get_object_or_404(Contest, name__iexact=contest_name).id
"""Get the standings for a contest in CTFTime format."""
last_sub_time = (
ContestSubmission.objects.filter(
participation_id=OuterRef("participation_id"), submission__is_correct=True
)
.prefetch_related("submission")
.order_by("-submission__date_created")
.values("submission__date_created")
)
standings = (
ContestScore.ranks(contest=contest_id)
.annotate(
pos=F("rank"),
score=F("points"),
team=F("participation__team__name"),
# team=Coalesce(F("participation__team__name"), F("participation__participants__username")),
# Using Coalesce and indexing
# team=Case(
# When(F("participation__team__isnull")==True, then=Q(("participation__participants")[0]["username"])),
# default=F("team_name"),
# output_field=TextField(),
# ),
lastAccept=Subquery(last_sub_time),
)
.values("pos", "score", "team", "lastAccept")
)
task_names = (
ContestProblem.objects.filter(contest_id=contest_id)
.prefetch_related("problem__name")
.values_list("problem__name", flat=True)
)

return {"standings": standings, "tasks": task_names}
4 changes: 2 additions & 2 deletions gameserver/forms/contest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ class ContestJoinForm(forms.Form):
)

def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user", None)
self.contest = kwargs.pop("contest", None)
self.user: models.User = kwargs.pop("user", None)
self.contest: models.Contest = kwargs.pop("contest", None)
super(ContestJoinForm, self).__init__(*args, **kwargs)

if (
Expand Down
8 changes: 6 additions & 2 deletions gameserver/models/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@


class ResetableCache(Protocol):
def can_reset(cls, request: HttpRequest) -> None: ...
def can_reset(cls, request: HttpRequest) -> None:
...


class CacheMeta(models.Model):
Expand Down Expand Up @@ -199,7 +200,10 @@ def ranks(
else:
query = cls.objects.filter(participation=participation)
else:
query = cls.objects.filter(participation__contest=contest)
if isinstance(contest, int):
query = cls.objects.filter(participation__contest_id=contest)
else:
query = cls.objects.filter(participation__contest=contest)
query = query.prefetch_related("participation") # .exclude(
# Q(participants__is_superuser=True)
# | Q(participants__groups__permissions=perm_edit_all_contests)
Expand Down
8 changes: 4 additions & 4 deletions gameserver/models/contest.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def __init__(self, *args, **kwargs):

organizers = models.ManyToManyField("User", related_name="contests_organized", blank=True)
curators = models.ManyToManyField("User", related_name="contests_curated", blank=True)
organizations = models.ManyToManyField("Organization", related_name="contests", blank=True)
organizations = models.ManyToManyField("Organization", related_name="contests", blank=True, help_text="Only users of these organizations can access the contest")

name = models.CharField(max_length=128)
slug = models.SlugField(unique=True, db_index=True)
Expand Down Expand Up @@ -97,7 +97,7 @@ def has_problem(self, problem):
def __meta_key(self):
return f"contest_ranks_{self.pk}"

def ranks(self, queryset=None):
def ranks(self):
return self.ContestScore.ranks(self)

def _ranks(self, queryset=None):
Expand Down Expand Up @@ -182,7 +182,7 @@ def is_accessible_by(self, user):
if self.is_editable_by(user):
return True

return self.is_public and not self.organizations.exists()
return self.is_public

def is_editable_by(self, user):
if not user.is_authenticated:
Expand Down Expand Up @@ -319,7 +319,7 @@ def time_taken(self) -> str:
)

def rank(self):
return self.contest.ranks(participation=self).filter(Q(points__gte=self.points())).count()
return self.contest.ranks().filter(Q(points__gte=self.points())).count()

def has_attempted(self, problem):
return problem.is_attempted_by(self)
Expand Down
17 changes: 6 additions & 11 deletions gameserver/models/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,17 @@ def _get_unique_correct_submissions(self, queryset=None):

def points(self, queryset=None):
cache = UserScore.get(user=self)

if cache is None:
return 0
return 0
return cache.points

def flags(self, queryset=None):
cache = UserScore.get(user=self)
if cache is None:
return 0
return cache.flag_count
return cache.flag_count

def rank(self, queryset=None):
return (
self.cached_ranks(f"user_{self.pk}", queryset)
Expand Down Expand Up @@ -162,10 +163,7 @@ def has_firstblooded(self, problem):
raise TypeError("problem must be a Problem or ContestProblem")

def participation_for_contest(self, contest):
try:
return ContestParticipation.objects.get(participants=self, contest=contest)
except ContestParticipation.DoesNotExist:
return None
return ContestParticipation.objects.filter(participants=self, contest=contest).first()

def remove_contest(self):
self.current_contest = None
Expand Down Expand Up @@ -284,7 +282,4 @@ def member_count(self):
return self.members.all().count()

def participation_for_contest(self, contest):
try:
return ContestParticipation.objects.get(team=self, contest=contest)
except ContestParticipation.DoesNotExist:
return None
return ContestParticipation.objects.filter(team=self, contest=contest).first()
2 changes: 1 addition & 1 deletion gameserver/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,5 @@
views.add_comment,
name="add_comment",
),
path("api/", api.urls)
path("api/", api.urls),
]
20 changes: 17 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion providers/CTFTimeOauth/provider.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
from .views import CTFTimeOauth2Adapter


class CTFTimeOauth2Account(ProviderAccount):
def get_username(self):
return self.account.extra_data.get("username")
Expand Down Expand Up @@ -51,4 +53,4 @@ def get_default_scope(self):
return ["profile:read"]


provider_classes = [CTFTimeOauth2AProvider]
provider_classes = [CTFTimeOauth2AProvider]
2 changes: 1 addition & 1 deletion providers/CTFTimeOauth/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ def get_mocked_response(self):
}
}
""",
) # noqa
) # noqa
2 changes: 1 addition & 1 deletion providers/CTFTimeOauth/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
from .provider import CTFTimeOauth2AProvider


urlpatterns = default_urlpatterns(CTFTimeOauth2AProvider)
urlpatterns = default_urlpatterns(CTFTimeOauth2AProvider)
2 changes: 1 addition & 1 deletion providers/CTFTimeOauth/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ def get_user_info(self, token):


oauth2_login = OAuth2LoginView.adapter_view(CTFTimeOauth2Adapter)
oauth2_callback = OAuth2CallbackView.adapter_view(CTFTimeOauth2Adapter)
oauth2_callback = OAuth2CallbackView.adapter_view(CTFTimeOauth2Adapter)
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ django-debug-toolbar = "^4.3.0"

gunicorn = "*"
django-ninja = "^1.1.0"
redis = "^5.0.3"

[build-system]
requires = ["poetry-core"]
Expand Down

0 comments on commit 7f8d1c6

Please sign in to comment.