后台出题人

This commit is contained in:
2025-10-03 14:30:40 +08:00
parent ce2a4629da
commit 7835cf013a
2 changed files with 190 additions and 110 deletions

View File

@@ -1,6 +1,7 @@
import hashlib import hashlib
import json import json
import os import os
# import shutil # import shutil
import tempfile import tempfile
import zipfile import zipfile
@@ -11,7 +12,7 @@ from django.db import transaction
from django.db.models import Q from django.db.models import Q
from django.http import StreamingHttpResponse, FileResponse from django.http import StreamingHttpResponse, FileResponse
from account.decorators import problem_permission_required, ensure_created_by, super_admin_required from account.decorators import problem_permission_required, ensure_created_by
from contest.models import Contest, ContestStatus from contest.models import Contest, ContestStatus
from fps.parser import FPSHelper, FPSParser from fps.parser import FPSHelper, FPSParser
from judge.dispatcher import SPJCompiler from judge.dispatcher import SPJCompiler
@@ -22,12 +23,23 @@ from utils.constants import Difficulty
from utils.shortcuts import rand_str, natural_sort_key from utils.shortcuts import rand_str, natural_sort_key
from utils.tasks import delete_files from utils.tasks import delete_files
from ..models import Problem, ProblemRuleType, ProblemTag from ..models import Problem, ProblemRuleType, ProblemTag
from ..serializers import (CreateContestProblemSerializer, CompileSPJSerializer, from ..serializers import (
CreateProblemSerializer, EditProblemSerializer, EditContestProblemSerializer, CreateContestProblemSerializer,
ProblemAdminSerializer, ProblemAdminListSerializer, TestCaseUploadForm, CompileSPJSerializer,
ContestProblemMakePublicSerializer, AddContestProblemSerializer, ExportProblemSerializer, CreateProblemSerializer,
ExportProblemRequestSerializer, UploadProblemForm, ImportProblemSerializer, EditProblemSerializer,
FPSProblemSerializer) EditContestProblemSerializer,
ProblemAdminSerializer,
ProblemAdminListSerializer,
TestCaseUploadForm,
ContestProblemMakePublicSerializer,
AddContestProblemSerializer,
ExportProblemSerializer,
ExportProblemRequestSerializer,
UploadProblemForm,
ImportProblemSerializer,
FPSProblemSerializer,
)
from ..utils import TEMPLATE_BASE, build_problem_template from ..utils import TEMPLATE_BASE, build_problem_template
@@ -70,11 +82,13 @@ class TestCaseZipProcessor(object):
# ["1.in", "1.out", "2.in", "2.out"] => [("1.in", "1.out"), ("2.in", "2.out")] # ["1.in", "1.out", "2.in", "2.out"] => [("1.in", "1.out"), ("2.in", "2.out")]
test_case_list = zip(*[test_case_list[i::2] for i in range(2)]) test_case_list = zip(*[test_case_list[i::2] for i in range(2)])
for index, item in enumerate(test_case_list): for index, item in enumerate(test_case_list):
data = {"stripped_output_md5": md5_cache[item[1]], data = {
"input_size": size_cache[item[0]], "stripped_output_md5": md5_cache[item[1]],
"output_size": size_cache[item[1]], "input_size": size_cache[item[0]],
"input_name": item[0], "output_size": size_cache[item[1]],
"output_name": item[1]} "input_name": item[0],
"output_name": item[1],
}
info.append(data) info.append(data)
test_case_info["test_cases"][str(index + 1)] = data test_case_info["test_cases"][str(index + 1)] = data
@@ -137,10 +151,13 @@ class TestCaseAPI(CSRFExemptAPIView, TestCaseZipProcessor):
with zipfile.ZipFile(file_name, "w") as file: with zipfile.ZipFile(file_name, "w") as file:
for test_case in name_list: for test_case in name_list:
file.write(f"{test_case_dir}/{test_case}", test_case) file.write(f"{test_case_dir}/{test_case}", test_case)
response = StreamingHttpResponse(FileWrapper(open(file_name, "rb")), response = StreamingHttpResponse(
content_type="application/octet-stream") FileWrapper(open(file_name, "rb")), content_type="application/octet-stream"
)
response["Content-Disposition"] = f"attachment; filename=problem_{problem.id}_test_cases.zip" response["Content-Disposition"] = (
f"attachment; filename=problem_{problem.id}_test_cases.zip"
)
response["Content-Length"] = os.path.getsize(file_name) response["Content-Length"] = os.path.getsize(file_name)
return response return response
@@ -165,7 +182,9 @@ class CompileSPJAPI(APIView):
def post(self, request): def post(self, request):
data = request.data data = request.data
spj_version = rand_str(8) spj_version = rand_str(8)
error = SPJCompiler(data["spj_code"], spj_version, data["spj_language"]).compile_spj() error = SPJCompiler(
data["spj_code"], spj_version, data["spj_language"]
).compile_spj()
if error: if error:
return self.error(error) return self.error(error)
else: else:
@@ -181,7 +200,8 @@ class ProblemBase(APIView):
if not data["spj_compile_ok"]: if not data["spj_compile_ok"]:
return "SPJ code must be compiled successfully" return "SPJ code must be compiled successfully"
data["spj_version"] = hashlib.md5( data["spj_version"] = hashlib.md5(
(data["spj_language"] + ":" + data["spj_code"]).encode("utf-8")).hexdigest() (data["spj_language"] + ":" + data["spj_code"]).encode("utf-8")
).hexdigest()
else: else:
data["spj_language"] = None data["spj_language"] = None
data["spj_code"] = None data["spj_code"] = None
@@ -227,7 +247,6 @@ class ProblemAPI(ProblemBase):
@problem_permission_required @problem_permission_required
def get(self, request): def get(self, request):
problem_id = request.GET.get("id") problem_id = request.GET.get("id")
rule_type = request.GET.get("rule_type")
user = request.user user = request.user
if problem_id: if problem_id:
try: try:
@@ -237,19 +256,24 @@ class ProblemAPI(ProblemBase):
except Problem.DoesNotExist: except Problem.DoesNotExist:
return self.error("Problem does not exist") return self.error("Problem does not exist")
problems = Problem.objects.filter(contest_id__isnull=True).order_by("-create_time") problems = Problem.objects.filter(contest_id__isnull=True).order_by(
if rule_type: "-create_time"
if rule_type not in ProblemRuleType.choices(): )
return self.error("Invalid rule_type")
else: author = request.GET.get("author", "")
problems = problems.filter(rule_type=rule_type) if author:
problems = problems.filter(created_by__username=author)
keyword = request.GET.get("keyword", "").strip() keyword = request.GET.get("keyword", "").strip()
if keyword: if keyword:
problems = problems.filter(Q(title__icontains=keyword) | Q(_id__icontains=keyword)) problems = problems.filter(
Q(title__icontains=keyword) | Q(_id__icontains=keyword)
)
if not user.can_mgmt_all_problem(): if not user.can_mgmt_all_problem():
problems = problems.filter(created_by=user) problems = problems.filter(created_by=user)
return self.success(self.paginate_data(request, problems, ProblemAdminListSerializer)) return self.success(
self.paginate_data(request, problems, ProblemAdminListSerializer)
)
@problem_permission_required @problem_permission_required
@validate_serializer(EditProblemSerializer) @validate_serializer(EditProblemSerializer)
@@ -266,7 +290,11 @@ class ProblemAPI(ProblemBase):
_id = data["_id"] _id = data["_id"]
if not _id: if not _id:
return self.error("Display ID is required") return self.error("Display ID is required")
if Problem.objects.exclude(id=problem_id).filter(_id=_id, contest_id__isnull=True).exists(): if (
Problem.objects.exclude(id=problem_id)
.filter(_id=_id, contest_id__isnull=True)
.exists()
):
return self.error("Display ID already exists") return self.error("Display ID already exists")
error_info = self.common_checks(request) error_info = self.common_checks(request)
@@ -370,7 +398,9 @@ class ContestProblemAPI(ProblemBase):
keyword = request.GET.get("keyword") keyword = request.GET.get("keyword")
if keyword: if keyword:
problems = problems.filter(title__contains=keyword) problems = problems.filter(title__contains=keyword)
return self.success(self.paginate_data(request, problems, ProblemAdminListSerializer)) return self.success(
self.paginate_data(request, problems, ProblemAdminListSerializer)
)
@validate_serializer(EditContestProblemSerializer) @validate_serializer(EditContestProblemSerializer)
def put(self, request): def put(self, request):
@@ -396,7 +426,11 @@ class ContestProblemAPI(ProblemBase):
_id = data["_id"] _id = data["_id"]
if not _id: if not _id:
return self.error("Display ID is required") return self.error("Display ID is required")
if Problem.objects.exclude(id=problem_id).filter(_id=_id, contest=contest).exists(): if (
Problem.objects.exclude(id=problem_id)
.filter(_id=_id, contest=contest)
.exists()
):
return self.error("Display ID already exists") return self.error("Display ID already exists")
error_info = self.common_checks(request) error_info = self.common_checks(request)
@@ -500,10 +534,16 @@ class ExportProblemAPI(APIView):
def choose_answers(self, user, problem): def choose_answers(self, user, problem):
ret = [] ret = []
for item in problem.languages: for item in problem.languages:
submission = Submission.objects.filter(problem=problem, submission = (
user_id=user.id, Submission.objects.filter(
language=item, problem=problem,
result=JudgeStatus.ACCEPTED).order_by("-create_time").first() user_id=user.id,
language=item,
result=JudgeStatus.ACCEPTED,
)
.order_by("-create_time")
.first()
)
if submission: if submission:
ret.append({"language": submission.language, "code": submission.code}) ret.append({"language": submission.language, "code": submission.code})
return ret return ret
@@ -512,20 +552,28 @@ class ExportProblemAPI(APIView):
info = ExportProblemSerializer(problem).data info = ExportProblemSerializer(problem).data
info["answers"] = self.choose_answers(user, problem=problem) info["answers"] = self.choose_answers(user, problem=problem)
compression = zipfile.ZIP_DEFLATED compression = zipfile.ZIP_DEFLATED
zip_file.writestr(zinfo_or_arcname=f"{index}/problem.json", zip_file.writestr(
data=json.dumps(info, indent=4), zinfo_or_arcname=f"{index}/problem.json",
compress_type=compression) data=json.dumps(info, indent=4),
problem_test_case_dir = os.path.join(settings.TEST_CASE_DIR, problem.test_case_id) compress_type=compression,
)
problem_test_case_dir = os.path.join(
settings.TEST_CASE_DIR, problem.test_case_id
)
with open(os.path.join(problem_test_case_dir, "info")) as f: with open(os.path.join(problem_test_case_dir, "info")) as f:
info = json.load(f) info = json.load(f)
for k, v in info["test_cases"].items(): for k, v in info["test_cases"].items():
zip_file.write(filename=os.path.join(problem_test_case_dir, v["input_name"]), zip_file.write(
arcname=f"{index}/testcase/{v['input_name']}", filename=os.path.join(problem_test_case_dir, v["input_name"]),
compress_type=compression) arcname=f"{index}/testcase/{v['input_name']}",
compress_type=compression,
)
if not info["spj"]: if not info["spj"]:
zip_file.write(filename=os.path.join(problem_test_case_dir, v["output_name"]), zip_file.write(
arcname=f"{index}/testcase/{v['output_name']}", filename=os.path.join(problem_test_case_dir, v["output_name"]),
compress_type=compression) arcname=f"{index}/testcase/{v['output_name']}",
compress_type=compression,
)
@validate_serializer(ExportProblemRequestSerializer) @validate_serializer(ExportProblemRequestSerializer)
def get(self, request): def get(self, request):
@@ -538,7 +586,12 @@ class ExportProblemAPI(APIView):
path = f"/tmp/{rand_str()}.zip" path = f"/tmp/{rand_str()}.zip"
with zipfile.ZipFile(path, "w") as zip_file: with zipfile.ZipFile(path, "w") as zip_file:
for index, problem in enumerate(problems): for index, problem in enumerate(problems):
self.process_one_problem(zip_file=zip_file, user=request.user, problem=problem, index=index + 1) self.process_one_problem(
zip_file=zip_file,
user=request.user,
problem=problem,
index=index + 1,
)
delete_files.send_with_options(args=(path,), delay=300_000) delete_files.send_with_options(args=(path,), delay=300_000)
resp = FileResponse(open(path, "rb")) resp = FileResponse(open(path, "rb"))
resp["Content-Type"] = "application/zip" resp["Content-Type"] = "application/zip"
@@ -572,7 +625,9 @@ class ImportProblemAPI(CSRFExemptAPIView, TestCaseZipProcessor):
problem_info = json.load(f) problem_info = json.load(f)
serializer = ImportProblemSerializer(data=problem_info) serializer = ImportProblemSerializer(data=problem_info)
if not serializer.is_valid(): if not serializer.is_valid():
return self.error(f"Invalid problem format, error is {serializer.errors}") return self.error(
f"Invalid problem format, error is {serializer.errors}"
)
else: else:
problem_info = serializer.data problem_info = serializer.data
for item in problem_info["template"].keys(): for item in problem_info["template"].keys():
@@ -581,44 +636,52 @@ class ImportProblemAPI(CSRFExemptAPIView, TestCaseZipProcessor):
problem_info["display_id"] = problem_info["display_id"][:24] problem_info["display_id"] = problem_info["display_id"][:24]
for k, v in problem_info["template"].items(): for k, v in problem_info["template"].items():
problem_info["template"][k] = build_problem_template(v["prepend"], v["template"], problem_info["template"][k] = build_problem_template(
v["append"]) v["prepend"], v["template"], v["append"]
)
spj = problem_info["spj"] is not None spj = problem_info["spj"] is not None
rule_type = problem_info["rule_type"] rule_type = problem_info["rule_type"]
test_case_score = problem_info["test_case_score"] test_case_score = problem_info["test_case_score"]
# process test case # process test case
_, test_case_id = self.process_zip(tmp_file, spj=spj, dir=f"{i}/testcase/") _, test_case_id = self.process_zip(
tmp_file, spj=spj, dir=f"{i}/testcase/"
)
problem_obj = Problem.objects.create(_id=problem_info["display_id"], problem_obj = Problem.objects.create(
title=problem_info["title"], _id=problem_info["display_id"],
description=problem_info["description"]["value"], title=problem_info["title"],
input_description=problem_info["input_description"][ description=problem_info["description"]["value"],
"value"], input_description=problem_info["input_description"][
output_description=problem_info["output_description"][ "value"
"value"], ],
hint=problem_info["hint"]["value"], output_description=problem_info["output_description"][
test_case_score=test_case_score if test_case_score else [], "value"
time_limit=problem_info["time_limit"], ],
memory_limit=problem_info["memory_limit"], hint=problem_info["hint"]["value"],
samples=problem_info["samples"], test_case_score=test_case_score if test_case_score else [],
template=problem_info["template"], time_limit=problem_info["time_limit"],
rule_type=problem_info["rule_type"], memory_limit=problem_info["memory_limit"],
source=problem_info["source"], samples=problem_info["samples"],
spj=spj, template=problem_info["template"],
spj_code=problem_info["spj"]["code"] if spj else None, rule_type=problem_info["rule_type"],
spj_language=problem_info["spj"][ source=problem_info["source"],
"language"] if spj else None, spj=spj,
spj_version=rand_str(8) if spj else "", spj_code=problem_info["spj"]["code"] if spj else None,
languages=SysOptions.language_names, spj_language=problem_info["spj"]["language"]
created_by=request.user, if spj
visible=False, else None,
difficulty=Difficulty.MID, spj_version=rand_str(8) if spj else "",
total_score=sum(item["score"] for item in test_case_score) languages=SysOptions.language_names,
if rule_type == ProblemRuleType.OI else 0, created_by=request.user,
test_case_id=test_case_id visible=False,
) difficulty=Difficulty.MID,
total_score=sum(item["score"] for item in test_case_score)
if rule_type == ProblemRuleType.OI
else 0,
test_case_id=test_case_id,
)
for tag_name in problem_info["tags"]: for tag_name in problem_info["tags"]:
tag_obj, _ = ProblemTag.objects.get_or_create(name=tag_name) tag_obj, _ = ProblemTag.objects.get_or_create(name=tag_name)
problem_obj.tags.add(tag_obj) problem_obj.tags.add(tag_obj)
@@ -644,30 +707,34 @@ class FPSProblemImport(CSRFExemptAPIView):
our_lang = lang = t["language"] our_lang = lang = t["language"]
if lang == "Python": if lang == "Python":
our_lang = "Python3" our_lang = "Python3"
template[our_lang] = TEMPLATE_BASE.format(prepend.get(lang, ""), t["code"], append.get(lang, "")) template[our_lang] = TEMPLATE_BASE.format(
prepend.get(lang, ""), t["code"], append.get(lang, "")
)
spj = problem_data["spj"] is not None spj = problem_data["spj"] is not None
Problem.objects.create(_id=f"fps-{rand_str(4)}", Problem.objects.create(
title=problem_data["title"], _id=f"fps-{rand_str(4)}",
description=problem_data["description"], title=problem_data["title"],
input_description=problem_data["input"], description=problem_data["description"],
output_description=problem_data["output"], input_description=problem_data["input"],
hint=problem_data["hint"], output_description=problem_data["output"],
test_case_score=problem_data["test_case_score"], hint=problem_data["hint"],
time_limit=time_limit, test_case_score=problem_data["test_case_score"],
memory_limit=problem_data["memory_limit"]["value"], time_limit=time_limit,
samples=problem_data["samples"], memory_limit=problem_data["memory_limit"]["value"],
template=template, samples=problem_data["samples"],
rule_type=ProblemRuleType.ACM, template=template,
source=problem_data.get("source", ""), rule_type=ProblemRuleType.ACM,
spj=spj, source=problem_data.get("source", ""),
spj_code=problem_data["spj"]["code"] if spj else None, spj=spj,
spj_language=problem_data["spj"]["language"] if spj else None, spj_code=problem_data["spj"]["code"] if spj else None,
spj_version=rand_str(8) if spj else "", spj_language=problem_data["spj"]["language"] if spj else None,
visible=False, spj_version=rand_str(8) if spj else "",
languages=SysOptions.language_names, visible=False,
created_by=creator, languages=SysOptions.language_names,
difficulty=Difficulty.MID, created_by=creator,
test_case_id=problem_data["test_case_id"]) difficulty=Difficulty.MID,
test_case_id=problem_data["test_case_id"],
)
def post(self, request): def post(self, request):
form = UploadProblemForm(request.POST, request.FILES) form = UploadProblemForm(request.POST, request.FILES)
@@ -691,10 +758,19 @@ class FPSProblemImport(CSRFExemptAPIView):
test_case_dir = os.path.join(settings.TEST_CASE_DIR, test_case_id) test_case_dir = os.path.join(settings.TEST_CASE_DIR, test_case_id)
os.mkdir(test_case_dir) os.mkdir(test_case_dir)
score = [] score = []
for item in helper.save_test_case(_problem, test_case_dir)["test_cases"].values(): for item in helper.save_test_case(_problem, test_case_dir)[
score.append({"score": 0, "input_name": item["input_name"], "test_cases"
"output_name": item.get("output_name")}) ].values():
problem_data = helper.save_image(_problem, settings.UPLOAD_DIR, settings.UPLOAD_PREFIX) score.append(
{
"score": 0,
"input_name": item["input_name"],
"output_name": item.get("output_name"),
}
)
problem_data = helper.save_image(
_problem, settings.UPLOAD_DIR, settings.UPLOAD_PREFIX
)
s = FPSProblemSerializer(data=problem_data) s = FPSProblemSerializer(data=problem_data)
if not s.is_valid(): if not s.is_valid():
return self.error(f"Parse FPS file error: {s.errors}") return self.error(f"Parse FPS file error: {s.errors}")
@@ -715,4 +791,4 @@ class ProblemVisibleAPI(APIView):
self.error("problem does not exists") self.error("problem does not exists")
problem.visible = not problem.visible problem.visible = not problem.visible
problem.save() problem.save()
return self.success() return self.success()

View File

@@ -192,15 +192,19 @@ class ProblemSolvedPeopleCount(APIView):
class ProblemAuthorAPI(APIView): class ProblemAuthorAPI(APIView):
def get(self, request): def get(self, request):
# 统计出题用户 show_all = request.GET.get("all", "0") == "1"
cached_data = cache.get(CacheKey.problem_authors) cached_data = cache.get(
f"{CacheKey.problem_authors}{'_all' if show_all else '_only_visible'}"
)
if cached_data: if cached_data:
return self.success(cached_data) return self.success(cached_data)
problem_filter = {"contest_id__isnull": True, "created_by__is_disabled": False}
if not show_all:
problem_filter["visible"] = True
authors = ( authors = (
Problem.objects.filter( Problem.objects.filter(**problem_filter)
visible=True, contest_id__isnull=True, created_by__is_disabled=False
)
.values("created_by__username") .values("created_by__username")
.annotate(problem_count=Count("id")) .annotate(problem_count=Count("id"))
.order_by("-problem_count") .order_by("-problem_count")
@@ -213,5 +217,5 @@ class ProblemAuthorAPI(APIView):
for author in authors for author in authors
] ]
cache.set(CacheKey.problem_authors, result, 3600) cache.set(CacheKey.problem_authors, result, 7200)
return self.success(result) return self.success(result)