merge problem and contest_problem

This commit is contained in:
zema1 2017-09-24 09:48:17 +08:00
parent e9c7344815
commit 51c229a2c5
13 changed files with 263 additions and 97 deletions

View File

@ -84,8 +84,8 @@ class ACMContestRank(ContestRank):
class OIContestRank(ContestRank):
total_score = models.IntegerField(default=0)
# {23: {"score": 80, "total_score": 100}}
# key is problem id
# {23: 333}}
# key is problem id, value is current score
submission_info = JSONField(default={})
class Meta:

View File

@ -12,7 +12,7 @@ from account.models import User
from conf.models import JudgeServer, JudgeServerToken
from contest.models import ContestRuleType, ACMContestRank, OIContestRank
from judge.languages import languages
from problem.models import Problem, ProblemRuleType, ContestProblem
from problem.models import Problem, ProblemRuleType
from submission.models import JudgeStatus, Submission
from utils.cache import judge_cache
from utils.constants import CacheKey
@ -35,12 +35,13 @@ class JudgeDispatcher(object):
self.token = hashlib.sha256(token.encode("utf-8")).hexdigest()
self.redis_conn = judge_cache
self.submission = Submission.objects.get(pk=submission_id)
if self.submission.contest_id:
self.problem = ContestProblem.objects.select_related("contest") \
.get(_id=problem_id, contest_id=self.submission.contest_id)
self.contest_id = self.submission.contest_id
if self.contest_id:
self.problem = Problem.objects.select_related("contest") \
.get(id=problem_id, contest_id=self.contest_id)
self.contest = self.problem.contest
else:
self.problem = Problem.objects.get(_id=problem_id)
self.problem = Problem.objects.get(id=problem_id)
def _request(self, url, data=None):
kwargs = {"headers": {"X-Judge-Server-Token": self.token,
@ -72,10 +73,28 @@ class JudgeDispatcher(object):
server.used_instance_number = F("task_number") - 1
server.save()
def _compute_statistic_info(self, resp_data):
# 用时和内存占用保存为多个测试点中最长的那个
self.submission.statistic_info["time_cost"] = max([x["cpu_time"] for x in resp_data])
self.submission.statistic_info["memory_cost"] = max([x["memory"] for x in resp_data])
# sum up the score in OI mode
if self.problem.rule_type == ProblemRuleType.OI:
score = 0
try:
for i in range(len(resp_data)):
if resp_data[i]["result"] == JudgeStatus.ACCEPTED:
score += self.problem.test_case_score[i]["score"]
except IndexError:
logger.error(f"Index Error raised when summing up the score in problem {self.problem.id}")
self.submission.statistic_info["score"] = 0
return
self.submission.statistic_info["score"] = score
def judge(self, output=False):
server = self.choose_judge_server()
if not server:
data = {"submission_id": self.submission.id, "problem_id": self.problem._id}
data = {"submission_id": self.submission.id, "problem_id": self.problem.id}
self.redis_conn.lpush(CacheKey.waiting_queue, json.dumps(data))
return
@ -108,11 +127,7 @@ class JudgeDispatcher(object):
self.submission.result = JudgeStatus.COMPILE_ERROR
self.submission.statistic_info["err_info"] = resp["data"]
else:
# 用时和内存占用保存为多个测试点中最长的那个
self.submission.statistic_info["time_cost"] = max([x["cpu_time"] for x in resp["data"]])
self.submission.statistic_info["memory_cost"] = max([x["memory"] for x in resp["data"]])
# todo OI statistic_info["score"]
self._compute_statistic_info(resp["data"])
error_test_case = list(filter(lambda case: case["result"] != 0, resp["data"]))
# ACM模式下,多个测试点全部正确则AC否则取第一个错误的测试点的状态
# OI模式下, 若多个测试点全部正确则AC 若全部错误则取第一个错误测试点状态,否则为部分正确
@ -126,7 +141,7 @@ class JudgeDispatcher(object):
self.release_judge_res(server.id)
self.update_problem_status()
if self.submission.contest_id:
if self.contest_id:
self.update_contest_rank()
# 至此判题结束,尝试处理任务队列中剩余的任务
@ -141,15 +156,11 @@ class JudgeDispatcher(object):
def update_problem_status(self):
with transaction.atomic():
# prepare problem and user_profile
if self.submission.contest_id:
problem = ContestProblem.objects.select_for_update().get(contest_id=self.contest.id,
_id=self.problem._id)
else:
problem = Problem.objects.select_for_update().get(_id=self.problem._id)
problem = Problem.objects.select_for_update().get(contest_id=self.contest_id, id=self.problem.id)
problem_info = problem.statistic_info
user = User.objects.select_for_update().select_for_update("userprofile").get(id=self.submission.user_id)
user_profile = user.userprofile
if self.submission.contest_id:
if self.contest_id:
key = "contest_problems"
else:
key = "problems"
@ -159,7 +170,7 @@ class JudgeDispatcher(object):
# update submission and accepted number counter
# only when submission is not in contest, we update user profile,
# in other words, users' submission in a contest will not be counted in user profile
if not self.submission.contest_id:
if not self.contest_id:
user_profile.submission_number += 1
if self.submission.result == JudgeStatus.ACCEPTED:
user_profile.accepted_number += 1
@ -167,7 +178,7 @@ class JudgeDispatcher(object):
if self.submission.result == JudgeStatus.ACCEPTED:
problem.accepted_number += 1
problem_id = str(self.problem._id)
problem_id = str(self.problem.id)
if self.problem.rule_type == ProblemRuleType.ACM:
# update acm problem info
result = str(self.submission.result)
@ -197,13 +208,13 @@ class JudgeDispatcher(object):
oi_problems_status[problem_id] = score
else:
# minus last time score, add this time score
user_profile.add_score(score, oi_problems_status[problem_id])
user_profile.add_score(this_time_score=score, last_time_score=oi_problems_status[problem_id])
oi_problems_status[problem_id] = score
user_profile.oi_problems_status[key] = oi_problems_status
problem.save(update_fields=["submission_number", "accepted_number", "statistic_info"])
user_profile.save(
update_fields=["submission_number", "accepted_number", "acm_problems_status", "oi_problems_status"])
user_profile.save(update_fields=[
"submission_number", "accepted_number", "acm_problems_status", "oi_problems_status"])
def update_contest_rank(self):
if self.contest.real_time_rank:
@ -221,7 +232,7 @@ class JudgeDispatcher(object):
def _update_acm_contest_rank(self, rank):
info = rank.submission_info.get(str(self.submission.problem_id))
# 因前面更改过,这里需要重新获取
problem = ContestProblem.objects.get(contest_id=self.contest.id, _id=self.problem._id)
problem = Problem.objects.get(contest_id=self.contest_id, _id=self.problem.id)
# 此题提交过
if info:
if info["is_ac"]:
@ -258,4 +269,10 @@ class JudgeDispatcher(object):
rank.save()
def _update_oi_contest_rank(self, rank):
pass
problem_id = str(self.submission.problem_id)
current_score = self.submission.statistic_info["score"]
last_score = rank.submission_info.get(problem_id)
if last_score:
rank.total_score = rank.total_score - last_score + current_score
rank.submission_info[problem_id] = current_score
rank.save()

View File

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-23 13:18
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contest', '0005_auto_20170823_0918'),
('problem', '0006_auto_20170823_0918'),
]
operations = [
migrations.AddField(
model_name='contestproblem',
name='total_score',
field=models.IntegerField(blank=True, default=0),
),
migrations.AddField(
model_name='problem',
name='total_score',
field=models.IntegerField(blank=True, default=0),
),
migrations.AlterUniqueTogether(
name='contestproblem',
unique_together=set([]),
),
migrations.RemoveField(
model_name='contestproblem',
name='contest',
),
migrations.RemoveField(
model_name='contestproblem',
name='created_by',
),
migrations.RemoveField(
model_name='contestproblem',
name='tags',
),
migrations.AddField(
model_name='problem',
name='contest',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contest.Contest'),
preserve_default=False,
),
migrations.AddField(
model_name='problem',
name='is_public',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='problem',
name='_id',
field=models.CharField(db_index=True, max_length=24),
),
migrations.AlterUniqueTogether(
name='problem',
unique_together=set([('_id', 'contest')]),
),
migrations.DeleteModel(
name='ContestProblem',
),
]

View File

@ -24,7 +24,12 @@ class ProblemDifficulty(object):
Low = "Low"
class AbstractProblem(models.Model):
class Problem(models.Model):
# display ID
_id = models.CharField(max_length=24, db_index=True)
contest = models.ForeignKey(Contest, null=True, blank=True)
# for contest problem
is_public = models.BooleanField(default=False)
title = models.CharField(max_length=128)
# HTML
description = RichTextField()
@ -33,6 +38,7 @@ class AbstractProblem(models.Model):
# [{input: "test", output: "123"}, {input: "test123", output: "456"}]
samples = JSONField()
test_case_id = models.CharField(max_length=32)
# [{"input_name": "1.in", "output_name": "1.out", "score": 0}]
test_case_score = JSONField()
hint = RichTextField(blank=True, null=True)
languages = JSONField()
@ -55,6 +61,8 @@ class AbstractProblem(models.Model):
difficulty = models.CharField(max_length=32)
tags = models.ManyToManyField(ProblemTag)
source = models.CharField(max_length=200, blank=True, null=True)
# for OI mode
total_score = models.IntegerField(default=0, blank=True)
submission_number = models.BigIntegerField(default=0)
accepted_number = models.BigIntegerField(default=0)
# ACM rule_type: {JudgeStatus.ACCEPTED: 3, JudgeStaus.WRONG_ANSWER: 11}, the number means count
@ -62,7 +70,7 @@ class AbstractProblem(models.Model):
class Meta:
db_table = "problem"
abstract = True
unique_together = (("_id", "contest"),)
def add_submission_number(self):
self.submission_number = models.F("submission_number") + 1
@ -71,18 +79,3 @@ class AbstractProblem(models.Model):
def add_ac_number(self):
self.accepted_number = models.F("accepted_number") + 1
self.save(update_fields=["accepted_number"])
class Problem(AbstractProblem):
_id = models.CharField(max_length=24, unique=True, db_index=True)
class ContestProblem(AbstractProblem):
_id = models.CharField(max_length=24, db_index=True)
contest = models.ForeignKey(Contest)
# 是否已经公开了题目,防止重复公开
is_public = models.BooleanField(default=False)
class Meta:
db_table = "contest_problem"
unique_together = (("_id", "contest"),)

View File

@ -4,7 +4,6 @@ from judge.languages import language_names, spj_language_names
from utils.api import DateTimeTZField, UsernameSerializer, serializers
from .models import Problem, ProblemRuleType, ProblemTag
from .models import ContestProblem
class TestCaseUploadForm(forms.Form):
@ -93,16 +92,16 @@ class ProblemAdminSerializer(BaseProblemSerializer):
class ContestProblemAdminSerializer(BaseProblemSerializer):
class Meta:
model = ContestProblem
model = Problem
class ProblemSerializer(BaseProblemSerializer):
class Meta:
model = Problem
exclude = ("test_case_score", "test_case_id", "visible")
exclude = ("contest", "test_case_score", "test_case_id", "visible", "is_public")
class ContestProblemSerializer(BaseProblemSerializer):
class Meta:
model = ContestProblem
model = Problem
exclude = ("test_case_score", "test_case_id", "visible", "is_public")

View File

@ -10,11 +10,10 @@ from contest.models import Contest
from utils.api import APIView, CSRFExemptAPIView, validate_serializer
from utils.shortcuts import rand_str
from ..models import ContestProblem, Problem, ProblemRuleType, ProblemTag
from ..serializers import (CreateContestProblemSerializer,
from ..models import Problem, ProblemRuleType, ProblemTag
from ..serializers import (CreateContestProblemSerializer, ContestProblemAdminSerializer,
CreateProblemSerializer, EditProblemSerializer,
ProblemAdminSerializer, TestCaseUploadForm,
ContestProblemAdminSerializer)
ProblemAdminSerializer, TestCaseUploadForm)
class TestCaseUploadAPI(CSRFExemptAPIView):
@ -134,9 +133,13 @@ class ProblemAPI(APIView):
data["spj_language"] = None
data["spj_code"] = None
if data["rule_type"] == ProblemRuleType.OI:
total_score = 0
for item in data["test_case_score"]:
if item["score"] <= 0:
return self.error("Invalid score")
else:
total_score += item["score"]
data["total_score"] = total_score
# todo check filename and score info
data["created_by"] = request.user
tags = data.pop("tags")
@ -211,9 +214,13 @@ class ProblemAPI(APIView):
data["spj_code"] = None
if data["rule_type"] == ProblemRuleType.OI:
total_score = 0
for item in data["test_case_score"]:
if item["score"] <= 0:
return self.error("Invalid score")
else:
total_score += item["score"]
data["total_score"] = total_score
# todo check filename and score info
tags = data.pop("tags")
@ -250,11 +257,9 @@ class ContestProblemAPI(APIView):
_id = data["_id"]
if not _id:
return self.error("Display id is required for contest problem")
try:
ContestProblem.objects.get(_id=_id, contest=contest)
if Problem.objects.filter(_id=_id, contest=contest).exists():
return self.error("Duplicate Display id")
except ContestProblem.DoesNotExist:
pass
if data["spj"]:
if not data["spj_language"] or not data["spj_code"]:
@ -275,7 +280,7 @@ class ContestProblemAPI(APIView):
tags = data.pop("tags")
data["languages"] = list(data["languages"])
problem = ContestProblem.objects.create(**data)
problem = Problem.objects.create(**data)
for item in tags:
try:
@ -291,17 +296,17 @@ class ContestProblemAPI(APIView):
user = request.user
if problem_id:
try:
problem = ContestProblem.objects.get(id=problem_id)
problem = Problem.objects.get(id=problem_id)
if user.is_admin() and problem.contest.created_by != user:
return self.error("Problem does not exist")
except ContestProblem.DoesNotExist:
except Problem.DoesNotExist:
return self.error("Problem does not exist")
return self.success(ProblemAdminSerializer(problem).data)
if not contest_id:
return self.error("Contest id is required")
problems = ContestProblem.objects.filter(contest_id=contest_id).order_by("-create_time")
problems = Problem.objects.filter(contest_id=contest_id).order_by("-create_time")
if user.is_admin():
problems = problems.filter(contest__created_by=user)
keyword = request.GET.get("keyword")

View File

@ -1,7 +1,7 @@
from django.db.models import Q
from utils.api import APIView
from account.decorators import check_contest_permission
from ..models import ProblemTag, Problem, ContestProblem, ProblemRuleType
from ..models import ProblemTag, Problem, ProblemRuleType
from ..serializers import ProblemSerializer, TagSerializer
from ..serializers import ContestProblemSerializer
from contest.models import ContestRuleType
@ -66,14 +66,14 @@ class ContestProblemAPI(APIView):
problem_id = request.GET.get("problem_id")
if problem_id:
try:
problem = ContestProblem.objects.select_related("created_by").get(_id=problem_id, contest=self.contest,
problem = Problem.objects.select_related("created_by").get(_id=problem_id,
contest=self.contest,
visible=True)
except ContestProblem.DoesNotExist:
except Problem.DoesNotExist:
return self.error("Problem does not exist.")
return self.success(ContestProblemSerializer(problem).data)
contest_problems = ContestProblem.objects.select_related("created_by").filter(contest=self.contest,
visible=True)
contest_problems = Problem.objects.select_related("created_by").filter(contest=self.contest, visible=True)
# 根据profile 为做过的题目添加标记
data = ContestProblemSerializer(contest_problems, many=True).data
if request.user.id:

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-23 13:18
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('submission', '0006_auto_20170830_1154'),
]
operations = [
migrations.AlterField(
model_name='submission',
name='contest_id',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='contest.Contest'),
),
migrations.AlterField(
model_name='submission',
name='problem_id',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='problem.Problem'),
),
migrations.RenameField(
model_name='submission',
old_name='contest_id',
new_name='contest',
),
migrations.RenameField(
model_name='submission',
old_name='problem_id',
new_name='problem',
),
]

View File

@ -1,6 +1,8 @@
from django.db import models
from jsonfield import JSONField
from account.models import AdminType
from problem.models import Problem
from contest.models import Contest
from utils.shortcuts import rand_str
@ -21,8 +23,8 @@ class JudgeStatus:
class Submission(models.Model):
id = models.CharField(max_length=32, default=rand_str, primary_key=True, db_index=True)
contest_id = models.IntegerField(db_index=True, null=True)
problem_id = models.IntegerField(db_index=True)
contest = models.ForeignKey(Contest, null=True)
problem = models.ForeignKey(Problem)
create_time = models.DateTimeField(auto_now_add=True)
user_id = models.IntegerField(db_index=True)
username = models.CharField(max_length=30)

View File

@ -18,13 +18,13 @@ class SubmissionModelSerializer(serializers.ModelSerializer):
model = Submission
# 不显示submission info详情的serializer
# 不显示submission info的serializer, 用于ACM rule_type
class SubmissionSafeSerializer(serializers.ModelSerializer):
statistic_info = serializers.JSONField()
class Meta:
model = Submission
exclude = ("info", "contest_id")
exclude = ("info", "contest")
class SubmissionListSerializer(serializers.ModelSerializer):
@ -37,7 +37,7 @@ class SubmissionListSerializer(serializers.ModelSerializer):
class Meta:
model = Submission
exclude = ("info", "contest_id", "code")
exclude = ("info", "contest", "code")
def get_show_link(self, obj):
# 没传user或为匿名user

View File

57
submission/tests.py Normal file
View File

@ -0,0 +1,57 @@
from unittest import mock
from copy import deepcopy
from .models import Submission
from problem.models import Problem, ProblemTag
from utils.api.tests import APITestCase
DEFAULT_PROBLEM_DATA = {"_id": "110", "title": "test", "description": "<p>test</p>", "input_description": "test",
"output_description": "test", "time_limit": 1000, "memory_limit": 256, "difficulty": "Low",
"visible": True, "tags": ["test"], "languages": ["C", "C++", "Java", "Python2"], "template": {},
"samples": [{"input": "test", "output": "test"}], "spj": False, "spj_language": "C",
"spj_code": "", "test_case_id": "499b26290cc7994e0b497212e842ea85",
"test_case_score": [{"output_name": "1.out", "input_name": "1.in", "output_size": 0,
"stripped_output_md5": "d41d8cd98f00b204e9800998ecf8427e",
"input_size": 0, "score": 0}],
"rule_type": "ACM", "hint": "<p>test</p>", "source": "test"}
DEFAULT_SUBMISSION_DATA = {
"problem_id": "110",
"user_id": 1,
"username": "test",
"code": "xxxxxxxxxxxxxx",
"result": -2,
"info": {},
"language": "C",
"statistic_info": {}
}
class SubmissionListTest(APITestCase):
def setUp(self):
self.create_user("123", "345")
self.url = self.reverse("submission_list_api")
Submission.objects.create(**DEFAULT_SUBMISSION_DATA)
def test_get_submission_list(self):
resp = self.client.get(self.url, data={"limit": "10"})
self.assertSuccess(resp)
@mock.patch("submission.views.oj.judge_task.delay")
class SubmissionAPITest(APITestCase):
def setUp(self):
self.user = self.create_user("test", "test123")
tag = ProblemTag.objects.create(name="test")
problem_data = deepcopy(DEFAULT_PROBLEM_DATA)
problem_data.pop("tags")
problem_data["created_by"] = self.user
self.problem = Problem.objects.create(**problem_data)
self.problem.tags.add(tag)
self.problem.save()
self.url = self.reverse("submission_api")
def test_create_submission(self, judge_task):
resp = self.client.post(self.url, DEFAULT_SUBMISSION_DATA)
self.assertSuccess(resp)
judge_task.assert_called()

View File

@ -1,8 +1,7 @@
from account.decorators import login_required, check_contest_permission
from judge.tasks import judge_task
# from judge.dispatcher import JudgeDispatcher
from problem.models import Problem, ProblemRuleType, ContestProblem
from problem.models import Problem, ProblemRuleType
from contest.models import Contest, ContestStatus
from utils.api import APIView, validate_serializer
from utils.throttling import TokenBucket, BucketController
@ -20,16 +19,15 @@ def _submit(response, user, problem_id, language, code, contest_id):
bucket = TokenBucket(fill_rate=10, capacity=20,
last_capacity=controller.last_capacity,
last_timestamp=controller.last_timestamp)
if bucket.consume():
controller.last_capacity -= 1
else:
return response.error("Please wait %d seconds" % int(bucket.expected_time() + 1))
try:
if contest_id:
problem = ContestProblem.objects.get(_id=problem_id, visible=True)
else:
problem = Problem.objects.get(_id=problem_id, visible=True)
problem = Problem.objects.get(_id=problem_id,
contest_id=contest_id,
visible=True)
except Problem.DoesNotExist:
return response.error("Problem not exist")
@ -37,11 +35,11 @@ def _submit(response, user, problem_id, language, code, contest_id):
username=user.username,
language=language,
code=code,
problem_id=problem._id,
problem_id=problem.id,
contest_id=contest_id)
# use this for debug
# JudgeDispatcher(submission.id, problem._id).judge()
judge_task.delay(submission.id, problem._id)
# JudgeDispatcher(submission.id, problem.id).judge()
judge_task.delay(submission.id, problem.id)
return response.success({"submission_id": submission.id})
@ -65,32 +63,21 @@ class SubmissionAPI(APIView):
if not submission_id:
return self.error("Parameter id doesn't exist.")
try:
submission = Submission.objects.get(id=submission_id)
submission = Submission.objects.select_related("problem").get(id=submission_id)
except Submission.DoesNotExist:
return self.error("Submission doesn't exist.")
if not submission.check_user_permission(request.user):
return self.error("No permission for this submission.")
if submission.contest_id:
# check problem'rule is ACM or IO.
if ContestProblem.objects.filter(contest_id=submission.contest_id,
_id=submission.problem_id,
visible=True,
rule_type=ProblemRuleType.ACM
).exists():
return self.success(SubmissionSafeSerializer(submission).data)
return self.success(SubmissionModelSerializer(submission).data)
if Problem.objects.filter(_id=submission.problem_id,
visible=True,
rule_type=ProblemRuleType.ACM
).exists():
if submission.problem.rule_type == ProblemRuleType.ACM:
return self.success(SubmissionSafeSerializer(submission).data)
return self.success(SubmissionModelSerializer(submission).data)
class SubmissionListAPI(APIView):
def get(self, request):
if not request.GET.get("limit"):
return self.error("Limit is needed")
if request.GET.get("contest_id"):
return self._get_contest_submission_list(request)
@ -107,7 +94,11 @@ class SubmissionListAPI(APIView):
myself = request.GET.get("myself")
result = request.GET.get("result")
if problem_id:
submissions = submissions.filter(problem_id=problem_id)
try:
problem = Problem.objects.get(_id=problem_id, visible=True)
except Problem.DoesNotExist:
return self.error("Problem doesn't exist")
submissions = problem.submission_set.all()
if myself and myself == "1":
submissions = submissions.filter(user_id=request.user.id)
if result: