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(),
name="contest_ranking",
),
url(
r"^/final_ranking/$",
contests.ContestFinalRanking.as_view(),
name="contest_final_ranking",
),
url(
r"^/ranking/ajax$",
contests.contest_ranking_ajax,

View file

@ -112,7 +112,7 @@ class AtCoderContestFormat(DefaultContestFormat):
participation.format_data = format_data
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))
if format_data:
penalty = (

View file

@ -48,7 +48,7 @@ class BaseContestFormat(metaclass=ABCMeta):
raise NotImplementedError()
@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
information from the format_data field instead of computing it from scratch.
@ -60,7 +60,7 @@ class BaseContestFormat(metaclass=ABCMeta):
raise NotImplementedError()
@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
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.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))
if format_data:
return format_html(
@ -94,7 +94,7 @@ class DefaultContestFormat(BaseContestFormat):
else:
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(
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
points=floatformat(participation.score, -self.contest.points_precision),

View file

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

View file

@ -114,7 +114,7 @@ class ICPCContestFormat(DefaultContestFormat):
participation.format_data = format_data
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))
if format_data:
penalty = (

View file

@ -86,8 +86,13 @@ class IOIContestFormat(DefaultContestFormat):
participation.format_data = format_data
participation.save()
def display_user_problem(self, participation, contest_problem):
format_data = (participation.format_data or {}).get(str(contest_problem.id))
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))
if format_data:
return format_html(
'<td class="{state} problem-score-col"><a data-featherlight="{url}" href="#">{points}<div class="solving-time">{time}</div></a></td>',
@ -121,11 +126,17 @@ class IOIContestFormat(DefaultContestFormat):
else:
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(
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
points=floatformat(participation.score, -self.contest.points_precision),
cumtime=nice_repr(timedelta(seconds=participation.cumtime), "noday")
points=floatformat(score, -self.contest.points_precision),
cumtime=nice_repr(timedelta(seconds=cumtime), "noday")
if self.config["cumtime"]
else "",
)

View file

@ -100,51 +100,73 @@ class NewIOIContestFormat(IOIContestFormat):
return cursor.fetchall()
def update_participation(self, participation):
cumtime = 0
score = 0
format_data = {}
frozen_subtasks = self.get_frozen_subtasks()
for (
problem_id,
problem_points,
time,
subtask_points,
total_subtask_points,
subtask,
sub_id,
) in self.get_results_by_subtask(participation):
problem_id = str(problem_id)
time = from_database_time(time)
if self.config["cumtime"]:
dt = (time - participation.start).total_seconds()
else:
dt = 0
def calculate_format_data(participation, include_frozen):
format_data = {}
for (
problem_id,
problem_points,
time,
subtask_points,
total_subtask_points,
subtask,
sub_id,
) in self.get_results_by_subtask(participation, include_frozen):
problem_id = str(problem_id)
time = from_database_time(time)
if self.config["cumtime"]:
dt = (time - participation.start).total_seconds()
else:
dt = 0
if format_data.get(problem_id) is None:
format_data[problem_id] = {"points": 0, "time": 0, "total_points": 0}
if subtask not in frozen_subtasks.get(problem_id, set()):
format_data[problem_id]["points"] += 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]["problem_points"] = problem_points
if format_data.get(problem_id) is None:
format_data[problem_id] = {
"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]["total_points"] += total_subtask_points
format_data[problem_id]["time"] = max(
dt, format_data[problem_id]["time"]
)
format_data[problem_id]["problem_points"] = problem_points
for problem_data in format_data.values():
if not problem_data["total_points"]:
continue
penalty = problem_data["time"]
problem_data["points"] = (
problem_data["points"]
/ problem_data["total_points"]
* problem_data["problem_points"]
)
if self.config["cumtime"] and problem_data["points"]:
cumtime += penalty
score += problem_data["points"]
return format_data
def recalculate_results(format_data):
cumtime = 0
score = 0
for problem_data in format_data.values():
if not problem_data["total_points"]:
continue
penalty = problem_data["time"]
problem_data["points"] = (
problem_data["points"]
/ problem_data["total_points"]
* problem_data["problem_points"]
)
if self.config["cumtime"] and problem_data["points"]:
cumtime += penalty
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)
participation.cumtime = max(cumtime, 0)
participation.score = round(score, self.contest.points_precision)
participation.tiebreaker = 0
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()

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(
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):
with transaction.atomic():

View file

@ -877,48 +877,73 @@ ContestRankingProfile = namedtuple(
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
return ContestRankingProfile(
id=user.id,
user=user.user,
css_class=user.css_class,
username=user.username,
points=participation.score,
cumtime=participation.cumtime,
points=points,
cumtime=cumtime,
tiebreaker=participation.tiebreaker,
organization=user.organization,
participation_rating=participation.rating.rating
if hasattr(participation, "rating")
else None,
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
],
result_cell=contest.format.display_participation_result(participation),
result_cell=contest.format.display_participation_result(
participation, show_final
),
participation=participation,
)
def base_contest_ranking_list(contest, problems, queryset):
def base_contest_ranking_list(contest, problems, queryset, show_final=False):
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(
"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:
queryset = contest.users.filter(virtual=0)
return base_contest_ranking_list(
contest,
problems,
queryset.prefetch_related("user__organizations")
.extra(select={"round_score": "round(score, 6)"})
.order_by("is_disqualified", "-round_score", "cumtime", "tiebreaker"),
)
if not show_final:
return base_contest_ranking_list(
contest,
problems,
queryset.prefetch_related("user__organizations")
.extra(select={"round_score": "round(score, 6)"})
.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,
)
def get_contest_ranking_list(
@ -928,6 +953,7 @@ def get_contest_ranking_list(
ranking_list=contest_ranking_list,
show_current_virtual=False,
ranker=ranker,
show_final=False,
):
problems = list(
contest.contest_problems.select_related("problem")
@ -936,7 +962,7 @@ def get_contest_ranking_list(
)
users = ranker(
ranking_list(contest, problems),
ranking_list(contest, problems, show_final=show_final),
key=attrgetter("points", "cumtime", "tiebreaker"),
)
@ -955,12 +981,17 @@ def get_contest_ranking_list(
def contest_ranking_ajax(request, contest, participation=None):
contest, exists = _find_contest(request, contest)
show_final = bool(request.GET.get("final", False))
if not exists:
return HttpResponseBadRequest("Invalid contest", content_type="text/plain")
if not contest.can_see_full_scoreboard(request.user):
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)
if request.GET.get("friend") == "true" and request.profile:
friends = list(request.profile.get_friends())
@ -973,6 +1004,7 @@ def contest_ranking_ajax(request, contest, participation=None):
contest,
participation,
ranking_list=partial(contest_ranking_list, queryset=queryset),
show_final=show_final,
)
return render(
request,
@ -1039,6 +1071,18 @@ class ContestRanking(ContestRankingBase):
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):
page_type = "participation"

File diff suppressed because it is too large Load diff

View file

@ -24,6 +24,7 @@
{% endif %}
{% if request.user.is_superuser and can_use_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 %}
{% if can_edit %}
{% if perms.judge.moss_contest and has_moss_api_key %}

View file

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

View file

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