-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Replaced UserCache with ContestScore and UserScore
- Loading branch information
1 parent
099c70e
commit 1fa8cf3
Showing
5 changed files
with
155 additions
and
121 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,70 +1,138 @@ | ||
from typing import Optional | ||
|
||
from django.db import models | ||
from django.db.models import Count, Sum | ||
from django.db import models, transaction | ||
from django.db.models.functions import Coalesce | ||
from django.db.models import Count, F, Sum | ||
from .profile import User | ||
|
||
|
||
class UserCache(models.Model): | ||
user = models.ForeignKey( | ||
"User", | ||
on_delete=models.CASCADE, | ||
related_name="caches", | ||
) | ||
|
||
participation = models.ForeignKey( | ||
"ContestParticipation", | ||
on_delete=models.SET_NULL, | ||
related_name="current_caches", | ||
null=True, | ||
blank=True, | ||
) | ||
from typing import TYPE_CHECKING | ||
|
||
points = models.IntegerField("Points") | ||
|
||
flags = models.IntegerField("Flags") | ||
if TYPE_CHECKING: | ||
from .contest import ContestParticipation, Contest | ||
|
||
|
||
class UserScore(models.Model): | ||
user = models.ForeignKey(User, on_delete=models.CASCADE, db_index=True, unique=True) | ||
points = models.PositiveIntegerField(help_text="The amount of points.", default=0) | ||
flag_count = models.PositiveIntegerField(help_text="The amount of flags the user/team has.", default=0) | ||
|
||
def __str__(self) -> str: | ||
return f"{self.user_id}'s score" | ||
|
||
@classmethod | ||
def update_or_create(cls, change_in_score: int, user: User, update_flags: bool = True): | ||
assert change_in_score > 0 | ||
queryset = cls.objects.filter(user=user) | ||
|
||
if not queryset.exists(): # no user found matching that | ||
cls.objects.create(user=user, flag_count=int(update_flags), points=change_in_score) | ||
return cls.update_or_create(change_in_score=change_in_score, user=user, update_flags=update_flags) | ||
|
||
if update_flags: | ||
queryset.update(points=F('points') + change_in_score) | ||
else: | ||
queryset.update(points=F('points') + change_in_score, flag_count=F('flag_count') + 1) | ||
|
||
@classmethod | ||
def invalidate(cls, user: User): | ||
try: | ||
cls.objects.get(user=user).delete() | ||
except cls.DoesNotExist: | ||
pass # user was not found. | ||
|
||
|
||
@classmethod | ||
def get(cls, user: User) -> None | "UserScore": | ||
obj = cls.objects.filter(user=user) | ||
if obj is None: | ||
return None | ||
return obj.first() | ||
|
||
@classmethod | ||
def initalize_data(cls): | ||
cls.objects.all().delete() # clear inital objs | ||
users = User.objects.all() | ||
scores_to_create = [] | ||
for user in users: | ||
queryset = user._get_unique_correct_submissions() | ||
queryset = queryset.aggregate( | ||
points=Coalesce(Sum("problem__points"), 0), | ||
flags=Count("problem"), | ||
) | ||
scores_to_create.append( | ||
UserScore( | ||
user=user, | ||
flag_count=queryset['flags'], | ||
points=queryset['points'] | ||
) | ||
) | ||
# Use bulk_create to create all objects in a single query | ||
cls.objects.bulk_create(scores_to_create) | ||
|
||
|
||
|
||
class Meta: | ||
unique_together = ('user', 'participation') | ||
|
||
|
||
class ContestScore(models.Model): | ||
participation=models.ForeignKey("ContestParticipation", on_delete=models.CASCADE, db_index=True, unique=True) | ||
points = models.PositiveIntegerField(help_text="The amount of points.", default=0) | ||
flag_count = models.PositiveIntegerField(help_text="The amount of flags the user/team has.", default=0) | ||
|
||
def __str__(self) -> str: | ||
return f'{self.user} {"global" if self.participation is None else self.participation}' | ||
return f'Score for {self.participation} on {self.participation.contest.name}' | ||
|
||
@classmethod | ||
def get(cls, user: "User", participation: Optional["ContestParticipation"]) -> "UserCache": | ||
assert user is not None | ||
q = cls.objects.filter(user=user, participation=participation) | ||
if not q: | ||
obj = cls.fill_cache(user, participation) | ||
obj.save() | ||
return obj | ||
else: | ||
return q.first() | ||
# TODO: cleanup duplicates | ||
|
||
@classmethod | ||
def invalidate(cls, user: "User", participation: Optional["ContestParticipation"]) -> None: | ||
assert user is not None | ||
q = cls.objects.filter(user=user, participation=participation) | ||
obj = cls.fill_cache(user, participation) | ||
if not q: | ||
obj.save() | ||
else: | ||
q.update(points=obj.points, flags=obj.flags) | ||
def update_or_create(cls, change_in_score: int, participant: "ContestParticipation", update_flags: bool = True): | ||
assert change_in_score > 0 | ||
queryset = cls.objects.filter(participation=participant) | ||
|
||
if not queryset.exists(): # no user/team found matching that | ||
cls.objects.create(participation=participant, flag_count=int(update_flags), points=change_in_score) | ||
return cls.update_or_create(change_in_score=change_in_score, participant=participant, update_flags=update_flags) | ||
|
||
with transaction.atomic(): | ||
queryset.select_for_update() # prevent race conditions with other team members | ||
|
||
if update_flags: | ||
queryset.update(points=F('points') + change_in_score) | ||
else: | ||
queryset.update(points=F('points') + change_in_score, flag_count=F('flag_count') + 1) | ||
|
||
@classmethod | ||
def fill_cache(cls, user: "User", participation: Optional["ContestParticipation"]) -> "UserCache": | ||
assert user is not None | ||
if participation is None: | ||
queryset = user._get_unique_correct_submissions().filter(problem__is_public=True) | ||
else: | ||
queryset = participation._get_unique_correct_submissions() | ||
queryset = queryset.aggregate( | ||
points=Coalesce(Sum("problem__points"), 0), | ||
flags=Count("problem"), | ||
) | ||
return UserCache( | ||
user=user, | ||
participation=participation, | ||
**{key: queryset[key] for key in ("points", "flags")} | ||
) | ||
def invalidate(cls, participant: ContestParticipation): | ||
try: | ||
cls.objects.get(participant=participant).delete() | ||
except cls.DoesNotExist: | ||
pass # participant was not found. | ||
|
||
|
||
@classmethod | ||
def get(cls, participant: ContestParticipation) -> None | "ContestScore": | ||
obj = cls.objects.filter(participant=participant) | ||
if obj is None: | ||
return None | ||
return obj.first() | ||
|
||
@classmethod | ||
def initalize_data(cls, contest: "Contest"): | ||
cls.objects.all().delete() # clear inital objs | ||
participants = contest.participations.all() | ||
scores_to_create = [] | ||
for participant in participants: | ||
queryset = participant._get_unique_correct_submissions() | ||
queryset = queryset.aggregate( | ||
points=Coalesce(Sum("problem__points"), 0), | ||
flags=Count("problem"), | ||
) | ||
scores_to_create.append( | ||
ContestScore( | ||
participation=participant, | ||
flag_count=queryset['flags'], | ||
points=queryset['points'] | ||
) | ||
) | ||
# Use bulk_create to create all objects in a single query | ||
cls.objects.bulk_create(scores_to_create) | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.