Change UI ranking
This commit is contained in:
parent
d38342ad43
commit
9b1724cdad
16 changed files with 291 additions and 631 deletions
10
dmoj/urls.py
10
dmoj/urls.py
|
@ -310,7 +310,6 @@ urlpatterns = [
|
|||
ticket.NewProblemTicketView.as_view(),
|
||||
name="new_problem_ticket",
|
||||
),
|
||||
url(r"^/vote$", problem.Vote.as_view(), name="vote"),
|
||||
url(
|
||||
r"^/manage/submission",
|
||||
include(
|
||||
|
@ -529,11 +528,18 @@ urlpatterns = [
|
|||
),
|
||||
),
|
||||
url(
|
||||
r"^/submissions/(?P<user>\w+)/(?P<problem>\w+)/",
|
||||
r"^/submissions/(?P<user>\w+)/(?P<problem>\w+)",
|
||||
paged_list_view(
|
||||
submission.UserContestSubmissions, "contest_user_submissions"
|
||||
),
|
||||
),
|
||||
url(
|
||||
r"^/submissions/(?P<user>\w+)/(?P<problem>\w+)/ajax",
|
||||
paged_list_view(
|
||||
submission.UserContestSubmissionsAjax,
|
||||
"contest_user_submissions_ajax",
|
||||
),
|
||||
),
|
||||
url(
|
||||
r"^/participations$",
|
||||
contests.ContestParticipationList.as_view(),
|
||||
|
|
|
@ -55,7 +55,7 @@ class DefaultContestFormat(BaseContestFormat):
|
|||
format_data = (participation.format_data or {}).get(str(contest_problem.id))
|
||||
if format_data:
|
||||
return format_html(
|
||||
'<td class="{state} problem-score-col"><a href="{url}">{points}<div class="solving-time">{time}</div></a></td>',
|
||||
'<td class="{state} problem-score-col"><a data-featherlight="{url}" href="#">{points}<div class="solving-time">{time}</div></a></td>',
|
||||
state=(
|
||||
(
|
||||
"pretest-"
|
||||
|
@ -68,7 +68,7 @@ class DefaultContestFormat(BaseContestFormat):
|
|||
)
|
||||
),
|
||||
url=reverse(
|
||||
"contest_user_submissions",
|
||||
"contest_user_submissions_ajax",
|
||||
args=[
|
||||
self.contest.key,
|
||||
participation.user.user.username,
|
||||
|
|
|
@ -171,7 +171,8 @@ msgid ""
|
|||
msgstr ""
|
||||
|
||||
"Content-Type: text/plain; charset=utf-8\\n"
|
||||
""")
|
||||
"""
|
||||
)
|
||||
if self.verbosity > 1:
|
||||
self.stdout.write("processing navigation bar")
|
||||
for label in NavigationBar.objects.values_list("label", flat=True):
|
||||
|
|
|
@ -561,29 +561,6 @@ class Problem(models.Model):
|
|||
|
||||
save.alters_data = True
|
||||
|
||||
def can_vote(self, request):
|
||||
return False
|
||||
user = request.user
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
|
||||
# If the user is in contest, nothing should be shown.
|
||||
if request.in_contest_mode:
|
||||
return False
|
||||
|
||||
# If the user is not allowed to vote
|
||||
if user.profile.is_unlisted or user.profile.is_banned_problem_voting:
|
||||
return False
|
||||
|
||||
# If the user is banned from submitting to the problem.
|
||||
if self.banned_users.filter(pk=user.pk).exists():
|
||||
return False
|
||||
|
||||
# If the user has a full AC submission to the problem (solved the problem).
|
||||
return self.submission_set.filter(
|
||||
user=user.profile, result="AC", points=F("problem__points")
|
||||
).exists()
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
("see_private_problem", "See hidden problems"),
|
||||
|
|
|
@ -644,6 +644,7 @@ class KickUserWidgetView(
|
|||
LoginRequiredMixin, AdminOrganizationMixin, SingleObjectMixin, View
|
||||
):
|
||||
model = Organization
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
organization = self.get_object()
|
||||
try:
|
||||
|
|
|
@ -306,78 +306,9 @@ class ProblemDetail(ProblemMixin, SolvedProblemMixin, CommentedDetailView):
|
|||
context["meta_description"] = self.object.summary or metadata[0]
|
||||
context["og_image"] = self.object.og_image or metadata[1]
|
||||
|
||||
context["can_vote"] = self.object.can_vote(self.request)
|
||||
if context["can_vote"]:
|
||||
try:
|
||||
context["vote"] = ProblemPointsVote.objects.get(
|
||||
voter=user.profile, problem=self.object
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
context["vote"] = None
|
||||
else:
|
||||
context["vote"] = None
|
||||
|
||||
context["has_votes"] = False
|
||||
if user.is_superuser:
|
||||
all_votes = list(
|
||||
self.object.problem_points_votes.order_by("points").values_list(
|
||||
"points", flat=True
|
||||
)
|
||||
)
|
||||
context["all_votes"] = all_votes
|
||||
context["has_votes"] = len(all_votes) > 0
|
||||
context["max_possible_vote"] = 600
|
||||
context["min_possible_vote"] = 100
|
||||
return context
|
||||
|
||||
|
||||
class DeleteVote(ProblemMixin, SingleObjectMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
return HttpResponseForbidden(status=405, content_type="text/plain")
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
if not request.user.is_authenticated:
|
||||
return HttpResponseForbidden("Not signed in.", content_type="text/plain")
|
||||
elif self.object.can_vote(request.user):
|
||||
ProblemPointsVote.objects.filter(
|
||||
voter=request.profile, problem=self.object
|
||||
).delete()
|
||||
return HttpResponse("success", content_type="text/plain")
|
||||
else:
|
||||
return HttpResponseForbidden(
|
||||
"Not allowed to delete votes on this problem.",
|
||||
content_type="text/plain",
|
||||
)
|
||||
|
||||
|
||||
class Vote(ProblemMixin, SingleObjectMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
return HttpResponseForbidden(status=405, content_type="text/plain")
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
if not self.object.can_vote(request): # Not allowed to vote for some reason.
|
||||
return HttpResponseForbidden(
|
||||
"Not allowed to vote on this problem.", content_type="text/plain"
|
||||
)
|
||||
|
||||
form = ProblemPointsVoteForm(request.POST)
|
||||
if form.is_valid():
|
||||
with transaction.atomic():
|
||||
# Delete any pre existing votes.
|
||||
ProblemPointsVote.objects.filter(
|
||||
voter=request.profile, problem=self.object
|
||||
).delete()
|
||||
vote = form.save(commit=False)
|
||||
vote.voter = request.profile
|
||||
vote.problem = self.object
|
||||
vote.save()
|
||||
return JsonResponse({"points": vote.points})
|
||||
else:
|
||||
return JsonResponse(form.errors, status=400)
|
||||
|
||||
|
||||
class LatexError(Exception):
|
||||
pass
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ from django.http import HttpResponseRedirect
|
|||
from django.http import JsonResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.shortcuts import render
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
|
@ -47,6 +48,7 @@ from judge.utils.problem_data import get_problem_case
|
|||
from judge.utils.raw_sql import join_sql_subquery, use_straight_join
|
||||
from judge.utils.views import DiggPaginatorMixin
|
||||
from judge.utils.views import TitleMixin
|
||||
from judge.utils.timedelta import nice_repr
|
||||
|
||||
|
||||
def submission_related(queryset):
|
||||
|
@ -358,7 +360,8 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
|
|||
)
|
||||
if self.selected_statuses:
|
||||
queryset = queryset.filter(
|
||||
Q(result__in=self.selected_statuses) | Q(status__in=self.selected_statuses)
|
||||
Q(result__in=self.selected_statuses)
|
||||
| Q(status__in=self.selected_statuses)
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
@ -392,9 +395,7 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
|
|||
hidden_codes = ["SC", "D", "G"]
|
||||
if not self.request.user.is_superuser and not self.request.user.is_staff:
|
||||
hidden_codes += ["IE"]
|
||||
return [
|
||||
(key, value) for key, value in all_statuses if key not in hidden_codes
|
||||
]
|
||||
return [(key, value) for key, value in all_statuses if key not in hidden_codes]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(SubmissionsListBase, self).get_context_data(**kwargs)
|
||||
|
@ -782,3 +783,30 @@ class UserContestSubmissions(ForceContestMixin, UserProblemSubmissions):
|
|||
self.contest.name,
|
||||
reverse("contest_view", args=[self.contest.key]),
|
||||
)
|
||||
|
||||
|
||||
class UserContestSubmissionsAjax(UserContestSubmissions):
|
||||
template_name = "submission/user-ajax.html"
|
||||
|
||||
def contest_time(self, s):
|
||||
if s.contest.participation.live:
|
||||
return s.date - s.contest.participation.real_start
|
||||
return None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(UserContestSubmissionsAjax, self).get_context_data(**kwargs)
|
||||
context["contest"] = self.contest
|
||||
context["problem"] = self.problem
|
||||
context["profile"] = self.profile
|
||||
|
||||
contest_problem = self.contest.contest_problems.get(problem=self.problem)
|
||||
for s in context["submissions"]:
|
||||
contest_time = self.contest_time(s)
|
||||
if contest_time:
|
||||
s.contest_time = nice_repr(contest_time, "noday")
|
||||
else:
|
||||
s.contest_time = None
|
||||
points = floatformat(s.contest.points, -self.contest.points_precision)
|
||||
total = floatformat(contest_problem.points, -self.contest.points_precision)
|
||||
s.display_point = f"{points} / {total}"
|
||||
return context
|
||||
|
|
|
@ -362,3 +362,9 @@ label[for="language"], label[for="status"] {
|
|||
color: gray;
|
||||
}
|
||||
}
|
||||
|
||||
.lightbox-submissions {
|
||||
td {
|
||||
padding-right: 0.2em;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,142 @@
|
|||
<script src="//cdn.jsdelivr.net/npm/featherlight@1.7.14/release/featherlight.min.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script type="text/javascript">
|
||||
function isFaster(time1, time2) {
|
||||
let arr1 = time1.split(':');
|
||||
let arr2 = time2.split(':');
|
||||
|
||||
for (let i in arr1) {
|
||||
let val1 = parseInt(arr1[i]);
|
||||
let val2 = parseInt(arr2[i]);
|
||||
if (val1 < val2) return true;
|
||||
if (val1 > val2) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function scoretimeComparison(sub1, sub2) {
|
||||
if (!sub2) return true;
|
||||
return sub1['score'] > sub2['score'] || (sub1['score'] === sub2['score'] && isFaster(sub1['time'], sub2['time']));
|
||||
}
|
||||
|
||||
function highlightFirstSolve() {
|
||||
// bucket to store submissions by problems
|
||||
let bestSubmissions = {};
|
||||
|
||||
// get information
|
||||
$('td a').each(function() {
|
||||
let td = $(this)[0]
|
||||
let link = td['attributes']['href']['value']
|
||||
if (link.includes('submissions')) {
|
||||
let scoreAndTime = (td.innerText.split('\n'))
|
||||
let linkElements = link.split('/')
|
||||
|
||||
// get information
|
||||
let problem = linkElements[linkElements.length - 2];
|
||||
let score = parseFloat(scoreAndTime[0]);
|
||||
let time = scoreAndTime[1];
|
||||
|
||||
if (time) {
|
||||
let curSubmission = {
|
||||
'td': $(this).parent(),
|
||||
'score': score,
|
||||
'time': time
|
||||
}
|
||||
|
||||
// update best submissions
|
||||
let curBest = bestSubmissions[problem]
|
||||
|
||||
if (scoretimeComparison(curSubmission, curBest) && score) {
|
||||
bestSubmissions[problem] = curSubmission;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
for (let problem in bestSubmissions) {
|
||||
bestSubmissions[problem]['td'].addClass('first-solve')
|
||||
}
|
||||
}
|
||||
|
||||
function renew_filter() {
|
||||
var checkboxes = [
|
||||
'#show-organizations-checkbox',
|
||||
'#show-fullnames-checkbox',
|
||||
'#show-total-score-checkbox',
|
||||
];
|
||||
|
||||
var checkboxes2 = [
|
||||
'#show-friends-checkbox',
|
||||
'#show-virtual-checkbox'
|
||||
]
|
||||
|
||||
for (var i of checkboxes) {
|
||||
var $box = $(i);
|
||||
if ($box.is(':checked')) {
|
||||
$box.prop('checked', false);
|
||||
$box.click();
|
||||
$box.prop('checked', true);
|
||||
}
|
||||
}
|
||||
|
||||
var to_update = false;
|
||||
for (var i of checkboxes2) {
|
||||
var $box = $(i);
|
||||
if ($box.is(':checked')) {
|
||||
to_update = true;
|
||||
}
|
||||
}
|
||||
if (to_update) {
|
||||
update_ranking();
|
||||
}
|
||||
}
|
||||
|
||||
function get_initial_rank() {
|
||||
var ranks = $('.rank-td').map(function() {return this.innerHTML}).get();
|
||||
var usernames = $('.user-name .rating a').map(function() {return this.text}).get();
|
||||
window.user_rank = new Map();
|
||||
for (var i = 0; i < ranks.length; i++) {
|
||||
window.user_rank[usernames[i]] = ranks[i];
|
||||
}
|
||||
}
|
||||
|
||||
function add_initial_friend_rank() {
|
||||
var usernames = $('.user-name .rating a').map(function() {return this.text}).get();
|
||||
|
||||
var is_virtual = [];
|
||||
$('.user-name').each(function() {
|
||||
if($(this).children('sup').length) {
|
||||
is_virtual.push(1);
|
||||
}
|
||||
else is_virtual.push(0);
|
||||
});
|
||||
|
||||
$('.rank-td').each(function(i) {
|
||||
if (!is_virtual[i]) this.innerHTML += ' (' + window.user_rank[usernames[i]] + ')';
|
||||
});
|
||||
}
|
||||
|
||||
function update_ranking() {
|
||||
var friend = $('#show-friends-checkbox').is(':checked');
|
||||
var virtual = $('#show-virtual-checkbox').is(':checked');
|
||||
$('#loading-gif').show();
|
||||
$.get({
|
||||
url: `{{url('contest_ranking_ajax', contest.key)}}?friend=${friend}&virtual=${virtual}`,
|
||||
success: function(HTML) {
|
||||
$('#users-table').html(HTML);
|
||||
highlightFirstSolve();
|
||||
$('#loading-gif').hide();
|
||||
if (!virtual && !friend) {
|
||||
get_initial_rank();
|
||||
}
|
||||
if (friend) {
|
||||
add_initial_friend_rank();
|
||||
}
|
||||
},
|
||||
fail: function() {
|
||||
console.log('Fail to update ranking');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$('.leaving-forever').click(function () {
|
||||
return confirm('{{ _('Are you sure you want to leave?') }}\n' +
|
||||
|
@ -9,5 +147,68 @@
|
|||
return confirm('{{ _('Are you sure you want to join?') }}\n' +
|
||||
'{{ _('Joining a contest starts your timer, after which it becomes unstoppable.') }}');
|
||||
});
|
||||
|
||||
var url = '{{ url('contest_participation', contest.key, '__username__') }}';
|
||||
var placeholder = $('#search-contest').replaceWith($('<select>').attr({
|
||||
id: 'search-contest'
|
||||
})).attr('placeholder');
|
||||
|
||||
$('#search-contest').select2({
|
||||
placeholder: placeholder,
|
||||
ajax: {
|
||||
url: '{{ url('contest_user_search_select2_ajax', contest.key) }}'
|
||||
},
|
||||
minimumInputLength: 1,
|
||||
escapeMarkup: function (markup) {
|
||||
return markup;
|
||||
},
|
||||
templateResult: function (data, container) {
|
||||
return ('<img class="user-search-image" src="' + data.gravatar_url + '" width="24" height="24">' +
|
||||
'<span class="' + data.display_rank + ' user-search-name">' + data.text + '</span>');
|
||||
}
|
||||
}).on('change', function () {
|
||||
window.location.href = url.replace('__username__', $(this).val());
|
||||
});
|
||||
|
||||
$('#show-organizations-checkbox').click(function () {
|
||||
$('.organization-column').toggle();
|
||||
});
|
||||
$('#show-fullnames-checkbox').click(function () {
|
||||
$('.fullname-column').toggle();
|
||||
});
|
||||
|
||||
{% if request.user.is_authenticated %}
|
||||
$('#show-friends-checkbox').click(function() {
|
||||
update_ranking();
|
||||
})
|
||||
{% endif %}
|
||||
$('#show-virtual-checkbox').click(function() {
|
||||
update_ranking();
|
||||
})
|
||||
$('#show-total-score-checkbox').click(function() {
|
||||
$('.problem-score-col').toggle();
|
||||
})
|
||||
|
||||
highlightFirstSolve();
|
||||
renew_filter();
|
||||
get_initial_rank();
|
||||
|
||||
{% if participation_tab %}
|
||||
$('#show-virtual-checkbox').hide();
|
||||
$('#show-virtual-label').hide();
|
||||
{% else %}
|
||||
{% if request.in_contest %}
|
||||
setInterval(update_ranking, 60 * 1000);
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
// $(".problem-score-a").on('click', function(e) {
|
||||
// var href = $(this).attr('href');
|
||||
// if (href !== '#') return;
|
||||
// e.preventDefault();
|
||||
|
||||
|
||||
// })
|
||||
});
|
||||
|
||||
</script>
|
|
@ -58,7 +58,7 @@
|
|||
|
||||
{% block before_point_head %}
|
||||
{% for problem in problems %}
|
||||
<th class="points header problem-score-col"><a href="{{ url('contest_ranked_submissions', contest.key, problem.problem.code) }}">
|
||||
<th class="points header problem-score-col" title="{{ problem.problem.name }}"><a href="{{ url('problem_detail', problem.problem.code) }}">
|
||||
{{- contest.get_label_for_problem(loop.index0) }}
|
||||
<div class="point-denominator">{{ problem.points }}</div>
|
||||
</a></th>
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block users_media %}
|
||||
<link href="//cdn.jsdelivr.net/npm/featherlight@1.7.14/release/featherlight.min.css" type="text/css" rel="stylesheet" />
|
||||
|
||||
<style>
|
||||
#content-left {
|
||||
overflow-x: auto;
|
||||
|
@ -130,6 +132,12 @@
|
|||
color: gray !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.featherlight-content {
|
||||
border-radius: 10px;
|
||||
height: 80%;
|
||||
width: 60%;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% if has_rating %}
|
||||
|
@ -227,215 +235,6 @@
|
|||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
<script type="text/javascript" src="{{ static('event.js') }}"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
function isFaster(time1, time2) {
|
||||
let arr1 = time1.split(':');
|
||||
let arr2 = time2.split(':');
|
||||
|
||||
for (let i in arr1) {
|
||||
let val1 = parseInt(arr1[i]);
|
||||
let val2 = parseInt(arr2[i]);
|
||||
if (val1 < val2) return true;
|
||||
if (val1 > val2) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function scoretimeComparison(sub1, sub2) {
|
||||
if (!sub2) return true;
|
||||
return sub1['score'] > sub2['score'] || (sub1['score'] === sub2['score'] && isFaster(sub1['time'], sub2['time']));
|
||||
}
|
||||
|
||||
function highlightFirstSolve() {
|
||||
// bucket to store submissions by problems
|
||||
let bestSubmissions = {};
|
||||
|
||||
// get information
|
||||
$('td a').each(function() {
|
||||
let td = $(this)[0]
|
||||
let link = td['attributes']['href']['value']
|
||||
if (link.includes('submissions')) {
|
||||
let scoreAndTime = (td.innerText.split('\n'))
|
||||
let linkElements = link.split('/')
|
||||
|
||||
// get information
|
||||
let problem = linkElements[linkElements.length - 2];
|
||||
let score = parseFloat(scoreAndTime[0]);
|
||||
let time = scoreAndTime[1];
|
||||
|
||||
if (time) {
|
||||
let curSubmission = {
|
||||
'td': $(this).parent(),
|
||||
'score': score,
|
||||
'time': time
|
||||
}
|
||||
|
||||
// update best submissions
|
||||
let curBest = bestSubmissions[problem]
|
||||
|
||||
if (scoretimeComparison(curSubmission, curBest) && score) {
|
||||
bestSubmissions[problem] = curSubmission;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
for (let problem in bestSubmissions) {
|
||||
bestSubmissions[problem]['td'].addClass('first-solve')
|
||||
}
|
||||
}
|
||||
|
||||
function renew_filter() {
|
||||
var checkboxes = [
|
||||
'#show-organizations-checkbox',
|
||||
'#show-fullnames-checkbox',
|
||||
'#show-total-score-checkbox',
|
||||
];
|
||||
|
||||
var checkboxes2 = [
|
||||
'#show-friends-checkbox',
|
||||
'#show-virtual-checkbox'
|
||||
]
|
||||
|
||||
for (var i of checkboxes) {
|
||||
var $box = $(i);
|
||||
if ($box.is(':checked')) {
|
||||
$box.prop('checked', false);
|
||||
$box.click();
|
||||
$box.prop('checked', true);
|
||||
}
|
||||
}
|
||||
|
||||
var to_update = false;
|
||||
for (var i of checkboxes2) {
|
||||
var $box = $(i);
|
||||
if ($box.is(':checked')) {
|
||||
to_update = true;
|
||||
}
|
||||
}
|
||||
if (to_update) {
|
||||
update_ranking();
|
||||
}
|
||||
}
|
||||
|
||||
function get_initial_rank() {
|
||||
var ranks = $('.rank-td').map(function() {return this.innerHTML}).get();
|
||||
var usernames = $('.user-name .rating a').map(function() {return this.text}).get();
|
||||
window.user_rank = new Map();
|
||||
for (var i = 0; i < ranks.length; i++) {
|
||||
window.user_rank[usernames[i]] = ranks[i];
|
||||
}
|
||||
}
|
||||
|
||||
function add_initial_friend_rank() {
|
||||
var usernames = $('.user-name .rating a').map(function() {return this.text}).get();
|
||||
|
||||
var is_virtual = [];
|
||||
$('.user-name').each(function() {
|
||||
if($(this).children('sup').length) {
|
||||
is_virtual.push(1);
|
||||
}
|
||||
else is_virtual.push(0);
|
||||
});
|
||||
|
||||
$('.rank-td').each(function(i) {
|
||||
if (!is_virtual[i]) this.innerHTML += ' (' + window.user_rank[usernames[i]] + ')';
|
||||
});
|
||||
}
|
||||
|
||||
function update_ranking() {
|
||||
var friend = $('#show-friends-checkbox').is(':checked');
|
||||
var virtual = $('#show-virtual-checkbox').is(':checked');
|
||||
$('#loading-gif').show();
|
||||
$.get({
|
||||
url: `/contest/{{contest.key}}/ranking/ajax?friend=${friend}&virtual=${virtual}`,
|
||||
success: function(HTML) {
|
||||
$('#users-table').html(HTML);
|
||||
highlightFirstSolve();
|
||||
$('#loading-gif').hide();
|
||||
if (!virtual && !friend) {
|
||||
get_initial_rank();
|
||||
}
|
||||
if (friend) {
|
||||
add_initial_friend_rank();
|
||||
}
|
||||
},
|
||||
fail: function() {
|
||||
console.log('Fail to update ranking');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// window.load_dynamic_update = function (last_msg) {
|
||||
// return new EventReceiver(
|
||||
// "{{ EVENT_DAEMON_LOCATION }}", "{{ EVENT_DAEMON_POLL_LOCATION }}",
|
||||
// ['contest_{{contest.id}}'], last_msg, function (message) {
|
||||
// switch (message.type) {
|
||||
// case 'update':
|
||||
// update_ranking();
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
|
||||
$(function () {
|
||||
var url = '{{ url('contest_participation', contest.key, '__username__') }}';
|
||||
var placeholder = $('#search-contest').replaceWith($('<select>').attr({
|
||||
id: 'search-contest'
|
||||
})).attr('placeholder');
|
||||
|
||||
$('#search-contest').select2({
|
||||
placeholder: placeholder,
|
||||
ajax: {
|
||||
url: '{{ url('contest_user_search_select2_ajax', contest.key) }}'
|
||||
},
|
||||
minimumInputLength: 1,
|
||||
escapeMarkup: function (markup) {
|
||||
return markup;
|
||||
},
|
||||
templateResult: function (data, container) {
|
||||
return ('<img class="user-search-image" src="' + data.gravatar_url + '" width="24" height="24">' +
|
||||
'<span class="' + data.display_rank + ' user-search-name">' + data.text + '</span>');
|
||||
}
|
||||
}).on('change', function () {
|
||||
window.location.href = url.replace('__username__', $(this).val());
|
||||
});
|
||||
|
||||
$('#show-organizations-checkbox').click(function () {
|
||||
$('.organization-column').toggle();
|
||||
});
|
||||
$('#show-fullnames-checkbox').click(function () {
|
||||
$('.fullname-column').toggle();
|
||||
});
|
||||
|
||||
{% if request.user.is_authenticated %}
|
||||
$('#show-friends-checkbox').click(function() {
|
||||
update_ranking();
|
||||
})
|
||||
{% endif %}
|
||||
$('#show-virtual-checkbox').click(function() {
|
||||
update_ranking();
|
||||
})
|
||||
$('#show-total-score-checkbox').click(function() {
|
||||
$('.problem-score-col').toggle();
|
||||
})
|
||||
|
||||
highlightFirstSolve();
|
||||
renew_filter();
|
||||
get_initial_rank();
|
||||
{% if participation_tab %}
|
||||
$('#show-virtual-checkbox').hide();
|
||||
$('#show-virtual-label').hide();
|
||||
{% else %}
|
||||
{% if request.in_contest %}
|
||||
setInterval(update_ranking, 60 * 1000);
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
});
|
||||
</script>
|
||||
{% include "contest/media-js.html" %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -346,9 +346,6 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block description %}
|
||||
{% if can_vote and not vote %}
|
||||
{% include 'problem/voting-form.html' %}
|
||||
{% endif %}
|
||||
{% if contest_problem and contest_problem.contest.use_clarifications and has_clarifications %}
|
||||
<div id="clarification_header_container">
|
||||
<i class="fa fa-question-circle"></i>
|
||||
|
@ -374,9 +371,6 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block post_description_end %}
|
||||
{% if can_vote or request.user.is_superuser %}
|
||||
{% include 'problem/voting-controls.html' %}
|
||||
{% endif %}
|
||||
{% if request.user.is_authenticated and not request.profile.mute %}
|
||||
<a href="{{ url('new_problem_ticket', problem.code) }}" class="clarify">
|
||||
<i class="fa fa-flag" style="margin-right:0.5em"></i>
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
<style>
|
||||
.featherlight .featherlight-inner{
|
||||
display: block !important;
|
||||
}
|
||||
.featherlight-content {
|
||||
overflow: inherit !important;
|
||||
}
|
||||
|
||||
.stars-container {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
.stars-container .star {
|
||||
display: inline-block;
|
||||
padding: 2px;
|
||||
color: orange;
|
||||
cursor: pointer;
|
||||
font-size: 30px;
|
||||
}
|
||||
.stars-container .star:before {
|
||||
content:"\f006";
|
||||
}
|
||||
.filled-star:before, .star:hover:before {
|
||||
content:"\f005" !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% if can_vote or request.user.is_superuser %}
|
||||
<div class="vote-form" id="id_vote_form_box" style="display: none">
|
||||
{% include 'problem/voting-form.html' %}
|
||||
</div>
|
||||
<span>
|
||||
{% if can_vote %}
|
||||
<a href="#" class="form-button" id="id_vote_button" data-featherlight="#id_vote_form_box"></a>
|
||||
{% endif %}
|
||||
{% if request.user.is_superuser %}
|
||||
- {% include 'problem/voting-stats.html' %}
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
<script src="{{ static('libs/featherlight/featherlight.min.js') }}" type="text/javascript"></script>
|
||||
<script>
|
||||
let voted_points = null;
|
||||
{% if vote is not none %}
|
||||
let has_voted = true;
|
||||
voted_points = {{ vote.points }};
|
||||
{% else %}
|
||||
let has_voted = false;
|
||||
{% endif %}
|
||||
|
||||
function voteUpdate(){
|
||||
$('#id_has_voted_prompt').show();
|
||||
$('#id_current_vote_value').prop('innerText', voted_points);
|
||||
$('#id_has_not_voted_prompt').hide();
|
||||
$('#id_vote_button').prop('innerText', `{{ _('Edit difficulty') }} (` + voted_points + ')');
|
||||
$('#id_points_error_box').hide();
|
||||
has_voted = true;
|
||||
}
|
||||
|
||||
function deleteVoteUpdate(){
|
||||
$('#id_has_voted_prompt').hide();
|
||||
$('#id_has_not_voted_prompt').show();
|
||||
$('#id_vote_button').prop('innerText', `{{ _('Vote difficulty') }}`);
|
||||
$('#id_points_error_box').hide();
|
||||
has_voted = false;
|
||||
}
|
||||
|
||||
if (has_voted) voteUpdate();
|
||||
else deleteVoteUpdate();
|
||||
|
||||
$('#id_vote_form').on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
|
@ -1,93 +0,0 @@
|
|||
<style>
|
||||
.vote_form {
|
||||
border: 3px solid blue;
|
||||
border-radius: 5px;
|
||||
width: fit-content;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding: 0.2em 1em;
|
||||
background: #ebf0ff;
|
||||
color: #2e69ff;
|
||||
}
|
||||
</style>
|
||||
<form id="id_vote_form" class="vote_form">
|
||||
{% csrf_token %}
|
||||
<input id="id_points" class="vote-form-value" type="hidden" step="1"
|
||||
min="{{ min_possible_vote }}" max="{{ max_possible_vote }}" name="points">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding-top: 1em; padding-right: 3em">
|
||||
<span><b>{{_('How difficult is this problem?')}}</b></span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="vote-rating">
|
||||
<span class="stars-container">
|
||||
{% for i in range(1, (max_possible_vote - min_possible_vote) // 100 + 2) %}
|
||||
<span id="star{{i}}" star-score="{{i * 100}}" class="fa star"></span>
|
||||
{% endfor %}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{_('This helps us improve the site')}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span style="padding-left: 0.2em"><b>{{min_possible_vote}}</b></span>
|
||||
<span style="float: right; padding-right: 0.2em"><b>{{max_possible_vote}}</b></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="id_points_error_box">
|
||||
<span id="id_points_error" class="voting-form-error"></span>
|
||||
</div>
|
||||
<div>
|
||||
<input type="hidden" id="id_vote_form_submit_button">
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
$('.star').hover(
|
||||
(e) => $(e.target).prevAll().addClass('filled-star'),
|
||||
(e) => $(e.target).prevAll().removeClass('filled-star')
|
||||
);
|
||||
$('.star').on('click', function() {
|
||||
$("#id_points").val($(this).attr('star-score'));
|
||||
$('#id_vote_form_submit_button').click();
|
||||
$('#id_vote_form').fadeOut(500);
|
||||
})
|
||||
$('#id_vote_form_submit_button').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
$.ajax({
|
||||
url: '{{ url('vote', object.code) }}',
|
||||
type: 'POST',
|
||||
data: $('#id_vote_form').serialize(),
|
||||
success: function (data) {
|
||||
{% if request.user.is_superuser %}
|
||||
updateUserVote(voted_points, data.points);
|
||||
{% endif %}
|
||||
voted_points = data.points;
|
||||
voteUpdate();
|
||||
// Forms are auto disabled to prevent resubmission, but we need to allow resubmission here.
|
||||
$('#id_vote_form_submit_button').removeAttr('disabled');
|
||||
var current = $.featherlight.current();
|
||||
if (current) current.close();
|
||||
},
|
||||
error: function (data) {
|
||||
let errors = data.responseJSON;
|
||||
if(errors === undefined) {
|
||||
alert('Unable to delete vote: ' + data.responsetext);
|
||||
}
|
||||
|
||||
if('points' in errors){
|
||||
$('#id_points_error_box').show();
|
||||
$('#id_points_error').prop('textContent', errors.points[0]);
|
||||
} else {
|
||||
$('#id_points_error_box').hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
|
@ -1,146 +0,0 @@
|
|||
<style>
|
||||
.vote-stats-background {
|
||||
background-color: rgb(255,255,255);
|
||||
padding: 20px;
|
||||
border-radius: 25px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
border: 1px;
|
||||
border: solid;
|
||||
border: #000000;
|
||||
}
|
||||
|
||||
.vote-stats-value {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.vote-stats-info {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
<a href="#" class="form-button" id="id_vote_stats_button">{{ _('Statistics') }}</a>
|
||||
<div class="vote-stats-background" id="id_vote_stats" style="display: none;">
|
||||
<script src={{ static('libs/chart.js/Chart.js') }}></script>
|
||||
<span class="vote-stats-info">{{ _('Voting Statistics') }}</span>
|
||||
<br/>
|
||||
<canvas class="canvas" id="id_vote_chart"></canvas>
|
||||
<span id="id_no_votes_error" class="vote-stats-info no_votes_error">{{ _('No Votes Available!') }}</span>
|
||||
<br/>
|
||||
<div id="id_has_votes_footer" class="has_votes_footer">
|
||||
<span class="vote-stats-info">{{ _('Median:') }}</span>
|
||||
<span class="vote-stats-value median_vote" id="id_median_vote"></span>
|
||||
<span class="vote-stats-info">{{ _('Mean:') }}</span>
|
||||
<span class="vote-stats-value mean_vote" id="id_mean_vote"></span>
|
||||
<span class="vote-stats-info">{{ _('Total:') }}</span>
|
||||
<span class="vote-stats-value total_vote" id="id_num_of_votes"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let voteChart = null;
|
||||
let allVotes = {{ all_votes }};
|
||||
|
||||
function reload_vote_graph() {
|
||||
if (voteChart !== null) voteChart.destroy();
|
||||
|
||||
if (allVotes.length === 0) {
|
||||
$('.canvas').hide();
|
||||
$('.has_votes_footer').hide();
|
||||
$('.no_votes_error').show();
|
||||
} else {
|
||||
$('.canvas').show();
|
||||
$('.has_votes_footer').show();
|
||||
$('.no_votes_error').hide();
|
||||
|
||||
allVotes.sort(function(a, b){return a - b});
|
||||
// Give the graph some padding on both sides.
|
||||
let min_points = {{ min_possible_vote }};
|
||||
let max_points = {{ max_possible_vote }};
|
||||
|
||||
let xlabels = [];
|
||||
let voteFreq = [];
|
||||
for (let i = min_points; i <= max_points; i += 100) {
|
||||
xlabels.push(i);
|
||||
voteFreq.push(0);
|
||||
}
|
||||
|
||||
let max_number_of_votes = 0;
|
||||
let total_votes = 0;
|
||||
let mean = 0;
|
||||
|
||||
for (let i = 0; i < allVotes.length; i++) {
|
||||
// Assume the allVotes is valid.
|
||||
voteFreq[(allVotes[i] - min_points) / 100]++;
|
||||
max_number_of_votes = Math.max(max_number_of_votes, voteFreq[(allVotes[i] - min_points) / 100]);
|
||||
mean += allVotes[i];
|
||||
total_votes++;
|
||||
}
|
||||
mean = mean / total_votes;
|
||||
let half = Math.floor(total_votes / 2);
|
||||
let median = allVotes[half];
|
||||
if (total_votes % 2 === 0) {
|
||||
median = (median + allVotes[half - 1]) / 2;
|
||||
}
|
||||
|
||||
$('.mean_vote').prop('innerText', mean.toFixed(2));
|
||||
$('.median_vote').prop('innerText', median.toFixed(2));
|
||||
$('.total_vote').prop('innerText', total_votes);
|
||||
|
||||
const voteData = {
|
||||
labels: xlabels,
|
||||
datasets: [{
|
||||
data: voteFreq,
|
||||
backgroundColor: 'pink',
|
||||
}]
|
||||
};
|
||||
|
||||
const voteDataConfig = {
|
||||
type: 'bar',
|
||||
data: voteData,
|
||||
options: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
responsive: true,
|
||||
scales: {
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
precision: 0,
|
||||
suggestedMax: Math.ceil(max_number_of_votes * 1.2),
|
||||
beginAtZero: true,
|
||||
}
|
||||
}],
|
||||
xAxes: [{
|
||||
ticks: {
|
||||
beginAtZero: false,
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
};
|
||||
voteChart = new Chart($('.featherlight-inner .canvas'), voteDataConfig);
|
||||
}
|
||||
}
|
||||
|
||||
function updateUserVote(prev_voted_points, voted_points) {
|
||||
let index = allVotes.indexOf(prev_voted_points);
|
||||
if (index > -1) {
|
||||
allVotes.splice(index, 1);
|
||||
}
|
||||
allVotes.push(voted_points);
|
||||
}
|
||||
|
||||
function deleteUserVote(prev_voted_points) {
|
||||
allVotes.splice(allVotes.indexOf(prev_voted_points), 1);
|
||||
}
|
||||
$(function() {
|
||||
$('#id_vote_stats_button').featherlight('#id_vote_stats', {
|
||||
afterOpen: reload_vote_graph,
|
||||
})
|
||||
});
|
||||
</script>
|
30
templates/submission/user-ajax.html
Normal file
30
templates/submission/user-ajax.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
<h4>
|
||||
{{_('Contest submissions of')}} {{link_user(profile)}} <a href="{{url('contest_user_submissions', contest.key, profile.user.username, problem.code)}}">#</a>
|
||||
</h4>
|
||||
<hr>
|
||||
<table class="lightbox-submissions"><tbody>
|
||||
{% for submission in submissions %}
|
||||
<tr>
|
||||
{% set can_view = submission_layout(submission, profile_id, request.user, editable_problem_ids, completed_problem_ids) %}
|
||||
<td>
|
||||
{% if submission.contest_time %}
|
||||
{{submission.contest_time}}
|
||||
{% else %}
|
||||
{% trans time=submission.date|date(_("N j, Y, g:i a")) %}
|
||||
{{ time }}
|
||||
{% endtrans %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="case-{{submission.result}}" style="margin-left: 1em">{{submission.display_point}}</td>
|
||||
<td class="case-{{submission.result}}" style="margin-left: 1em">({{submission.short_status}})</td>
|
||||
<td>
|
||||
[{{_('pretests') if submission.contest.is_pretest else _('main tests')}}]
|
||||
</td>
|
||||
{% if can_view %}
|
||||
<td>
|
||||
→ <a href="{{url('submission_status', submission.id)}}">{{submission.id}}</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody></table>
|
Loading…
Reference in a new issue