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/summary/(?P<key>\w+)$",
|
||||
contests.contests_summary_view,
|
||||
name="contests_summary",
|
||||
),
|
||||
url(r"^course/", paged_list_view(course.CourseList, "course_list")),
|
||||
url(
|
||||
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 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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(" ", "_")
|
||||
|
|
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,
|
||||
Rating,
|
||||
ContestProblemClarification,
|
||||
ContestsSummary,
|
||||
)
|
||||
from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex
|
||||
from judge.models.message import PrivateMessage, PrivateMessageThread
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
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