Skip to content

Commit 99e36cb

Browse files
Bentechy660xAda
andauthored
Leaderboard group endpoints :) (#321)
* Pin to python3.10 and fix pyyaml * backend work for leaderboard groups * lol * return names * add uritemplate --------- Co-authored-by: Ada Cooke <[email protected]>
1 parent eab79e9 commit 99e36cb

File tree

11 files changed

+1328
-967
lines changed

11 files changed

+1328
-967
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM docker.io/library/python:3.9-slim
1+
FROM docker.io/library/python:3.10-slim
22

33
ARG BUILD_DEPS="build-essential"
44

poetry.lock

Lines changed: 1218 additions & 953 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description = "The Django backend for RACTF."
55
authors = ["RACTF Admins <[email protected]>"]
66

77
[tool.poetry.dependencies]
8-
python = "^3.9"
8+
python = "^3.10"
99
Django = "^4.0"
1010
django-cors-headers = "^3.11"
1111
django-redis = "^5.2"
@@ -31,14 +31,15 @@ Twisted = "22.10.0"
3131
channels-redis = "^3.2.0"
3232
requests = "^2.31.0"
3333
django-anymail = {extras = ["amazon_ses", "mailgun", "sendgrid", "console", "mailjet", "mandrill", "postal", "postmark", "sendinblue", "sparkpost"], version = "^8.5"}
34+
uritemplate = "^4.1.1"
3435

3536
[tool.poetry.dev-dependencies]
3637
ipython = "^8.10.0"
3738
coverage = "^5.3.1"
3839
django-stubs = "^1.7.0"
3940
black = "^20.8b1"
4041
djangorestframework-stubs = "^1.3.0"
41-
PyYAML = "^5.4.1"
42+
PyYAML = "6.0.1"
4243
autoflake = "^1.4"
4344
pytest = "^6.2.4"
4445
pytest-cov = "^2.12.0"

src/backend/viewsets.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def get_serializer_class(self):
2929
class AdminListModelViewSet(ModelViewSet):
3030
def get_serializer_class(self):
3131
if self.request is None:
32-
return self.admin_serializer_class
32+
return self.serializer_class
3333
if self.action == "list" and not is_exporting(self.request):
3434
if self.request.user.is_staff and not self.request.user.should_deny_admin():
3535
return self.list_admin_serializer_class

src/leaderboard/serializers.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ def get_position(self, _) -> int:
2020

2121
class LeaderboardTeamScoreSerializer(serializers.ModelSerializer):
2222
team_name = serializers.ReadOnlyField(source="team.name")
23+
leaderboard_group_name = serializers.ReadOnlyField(source="team.leaderboard_group.name")
2324

2425
class Meta:
2526
model = Score
26-
fields = ["points", "timestamp", "team_name", "reason", "metadata"]
27+
fields = ["points", "timestamp", "team_name", "reason", "metadata", "leaderboard_group_name"]
2728

2829

2930
class LeaderboardUserScoreSerializer(serializers.ModelSerializer):
@@ -35,9 +36,11 @@ class Meta:
3536

3637

3738
class TeamPointsSerializer(serializers.ModelSerializer):
39+
leaderboard_group_name = serializers.ReadOnlyField(source="leaderboard_group.name")
40+
3841
class Meta:
3942
model = Team
40-
fields = ["name", "id", "leaderboard_points"]
43+
fields = ["name", "id", "leaderboard_points", "leaderboard_group_name"]
4144

4245

4346
class UserPointsSerializer(serializers.ModelSerializer):

src/leaderboard/views.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import time
22

33
from django.core.cache import caches
4+
from django.db.models import Window, Q
5+
from django.db.models.functions import RowNumber
6+
47
from rest_framework.generics import ListAPIView
58
from rest_framework.renderers import JSONRenderer
69
from rest_framework.response import Response
@@ -55,7 +58,19 @@ def get(self, request, *args, **kwargs):
5558
return FormattedResponse(cached_leaderboard)
5659

5760
graph_members = config.get("graph_members")
58-
top_teams = Team.objects.visible().ranked()[:graph_members]
61+
62+
teams_with_row_numbers = Team.objects.visible().annotate(
63+
row_number=Window(
64+
expression=RowNumber(),
65+
partition_by=['leaderboard_group'],
66+
order_by=["-leaderboard_points", "last_score"]
67+
)
68+
)
69+
top_teams = teams_with_row_numbers.filter(
70+
Q(row_number__lte=graph_members) &
71+
(Q(leaderboard_group__has_own_leaderboard=True) | Q(leaderboard_group__isnull=True))
72+
)
73+
5974
top_users = (
6075
Member
6176
.objects.filter(is_visible=True)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Generated by Django 4.2.6 on 2023-10-08 17:34
2+
3+
import backend.validators
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
import django_prometheus.models
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
("team", "0004_alter_team_name_team_team_team_username_uniq_idx"),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name="LeaderboardGroup",
18+
fields=[
19+
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
20+
("name", models.CharField(max_length=31, unique=True, validators=[backend.validators.printable_name])),
21+
("description", models.TextField(blank=True, max_length=255)),
22+
("is_self_assignable", models.BooleanField(default=True)),
23+
("has_own_leaderboard", models.BooleanField(default=True)),
24+
],
25+
bases=(django_prometheus.models.ExportModelOperationsMixin("leaderboard_group"), models.Model),
26+
),
27+
migrations.AddField(
28+
model_name="team",
29+
name="leaderboard_group",
30+
field=models.ForeignKey(
31+
null=True,
32+
on_delete=django.db.models.deletion.SET_NULL,
33+
related_name="teams",
34+
to="team.leaderboardgroup",
35+
),
36+
),
37+
]

src/team/models.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from django.db import models
2-
from django.db.models import CASCADE, Prefetch
2+
from django.db.models import CASCADE, SET_NULL, Prefetch
33
from django.db.models.functions import Lower
44
from django.utils import timezone
55
from django_prometheus.models import ExportModelOperationsMixin
@@ -29,6 +29,15 @@ def prefetch_solves(self) -> "models.QuerySet[Team]":
2929
return self.prefetch_related(Prefetch("solves", queryset=Solve.objects.filter(correct=True)))
3030

3131

32+
class LeaderboardGroup(ExportModelOperationsMixin("leaderboard_group"), models.Model):
33+
"""Represents a group which teams can assign themselves to."""
34+
35+
name = models.CharField(max_length=31, unique=True, validators=[printable_name])
36+
description = models.TextField(blank=True, max_length=255)
37+
is_self_assignable = models.BooleanField(default=True)
38+
has_own_leaderboard = models.BooleanField(default=True)
39+
40+
3241
class Team(ExportModelOperationsMixin("team"), models.Model):
3342
"""Represents a team of one or more Members."""
3443

@@ -41,6 +50,7 @@ class Team(ExportModelOperationsMixin("team"), models.Model):
4150
leaderboard_points = models.IntegerField(default=0)
4251
last_score = models.DateTimeField(default=timezone.now)
4352
size_limit_exempt = models.BooleanField(default=False)
53+
leaderboard_group = models.ForeignKey(LeaderboardGroup, on_delete=SET_NULL, related_name="teams", null=True)
4454

4555
objects = TeamQuerySet.as_manager()
4656

src/team/serializers.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from backend.signals import team_create
99
from challenge.serializers import SolveSerializer
1010
from member.serializers import MinimalMemberSerializer
11-
from team.models import Team
11+
from team.models import Team, LeaderboardGroup
1212

1313

1414
class SelfTeamSerializer(IncorrectSolvesMixin, serializers.ModelSerializer):
@@ -30,8 +30,9 @@ class Meta:
3030
"incorrect_solves",
3131
"points",
3232
"leaderboard_points",
33+
"leaderboard_group"
3334
]
34-
read_only_fields = ["id", "is_visible", "incorrect_solves"]
35+
read_only_fields = ["id", "is_visible", "incorrect_solves", "leaderboard_group"]
3536

3637

3738
class TeamSerializer(IncorrectSolvesMixin, serializers.ModelSerializer):
@@ -52,6 +53,7 @@ class Meta:
5253
"incorrect_solves",
5354
"points",
5455
"leaderboard_points",
56+
"leaderboard_group"
5557
]
5658

5759
def get_incorrect_solves(self, instance):
@@ -63,7 +65,13 @@ class ListTeamSerializer(serializers.ModelSerializer):
6365

6466
class Meta:
6567
model = Team
66-
fields = ["id", "name", "members"]
68+
fields = ["id", "name", "members", "leaderboard_group"]
69+
70+
71+
class LeaderboardGroupSerializer(serializers.ModelSerializer):
72+
class Meta:
73+
model = LeaderboardGroup
74+
fields = ["id", "name", "description", "is_self_assignable", "has_own_leaderboard"]
6775

6876

6977
class AdminTeamSerializer(IncorrectSolvesMixin, serializers.ModelSerializer):
@@ -86,6 +94,7 @@ class Meta:
8694
"size_limit_exempt",
8795
"points",
8896
"leaderboard_points",
97+
"leaderboard_group"
8998
]
9099

91100

@@ -98,18 +107,25 @@ class Meta:
98107
class CreateTeamSerializer(serializers.ModelSerializer):
99108
class Meta:
100109
model = Team
101-
fields = ["id", "is_visible", "name", "owner", "password"]
110+
fields = ["id", "is_visible", "name", "owner", "password", "leaderboard_group"]
102111
read_only_fields = ["id", "is_visible", "owner"]
103112

104113
def create(self, validated_data):
105114
try:
106115
name = validated_data["name"]
107116
password = validated_data["password"]
117+
leaderboard_group = validated_data.get("leaderboard_group", None)
118+
119+
if leaderboard_group is not None and not leaderboard_group.is_self_assignable:
120+
raise ValidationError("illegal_leaderboard_group")
121+
108122
team = Team.objects.create(
109123
name=name,
110124
password=password,
111125
owner=self.context["request"].user,
126+
leaderboard_group=leaderboard_group
112127
)
128+
113129
self.context["request"].user.team = team
114130
self.context["request"].user.save()
115131
team_create.send(sender=self.__class__, team=team)

src/team/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@
66
router = DefaultRouter()
77
router.register(r"", views.TeamViewSet, basename="team")
88

9+
group_router = DefaultRouter()
10+
group_router.register(r"", views.LeaderboardGroupViewSet, basename="groups")
11+
912
urlpatterns = [
1013
path("self/", views.SelfView.as_view(), name="team-self"),
1114
path("create/", views.CreateTeamView.as_view(), name="team-create"),
1215
path("join/", views.JoinTeamView.as_view(), name="team-join"),
1316
path("leave/", views.LeaveTeamView.as_view(), name="team-leave"),
17+
path("groups/", include(group_router.urls), name="leaderboard-groups"),
1418
path("", include(router.urls), name="team"),
1519
]

0 commit comments

Comments
 (0)