Test Formatter (#100)

This commit is contained in:
Bao Le 2024-01-09 02:27:20 +08:00 committed by GitHub
parent 14ecef649e
commit 04c6af1dff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1624 additions and 0 deletions

View file

@ -84,6 +84,7 @@ DMOJ_STATS_SUBMISSION_RESULT_COLORS = {
"ERR": "#ffa71c", "ERR": "#ffa71c",
} }
DMOJ_PROFILE_IMAGE_ROOT = "profile_images" DMOJ_PROFILE_IMAGE_ROOT = "profile_images"
DMOJ_TEST_FORMATTER_ROOT = "test_formatter"
MARKDOWN_STYLES = {} MARKDOWN_STYLES = {}
MARKDOWN_DEFAULT_STYLE = {} MARKDOWN_DEFAULT_STYLE = {}

View file

@ -45,6 +45,7 @@ from judge.views import (
license, license,
mailgun, mailgun,
markdown_editor, markdown_editor,
test_formatter,
notification, notification,
organization, organization,
preview, preview,
@ -68,6 +69,9 @@ from judge.views import (
course, course,
email, email,
) )
from judge.views.test_formatter import test_formatter
from judge.views.problem_data import ( from judge.views.problem_data import (
ProblemDataView, ProblemDataView,
ProblemSubmissionDiff, ProblemSubmissionDiff,
@ -406,11 +410,51 @@ urlpatterns = [
] ]
), ),
), ),
url(
r"^test_formatter/",
include(
[
url(
r"^$", test_formatter.TestFormatter.as_view(), name="test_formatter"
),
url(
r"^edit_page$",
test_formatter.EditTestFormatter.as_view(),
name="edit_page",
),
url(
r"^download_page$",
test_formatter.DownloadTestFormatter.as_view(),
name="download_page",
),
]
),
),
url( url(
r"^markdown_editor/", r"^markdown_editor/",
markdown_editor.MarkdownEditor.as_view(), markdown_editor.MarkdownEditor.as_view(),
name="markdown_editor", name="markdown_editor",
), ),
url(
r"^test_formatter/",
include(
[
url(
r"^$", test_formatter.TestFormatter.as_view(), name="test_formatter"
),
url(
r"^edit_page$",
test_formatter.EditTestFormatter.as_view(),
name="edit_page",
),
url(
r"^download_page$",
test_formatter.DownloadTestFormatter.as_view(),
name="download_page",
),
]
),
),
url( url(
r"^submission_source_file/(?P<filename>(\w|\.)+)", r"^submission_source_file/(?P<filename>(\w|\.)+)",
submission.SubmissionSourceFileView.as_view(), submission.SubmissionSourceFileView.as_view(),

View file

@ -29,6 +29,7 @@ from django_ace import AceWidget
from judge.models import ( from judge.models import (
Contest, Contest,
Language, Language,
TestFormatterModel,
Organization, Organization,
PrivateMessage, PrivateMessage,
Problem, Problem,
@ -37,6 +38,7 @@ from judge.models import (
Submission, Submission,
BlogPost, BlogPost,
ContestProblem, ContestProblem,
TestFormatterModel,
) )
from judge.widgets import ( from judge.widgets import (
@ -568,3 +570,9 @@ class ContestProblemFormSet(
) )
): ):
model = ContestProblem model = ContestProblem
class TestFormatterForm(ModelForm):
class Meta:
model = TestFormatterModel
fields = ["file"]

View file

@ -0,0 +1,37 @@
# Generated by Django 3.2.20 on 2023-11-21 07:22
from django.db import migrations, models
import judge.models.test_formatter
class Migration(migrations.Migration):
dependencies = [
("judge", "0173_fulltext"),
]
operations = [
migrations.CreateModel(
name="TestFormatterModel",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"file",
models.FileField(
blank=True,
null=True,
upload_to=judge.models.test_formatter.test_formatter_path,
verbose_name="testcase file",
),
),
],
)
]

View file

@ -0,0 +1,35 @@
from django.db import migrations, models
import judge.models.test_formatter
class Migration(migrations.Migration):
dependencies = [
("judge", "0173_fulltext"),
]
operations = [
migrations.CreateModel(
name="TestFormatterModel",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"file",
models.FileField(
blank=True,
null=True,
upload_to=judge.models.test_formatter.test_formatter_path,
verbose_name="testcase file",
),
),
],
)
]

View file

@ -53,12 +53,15 @@ from judge.models.submission import (
SubmissionSource, SubmissionSource,
SubmissionTestCase, SubmissionTestCase,
) )
from judge.models.test_formatter import TestFormatterModel
from judge.models.ticket import Ticket, TicketMessage from judge.models.ticket import Ticket, TicketMessage
from judge.models.volunteer import VolunteerProblemVote from judge.models.volunteer import VolunteerProblemVote
from judge.models.pagevote import PageVote, PageVoteVoter from judge.models.pagevote import PageVote, PageVoteVoter
from judge.models.bookmark import BookMark, MakeBookMark from judge.models.bookmark import BookMark, MakeBookMark
from judge.models.course import Course from judge.models.course import Course
from judge.models.notification import Notification, NotificationProfile from judge.models.notification import Notification, NotificationProfile
from judge.models.test_formatter import TestFormatterModel
revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"]) revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"])
revisions.register(Problem, follow=["language_limits"]) revisions.register(Problem, follow=["language_limits"])

View file

@ -0,0 +1,26 @@
import os
from django.db import models
from dmoj import settings
from django.utils.translation import gettext_lazy as _
__all__ = [
"TestFormatterModel",
]
def test_formatter_path(test_formatter, filename):
tail = filename.split(".")[-1]
head = filename.split(".")[0]
if str(tail).lower() != "zip":
raise Exception("400: Only ZIP files are supported")
new_filename = f"{head}.{tail}"
return os.path.join(settings.DMOJ_TEST_FORMATTER_ROOT, new_filename)
class TestFormatterModel(models.Model):
file = models.FileField(
verbose_name=_("testcase file"),
null=True,
blank=True,
upload_to=test_formatter_path,
)

View file

@ -0,0 +1,204 @@
from django.views import View
from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse
from django.core.files import File
from django.core.files.base import ContentFile
from django.http import (
FileResponse,
HttpResponseRedirect,
HttpResponseBadRequest,
HttpResponse,
)
from judge.models import TestFormatterModel
from judge.forms import TestFormatterForm
from judge.views import tf_logic, tf_utils
from django.utils.translation import gettext_lazy as _
from zipfile import ZipFile, ZIP_DEFLATED
import os
import uuid
from dmoj import settings
def id_to_path(id):
return os.path.join(settings.MEDIA_ROOT, "test_formatter/" + id + "/")
def get_names_in_archive(file_path):
with ZipFile(os.path.join(settings.MEDIA_ROOT, file_path)) as f:
result = [x for x in f.namelist() if not x.endswith("/")]
return list(sorted(result, key=tf_utils.natural_sorting_key))
def get_renamed_archive(file_str, file_name, file_path, bef, aft):
target_file_id = str(uuid.uuid4())
source_path = os.path.join(settings.MEDIA_ROOT, file_str)
target_path = os.path.join(settings.MEDIA_ROOT, file_str + "_" + target_file_id)
new_path = os.path.join(settings.MEDIA_ROOT, "test_formatter/" + file_name)
source = ZipFile(source_path, "r")
target = ZipFile(target_path, "w", ZIP_DEFLATED)
for bef_name, aft_name in zip(bef, aft):
target.writestr(aft_name, source.read(bef_name))
os.remove(source_path)
os.rename(target_path, new_path)
target.close()
source.close()
return {"file_path": "test_formatter/" + file_name}
class TestFormatter(View):
form_class = TestFormatterForm()
def get(self, request):
return render(
request,
"test_formatter/test_formatter.html",
{"title": _("Test Formatter"), "form": self.form_class},
)
def post(self, request):
form = TestFormatterForm(request.POST, request.FILES)
if form.is_valid():
form.save()
return HttpResponseRedirect("edit_page")
return render(
request, "test_formatter/test_formatter.html", {"form": self.form_class}
)
class EditTestFormatter(View):
file_path = ""
def get(self, request):
file = TestFormatterModel.objects.last()
filestr = str(file.file)
filename = filestr.split("/")[-1]
filepath = filestr.split("/")[0]
bef_file = get_names_in_archive(filestr)
preview_data = {
"bef_inp_format": bef_file[0],
"bef_out_format": bef_file[1],
"aft_inp_format": "input.000",
"aft_out_format": "output.000",
"file_str": filestr,
}
preview = tf_logic.preview(preview_data)
response = ""
for i in range(len(bef_file)):
bef = preview["bef_preview"][i]["value"]
aft = preview["aft_preview"][i]["value"]
response = response + f"<p>{bef} => {aft}</p>\n"
return render(
request,
"test_formatter/edit_test_formatter.html",
{
"title": _("Test Formatter"),
"check": 0,
"files_list": bef_file,
"file_name": filename,
"res": response,
},
)
def post(self, request, *args, **kwargs):
action = request.POST.get("action")
if action == "convert":
try:
file = TestFormatterModel.objects.last()
filestr = str(file.file)
filename = filestr.split("/")[-1]
filepath = filestr.split("/")[0]
bef_inp_format = request.POST["bef_inp_format"]
bef_out_format = request.POST["bef_out_format"]
aft_inp_format = request.POST["aft_inp_format"]
aft_out_format = request.POST["aft_out_format"]
aft_file_name = request.POST["file_name"]
except KeyError:
return HttpResponseBadRequest("No data.")
if filename != aft_file_name:
source_path = os.path.join(settings.MEDIA_ROOT, filestr)
new_path = os.path.join(
settings.MEDIA_ROOT, "test_formatter/" + aft_file_name
)
os.rename(source_path, new_path)
filename = aft_file_name
preview_data = {
"bef_inp_format": bef_inp_format,
"bef_out_format": bef_out_format,
"aft_inp_format": aft_inp_format,
"aft_out_format": aft_out_format,
"file_name": filename,
"file_path": filepath,
"file_str": filepath + "/" + filename,
}
converted_zip = tf_logic.convert(preview_data)
global file_path
file_path = converted_zip["file_path"]
zip_instance = TestFormatterModel()
zip_instance.file = file_path
zip_instance.save()
preview = tf_logic.preview(preview_data)
response = HttpResponse()
for i in range(len(preview["bef_preview"])):
bef = preview["bef_preview"][i]["value"]
aft = preview["aft_preview"][i]["value"]
response.write(f"<p>{bef} => {aft}</p>")
return response
elif action == "download":
return HttpResponse(file_path)
return HttpResponseBadRequest("Invalid action")
class DownloadTestFormatter(View):
def get(self, request):
file_path = request.GET.get("file_path")
file_name = file_path.split("/")[-1]
preview_file = tf_logic.preview_file(file_path)
response = ""
for i in range(len(preview_file)):
response = response + (f"<p>{preview_file[i]}</p>\n")
files_list = [preview_file[0], preview_file[1]]
return render(
request,
"test_formatter/download_test_formatter.html",
{
"title": _("Test Formatter"),
"response": response,
"files_list": files_list,
"file_path": os.path.join(settings.MEDIA_ROOT, file_path),
"file_path_getnames": file_path,
"file_name": file_name,
},
)
def post(self, request):
file_path = request.POST.get("file_path")
with open(file_path, "rb") as zip_file:
response = HttpResponse(zip_file.read(), content_type="application/zip")
response[
"Content-Disposition"
] = f"attachment; filename={os.path.basename(file_path)}"
return response

View file

@ -0,0 +1,204 @@
from django.views import View
from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse
from django.core.files import File
from django.core.files.base import ContentFile
from django.http import (
FileResponse,
HttpResponseRedirect,
HttpResponseBadRequest,
HttpResponse,
)
from judge.models import TestFormatterModel
from judge.forms import TestFormatterForm
from judge.views.test_formatter import tf_logic, tf_utils
from django.utils.translation import gettext_lazy as _
from zipfile import ZipFile, ZIP_DEFLATED
import os
import uuid
from dmoj import settings
def id_to_path(id):
return os.path.join(settings.MEDIA_ROOT, "test_formatter/" + id + "/")
def get_names_in_archive(file_path):
with ZipFile(os.path.join(settings.MEDIA_ROOT, file_path)) as f:
result = [x for x in f.namelist() if not x.endswith("/")]
return list(sorted(result, key=tf_utils.natural_sorting_key))
def get_renamed_archive(file_str, file_name, file_path, bef, aft):
target_file_id = str(uuid.uuid4())
source_path = os.path.join(settings.MEDIA_ROOT, file_str)
target_path = os.path.join(settings.MEDIA_ROOT, file_str + "_" + target_file_id)
new_path = os.path.join(settings.MEDIA_ROOT, "test_formatter/" + file_name)
source = ZipFile(source_path, "r")
target = ZipFile(target_path, "w", ZIP_DEFLATED)
for bef_name, aft_name in zip(bef, aft):
target.writestr(aft_name, source.read(bef_name))
os.remove(source_path)
os.rename(target_path, new_path)
target.close()
source.close()
return {"file_path": "test_formatter/" + file_name}
class TestFormatter(View):
form_class = TestFormatterForm()
def get(self, request):
return render(
request,
"test_formatter/test_formatter.html",
{"title": _("Test Formatter"), "form": self.form_class},
)
def post(self, request):
form = TestFormatterForm(request.POST, request.FILES)
if form.is_valid():
form.save()
return HttpResponseRedirect("edit_page")
return render(
request, "test_formatter/test_formatter.html", {"form": self.form_class}
)
class EditTestFormatter(View):
file_path = ""
def get(self, request):
file = TestFormatterModel.objects.last()
filestr = str(file.file)
filename = filestr.split("/")[-1]
filepath = filestr.split("/")[0]
bef_file = get_names_in_archive(filestr)
preview_data = {
"bef_inp_format": bef_file[0],
"bef_out_format": bef_file[1],
"aft_inp_format": "input.000",
"aft_out_format": "output.000",
"file_str": filestr,
}
preview = tf_logic.preview(preview_data)
response = ""
for i in range(len(bef_file)):
bef = preview["bef_preview"][i]["value"]
aft = preview["aft_preview"][i]["value"]
response = response + f"<p>{bef} => {aft}</p>\n"
return render(
request,
"test_formatter/edit_test_formatter.html",
{
"title": _("Test Formatter"),
"check": 0,
"files_list": bef_file,
"file_name": filename,
"res": response,
},
)
def post(self, request, *args, **kwargs):
action = request.POST.get("action")
if action == "convert":
try:
file = TestFormatterModel.objects.last()
filestr = str(file.file)
filename = filestr.split("/")[-1]
filepath = filestr.split("/")[0]
bef_inp_format = request.POST["bef_inp_format"]
bef_out_format = request.POST["bef_out_format"]
aft_inp_format = request.POST["aft_inp_format"]
aft_out_format = request.POST["aft_out_format"]
aft_file_name = request.POST["file_name"]
except KeyError:
return HttpResponseBadRequest("No data.")
if filename != aft_file_name:
source_path = os.path.join(settings.MEDIA_ROOT, filestr)
new_path = os.path.join(
settings.MEDIA_ROOT, "test_formatter/" + aft_file_name
)
os.rename(source_path, new_path)
filename = aft_file_name
preview_data = {
"bef_inp_format": bef_inp_format,
"bef_out_format": bef_out_format,
"aft_inp_format": aft_inp_format,
"aft_out_format": aft_out_format,
"file_name": filename,
"file_path": filepath,
"file_str": filepath + "/" + filename,
}
converted_zip = tf_logic.convert(preview_data)
global file_path
file_path = converted_zip["file_path"]
zip_instance = TestFormatterModel()
zip_instance.file = file_path
zip_instance.save()
preview = tf_logic.preview(preview_data)
response = HttpResponse()
for i in range(len(preview["bef_preview"])):
bef = preview["bef_preview"][i]["value"]
aft = preview["aft_preview"][i]["value"]
response.write(f"<p>{bef} => {aft}</p>")
return response
elif action == "download":
return HttpResponse(file_path)
return HttpResponseBadRequest("Invalid action")
class DownloadTestFormatter(View):
def get(self, request):
file_path = request.GET.get("file_path")
file_name = file_path.split("/")[-1]
preview_file = tf_logic.preview_file(file_path)
response = ""
for i in range(len(preview_file)):
response = response + (f"<p>{preview_file[i]}</p>\n")
files_list = [preview_file[0], preview_file[1]]
return render(
request,
"test_formatter/download_test_formatter.html",
{
"title": _("Test Formatter"),
"response": response,
"files_list": files_list,
"file_path": os.path.join(settings.MEDIA_ROOT, file_path),
"file_path_getnames": file_path,
"file_name": file_name,
},
)
def post(self, request):
file_path = request.POST.get("file_path")
with open(file_path, "rb") as zip_file:
response = HttpResponse(zip_file.read(), content_type="application/zip")
response[
"Content-Disposition"
] = f"attachment; filename={os.path.basename(file_path)}"
return response

View file

@ -0,0 +1,116 @@
import os
from judge.views.test_formatter import test_formatter as tf
from judge.views.test_formatter import tf_pattern as pattern
class TestSuite:
def __init__(
self,
file_id: str,
pattern_pair: pattern.PatternPair,
test_id_list: list,
extra_files: list,
):
self.file_id = file_id
self.pattern_pair = pattern_pair
self.test_id_list = test_id_list
self.extra_files = extra_files
@classmethod
def get_test_suite(cls, file_name: str, inp_format: str, out_format: str):
pattern_pair = pattern.PatternPair.from_string_pair(inp_format, out_format)
names = tf.get_names_in_archive(file_name)
test_id_list, extra_files = pattern_pair.matches(
names, returns="test_id_with_extra_files"
)
return cls(file_name, pattern_pair, test_id_list, extra_files)
def get_name_list(self, add_extra_info=False):
important_files = []
for index, t in enumerate(self.test_id_list):
inp_name = self.pattern_pair.x.get_name(t, index=index, use_index=True)
out_name = self.pattern_pair.y.get_name(t, index=index, use_index=True)
important_files.extend([inp_name, out_name])
result = []
for name in important_files:
if add_extra_info:
result.append({"value": name, "is_extra_file": False})
else:
result.append(name)
for name in self.extra_files:
if add_extra_info:
result.append({"value": name, "is_extra_file": True})
else:
result.append(name)
return result
def is_valid_file_type(file_name):
_, ext = os.path.splitext(file_name)
return ext in [".zip", ".ZIP"]
def preview(params):
bif = params["bef_inp_format"]
bof = params["bef_out_format"]
aif = params["aft_inp_format"]
aof = params["aft_out_format"]
file_str = params["file_str"]
try:
test_suite = TestSuite.get_test_suite(file_str, bif, bof)
bef_preview = test_suite.get_name_list(add_extra_info=True)
try:
test_suite.pattern_pair = pattern.PatternPair.from_string_pair(aif, aof)
aft_preview = test_suite.get_name_list(add_extra_info=True)
return {"bef_preview": bef_preview, "aft_preview": aft_preview}
except:
return {"bef_preview": bef_preview, "aft_preview": []}
except:
test_suite = TestSuite.get_test_suite(file_id, "*", "*")
preview = test_suite.get_name_list(add_extra_info=True)
return {"bef_preview": preview, "aft_preview": []}
def convert(params):
bif = params["bef_inp_format"]
bof = params["bef_out_format"]
aif = params["aft_inp_format"]
aof = params["aft_out_format"]
file_str = params["file_str"]
file_name = params["file_name"]
file_path = params["file_path"]
test_suite = TestSuite.get_test_suite(file_str, bif, bof)
bef_preview = test_suite.get_name_list()
test_suite.pattern_pair = pattern.PatternPair.from_string_pair(aif, aof)
aft_preview = test_suite.get_name_list()
result = tf.get_renamed_archive(
file_str, file_name, file_path, bef_preview, aft_preview
)
return result
def prefill(params):
file_str = params["file_str"]
file_name = params["file_name"]
names = tf.get_names_in_archive(file_str)
pattern_pair = pattern.find_best_pattern_pair(names)
return {
"file_name": file_name,
"inp_format": pattern_pair.x.to_string(),
"out_format": pattern_pair.y.to_string(),
}
def preview_file(file_str):
names = tf.get_names_in_archive(file_str)
return names

View file

@ -0,0 +1,268 @@
import os
import random
from judge.views.test_formatter import tf_utils as utils
SAMPLE_SIZE = 16
NUMBERED_MM = ["0", "1", "00", "01", "000", "001", "0000", "0001"]
VALID_MM = ["*"] + NUMBERED_MM
MSG_TOO_MANY_OCCURRENCES = (
"400: Invalid pattern: Pattern cannot have more than one '{}'"
)
MSG_MM_NOT_FOUND = "400: Invalid pattern: Wildcard not found. Wildcard list: {}"
class Pattern:
def __init__(self, ll, mm, rr):
assert mm in VALID_MM, "Invalid wildcard"
self.ll = ll
self.mm = mm
self.rr = rr
def __repr__(self):
return "Pattern('{}', '{}', '{}')".format(self.ll, self.mm, self.rr)
def __eq__(self, other):
return self.__repr__() == other.__repr__()
def __hash__(self):
return self.__repr__().__hash__()
@classmethod
def from_string(cls, text):
for mm in ["*"] + sorted(NUMBERED_MM, key=len, reverse=True):
if mm in text:
if text.count(mm) > 1:
raise Exception(MSG_TOO_MANY_OCCURRENCES.format(mm))
i = text.index(mm)
return cls(text[:i], mm, text[i + len(mm) :])
raise Exception(MSG_MM_NOT_FOUND.format(",".join(VALID_MM)))
def to_string(self):
return self.ll + self.mm + self.rr
def is_valid_test_id(self, test_id):
if self.mm == "*":
return True
if self.mm in NUMBERED_MM:
return test_id.isdigit() and len(test_id) >= len(self.mm)
raise NotImplementedError
def matched(self, name):
return (
name.startswith(self.ll)
and name.endswith(self.rr)
and len(name) >= len(self.ll) + len(self.rr)
and self.is_valid_test_id(self.get_test_id(name))
)
def get_test_id(self, name):
return name[len(self.ll) : len(name) - len(self.rr)]
def get_test_id_from_index(self, index):
assert self.mm in NUMBERED_MM, "Wildcard is not a number"
return str(int(self.mm) + index).zfill(len(self.mm))
def get_name(self, test_id, index=None, use_index=False):
if use_index and self.mm in NUMBERED_MM:
return self.ll + self.get_test_id_from_index(index) + self.rr
return self.ll + test_id + self.rr
def matches(self, names, returns):
if returns == "test_id":
result = [n for n in names]
result = [n for n in result if self.matched(n)]
result = [self.get_test_id(n) for n in result]
return result
else:
raise NotImplementedError
class PatternPair:
def __init__(self, x: Pattern, y: Pattern):
assert x.mm == y.mm, "Input wildcard and output wildcard must be equal"
self.x = x
self.y = y
def __repr__(self):
return "PatternPair({}, {})".format(self.x, self.y)
def __eq__(self, other):
return self.__repr__() == other.__repr__()
def __hash__(self):
return self.__repr__().__hash__()
@classmethod
def from_string_pair(cls, inp_format, out_format):
return cls(Pattern.from_string(inp_format), Pattern.from_string(out_format))
def matches(self, names, returns):
x_test_ids = self.x.matches(names, returns="test_id")
y_test_ids = self.y.matches(names, returns="test_id")
test_ids = set(x_test_ids) & set(y_test_ids)
test_ids = list(sorted(test_ids, key=utils.natural_sorting_key))
if returns == "fast_count":
if self.x.mm == "*":
return len(test_ids)
elif self.x.mm in NUMBERED_MM:
count_valid = 0
for t in test_ids:
if t == self.x.get_test_id_from_index(count_valid):
count_valid += 1
return count_valid
extra_files = list(names)
valid_test_ids = []
for t in test_ids:
if self.x.mm in NUMBERED_MM:
if t != self.x.get_test_id_from_index(len(valid_test_ids)):
continue
inp_name = self.x.get_name(t)
out_name = self.y.get_name(t)
if inp_name == out_name:
continue
if inp_name not in extra_files:
continue
if out_name not in extra_files:
continue
valid_test_ids.append(t)
extra_files.remove(inp_name)
extra_files.remove(out_name)
if returns == "count":
return len(valid_test_ids)
elif returns == "test_id":
return valid_test_ids
elif returns == "test_id_with_extra_files":
return valid_test_ids, extra_files
else:
raise NotImplementedError
def score(self, names):
def ls(s):
return len(s) - s.count("0")
def zs(s):
return -s.count("0")
def vs(s):
return sum(
s.lower().count(c) * w
for c, w in [("a", -1), ("e", -1), ("i", +1), ("o", -1), ("u", -1)]
)
count_score = self.matches(names, returns="fast_count")
len_score = ls(self.x.ll + self.x.rr + self.y.ll + self.y.rr)
zero_score = zs(self.x.ll + self.x.rr + self.y.ll + self.y.rr)
assert self.x.mm in ["*"] + NUMBERED_MM
specific_score = 0 if self.x.mm == "*" else len(self.x.mm)
vowel_score = vs(self.x.ll + self.x.rr) - vs(self.y.ll + self.y.rr)
return count_score, specific_score, len_score, zero_score, vowel_score
def is_string_safe(self):
try:
x = Pattern.from_string(self.x.to_string())
y = Pattern.from_string(self.y.to_string())
return self == PatternPair(x, y)
except:
return False
def maximal(a, key):
max_score = max(map(key, a))
result = [x for x in a if key(x) == max_score]
if len(result) == 1:
return result[0]
else:
print(result)
raise Exception("More than one maximum values")
def get_all_star_pattern_pairs(names):
sample = random.sample(names, min(len(names), SAMPLE_SIZE))
star_pattern_pairs = []
all_prefixes = [n[:i] for n in sample for i in range(len(n) + 1)]
all_prefixes = list(sorted(set(all_prefixes)))
all_suffixes = [n[i:] for n in sample for i in range(len(n) + 1)]
all_suffixes = list(sorted(set(all_suffixes)))
for prefix in all_prefixes:
matched_names = [n for n in names if n.startswith(prefix)]
if len(matched_names) == 2:
mn0, mn1 = matched_names
for i in range(len(prefix) + 1):
x = Pattern(prefix[:i], "*", mn0[len(prefix) :])
y = Pattern(prefix[:i], "*", mn1[len(prefix) :])
star_pattern_pairs.append(PatternPair(x, y))
for suffix in all_suffixes:
matched_names = [n for n in names if n.endswith(suffix)]
if len(matched_names) == 2:
mn0, mn1 = matched_names
for i in range(len(suffix) + 1):
x = Pattern(mn0[: len(mn0) - len(suffix)], "*", suffix[i:])
y = Pattern(mn1[: len(mn1) - len(suffix)], "*", suffix[i:])
star_pattern_pairs.append(PatternPair(x, y))
star_pattern_pairs = list(set(star_pattern_pairs))
return star_pattern_pairs
def get_variant_pattern_pairs(pp):
return [
PatternPair(Pattern(pp.x.ll, mm, pp.x.rr), Pattern(pp.y.ll, mm, pp.y.rr))
for mm in VALID_MM
] + [
PatternPair(Pattern(pp.y.ll, mm, pp.y.rr), Pattern(pp.x.ll, mm, pp.x.rr))
for mm in VALID_MM
]
def find_best_pattern_pair(names):
star_pattern_pairs = get_all_star_pattern_pairs(names)
star_pattern_pairs = [
pp for pp in star_pattern_pairs if pp.matches(names, returns="fast_count") >= 2
]
# for pp in star_pattern_pairs:
# print(pp, pp.is_string_safe(), pp.score(names))
if len(star_pattern_pairs) == 0:
return PatternPair(Pattern("", "*", ""), Pattern("", "*", ""))
best_star_pattern_pair = maximal(star_pattern_pairs, key=lambda pp: pp.score(names))
pattern_pairs = get_variant_pattern_pairs(best_star_pattern_pair)
# for pp in pattern_pairs:
# print(pp, pp.is_string_safe(), pp.score(names))
pattern_pairs = [pp for pp in pattern_pairs if pp.is_string_safe()]
best_pattern_pair = maximal(pattern_pairs, key=lambda pp: pp.score(names))
return best_pattern_pair
def list_dir_recursively(folder):
old_cwd = os.getcwd()
os.chdir(folder)
result = []
for root, _, filenames in os.walk("."):
for filename in filenames:
result.append(os.path.join(root, filename))
os.chdir(old_cwd)
return result
def test_with_dir(folder):
names = list_dir_recursively(folder)
print(folder, find_best_pattern_pair(names))

View file

@ -0,0 +1,15 @@
def get_char_kind(char):
return 1 if char.isdigit() else 2 if char.isalpha() else 3
def natural_sorting_key(name):
result = []
last_kind = -1
for char in name:
curr_kind = get_char_kind(char)
if curr_kind != last_kind:
result.append("")
result[-1] += char
last_kind = curr_kind
return [x.zfill(16) if x.isdigit() else x for x in result]

116
judge/views/tf_logic.py Normal file
View file

@ -0,0 +1,116 @@
import os
from judge.views import test_formatter as tf
from judge.views import tf_pattern as pattern
class TestSuite:
def __init__(
self,
file_id: str,
pattern_pair: pattern.PatternPair,
test_id_list: list,
extra_files: list,
):
self.file_id = file_id
self.pattern_pair = pattern_pair
self.test_id_list = test_id_list
self.extra_files = extra_files
@classmethod
def get_test_suite(cls, file_name: str, inp_format: str, out_format: str):
pattern_pair = pattern.PatternPair.from_string_pair(inp_format, out_format)
names = tf.get_names_in_archive(file_name)
test_id_list, extra_files = pattern_pair.matches(
names, returns="test_id_with_extra_files"
)
return cls(file_name, pattern_pair, test_id_list, extra_files)
def get_name_list(self, add_extra_info=False):
important_files = []
for index, t in enumerate(self.test_id_list):
inp_name = self.pattern_pair.x.get_name(t, index=index, use_index=True)
out_name = self.pattern_pair.y.get_name(t, index=index, use_index=True)
important_files.extend([inp_name, out_name])
result = []
for name in important_files:
if add_extra_info:
result.append({"value": name, "is_extra_file": False})
else:
result.append(name)
for name in self.extra_files:
if add_extra_info:
result.append({"value": name, "is_extra_file": True})
else:
result.append(name)
return result
def is_valid_file_type(file_name):
_, ext = os.path.splitext(file_name)
return ext in [".zip", ".ZIP"]
def preview(params):
bif = params["bef_inp_format"]
bof = params["bef_out_format"]
aif = params["aft_inp_format"]
aof = params["aft_out_format"]
file_str = params["file_str"]
try:
test_suite = TestSuite.get_test_suite(file_str, bif, bof)
bef_preview = test_suite.get_name_list(add_extra_info=True)
try:
test_suite.pattern_pair = pattern.PatternPair.from_string_pair(aif, aof)
aft_preview = test_suite.get_name_list(add_extra_info=True)
return {"bef_preview": bef_preview, "aft_preview": aft_preview}
except:
return {"bef_preview": bef_preview, "aft_preview": []}
except:
test_suite = TestSuite.get_test_suite(file_id, "*", "*")
preview = test_suite.get_name_list(add_extra_info=True)
return {"bef_preview": preview, "aft_preview": []}
def convert(params):
bif = params["bef_inp_format"]
bof = params["bef_out_format"]
aif = params["aft_inp_format"]
aof = params["aft_out_format"]
file_str = params["file_str"]
file_name = params["file_name"]
file_path = params["file_path"]
test_suite = TestSuite.get_test_suite(file_str, bif, bof)
bef_preview = test_suite.get_name_list()
test_suite.pattern_pair = pattern.PatternPair.from_string_pair(aif, aof)
aft_preview = test_suite.get_name_list()
result = tf.get_renamed_archive(
file_str, file_name, file_path, bef_preview, aft_preview
)
return result
def prefill(params):
file_str = params["file_str"]
file_name = params["file_name"]
names = tf.get_names_in_archive(file_str)
pattern_pair = pattern.find_best_pattern_pair(names)
return {
"file_name": file_name,
"inp_format": pattern_pair.x.to_string(),
"out_format": pattern_pair.y.to_string(),
}
def preview_file(file_str):
names = tf.get_names_in_archive(file_str)
return names

268
judge/views/tf_pattern.py Normal file
View file

@ -0,0 +1,268 @@
import os
import random
from judge.views import tf_utils as utils
SAMPLE_SIZE = 16
NUMBERED_MM = ["0", "1", "00", "01", "000", "001", "0000", "0001"]
VALID_MM = ["*"] + NUMBERED_MM
MSG_TOO_MANY_OCCURRENCES = (
"400: Invalid pattern: Pattern cannot have more than one '{}'"
)
MSG_MM_NOT_FOUND = "400: Invalid pattern: Wildcard not found. Wildcard list: {}"
class Pattern:
def __init__(self, ll, mm, rr):
assert mm in VALID_MM, "Invalid wildcard"
self.ll = ll
self.mm = mm
self.rr = rr
def __repr__(self):
return "Pattern('{}', '{}', '{}')".format(self.ll, self.mm, self.rr)
def __eq__(self, other):
return self.__repr__() == other.__repr__()
def __hash__(self):
return self.__repr__().__hash__()
@classmethod
def from_string(cls, text):
for mm in ["*"] + sorted(NUMBERED_MM, key=len, reverse=True):
if mm in text:
if text.count(mm) > 1:
raise Exception(MSG_TOO_MANY_OCCURRENCES.format(mm))
i = text.index(mm)
return cls(text[:i], mm, text[i + len(mm) :])
raise Exception(MSG_MM_NOT_FOUND.format(",".join(VALID_MM)))
def to_string(self):
return self.ll + self.mm + self.rr
def is_valid_test_id(self, test_id):
if self.mm == "*":
return True
if self.mm in NUMBERED_MM:
return test_id.isdigit() and len(test_id) >= len(self.mm)
raise NotImplementedError
def matched(self, name):
return (
name.startswith(self.ll)
and name.endswith(self.rr)
and len(name) >= len(self.ll) + len(self.rr)
and self.is_valid_test_id(self.get_test_id(name))
)
def get_test_id(self, name):
return name[len(self.ll) : len(name) - len(self.rr)]
def get_test_id_from_index(self, index):
assert self.mm in NUMBERED_MM, "Wildcard is not a number"
return str(int(self.mm) + index).zfill(len(self.mm))
def get_name(self, test_id, index=None, use_index=False):
if use_index and self.mm in NUMBERED_MM:
return self.ll + self.get_test_id_from_index(index) + self.rr
return self.ll + test_id + self.rr
def matches(self, names, returns):
if returns == "test_id":
result = [n for n in names]
result = [n for n in result if self.matched(n)]
result = [self.get_test_id(n) for n in result]
return result
else:
raise NotImplementedError
class PatternPair:
def __init__(self, x: Pattern, y: Pattern):
assert x.mm == y.mm, "Input wildcard and output wildcard must be equal"
self.x = x
self.y = y
def __repr__(self):
return "PatternPair({}, {})".format(self.x, self.y)
def __eq__(self, other):
return self.__repr__() == other.__repr__()
def __hash__(self):
return self.__repr__().__hash__()
@classmethod
def from_string_pair(cls, inp_format, out_format):
return cls(Pattern.from_string(inp_format), Pattern.from_string(out_format))
def matches(self, names, returns):
x_test_ids = self.x.matches(names, returns="test_id")
y_test_ids = self.y.matches(names, returns="test_id")
test_ids = set(x_test_ids) & set(y_test_ids)
test_ids = list(sorted(test_ids, key=utils.natural_sorting_key))
if returns == "fast_count":
if self.x.mm == "*":
return len(test_ids)
elif self.x.mm in NUMBERED_MM:
count_valid = 0
for t in test_ids:
if t == self.x.get_test_id_from_index(count_valid):
count_valid += 1
return count_valid
extra_files = list(names)
valid_test_ids = []
for t in test_ids:
if self.x.mm in NUMBERED_MM:
if t != self.x.get_test_id_from_index(len(valid_test_ids)):
continue
inp_name = self.x.get_name(t)
out_name = self.y.get_name(t)
if inp_name == out_name:
continue
if inp_name not in extra_files:
continue
if out_name not in extra_files:
continue
valid_test_ids.append(t)
extra_files.remove(inp_name)
extra_files.remove(out_name)
if returns == "count":
return len(valid_test_ids)
elif returns == "test_id":
return valid_test_ids
elif returns == "test_id_with_extra_files":
return valid_test_ids, extra_files
else:
raise NotImplementedError
def score(self, names):
def ls(s):
return len(s) - s.count("0")
def zs(s):
return -s.count("0")
def vs(s):
return sum(
s.lower().count(c) * w
for c, w in [("a", -1), ("e", -1), ("i", +1), ("o", -1), ("u", -1)]
)
count_score = self.matches(names, returns="fast_count")
len_score = ls(self.x.ll + self.x.rr + self.y.ll + self.y.rr)
zero_score = zs(self.x.ll + self.x.rr + self.y.ll + self.y.rr)
assert self.x.mm in ["*"] + NUMBERED_MM
specific_score = 0 if self.x.mm == "*" else len(self.x.mm)
vowel_score = vs(self.x.ll + self.x.rr) - vs(self.y.ll + self.y.rr)
return count_score, specific_score, len_score, zero_score, vowel_score
def is_string_safe(self):
try:
x = Pattern.from_string(self.x.to_string())
y = Pattern.from_string(self.y.to_string())
return self == PatternPair(x, y)
except:
return False
def maximal(a, key):
max_score = max(map(key, a))
result = [x for x in a if key(x) == max_score]
if len(result) == 1:
return result[0]
else:
print(result)
raise Exception("More than one maximum values")
def get_all_star_pattern_pairs(names):
sample = random.sample(names, min(len(names), SAMPLE_SIZE))
star_pattern_pairs = []
all_prefixes = [n[:i] for n in sample for i in range(len(n) + 1)]
all_prefixes = list(sorted(set(all_prefixes)))
all_suffixes = [n[i:] for n in sample for i in range(len(n) + 1)]
all_suffixes = list(sorted(set(all_suffixes)))
for prefix in all_prefixes:
matched_names = [n for n in names if n.startswith(prefix)]
if len(matched_names) == 2:
mn0, mn1 = matched_names
for i in range(len(prefix) + 1):
x = Pattern(prefix[:i], "*", mn0[len(prefix) :])
y = Pattern(prefix[:i], "*", mn1[len(prefix) :])
star_pattern_pairs.append(PatternPair(x, y))
for suffix in all_suffixes:
matched_names = [n for n in names if n.endswith(suffix)]
if len(matched_names) == 2:
mn0, mn1 = matched_names
for i in range(len(suffix) + 1):
x = Pattern(mn0[: len(mn0) - len(suffix)], "*", suffix[i:])
y = Pattern(mn1[: len(mn1) - len(suffix)], "*", suffix[i:])
star_pattern_pairs.append(PatternPair(x, y))
star_pattern_pairs = list(set(star_pattern_pairs))
return star_pattern_pairs
def get_variant_pattern_pairs(pp):
return [
PatternPair(Pattern(pp.x.ll, mm, pp.x.rr), Pattern(pp.y.ll, mm, pp.y.rr))
for mm in VALID_MM
] + [
PatternPair(Pattern(pp.y.ll, mm, pp.y.rr), Pattern(pp.x.ll, mm, pp.x.rr))
for mm in VALID_MM
]
def find_best_pattern_pair(names):
star_pattern_pairs = get_all_star_pattern_pairs(names)
star_pattern_pairs = [
pp for pp in star_pattern_pairs if pp.matches(names, returns="fast_count") >= 2
]
# for pp in star_pattern_pairs:
# print(pp, pp.is_string_safe(), pp.score(names))
if len(star_pattern_pairs) == 0:
return PatternPair(Pattern("", "*", ""), Pattern("", "*", ""))
best_star_pattern_pair = maximal(star_pattern_pairs, key=lambda pp: pp.score(names))
pattern_pairs = get_variant_pattern_pairs(best_star_pattern_pair)
# for pp in pattern_pairs:
# print(pp, pp.is_string_safe(), pp.score(names))
pattern_pairs = [pp for pp in pattern_pairs if pp.is_string_safe()]
best_pattern_pair = maximal(pattern_pairs, key=lambda pp: pp.score(names))
return best_pattern_pair
def list_dir_recursively(folder):
old_cwd = os.getcwd()
os.chdir(folder)
result = []
for root, _, filenames in os.walk("."):
for filename in filenames:
result.append(os.path.join(root, filename))
os.chdir(old_cwd)
return result
def test_with_dir(folder):
names = list_dir_recursively(folder)
print(folder, find_best_pattern_pair(names))

15
judge/views/tf_utils.py Normal file
View file

@ -0,0 +1,15 @@
def get_char_kind(char):
return 1 if char.isdigit() else 2 if char.isalpha() else 3
def natural_sorting_key(name):
result = []
last_kind = -1
for char in name:
curr_kind = get_char_kind(char)
if curr_kind != last_kind:
result.append("")
result[-1] += char
last_kind = curr_kind
return [x.zfill(16) if x.isdigit() else x for x in result]

View file

@ -5612,6 +5612,42 @@ msgstr "pretests"
msgid "main tests" msgid "main tests"
msgstr "test chính thức" msgstr "test chính thức"
#: templates/test_formatter/test_formatter.html:7
msgid "Upload"
msgstr "Tải lên"
#: templates/test_formatter/edit_test_formatter.html:103
msgid "Before"
msgstr "Trước"
#: templates/test_formatter/edit_test_formatter.html:104
msgid "Input format"
msgstr "Định dạng đầu vào"
#: templates/test_formatter/edit_test_formatter.html:107
msgid "Output format"
msgstr "Định dạng đầu ra"
#: templates/test_formatter/edit_test_formatter.html:111
msgid "After"
msgstr "Sau"
#: templates/test_formatter/edit_test_formatter.html:126
msgid "File name"
msgstr "Tên file"
#: templates/test_formatter/edit_test_formatter.html:127
msgid "Preview"
msgstr "Xem trước"
#: templates/test_formatter/edit_test_formatter.html:131
msgid "Convert"
msgstr "Chuyển đổi"
#: templates/test_formatter/edit_test_formatter.html:132
msgid "Download"
msgstr "Tải xuống"
#: templates/ticket/feed.html:21 #: templates/ticket/feed.html:21
msgid " replied" msgid " replied"
msgstr "" msgstr ""

View file

@ -0,0 +1,82 @@
{% extends 'base.html' %}
{% block media %}
<style>
.container {
width: 100%;
margin: auto;
}
.preview-container {
padding-bottom: 16px;
}
#preview {
background-color: rgb(220, 220, 220);
border: solid 2px rgb(180, 180, 180);
padding: 8px;
max-height: 134px;
height: 20%;
overflow-y: scroll;
}
.button-container {
text-align:left;
}
.button {
display:inline-block;
}
</style>
{% endblock %}
{% block js_media %}
<script>
$(document).ready(function(){
$("#download").on("click", function(event) {
event.preventDefault()
var file_path = document.getElementById('file_path').value;
$.ajax({
url: "{{url('download_page')}}",
type: 'POST',
data: {
file_path: file_path
},
xhrFields: {
responseType: 'blob'
},
success: function(data) {
var url = window.URL.createObjectURL(data);
var a = document.createElement('a');
a.href = url;
a.download = file_path.split('/').pop();
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
},
error: function(error) {
alert(error);
console.log(error.message)
}
})
});
});
</script>
{% endblock %}
{% block body %}
<div class="container">
<input type="hidden" id="file_path" value={{file_path}}>
<input type="hidden" id="file_path_getnames" value={{file_path_getnames}}>
<div class="preview-container">
<h2>{{_('Download')}}</h2><br>
<h4>{{file_name}}</h4><br>
<div id="preview">
{{ response|safe }}
</div>
</div>
<div class="button-container">
<button type="submit" id="download" class="button">{{_('Download')}}</button>
<a href="{{url('edit_page')}}?file_path={{file_path_getnames}}" id="edit" class="button">{{_('Edit')}}</a>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,135 @@
{% extends 'base.html' %}
{% block media %}
<style>
.container {
width: 100%;
margin: auto;
}
.column {
flex: 50%;
}
.left {
padding-right: 16px;
}
.right {
padding-left: 16px;
}
.row {
display: flex;
}
input {
width: 100%;
}
.preview-container {
padding-bottom: 16px;
}
#preview {
background-color: rgb(220, 220, 220);
border: solid 2px rgb(180, 180, 180);
padding: 8px;
max-height: 134px;
height: 20%;
overflow-y: scroll;
}
.button-container {
text-align:left;
}
.button {
display:inline-block;
}
</style>
{% endblock %}
{% block js_media %}
<script>
$(document).ready(function(){
$("#convert").on("click", function(event) {
event.preventDefault()
$.ajax({
url: "{{url('edit_page')}}",
type: 'POST',
data: {
action: 'convert',
bef_inp_format: $("#bef_inp_format").val(),
bef_out_format: $("#bef_out_format").val(),
aft_inp_format: $("#aft_inp_format").val(),
aft_out_format: $("#aft_out_format").val(),
file_name: $('#file_name').val()
},
success: function(data) {
console.log(data)
$('#preview').html(data);
},
error: function(error) {
alert(error);
console.log(error.message)
}
})
});
});
$(document).ready(function(){
$("#download").on("click", function(event) {
event.preventDefault()
window.location.href = "{{url('edit_page')}}";
$.ajax({
url: "{{url('edit_page')}}",
type: 'POST',
data: {
action: 'download'
},
success: function(data) {
var file_path = data;
window.location.href = "{{url('download_page')}}" + "?file_path="+file_path;
},
error: function(error) {
alert(error);
console.log(error.message)
}
})
});
});
</script>
<script src="{{ static('mathjax3_config.js') }}"></script>
<script src="http://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script>
<script src="{{ static('pagedown_math.js') }}"></script>
{% endblock %}
{% block body %}
<div class="container">
<div class="row">
{% csrf_token %}
<div class="column left">
<h3>{{_('Before')}}</h3><br>
<label for="fname">{{_('Input format')}}</label><br>
<input type="text" id="bef_inp_format" value="{{files_list[0]}}" readonly><br>
<label for="lname">{{_('Output format')}}</label><br>
<input type="text" id="bef_out_format" value="{{files_list[1]}}" readonly><br><br>
</div>
<div class="column right">
<h3>{{_('After')}}</h3><br>
<label for="fname">{{_('Input format')}}</label><br>
<input type="text" id="aft_inp_format" value="input.000"><br>
<label for="lname">{{_('Output format')}}</label><br>
<input type="text" id="aft_out_format" value="output.000"><br><br>
</div>
</div>
<div class="preview-container">
<h3>{{_('Preview')}}</h3><br>
<div id="preview">
{{ res|safe }}
</div>
</div>
<div class="filename-container">
<h3>{{_('File name')}}</h3><br>
<input type="text" id="file_name" value="{{file_name}}"><br><br>
</div>
<div class="button-container">
<button type="submit" id="convert" class="button">{{_('Convert')}}</button>
<button type="submit" id="download" class="button">{{_('Download')}}</button>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% block body %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form }}
<button type="submit">{{_('Upload')}}</button>
</form>
{% endblock %}