diff --git a/dmoj/urls.py b/dmoj/urls.py index 4e0691e..e6610f8 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -517,6 +517,11 @@ urlpatterns = [ ), ), url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")), + url( + r"^contests/summary/(?P\w+)$", + contests.contests_summary_view, + name="contests_summary", + ), url(r"^course/", paged_list_view(course.CourseList, "course_list")), url( r"^contests/(?P\d+)/(?P\d+)/$", diff --git a/judge/admin/__init__.py b/judge/admin/__init__.py index 94af5aa..05032d6 100644 --- a/judge/admin/__init__.py +++ b/judge/admin/__init__.py @@ -3,7 +3,12 @@ from django.contrib.admin.models import LogEntry from django.contrib.auth.models import User from judge.admin.comments import CommentAdmin -from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin +from judge.admin.contest import ( + ContestAdmin, + ContestParticipationAdmin, + ContestTagAdmin, + ContestsSummaryAdmin, +) from judge.admin.interface import ( BlogPostAdmin, LicenseAdmin, @@ -41,6 +46,7 @@ from judge.models import ( Ticket, VolunteerProblemVote, Course, + ContestsSummary, ) @@ -69,3 +75,4 @@ admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin) admin.site.register(Course) admin.site.unregister(User) admin.site.register(User, UserAdmin) +admin.site.register(ContestsSummary, ContestsSummaryAdmin) diff --git a/judge/admin/contest.py b/judge/admin/contest.py index a126e83..b7b5457 100644 --- a/judge/admin/contest.py +++ b/judge/admin/contest.py @@ -502,3 +502,19 @@ class ContestParticipationAdmin(admin.ModelAdmin): show_virtual.short_description = _("virtual") show_virtual.admin_order_field = "virtual" + + +class ContestsSummaryForm(ModelForm): + class Meta: + widgets = { + "contests": AdminHeavySelect2MultipleWidget( + data_view="contest_select2", attrs={"style": "width: 100%"} + ), + } + + +class ContestsSummaryAdmin(admin.ModelAdmin): + fields = ("key", "contests", "scores") + list_display = ("key",) + search_fields = ("key", "contests__key") + form = ContestsSummaryForm diff --git a/judge/caching.py b/judge/caching.py index 42e9311..70b5613 100644 --- a/judge/caching.py +++ b/judge/caching.py @@ -1,6 +1,7 @@ from inspect import signature from django.core.cache import cache from django.db.models.query import QuerySet +from django.core.handlers.wsgi import WSGIRequest import hashlib @@ -18,10 +19,14 @@ def cache_wrapper(prefix, timeout=None): return str(arg)[:MAX_NUM_CHAR] return str(arg) + def filter_args(args_list): + return [x for x in args_list if not isinstance(x, WSGIRequest)] + def get_key(func, *args, **kwargs): args_list = list(args) signature_args = list(signature(func).parameters.keys()) args_list += [kwargs.get(k) for k in signature_args[len(args) :]] + args_list = filter_args(args_list) args_list = [arg_to_str(i) for i in args_list] key = prefix + ":" + ":".join(args_list) key = key.replace(" ", "_") diff --git a/judge/migrations/0170_contests_summary.py b/judge/migrations/0170_contests_summary.py new file mode 100644 index 0000000..a2b19de --- /dev/null +++ b/judge/migrations/0170_contests_summary.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.21 on 2023-10-02 03:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("judge", "0169_public_scoreboard"), + ] + + operations = [ + migrations.CreateModel( + name="ContestsSummary", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("scores", models.JSONField(blank=True, null=True)), + ("key", models.CharField(max_length=20, unique=True)), + ("contests", models.ManyToManyField(to="judge.Contest")), + ], + ), + ] diff --git a/judge/models/__init__.py b/judge/models/__init__.py index ee9d364..8226c96 100644 --- a/judge/models/__init__.py +++ b/judge/models/__init__.py @@ -16,6 +16,7 @@ from judge.models.contest import ( ContestTag, Rating, ContestProblemClarification, + ContestsSummary, ) from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex from judge.models.message import PrivateMessage, PrivateMessageThread diff --git a/judge/models/contest.py b/judge/models/contest.py index a724f1a..5052399 100644 --- a/judge/models/contest.py +++ b/judge/models/contest.py @@ -33,6 +33,7 @@ __all__ = [ "ContestSubmission", "Rating", "ContestProblemClarification", + "ContestsSummary", ] @@ -900,3 +901,24 @@ class ContestProblemClarification(models.Model): date = models.DateTimeField( verbose_name=_("clarification timestamp"), auto_now_add=True ) + + +class ContestsSummary(models.Model): + contests = models.ManyToManyField( + Contest, + ) + scores = models.JSONField( + null=True, + blank=True, + ) + key = models.CharField( + max_length=20, + unique=True, + ) + + class Meta: + verbose_name = _("contests summary") + verbose_name_plural = _("contests summaries") + + def __str__(self): + return self.key diff --git a/judge/views/contests.py b/judge/views/contests.py index 97ff251..b6f7242 100644 --- a/judge/views/contests.py +++ b/judge/views/contests.py @@ -27,6 +27,8 @@ from django.db.models import ( Value, When, ) +from django.db.models.signals import post_save, post_delete +from django.dispatch import receiver from django.db.models.expressions import CombinedExpression from django.http import ( Http404, @@ -67,6 +69,7 @@ from judge.models import ( Profile, Submission, ContestProblemClarification, + ContestsSummary, ) from judge.tasks import run_moss from judge.utils.celery import redirect_to_task_status @@ -1380,3 +1383,65 @@ def update_contest_mode(request): old_mode = request.session.get("contest_mode", True) request.session["contest_mode"] = not old_mode return HttpResponse() + + +ContestsSummaryData = namedtuple( + "ContestsSummaryData", + "user points point_contests css_class", +) + + +def contests_summary_view(request, key): + try: + contests_summary = ContestsSummary.objects.get(key=key) + except: + raise Http404() + + cache_key = "csv:" + key + context = cache.get(cache_key) + if context: + return render(request, "contest/contests_summary.html", context) + + scores_system = contests_summary.scores + contests = contests_summary.contests.all() + total_points = defaultdict(int) + result_per_contest = defaultdict(lambda: [(0, 0)] * len(contests)) + user_css_class = {} + + for i in range(len(contests)): + contest = contests[i] + users, problems = get_contest_ranking_list(request, contest) + for rank, user in users: + curr_score = 0 + if rank - 1 < len(scores_system): + curr_score = scores_system[rank - 1] + total_points[user.user] += curr_score + result_per_contest[user.user][i] = (curr_score, rank) + user_css_class[user.user] = user.css_class + + sorted_total_points = [ + ContestsSummaryData( + user=user, + points=total_points[user], + point_contests=result_per_contest[user], + css_class=user_css_class[user], + ) + for user in total_points + ] + + sorted_total_points.sort(key=lambda x: x.points, reverse=True) + total_rank = ranker(sorted_total_points) + + context = { + "total_rank": list(total_rank), + "title": _("Contests Summary"), + "contests": contests, + } + cache.set(cache_key, context) + + return render(request, "contest/contests_summary.html", context) + + +@receiver([post_save, post_delete], sender=ContestsSummary) +def clear_cache(sender, instance, **kwargs): + cache.delete("csv:" + instance.key) diff --git a/templates/contest/contests_summary.html b/templates/contest/contests_summary.html new file mode 100644 index 0000000..a7fbe12 --- /dev/null +++ b/templates/contest/contests_summary.html @@ -0,0 +1,71 @@ +{% extends "base.html" %} + +{% block title_row %}{% endblock %} +{% block title_ruler %}{% endblock %} + +{% block media %} + +{% endblock %} + +{% block body %} + + + + + + {% for contest in contests %} + + {% endfor %} + + + + + {% for rank, item in total_rank %} + + + + {% for point_contest, rank_contest in item.point_contests %} + + {% endfor %} + + + {% endfor %} + +
{{_('Rank')}}{{_('Name')}}{{ loop.index }}{{_('Points')}}
+ {{ rank }} + +
+ {{item.user.username}} +
+
{{item.user.first_name}}
+
+
{{ point_contest }}
+ {% if rank_contest %} +
(#{{ rank_contest }})
+ {% endif %} +
+ {{ item.points }} +
+{% endblock %}