Skip to content

Commit

Permalink
backend work for leaderboard groups
Browse files Browse the repository at this point in the history
  • Loading branch information
Bentechy66 committed Oct 8, 2023
1 parent 8c621e3 commit cd12628
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 10 deletions.
Binary file added PyYAML-6.0.1-cp311-cp311-win_amd64.whl
Binary file not shown.
2 changes: 1 addition & 1 deletion src/backend/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def get_serializer_class(self):
class AdminListModelViewSet(ModelViewSet):
def get_serializer_class(self):
if self.request is None:
return self.admin_serializer_class
return self.serializer_class
if self.action == "list" and not is_exporting(self.request):
if self.request.user.is_staff and not self.request.user.should_deny_admin():
return self.list_admin_serializer_class
Expand Down
5 changes: 3 additions & 2 deletions src/leaderboard/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ def get_position(self, _) -> int:

class LeaderboardTeamScoreSerializer(serializers.ModelSerializer):
team_name = serializers.ReadOnlyField(source="team.name")
leaderboard_group_name = serializers.ReadOnlyField(source="team.leaderboard_group.name")

class Meta:
model = Score
fields = ["points", "timestamp", "team_name", "reason", "metadata"]
fields = ["points", "timestamp", "team_name", "reason", "metadata", "leaderboard_group_name"]


class LeaderboardUserScoreSerializer(serializers.ModelSerializer):
Expand All @@ -37,7 +38,7 @@ class Meta:
class TeamPointsSerializer(serializers.ModelSerializer):
class Meta:
model = Team
fields = ["name", "id", "leaderboard_points"]
fields = ["name", "id", "leaderboard_points", "leaderboard_group"]


class UserPointsSerializer(serializers.ModelSerializer):
Expand Down
17 changes: 16 additions & 1 deletion src/leaderboard/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import time

from django.core.cache import caches
from django.db.models import Window, Q
from django.db.models.functions import RowNumber

from rest_framework.generics import ListAPIView
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
Expand Down Expand Up @@ -55,7 +58,19 @@ def get(self, request, *args, **kwargs):
return FormattedResponse(cached_leaderboard)

graph_members = config.get("graph_members")
top_teams = Team.objects.visible().ranked()[:graph_members]

teams_with_row_numbers = Team.objects.visible().annotate(
row_number=Window(
expression=RowNumber(),
partition_by=['leaderboard_group'],
order_by=["-leaderboard_points", "last_score"]
)
)
top_teams = teams_with_row_numbers.filter(
Q(row_number__lte=graph_members) &
(Q(leaderboard_group__has_own_leaderboard=True) | Q(leaderboard_group__isnull=True))
)

top_users = (
Member
.objects.filter(is_visible=True)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generated by Django 4.2.6 on 2023-10-08 17:34

import backend.validators
from django.db import migrations, models
import django.db.models.deletion
import django_prometheus.models


class Migration(migrations.Migration):

dependencies = [
("team", "0004_alter_team_name_team_team_team_username_uniq_idx"),
]

operations = [
migrations.CreateModel(
name="LeaderboardGroup",
fields=[
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(max_length=31, unique=True, validators=[backend.validators.printable_name])),
("description", models.TextField(blank=True, max_length=255)),
("is_self_assignable", models.BooleanField(default=True)),
("has_own_leaderboard", models.BooleanField(default=True)),
],
bases=(django_prometheus.models.ExportModelOperationsMixin("leaderboard_group"), models.Model),
),
migrations.AddField(
model_name="team",
name="leaderboard_group",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="teams",
to="team.leaderboardgroup",
),
),
]
12 changes: 11 additions & 1 deletion src/team/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.db import models
from django.db.models import CASCADE, Prefetch
from django.db.models import CASCADE, SET_NULL, Prefetch
from django.db.models.functions import Lower
from django.utils import timezone
from django_prometheus.models import ExportModelOperationsMixin
Expand Down Expand Up @@ -29,6 +29,15 @@ def prefetch_solves(self) -> "models.QuerySet[Team]":
return self.prefetch_related(Prefetch("solves", queryset=Solve.objects.filter(correct=True)))


class LeaderboardGroup(ExportModelOperationsMixin("leaderboard_group"), models.Model):
"""Represents a group which teams can assign themselves to."""

name = models.CharField(max_length=31, unique=True, validators=[printable_name])
description = models.TextField(blank=True, max_length=255)
is_self_assignable = models.BooleanField(default=True)
has_own_leaderboard = models.BooleanField(default=True)


class Team(ExportModelOperationsMixin("team"), models.Model):
"""Represents a team of one or more Members."""

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

objects = TeamQuerySet.as_manager()

Expand Down
24 changes: 20 additions & 4 deletions src/team/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from backend.signals import team_create
from challenge.serializers import SolveSerializer
from member.serializers import MinimalMemberSerializer
from team.models import Team
from team.models import Team, LeaderboardGroup


class SelfTeamSerializer(IncorrectSolvesMixin, serializers.ModelSerializer):
Expand All @@ -30,8 +30,9 @@ class Meta:
"incorrect_solves",
"points",
"leaderboard_points",
"leaderboard_group"
]
read_only_fields = ["id", "is_visible", "incorrect_solves"]
read_only_fields = ["id", "is_visible", "incorrect_solves", "leaderboard_group"]


class TeamSerializer(IncorrectSolvesMixin, serializers.ModelSerializer):
Expand All @@ -52,6 +53,7 @@ class Meta:
"incorrect_solves",
"points",
"leaderboard_points",
"leaderboard_group"
]

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

class Meta:
model = Team
fields = ["id", "name", "members"]
fields = ["id", "name", "members", "leaderboard_group"]


class LeaderboardGroupSerializer(serializers.ModelSerializer):
class Meta:
model = LeaderboardGroup
fields = ["name", "description", "is_self_assignable", "has_own_leaderboard"]


class AdminTeamSerializer(IncorrectSolvesMixin, serializers.ModelSerializer):
Expand All @@ -86,6 +94,7 @@ class Meta:
"size_limit_exempt",
"points",
"leaderboard_points",
"leaderboard_group"
]


Expand All @@ -98,18 +107,25 @@ class Meta:
class CreateTeamSerializer(serializers.ModelSerializer):
class Meta:
model = Team
fields = ["id", "is_visible", "name", "owner", "password"]
fields = ["id", "is_visible", "name", "owner", "password", "leaderboard_group"]
read_only_fields = ["id", "is_visible", "owner"]

def create(self, validated_data):
try:
name = validated_data["name"]
password = validated_data["password"]
leaderboard_group = validated_data.get("leaderboard_group", None)

if leaderboard_group is not None and not leaderboard_group.is_self_assignable:
raise ValidationError("illegal_leaderboard_group")

team = Team.objects.create(
name=name,
password=password,
owner=self.context["request"].user,
leaderboard_group=leaderboard_group
)

self.context["request"].user.team = team
self.context["request"].user.save()
team_create.send(sender=self.__class__, team=team)
Expand Down
4 changes: 4 additions & 0 deletions src/team/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@
router = DefaultRouter()
router.register(r"", views.TeamViewSet, basename="team")

group_router = DefaultRouter()
group_router.register(r"", views.LeaderboardGroupViewSet, basename="groups")

urlpatterns = [
path("self/", views.SelfView.as_view(), name="team-self"),
path("create/", views.CreateTeamView.as_view(), name="team-create"),
path("join/", views.JoinTeamView.as_view(), name="team-join"),
path("leave/", views.LeaveTeamView.as_view(), name="team-leave"),
path("groups/", include(group_router.urls), name="leaderboard-groups"),
path("", include(router.urls), name="team"),
]
11 changes: 10 additions & 1 deletion src/team/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from rest_framework.views import APIView

from backend.exceptions import FormattedException
from backend.permissions import AdminOrReadOnlyVisible, ReadOnlyBot
from backend.permissions import AdminOrReadOnlyVisible, ReadOnlyBot, AdminOrReadOnly
from backend.response import FormattedResponse
from backend.signals import team_join, team_join_attempt, team_join_reject
from backend.viewsets import AdminListModelViewSet
Expand All @@ -30,6 +30,7 @@
ListTeamSerializer,
SelfTeamSerializer,
TeamSerializer,
LeaderboardGroupSerializer
)


Expand All @@ -56,6 +57,14 @@ def get_object(self):
)


class LeaderboardGroupViewSet(AdminListModelViewSet):
permission_classes = (AdminOrReadOnly,)
serializer_class = LeaderboardGroupSerializer
admin_serializer_class = LeaderboardGroupSerializer
list_serializer_class = LeaderboardGroupSerializer
list_admin_serializer_class = LeaderboardGroupSerializer


class TeamViewSet(AdminListModelViewSet):
permission_classes = (AdminOrReadOnlyVisible,)
throttle_scope = "team"
Expand Down

0 comments on commit cd12628

Please sign in to comment.