Change UI ranking

This commit is contained in:
cuom1999 2022-06-01 00:28:56 -05:00
parent d38342ad43
commit 9b1724cdad
16 changed files with 291 additions and 631 deletions

View file

@ -310,7 +310,6 @@ urlpatterns = [
ticket.NewProblemTicketView.as_view(), ticket.NewProblemTicketView.as_view(),
name="new_problem_ticket", name="new_problem_ticket",
), ),
url(r"^/vote$", problem.Vote.as_view(), name="vote"),
url( url(
r"^/manage/submission", r"^/manage/submission",
include( include(
@ -529,11 +528,18 @@ urlpatterns = [
), ),
), ),
url( url(
r"^/submissions/(?P<user>\w+)/(?P<problem>\w+)/", r"^/submissions/(?P<user>\w+)/(?P<problem>\w+)",
paged_list_view( paged_list_view(
submission.UserContestSubmissions, "contest_user_submissions" 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( url(
r"^/participations$", r"^/participations$",
contests.ContestParticipationList.as_view(), contests.ContestParticipationList.as_view(),

View file

@ -55,7 +55,7 @@ class DefaultContestFormat(BaseContestFormat):
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(
'<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=( state=(
( (
"pretest-" "pretest-"
@ -68,7 +68,7 @@ class DefaultContestFormat(BaseContestFormat):
) )
), ),
url=reverse( url=reverse(
"contest_user_submissions", "contest_user_submissions_ajax",
args=[ args=[
self.contest.key, self.contest.key,
participation.user.user.username, participation.user.user.username,

View file

@ -171,7 +171,8 @@ msgid ""
msgstr "" msgstr ""
"Content-Type: text/plain; charset=utf-8\\n" "Content-Type: text/plain; charset=utf-8\\n"
""") """
)
if self.verbosity > 1: if self.verbosity > 1:
self.stdout.write("processing navigation bar") self.stdout.write("processing navigation bar")
for label in NavigationBar.objects.values_list("label", flat=True): for label in NavigationBar.objects.values_list("label", flat=True):

View file

@ -561,29 +561,6 @@ class Problem(models.Model):
save.alters_data = True 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: class Meta:
permissions = ( permissions = (
("see_private_problem", "See hidden problems"), ("see_private_problem", "See hidden problems"),

View file

@ -644,6 +644,7 @@ class KickUserWidgetView(
LoginRequiredMixin, AdminOrganizationMixin, SingleObjectMixin, View LoginRequiredMixin, AdminOrganizationMixin, SingleObjectMixin, View
): ):
model = Organization model = Organization
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
organization = self.get_object() organization = self.get_object()
try: try:

View file

@ -306,78 +306,9 @@ class ProblemDetail(ProblemMixin, SolvedProblemMixin, CommentedDetailView):
context["meta_description"] = self.object.summary or metadata[0] context["meta_description"] = self.object.summary or metadata[0]
context["og_image"] = self.object.og_image or metadata[1] 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 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): class LatexError(Exception):
pass pass

View file

@ -18,6 +18,7 @@ from django.http import HttpResponseRedirect
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.shortcuts import render from django.shortcuts import render
from django.template.defaultfilters import floatformat
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property 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.raw_sql import join_sql_subquery, use_straight_join
from judge.utils.views import DiggPaginatorMixin from judge.utils.views import DiggPaginatorMixin
from judge.utils.views import TitleMixin from judge.utils.views import TitleMixin
from judge.utils.timedelta import nice_repr
def submission_related(queryset): def submission_related(queryset):
@ -358,7 +360,8 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
) )
if self.selected_statuses: if self.selected_statuses:
queryset = queryset.filter( 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 return queryset
@ -392,9 +395,7 @@ class SubmissionsListBase(DiggPaginatorMixin, TitleMixin, ListView):
hidden_codes = ["SC", "D", "G"] hidden_codes = ["SC", "D", "G"]
if not self.request.user.is_superuser and not self.request.user.is_staff: if not self.request.user.is_superuser and not self.request.user.is_staff:
hidden_codes += ["IE"] hidden_codes += ["IE"]
return [ return [(key, value) for key, value in all_statuses if key not in hidden_codes]
(key, value) for key, value in all_statuses if key not in hidden_codes
]
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(SubmissionsListBase, self).get_context_data(**kwargs) context = super(SubmissionsListBase, self).get_context_data(**kwargs)
@ -782,3 +783,30 @@ class UserContestSubmissions(ForceContestMixin, UserProblemSubmissions):
self.contest.name, self.contest.name,
reverse("contest_view", args=[self.contest.key]), 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

View file

@ -362,3 +362,9 @@ label[for="language"], label[for="status"] {
color: gray; color: gray;
} }
} }
.lightbox-submissions {
td {
padding-right: 0.2em;
}
}

View file

@ -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"> <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 () { $(function () {
$('.leaving-forever').click(function () { $('.leaving-forever').click(function () {
return confirm('{{ _('Are you sure you want to leave?') }}\n' + return confirm('{{ _('Are you sure you want to leave?') }}\n' +
@ -9,5 +147,68 @@
return confirm('{{ _('Are you sure you want to join?') }}\n' + return confirm('{{ _('Are you sure you want to join?') }}\n' +
'{{ _('Joining a contest starts your timer, after which it becomes unstoppable.') }}'); '{{ _('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> </script>

View file

@ -58,7 +58,7 @@
{% block before_point_head %} {% block before_point_head %}
{% for problem in problems %} {% 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) }} {{- contest.get_label_for_problem(loop.index0) }}
<div class="point-denominator">{{ problem.points }}</div> <div class="point-denominator">{{ problem.points }}</div>
</a></th> </a></th>

View file

@ -8,6 +8,8 @@
{% endblock %} {% endblock %}
{% block users_media %} {% block users_media %}
<link href="//cdn.jsdelivr.net/npm/featherlight@1.7.14/release/featherlight.min.css" type="text/css" rel="stylesheet" />
<style> <style>
#content-left { #content-left {
overflow-x: auto; overflow-x: auto;
@ -130,6 +132,12 @@
color: gray !important; color: gray !important;
font-weight: 600; font-weight: 600;
} }
.featherlight-content {
border-radius: 10px;
height: 80%;
width: 60%;
}
</style> </style>
{% if has_rating %} {% if has_rating %}
@ -227,215 +235,6 @@
}); });
</script> </script>
{% endif %} {% 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" %} {% include "contest/media-js.html" %}
{% endblock %} {% endblock %}

View file

@ -346,9 +346,6 @@
{% endblock %} {% endblock %}
{% block description %} {% 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 %} {% if contest_problem and contest_problem.contest.use_clarifications and has_clarifications %}
<div id="clarification_header_container"> <div id="clarification_header_container">
<i class="fa fa-question-circle"></i> <i class="fa fa-question-circle"></i>
@ -374,9 +371,6 @@
{% endblock %} {% endblock %}
{% block post_description_end %} {% 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 %} {% if request.user.is_authenticated and not request.profile.mute %}
<a href="{{ url('new_problem_ticket', problem.code) }}" class="clarify"> <a href="{{ url('new_problem_ticket', problem.code) }}" class="clarify">
<i class="fa fa-flag" style="margin-right:0.5em"></i> <i class="fa fa-flag" style="margin-right:0.5em"></i>

View file

@ -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 %}

View file

@ -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>

View file

@ -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>

View 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>