965 lines
No EOL
40 KiB
Python
965 lines
No EOL
40 KiB
Python
import json
|
|
import math
|
|
from calendar import Calendar, SUNDAY
|
|
from collections import defaultdict, namedtuple
|
|
from datetime import date, datetime, time, timedelta
|
|
from functools import partial
|
|
from itertools import chain
|
|
from operator import attrgetter, itemgetter
|
|
|
|
from django import forms
|
|
from django.conf import settings
|
|
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
|
from django.core.cache import cache
|
|
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
|
|
from django.db import IntegrityError
|
|
from django.db.models import Case, Count, F, FloatField, IntegerField, Max, Min, Q, Sum, Value, When
|
|
from django.db.models.expressions import CombinedExpression
|
|
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect, JsonResponse, HttpResponseNotAllowed
|
|
from django.shortcuts import get_object_or_404, render
|
|
from django.template.defaultfilters import date as date_filter
|
|
from django.urls import reverse, reverse_lazy
|
|
from django.utils import timezone
|
|
from django.utils.functional import cached_property
|
|
from django.utils.html import format_html, escape
|
|
from django.utils.safestring import mark_safe
|
|
from django.utils.timezone import make_aware
|
|
from django.utils.translation import gettext as _, gettext_lazy
|
|
from django.views.generic import ListView, TemplateView
|
|
from django.views.generic.detail import BaseDetailView, DetailView, SingleObjectMixin, View
|
|
|
|
from judge import event_poster as event
|
|
from judge.comments import CommentedDetailView
|
|
from judge.forms import ContestCloneForm
|
|
from judge.models import Contest, ContestMoss, ContestParticipation, ContestProblem, ContestTag, \
|
|
Organization, Problem, Profile, Submission, ProblemClarification
|
|
from judge.tasks import run_moss
|
|
from judge.utils.celery import redirect_to_task_status
|
|
from judge.utils.opengraph import generate_opengraph
|
|
from judge.utils.problems import _get_result_data
|
|
from judge.utils.ranker import ranker
|
|
from judge.utils.stats import get_bar_chart, get_pie_chart, get_histogram
|
|
from judge.utils.views import DiggPaginatorMixin, QueryStringSortMixin, SingleObjectFormView, TitleMixin, \
|
|
generic_message
|
|
from judge.widgets import HeavyPreviewPageDownWidget
|
|
|
|
__all__ = ['ContestList', 'ContestDetail', 'ContestRanking', 'ContestJoin', 'ContestLeave', 'ContestCalendar',
|
|
'ContestClone', 'ContestStats', 'ContestMossView', 'ContestMossDelete', 'contest_ranking_ajax',
|
|
'ContestParticipationList', 'ContestParticipationDisqualify', 'get_contest_ranking_list',
|
|
'base_contest_ranking_list', 'ContestClarificationView', 'update_contest_mode']
|
|
|
|
|
|
def _find_contest(request, key, private_check=True):
|
|
try:
|
|
contest = Contest.objects.get(key=key)
|
|
if private_check and not contest.is_accessible_by(request.user):
|
|
raise ObjectDoesNotExist()
|
|
except ObjectDoesNotExist:
|
|
return generic_message(request, _('No such contest'),
|
|
_('Could not find a contest with the key "%s".') % key, status=404), False
|
|
return contest, True
|
|
|
|
|
|
class ContestListMixin(object):
|
|
def get_queryset(self):
|
|
return Contest.get_visible_contests(self.request.user)
|
|
|
|
|
|
class ContestList(QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView):
|
|
model = Contest
|
|
paginate_by = 20
|
|
template_name = 'contest/list.html'
|
|
title = gettext_lazy('Contests')
|
|
context_object_name = 'past_contests'
|
|
all_sorts = frozenset(('name', 'user_count', 'start_time'))
|
|
default_desc = frozenset(('name', 'user_count'))
|
|
default_sort = '-start_time'
|
|
|
|
@cached_property
|
|
def _now(self):
|
|
return timezone.now()
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
self.contest_query = None
|
|
self.org_query = []
|
|
|
|
if 'orgs' in self.request.GET:
|
|
try:
|
|
self.org_query = list(map(int, request.GET.getlist('orgs')))
|
|
except ValueError:
|
|
pass
|
|
|
|
return super(ContestList, self).get(request, *args, **kwargs)
|
|
|
|
def _get_queryset(self):
|
|
queryset = super(ContestList, self).get_queryset() \
|
|
.prefetch_related('tags', 'organizations', 'authors', 'curators', 'testers')
|
|
|
|
if 'contest' in self.request.GET:
|
|
self.contest_query = query = ' '.join(self.request.GET.getlist('contest')).strip()
|
|
if query:
|
|
queryset = queryset.filter(
|
|
Q(key__icontains=query) | Q(name__icontains=query))
|
|
if self.org_query:
|
|
queryset = queryset.filter(organizations__in=self.org_query)
|
|
|
|
return queryset
|
|
|
|
def get_queryset(self):
|
|
return self._get_queryset().order_by(self.order, 'key').filter(end_time__lt=self._now)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(ContestList, self).get_context_data(**kwargs)
|
|
present, active, future = [], [], []
|
|
for contest in self._get_queryset().exclude(end_time__lt=self._now):
|
|
if contest.start_time > self._now:
|
|
future.append(contest)
|
|
else:
|
|
present.append(contest)
|
|
|
|
if self.request.user.is_authenticated:
|
|
for participation in ContestParticipation.objects.filter(virtual=0, user=self.request.profile,
|
|
contest_id__in=present) \
|
|
.select_related('contest') \
|
|
.prefetch_related('contest__authors', 'contest__curators', 'contest__testers')\
|
|
.annotate(key=F('contest__key')):
|
|
if not participation.ended:
|
|
active.append(participation)
|
|
present.remove(participation.contest)
|
|
|
|
active.sort(key=attrgetter('end_time', 'key'))
|
|
present.sort(key=attrgetter('end_time', 'key'))
|
|
future.sort(key=attrgetter('start_time'))
|
|
context['active_participations'] = active
|
|
context['current_contests'] = present
|
|
context['future_contests'] = future
|
|
context['now'] = self._now
|
|
context['first_page_href'] = '.'
|
|
context['contest_query'] = self.contest_query
|
|
context['org_query'] = self.org_query
|
|
context['organizations'] = Organization.objects.all()
|
|
context.update(self.get_sort_context())
|
|
context.update(self.get_sort_paginate_context())
|
|
return context
|
|
|
|
|
|
class PrivateContestError(Exception):
|
|
def __init__(self, name, is_private, is_organization_private, orgs):
|
|
self.name = name
|
|
self.is_private = is_private
|
|
self.is_organization_private = is_organization_private
|
|
self.orgs = orgs
|
|
|
|
|
|
class ContestMixin(object):
|
|
context_object_name = 'contest'
|
|
model = Contest
|
|
slug_field = 'key'
|
|
slug_url_kwarg = 'contest'
|
|
|
|
@cached_property
|
|
def is_editor(self):
|
|
if not self.request.user.is_authenticated:
|
|
return False
|
|
return self.request.profile.id in self.object.editor_ids
|
|
|
|
@cached_property
|
|
def is_tester(self):
|
|
if not self.request.user.is_authenticated:
|
|
return False
|
|
return self.request.profile.id in self.object.tester_ids
|
|
|
|
@cached_property
|
|
def can_edit(self):
|
|
return self.object.is_editable_by(self.request.user)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(ContestMixin, self).get_context_data(**kwargs)
|
|
if self.request.user.is_authenticated:
|
|
try:
|
|
context['live_participation'] = (
|
|
self.request.profile.contest_history.get(
|
|
contest=self.object,
|
|
virtual=ContestParticipation.LIVE,
|
|
)
|
|
)
|
|
except ContestParticipation.DoesNotExist:
|
|
context['live_participation'] = None
|
|
context['has_joined'] = False
|
|
else:
|
|
context['has_joined'] = True
|
|
else:
|
|
context['live_participation'] = None
|
|
context['has_joined'] = False
|
|
|
|
context['now'] = timezone.now()
|
|
context['is_editor'] = self.is_editor
|
|
context['is_tester'] = self.is_tester
|
|
context['can_edit'] = self.can_edit
|
|
|
|
if not self.object.og_image or not self.object.summary:
|
|
metadata = generate_opengraph('generated-meta-contest:%d' % self.object.id,
|
|
self.object.description, 'contest')
|
|
context['meta_description'] = self.object.summary or metadata[0]
|
|
context['og_image'] = self.object.og_image or metadata[1]
|
|
context['has_moss_api_key'] = settings.MOSS_API_KEY is not None
|
|
context['logo_override_image'] = self.object.logo_override_image
|
|
if not context['logo_override_image'] and self.object.organizations.count() == 1:
|
|
context['logo_override_image'] = self.object.organizations.first().logo_override_image
|
|
|
|
return context
|
|
|
|
def get_object(self, queryset=None):
|
|
contest = super(ContestMixin, self).get_object(queryset)
|
|
profile = self.request.profile
|
|
|
|
if (profile is not None and
|
|
ContestParticipation.objects.filter(id=profile.current_contest_id, contest_id=contest.id).exists()):
|
|
return contest
|
|
|
|
try:
|
|
contest.access_check(self.request.user)
|
|
except Contest.PrivateContest:
|
|
raise PrivateContestError(contest.name, contest.is_private, contest.is_organization_private,
|
|
contest.organizations.all())
|
|
except Contest.Inaccessible:
|
|
raise Http404()
|
|
else:
|
|
return contest
|
|
|
|
if contest.is_private or contest.is_organization_private:
|
|
private_contest_error = PrivateContestError(contest.name, contest.is_private,
|
|
contest.is_organization_private, contest.organizations.all())
|
|
if profile is None:
|
|
raise private_contest_error
|
|
if user.has_perm('judge.edit_all_contest'):
|
|
return contest
|
|
if not (contest.is_organization_private and
|
|
contest.organizations.filter(id__in=profile.organizations.all()).exists()) and \
|
|
not (contest.is_private and contest.private_contestants.filter(id=profile.id).exists()):
|
|
raise private_contest_error
|
|
|
|
return contest
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
try:
|
|
return super(ContestMixin, self).dispatch(request, *args, **kwargs)
|
|
except Http404:
|
|
key = kwargs.get(self.slug_url_kwarg, None)
|
|
if key:
|
|
return generic_message(request, _('No such contest'),
|
|
_('Could not find a contest with the key "%s".') % key)
|
|
else:
|
|
return generic_message(request, _('No such contest'),
|
|
_('Could not find such contest.'))
|
|
except PrivateContestError as e:
|
|
return render(request, 'contest/private.html', {
|
|
'error': e, 'title': _('Access to contest "%s" denied') % e.name,
|
|
}, status=403)
|
|
|
|
|
|
class ContestDetail(ContestMixin, TitleMixin, CommentedDetailView):
|
|
template_name = 'contest/contest.html'
|
|
|
|
def get_comment_page(self):
|
|
return 'c:%s' % self.object.key
|
|
|
|
def get_title(self):
|
|
return self.object.name
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(ContestDetail, self).get_context_data(**kwargs)
|
|
context['contest_problems'] = Problem.objects.filter(contests__contest=self.object) \
|
|
.order_by('contests__order').defer('description') \
|
|
.annotate(has_public_editorial=Sum(Case(When(solution__is_public=True, then=1),
|
|
default=0, output_field=IntegerField()))) \
|
|
.add_i18n_name(self.request.LANGUAGE_CODE)
|
|
return context
|
|
|
|
|
|
class ContestClone(ContestMixin, PermissionRequiredMixin, TitleMixin, SingleObjectFormView):
|
|
title = _('Clone Contest')
|
|
template_name = 'contest/clone.html'
|
|
form_class = ContestCloneForm
|
|
permission_required = 'judge.clone_contest'
|
|
|
|
def form_valid(self, form):
|
|
contest = self.object
|
|
|
|
tags = contest.tags.all()
|
|
organizations = contest.organizations.all()
|
|
private_contestants = contest.private_contestants.all()
|
|
view_contest_scoreboard = contest.view_contest_scoreboard.all()
|
|
contest_problems = contest.contest_problems.all()
|
|
|
|
contest.pk = None
|
|
contest.is_visible = False
|
|
contest.user_count = 0
|
|
contest.key = form.cleaned_data['key']
|
|
contest.save()
|
|
|
|
contest.tags.set(tags)
|
|
contest.organizations.set(organizations)
|
|
contest.private_contestants.set(private_contestants)
|
|
contest.view_contest_scoreboard.set(view_contest_scoreboard)
|
|
contest.authors.add(self.request.profile)
|
|
|
|
for problem in contest_problems:
|
|
problem.contest = contest
|
|
problem.pk = None
|
|
ContestProblem.objects.bulk_create(contest_problems)
|
|
|
|
return HttpResponseRedirect(reverse('admin:judge_contest_change', args=(contest.id,)))
|
|
|
|
|
|
class ContestAccessDenied(Exception):
|
|
pass
|
|
|
|
|
|
class ContestAccessCodeForm(forms.Form):
|
|
access_code = forms.CharField(max_length=255)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(ContestAccessCodeForm, self).__init__(*args, **kwargs)
|
|
self.fields['access_code'].widget.attrs.update({'autocomplete': 'off'})
|
|
|
|
|
|
class ContestJoin(LoginRequiredMixin, ContestMixin, BaseDetailView):
|
|
def get(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
return self.ask_for_access_code()
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
try:
|
|
return self.join_contest(request)
|
|
except ContestAccessDenied:
|
|
if request.POST.get('access_code'):
|
|
return self.ask_for_access_code(ContestAccessCodeForm(request.POST))
|
|
else:
|
|
return HttpResponseRedirect(request.path)
|
|
|
|
def join_contest(self, request, access_code=None):
|
|
contest = self.object
|
|
|
|
if not contest.can_join and not (self.is_editor or self.is_tester):
|
|
return generic_message(request, _('Contest not ongoing'),
|
|
_('"%s" is not currently ongoing.') % contest.name)
|
|
|
|
profile = request.profile
|
|
if profile.current_contest is not None:
|
|
return generic_message(request, _('Already in contest'),
|
|
_('You are already in a contest: "%s".') % profile.current_contest.contest.name)
|
|
|
|
if not request.user.is_superuser and contest.banned_users.filter(id=profile.id).exists():
|
|
return generic_message(request, _('Banned from joining'),
|
|
_('You have been declared persona non grata for this contest. '
|
|
'You are permanently barred from joining this contest.'))
|
|
|
|
requires_access_code = (not self.can_edit and contest.access_code and access_code != contest.access_code)
|
|
if contest.ended:
|
|
if requires_access_code:
|
|
raise ContestAccessDenied()
|
|
|
|
while True:
|
|
virtual_id = max((ContestParticipation.objects.filter(contest=contest, user=profile)
|
|
.aggregate(virtual_id=Max('virtual'))['virtual_id'] or 0) + 1, 1)
|
|
try:
|
|
participation = ContestParticipation.objects.create(
|
|
contest=contest, user=profile, virtual=virtual_id,
|
|
real_start=timezone.now(),
|
|
)
|
|
# There is obviously a race condition here, so we keep trying until we win the race.
|
|
except IntegrityError:
|
|
pass
|
|
else:
|
|
break
|
|
else:
|
|
SPECTATE = ContestParticipation.SPECTATE
|
|
LIVE = ContestParticipation.LIVE
|
|
try:
|
|
participation = ContestParticipation.objects.get(
|
|
contest=contest, user=profile, virtual=(SPECTATE if self.is_editor or self.is_tester else LIVE),
|
|
)
|
|
except ContestParticipation.DoesNotExist:
|
|
if requires_access_code:
|
|
raise ContestAccessDenied()
|
|
|
|
participation = ContestParticipation.objects.create(
|
|
contest=contest, user=profile, virtual=(SPECTATE if self.is_editor or self.is_tester else LIVE),
|
|
real_start=timezone.now(),
|
|
)
|
|
else:
|
|
if participation.ended:
|
|
participation = ContestParticipation.objects.get_or_create(
|
|
contest=contest, user=profile, virtual=SPECTATE,
|
|
defaults={'real_start': timezone.now()},
|
|
)[0]
|
|
|
|
profile.current_contest = participation
|
|
profile.save()
|
|
contest._updating_stats_only = True
|
|
contest.update_user_count()
|
|
return HttpResponseRedirect(reverse('problem_list'))
|
|
|
|
def ask_for_access_code(self, form=None):
|
|
contest = self.object
|
|
wrong_code = False
|
|
if form:
|
|
if form.is_valid():
|
|
if form.cleaned_data['access_code'] == contest.access_code:
|
|
return self.join_contest(self.request, form.cleaned_data['access_code'])
|
|
wrong_code = True
|
|
else:
|
|
form = ContestAccessCodeForm()
|
|
return render(self.request, 'contest/access_code.html', {
|
|
'form': form, 'wrong_code': wrong_code,
|
|
'title': _('Enter access code for "%s"') % contest.name,
|
|
})
|
|
|
|
|
|
class ContestLeave(LoginRequiredMixin, ContestMixin, BaseDetailView):
|
|
def post(self, request, *args, **kwargs):
|
|
contest = self.get_object()
|
|
|
|
profile = request.profile
|
|
if profile.current_contest is None or profile.current_contest.contest_id != contest.id:
|
|
return generic_message(request, _('No such contest'),
|
|
_('You are not in contest "%s".') % contest.key, 404)
|
|
|
|
profile.remove_contest()
|
|
request.session['contest_mode'] = True # reset contest_mode
|
|
return HttpResponseRedirect(reverse('contest_view', args=(contest.key,)))
|
|
|
|
|
|
ContestDay = namedtuple('ContestDay', 'date weekday is_pad is_today starts ends oneday')
|
|
|
|
|
|
class ContestCalendar(TitleMixin, ContestListMixin, TemplateView):
|
|
firstweekday = SUNDAY
|
|
weekday_classes = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
|
|
template_name = 'contest/calendar.html'
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
try:
|
|
self.year = int(kwargs['year'])
|
|
self.month = int(kwargs['month'])
|
|
except (KeyError, ValueError):
|
|
raise ImproperlyConfigured(_('ContestCalendar requires integer year and month'))
|
|
self.today = timezone.now().date()
|
|
return self.render()
|
|
|
|
def render(self):
|
|
context = self.get_context_data()
|
|
return self.render_to_response(context)
|
|
|
|
def get_contest_data(self, start, end):
|
|
end += timedelta(days=1)
|
|
contests = self.get_queryset().filter(Q(start_time__gte=start, start_time__lt=end) |
|
|
Q(end_time__gte=start, end_time__lt=end))
|
|
starts, ends, oneday = (defaultdict(list) for i in range(3))
|
|
for contest in contests:
|
|
start_date = timezone.localtime(contest.start_time).date()
|
|
end_date = timezone.localtime(contest.end_time - timedelta(seconds=1)).date()
|
|
if start_date == end_date:
|
|
oneday[start_date].append(contest)
|
|
else:
|
|
starts[start_date].append(contest)
|
|
ends[end_date].append(contest)
|
|
return starts, ends, oneday
|
|
|
|
def get_table(self):
|
|
calendar = Calendar(self.firstweekday).monthdatescalendar(self.year, self.month)
|
|
starts, ends, oneday = self.get_contest_data(make_aware(datetime.combine(calendar[0][0], time.min)),
|
|
make_aware(datetime.combine(calendar[-1][-1], time.min)))
|
|
return [[ContestDay(
|
|
date=date, weekday=self.weekday_classes[weekday], is_pad=date.month != self.month,
|
|
is_today=date == self.today, starts=starts[date], ends=ends[date], oneday=oneday[date],
|
|
) for weekday, date in enumerate(week)] for week in calendar]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(ContestCalendar, self).get_context_data(**kwargs)
|
|
|
|
try:
|
|
month = date(self.year, self.month, 1)
|
|
except ValueError:
|
|
raise Http404()
|
|
else:
|
|
context['title'] = _('Contests in %(month)s') % {'month': date_filter(month, _("F Y"))}
|
|
|
|
dates = Contest.objects.aggregate(min=Min('start_time'), max=Max('end_time'))
|
|
min_month = (self.today.year, self.today.month)
|
|
if dates['min'] is not None:
|
|
min_month = dates['min'].year, dates['min'].month
|
|
max_month = (self.today.year, self.today.month)
|
|
if dates['max'] is not None:
|
|
max_month = max((dates['max'].year, dates['max'].month), (self.today.year, self.today.month))
|
|
|
|
month = (self.year, self.month)
|
|
if month < min_month or month > max_month:
|
|
# 404 is valid because it merely declares the lack of existence, without any reason
|
|
raise Http404()
|
|
|
|
context['now'] = timezone.now()
|
|
context['calendar'] = self.get_table()
|
|
context['curr_month'] = date(self.year, self.month, 1)
|
|
|
|
if month > min_month:
|
|
context['prev_month'] = date(self.year - (self.month == 1), 12 if self.month == 1 else self.month - 1, 1)
|
|
else:
|
|
context['prev_month'] = None
|
|
|
|
if month < max_month:
|
|
context['next_month'] = date(self.year + (self.month == 12), 1 if self.month == 12 else self.month + 1, 1)
|
|
else:
|
|
context['next_month'] = None
|
|
return context
|
|
|
|
|
|
class CachedContestCalendar(ContestCalendar):
|
|
def render(self):
|
|
key = 'contest_cal:%d:%d' % (self.year, self.month)
|
|
cached = cache.get(key)
|
|
if cached is not None:
|
|
return HttpResponse(cached)
|
|
response = super(CachedContestCalendar, self).render()
|
|
response.render()
|
|
cached.set(key, response.content)
|
|
return response
|
|
|
|
|
|
class ContestStats(TitleMixin, ContestMixin, DetailView):
|
|
template_name = 'contest/stats.html'
|
|
POINT_BIN = 10 # in point distribution
|
|
|
|
def get_title(self):
|
|
return _('%s Statistics') % self.object.name
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
if not (self.object.ended or self.can_edit):
|
|
raise Http404()
|
|
|
|
queryset = Submission.objects.filter(contest_object=self.object)
|
|
|
|
ac_count = Count(Case(When(result='AC', then=Value(1)), output_field=IntegerField()))
|
|
ac_rate = CombinedExpression(ac_count / Count('problem'), '*', Value(100.0), output_field=FloatField())
|
|
|
|
status_count_queryset = list(
|
|
queryset.values('problem__code', 'result').annotate(count=Count('result'))
|
|
.values_list('problem__code', 'result', 'count'),
|
|
)
|
|
labels, codes = [], []
|
|
contest_problems = self.object.contest_problems.order_by('order').values_list('problem__name', 'problem__code')
|
|
if contest_problems:
|
|
labels, codes = zip(*contest_problems)
|
|
num_problems = len(labels)
|
|
status_counts = [[] for i in range(num_problems)]
|
|
for problem_code, result, count in status_count_queryset:
|
|
if problem_code in codes:
|
|
status_counts[codes.index(problem_code)].append((result, count))
|
|
|
|
result_data = defaultdict(partial(list, [0] * num_problems))
|
|
for i in range(num_problems):
|
|
for category in _get_result_data(defaultdict(int, status_counts[i]))['categories']:
|
|
result_data[category['code']][i] = category['count']
|
|
|
|
problem_points = [[] for _ in range(num_problems)]
|
|
point_count_queryset = list(queryset.values('problem__code', 'contest__points', 'contest__problem__points')
|
|
.annotate(count=Count('contest__points'))
|
|
.order_by('problem__code', 'contest__points')
|
|
.values_list('problem__code', 'contest__points', 'contest__problem__points', 'count'))
|
|
counter = [[0 for _ in range(self.POINT_BIN + 1)] for _ in range(num_problems)]
|
|
for problem_code, point, max_point, count in point_count_queryset:
|
|
if (point == None) or (problem_code not in codes): continue
|
|
problem_idx = codes.index(problem_code)
|
|
bin_idx = math.floor(point * self.POINT_BIN / max_point)
|
|
counter[problem_idx][bin_idx] += count
|
|
for i in range(num_problems):
|
|
problem_points[i] = [(j * 100 / self.POINT_BIN, counter[i][j])
|
|
for j in range(len(counter[i]))]
|
|
|
|
stats = {
|
|
'problem_status_count': {
|
|
'labels': labels,
|
|
'datasets': [
|
|
{
|
|
'label': name,
|
|
'backgroundColor': settings.DMOJ_STATS_SUBMISSION_RESULT_COLORS[name],
|
|
'data': data,
|
|
}
|
|
for name, data in result_data.items()
|
|
],
|
|
},
|
|
'problem_ac_rate': get_bar_chart(
|
|
queryset.values('contest__problem__order', 'problem__name').annotate(ac_rate=ac_rate)
|
|
.order_by('contest__problem__order').values_list('problem__name', 'ac_rate'),
|
|
),
|
|
'problem_point': [get_histogram(problem_points[i])
|
|
for i in range(num_problems)
|
|
],
|
|
'language_count': get_pie_chart(
|
|
queryset.values('language__name').annotate(count=Count('language__name'))
|
|
.filter(count__gt=0).order_by('-count').values_list('language__name', 'count'),
|
|
),
|
|
'language_ac_rate': get_bar_chart(
|
|
queryset.values('language__name').annotate(ac_rate=ac_rate)
|
|
.filter(ac_rate__gt=0).values_list('language__name', 'ac_rate'),
|
|
),
|
|
}
|
|
|
|
context['stats'] = mark_safe(json.dumps(stats))
|
|
context['problems'] = labels
|
|
return context
|
|
|
|
|
|
ContestRankingProfile = namedtuple(
|
|
'ContestRankingProfile',
|
|
'id user css_class username points cumtime tiebreaker organization participation '
|
|
'participation_rating problem_cells result_cell',
|
|
)
|
|
|
|
BestSolutionData = namedtuple('BestSolutionData', 'code points time state is_pretested')
|
|
|
|
|
|
def make_contest_ranking_profile(contest, participation, contest_problems):
|
|
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,
|
|
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)
|
|
for contest_problem in contest_problems],
|
|
result_cell=contest.format.display_participation_result(participation),
|
|
participation=participation,
|
|
)
|
|
|
|
|
|
def base_contest_ranking_list(contest, problems, queryset):
|
|
return [make_contest_ranking_profile(contest, participation, problems) for participation in
|
|
queryset.select_related('user__user', 'rating').defer('user__about', 'user__organizations__about')]
|
|
|
|
|
|
def contest_ranking_list(contest, problems, queryset=None):
|
|
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'))
|
|
|
|
|
|
def get_contest_ranking_list(request, contest, participation=None, ranking_list=contest_ranking_list,
|
|
show_current_virtual=False, ranker=ranker):
|
|
problems = list(contest.contest_problems.select_related('problem').defer('problem__description').order_by('order'))
|
|
|
|
users = ranker(ranking_list(contest, problems), key=attrgetter('points', 'cumtime', 'tiebreaker'))
|
|
|
|
if show_current_virtual:
|
|
if participation is None and request.user.is_authenticated:
|
|
participation = request.profile.current_contest
|
|
if participation is None or participation.contest_id != contest.id:
|
|
participation = None
|
|
if participation is not None and participation.virtual:
|
|
users = chain([('-', make_contest_ranking_profile(contest, participation, problems))], users)
|
|
return users, problems
|
|
|
|
|
|
def contest_ranking_ajax(request, contest, participation=None):
|
|
contest, exists = _find_contest(request, contest)
|
|
if not exists:
|
|
return HttpResponseBadRequest('Invalid contest', content_type='text/plain')
|
|
|
|
if not contest.can_see_full_scoreboard(request.user):
|
|
raise Http404()
|
|
|
|
queryset = contest.users.filter(virtual__gte=0)
|
|
if request.GET.get('friend') == 'true' and request.profile:
|
|
friends = list(request.profile.get_friends())
|
|
queryset = queryset.filter(user__user__username__in=friends)
|
|
if request.GET.get('virtual') != 'true':
|
|
queryset = queryset.filter(virtual=0)
|
|
|
|
users, problems = get_contest_ranking_list(request, contest, participation,
|
|
ranking_list=partial(contest_ranking_list, queryset=queryset))
|
|
return render(request, 'contest/ranking-table.html', {
|
|
'users': users,
|
|
'problems': problems,
|
|
'contest': contest,
|
|
'has_rating': contest.ratings.exists(),
|
|
'can_edit': contest.is_editable_by(request.user)
|
|
})
|
|
|
|
|
|
class ContestRankingBase(ContestMixin, TitleMixin, DetailView):
|
|
template_name = 'contest/ranking.html'
|
|
tab = None
|
|
|
|
def get_title(self):
|
|
raise NotImplementedError()
|
|
|
|
def get_content_title(self):
|
|
return self.object.name
|
|
|
|
def get_ranking_list(self):
|
|
raise NotImplementedError()
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
if not self.object.can_see_own_scoreboard(self.request.user):
|
|
raise Http404()
|
|
|
|
users, problems = self.get_ranking_list()
|
|
context['users'] = users
|
|
context['problems'] = problems
|
|
context['tab'] = self.tab
|
|
return context
|
|
|
|
|
|
class ContestRanking(ContestRankingBase):
|
|
tab = 'ranking'
|
|
|
|
def get_title(self):
|
|
return _('%s Rankings') % self.object.name
|
|
|
|
def get_ranking_list(self):
|
|
if not self.object.can_see_full_scoreboard(self.request.user):
|
|
queryset = self.object.users.filter(user=self.request.profile, virtual=ContestParticipation.LIVE)
|
|
return get_contest_ranking_list(
|
|
self.request, self.object,
|
|
ranking_list=partial(base_contest_ranking_list, queryset=queryset),
|
|
ranker=lambda users, key: ((_('???'), user) for user in users),
|
|
)
|
|
|
|
return get_contest_ranking_list(self.request, self.object)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['has_rating'] = self.object.ratings.exists()
|
|
return context
|
|
|
|
|
|
class ContestParticipationList(LoginRequiredMixin, ContestRankingBase):
|
|
tab = 'participation'
|
|
|
|
def get_title(self):
|
|
if self.profile == self.request.profile:
|
|
return _('Your participation in %s') % self.object.name
|
|
return _("%s's participation in %s") % (self.profile.username, self.object.name)
|
|
|
|
def get_ranking_list(self):
|
|
if not self.object.can_see_full_scoreboard(self.request.user) and self.profile != self.request.profile:
|
|
raise Http404()
|
|
|
|
queryset = self.object.users.filter(user=self.profile, virtual__gte=0).order_by('-virtual')
|
|
live_link = format_html('<a href="{2}#!{1}">{0}</a>', _('Live'), self.profile.username,
|
|
reverse('contest_ranking', args=[self.object.key]))
|
|
|
|
return get_contest_ranking_list(
|
|
self.request, self.object, show_current_virtual=False,
|
|
ranking_list=partial(base_contest_ranking_list, queryset=queryset),
|
|
ranker=lambda users, key: ((user.participation.virtual or live_link, user) for user in users))
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['has_rating'] = False
|
|
context['now'] = timezone.now()
|
|
context['rank_header'] = _('Participation')
|
|
context['participation_tab'] = True
|
|
return context
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if 'user' in kwargs:
|
|
self.profile = get_object_or_404(Profile, user__username=kwargs['user'])
|
|
else:
|
|
self.profile = self.request.profile
|
|
return super().get(request, *args, **kwargs)
|
|
|
|
|
|
class ContestParticipationDisqualify(ContestMixin, SingleObjectMixin, View):
|
|
def get_object(self, queryset=None):
|
|
contest = super().get_object(queryset)
|
|
if not contest.is_editable_by(self.request.user):
|
|
raise Http404()
|
|
return contest
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
|
|
try:
|
|
participation = self.object.users.get(pk=request.POST.get('participation'))
|
|
except ObjectDoesNotExist:
|
|
pass
|
|
else:
|
|
participation.set_disqualified(not participation.is_disqualified)
|
|
return HttpResponseRedirect(reverse('contest_ranking', args=(self.object.key,)))
|
|
|
|
|
|
class ContestMossMixin(ContestMixin, PermissionRequiredMixin):
|
|
permission_required = 'judge.moss_contest'
|
|
|
|
def get_object(self, queryset=None):
|
|
contest = super().get_object(queryset)
|
|
if settings.MOSS_API_KEY is None or not contest.is_editable_by(self.request.user):
|
|
raise Http404()
|
|
if not contest.is_editable_by(self.request.user):
|
|
raise Http404()
|
|
return contest
|
|
|
|
|
|
class ContestMossView(ContestMossMixin, TitleMixin, DetailView):
|
|
template_name = 'contest/moss.html'
|
|
|
|
def get_title(self):
|
|
return _('%s MOSS Results') % self.object.name
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
problems = list(map(attrgetter('problem'), self.object.contest_problems.order_by('order')
|
|
.select_related('problem')))
|
|
languages = list(map(itemgetter(0), ContestMoss.LANG_MAPPING))
|
|
|
|
results = ContestMoss.objects.filter(contest=self.object)
|
|
moss_results = defaultdict(list)
|
|
for result in results:
|
|
moss_results[result.problem].append(result)
|
|
|
|
for result_list in moss_results.values():
|
|
result_list.sort(key=lambda x: languages.index(x.language))
|
|
|
|
context['languages'] = languages
|
|
context['has_results'] = results.exists()
|
|
context['moss_results'] = [(problem, moss_results[problem]) for problem in problems]
|
|
|
|
return context
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
status = run_moss.delay(self.object.key)
|
|
return redirect_to_task_status(
|
|
status, message=_('Running MOSS for %s...') % (self.object.name,),
|
|
redirect=reverse('contest_moss', args=(self.object.key,)),
|
|
)
|
|
|
|
|
|
class ContestMossDelete(ContestMossMixin, SingleObjectMixin, View):
|
|
def post(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
ContestMoss.objects.filter(contest=self.object).delete()
|
|
return HttpResponseRedirect(reverse('contest_moss', args=(self.object.key,)))
|
|
|
|
|
|
class ContestTagDetailAjax(DetailView):
|
|
model = ContestTag
|
|
slug_field = slug_url_kwarg = 'name'
|
|
context_object_name = 'tag'
|
|
template_name = 'contest/tag-ajax.html'
|
|
|
|
|
|
class ContestTagDetail(TitleMixin, ContestTagDetailAjax):
|
|
template_name = 'contest/tag.html'
|
|
|
|
def get_title(self):
|
|
return _('Contest tag: %s') % self.object.name
|
|
|
|
|
|
class ProblemClarificationForm(forms.Form):
|
|
body = forms.CharField(widget=HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'),
|
|
preview_timeout=1000, hide_preview_button=True))
|
|
|
|
def __init__(self, request, *args, **kwargs):
|
|
self.request = request
|
|
super(ProblemClarificationForm, self).__init__(*args, **kwargs)
|
|
self.fields['body'].widget.attrs.update({'placeholder': _('Issue description')})
|
|
|
|
|
|
class NewContestClarificationView(ContestMixin, TitleMixin, SingleObjectFormView):
|
|
form_class = ProblemClarificationForm
|
|
template_name = 'contest/clarification.html'
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super(NewContestClarificationView, self).get_form_kwargs()
|
|
kwargs['request'] = self.request
|
|
return kwargs
|
|
|
|
def is_accessible(self):
|
|
if not self.request.user.is_authenticated:
|
|
return False
|
|
if not self.request.in_contest:
|
|
return False
|
|
if not self.request.participation.contest == self.get_object():
|
|
return False
|
|
return self.request.user.is_superuser or \
|
|
self.request.profile in self.request.participation.contest.authors.all() or \
|
|
self.request.profile in self.request.participation.contest.curators.all()
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if not self.is_accessible():
|
|
raise Http404()
|
|
return super().get(self, request, *args, **kwargs)
|
|
|
|
def form_valid(self, form):
|
|
problem_code = self.request.POST['problem']
|
|
description = form.cleaned_data['body']
|
|
|
|
clarification = ProblemClarification(description=description)
|
|
clarification.problem = Problem.objects.get(code=problem_code)
|
|
clarification.save()
|
|
|
|
link = reverse('home')
|
|
return HttpResponseRedirect(link)
|
|
|
|
def get_title(self):
|
|
return "New clarification for %s" % self.object.name
|
|
|
|
def get_content_title(self):
|
|
return mark_safe(escape(_('New clarification for %s')) %
|
|
format_html('<a href="{0}">{1}</a>', reverse('problem_detail', args=[self.object.key]),
|
|
self.object.name))
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(NewContestClarificationView, self).get_context_data(**kwargs)
|
|
context['problems'] = ContestProblem.objects.filter(contest=self.object)\
|
|
.order_by('order')
|
|
return context
|
|
|
|
|
|
class ContestClarificationAjax(ContestMixin, DetailView):
|
|
def get(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
if not self.object.is_accessible_by(request.user):
|
|
raise Http404()
|
|
|
|
polling_time = 1 # minute
|
|
last_one_minute = last_five_minutes = timezone.now()-timezone.timedelta(minutes=polling_time)
|
|
|
|
queryset = list(ProblemClarification.objects.filter(
|
|
problem__in=self.object.problems.all(),
|
|
date__gte=last_one_minute
|
|
).values('problem', 'problem__name', 'description'))
|
|
|
|
problems = list(ContestProblem.objects.filter(contest=self.object)\
|
|
.order_by('order').values('problem'))
|
|
problems = [i['problem'] for i in problems]
|
|
for cla in queryset:
|
|
cla['order'] = self.object.get_label_for_problem(problems.index(cla['problem']))
|
|
|
|
return JsonResponse(queryset, safe=False, json_dumps_params={'ensure_ascii': False})
|
|
|
|
|
|
def update_contest_mode(request):
|
|
if not request.is_ajax() or not request.method=='POST':
|
|
return HttpResponseNotAllowed(['POST'])
|
|
|
|
old_mode = request.session.get('contest_mode', True)
|
|
request.session['contest_mode'] = not old_mode
|
|
return HttpResponse() |