Add output-only on UI

This commit is contained in:
cuom1999 2023-03-09 22:31:55 -06:00
parent bdae79eeda
commit 195450ebc3
15 changed files with 708 additions and 566 deletions

View file

@ -60,6 +60,7 @@ DMOJ_PROBLEM_MAX_TIME_LIMIT = 60 # seconds
DMOJ_PROBLEM_MIN_MEMORY_LIMIT = 0 # kilobytes DMOJ_PROBLEM_MIN_MEMORY_LIMIT = 0 # kilobytes
DMOJ_PROBLEM_MAX_MEMORY_LIMIT = 1048576 # kilobytes DMOJ_PROBLEM_MAX_MEMORY_LIMIT = 1048576 # kilobytes
DMOJ_PROBLEM_MIN_PROBLEM_POINTS = 0 DMOJ_PROBLEM_MIN_PROBLEM_POINTS = 0
DMOJ_SUBMISSION_ROOT = "/tmp"
DMOJ_RATING_COLORS = True DMOJ_RATING_COLORS = True
DMOJ_EMAIL_THROTTLING = (10, 60) DMOJ_EMAIL_THROTTLING = (10, 60)
DMOJ_STATS_LANGUAGE_THRESHOLD = 10 DMOJ_STATS_LANGUAGE_THRESHOLD = 10

View file

@ -393,6 +393,11 @@ urlpatterns = [
] ]
), ),
), ),
url(
r"^submission_source_file/(?P<filename>(\w|\.)+)",
submission.SubmissionSourceFileView.as_view(),
name="submission_source_file",
),
url( url(
r"^users/", r"^users/",
include( include(

View file

@ -3,6 +3,7 @@ import json
import logging import logging
import threading import threading
import time import time
import os
from collections import deque, namedtuple from collections import deque, namedtuple
from operator import itemgetter from operator import itemgetter
@ -10,6 +11,7 @@ from django import db
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.db.models import F from django.db.models import F
from django.core.cache import cache
from judge import event_poster as event from judge import event_poster as event
from judge.bridge.base_handler import ZlibPacketHandler, proxy_list from judge.bridge.base_handler import ZlibPacketHandler, proxy_list
@ -568,6 +570,13 @@ class JudgeHandler(ZlibPacketHandler):
event.post("contest_%d" % participation.contest_id, {"type": "update"}) event.post("contest_%d" % participation.contest_id, {"type": "update"})
self._post_update_submission(submission.id, "grading-end", done=True) self._post_update_submission(submission.id, "grading-end", done=True)
# Clean up submission source file (if any)
source_file = cache.get(f"submission_source_file:{submission.id}")
if source_file:
filepath = os.path.join(settings.DMOJ_SUBMISSION_ROOT, source_file)
if os.path.exists(filepath):
os.remove(filepath)
def on_compile_error(self, packet): def on_compile_error(self, packet):
logger.info( logger.info(
"%s: Submission failed to compile: %s", self.name, packet["submission-id"] "%s: Submission failed to compile: %s", self.name, packet["submission-id"]

View file

@ -1,6 +1,8 @@
import os
import secrets
from operator import attrgetter from operator import attrgetter
import pyotp import pyotp
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -16,8 +18,9 @@ from django.forms import (
ModelForm, ModelForm,
formset_factory, formset_factory,
BaseModelFormSet, BaseModelFormSet,
FileField,
) )
from django.urls import reverse_lazy from django.urls import reverse_lazy, reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils import timezone from django.utils import timezone
@ -115,14 +118,23 @@ class ProfileForm(ModelForm):
) )
def file_size_validator(file):
limit = 10 * 1024 * 1024
if file.size > limit:
raise ValidationError("File too large. Size should not exceed 10MB.")
class ProblemSubmitForm(ModelForm): class ProblemSubmitForm(ModelForm):
source = CharField( source = CharField(
max_length=65536, widget=AceWidget(theme="twilight", no_ace_media=True) max_length=65536, widget=AceWidget(theme="twilight", no_ace_media=True)
) )
judge = ChoiceField(choices=(), widget=forms.HiddenInput(), required=False) judge = ChoiceField(choices=(), widget=forms.HiddenInput(), required=False)
source_file = FileField(required=False, validators=[file_size_validator])
def __init__(self, *args, judge_choices=(), **kwargs): def __init__(self, *args, judge_choices=(), request=None, **kwargs):
super(ProblemSubmitForm, self).__init__(*args, **kwargs) super(ProblemSubmitForm, self).__init__(*args, **kwargs)
self.source_file_name = None
self.request = request
self.fields["language"].empty_label = None self.fields["language"].empty_label = None
self.fields["language"].label_from_instance = attrgetter("display_name") self.fields["language"].label_from_instance = attrgetter("display_name")
self.fields["language"].queryset = Language.objects.filter( self.fields["language"].queryset = Language.objects.filter(
@ -135,6 +147,24 @@ class ProblemSubmitForm(ModelForm):
) )
self.fields["judge"].choices = judge_choices self.fields["judge"].choices = judge_choices
def clean(self):
if "source_file" in self.files:
if self.cleaned_data["language"].key == "OUTPUT" and self.files[
"source_file"
].name.endswith(".zip"):
self.source_file_name = secrets.token_hex(16) + ".zip"
filepath = os.path.join(
settings.DMOJ_SUBMISSION_ROOT, self.source_file_name
)
with open(filepath, "wb+") as destination:
for chunk in self.files["source_file"].chunks():
destination.write(chunk)
self.cleaned_data["source"] = self.request.build_absolute_uri(
reverse("submission_source_file", args=(self.source_file_name,))
)
del self.files["source_file"]
return self.cleaned_data
class Meta: class Meta:
model = Submission model = Submission
fields = ["language"] fields = ["language"]

View file

@ -0,0 +1,22 @@
# Generated by Django 3.2.18 on 2023-03-10 04:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0154_add_submission_indexes"),
]
operations = [
migrations.AddField(
model_name="problemdata",
name="output_only",
field=models.BooleanField(
help_text="Support output-only problem",
null=True,
verbose_name="is output only",
),
),
]

View file

@ -118,6 +118,11 @@ class ProblemData(models.Model):
null=True, null=True,
help_text=_("Leave empty for stdout"), help_text=_("Leave empty for stdout"),
) )
output_only = models.BooleanField(
verbose_name=_("is output only"),
help_text=_("Support output-only problem"),
null=True,
)
__original_zipfile = None __original_zipfile = None

View file

@ -254,6 +254,8 @@ class ProblemDataCompiler(object):
if "file_io" not in init: if "file_io" not in init:
init["file_io"] = {} init["file_io"] = {}
init["file_io"]["output"] = self.data.fileio_output init["file_io"]["output"] = self.data.fileio_output
if self.data.output_only:
init["output_only"] = True
return init return init

View file

@ -7,6 +7,7 @@ from random import randrange
import random import random
from copy import deepcopy from copy import deepcopy
from django.core.cache import cache
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
@ -486,7 +487,7 @@ class ProblemList(QueryStringSortMixin, TitleMixin, SolvedProblemMixin, ListView
orphans=orphans, orphans=orphans,
allow_empty_first_page=allow_empty_first_page, allow_empty_first_page=allow_empty_first_page,
count=queryset.values("pk").count() if not self.in_contest else None, count=queryset.values("pk").count() if not self.in_contest else None,
**kwargs **kwargs,
) )
if not self.in_contest: if not self.in_contest:
queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE) queryset = queryset.add_i18n_name(self.request.LANGUAGE_CODE)
@ -1039,8 +1040,10 @@ def problem_submit(request, problem, submission=None):
if request.method == "POST": if request.method == "POST":
form = ProblemSubmitForm( form = ProblemSubmitForm(
request.POST, request.POST,
request.FILES,
judge_choices=judge_choices, judge_choices=judge_choices,
instance=Submission(user=profile, problem=problem), instance=Submission(user=profile, problem=problem),
request=request,
) )
if form.is_valid(): if form.is_valid():
if ( if (
@ -1114,6 +1117,7 @@ def problem_submit(request, problem, submission=None):
# Save a query # Save a query
model.source = source model.source = source
cache.set(f"submission_source_file:{model.id}", form.source_file_name, 3600)
model.judge(rejudge=False, judge_id=form.cleaned_data["judge"]) model.judge(rejudge=False, judge_id=form.cleaned_data["judge"])
return HttpResponseRedirect( return HttpResponseRedirect(

View file

@ -27,6 +27,7 @@ from django.forms import (
formset_factory, formset_factory,
FileInput, FileInput,
TextInput, TextInput,
CheckboxInput,
) )
from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
@ -91,6 +92,7 @@ class ProblemDataForm(ModelForm):
"interactive_judge", "interactive_judge",
"fileio_input", "fileio_input",
"fileio_output", "fileio_output",
"output_only",
] ]
widgets = { widgets = {
"zipfile": FineUploadFileInput, "zipfile": FineUploadFileInput,
@ -100,6 +102,7 @@ class ProblemDataForm(ModelForm):
"output_prefix": HiddenInput, "output_prefix": HiddenInput,
"fileio_input": TextInput, "fileio_input": TextInput,
"fileio_output": TextInput, "fileio_output": TextInput,
"output_only": CheckboxInput,
} }

View file

@ -30,6 +30,7 @@ from django.utils.translation import gettext_lazy
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django.views.generic import DetailView from django.views.generic import DetailView
from django.views.generic import ListView from django.views.generic import ListView
from django.views import View
from judge import event_poster as event from judge import event_poster as event
from judge.highlight_code import highlight_code from judge.highlight_code import highlight_code
@ -1049,3 +1050,16 @@ class UserContestSubmissionsAjax(UserContestSubmissions):
return super(UserContestSubmissionsAjax, self).get(request, *args, **kwargs) return super(UserContestSubmissionsAjax, self).get(request, *args, **kwargs)
except Http404: except Http404:
return HttpResponse(_("You don't have permission to access.")) return HttpResponse(_("You don't have permission to access."))
class SubmissionSourceFileView(View):
def get(self, request, filename):
filepath = os.path.join(settings.DMOJ_SUBMISSION_ROOT, filename)
if not os.path.exists(filepath):
raise Http404("File not found")
response = HttpResponse()
with open(filepath, "rb") as f:
response.content = f.read()
response["Content-Type"] = "application/octet-stream"
response["Content-Disposition"] = "attachment; filename=%s" % (filename,)
return response

File diff suppressed because it is too large Load diff

View file

@ -402,6 +402,9 @@ msgstr ""
msgid "monotonic-queue" msgid "monotonic-queue"
msgstr "" msgstr ""
msgid "mst"
msgstr ""
msgid "multiplicative" msgid "multiplicative"
msgstr "" msgstr ""
@ -555,6 +558,9 @@ msgstr ""
msgid "tortoise-hare" msgid "tortoise-hare"
msgstr "" msgstr ""
msgid "Training"
msgstr ""
msgid "treap/splay" msgid "treap/splay"
msgstr "" msgstr ""
@ -576,6 +582,9 @@ msgstr ""
msgid "two-pointers" msgid "two-pointers"
msgstr "" msgstr ""
msgid "unlabelled"
msgstr ""
msgid "VOI" msgid "VOI"
msgstr "" msgstr ""

View file

@ -181,6 +181,7 @@ label[for="language"], label[for="status"] {
.source-code { .source-code {
padding-left: 15px; padding-left: 15px;
width: 100%;
} }
.source-wrap { .source-wrap {

View file

@ -210,9 +210,13 @@ input {
.copy-clipboard { .copy-clipboard {
position: relative; position: relative;
}
.md-typeset .copy-clipboard {
margin-top: 1.5em; margin-top: 1.5em;
} }
// Bootstrap-y tabs // Bootstrap-y tabs
.ul_tab_a_active { .ul_tab_a_active {
color: #045343; color: #045343;

View file

@ -105,7 +105,7 @@
} }
}); });
editor.getSession().setUseWrapMode(true); editor.getSession().setUseWrapMode(true);
editor.setFontSize(14); editor.setFontSize(15);
editor.setShowPrintMargin(false); editor.setShowPrintMargin(false);
// editor.setPrintMarginColumn(100); // editor.setPrintMarginColumn(100);
editor.focus(); editor.focus();
@ -143,10 +143,13 @@
}); });
} }
$('#file-upload').on('click change', function(e) { $('#id_source_file').on('click change', function(e) {
var file = $(this)[0].files[0]; var file = $(this)[0].files[0];
if (file) { if (file) {
if (file.name.endsWith('sb3')) { if (file.name.endsWith('zip')) {
update_submit_area(file.name);
}
else if (file.name.endsWith('sb3')) {
get_source_scratch(file); get_source_scratch(file);
} }
else { else {
@ -195,7 +198,7 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
<form id="problem_submit" action="" method="post" class="form-area"> <form id="problem_submit" action="" method="post" class="form-area" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{{ form.non_field_errors() }} {{ form.non_field_errors() }}
<div id="submit-wrapper"> <div id="submit-wrapper">
@ -225,7 +228,8 @@
{% if no_judges %} {% if no_judges %}
<span style="color: red">{{ _('No judge is available for this problem.') }}</span> <span style="color: red">{{ _('No judge is available for this problem.') }}</span>
{% else %} {% else %}
<input type="file" id="file-upload"> {{ form.source_file.errors }}
{{ form.source_file }}
<div class="submit-bar"> <div class="submit-bar">
{{ form.judge }} {{ form.judge }}
<input type="submit" value="{{ _('Submit!') }}" class="button small" <input type="submit" value="{{ _('Submit!') }}" class="button small"