NDOJ/judge/views/contests.py

1663 lines
55 KiB
Python
Raw Permalink Normal View History

2022-11-07 21:39:10 +00:00
from copy import deepcopy
2020-01-21 06:35:58 +00:00
import json
2021-06-02 00:20:39 +00:00
import math
2020-01-21 06:35:58 +00:00
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
2022-05-14 17:57:27 +00:00
from django.db.models import (
Case,
Count,
F,
FloatField,
IntegerField,
Max,
Min,
Q,
Sum,
Value,
When,
)
2023-10-06 08:54:37 +00:00
from django.dispatch import receiver
2020-01-21 06:35:58 +00:00
from django.db.models.expressions import CombinedExpression
2022-05-14 17:57:27 +00:00
from django.http import (
Http404,
HttpResponse,
HttpResponseBadRequest,
HttpResponseRedirect,
JsonResponse,
HttpResponseNotAllowed,
)
2020-01-21 06:35:58 +00:00
from django.shortcuts import get_object_or_404, render
from django.template.defaultfilters import date as date_filter
2021-07-19 01:22:44 +00:00
from django.urls import reverse, reverse_lazy
2020-01-21 06:35:58 +00:00
from django.utils import timezone
from django.utils.functional import cached_property
2021-07-19 01:22:44 +00:00
from django.utils.html import format_html, escape
2020-01-21 06:35:58 +00:00
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
2022-05-14 17:57:27 +00:00
from django.views.generic.detail import (
BaseDetailView,
DetailView,
SingleObjectMixin,
View,
)
2020-01-21 06:35:58 +00:00
from judge import event_poster as event
2024-04-13 22:02:54 +00:00
from judge.views.comment import CommentedDetailView
2020-01-21 06:35:58 +00:00
from judge.forms import ContestCloneForm
2022-05-14 17:57:27 +00:00
from judge.models import (
Contest,
ContestMoss,
ContestParticipation,
ContestProblem,
ContestTag,
Organization,
Problem,
Profile,
Submission,
ContestProblemClarification,
2023-10-06 08:54:37 +00:00
ContestsSummary,
2024-05-30 07:59:22 +00:00
OfficialContestCategory,
OfficialContestLocation,
2022-05-14 17:57:27 +00:00
)
2020-01-21 06:35:58 +00:00
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
2021-06-02 00:20:39 +00:00
from judge.utils.stats import get_bar_chart, get_pie_chart, get_histogram
2022-05-14 17:57:27 +00:00
from judge.utils.views import (
DiggPaginatorMixin,
QueryStringSortMixin,
SingleObjectFormView,
TitleMixin,
generic_message,
)
2021-07-19 01:22:44 +00:00
from judge.widgets import HeavyPreviewPageDownWidget
2022-11-17 00:48:32 +00:00
from judge.views.pagevote import PageVoteDetailView
2022-11-17 19:17:45 +00:00
from judge.views.bookmark import BookMarkDetailView
2022-11-17 00:48:32 +00:00
2020-01-21 06:35:58 +00:00
2022-05-14 17:57:27 +00:00
__all__ = [
"ContestList",
"ContestDetail",
"ContestRanking",
"ContestJoin",
"ContestLeave",
"ContestCalendar",
"ContestClone",
"ContestStats",
"ContestMossView",
"ContestMossDelete",
"ContestParticipationList",
"ContestParticipationDisqualify",
"get_contest_ranking_list",
"base_contest_ranking_list",
"ContestClarificationView",
"update_contest_mode",
2024-05-30 07:59:22 +00:00
"OfficialContestList",
2022-05-14 17:57:27 +00:00
]
2020-01-21 06:35:58 +00:00
2023-09-17 04:55:24 +00:00
def _find_contest(request, key):
2020-01-21 06:35:58 +00:00
try:
contest = Contest.objects.get(key=key)
2023-09-17 04:55:24 +00:00
private_check = not contest.public_scoreboard
2020-01-21 06:35:58 +00:00
if private_check and not contest.is_accessible_by(request.user):
raise ObjectDoesNotExist()
except ObjectDoesNotExist:
2022-05-14 17:57:27 +00:00
return (
generic_message(
request,
_("No such contest"),
_('Could not find a contest with the key "%s".') % key,
status=404,
),
False,
)
2020-01-21 06:35:58 +00:00
return contest, True
class ContestListMixin(object):
2024-05-30 07:59:22 +00:00
official = False
2020-01-21 06:35:58 +00:00
def get_queryset(self):
2024-05-30 07:59:22 +00:00
q = Contest.get_visible_contests(self.request.user)
if self.official:
q = q.filter(official__isnull=False).select_related(
"official", "official__category", "official__location"
)
else:
q = q.filter(official__isnull=True)
2024-05-30 07:59:22 +00:00
return q
2020-01-21 06:35:58 +00:00
2022-05-14 17:57:27 +00:00
class ContestList(
QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView
):
2020-01-21 06:35:58 +00:00
model = Contest
2023-09-08 16:22:57 +00:00
paginate_by = 10
2022-05-14 17:57:27 +00:00
template_name = "contest/list.html"
title = gettext_lazy("Contests")
all_sorts = frozenset(("name", "user_count", "start_time"))
default_desc = frozenset(("name", "user_count"))
context_object_name = "contests"
2023-11-17 02:35:15 +00:00
def get_default_sort_order(self, request):
2023-11-17 07:11:54 +00:00
if request.GET.get("contest") and settings.ENABLE_FTS:
2023-11-17 02:35:15 +00:00
return "-relevance"
if self.current_tab == "future":
return "start_time"
2023-11-17 02:35:15 +00:00
return "-start_time"
2022-05-14 17:57:27 +00:00
2020-01-21 06:35:58 +00:00
@cached_property
def _now(self):
return timezone.now()
def GET_with_session(self, request, key):
if not request.GET.get(key):
return request.session.get(key, False)
return request.GET.get(key, None) == "1"
2024-05-30 07:59:22 +00:00
def setup_contest_list(self, request):
self.contest_query = request.GET.get("contest", "")
2024-05-30 07:59:22 +00:00
self.hide_organization_contests = 0
if self.GET_with_session(request, "hide_organization_contests"):
self.hide_organization_contests = 1
2021-02-19 08:02:12 +00:00
2024-05-30 07:59:22 +00:00
self.org_query = []
if request.GET.get("orgs") and request.profile:
2021-02-19 08:02:12 +00:00
try:
2022-05-14 17:57:27 +00:00
self.org_query = list(map(int, request.GET.getlist("orgs")))
2024-05-30 07:59:22 +00:00
if not request.user.is_superuser:
2023-02-08 10:22:43 +00:00
self.org_query = [
i
for i in self.org_query
if i
2023-11-17 07:11:54 +00:00
in set(
2024-05-30 07:59:22 +00:00
request.profile.organizations.values_list("id", flat=True)
2023-02-13 03:35:48 +00:00
)
2023-02-08 10:22:43 +00:00
]
2021-02-19 08:02:12 +00:00
except ValueError:
pass
2024-05-30 07:59:22 +00:00
def get(self, request, *args, **kwargs):
default_tab = "active"
if not self.request.user.is_authenticated:
default_tab = "current"
self.current_tab = self.request.GET.get("tab", default_tab)
self.setup_contest_list(request)
2021-02-19 08:02:12 +00:00
return super(ContestList, self).get(request, *args, **kwargs)
2024-05-06 03:47:57 +00:00
def post(self, request, *args, **kwargs):
2024-05-30 07:59:22 +00:00
to_update = ("hide_organization_contests",)
2024-05-06 03:47:57 +00:00
for key in to_update:
if key in request.GET:
val = request.GET.get(key) == "1"
request.session[key] = val
else:
request.session[key] = False
return HttpResponseRedirect(request.get_full_path())
2024-05-30 07:59:22 +00:00
def extra_queryset_filters(self, queryset):
return queryset
2020-01-21 06:35:58 +00:00
def _get_queryset(self):
2022-05-14 17:57:27 +00:00
queryset = (
super(ContestList, self)
.get_queryset()
2024-04-25 06:58:47 +00:00
.prefetch_related("tags", "organizations")
2022-05-14 17:57:27 +00:00
)
2024-05-30 07:59:22 +00:00
if self.contest_query:
substr_queryset = queryset.filter(
Q(key__icontains=self.contest_query)
| Q(name__icontains=self.contest_query)
)
if settings.ENABLE_FTS:
queryset = (
queryset.search(self.contest_query).extra(order_by=["-relevance"])
| substr_queryset
2022-05-14 17:57:27 +00:00
)
2024-05-30 07:59:22 +00:00
else:
queryset = substr_queryset
2023-01-24 02:36:44 +00:00
if not self.org_query and self.request.organization:
self.org_query = [self.request.organization.id]
2024-05-30 07:59:22 +00:00
if self.hide_organization_contests:
2023-02-08 19:12:23 +00:00
queryset = queryset.filter(organizations=None)
2021-02-19 08:02:12 +00:00
if self.org_query:
queryset = queryset.filter(organizations__in=self.org_query)
2024-05-30 07:59:22 +00:00
queryset = self.extra_queryset_filters(queryset)
2021-02-19 08:02:12 +00:00
return queryset
2020-01-21 06:35:58 +00:00
def _get_past_contests_queryset(self):
2022-05-14 17:57:27 +00:00
return (
self._get_queryset()
.filter(end_time__lt=self._now)
.order_by(self.order, "key")
)
def _active_participations(self):
return ContestParticipation.objects.filter(
virtual=0,
user=self.request.profile,
contest__start_time__lte=self._now,
contest__end_time__gte=self._now,
)
@cached_property
def _active_contests_ids(self):
2024-05-27 03:26:38 +00:00
return [
participation.contest_id
for participation in self._active_participations().select_related("contest")
if not participation.ended
]
def _get_current_contests_queryset(self):
return (
self._get_queryset()
.exclude(id__in=self._active_contests_ids)
.filter(start_time__lte=self._now, end_time__gte=self._now)
.order_by(self.order, "key")
)
def _get_future_contests_queryset(self):
return (
self._get_queryset()
.filter(start_time__gt=self._now)
.order_by(self.order, "key")
2022-05-14 17:57:27 +00:00
)
2020-01-21 06:35:58 +00:00
def _get_active_participations_queryset(self):
active_contests = (
self._get_queryset()
.filter(id__in=self._active_contests_ids)
.order_by(self.order, "key")
)
ordered_ids = list(active_contests.values_list("id", flat=True))
participations = self._active_participations().filter(
contest_id__in=ordered_ids
)
participations = sorted(
participations, key=lambda p: ordered_ids.index(p.contest_id)
)
return participations
def get_queryset(self):
if self.current_tab == "past":
return self._get_past_contests_queryset()
elif self.current_tab == "current":
return self._get_current_contests_queryset()
elif self.current_tab == "future":
return self._get_future_contests_queryset()
else: # Default to active
return self._get_active_participations_queryset()
2020-01-21 06:35:58 +00:00
def get_context_data(self, **kwargs):
context = super(ContestList, self).get_context_data(**kwargs)
context["current_tab"] = self.current_tab
context["current_count"] = self._get_current_contests_queryset().count()
context["future_count"] = self._get_future_contests_queryset().count()
context["active_count"] = len(self._get_active_participations_queryset())
2022-05-14 17:57:27 +00:00
context["now"] = self._now
context["first_page_href"] = "."
context["contest_query"] = self.contest_query
context["org_query"] = self.org_query
2024-05-30 07:59:22 +00:00
context["hide_organization_contests"] = int(self.hide_organization_contests)
2022-05-28 04:28:22 +00:00
if self.request.profile:
2023-11-17 07:11:54 +00:00
context["organizations"] = self.request.profile.organizations.all()
2022-05-28 04:28:22 +00:00
context["page_type"] = "list"
2024-05-30 07:59:22 +00:00
context["selected_order"] = self.request.GET.get("order")
context["all_sort_options"] = [
("start_time", _("Start time (asc.)")),
("-start_time", _("Start time (desc.)")),
("name", _("Name (asc.)")),
("-name", _("Name (desc.)")),
("user_count", _("User count (asc.)")),
("-user_count", _("User count (desc.)")),
]
2021-12-09 19:31:08 +00:00
context.update(self.get_sort_context())
context.update(self.get_sort_paginate_context())
2020-01-21 06:35:58 +00:00
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):
2022-05-14 17:57:27 +00:00
context_object_name = "contest"
2020-01-21 06:35:58 +00:00
model = Contest
2022-05-14 17:57:27 +00:00
slug_field = "key"
slug_url_kwarg = "contest"
2020-01-21 06:35:58 +00:00
@cached_property
2021-05-24 20:00:36 +00:00
def is_editor(self):
if not self.request.user.is_authenticated:
return False
return self.request.profile.id in self.object.editor_ids
2020-01-21 06:35:58 +00:00
2021-05-24 20:00:36 +00:00
@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
2022-05-14 17:57:27 +00:00
2021-05-24 20:00:36 +00:00
@cached_property
def can_edit(self):
return self.object.is_editable_by(self.request.user)
2020-01-21 06:35:58 +00:00
2023-09-17 05:44:07 +00:00
@cached_property
def can_access(self):
return self.object.is_accessible_by(self.request.user)
2023-09-17 04:55:24 +00:00
def should_bypass_access_check(self, contest):
return False
2020-01-21 06:35:58 +00:00
def get_context_data(self, **kwargs):
context = super(ContestMixin, self).get_context_data(**kwargs)
if self.request.user.is_authenticated:
2021-05-24 20:00:36 +00:00
try:
2022-05-14 17:57:27 +00:00
context[
"live_participation"
] = self.request.profile.contest_history.get(
contest=self.object,
virtual=ContestParticipation.LIVE,
2021-05-24 20:00:36 +00:00
)
except ContestParticipation.DoesNotExist:
2022-05-14 17:57:27 +00:00
context["live_participation"] = None
context["has_joined"] = False
2020-01-21 06:35:58 +00:00
else:
2022-05-14 17:57:27 +00:00
context["has_joined"] = True
2020-01-21 06:35:58 +00:00
else:
2022-05-14 17:57:27 +00:00
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
2023-09-17 05:44:07 +00:00
context["can_access"] = self.can_access
2020-01-21 06:35:58 +00:00
if not self.object.og_image or not self.object.summary:
2022-05-14 17:57:27 +00:00
metadata = generate_opengraph(
"generated-meta-contest:%d" % self.object.id,
self.object.description,
)
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
2023-01-04 21:21:03 +00:00
context["contest_has_hidden_subtasks"] = self.object.format.has_hidden_subtasks
context[
"show_final_ranking"
] = self.object.format.has_hidden_subtasks and self.object.is_editable_by(
self.request.user
)
2022-05-14 17:57:27 +00:00
context["logo_override_image"] = self.object.logo_override_image
2024-07-18 02:20:31 +00:00
2022-05-14 17:57:27 +00:00
if (
not context["logo_override_image"]
and self.object.organizations.count() == 1
):
2024-07-18 02:20:31 +00:00
org_image = self.object.organizations.first().organization_image
if org_image:
context["logo_override_image"] = org_image.url
2020-01-21 06:35:58 +00:00
return context
2024-04-30 02:08:48 +00:00
def contest_access_check(self, 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()
2020-01-21 06:35:58 +00:00
def get_object(self, queryset=None):
contest = super(ContestMixin, self).get_object(queryset)
profile = self.request.profile
2022-05-14 17:57:27 +00:00
if (
profile is not None
and ContestParticipation.objects.filter(
id=profile.current_contest_id, contest_id=contest.id
).exists()
):
2020-01-21 06:35:58 +00:00
return contest
2023-09-17 04:55:24 +00:00
if self.should_bypass_access_check(contest):
return contest
2024-04-30 02:08:48 +00:00
self.contest_access_check(contest)
return contest
2022-05-14 17:57:27 +00:00
2020-01-21 06:35:58 +00:00
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:
2022-05-14 17:57:27 +00:00
return generic_message(
request,
_("No such contest"),
_('Could not find a contest with the key "%s".') % key,
)
2020-01-21 06:35:58 +00:00
else:
2022-05-14 17:57:27 +00:00
return generic_message(
request, _("No such contest"), _("Could not find such contest.")
)
2020-01-21 06:35:58 +00:00
except PrivateContestError as e:
2022-05-14 17:57:27 +00:00
return render(
request,
"contest/private.html",
{
"error": e,
"title": _('Access to contest "%s" denied') % e.name,
},
status=403,
)
2020-01-21 06:35:58 +00:00
2022-11-17 22:11:47 +00:00
class ContestDetail(
ContestMixin,
TitleMixin,
CommentedDetailView,
PageVoteDetailView,
BookMarkDetailView,
):
2022-05-14 17:57:27 +00:00
template_name = "contest/contest.html"
2020-01-21 06:35:58 +00:00
def get_title(self):
return self.object.name
2023-01-24 02:36:44 +00:00
def get_editable_organizations(self):
if not self.request.profile:
return []
res = []
for organization in self.object.organizations.all():
can_edit = False
2023-01-24 02:36:44 +00:00
if self.request.profile.can_edit_organization(organization):
can_edit = True
if self.request.profile in organization and self.object.is_editable_by(
self.request.user
):
can_edit = True
if can_edit:
2023-01-24 02:36:44 +00:00
res.append(organization)
return res
2020-01-21 06:35:58 +00:00
def get_context_data(self, **kwargs):
context = super(ContestDetail, self).get_context_data(**kwargs)
2022-05-14 17:57:27 +00:00
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(),
)
)
)
2020-01-21 06:35:58 +00:00
.add_i18n_name(self.request.LANGUAGE_CODE)
2022-05-14 17:57:27 +00:00
)
2023-01-24 02:36:44 +00:00
context["editable_organizations"] = self.get_editable_organizations()
2023-10-16 22:37:52 +00:00
context["is_clonable"] = is_contest_clonable(self.request, self.object)
2024-10-02 20:06:33 +00:00
if self.object.is_in_course:
from judge.models import Course, CourseContest
course = CourseContest.get_course_of_contest(self.object)
if Course.is_editable_by(course, self.request.profile):
context["editable_course"] = course
if self.request.in_contest:
context["current_contest"] = self.request.participation.contest
else:
context["current_contest"] = None
2020-01-21 06:35:58 +00:00
return context
2023-10-16 22:37:52 +00:00
def is_contest_clonable(request, contest):
if not request.profile:
return False
if not Organization.objects.filter(admins=request.profile).exists():
return False
if request.user.has_perm("judge.clone_contest"):
return True
if contest.access_code and not contest.is_editable_by(request.user):
return False
2024-04-24 19:13:11 +00:00
if (
contest.end_time is not None
and contest.end_time + timedelta(days=1) < contest._now
):
2023-10-16 22:37:52 +00:00
return True
return False
class ContestClone(ContestMixin, TitleMixin, SingleObjectFormView):
2022-05-14 17:57:27 +00:00
title = _("Clone Contest")
template_name = "contest/clone.html"
2020-01-21 06:35:58 +00:00
form_class = ContestCloneForm
2023-10-16 22:37:52 +00:00
def get_object(self, queryset=None):
contest = super().get_object(queryset)
if not is_contest_clonable(self.request, contest):
raise Http404()
return contest
2020-01-21 06:35:58 +00:00
2023-08-14 14:10:28 +00:00
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["org_choices"] = tuple(
Organization.objects.filter(admins=self.request.profile).values_list(
"id", "name"
)
)
kwargs["profile"] = self.request.profile
return kwargs
2020-01-21 06:35:58 +00:00
def form_valid(self, form):
2022-11-07 21:39:10 +00:00
tags = self.object.tags.all()
2023-08-14 14:10:28 +00:00
organization = form.cleaned_data["organization"]
2022-11-07 21:39:10 +00:00
private_contestants = self.object.private_contestants.all()
view_contest_scoreboard = self.object.view_contest_scoreboard.all()
contest_problems = self.object.contest_problems.all()
2020-01-21 06:35:58 +00:00
2022-11-07 21:39:10 +00:00
contest = deepcopy(self.object)
2020-01-21 06:35:58 +00:00
contest.pk = None
contest.is_visible = False
contest.user_count = 0
2022-05-14 17:57:27 +00:00
contest.key = form.cleaned_data["key"]
2023-11-10 06:37:21 +00:00
contest.is_rated = False
2020-01-21 06:35:58 +00:00
contest.save()
contest.tags.set(tags)
2023-08-14 14:10:28 +00:00
contest.organizations.set([organization])
2020-01-21 06:35:58 +00:00
contest.private_contestants.set(private_contestants)
2020-12-28 01:59:57 +00:00
contest.view_contest_scoreboard.set(view_contest_scoreboard)
2021-05-24 20:00:36 +00:00
contest.authors.add(self.request.profile)
2020-01-21 06:35:58 +00:00
for problem in contest_problems:
problem.contest = contest
problem.pk = None
ContestProblem.objects.bulk_create(contest_problems)
2022-05-14 17:57:27 +00:00
return HttpResponseRedirect(
2023-08-14 14:10:28 +00:00
reverse(
"organization_contest_edit",
args=(
organization.id,
organization.slug,
contest.key,
),
)
2022-05-14 17:57:27 +00:00
)
2020-01-21 06:35:58 +00:00
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)
2022-05-14 17:57:27 +00:00
self.fields["access_code"].widget.attrs.update({"autocomplete": "off"})
2020-01-21 06:35:58 +00:00
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:
2022-05-14 17:57:27 +00:00
if request.POST.get("access_code"):
2020-01-21 06:35:58 +00:00
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
2021-05-24 20:00:36 +00:00
if not contest.can_join and not (self.is_editor or self.is_tester):
2022-05-14 17:57:27 +00:00
return generic_message(
request,
_("Contest not ongoing"),
_('"%s" is not currently ongoing.') % contest.name,
)
2020-01-21 06:35:58 +00:00
profile = request.profile
if profile.current_contest is not None:
profile.remove_contest()
2020-01-21 06:35:58 +00:00
2022-05-14 17:57:27 +00:00
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."
),
)
2020-01-21 06:35:58 +00:00
2022-05-14 17:57:27 +00:00
requires_access_code = (
not self.can_edit
and contest.access_code
and access_code != contest.access_code
)
2020-01-21 06:35:58 +00:00
if contest.ended:
if requires_access_code:
raise ContestAccessDenied()
while True:
2022-05-14 17:57:27 +00:00
virtual_id = max(
(
ContestParticipation.objects.filter(
contest=contest, user=profile
).aggregate(virtual_id=Max("virtual"))["virtual_id"]
or 0
)
+ 1,
1,
)
2020-01-21 06:35:58 +00:00
try:
participation = ContestParticipation.objects.create(
2022-05-14 17:57:27 +00:00
contest=contest,
user=profile,
virtual=virtual_id,
2020-01-21 06:35:58 +00:00
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:
2021-05-24 20:00:36 +00:00
SPECTATE = ContestParticipation.SPECTATE
LIVE = ContestParticipation.LIVE
2020-01-21 06:35:58 +00:00
try:
participation = ContestParticipation.objects.get(
2022-05-14 17:57:27 +00:00
contest=contest,
user=profile,
virtual=(SPECTATE if self.is_editor or self.is_tester else LIVE),
2020-01-21 06:35:58 +00:00
)
except ContestParticipation.DoesNotExist:
if requires_access_code:
raise ContestAccessDenied()
participation = ContestParticipation.objects.create(
2022-05-14 17:57:27 +00:00
contest=contest,
user=profile,
virtual=(SPECTATE if self.is_editor or self.is_tester else LIVE),
2020-01-21 06:35:58 +00:00
real_start=timezone.now(),
)
else:
if participation.ended:
participation = ContestParticipation.objects.get_or_create(
2022-05-14 17:57:27 +00:00
contest=contest,
user=profile,
virtual=SPECTATE,
defaults={"real_start": timezone.now()},
2020-01-21 06:35:58 +00:00
)[0]
profile.current_contest = participation
profile.save()
contest._updating_stats_only = True
contest.update_user_count()
request.session["contest_mode"] = True
2022-05-14 17:57:27 +00:00
return HttpResponseRedirect(reverse("problem_list"))
2020-01-21 06:35:58 +00:00
def ask_for_access_code(self, form=None):
contest = self.object
wrong_code = False
if form:
if form.is_valid():
2022-05-14 17:57:27 +00:00
if form.cleaned_data["access_code"] == contest.access_code:
return self.join_contest(
self.request, form.cleaned_data["access_code"]
)
2020-01-21 06:35:58 +00:00
wrong_code = True
else:
form = ContestAccessCodeForm()
2022-05-14 17:57:27 +00:00
return render(
self.request,
"contest/access_code.html",
{
"form": form,
"wrong_code": wrong_code,
"title": _('Enter access code for "%s"') % contest.name,
},
)
2020-01-21 06:35:58 +00:00
class ContestLeave(LoginRequiredMixin, ContestMixin, BaseDetailView):
def post(self, request, *args, **kwargs):
contest = self.get_object()
profile = request.profile
2022-05-14 17:57:27 +00:00
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,
)
2020-01-21 06:35:58 +00:00
profile.remove_contest()
2022-05-14 17:57:27 +00:00
request.session["contest_mode"] = True # reset contest_mode
return HttpResponseRedirect(reverse("contest_view", args=(contest.key,)))
2020-01-21 06:35:58 +00:00
2022-05-14 17:57:27 +00:00
ContestDay = namedtuple("ContestDay", "date weekday is_pad is_today starts ends oneday")
2020-01-21 06:35:58 +00:00
class ContestCalendar(TitleMixin, ContestListMixin, TemplateView):
firstweekday = SUNDAY
2022-05-14 17:57:27 +00:00
weekday_classes = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"]
template_name = "contest/calendar.html"
2020-01-21 06:35:58 +00:00
def get(self, request, *args, **kwargs):
try:
2022-05-14 17:57:27 +00:00
self.year = int(kwargs["year"])
self.month = int(kwargs["month"])
2020-01-21 06:35:58 +00:00
except (KeyError, ValueError):
2022-05-14 17:57:27 +00:00
raise ImproperlyConfigured(
_("ContestCalendar requires integer year and month")
)
2020-01-21 06:35:58 +00:00
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)
2022-05-14 17:57:27 +00:00
contests = self.get_queryset().filter(
Q(start_time__gte=start, start_time__lt=end)
| Q(end_time__gte=start, end_time__lt=end)
)
2020-01-21 06:35:58 +00:00
starts, ends, oneday = (defaultdict(list) for i in range(3))
for contest in contests:
start_date = timezone.localtime(contest.start_time).date()
2022-05-14 17:57:27 +00:00
end_date = timezone.localtime(
contest.end_time - timedelta(seconds=1)
).date()
2020-01-21 06:35:58 +00:00
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)
2022-05-14 17:57:27 +00:00
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
]
2020-01-21 06:35:58 +00:00
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:
2022-05-14 17:57:27 +00:00
context["title"] = _("Contests in %(month)s") % {
"month": date_filter(month, _("F Y"))
}
2020-01-21 06:35:58 +00:00
2022-05-14 17:57:27 +00:00
dates = Contest.objects.aggregate(min=Min("start_time"), max=Max("end_time"))
2020-01-21 06:35:58 +00:00
min_month = (self.today.year, self.today.month)
2022-05-14 17:57:27 +00:00
if dates["min"] is not None:
min_month = dates["min"].year, dates["min"].month
2020-01-21 06:35:58 +00:00
max_month = (self.today.year, self.today.month)
2022-05-14 17:57:27 +00:00
if dates["max"] is not None:
max_month = max(
(dates["max"].year, dates["max"].month),
(self.today.year, self.today.month),
)
2020-01-21 06:35:58 +00:00
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()
2022-05-14 17:57:27 +00:00
context["now"] = timezone.now()
context["calendar"] = self.get_table()
context["curr_month"] = date(self.year, self.month, 1)
2020-01-21 06:35:58 +00:00
if month > min_month:
2022-05-14 17:57:27 +00:00
context["prev_month"] = date(
self.year - (self.month == 1),
12 if self.month == 1 else self.month - 1,
1,
)
2020-01-21 06:35:58 +00:00
else:
2022-05-14 17:57:27 +00:00
context["prev_month"] = None
2020-01-21 06:35:58 +00:00
if month < max_month:
2022-05-14 17:57:27 +00:00
context["next_month"] = date(
self.year + (self.month == 12),
1 if self.month == 12 else self.month + 1,
1,
)
2020-01-21 06:35:58 +00:00
else:
2022-05-14 17:57:27 +00:00
context["next_month"] = None
2020-01-21 06:35:58 +00:00
return context
class CachedContestCalendar(ContestCalendar):
def render(self):
2022-05-14 17:57:27 +00:00
key = "contest_cal:%d:%d" % (self.year, self.month)
2020-01-21 06:35:58 +00:00
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):
2022-05-14 17:57:27 +00:00
template_name = "contest/stats.html"
POINT_BIN = 10 # in point distribution
2020-01-21 06:35:58 +00:00
def get_title(self):
2022-05-14 17:57:27 +00:00
return _("%s Statistics") % self.object.name
2020-01-21 06:35:58 +00:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
2021-05-24 20:00:36 +00:00
if not (self.object.ended or self.can_edit):
2020-01-21 06:35:58 +00:00
raise Http404()
queryset = Submission.objects.filter(contest_object=self.object)
2022-05-14 17:57:27 +00:00
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()
)
2020-01-21 06:35:58 +00:00
status_count_queryset = list(
2022-05-14 17:57:27 +00:00
queryset.values("problem__code", "result")
.annotate(count=Count("result"))
.values_list("problem__code", "result", "count"),
2020-01-21 06:35:58 +00:00
)
2021-05-24 20:00:36 +00:00
labels, codes = [], []
2022-05-14 17:57:27 +00:00
contest_problems = self.object.contest_problems.order_by("order").values_list(
"problem__name", "problem__code"
)
2021-05-24 20:00:36 +00:00
if contest_problems:
labels, codes = zip(*contest_problems)
2020-01-21 06:35:58 +00:00
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):
2022-05-14 17:57:27 +00:00
for category in _get_result_data(defaultdict(int, status_counts[i]))[
"categories"
]:
result_data[category["code"]][i] = category["count"]
2020-01-21 06:35:58 +00:00
2021-06-02 00:20:39 +00:00
problem_points = [[] for _ in range(num_problems)]
2022-05-14 17:57:27 +00:00
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"
)
)
2021-06-02 00:20:39 +00:00
counter = [[0 for _ in range(self.POINT_BIN + 1)] for _ in range(num_problems)]
2021-06-02 13:31:58 +00:00
for problem_code, point, max_point, count in point_count_queryset:
2022-05-14 17:57:27 +00:00
if (point == None) or (problem_code not in codes):
continue
2021-06-02 13:31:58 +00:00
problem_idx = codes.index(problem_code)
2024-04-22 01:08:25 +00:00
if max_point > 0:
bin_idx = math.floor(point * self.POINT_BIN / max_point)
else:
bin_idx = 0
2023-04-24 16:56:10 +00:00
bin_idx = max(min(bin_idx, self.POINT_BIN), 0)
2021-06-02 13:31:58 +00:00
counter[problem_idx][bin_idx] += count
2021-06-02 00:20:39 +00:00
for i in range(num_problems):
2022-05-14 17:57:27 +00:00
problem_points[i] = [
(j * 100 / self.POINT_BIN, counter[i][j])
for j in range(len(counter[i]))
]
2020-01-21 06:35:58 +00:00
stats = {
2022-05-14 17:57:27 +00:00
"problem_status_count": {
"labels": labels,
"datasets": [
2020-01-21 06:35:58 +00:00
{
2022-05-14 17:57:27 +00:00
"label": name,
"backgroundColor": settings.DMOJ_STATS_SUBMISSION_RESULT_COLORS[
name
],
"data": data,
2020-01-21 06:35:58 +00:00
}
for name, data in result_data.items()
],
},
2022-05-14 17:57:27 +00:00
"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"),
2020-01-21 06:35:58 +00:00
),
2022-05-14 17:57:27 +00:00
"problem_point": [
get_histogram(problem_points[i]) for i in range(num_problems)
2021-06-02 00:20:39 +00:00
],
2022-05-14 17:57:27 +00:00
"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"),
2020-01-21 06:35:58 +00:00
),
2022-05-14 17:57:27 +00:00
"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"),
2020-01-21 06:35:58 +00:00
),
}
2022-05-14 17:57:27 +00:00
context["stats"] = mark_safe(json.dumps(stats))
context["problems"] = labels
2020-01-21 06:35:58 +00:00
return context
ContestRankingProfile = namedtuple(
2022-05-14 17:57:27 +00:00
"ContestRankingProfile",
2024-05-03 00:13:19 +00:00
"id user username points cumtime tiebreaker participation "
2022-05-14 17:57:27 +00:00
"participation_rating problem_cells result_cell",
2020-01-21 06:35:58 +00:00
)
2022-05-14 17:57:27 +00:00
BestSolutionData = namedtuple("BestSolutionData", "code points time state is_pretested")
2020-01-21 06:35:58 +00:00
def make_contest_ranking_profile(
contest, participation, contest_problems, show_final=False
):
if not show_final:
points = participation.score
cumtime = participation.cumtime
else:
points = participation.score_final
cumtime = participation.cumtime_final
2020-01-21 06:35:58 +00:00
user = participation.user
return ContestRankingProfile(
id=user.id,
2024-04-13 22:02:54 +00:00
user=user,
2024-05-03 00:13:19 +00:00
username=user.username,
points=points,
cumtime=cumtime,
2021-05-24 20:18:39 +00:00
tiebreaker=participation.tiebreaker,
2022-05-14 17:57:27 +00:00
participation_rating=participation.rating.rating
if hasattr(participation, "rating")
else None,
problem_cells=[
contest.format.display_user_problem(
participation, contest_problem, show_final
)
2022-05-14 17:57:27 +00:00
for contest_problem in contest_problems
],
result_cell=contest.format.display_participation_result(
participation, show_final
),
2020-01-21 06:35:58 +00:00
participation=participation,
)
2024-05-31 06:38:25 +00:00
def base_contest_ranking_list(
contest, problems, queryset, show_final=False, extra_participation=None
):
2024-04-13 22:02:54 +00:00
participation_fields = [
field.name
for field in ContestParticipation._meta.get_fields()
if field.concrete and not field.many_to_many
]
fields_to_fetch = participation_fields + [
"user__id",
"rating__rating",
]
res = [
make_contest_ranking_profile(contest, participation, problems, show_final)
2024-04-13 22:02:54 +00:00
for participation in queryset.select_related("user", "rating").only(
*fields_to_fetch
2022-05-14 17:57:27 +00:00
)
]
2024-04-13 22:02:54 +00:00
Profile.prefetch_profile_cache([p.id for p in res])
return res
2020-01-21 06:35:58 +00:00
def contest_ranking_list(
contest, problems, queryset=None, show_final=False, extra_participation=None
):
if queryset is None:
2021-11-07 23:59:12 +00:00
queryset = contest.users.filter(virtual=0)
if extra_participation and extra_participation.virtual:
queryset = queryset | contest.users.filter(id=extra_participation.id)
if show_final:
queryset = queryset.order_by(
"is_disqualified", "-score_final", "cumtime_final", "tiebreaker"
)
else:
queryset = queryset.order_by(
"is_disqualified", "-score", "cumtime", "tiebreaker"
)
2020-01-21 06:35:58 +00:00
return base_contest_ranking_list(
contest,
problems,
queryset,
show_final,
)
2020-01-21 06:35:58 +00:00
2022-05-14 17:57:27 +00:00
def get_contest_ranking_list(
request,
contest,
participation=None,
ranking_list=contest_ranking_list,
ranker=ranker,
show_final=False,
2022-05-14 17:57:27 +00:00
):
problems = list(
contest.contest_problems.select_related("problem")
.defer("problem__description")
.order_by("order")
)
2020-01-21 06:35:58 +00:00
if participation is None:
participation = _get_current_virtual_participation(request, contest)
ranking_list_result = ranking_list(
contest, problems, show_final=show_final, extra_participation=participation
)
2022-05-14 17:57:27 +00:00
users = ranker(
ranking_list_result,
2022-05-14 17:57:27 +00:00
key=attrgetter("points", "cumtime", "tiebreaker"),
)
2020-01-21 06:35:58 +00:00
return users, problems
def _get_current_virtual_participation(request, contest):
# Return None if not eligible
if not request.user.is_authenticated:
return None
participation = request.profile.current_contest
if participation is None or participation.contest_id != contest.id:
return None
return participation
2020-01-21 06:35:58 +00:00
class ContestRankingBase(ContestMixin, TitleMixin, DetailView):
2022-05-14 17:57:27 +00:00
template_name = "contest/ranking.html"
2022-11-27 07:03:38 +00:00
page_type = None
2020-01-21 06:35:58 +00:00
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)
2021-05-24 20:00:36 +00:00
if not self.object.can_see_own_scoreboard(self.request.user):
2020-01-21 06:35:58 +00:00
raise Http404()
users, problems = self.get_ranking_list()
2022-05-14 17:57:27 +00:00
context["users"] = users
context["problems"] = problems
2022-11-27 07:03:38 +00:00
context["page_type"] = self.page_type
2020-01-21 06:35:58 +00:00
return context
class ContestRanking(ContestRankingBase):
2022-11-27 07:03:38 +00:00
page_type = "ranking"
2024-06-25 05:23:40 +00:00
show_final = False
2020-01-21 06:35:58 +00:00
2023-09-17 04:55:24 +00:00
def should_bypass_access_check(self, contest):
return contest.public_scoreboard
2020-01-21 06:35:58 +00:00
def get_title(self):
2022-05-14 17:57:27 +00:00
return _("%s Rankings") % self.object.name
2020-01-21 06:35:58 +00:00
def get_ranking_list(self):
2021-05-24 20:00:36 +00:00
if not self.object.can_see_full_scoreboard(self.request.user):
2022-05-14 17:57:27 +00:00
queryset = self.object.users.filter(
user=self.request.profile, virtual=ContestParticipation.LIVE
)
2021-05-24 20:00:36 +00:00
return get_contest_ranking_list(
2022-05-14 17:57:27 +00:00
self.request,
self.object,
2021-05-24 20:00:36 +00:00
ranking_list=partial(base_contest_ranking_list, queryset=queryset),
2022-05-14 17:57:27 +00:00
ranker=lambda users, key: ((_("???"), user) for user in users),
2021-05-24 20:00:36 +00:00
)
2024-06-25 05:23:40 +00:00
queryset = self.object.users
if self.friend_only:
friends = self.request.profile.get_friends()
queryset = queryset.filter(user_id__in=friends)
if not self.include_virtual:
queryset = queryset.filter(virtual=0)
else:
queryset = queryset.filter(virtual__gte=0)
return get_contest_ranking_list(
self.request,
self.object,
ranking_list=partial(contest_ranking_list, queryset=queryset),
show_final=self.show_final,
)
def _get_default_include_virtual(self):
if hasattr(self.object, "official"):
return "1"
return "0"
def setup_filters(self):
if self.request.profile:
self.friend_only = bool(self.request.GET.get("friend") == "1")
else:
self.friend_only = False
self.include_virtual = bool(
self.request.GET.get("virtual", self._get_default_include_virtual()) == "1"
)
self.ajax_only = bool(self.request.GET.get("ajax") == "1")
if self.ajax_only:
self.template_name = "contest/ranking-table.html"
2020-01-21 06:35:58 +00:00
def get_context_data(self, **kwargs):
2024-06-25 05:23:40 +00:00
self.setup_filters()
2020-01-21 06:35:58 +00:00
context = super().get_context_data(**kwargs)
2022-05-14 17:57:27 +00:00
context["has_rating"] = self.object.ratings.exists()
2024-06-25 05:23:40 +00:00
if not self.ajax_only:
context["include_virtual"] = self.include_virtual
context["friend_only"] = self.friend_only
2020-01-21 06:35:58 +00:00
return context
class ContestFinalRanking(LoginRequiredMixin, ContestRanking):
page_type = "final_ranking"
2024-06-25 05:23:40 +00:00
show_final = True
def get_ranking_list(self):
2023-01-04 21:21:03 +00:00
if not self.object.is_editable_by(self.request.user):
raise Http404()
2023-02-08 04:12:01 +00:00
if not self.object.format.has_hidden_subtasks:
raise Http404()
2024-06-25 05:23:40 +00:00
return super().get_ranking_list()
2020-01-21 06:35:58 +00:00
class ContestParticipationList(LoginRequiredMixin, ContestRankingBase):
2022-11-27 07:03:38 +00:00
page_type = "participation"
2020-01-21 06:35:58 +00:00
def get_title(self):
if self.profile == self.request.profile:
2022-05-14 17:57:27 +00:00
return _("Your participation in %s") % self.object.name
2020-01-21 06:35:58 +00:00
return _("%s's participation in %s") % (self.profile.username, self.object.name)
def get_ranking_list(self):
2022-05-14 17:57:27 +00:00
if (
not self.object.can_see_full_scoreboard(self.request.user)
and self.profile != self.request.profile
):
2021-05-24 20:00:36 +00:00
raise Http404()
2022-05-14 17:57:27 +00:00
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]),
)
2020-01-21 06:35:58 +00:00
return get_contest_ranking_list(
2022-05-14 17:57:27 +00:00
self.request,
self.object,
2020-01-21 06:35:58 +00:00
ranking_list=partial(base_contest_ranking_list, queryset=queryset),
2022-05-14 17:57:27 +00:00
ranker=lambda users, key: (
(user.participation.virtual or live_link, user) for user in users
),
)
2020-01-21 06:35:58 +00:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
2022-05-14 17:57:27 +00:00
context["has_rating"] = False
context["now"] = timezone.now()
context["rank_header"] = _("Participation")
context["participation_tab"] = True
2020-01-21 06:35:58 +00:00
return context
def get(self, request, *args, **kwargs):
2022-05-14 17:57:27 +00:00
if "user" in kwargs:
self.profile = get_object_or_404(Profile, user__username=kwargs["user"])
2020-01-21 06:35:58 +00:00
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)
2021-05-24 20:40:07 +00:00
if not contest.is_editable_by(self.request.user):
2020-01-21 06:35:58 +00:00
raise Http404()
return contest
def post(self, request, *args, **kwargs):
self.object = self.get_object()
try:
2022-05-14 17:57:27 +00:00
participation = self.object.users.get(pk=request.POST.get("participation"))
2020-01-21 06:35:58 +00:00
except ObjectDoesNotExist:
pass
else:
participation.set_disqualified(not participation.is_disqualified)
2022-05-14 17:57:27 +00:00
return HttpResponseRedirect(reverse("contest_ranking", args=(self.object.key,)))
2020-01-21 06:35:58 +00:00
class ContestMossMixin(ContestMixin, PermissionRequiredMixin):
2022-05-14 17:57:27 +00:00
permission_required = "judge.moss_contest"
2020-01-21 06:35:58 +00:00
def get_object(self, queryset=None):
contest = super().get_object(queryset)
2022-05-14 17:57:27 +00:00
if settings.MOSS_API_KEY is None or not contest.is_editable_by(
self.request.user
):
2020-01-21 06:35:58 +00:00
raise Http404()
if not contest.is_editable_by(self.request.user):
raise Http404()
return contest
class ContestMossView(ContestMossMixin, TitleMixin, DetailView):
2022-05-14 17:57:27 +00:00
template_name = "contest/moss.html"
2020-01-21 06:35:58 +00:00
def get_title(self):
2022-05-14 17:57:27 +00:00
return _("%s MOSS Results") % self.object.name
2020-01-21 06:35:58 +00:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
2022-05-14 17:57:27 +00:00
problems = list(
map(
attrgetter("problem"),
self.object.contest_problems.order_by("order").select_related(
"problem"
),
)
)
2020-01-21 06:35:58 +00:00
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))
2022-05-14 17:57:27 +00:00
context["languages"] = languages
context["has_results"] = results.exists()
context["moss_results"] = [
(problem, moss_results[problem]) for problem in problems
]
2020-01-21 06:35:58 +00:00
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(
2022-05-14 17:57:27 +00:00
status,
message=_("Running MOSS for %s...") % (self.object.name,),
redirect=reverse("contest_moss", args=(self.object.key,)),
2020-01-21 06:35:58 +00:00
)
class ContestMossDelete(ContestMossMixin, SingleObjectMixin, View):
def post(self, request, *args, **kwargs):
self.object = self.get_object()
ContestMoss.objects.filter(contest=self.object).delete()
2022-05-14 17:57:27 +00:00
return HttpResponseRedirect(reverse("contest_moss", args=(self.object.key,)))
2020-01-21 06:35:58 +00:00
class ContestTagDetailAjax(DetailView):
model = ContestTag
2022-05-14 17:57:27 +00:00
slug_field = slug_url_kwarg = "name"
context_object_name = "tag"
template_name = "contest/tag-ajax.html"
2020-01-21 06:35:58 +00:00
class ContestTagDetail(TitleMixin, ContestTagDetailAjax):
2022-05-14 17:57:27 +00:00
template_name = "contest/tag.html"
2020-01-21 06:35:58 +00:00
def get_title(self):
2022-05-14 17:57:27 +00:00
return _("Contest tag: %s") % self.object.name
2021-07-19 01:22:44 +00:00
class ContestProblemClarificationForm(forms.Form):
2022-05-14 17:57:27 +00:00
body = forms.CharField(
widget=HeavyPreviewPageDownWidget(
preview=reverse_lazy("comment_preview"),
preview_timeout=1000,
hide_preview_button=True,
)
)
2021-07-19 01:22:44 +00:00
def __init__(self, request, *args, **kwargs):
self.request = request
super(ContestProblemClarificationForm, self).__init__(*args, **kwargs)
2022-05-14 17:57:27 +00:00
self.fields["body"].widget.attrs.update({"placeholder": _("Issue description")})
2021-07-19 01:22:44 +00:00
class NewContestClarificationView(ContestMixin, TitleMixin, SingleObjectFormView):
form_class = ContestProblemClarificationForm
2022-05-14 17:57:27 +00:00
template_name = "contest/clarification.html"
2021-07-19 01:22:44 +00:00
def get_form_kwargs(self):
kwargs = super(NewContestClarificationView, self).get_form_kwargs()
2022-05-14 17:57:27 +00:00
kwargs["request"] = self.request
2021-07-19 01:22:44 +00:00
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
2023-01-04 21:21:03 +00:00
return self.get_object().is_editable_by(self.request.user)
2021-07-19 01:22:44 +00:00
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):
2022-05-14 17:57:27 +00:00
problem_code = self.request.POST["problem"]
description = form.cleaned_data["body"]
2021-07-19 01:22:44 +00:00
clarification = ContestProblemClarification(description=description)
clarification.problem = get_object_or_404(
ContestProblem, contest=self.get_object(), problem__code=problem_code
)
2021-07-19 01:22:44 +00:00
clarification.save()
2022-05-14 17:57:27 +00:00
return HttpResponseRedirect(reverse("problem_list"))
2021-07-19 01:22:44 +00:00
def get_title(self):
return "New clarification for %s" % self.object.name
def get_content_title(self):
2022-05-14 17:57:27 +00:00
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,
)
)
2021-07-19 01:22:44 +00:00
def get_context_data(self, **kwargs):
context = super(NewContestClarificationView, self).get_context_data(**kwargs)
2022-05-14 17:57:27 +00:00
context["problems"] = ContestProblem.objects.filter(
contest=self.object
).order_by("order")
2021-10-16 22:40:02 +00:00
return context
class ContestClarificationAjax(ContestMixin, DetailView):
def get(self, request, *args, **kwargs):
self.object = self.get_object()
2021-10-19 22:41:53 +00:00
if not self.object.is_accessible_by(request.user):
2021-10-16 22:40:02 +00:00
raise Http404()
2022-05-14 17:57:27 +00:00
polling_time = 1 # minute
2023-08-30 18:07:57 +00:00
last_one_minute = timezone.now() - timezone.timedelta(minutes=polling_time)
2022-05-14 17:57:27 +00:00
queryset = ContestProblemClarification.objects.filter(
problem__in=self.object.contest_problems.all(), date__gte=last_one_minute
2022-05-14 17:57:27 +00:00
)
problems = list(
ContestProblem.objects.filter(contest=self.object)
.order_by("order")
.values_list("problem__code", flat=True)
2022-05-14 17:57:27 +00:00
)
res = []
for clarification in queryset:
value = {
"order": self.object.get_label_for_problem(
problems.index(clarification.problem.problem.code)
),
"problem__name": clarification.problem.problem.name,
"description": clarification.description,
}
res.append(value)
2021-10-16 22:40:02 +00:00
return JsonResponse(res, safe=False, json_dumps_params={"ensure_ascii": False})
2022-01-10 11:13:46 +00:00
def update_contest_mode(request):
2022-05-14 17:57:27 +00:00
if not request.is_ajax() or not request.method == "POST":
return HttpResponseNotAllowed(["POST"])
2022-01-10 11:13:46 +00:00
2022-05-14 17:57:27 +00:00
old_mode = request.session.get("contest_mode", True)
request.session["contest_mode"] = not old_mode
return HttpResponse()
2023-10-06 08:54:37 +00:00
ContestsSummaryData = namedtuple(
"ContestsSummaryData",
"username first_name last_name points point_contests css_class",
2023-10-06 08:54:37 +00:00
)
2023-11-24 05:16:01 +00:00
class ContestsSummaryView(DiggPaginatorMixin, ListView):
paginate_by = 50
template_name = "contest/contests_summary.html"
2023-10-06 08:54:37 +00:00
2023-11-24 05:16:01 +00:00
def get(self, *args, **kwargs):
try:
self.contests_summary = ContestsSummary.objects.get(key=kwargs["key"])
except:
raise Http404()
return super().get(*args, **kwargs)
def get_queryset(self):
total_rank = self.contests_summary.results
2023-11-24 05:16:01 +00:00
return total_rank
2023-10-06 08:54:37 +00:00
2023-11-24 05:16:01 +00:00
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["contests"] = self.contests_summary.contests.all()
2023-11-24 05:16:01 +00:00
context["title"] = _("Contests")
context["first_page_href"] = "."
return context
2023-10-06 08:54:37 +00:00
2024-10-01 16:07:55 +00:00
def recalculate_contest_summary_result(request, contest_summary):
scores_system = contest_summary.scores
contests = contest_summary.contests.all()
total_points = defaultdict(int)
result_per_contest = defaultdict(lambda: [(0, 0)] * len(contests))
user_css_class = {}
for i in range(len(contests)):
contest = contests[i]
2024-10-01 16:07:55 +00:00
users, problems = get_contest_ranking_list(request, contest)
for rank, user in users:
curr_score = 0
if rank - 1 < len(scores_system):
curr_score = scores_system[rank - 1]
total_points[user.user] += curr_score
result_per_contest[user.user][i] = (curr_score, rank)
2024-10-01 16:07:55 +00:00
user_css_class[user.user] = user.user.css_class
sorted_total_points = [
ContestsSummaryData(
username=user.username,
first_name=user.first_name,
last_name=user.last_name,
points=total_points[user],
point_contests=result_per_contest[user],
css_class=user_css_class[user],
)
for user in total_points
]
sorted_total_points.sort(key=lambda x: x.points, reverse=True)
total_rank = ranker(sorted_total_points)
return [(rank, item._asdict()) for rank, item in total_rank]
2024-05-30 07:59:22 +00:00
class OfficialContestList(ContestList):
official = True
template_name = "contest/official_list.html"
def setup_contest_list(self, request):
self.contest_query = request.GET.get("contest", "")
self.org_query = []
self.hide_organization_contests = False
self.selected_categories = []
self.selected_locations = []
self.year_from = None
self.year_to = None
if "category" in request.GET:
try:
self.selected_categories = list(
map(int, request.GET.getlist("category"))
)
except ValueError:
pass
if "location" in request.GET:
try:
self.selected_locations = list(
map(int, request.GET.getlist("location"))
)
except ValueError:
pass
if "year_from" in request.GET:
try:
self.year_from = int(request.GET.get("year_from"))
except ValueError:
pass
if "year_to" in request.GET:
try:
self.year_to = int(request.GET.get("year_to"))
except ValueError:
pass
def extra_queryset_filters(self, queryset):
if self.selected_categories:
queryset = queryset.filter(official__category__in=self.selected_categories)
if self.selected_locations:
queryset = queryset.filter(official__location__in=self.selected_locations)
if self.year_from:
queryset = queryset.filter(official__year__gte=self.year_from)
if self.year_to:
queryset = queryset.filter(official__year__lte=self.year_to)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["page_type"] = "official"
context["is_official"] = True
context["categories"] = OfficialContestCategory.objects.all()
context["locations"] = OfficialContestLocation.objects.all()
context["selected_categories"] = self.selected_categories
context["selected_locations"] = self.selected_locations
context["year_from"] = self.year_from
context["year_to"] = self.year_to
return context