2020-01-21 06:35:58 +00:00
|
|
|
import json
|
|
|
|
import mimetypes
|
|
|
|
import os
|
|
|
|
from itertools import chain
|
2021-11-28 03:28:48 +00:00
|
|
|
import shutil
|
|
|
|
from tempfile import gettempdir
|
2020-01-21 06:35:58 +00:00
|
|
|
from zipfile import BadZipfile, ZipFile
|
|
|
|
|
2021-11-28 03:28:48 +00:00
|
|
|
from django import forms
|
|
|
|
from django.conf import settings
|
|
|
|
from django.http import HttpResponse, HttpRequest
|
|
|
|
from django.shortcuts import render
|
|
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
|
|
from django.views.generic import View
|
|
|
|
|
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 LoginRequiredMixin
|
2021-11-28 03:28:48 +00:00
|
|
|
from django.core.files import File
|
2020-01-21 06:35:58 +00:00
|
|
|
from django.core.exceptions import ValidationError
|
2022-05-14 17:57:27 +00:00
|
|
|
from django.forms import (
|
|
|
|
BaseModelFormSet,
|
|
|
|
HiddenInput,
|
|
|
|
ModelForm,
|
|
|
|
NumberInput,
|
|
|
|
Select,
|
|
|
|
formset_factory,
|
|
|
|
FileInput,
|
2022-06-02 04:59:46 +00:00
|
|
|
TextInput,
|
2023-03-10 04:31:55 +00:00
|
|
|
CheckboxInput,
|
2022-05-14 17:57:27 +00:00
|
|
|
)
|
2021-11-28 03:28:48 +00:00
|
|
|
from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
|
2020-01-21 06:35:58 +00:00
|
|
|
from django.shortcuts import get_object_or_404, render
|
|
|
|
from django.urls import reverse
|
|
|
|
from django.utils.html import escape, format_html
|
|
|
|
from django.utils.safestring import mark_safe
|
|
|
|
from django.utils.translation import gettext as _
|
|
|
|
from django.views.generic import DetailView
|
|
|
|
|
|
|
|
from judge.highlight_code import highlight_code
|
2022-05-14 17:57:27 +00:00
|
|
|
from judge.models import (
|
|
|
|
Problem,
|
|
|
|
ProblemData,
|
|
|
|
ProblemTestCase,
|
|
|
|
Submission,
|
|
|
|
problem_data_storage,
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
from judge.utils.problem_data import ProblemDataCompiler
|
|
|
|
from judge.utils.unicode import utf8text
|
|
|
|
from judge.utils.views import TitleMixin
|
2022-05-14 17:57:27 +00:00
|
|
|
from judge.utils.fine_uploader import (
|
|
|
|
combine_chunks,
|
|
|
|
save_upload,
|
|
|
|
handle_upload,
|
|
|
|
FineUploadFileInput,
|
|
|
|
FineUploadForm,
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
from judge.views.problem import ProblemMixin
|
|
|
|
|
|
|
|
mimetypes.init()
|
2022-05-14 17:57:27 +00:00
|
|
|
mimetypes.add_type("application/x-yaml", ".yml")
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
|
|
|
|
def checker_args_cleaner(self):
|
2022-05-14 17:57:27 +00:00
|
|
|
data = self.cleaned_data["checker_args"]
|
2020-01-21 06:35:58 +00:00
|
|
|
if not data or data.isspace():
|
2022-05-14 17:57:27 +00:00
|
|
|
return ""
|
2020-01-21 06:35:58 +00:00
|
|
|
try:
|
|
|
|
if not isinstance(json.loads(data), dict):
|
2022-05-14 17:57:27 +00:00
|
|
|
raise ValidationError(_("Checker arguments must be a JSON object"))
|
2020-01-21 06:35:58 +00:00
|
|
|
except ValueError:
|
2022-05-14 17:57:27 +00:00
|
|
|
raise ValidationError(_("Checker arguments is invalid JSON"))
|
2020-01-21 06:35:58 +00:00
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
class ProblemDataForm(ModelForm):
|
|
|
|
def clean_zipfile(self):
|
2022-05-14 17:57:27 +00:00
|
|
|
if hasattr(self, "zip_valid") and not self.zip_valid:
|
|
|
|
raise ValidationError(_("Your zip file is invalid!"))
|
|
|
|
return self.cleaned_data["zipfile"]
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
clean_checker_args = checker_args_cleaner
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
model = ProblemData
|
2022-05-14 17:57:27 +00:00
|
|
|
fields = [
|
|
|
|
"zipfile",
|
|
|
|
"checker",
|
|
|
|
"checker_args",
|
|
|
|
"custom_checker",
|
|
|
|
"custom_validator",
|
|
|
|
"interactive_judge",
|
2022-06-02 04:59:46 +00:00
|
|
|
"fileio_input",
|
|
|
|
"fileio_output",
|
2023-03-10 04:31:55 +00:00
|
|
|
"output_only",
|
2023-08-01 05:26:15 +00:00
|
|
|
"use_ioi_signature",
|
|
|
|
"signature_handler",
|
|
|
|
"signature_header",
|
2022-05-14 17:57:27 +00:00
|
|
|
]
|
2020-01-21 06:35:58 +00:00
|
|
|
widgets = {
|
2022-05-14 17:57:27 +00:00
|
|
|
"zipfile": FineUploadFileInput,
|
|
|
|
"checker_args": HiddenInput,
|
|
|
|
"generator": HiddenInput,
|
|
|
|
"output_limit": HiddenInput,
|
|
|
|
"output_prefix": HiddenInput,
|
2022-06-02 04:59:46 +00:00
|
|
|
"fileio_input": TextInput,
|
|
|
|
"fileio_output": TextInput,
|
2023-03-10 04:31:55 +00:00
|
|
|
"output_only": CheckboxInput,
|
2023-08-01 05:26:15 +00:00
|
|
|
"use_ioi_signature": CheckboxInput,
|
2020-01-21 06:35:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class ProblemCaseForm(ModelForm):
|
|
|
|
clean_checker_args = checker_args_cleaner
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
model = ProblemTestCase
|
2022-05-14 17:57:27 +00:00
|
|
|
fields = (
|
|
|
|
"order",
|
|
|
|
"type",
|
|
|
|
"input_file",
|
|
|
|
"output_file",
|
|
|
|
"points",
|
|
|
|
"is_pretest",
|
|
|
|
"checker",
|
|
|
|
"checker_args",
|
|
|
|
) # , 'output_limit', 'output_prefix', 'generator_args')
|
2020-01-21 06:35:58 +00:00
|
|
|
widgets = {
|
2020-03-17 06:11:03 +00:00
|
|
|
# 'generator_args': HiddenInput,
|
2022-05-14 17:57:27 +00:00
|
|
|
"type": Select(attrs={"style": "width: 100%"}),
|
|
|
|
"points": NumberInput(attrs={"style": "width: 4em"}),
|
2020-03-17 06:11:03 +00:00
|
|
|
# 'output_prefix': NumberInput(attrs={'style': 'width: 4.5em'}),
|
|
|
|
# 'output_limit': NumberInput(attrs={'style': 'width: 6em'}),
|
|
|
|
# 'checker_args': HiddenInput,
|
2020-01-21 06:35:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-05-14 17:57:27 +00:00
|
|
|
class ProblemCaseFormSet(
|
|
|
|
formset_factory(
|
|
|
|
ProblemCaseForm, formset=BaseModelFormSet, extra=1, max_num=1, can_delete=True
|
|
|
|
)
|
|
|
|
):
|
2020-01-21 06:35:58 +00:00
|
|
|
model = ProblemTestCase
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
2022-05-14 17:57:27 +00:00
|
|
|
self.valid_files = kwargs.pop("valid_files", None)
|
2020-01-21 06:35:58 +00:00
|
|
|
super(ProblemCaseFormSet, self).__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
def _construct_form(self, i, **kwargs):
|
|
|
|
form = super(ProblemCaseFormSet, self)._construct_form(i, **kwargs)
|
|
|
|
form.valid_files = self.valid_files
|
|
|
|
return form
|
|
|
|
|
|
|
|
|
|
|
|
class ProblemManagerMixin(LoginRequiredMixin, ProblemMixin, DetailView):
|
|
|
|
def get_object(self, queryset=None):
|
|
|
|
problem = super(ProblemManagerMixin, self).get_object(queryset)
|
|
|
|
if problem.is_manually_managed:
|
|
|
|
raise Http404()
|
|
|
|
if self.request.user.is_superuser or problem.is_editable_by(self.request.user):
|
|
|
|
return problem
|
|
|
|
raise Http404()
|
|
|
|
|
|
|
|
|
|
|
|
class ProblemSubmissionDiff(TitleMixin, ProblemMixin, DetailView):
|
2022-05-14 17:57:27 +00:00
|
|
|
template_name = "problem/submission-diff.html"
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def get_title(self):
|
2022-05-14 17:57:27 +00:00
|
|
|
return _("Comparing submissions for {0}").format(self.object.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(
|
|
|
|
_('Comparing submissions for <a href="{1}">{0}</a>'),
|
|
|
|
self.object.name,
|
|
|
|
reverse("problem_detail", args=[self.object.code]),
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def get_object(self, queryset=None):
|
|
|
|
problem = super(ProblemSubmissionDiff, self).get_object(queryset)
|
|
|
|
if self.request.user.is_superuser or problem.is_editable_by(self.request.user):
|
|
|
|
return problem
|
|
|
|
raise Http404()
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
context = super(ProblemSubmissionDiff, self).get_context_data(**kwargs)
|
|
|
|
try:
|
2022-05-14 17:57:27 +00:00
|
|
|
ids = self.request.GET.getlist("id")
|
2020-01-21 06:35:58 +00:00
|
|
|
subs = Submission.objects.filter(id__in=ids)
|
|
|
|
except ValueError:
|
|
|
|
raise Http404
|
|
|
|
if not subs:
|
|
|
|
raise Http404
|
|
|
|
|
2022-05-14 17:57:27 +00:00
|
|
|
context["submissions"] = subs
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
# If we have associated data we can do better than just guess
|
2022-05-14 17:57:27 +00:00
|
|
|
data = ProblemTestCase.objects.filter(dataset=self.object, type="C")
|
2020-01-21 06:35:58 +00:00
|
|
|
if data:
|
|
|
|
num_cases = data.count()
|
|
|
|
else:
|
|
|
|
num_cases = subs.first().test_cases.count()
|
2022-05-14 17:57:27 +00:00
|
|
|
context["num_cases"] = num_cases
|
2020-01-21 06:35:58 +00:00
|
|
|
return context
|
|
|
|
|
|
|
|
|
|
|
|
class ProblemDataView(TitleMixin, ProblemManagerMixin):
|
2022-05-14 17:57:27 +00:00
|
|
|
template_name = "problem/data.html"
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def get_title(self):
|
2022-05-14 17:57:27 +00:00
|
|
|
return _("Editing data for {0}").format(self.object.name)
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def get_content_title(self):
|
2022-05-14 17:57:27 +00:00
|
|
|
return mark_safe(
|
|
|
|
escape(_("Editing data for %s"))
|
|
|
|
% (
|
|
|
|
format_html(
|
|
|
|
'<a href="{1}">{0}</a>',
|
|
|
|
self.object.name,
|
|
|
|
reverse("problem_detail", args=[self.object.code]),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def get_data_form(self, post=False):
|
2022-05-14 17:57:27 +00:00
|
|
|
return ProblemDataForm(
|
|
|
|
data=self.request.POST if post else None,
|
|
|
|
prefix="problem-data",
|
|
|
|
files=self.request.FILES if post else None,
|
|
|
|
instance=ProblemData.objects.get_or_create(problem=self.object)[0],
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def get_case_formset(self, files, post=False):
|
2022-05-14 17:57:27 +00:00
|
|
|
return ProblemCaseFormSet(
|
|
|
|
data=self.request.POST if post else None,
|
|
|
|
prefix="cases",
|
|
|
|
valid_files=files,
|
|
|
|
queryset=ProblemTestCase.objects.filter(dataset_id=self.object.pk).order_by(
|
|
|
|
"order"
|
|
|
|
),
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
def get_valid_files(self, data, post=False):
|
|
|
|
try:
|
2022-05-14 17:57:27 +00:00
|
|
|
if post and "problem-data-zipfile-clear" in self.request.POST:
|
2020-01-21 06:35:58 +00:00
|
|
|
return []
|
2022-05-14 17:57:27 +00:00
|
|
|
elif post and "problem-data-zipfile" in self.request.FILES:
|
|
|
|
return ZipFile(self.request.FILES["problem-data-zipfile"]).namelist()
|
2020-01-21 06:35:58 +00:00
|
|
|
elif data.zipfile:
|
|
|
|
return ZipFile(data.zipfile.path).namelist()
|
|
|
|
except BadZipfile:
|
|
|
|
return []
|
|
|
|
return []
|
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
context = super(ProblemDataView, self).get_context_data(**kwargs)
|
2022-05-14 17:57:27 +00:00
|
|
|
if "data_form" not in context:
|
|
|
|
context["data_form"] = self.get_data_form()
|
|
|
|
valid_files = context["valid_files"] = self.get_valid_files(
|
|
|
|
context["data_form"].instance
|
|
|
|
)
|
|
|
|
context["data_form"].zip_valid = valid_files is not False
|
|
|
|
context["cases_formset"] = self.get_case_formset(valid_files)
|
|
|
|
context["valid_files_json"] = mark_safe(json.dumps(context["valid_files"]))
|
|
|
|
context["valid_files"] = set(context["valid_files"])
|
|
|
|
context["all_case_forms"] = chain(
|
|
|
|
context["cases_formset"], [context["cases_formset"].empty_form]
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
return context
|
|
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
|
|
self.object = problem = self.get_object()
|
|
|
|
data_form = self.get_data_form(post=True)
|
|
|
|
valid_files = self.get_valid_files(data_form.instance, post=True)
|
|
|
|
data_form.zip_valid = valid_files is not False
|
|
|
|
cases_formset = self.get_case_formset(valid_files, post=True)
|
|
|
|
if data_form.is_valid() and cases_formset.is_valid():
|
|
|
|
data = data_form.save()
|
|
|
|
for case in cases_formset.save(commit=False):
|
|
|
|
case.dataset_id = problem.id
|
|
|
|
case.save()
|
|
|
|
for case in cases_formset.deleted_objects:
|
|
|
|
case.delete()
|
2022-05-14 17:57:27 +00:00
|
|
|
ProblemDataCompiler.generate(
|
|
|
|
problem, data, problem.cases.order_by("order"), valid_files
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
return HttpResponseRedirect(request.get_full_path())
|
2022-05-14 17:57:27 +00:00
|
|
|
return self.render_to_response(
|
|
|
|
self.get_context_data(
|
|
|
|
data_form=data_form,
|
|
|
|
cases_formset=cases_formset,
|
|
|
|
valid_files=valid_files,
|
|
|
|
)
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
|
|
|
|
put = post
|
|
|
|
|
|
|
|
|
|
|
|
@login_required
|
|
|
|
def problem_data_file(request, problem, path):
|
|
|
|
object = get_object_or_404(Problem, code=problem)
|
|
|
|
if not object.is_editable_by(request.user):
|
|
|
|
raise Http404()
|
|
|
|
|
|
|
|
response = HttpResponse()
|
2022-05-14 17:57:27 +00:00
|
|
|
if hasattr(settings, "DMOJ_PROBLEM_DATA_INTERNAL") and request.META.get(
|
|
|
|
"SERVER_SOFTWARE", ""
|
|
|
|
).startswith("nginx/"):
|
|
|
|
response["X-Accel-Redirect"] = "%s/%s/%s" % (
|
|
|
|
settings.DMOJ_PROBLEM_DATA_INTERNAL,
|
|
|
|
problem,
|
|
|
|
path,
|
|
|
|
)
|
2020-01-21 06:35:58 +00:00
|
|
|
else:
|
|
|
|
try:
|
2022-05-14 17:57:27 +00:00
|
|
|
with problem_data_storage.open(os.path.join(problem, path), "rb") as f:
|
2020-01-21 06:35:58 +00:00
|
|
|
response.content = f.read()
|
|
|
|
except IOError:
|
|
|
|
raise Http404()
|
|
|
|
|
2022-05-14 17:57:27 +00:00
|
|
|
response["Content-Type"] = "application/octet-stream"
|
2020-01-21 06:35:58 +00:00
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
@login_required
|
|
|
|
def problem_init_view(request, problem):
|
|
|
|
problem = get_object_or_404(Problem, code=problem)
|
|
|
|
if not request.user.is_superuser and not problem.is_editable_by(request.user):
|
|
|
|
raise Http404()
|
|
|
|
|
|
|
|
try:
|
2022-05-14 17:57:27 +00:00
|
|
|
with problem_data_storage.open(
|
|
|
|
os.path.join(problem.code, "init.yml"), "rb"
|
|
|
|
) as f:
|
|
|
|
data = utf8text(f.read()).rstrip("\n")
|
2020-01-21 06:35:58 +00:00
|
|
|
except IOError:
|
|
|
|
raise Http404()
|
|
|
|
|
2022-05-14 17:57:27 +00:00
|
|
|
return render(
|
|
|
|
request,
|
|
|
|
"problem/yaml.html",
|
|
|
|
{
|
|
|
|
"raw_source": data,
|
|
|
|
"highlighted_source": highlight_code(data, "yaml", linenos=False),
|
|
|
|
"title": _("Generated init.yml for %s") % problem.name,
|
|
|
|
"content_title": mark_safe(
|
|
|
|
escape(_("Generated init.yml for %s"))
|
|
|
|
% (
|
|
|
|
format_html(
|
|
|
|
'<a href="{1}">{0}</a>',
|
|
|
|
problem.name,
|
|
|
|
reverse("problem_detail", args=[problem.code]),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
),
|
|
|
|
},
|
|
|
|
)
|
2021-11-28 03:28:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
class ProblemZipUploadView(ProblemManagerMixin, View):
|
|
|
|
def dispatch(self, *args, **kwargs):
|
|
|
|
return super(ProblemZipUploadView, self).dispatch(*args, **kwargs)
|
2022-05-14 17:57:27 +00:00
|
|
|
|
2021-11-28 03:28:48 +00:00
|
|
|
def post(self, request, *args, **kwargs):
|
|
|
|
self.object = problem = self.get_object()
|
|
|
|
problem_data = get_object_or_404(ProblemData, problem=self.object)
|
|
|
|
form = FineUploadForm(request.POST, request.FILES)
|
2022-05-14 17:57:27 +00:00
|
|
|
|
2021-11-28 03:28:48 +00:00
|
|
|
if form.is_valid():
|
2022-05-14 17:57:27 +00:00
|
|
|
fileuid = form.cleaned_data["qquuid"]
|
|
|
|
filename = form.cleaned_data["qqfilename"]
|
2021-11-28 03:28:48 +00:00
|
|
|
dest = os.path.join(gettempdir(), fileuid)
|
|
|
|
|
|
|
|
def post_upload():
|
|
|
|
zip_dest = os.path.join(dest, filename)
|
|
|
|
try:
|
2022-05-14 17:57:27 +00:00
|
|
|
ZipFile(zip_dest).namelist() # check if this file is valid
|
|
|
|
with open(zip_dest, "rb") as f:
|
2021-11-28 03:28:48 +00:00
|
|
|
problem_data.zipfile.delete()
|
|
|
|
problem_data.zipfile.save(filename, File(f))
|
|
|
|
f.close()
|
|
|
|
except Exception as e:
|
|
|
|
raise Exception(e)
|
|
|
|
finally:
|
|
|
|
shutil.rmtree(dest)
|
2022-05-14 17:57:27 +00:00
|
|
|
|
2021-11-28 03:28:48 +00:00
|
|
|
try:
|
2022-05-14 17:57:27 +00:00
|
|
|
handle_upload(
|
|
|
|
request.FILES["qqfile"],
|
|
|
|
form.cleaned_data,
|
|
|
|
dest,
|
|
|
|
post_upload=post_upload,
|
|
|
|
)
|
2021-11-28 03:28:48 +00:00
|
|
|
except Exception as e:
|
2022-05-14 17:57:27 +00:00
|
|
|
return JsonResponse({"success": False, "error": str(e)})
|
|
|
|
return JsonResponse({"success": True})
|
2021-11-28 03:28:48 +00:00
|
|
|
else:
|
2022-05-14 17:57:27 +00:00
|
|
|
return HttpResponse(status_code=400)
|