GP Ranking (#90)
This commit is contained in:
parent
9decd11218
commit
b4c1620497
9 changed files with 223 additions and 1 deletions
|
@ -517,6 +517,11 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")),
|
url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")),
|
||||||
|
url(
|
||||||
|
r"^contests/summary/(?P<key>\w+)$",
|
||||||
|
contests.contests_summary_view,
|
||||||
|
name="contests_summary",
|
||||||
|
),
|
||||||
url(r"^course/", paged_list_view(course.CourseList, "course_list")),
|
url(r"^course/", paged_list_view(course.CourseList, "course_list")),
|
||||||
url(
|
url(
|
||||||
r"^contests/(?P<year>\d+)/(?P<month>\d+)/$",
|
r"^contests/(?P<year>\d+)/(?P<month>\d+)/$",
|
||||||
|
|
|
@ -3,7 +3,12 @@ from django.contrib.admin.models import LogEntry
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
from judge.admin.comments import CommentAdmin
|
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 (
|
from judge.admin.interface import (
|
||||||
BlogPostAdmin,
|
BlogPostAdmin,
|
||||||
LicenseAdmin,
|
LicenseAdmin,
|
||||||
|
@ -41,6 +46,7 @@ from judge.models import (
|
||||||
Ticket,
|
Ticket,
|
||||||
VolunteerProblemVote,
|
VolunteerProblemVote,
|
||||||
Course,
|
Course,
|
||||||
|
ContestsSummary,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -69,3 +75,4 @@ admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin)
|
||||||
admin.site.register(Course)
|
admin.site.register(Course)
|
||||||
admin.site.unregister(User)
|
admin.site.unregister(User)
|
||||||
admin.site.register(User, UserAdmin)
|
admin.site.register(User, UserAdmin)
|
||||||
|
admin.site.register(ContestsSummary, ContestsSummaryAdmin)
|
||||||
|
|
|
@ -502,3 +502,19 @@ class ContestParticipationAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
show_virtual.short_description = _("virtual")
|
show_virtual.short_description = _("virtual")
|
||||||
show_virtual.admin_order_field = "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
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from inspect import signature
|
from inspect import signature
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
|
@ -18,10 +19,14 @@ def cache_wrapper(prefix, timeout=None):
|
||||||
return str(arg)[:MAX_NUM_CHAR]
|
return str(arg)[:MAX_NUM_CHAR]
|
||||||
return str(arg)
|
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):
|
def get_key(func, *args, **kwargs):
|
||||||
args_list = list(args)
|
args_list = list(args)
|
||||||
signature_args = list(signature(func).parameters.keys())
|
signature_args = list(signature(func).parameters.keys())
|
||||||
args_list += [kwargs.get(k) for k in signature_args[len(args) :]]
|
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]
|
args_list = [arg_to_str(i) for i in args_list]
|
||||||
key = prefix + ":" + ":".join(args_list)
|
key = prefix + ":" + ":".join(args_list)
|
||||||
key = key.replace(" ", "_")
|
key = key.replace(" ", "_")
|
||||||
|
|
30
judge/migrations/0170_contests_summary.py
Normal file
30
judge/migrations/0170_contests_summary.py
Normal file
|
@ -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")),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -16,6 +16,7 @@ from judge.models.contest import (
|
||||||
ContestTag,
|
ContestTag,
|
||||||
Rating,
|
Rating,
|
||||||
ContestProblemClarification,
|
ContestProblemClarification,
|
||||||
|
ContestsSummary,
|
||||||
)
|
)
|
||||||
from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex
|
from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex
|
||||||
from judge.models.message import PrivateMessage, PrivateMessageThread
|
from judge.models.message import PrivateMessage, PrivateMessageThread
|
||||||
|
|
|
@ -33,6 +33,7 @@ __all__ = [
|
||||||
"ContestSubmission",
|
"ContestSubmission",
|
||||||
"Rating",
|
"Rating",
|
||||||
"ContestProblemClarification",
|
"ContestProblemClarification",
|
||||||
|
"ContestsSummary",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -900,3 +901,24 @@ class ContestProblemClarification(models.Model):
|
||||||
date = models.DateTimeField(
|
date = models.DateTimeField(
|
||||||
verbose_name=_("clarification timestamp"), auto_now_add=True
|
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
|
||||||
|
|
|
@ -27,6 +27,8 @@ from django.db.models import (
|
||||||
Value,
|
Value,
|
||||||
When,
|
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.db.models.expressions import CombinedExpression
|
||||||
from django.http import (
|
from django.http import (
|
||||||
Http404,
|
Http404,
|
||||||
|
@ -67,6 +69,7 @@ from judge.models import (
|
||||||
Profile,
|
Profile,
|
||||||
Submission,
|
Submission,
|
||||||
ContestProblemClarification,
|
ContestProblemClarification,
|
||||||
|
ContestsSummary,
|
||||||
)
|
)
|
||||||
from judge.tasks import run_moss
|
from judge.tasks import run_moss
|
||||||
from judge.utils.celery import redirect_to_task_status
|
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)
|
old_mode = request.session.get("contest_mode", True)
|
||||||
request.session["contest_mode"] = not old_mode
|
request.session["contest_mode"] = not old_mode
|
||||||
return HttpResponse()
|
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)
|
||||||
|
|
71
templates/contest/contests_summary.html
Normal file
71
templates/contest/contests_summary.html
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title_row %}{% endblock %}
|
||||||
|
{% block title_ruler %}{% endblock %}
|
||||||
|
|
||||||
|
{% block media %}
|
||||||
|
<style>
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 12px 15px;
|
||||||
|
border: 1px solid #595656;
|
||||||
|
padding: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<table class="table" id="users-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{_('Rank')}}</th>
|
||||||
|
<th>{{_('Name')}}</th>
|
||||||
|
{% for contest in contests %}
|
||||||
|
<th><a href="{{url('contest_view', contest.key)}}" title="{{contest.name}}">{{ loop.index }}</a></th>
|
||||||
|
{% endfor %}
|
||||||
|
<th>{{_('Points')}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for rank, item in total_rank %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{ rank }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
<span class="username {{ item.css_class }} wrapline" href="{{url('user_page', item.user.username)}}" >{{item.user.username}}</span>
|
||||||
|
</div>
|
||||||
|
<div>{{item.user.first_name}}</div>
|
||||||
|
</td>
|
||||||
|
{% for point_contest, rank_contest in item.point_contests %}
|
||||||
|
<td>
|
||||||
|
<div>{{ point_contest }}</div>
|
||||||
|
{% if rank_contest %}
|
||||||
|
<div><small>(#{{ rank_contest }})</small></div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
|
<td>
|
||||||
|
<b>{{ item.points }}</b>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
Loading…
Reference in a new issue