NDOJ/judge/views/problem.py

1198 lines
43 KiB
Python
Raw Normal View History

2020-01-21 06:35:58 +00:00
import logging
import os
import shutil
from operator import itemgetter
from random import randrange
2022-11-07 21:39:10 +00:00
from copy import deepcopy
2020-01-21 06:35:58 +00:00
2023-03-10 04:31:55 +00:00
from django.core.cache import cache
2020-01-21 06:35:58 +00:00
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import transaction
2022-11-01 01:43:06 +00:00
from django.db.models import (
BooleanField,
Case,
CharField,
Count,
F,
FilteredRelation,
Prefetch,
Q,
When,
IntegerField,
)
from django.db.models.functions import Coalesce
2020-01-21 06:35:58 +00:00
from django.db.utils import ProgrammingError
2022-05-14 17:57:27 +00:00
from django.http import (
Http404,
HttpResponse,
HttpResponseForbidden,
HttpResponseRedirect,
JsonResponse,
)
2020-01-21 06:35:58 +00:00
from django.shortcuts import get_object_or_404, render
from django.template.loader import get_template
from django.urls import reverse
from django.utils import timezone, translation
from django.utils.functional import cached_property
from django.utils.html import escape, format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _, gettext_lazy
2022-03-21 21:09:16 +00:00
from django.views.generic import ListView, View
2020-01-21 06:35:58 +00:00
from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin
from judge.comments import CommentedDetailView
2022-03-10 05:38:29 +00:00
from judge.forms import ProblemCloneForm, ProblemSubmitForm, ProblemPointsVoteForm
2022-05-14 17:57:27 +00:00
from judge.models import (
ContestProblem,
ContestSubmission,
Judge,
Language,
Problem,
ContestProblemClarification,
2022-05-14 17:57:27 +00:00
ProblemGroup,
ProblemTranslation,
ProblemType,
ProblemPointsVote,
RuntimeVersion,
Solution,
Submission,
SubmissionSource,
Organization,
2022-05-22 01:30:44 +00:00
Profile,
2022-06-12 07:57:46 +00:00
LanguageTemplate,
2022-05-14 17:57:27 +00:00
)
2020-01-21 06:35:58 +00:00
from judge.pdf_problems import DefaultPdfMaker, HAS_PDF
from judge.utils.diggpaginator import DiggPaginator
from judge.utils.opengraph import generate_opengraph
2022-05-14 17:57:27 +00:00
from judge.utils.problems import (
contest_attempted_ids,
contest_completed_ids,
hot_problems,
user_attempted_ids,
user_completed_ids,
2023-01-28 01:15:37 +00:00
get_related_problems,
2023-11-09 08:43:11 +00:00
get_user_recommended_problems,
RecommendationType,
2022-05-14 17:57:27 +00:00
)
2020-01-21 06:35:58 +00:00
from judge.utils.strings import safe_float_or_none, safe_int_or_none
from judge.utils.tickets import own_ticket_filter
2022-05-14 17:57:27 +00:00
from judge.utils.views import (
QueryStringSortMixin,
SingleObjectFormView,
TitleMixin,
generic_message,
)
2022-04-12 02:18:01 +00:00
from judge.ml.collab_filter import CollabFilter
from judge.views.pagevote import PageVoteDetailView
from judge.views.bookmark import BookMarkDetailView
from judge.views.feed import FeedView
2020-01-21 06:35:58 +00:00
def get_contest_problem(problem, profile):
try:
return problem.contests.get(contest_id=profile.current_contest.contest_id)
except ObjectDoesNotExist:
return None
def get_contest_submission_count(problem, profile, virtual):
2022-05-14 17:57:27 +00:00
return (
profile.current_contest.submissions.exclude(submission__status__in=["IE"])
.filter(problem__problem=problem, participation__virtual=virtual)
.count()
)
2020-01-21 06:35:58 +00:00
2023-01-24 02:36:44 +00:00
def get_problems_in_organization(request, organization):
problem_list = ProblemList(request=request)
problem_list.setup_problem_list(request)
problem_list.org_query = [organization.id]
problems = problem_list.get_normal_queryset()
return problems
2020-01-21 06:35:58 +00:00
class ProblemMixin(object):
model = Problem
2022-05-14 17:57:27 +00:00
slug_url_kwarg = "problem"
slug_field = "code"
2020-01-21 06:35:58 +00:00
def get_object(self, queryset=None):
problem = super(ProblemMixin, self).get_object(queryset)
if not problem.is_accessible_by(self.request.user):
raise Http404()
return problem
def no_such_problem(self):
code = self.kwargs.get(self.slug_url_kwarg, None)
2022-05-14 17:57:27 +00:00
return generic_message(
self.request,
_("No such problem"),
_('Could not find a problem with the code "%s".') % code,
status=404,
)
2020-01-21 06:35:58 +00:00
def get(self, request, *args, **kwargs):
try:
return super(ProblemMixin, self).get(request, *args, **kwargs)
2022-01-10 11:13:46 +00:00
except Http404 as e:
2020-01-21 06:35:58 +00:00
return self.no_such_problem()
class SolvedProblemMixin(object):
def get_completed_problems(self):
if self.in_contest:
return contest_completed_ids(self.profile.current_contest)
else:
return user_completed_ids(self.profile) if self.profile is not None else ()
def get_attempted_problems(self):
if self.in_contest:
return contest_attempted_ids(self.profile.current_contest)
else:
return user_attempted_ids(self.profile) if self.profile is not None else ()
2023-01-24 02:36:44 +00:00
def get_latest_attempted_problems(self, limit=None, queryset=None):
2022-05-22 01:30:44 +00:00
if self.in_contest or not self.profile:
return ()
result = list(user_attempted_ids(self.profile).values())
2023-01-24 02:36:44 +00:00
if queryset:
queryset_ids = set([i.code for i in queryset])
result = filter(lambda i: i["code"] in queryset_ids, result)
2022-05-22 01:30:44 +00:00
result = sorted(result, key=lambda d: -d["last_submission"])
if limit:
result = result[:limit]
return result
2020-01-21 06:35:58 +00:00
@cached_property
def in_contest(self):
2022-05-14 17:57:27 +00:00
return (
self.profile is not None
and self.profile.current_contest is not None
2022-01-10 11:13:46 +00:00
and self.request.in_contest_mode
2022-05-14 17:57:27 +00:00
)
2020-01-21 06:35:58 +00:00
@cached_property
def contest(self):
return self.request.profile.current_contest.contest
@cached_property
def profile(self):
if not self.request.user.is_authenticated:
return None
return self.request.profile
2022-05-14 17:57:27 +00:00
class ProblemSolution(
2022-11-17 00:48:32 +00:00
SolvedProblemMixin,
ProblemMixin,
TitleMixin,
CommentedDetailView,
PageVoteDetailView,
2022-11-17 19:17:45 +00:00
BookMarkDetailView,
2022-05-14 17:57:27 +00:00
):
2023-04-28 16:42:03 +00:00
context_object_name = "solution"
2022-05-14 17:57:27 +00:00
template_name = "problem/editorial.html"
2020-01-21 06:35:58 +00:00
def get_title(self):
return _("Editorial for {0}").format(self.problem.name)
2020-01-21 06:35:58 +00:00
def get_content_title(self):
2022-05-14 17:57:27 +00:00
return format_html(
_('Editorial for <a href="{1}">{0}</a>'),
self.problem.name,
reverse("problem_detail", args=[self.problem.code]),
2022-05-14 17:57:27 +00:00
)
2020-01-21 06:35:58 +00:00
def get_object(self):
self.problem = super().get_object()
solution = get_object_or_404(Solution, problem=self.problem)
return solution
2020-01-21 06:35:58 +00:00
def get_context_data(self, **kwargs):
context = super(ProblemSolution, self).get_context_data(**kwargs)
solution = self.get_object()
2022-05-14 17:57:27 +00:00
if (
not solution.is_public or solution.publish_on > timezone.now()
) and not self.request.user.has_perm("judge.see_private_solution"):
2020-01-21 06:35:58 +00:00
raise Http404()
2022-01-10 11:13:46 +00:00
2023-04-28 16:42:03 +00:00
context["problem"] = self.problem
context["has_solved_problem"] = self.problem.id in self.get_completed_problems()
2020-01-21 06:35:58 +00:00
return context
2022-05-14 17:57:27 +00:00
class ProblemRaw(
ProblemMixin, TitleMixin, TemplateResponseMixin, SingleObjectMixin, View
):
context_object_name = "problem"
template_name = "problem/raw.html"
2020-01-21 06:35:58 +00:00
def get_title(self):
return self.object.name
def get_context_data(self, **kwargs):
context = super(ProblemRaw, self).get_context_data(**kwargs)
2022-05-14 17:57:27 +00:00
context["problem_name"] = self.object.name
context["url"] = self.request.build_absolute_uri()
context["description"] = self.object.description
2022-06-02 16:23:16 +00:00
if hasattr(self.object, "data_files"):
context["fileio_input"] = self.object.data_files.fileio_input
context["fileio_output"] = self.object.data_files.fileio_output
else:
context["fileio_input"] = None
context["fileio_output"] = None
2020-01-21 06:35:58 +00:00
return context
def get(self, request, *args, **kwargs):
self.object = self.get_object()
with translation.override(settings.LANGUAGE_CODE):
2022-05-14 17:57:27 +00:00
return self.render_to_response(
self.get_context_data(
object=self.object,
)
)
2020-01-21 06:35:58 +00:00
2022-11-17 00:48:32 +00:00
class ProblemDetail(
2022-11-17 22:11:47 +00:00
ProblemMixin,
SolvedProblemMixin,
CommentedDetailView,
PageVoteDetailView,
BookMarkDetailView,
2022-11-17 00:48:32 +00:00
):
2022-05-14 17:57:27 +00:00
context_object_name = "problem"
template_name = "problem/problem.html"
2020-01-21 06:35:58 +00:00
def get_context_data(self, **kwargs):
context = super(ProblemDetail, self).get_context_data(**kwargs)
user = self.request.user
authed = user.is_authenticated
2022-05-14 17:57:27 +00:00
context["has_submissions"] = (
authed
and Submission.objects.filter(
user=user.profile, problem=self.object
).exists()
)
contest_problem = (
None
if not authed or user.profile.current_contest is None
else get_contest_problem(self.object, user.profile)
)
context["contest_problem"] = contest_problem
2021-07-19 01:22:44 +00:00
2020-01-21 06:35:58 +00:00
if contest_problem:
clarifications = contest_problem.clarifications
2022-05-14 17:57:27 +00:00
context["has_clarifications"] = clarifications.count() > 0
context["clarifications"] = clarifications.order_by("-date")
context["submission_limit"] = contest_problem.max_submissions
2020-01-21 06:35:58 +00:00
if contest_problem.max_submissions:
2022-05-14 17:57:27 +00:00
context["submissions_left"] = max(
contest_problem.max_submissions
- get_contest_submission_count(
self.object, user.profile, user.profile.current_contest.virtual
),
0,
)
context["available_judges"] = Judge.objects.filter(
online=True, problems=self.object
)
context["show_languages"] = (
self.object.allowed_languages.count() != Language.objects.count()
)
context["has_pdf_render"] = HAS_PDF
context["completed_problem_ids"] = self.get_completed_problems()
context["attempted_problems"] = self.get_attempted_problems()
2020-01-21 06:35:58 +00:00
can_edit = self.object.is_editable_by(user)
2022-05-14 17:57:27 +00:00
context["can_edit_problem"] = can_edit
2020-01-21 06:35:58 +00:00
if user.is_authenticated:
tickets = self.object.tickets
if not can_edit:
tickets = tickets.filter(own_ticket_filter(user.profile.id))
2022-05-14 17:57:27 +00:00
context["has_tickets"] = tickets.exists()
context["num_open_tickets"] = (
tickets.filter(is_open=True).values("id").distinct().count()
)
2020-01-21 06:35:58 +00:00
try:
2022-05-14 17:57:27 +00:00
context["editorial"] = Solution.objects.get(problem=self.object)
2020-01-21 06:35:58 +00:00
except ObjectDoesNotExist:
pass
try:
2022-05-14 17:57:27 +00:00
translation = self.object.translations.get(
language=self.request.LANGUAGE_CODE
)
2020-01-21 06:35:58 +00:00
except ProblemTranslation.DoesNotExist:
2022-05-14 17:57:27 +00:00
context["title"] = self.object.name
context["language"] = settings.LANGUAGE_CODE
context["description"] = self.object.description
context["translated"] = False
2020-01-21 06:35:58 +00:00
else:
2022-05-14 17:57:27 +00:00
context["title"] = translation.name
context["language"] = self.request.LANGUAGE_CODE
context["description"] = translation.description
context["translated"] = True
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-problem:%s:%d" % (context["language"], self.object.id),
context["description"],
)
context["meta_description"] = self.object.summary or metadata[0]
context["og_image"] = self.object.og_image or metadata[1]
2022-06-02 16:21:55 +00:00
if hasattr(self.object, "data_files"):
context["fileio_input"] = self.object.data_files.fileio_input
context["fileio_output"] = self.object.data_files.fileio_output
else:
context["fileio_input"] = None
context["fileio_output"] = None
2024-01-14 00:35:48 +00:00
if not self.in_contest and settings.ML_OUTPUT_PATH:
2023-01-28 01:15:37 +00:00
context["related_problems"] = get_related_problems(
self.profile, self.object
)
2022-05-14 17:57:27 +00:00
2020-01-21 06:35:58 +00:00
return context
2022-03-21 21:09:16 +00:00
2020-01-21 06:35:58 +00:00
class LatexError(Exception):
pass
class ProblemPdfView(ProblemMixin, SingleObjectMixin, View):
2022-05-14 17:57:27 +00:00
logger = logging.getLogger("judge.problem.pdf")
2020-01-21 06:35:58 +00:00
languages = set(map(itemgetter(0), settings.LANGUAGES))
def get(self, request, *args, **kwargs):
if not HAS_PDF:
raise Http404()
2022-05-14 17:57:27 +00:00
language = kwargs.get("language", self.request.LANGUAGE_CODE)
2020-01-21 06:35:58 +00:00
if language not in self.languages:
raise Http404()
problem = self.get_object()
try:
trans = problem.translations.get(language=language)
except ProblemTranslation.DoesNotExist:
trans = None
2022-05-14 17:57:27 +00:00
cache = os.path.join(
settings.DMOJ_PDF_PROBLEM_CACHE, "%s.%s.pdf" % (problem.code, language)
)
2020-01-21 06:35:58 +00:00
if not os.path.exists(cache):
2022-05-14 17:57:27 +00:00
self.logger.info("Rendering: %s.%s.pdf", problem.code, language)
2020-01-21 06:35:58 +00:00
with DefaultPdfMaker() as maker, translation.override(language):
problem_name = problem.name if trans is None else trans.name
2022-05-14 17:57:27 +00:00
maker.html = (
get_template("problem/raw.html")
.render(
{
"problem": problem,
"problem_name": problem_name,
"description": problem.description
if trans is None
else trans.description,
"url": request.build_absolute_uri(),
"math_engine": maker.math_engine,
}
)
.replace('"//', '"https://')
.replace("'//", "'https://")
)
2020-01-21 06:35:58 +00:00
maker.title = problem_name
2022-05-14 17:57:27 +00:00
assets = ["style.css", "pygment-github.css"]
if maker.math_engine == "jax":
2022-11-25 18:52:49 +00:00
assets.append("mathjax3_config.js")
2020-01-21 06:35:58 +00:00
for file in assets:
maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file))
maker.make()
if not maker.success:
2022-05-14 17:57:27 +00:00
self.logger.error("Failed to render PDF for %s", problem.code)
return HttpResponse(
maker.log, status=500, content_type="text/plain"
)
2020-01-21 06:35:58 +00:00
shutil.move(maker.pdffile, cache)
response = HttpResponse()
2022-05-14 17:57:27 +00:00
if hasattr(settings, "DMOJ_PDF_PROBLEM_INTERNAL") and request.META.get(
"SERVER_SOFTWARE", ""
).startswith("nginx/"):
response["X-Accel-Redirect"] = "%s/%s.%s.pdf" % (
settings.DMOJ_PDF_PROBLEM_INTERNAL,
problem.code,
language,
)
2020-01-21 06:35:58 +00:00
else:
2022-05-14 17:57:27 +00:00
with open(cache, "rb") as f:
2020-01-21 06:35:58 +00:00
response.content = f.read()
2022-05-14 17:57:27 +00:00
response["Content-Type"] = "application/pdf"
response["Content-Disposition"] = "inline; filename=%s.%s.pdf" % (
problem.code,
language,
)
2020-01-21 06:35:58 +00:00
return response
2022-08-31 03:50:08 +00:00
class ProblemPdfDescriptionView(ProblemMixin, SingleObjectMixin, View):
def get(self, request, *args, **kwargs):
problem = self.get_object()
if not problem.pdf_description:
raise Http404()
response = HttpResponse()
2023-03-09 03:31:41 +00:00
# if request.META.get("SERVER_SOFTWARE", "").startswith("nginx/"):
# response["X-Accel-Redirect"] = problem.pdf_description.path
# else:
with open(problem.pdf_description.path, "rb") as f:
response.content = f.read()
2022-08-31 03:50:08 +00:00
response["Content-Type"] = "application/pdf"
2022-08-31 05:23:23 +00:00
response["Content-Disposition"] = "inline; filename=%s.pdf" % (problem.code,)
2022-08-31 03:50:08 +00:00
return response
2020-01-21 06:35:58 +00:00
class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView):
model = Problem
2022-05-14 17:57:27 +00:00
title = gettext_lazy("Problems")
context_object_name = "problems"
template_name = "problem/list.html"
2020-01-21 06:35:58 +00:00
paginate_by = 50
2022-05-14 17:57:27 +00:00
sql_sort = frozenset(("date", "points", "ac_rate", "user_count", "code"))
manual_sort = frozenset(("name", "group", "solved", "type"))
2020-01-21 06:35:58 +00:00
all_sorts = sql_sort | manual_sort
2022-05-14 17:57:27 +00:00
default_desc = frozenset(("date", "points", "ac_rate", "user_count"))
2022-04-14 20:40:48 +00:00
first_page_href = None
2023-01-24 02:36:44 +00:00
filter_organization = False
2020-01-21 06:35:58 +00:00
2023-10-14 19:56:22 +00:00
def get_default_sort_order(self, request):
if "search" in request.GET and settings.ENABLE_FTS:
return "-relevance"
return "-date"
2022-05-14 17:57:27 +00:00
def get_paginator(
self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs
):
paginator = DiggPaginator(
queryset,
per_page,
body=6,
padding=2,
orphans=orphans,
allow_empty_first_page=allow_empty_first_page,
2023-02-18 22:38:47 +00:00
count=queryset.values("pk").count() if not self.in_contest else None,
2023-03-10 04:31:55 +00:00
**kwargs,
2022-05-14 17:57:27 +00:00
)
2020-01-21 06:35:58 +00:00
if not self.in_contest:
queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE)
2023-10-14 19:56:22 +00:00
queryset = self.sort_queryset(queryset)
2020-01-21 06:35:58 +00:00
paginator.object_list = queryset
return paginator
2023-10-14 19:56:22 +00:00
def sort_queryset(self, queryset):
sort_key = self.order.lstrip("-")
if sort_key in self.sql_sort:
queryset = queryset.order_by(self.order)
elif sort_key == "name":
queryset = queryset.order_by(self.order.replace("name", "i18n_name"))
elif sort_key == "group":
queryset = queryset.order_by(self.order + "__name")
elif sort_key == "solved":
if self.request.user.is_authenticated:
profile = self.request.profile
solved = user_completed_ids(profile)
attempted = user_attempted_ids(profile)
def _solved_sort_order(problem):
if problem.id in solved:
return 1
if problem.id in attempted:
return 0
return -1
queryset = list(queryset)
queryset.sort(
key=_solved_sort_order, reverse=self.order.startswith("-")
)
elif sort_key == "type":
if self.show_types:
queryset = list(queryset)
queryset.sort(
key=lambda problem: problem.types_list[0]
if problem.types_list
else "",
reverse=self.order.startswith("-"),
)
return queryset
2020-01-21 06:35:58 +00:00
@cached_property
def profile(self):
if not self.request.user.is_authenticated:
return None
return self.request.profile
def get_contest_queryset(self):
2022-05-14 17:57:27 +00:00
queryset = (
self.profile.current_contest.contest.contest_problems.select_related(
"problem__group"
)
.defer("problem__description")
.order_by("problem__code")
.annotate(user_count=Count("submission__participation", distinct=True))
2022-11-01 01:43:06 +00:00
.annotate(
i18n_translation=FilteredRelation(
"problem__translations",
condition=Q(
problem__translations__language=self.request.LANGUAGE_CODE
),
)
)
.annotate(
i18n_name=Coalesce(
F("i18n_translation__name"),
F("problem__name"),
output_field=CharField(),
)
)
2022-05-14 17:57:27 +00:00
.order_by("order")
)
return [
{
"id": p["problem_id"],
"code": p["problem__code"],
"name": p["problem__name"],
"i18n_name": p["i18n_name"],
"group": {"full_name": p["problem__group__full_name"]},
"points": p["points"],
"partial": p["partial"],
"user_count": p["user_count"],
}
for p in queryset.values(
"problem_id",
"problem__code",
"problem__name",
"i18n_name",
"problem__group__full_name",
"points",
"partial",
"user_count",
)
]
2020-01-21 06:35:58 +00:00
2022-05-28 04:28:22 +00:00
def get_org_query(self, query):
2022-05-30 06:59:53 +00:00
if not self.profile:
2022-06-03 19:01:49 +00:00
return []
2022-05-28 04:28:22 +00:00
return [
i
for i in query
if i in self.profile.organizations.values_list("id", flat=True)
]
2020-01-21 06:35:58 +00:00
def get_normal_queryset(self):
2023-01-27 23:51:38 +00:00
queryset = Problem.get_visible_problems(self.request.user)
queryset = queryset.select_related("group")
2020-01-21 06:35:58 +00:00
if self.profile is not None and self.hide_solved:
2023-01-27 23:58:44 +00:00
solved_problems = self.get_completed_problems()
queryset = queryset.exclude(id__in=solved_problems)
2023-01-24 02:36:44 +00:00
if not self.org_query and self.request.organization:
self.org_query = [self.request.organization.id]
2021-02-20 07:36:16 +00:00
if self.org_query:
2022-05-28 04:28:22 +00:00
self.org_query = self.get_org_query(self.org_query)
2021-02-20 07:36:16 +00:00
queryset = queryset.filter(
2022-05-14 17:57:27 +00:00
Q(organizations__in=self.org_query)
| Q(contests__contest__organizations__in=self.org_query)
)
2022-05-22 01:30:44 +00:00
if self.author_query:
queryset = queryset.filter(authors__in=self.author_query)
2020-01-21 06:35:58 +00:00
if self.show_types:
2022-05-14 17:57:27 +00:00
queryset = queryset.prefetch_related("types")
2020-01-21 06:35:58 +00:00
if self.category is not None:
queryset = queryset.filter(group__id=self.category)
if self.selected_types:
queryset = queryset.filter(types__in=self.selected_types)
2022-05-14 17:57:27 +00:00
if "search" in self.request.GET:
self.search_query = query = " ".join(
self.request.GET.getlist("search")
).strip()
2020-01-21 06:35:58 +00:00
if query:
2023-10-14 19:56:22 +00:00
substr_queryset = queryset.filter(
Q(code__icontains=query)
| Q(name__icontains=query)
| Q(
translations__name__icontains=query,
translations__language=self.request.LANGUAGE_CODE,
2022-05-14 17:57:27 +00:00
)
2023-10-14 19:56:22 +00:00
)
if settings.ENABLE_FTS:
queryset = (
queryset.search(query, queryset.BOOLEAN).extra(
order_by=["-relevance"]
2022-05-14 17:57:27 +00:00
)
2023-10-14 19:56:22 +00:00
| substr_queryset
2022-05-14 17:57:27 +00:00
)
2023-10-14 19:56:22 +00:00
else:
queryset = substr_queryset
2020-01-21 06:35:58 +00:00
self.prepoint_queryset = queryset
if self.point_start is not None:
queryset = queryset.filter(points__gte=self.point_start)
if self.point_end is not None:
queryset = queryset.filter(points__lte=self.point_end)
return queryset.distinct()
def get_queryset(self):
if self.in_contest:
return self.get_contest_queryset()
else:
return self.get_normal_queryset()
def get_context_data(self, **kwargs):
context = super(ProblemList, self).get_context_data(**kwargs)
2022-05-14 17:57:27 +00:00
2023-01-24 02:36:44 +00:00
if self.request.organization:
self.filter_organization = True
2022-05-14 17:57:27 +00:00
context["hide_solved"] = 0 if self.in_contest else int(self.hide_solved)
context["show_types"] = 0 if self.in_contest else int(self.show_types)
context["full_text"] = 0 if self.in_contest else int(self.full_text)
context["show_editorial"] = 0 if self.in_contest else int(self.show_editorial)
2022-07-18 05:59:45 +00:00
context["show_solved_only"] = (
0 if self.in_contest else int(self.show_solved_only)
)
2022-05-14 17:57:27 +00:00
2022-05-28 04:28:22 +00:00
if self.request.profile:
context["organizations"] = self.request.profile.organizations.all()
2023-02-18 22:38:47 +00:00
all_authors_ids = Problem.objects.values_list("authors", flat=True)
context["all_authors"] = (
Profile.objects.filter(id__in=all_authors_ids)
.select_related("user")
.values("id", "user__username")
)
2022-05-14 17:57:27 +00:00
context["category"] = self.category
context["categories"] = ProblemGroup.objects.all()
2020-01-21 06:35:58 +00:00
if self.show_types:
2022-05-14 17:57:27 +00:00
context["selected_types"] = self.selected_types
context["problem_types"] = ProblemType.objects.all()
context["has_fts"] = settings.ENABLE_FTS
context["org_query"] = self.org_query
2022-05-22 01:30:44 +00:00
context["author_query"] = self.author_query
2022-05-14 17:57:27 +00:00
context["search_query"] = self.search_query
context["completed_problem_ids"] = self.get_completed_problems()
context["attempted_problems"] = self.get_attempted_problems()
2023-01-24 02:36:44 +00:00
context["last_attempted_problems"] = self.get_latest_attempted_problems(
15, context["problems"] if self.filter_organization else None
)
2022-05-14 17:57:27 +00:00
context["page_type"] = "list"
2020-01-21 06:35:58 +00:00
context.update(self.get_sort_paginate_context())
if not self.in_contest:
context.update(self.get_sort_context())
2022-05-14 17:57:27 +00:00
(
context["point_start"],
context["point_end"],
context["point_values"],
) = self.get_noui_slider_points()
2020-01-21 06:35:58 +00:00
else:
2022-05-14 17:57:27 +00:00
context["point_start"], context["point_end"], context["point_values"] = (
0,
0,
{},
)
context["hide_contest_scoreboard"] = self.contest.scoreboard_visibility in (
self.contest.SCOREBOARD_AFTER_CONTEST,
self.contest.SCOREBOARD_AFTER_PARTICIPATION,
)
context["has_clarifications"] = False
2021-10-19 22:41:53 +00:00
if self.request.user.is_authenticated:
participation = self.request.profile.current_contest
if participation:
clarifications = ContestProblemClarification.objects.filter(
problem__in=participation.contest.contest_problems.all()
2022-05-14 17:57:27 +00:00
)
context["has_clarifications"] = clarifications.count() > 0
context["clarifications"] = clarifications.order_by("-date")
2021-10-19 22:41:53 +00:00
if participation.contest.is_editable_by(self.request.user):
2022-05-14 17:57:27 +00:00
context["can_edit_contest"] = True
context["page_prefix"] = None
context["page_suffix"] = suffix = (
("?" + self.request.GET.urlencode()) if self.request.GET else ""
)
context["first_page_href"] = (self.first_page_href or ".") + suffix
2022-05-28 04:28:22 +00:00
context["has_show_editorial_option"] = True
context["show_contest_mode"] = self.request.in_contest_mode
2020-01-21 06:35:58 +00:00
return context
def get_noui_slider_points(self):
2022-05-14 17:57:27 +00:00
points = sorted(
self.prepoint_queryset.values_list("points", flat=True).distinct()
)
2020-01-21 06:35:58 +00:00
if not points:
return 0, 0, {}
if len(points) == 1:
2022-05-14 17:57:27 +00:00
return (
points[0],
points[0],
{
"min": points[0] - 1,
"max": points[0] + 1,
},
)
2020-01-21 06:35:58 +00:00
start, end = points[0], points[-1]
if self.point_start is not None:
start = self.point_start
if self.point_end is not None:
end = self.point_end
2022-05-14 17:57:27 +00:00
points_map = {0.0: "min", 1.0: "max"}
2020-01-21 06:35:58 +00:00
size = len(points) - 1
2022-05-14 17:57:27 +00:00
return (
start,
end,
{
points_map.get(i / size, "%.2f%%" % (100 * i / size,)): j
for i, j in enumerate(points)
},
)
2020-01-21 06:35:58 +00:00
def GET_with_session(self, request, key):
2023-02-22 01:00:05 +00:00
if not request.GET.get(key):
2020-01-21 06:35:58 +00:00
return request.session.get(key, False)
2022-05-14 17:57:27 +00:00
return request.GET.get(key, None) == "1"
2020-01-21 06:35:58 +00:00
def setup_problem_list(self, request):
2022-05-14 17:57:27 +00:00
self.hide_solved = self.GET_with_session(request, "hide_solved")
self.show_types = self.GET_with_session(request, "show_types")
self.full_text = self.GET_with_session(request, "full_text")
self.show_editorial = self.GET_with_session(request, "show_editorial")
2022-07-18 05:59:45 +00:00
self.show_solved_only = self.GET_with_session(request, "show_solved_only")
2022-05-14 17:57:27 +00:00
2020-01-21 06:35:58 +00:00
self.search_query = None
self.category = None
2021-02-20 07:36:16 +00:00
self.org_query = []
2022-05-22 01:30:44 +00:00
self.author_query = []
2020-01-21 06:35:58 +00:00
self.selected_types = []
# This actually copies into the instance dictionary...
self.all_sorts = set(self.all_sorts)
if not self.show_types:
2022-05-14 17:57:27 +00:00
self.all_sorts.discard("type")
2020-01-21 06:35:58 +00:00
2022-05-14 17:57:27 +00:00
self.category = safe_int_or_none(request.GET.get("category"))
if "type" in request.GET:
2020-01-21 06:35:58 +00:00
try:
2022-05-14 17:57:27 +00:00
self.selected_types = list(map(int, request.GET.getlist("type")))
2020-01-21 06:35:58 +00:00
except ValueError:
pass
2022-05-14 17:57:27 +00:00
if "orgs" in request.GET:
2021-02-20 07:36:16 +00:00
try:
2022-05-14 17:57:27 +00:00
self.org_query = list(map(int, request.GET.getlist("orgs")))
2021-02-20 07:36:16 +00:00
except ValueError:
pass
2022-05-22 01:30:44 +00:00
if "authors" in request.GET:
try:
self.author_query = list(map(int, request.GET.getlist("authors")))
except ValueError:
pass
2021-02-20 07:36:16 +00:00
2022-05-14 17:57:27 +00:00
self.point_start = safe_float_or_none(request.GET.get("point_start"))
self.point_end = safe_float_or_none(request.GET.get("point_end"))
2020-01-21 06:35:58 +00:00
def get(self, request, *args, **kwargs):
self.setup_problem_list(request)
try:
return super(ProblemList, self).get(request, *args, **kwargs)
except ProgrammingError as e:
2022-05-14 17:57:27 +00:00
return generic_message(request, "FTS syntax error", e.args[1], status=400)
2020-01-21 06:35:58 +00:00
def post(self, request, *args, **kwargs):
2022-05-14 17:57:27 +00:00
to_update = (
"hide_solved",
"show_types",
"full_text",
"show_editorial",
2022-07-18 05:59:45 +00:00
"show_solved_only",
2022-05-14 17:57:27 +00:00
)
2020-01-21 06:35:58 +00:00
for key in to_update:
if key in request.GET:
2022-05-14 17:57:27 +00:00
val = request.GET.get(key) == "1"
2020-01-21 06:35:58 +00:00
request.session[key] = val
else:
2022-04-13 05:52:03 +00:00
request.session[key] = False
2020-01-21 06:35:58 +00:00
return HttpResponseRedirect(request.get_full_path())
class ProblemFeed(ProblemList, FeedView):
2022-03-21 21:09:16 +00:00
model = Problem
2022-05-14 17:57:27 +00:00
context_object_name = "problems"
2022-05-28 04:28:22 +00:00
template_name = "problem/feed.html"
feed_content_template_name = "problem/feed/problems.html"
paginate_by = 4
2022-05-14 17:57:27 +00:00
title = _("Problem feed")
2022-04-13 05:52:03 +00:00
feed_type = None
2022-03-21 21:09:16 +00:00
2023-11-09 08:43:11 +00:00
def get_recommended_problem_ids(self, queryset):
user_id = self.request.profile.id
problem_ids = queryset.values_list("id", flat=True)
rec_types = [
RecommendationType.CF_DOT,
RecommendationType.CF_COSINE,
RecommendationType.CF_TIME_DOT,
RecommendationType.CF_TIME_COSINE,
RecommendationType.HOT_PROBLEM,
]
limits = [100, 100, 100, 100, 20]
shuffle = True
allow_debug_type = (
self.request.user.is_impersonate or self.request.user.is_superuser
)
if allow_debug_type and "debug_type" in self.request.GET:
try:
debug_type = int(self.request.GET.get("debug_type"))
except ValueError:
raise Http404()
rec_types = [debug_type]
limits = [100]
shuffle = False
return get_user_recommended_problems(
user_id, problem_ids, rec_types, limits, shuffle
)
2022-04-12 02:18:01 +00:00
2022-03-21 21:09:16 +00:00
def get_queryset(self):
2022-07-18 05:59:45 +00:00
if self.feed_type == "volunteer":
self.hide_solved = 0
self.show_types = 1
2022-04-13 05:52:03 +00:00
queryset = super(ProblemFeed, self).get_queryset()
2022-05-14 17:57:27 +00:00
2022-04-12 02:18:01 +00:00
user = self.request.profile
2022-04-13 05:52:03 +00:00
2022-05-14 17:57:27 +00:00
if self.feed_type == "new":
2022-10-29 08:11:15 +00:00
return queryset.order_by("-date").add_i18n_name(self.request.LANGUAGE_CODE)
2022-05-14 17:57:27 +00:00
elif user and self.feed_type == "volunteer":
2022-09-29 17:23:16 +00:00
voted_problems = (
user.volunteer_problem_votes.values_list("problem", flat=True)
if not bool(self.search_query)
else []
2022-05-14 17:57:27 +00:00
)
2022-07-18 05:59:45 +00:00
if self.show_solved_only:
queryset = queryset.filter(
id__in=Submission.objects.filter(
user=self.profile, points=F("problem__points")
).values_list("problem__id", flat=True)
)
2022-10-29 08:11:15 +00:00
return (
queryset.exclude(id__in=voted_problems)
.order_by("?")
.add_i18n_name(self.request.LANGUAGE_CODE)
)
2023-10-14 19:56:22 +00:00
if "search" in self.request.GET:
return queryset.add_i18n_name(self.request.LANGUAGE_CODE)
2022-04-12 02:18:01 +00:00
if not settings.ML_OUTPUT_PATH or not user:
2022-10-29 08:11:15 +00:00
return queryset.order_by("?").add_i18n_name(self.request.LANGUAGE_CODE)
2022-05-14 17:57:27 +00:00
2023-11-09 08:43:11 +00:00
q = self.get_recommended_problem_ids(queryset)
2022-04-16 21:05:55 +00:00
2022-10-29 02:29:48 +00:00
queryset = Problem.objects.filter(id__in=q)
queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE)
# Reorder results from database to correct positions
res = [None for _ in range(len(q))]
position_in_q = {i: idx for idx, i in enumerate(q)}
for problem in queryset:
res[position_in_q[problem.id]] = problem
return res
2022-03-21 21:09:16 +00:00
2023-02-22 00:20:06 +00:00
def get_feed_context(self, object_list):
return {
"completed_problem_ids": self.get_completed_problems(),
"attempted_problems": self.get_attempted_problems(),
2023-05-31 09:04:47 +00:00
"show_types": self.show_types,
2023-02-22 00:20:06 +00:00
}
2022-03-21 21:09:16 +00:00
def get_context_data(self, **kwargs):
context = super(ProblemFeed, self).get_context_data(**kwargs)
2022-05-14 17:57:27 +00:00
context["page_type"] = "feed"
context["title"] = self.title
context["feed_type"] = self.feed_type
2022-05-28 04:28:22 +00:00
context["has_show_editorial_option"] = False
2022-03-21 21:09:16 +00:00
return context
2022-04-13 05:52:03 +00:00
def get(self, request, *args, **kwargs):
if request.in_contest_mode:
2022-05-14 17:57:27 +00:00
return HttpResponseRedirect(reverse("problem_list"))
2022-04-13 05:52:03 +00:00
return super(ProblemFeed, self).get(request, *args, **kwargs)
2022-03-21 21:09:16 +00:00
2020-01-21 06:35:58 +00:00
class LanguageTemplateAjax(View):
def get(self, request, *args, **kwargs):
try:
2022-06-12 07:57:46 +00:00
problem = request.GET.get("problem", None)
lang_id = int(request.GET.get("id", 0))
res = None
if problem:
try:
res = LanguageTemplate.objects.get(
language__id=lang_id, problem__id=problem
).source
except ObjectDoesNotExist:
pass
if not res:
res = get_object_or_404(Language, id=lang_id).template
2020-01-21 06:35:58 +00:00
except ValueError:
raise Http404()
2022-06-12 07:57:46 +00:00
return HttpResponse(res, content_type="text/plain")
2020-01-21 06:35:58 +00:00
class RandomProblem(ProblemList):
def get(self, request, *args, **kwargs):
self.setup_problem_list(request)
if self.in_contest:
raise Http404()
queryset = self.get_normal_queryset()
count = queryset.count()
if not count:
2022-05-14 17:57:27 +00:00
return HttpResponseRedirect(
"%s%s%s"
% (
reverse("problem_list"),
request.META["QUERY_STRING"] and "?",
request.META["QUERY_STRING"],
)
)
2020-01-21 06:35:58 +00:00
return HttpResponseRedirect(queryset[randrange(count)].get_absolute_url())
2022-05-14 17:57:27 +00:00
user_logger = logging.getLogger("judge.user")
2020-01-21 06:35:58 +00:00
@login_required
2020-07-19 21:39:28 +00:00
def problem_submit(request, problem, submission=None):
2022-05-14 17:57:27 +00:00
if (
submission is not None
and not request.user.has_perm("judge.resubmit_other")
and get_object_or_404(Submission, id=int(submission)).user.user != request.user
):
2020-01-21 06:35:58 +00:00
raise PermissionDenied()
profile = request.profile
2020-07-19 21:39:28 +00:00
problem = get_object_or_404(Problem, code=problem)
if not problem.is_accessible_by(request.user):
2022-05-14 17:57:27 +00:00
if request.method == "POST":
user_logger.info(
"Naughty user %s wants to submit to %s without permission",
request.user.username,
problem.code,
)
return HttpResponseForbidden("<h1>Not allowed to submit. Try later.</h1>")
2020-07-19 21:39:28 +00:00
raise Http404()
2020-07-19 21:27:14 +00:00
if problem.is_editable_by(request.user):
2022-05-14 17:57:27 +00:00
judge_choices = tuple(
Judge.objects.filter(online=True, problems=problem).values_list(
"name", "name"
)
)
2020-07-19 21:27:14 +00:00
else:
judge_choices = ()
2022-05-14 17:57:27 +00:00
if request.method == "POST":
form = ProblemSubmitForm(
request.POST,
2023-03-10 04:31:55 +00:00
request.FILES,
2022-05-14 17:57:27 +00:00
judge_choices=judge_choices,
instance=Submission(user=profile, problem=problem),
2023-03-10 04:31:55 +00:00
request=request,
2023-03-26 08:00:05 +00:00
problem=problem,
2022-05-14 17:57:27 +00:00
)
2020-01-21 06:35:58 +00:00
if form.is_valid():
2022-05-14 17:57:27 +00:00
if (
not request.user.has_perm("judge.spam_submission")
and Submission.objects.filter(user=profile, was_rejudged=False)
.exclude(status__in=["D", "IE", "CE", "AB"])
.count()
>= settings.DMOJ_SUBMISSION_LIMIT
):
return HttpResponse(
"<h1>You submitted too many submissions.</h1>", status=429
)
if not problem.allowed_languages.filter(
id=form.cleaned_data["language"].id
).exists():
2020-01-21 06:35:58 +00:00
raise PermissionDenied()
2022-05-14 17:57:27 +00:00
if (
not request.user.is_superuser
and problem.banned_users.filter(id=profile.id).exists()
):
return generic_message(
request,
_("Banned from submitting"),
_(
"You have been declared persona non grata for this problem. "
"You are permanently barred from submitting this problem."
),
)
2020-01-21 06:35:58 +00:00
with transaction.atomic():
if profile.current_contest is not None:
contest_id = profile.current_contest.contest_id
try:
2020-07-19 21:39:28 +00:00
contest_problem = problem.contests.get(contest_id=contest_id)
2020-01-21 06:35:58 +00:00
except ContestProblem.DoesNotExist:
model = form.save()
else:
max_subs = contest_problem.max_submissions
2022-05-14 17:57:27 +00:00
if (
max_subs
and get_contest_submission_count(
problem, profile, profile.current_contest.virtual
)
>= max_subs
):
return generic_message(
request,
_("Too many submissions"),
_(
"You have exceeded the submission limit for this problem."
),
)
2020-01-21 06:35:58 +00:00
model = form.save()
model.contest_object_id = contest_id
2022-05-14 17:57:27 +00:00
contest = ContestSubmission(
submission=model,
problem=contest_problem,
participation=profile.current_contest,
)
2020-01-21 06:35:58 +00:00
contest.save()
else:
model = form.save()
# Create the SubmissionSource object
2022-05-14 17:57:27 +00:00
source = SubmissionSource(
submission=model, source=form.cleaned_data["source"]
)
2020-01-21 06:35:58 +00:00
source.save()
profile.update_contest()
# Save a query
model.source = source
2022-05-14 17:57:27 +00:00
model.judge(rejudge=False, judge_id=form.cleaned_data["judge"])
2020-01-21 06:35:58 +00:00
2022-05-14 17:57:27 +00:00
return HttpResponseRedirect(
reverse("submission_status", args=[str(model.id)])
)
2020-01-21 06:35:58 +00:00
else:
form_data = form.cleaned_data
if submission is not None:
sub = get_object_or_404(Submission, id=int(submission))
else:
2022-05-14 17:57:27 +00:00
initial = {"language": profile.language}
2020-01-21 06:35:58 +00:00
if submission is not None:
try:
2022-05-14 17:57:27 +00:00
sub = get_object_or_404(
Submission.objects.select_related("source", "language"),
id=int(submission),
)
initial["source"] = sub.source.source
initial["language"] = sub.language
2020-01-21 06:35:58 +00:00
except ValueError:
raise Http404()
2020-07-19 21:27:14 +00:00
form = ProblemSubmitForm(judge_choices=judge_choices, initial=initial)
2020-01-21 06:35:58 +00:00
form_data = initial
2022-05-14 17:57:27 +00:00
form.fields["language"].queryset = problem.usable_languages.order_by(
"name", "key"
).prefetch_related(
Prefetch("runtimeversion_set", RuntimeVersion.objects.order_by("priority"))
2020-07-19 21:39:28 +00:00
)
2022-05-14 17:57:27 +00:00
if "language" in form_data:
form.fields["source"].widget.mode = form_data["language"].ace
form.fields["source"].widget.theme = profile.ace_theme
2020-01-21 06:35:58 +00:00
if submission is not None:
default_lang = sub.language
else:
default_lang = request.profile.language
submission_limit = submissions_left = None
if profile.current_contest is not None:
try:
2022-05-14 17:57:27 +00:00
submission_limit = problem.contests.get(
contest=profile.current_contest.contest
).max_submissions
2020-01-21 06:35:58 +00:00
except ContestProblem.DoesNotExist:
pass
else:
if submission_limit:
2022-05-14 17:57:27 +00:00
submissions_left = submission_limit - get_contest_submission_count(
problem, profile, profile.current_contest.virtual
)
return render(
request,
"problem/submit.html",
{
"form": form,
"title": _("Submit to %(problem)s")
% {
"problem": problem.translated_name(request.LANGUAGE_CODE),
},
"content_title": mark_safe(
escape(_("Submit to %(problem)s"))
% {
"problem": format_html(
'<a href="{0}">{1}</a>',
reverse("problem_detail", args=[problem.code]),
problem.translated_name(request.LANGUAGE_CODE),
),
}
),
"langs": Language.objects.all(),
"no_judges": not form.fields["language"].queryset,
"submission_limit": submission_limit,
"submissions_left": submissions_left,
"ACE_URL": settings.ACE_URL,
"default_lang": default_lang,
2022-06-12 07:57:46 +00:00
"problem_id": problem.id,
2023-03-18 03:24:23 +00:00
"output_only": problem.data_files.output_only
if hasattr(problem, "data_files")
else False,
2020-01-21 06:35:58 +00:00
},
2022-05-14 17:57:27 +00:00
)
class ProblemClone(
ProblemMixin, PermissionRequiredMixin, TitleMixin, SingleObjectFormView
):
title = _("Clone Problem")
template_name = "problem/clone.html"
2020-01-21 06:35:58 +00:00
form_class = ProblemCloneForm
2022-05-14 17:57:27 +00:00
permission_required = "judge.clone_problem"
2020-01-21 06:35:58 +00:00
def form_valid(self, form):
2022-11-07 21:39:10 +00:00
languages = self.object.allowed_languages.all()
language_limits = self.object.language_limits.all()
types = self.object.types.all()
problem = deepcopy(self.object)
2020-01-21 06:35:58 +00:00
problem.pk = None
problem.is_public = False
problem.ac_rate = 0
problem.user_count = 0
2022-05-14 17:57:27 +00:00
problem.code = form.cleaned_data["code"]
2024-01-04 17:25:39 +00:00
problem.save(should_move_data=False)
2020-01-21 06:35:58 +00:00
problem.authors.add(self.request.profile)
problem.allowed_languages.set(languages)
problem.language_limits.set(language_limits)
problem.types.set(types)
2022-05-14 17:57:27 +00:00
return HttpResponseRedirect(
reverse("admin:judge_problem_change", args=(problem.id,))
)