commit
f771aac922
9 changed files with 42 additions and 58 deletions
|
@ -32,6 +32,7 @@ from judge.widgets import (
|
||||||
HeavyPreviewAdminPageDownWidget,
|
HeavyPreviewAdminPageDownWidget,
|
||||||
)
|
)
|
||||||
from judge.views.contests import recalculate_contest_summary_result
|
from judge.views.contests import recalculate_contest_summary_result
|
||||||
|
from judge.utils.contest import maybe_trigger_contest_rescore
|
||||||
|
|
||||||
|
|
||||||
class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
|
class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
|
||||||
|
@ -310,39 +311,14 @@ class ContestAdmin(CompareVersionAdmin):
|
||||||
|
|
||||||
super().save_model(request, obj, form, change)
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
# We need this flag because `save_related` deals with the inlines, but does not know if we have already rescored
|
|
||||||
self._rescored = False
|
|
||||||
if form.changed_data and any(
|
|
||||||
f in form.changed_data
|
|
||||||
for f in (
|
|
||||||
"start_time",
|
|
||||||
"end_time",
|
|
||||||
"time_limit",
|
|
||||||
"format_config",
|
|
||||||
"format_name",
|
|
||||||
"freeze_after",
|
|
||||||
)
|
|
||||||
):
|
|
||||||
self._rescore(obj.key)
|
|
||||||
self._rescored = True
|
|
||||||
|
|
||||||
if form.changed_data and any(
|
|
||||||
f in form.changed_data
|
|
||||||
for f in (
|
|
||||||
"authors",
|
|
||||||
"curators",
|
|
||||||
"testers",
|
|
||||||
)
|
|
||||||
):
|
|
||||||
Contest._author_ids.dirty(obj)
|
|
||||||
Contest._curator_ids.dirty(obj)
|
|
||||||
Contest._tester_ids.dirty(obj)
|
|
||||||
|
|
||||||
def save_related(self, request, form, formsets, change):
|
def save_related(self, request, form, formsets, change):
|
||||||
super().save_related(request, form, formsets, change)
|
super().save_related(request, form, formsets, change)
|
||||||
# Only rescored if we did not already do so in `save_model`
|
# Only rescored if we did not already do so in `save_model`
|
||||||
if not self._rescored and any(formset.has_changed() for formset in formsets):
|
formset_changed = False
|
||||||
self._rescore(form.cleaned_data["key"])
|
if any(formset.has_changed() for formset in formsets):
|
||||||
|
formset_changed = True
|
||||||
|
|
||||||
|
maybe_trigger_contest_rescore(form, form.instance, formset_changed)
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
def has_change_permission(self, request, obj=None):
|
||||||
if not request.user.has_perm("judge.edit_own_contest"):
|
if not request.user.has_perm("judge.edit_own_contest"):
|
||||||
|
@ -351,11 +327,6 @@ class ContestAdmin(CompareVersionAdmin):
|
||||||
return True
|
return True
|
||||||
return obj.is_editable_by(request.user)
|
return obj.is_editable_by(request.user)
|
||||||
|
|
||||||
def _rescore(self, contest_key):
|
|
||||||
from judge.tasks import rescore_contest
|
|
||||||
|
|
||||||
transaction.on_commit(rescore_contest.s(contest_key).delay)
|
|
||||||
|
|
||||||
def make_visible(self, request, queryset):
|
def make_visible(self, request, queryset):
|
||||||
if not request.user.has_perm("judge.change_contest_visibility"):
|
if not request.user.has_perm("judge.change_contest_visibility"):
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
|
|
|
@ -5,17 +5,20 @@ from judge.models import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def maybe_trigger_contest_rescore(form, contest):
|
def maybe_trigger_contest_rescore(form, contest, force_rescore=False):
|
||||||
if any(
|
if (
|
||||||
f in form.changed_data
|
any(
|
||||||
for f in (
|
f in form.changed_data
|
||||||
"start_time",
|
for f in (
|
||||||
"end_time",
|
"start_time",
|
||||||
"time_limit",
|
"end_time",
|
||||||
"format_config",
|
"time_limit",
|
||||||
"format_name",
|
"format_config",
|
||||||
"freeze_after",
|
"format_name",
|
||||||
|
"freeze_after",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
or force_rescore
|
||||||
):
|
):
|
||||||
transaction.on_commit(rescore_contest.s(contest.key).delay)
|
transaction.on_commit(rescore_contest.s(contest.key).delay)
|
||||||
|
|
||||||
|
|
|
@ -715,7 +715,10 @@ class EditCourseContest(CourseEditableMixin, FormView):
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
problem_formset = self.get_problem_formset(True)
|
problem_formset = self.get_problem_formset(True)
|
||||||
if problem_formset.is_valid():
|
if problem_formset.is_valid():
|
||||||
|
self.problem_form_changes = False
|
||||||
for problem_form in problem_formset:
|
for problem_form in problem_formset:
|
||||||
|
if problem_form.has_changed():
|
||||||
|
self.problem_form_changes = True
|
||||||
if problem_form.cleaned_data.get("DELETE") and problem_form.instance.pk:
|
if problem_form.cleaned_data.get("DELETE") and problem_form.instance.pk:
|
||||||
problem_form.instance.delete()
|
problem_form.instance.delete()
|
||||||
|
|
||||||
|
@ -741,7 +744,8 @@ class EditCourseContest(CourseEditableMixin, FormView):
|
||||||
revisions.set_comment(_("Edited from course") + " " + self.course.name)
|
revisions.set_comment(_("Edited from course") + " " + self.course.name)
|
||||||
revisions.set_user(self.request.user)
|
revisions.set_user(self.request.user)
|
||||||
|
|
||||||
maybe_trigger_contest_rescore(form, self.contest)
|
if self.problem_form_changes:
|
||||||
|
maybe_trigger_contest_rescore(form, self.contest, True)
|
||||||
|
|
||||||
form.save()
|
form.save()
|
||||||
|
|
||||||
|
|
|
@ -1039,7 +1039,7 @@ class EditOrganizationContest(
|
||||||
self.object.is_organization_private = True
|
self.object.is_organization_private = True
|
||||||
self.object.save()
|
self.object.save()
|
||||||
|
|
||||||
maybe_trigger_contest_rescore(form, self.object)
|
maybe_trigger_contest_rescore(form, self.object, True)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
|
@ -843,6 +843,10 @@ noscript #noscript {
|
||||||
.nav-fa-icon {
|
.nav-fa-icon {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media(min-width: 800px) {
|
@media(min-width: 800px) {
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
td:first-child {
|
th:first-child, td:first-child {
|
||||||
border-color: $border_gray;
|
border-color: $border_gray;
|
||||||
border-width: 1px 1px 0 1px;
|
border-width: 1px 1px 0 1px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,6 +179,7 @@ th.header.rank {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
width: 225px;
|
width: 225px;
|
||||||
float: right;
|
float: right;
|
||||||
|
margin-top: 1em;
|
||||||
|
|
||||||
.select2-selection__arrow {
|
.select2-selection__arrow {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -47,8 +47,8 @@
|
||||||
var rows = $('#users-table tbody tr').get();
|
var rows = $('#users-table tbody tr').get();
|
||||||
var sortBy = $(this).val();
|
var sortBy = $(this).val();
|
||||||
rows.sort(function(a, b) {
|
rows.sort(function(a, b) {
|
||||||
var keyA = $(a).find(sortBy === 'username' ? '.user-name' : 'td:last-child').text().trim();
|
var keyA = $(a).find(sortBy === 'username' ? '.user-name' : '.total-score').text().trim();
|
||||||
var keyB = $(b).find(sortBy === 'username' ? '.user-name' : 'td:last-child').text().trim();
|
var keyB = $(b).find(sortBy === 'username' ? '.user-name' : '.total-score').text().trim();
|
||||||
|
|
||||||
if(sortBy === 'total') {
|
if(sortBy === 'total') {
|
||||||
// Convert percentage string to number for comparison
|
// Convert percentage string to number for comparison
|
||||||
|
@ -76,11 +76,13 @@
|
||||||
{% block middle_content %}
|
{% block middle_content %}
|
||||||
<center><h2>{{content_title}}</h2></center>
|
<center><h2>{{content_title}}</h2></center>
|
||||||
{{_("Sort by")}}:
|
{{_("Sort by")}}:
|
||||||
<select id="sortSelect">
|
<div>
|
||||||
<option value="username">{{_("Username")}}</option>
|
<select id="sortSelect">
|
||||||
<option value="total">{{_("Score")}}</option>
|
<option value="username">{{_("Username")}}</option>
|
||||||
</select>
|
<option value="total">{{_("Score")}}</option>
|
||||||
<input type="text" id="search-input" placeholder="{{_('Search')}}" autofocus>
|
</select>
|
||||||
|
<input type="text" id="search-input" placeholder="{{_('Search')}}" autofocus>
|
||||||
|
</div>
|
||||||
<div style="overflow-x: auto; margin-top: 1em">
|
<div style="overflow-x: auto; margin-top: 1em">
|
||||||
<table class="table striped" id="users-table">
|
<table class="table striped" id="users-table">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -119,7 +121,7 @@
|
||||||
{{student.first_name}}
|
{{student.first_name}}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td style="font-weight: bold">
|
<td class="total-score" style="font-weight: bold">
|
||||||
{% if grade_total_student %}
|
{% if grade_total_student %}
|
||||||
{{ grade_total_student['percentage'] | floatformat(0) }}%
|
{{ grade_total_student['percentage'] | floatformat(0) }}%
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="score">{{ breakdown.sub_points|floatformat(0) }} / {{ breakdown.sub_total|floatformat(0) }}</div>
|
<div class="score">{{ breakdown.sub_points|floatformat(0) }} / {{ breakdown.sub_total|floatformat(0) }}</div>
|
||||||
<div class="language">{{ breakdown.sub_lang }}</div>
|
<div class="language">{{ breakdown.sub_lang }}</div>
|
||||||
<span class="time">{{ relative_time(breakdown.sub_date) }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sub-pp sub-usage">
|
<div class="sub-pp sub-usage">
|
||||||
|
@ -19,7 +18,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="pp-weighted">
|
<div class="pp-weighted">
|
||||||
{% trans weight=breakdown.weight|floatformat(0) %}
|
{% trans weight=breakdown.weight|floatformat(0) %}
|
||||||
weighted <b>{{ weight }}%</b>
|
<b>{{ weight }}%</b>
|
||||||
{% endtrans %}
|
{% endtrans %}
|
||||||
|
|
||||||
{% if breakdown.scaled_points < 10 %}
|
{% if breakdown.scaled_points < 10 %}
|
||||||
|
|
Loading…
Reference in a new issue