Add final ranking for superuser (ioi16 only)

This commit is contained in:
cuom1999 2022-12-28 14:38:32 -06:00
parent 6635c3ee99
commit 309b6298e2
16 changed files with 424 additions and 336 deletions

View file

@ -523,6 +523,11 @@ urlpatterns = [
contests.ContestRanking.as_view(), contests.ContestRanking.as_view(),
name="contest_ranking", name="contest_ranking",
), ),
url(
r"^/final_ranking/$",
contests.ContestFinalRanking.as_view(),
name="contest_final_ranking",
),
url( url(
r"^/ranking/ajax$", r"^/ranking/ajax$",
contests.contest_ranking_ajax, contests.contest_ranking_ajax,

View file

@ -112,7 +112,7 @@ class AtCoderContestFormat(DefaultContestFormat):
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()
def display_user_problem(self, participation, contest_problem): def display_user_problem(self, participation, contest_problem, show_final=False):
format_data = (participation.format_data or {}).get(str(contest_problem.id)) format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data: if format_data:
penalty = ( penalty = (

View file

@ -48,7 +48,7 @@ class BaseContestFormat(metaclass=ABCMeta):
raise NotImplementedError() raise NotImplementedError()
@abstractmethod @abstractmethod
def display_user_problem(self, participation, contest_problem): def display_user_problem(self, participation, contest_problem, show_final):
""" """
Returns the HTML fragment to show a user's performance on an individual problem. This is expected to use Returns the HTML fragment to show a user's performance on an individual problem. This is expected to use
information from the format_data field instead of computing it from scratch. information from the format_data field instead of computing it from scratch.
@ -60,7 +60,7 @@ class BaseContestFormat(metaclass=ABCMeta):
raise NotImplementedError() raise NotImplementedError()
@abstractmethod @abstractmethod
def display_participation_result(self, participation): def display_participation_result(self, participation, show_final):
""" """
Returns the HTML fragment to show a user's performance on the whole contest. This is expected to use Returns the HTML fragment to show a user's performance on the whole contest. This is expected to use
information from the format_data field instead of computing it from scratch. information from the format_data field instead of computing it from scratch.

View file

@ -61,7 +61,7 @@ class DefaultContestFormat(BaseContestFormat):
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()
def display_user_problem(self, participation, contest_problem): def display_user_problem(self, participation, contest_problem, show_final=False):
format_data = (participation.format_data or {}).get(str(contest_problem.id)) format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data: if format_data:
return format_html( return format_html(
@ -94,7 +94,7 @@ class DefaultContestFormat(BaseContestFormat):
else: else:
return mark_safe('<td class="problem-score-col"></td>') return mark_safe('<td class="problem-score-col"></td>')
def display_participation_result(self, participation): def display_participation_result(self, participation, show_final=False):
return format_html( return format_html(
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>', '<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
points=floatformat(participation.score, -self.contest.points_precision), points=floatformat(participation.score, -self.contest.points_precision),

View file

@ -122,7 +122,7 @@ class ECOOContestFormat(DefaultContestFormat):
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()
def display_user_problem(self, participation, contest_problem): def display_user_problem(self, participation, contest_problem, show_final=False):
format_data = (participation.format_data or {}).get(str(contest_problem.id)) format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data: if format_data:
bonus = ( bonus = (
@ -162,7 +162,7 @@ class ECOOContestFormat(DefaultContestFormat):
else: else:
return mark_safe("<td></td>") return mark_safe("<td></td>")
def display_participation_result(self, participation): def display_participation_result(self, participation, show_final=False):
return format_html( return format_html(
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>', '<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
points=floatformat(participation.score), points=floatformat(participation.score),

View file

@ -114,7 +114,7 @@ class ICPCContestFormat(DefaultContestFormat):
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()
def display_user_problem(self, participation, contest_problem): def display_user_problem(self, participation, contest_problem, show_final=False):
format_data = (participation.format_data or {}).get(str(contest_problem.id)) format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data: if format_data:
penalty = ( penalty = (

View file

@ -86,7 +86,12 @@ class IOIContestFormat(DefaultContestFormat):
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()
def display_user_problem(self, participation, contest_problem): def display_user_problem(self, participation, contest_problem, show_final=False):
if show_final:
format_data = (participation.format_data_final or {}).get(
str(contest_problem.id)
)
else:
format_data = (participation.format_data or {}).get(str(contest_problem.id)) format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data: if format_data:
return format_html( return format_html(
@ -121,11 +126,17 @@ class IOIContestFormat(DefaultContestFormat):
else: else:
return mark_safe('<td class="problem-score-col"></td>') return mark_safe('<td class="problem-score-col"></td>')
def display_participation_result(self, participation): def display_participation_result(self, participation, show_final=False):
if show_final:
score = participation.score_final
cumtime = participation.cumtime_final
else:
score = participation.score
cumtime = participation.cumtime
return format_html( return format_html(
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>', '<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
points=floatformat(participation.score, -self.contest.points_precision), points=floatformat(score, -self.contest.points_precision),
cumtime=nice_repr(timedelta(seconds=participation.cumtime), "noday") cumtime=nice_repr(timedelta(seconds=cumtime), "noday")
if self.config["cumtime"] if self.config["cumtime"]
else "", else "",
) )

View file

@ -100,11 +100,10 @@ class NewIOIContestFormat(IOIContestFormat):
return cursor.fetchall() return cursor.fetchall()
def update_participation(self, participation): def update_participation(self, participation):
cumtime = 0
score = 0
format_data = {}
frozen_subtasks = self.get_frozen_subtasks() frozen_subtasks = self.get_frozen_subtasks()
def calculate_format_data(participation, include_frozen):
format_data = {}
for ( for (
problem_id, problem_id,
problem_points, problem_points,
@ -113,7 +112,7 @@ class NewIOIContestFormat(IOIContestFormat):
total_subtask_points, total_subtask_points,
subtask, subtask,
sub_id, sub_id,
) in self.get_results_by_subtask(participation): ) in self.get_results_by_subtask(participation, include_frozen):
problem_id = str(problem_id) problem_id = str(problem_id)
time = from_database_time(time) time = from_database_time(time)
if self.config["cumtime"]: if self.config["cumtime"]:
@ -122,13 +121,27 @@ class NewIOIContestFormat(IOIContestFormat):
dt = 0 dt = 0
if format_data.get(problem_id) is None: if format_data.get(problem_id) is None:
format_data[problem_id] = {"points": 0, "time": 0, "total_points": 0} format_data[problem_id] = {
if subtask not in frozen_subtasks.get(problem_id, set()): "points": 0,
"time": 0,
"total_points": 0,
}
if (
subtask not in frozen_subtasks.get(problem_id, set())
or include_frozen
):
format_data[problem_id]["points"] += subtask_points format_data[problem_id]["points"] += subtask_points
format_data[problem_id]["total_points"] += total_subtask_points format_data[problem_id]["total_points"] += total_subtask_points
format_data[problem_id]["time"] = max(dt, format_data[problem_id]["time"]) format_data[problem_id]["time"] = max(
dt, format_data[problem_id]["time"]
)
format_data[problem_id]["problem_points"] = problem_points format_data[problem_id]["problem_points"] = problem_points
return format_data
def recalculate_results(format_data):
cumtime = 0
score = 0
for problem_data in format_data.values(): for problem_data in format_data.values():
if not problem_data["total_points"]: if not problem_data["total_points"]:
continue continue
@ -141,10 +154,19 @@ class NewIOIContestFormat(IOIContestFormat):
if self.config["cumtime"] and problem_data["points"]: if self.config["cumtime"] and problem_data["points"]:
cumtime += penalty cumtime += penalty
score += problem_data["points"] score += problem_data["points"]
return score, cumtime
format_data = calculate_format_data(participation, False)
score, cumtime = recalculate_results(format_data)
self.handle_frozen_state(participation, format_data) self.handle_frozen_state(participation, format_data)
participation.cumtime = max(cumtime, 0) participation.cumtime = max(cumtime, 0)
participation.score = round(score, self.contest.points_precision) participation.score = round(score, self.contest.points_precision)
participation.tiebreaker = 0 participation.tiebreaker = 0
participation.format_data = format_data participation.format_data = format_data
format_data_final = calculate_format_data(participation, True)
score_final, cumtime_final = recalculate_results(format_data_final)
participation.cumtime_final = max(cumtime_final, 0)
participation.score_final = round(score_final, self.contest.points_precision)
participation.format_data_final = format_data_final
participation.save() participation.save()

View file

@ -0,0 +1,23 @@
# Generated by Django 3.2.16 on 2022-12-28 18:36
from django.db import migrations
import jsonfield.fields
class Migration(migrations.Migration):
dependencies = [
("judge", "0141_contestproblem_frozen_subtasks"),
]
operations = [
migrations.AddField(
model_name="contestparticipation",
name="format_data_final",
field=jsonfield.fields.JSONField(
blank=True,
null=True,
verbose_name="same as format_data, but including frozen results",
),
),
]

View file

@ -0,0 +1,25 @@
# Generated by Django 3.2.16 on 2022-12-28 18:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0142_contestparticipation_format_data_final"),
]
operations = [
migrations.AddField(
model_name="contestparticipation",
name="cumtime_final",
field=models.PositiveIntegerField(
default=0, verbose_name="final cumulative time"
),
),
migrations.AddField(
model_name="contestparticipation",
name="score_final",
field=models.FloatField(default=0, verbose_name="final score"),
),
]

View file

@ -650,6 +650,15 @@ class ContestParticipation(models.Model):
format_data = JSONField( format_data = JSONField(
verbose_name=_("contest format specific data"), null=True, blank=True verbose_name=_("contest format specific data"), null=True, blank=True
) )
format_data_final = JSONField(
verbose_name=_("same as format_data, but including frozen results"),
null=True,
blank=True,
)
score_final = models.FloatField(verbose_name=_("final score"), default=0)
cumtime_final = models.PositiveIntegerField(
verbose_name=_("final cumulative time"), default=0
)
def recompute_results(self): def recompute_results(self):
with transaction.atomic(): with transaction.atomic():

View file

@ -877,47 +877,72 @@ ContestRankingProfile = namedtuple(
BestSolutionData = namedtuple("BestSolutionData", "code points time state is_pretested") BestSolutionData = namedtuple("BestSolutionData", "code points time state is_pretested")
def make_contest_ranking_profile(contest, participation, contest_problems): def make_contest_ranking_profile(
contest, participation, contest_problems, show_final=False
):
if not show_final:
points = participation.score
cumtime = participation.cumtime
else:
points = participation.score_final
cumtime = participation.cumtime_final
user = participation.user user = participation.user
return ContestRankingProfile( return ContestRankingProfile(
id=user.id, id=user.id,
user=user.user, user=user.user,
css_class=user.css_class, css_class=user.css_class,
username=user.username, username=user.username,
points=participation.score, points=points,
cumtime=participation.cumtime, cumtime=cumtime,
tiebreaker=participation.tiebreaker, tiebreaker=participation.tiebreaker,
organization=user.organization, organization=user.organization,
participation_rating=participation.rating.rating participation_rating=participation.rating.rating
if hasattr(participation, "rating") if hasattr(participation, "rating")
else None, else None,
problem_cells=[ problem_cells=[
contest.format.display_user_problem(participation, contest_problem) contest.format.display_user_problem(
participation, contest_problem, show_final
)
for contest_problem in contest_problems for contest_problem in contest_problems
], ],
result_cell=contest.format.display_participation_result(participation), result_cell=contest.format.display_participation_result(
participation, show_final
),
participation=participation, participation=participation,
) )
def base_contest_ranking_list(contest, problems, queryset): def base_contest_ranking_list(contest, problems, queryset, show_final=False):
return [ return [
make_contest_ranking_profile(contest, participation, problems) make_contest_ranking_profile(contest, participation, problems, show_final)
for participation in queryset.select_related("user__user", "rating").defer( for participation in queryset.select_related("user__user", "rating").defer(
"user__about", "user__organizations__about" "user__about", "user__organizations__about"
) )
] ]
def contest_ranking_list(contest, problems, queryset=None): def contest_ranking_list(contest, problems, queryset=None, show_final=False):
if not queryset: if not queryset:
queryset = contest.users.filter(virtual=0) queryset = contest.users.filter(virtual=0)
if not show_final:
return base_contest_ranking_list( return base_contest_ranking_list(
contest, contest,
problems, problems,
queryset.prefetch_related("user__organizations") queryset.prefetch_related("user__organizations")
.extra(select={"round_score": "round(score, 6)"}) .extra(select={"round_score": "round(score, 6)"})
.order_by("is_disqualified", "-round_score", "cumtime", "tiebreaker"), .order_by("is_disqualified", "-round_score", "cumtime", "tiebreaker"),
show_final,
)
else:
return base_contest_ranking_list(
contest,
problems,
queryset.prefetch_related("user__organizations")
.extra(select={"round_score": "round(score_final, 6)"})
.order_by("is_disqualified", "-round_score", "cumtime_final", "tiebreaker"),
show_final,
) )
@ -928,6 +953,7 @@ def get_contest_ranking_list(
ranking_list=contest_ranking_list, ranking_list=contest_ranking_list,
show_current_virtual=False, show_current_virtual=False,
ranker=ranker, ranker=ranker,
show_final=False,
): ):
problems = list( problems = list(
contest.contest_problems.select_related("problem") contest.contest_problems.select_related("problem")
@ -936,7 +962,7 @@ def get_contest_ranking_list(
) )
users = ranker( users = ranker(
ranking_list(contest, problems), ranking_list(contest, problems, show_final=show_final),
key=attrgetter("points", "cumtime", "tiebreaker"), key=attrgetter("points", "cumtime", "tiebreaker"),
) )
@ -955,12 +981,17 @@ def get_contest_ranking_list(
def contest_ranking_ajax(request, contest, participation=None): def contest_ranking_ajax(request, contest, participation=None):
contest, exists = _find_contest(request, contest) contest, exists = _find_contest(request, contest)
show_final = bool(request.GET.get("final", False))
if not exists: if not exists:
return HttpResponseBadRequest("Invalid contest", content_type="text/plain") return HttpResponseBadRequest("Invalid contest", content_type="text/plain")
if not contest.can_see_full_scoreboard(request.user): if not contest.can_see_full_scoreboard(request.user):
raise Http404() raise Http404()
if show_final:
if not request.user.is_superuser or contest.format_name != "ioi16":
raise Http404()
queryset = contest.users.filter(virtual__gte=0) queryset = contest.users.filter(virtual__gte=0)
if request.GET.get("friend") == "true" and request.profile: if request.GET.get("friend") == "true" and request.profile:
friends = list(request.profile.get_friends()) friends = list(request.profile.get_friends())
@ -973,6 +1004,7 @@ def contest_ranking_ajax(request, contest, participation=None):
contest, contest,
participation, participation,
ranking_list=partial(contest_ranking_list, queryset=queryset), ranking_list=partial(contest_ranking_list, queryset=queryset),
show_final=show_final,
) )
return render( return render(
request, request,
@ -1039,6 +1071,18 @@ class ContestRanking(ContestRankingBase):
return context return context
class ContestFinalRanking(LoginRequiredMixin, ContestRanking):
page_type = "final_ranking"
def get_ranking_list(self):
if not self.request.user.is_superuser:
raise Http404()
if self.object.format_name != "ioi16":
raise Http404()
return get_contest_ranking_list(self.request, self.object, show_final=True)
class ContestParticipationList(LoginRequiredMixin, ContestRankingBase): class ContestParticipationList(LoginRequiredMixin, ContestRankingBase):
page_type = "participation" page_type = "participation"

File diff suppressed because it is too large Load diff

View file

@ -24,6 +24,7 @@
{% endif %} {% endif %}
{% if request.user.is_superuser and can_use_resolver %} {% if request.user.is_superuser and can_use_resolver %}
{{ make_tab_item('resolver', 'fa fa-check', url('resolver', contest.key), _('Resolver')) }} {{ make_tab_item('resolver', 'fa fa-check', url('resolver', contest.key), _('Resolver')) }}
{{ make_tab_item('final_ranking', 'fa fa-bar-chart', url('contest_final_ranking', contest.key), _('Final rankings')) }}
{% endif %} {% endif %}
{% if can_edit %} {% if can_edit %}
{% if perms.judge.moss_contest and has_moss_api_key %} {% if perms.judge.moss_contest and has_moss_api_key %}

View file

@ -118,8 +118,12 @@
var friend = $('#show-friends-checkbox').is(':checked'); var friend = $('#show-friends-checkbox').is(':checked');
var virtual = $('#show-virtual-checkbox').is(':checked'); var virtual = $('#show-virtual-checkbox').is(':checked');
$('#loading-gif').show(); $('#loading-gif').show();
var url = `{{url('contest_ranking_ajax', contest.key)}}?friend=${friend}&virtual=${virtual}`;
{% if page_type == 'final_ranking' %}
url += "&final=true";
{% endif %}
$.get({ $.get({
url: `{{url('contest_ranking_ajax', contest.key)}}?friend=${friend}&virtual=${virtual}`, url: url,
success: function(HTML) { success: function(HTML) {
$('#users-table').html(HTML); $('#users-table').html(HTML);
highlightFirstSolve(); highlightFirstSolve();
@ -199,7 +203,8 @@
$('#show-virtual-label').hide(); $('#show-virtual-label').hide();
{% else %} {% else %}
{% if request.in_contest %} {% if request.in_contest %}
setInterval(update_ranking, 60 * 1000); clearInterval(window.rankingInterval);
window.rankingInterval = setInterval(update_ranking, 60 * 1000);
{% endif %} {% endif %}
{% endif %} {% endif %}
}); });

View file

@ -57,7 +57,7 @@
}); });
</script> </script>
{% endif %} {% endif %}
{% if page_type == 'ranking' %} {% if page_type == 'ranking' or page_type == 'final_ranking' %}
<script type="text/javascript"> <script type="text/javascript">
$.fn.ignore = function(sel) { $.fn.ignore = function(sel) {
return this.clone().find(sel || '>*').remove().end(); return this.clone().find(sel || '>*').remove().end();