Add output-only on UI
This commit is contained in:
parent
bdae79eeda
commit
195450ebc3
15 changed files with 708 additions and 566 deletions
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
22
judge/migrations/0155_output_only.py
Normal file
22
judge/migrations/0155_output_only.py
Normal 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
@ -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 ""
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue