diff --git a/account/models.py b/account/models.py index 8802c29a..3563dd75 100644 --- a/account/models.py +++ b/account/models.py @@ -92,9 +92,5 @@ class UserProfile(models.Model): self.submission_number = models.F("submission_number") + 1 self.save() - def minus_accepted_problem_number(self): - self.accepted_problem_number = models.F("accepted_problem_number") - 1 - self.save() - class Meta: db_table = "user_profile" diff --git a/account/urls/user.py b/account/urls/user.py index 47a601b7..8fdb7db3 100644 --- a/account/urls/user.py +++ b/account/urls/user.py @@ -1,7 +1,7 @@ from django.conf.urls import url from ..views.user import (SSOAPI, AvatarUploadAPI, TwoFactorAuthAPI, - UserNameAPI, UserProfileAPI) + UserProfileAPI) urlpatterns = [ # url(r"^username/?$", UserNameAPI.as_view(), name="user_name_api"), diff --git a/contest/migrations/0004_auto_20170717_1324.py b/contest/migrations/0004_auto_20170717_1324.py index 6a7aa092..617790ab 100644 --- a/contest/migrations/0004_auto_20170717_1324.py +++ b/contest/migrations/0004_auto_20170717_1324.py @@ -14,6 +14,10 @@ class Migration(migrations.Migration): operations = [ migrations.AlterModelOptions( name='contest', - options={'ordering': ('create_time',)}, + options={'ordering': ('-create_time',)}, + ), + migrations.AlterModelOptions( + name='contestannouncement', + options={'ordering': ('-create_time',)}, ), ] diff --git a/contest/models.py b/contest/models.py index dc52f476..ca991423 100644 --- a/contest/models.py +++ b/contest/models.py @@ -58,7 +58,7 @@ class Contest(models.Model): class Meta: db_table = "contest" - ordering = ("create_time",) + ordering = ("-create_time",) class ContestRank(models.Model): @@ -91,6 +91,9 @@ class OIContestRank(ContestRank): class Meta: db_table = "oi_contest_rank" + def update_rank(self, submission): + self.total_submission_number += 1 + class ContestAnnouncement(models.Model): contest = models.ForeignKey(Contest) @@ -101,3 +104,4 @@ class ContestAnnouncement(models.Model): class Meta: db_table = "contest_announcement" + ordering = ("-create_time",) diff --git a/contest/serializers.py b/contest/serializers.py index 8fb4beee..356a150c 100644 --- a/contest/serializers.py +++ b/contest/serializers.py @@ -1,6 +1,7 @@ from utils.api import DateTimeTZField, UsernameSerializer, serializers from .models import Contest, ContestAnnouncement, ContestRuleType +from .models import ACMContestRank, OIContestRank class CreateConetestSeriaizer(serializers.Serializer): @@ -61,3 +62,19 @@ class CreateContestAnnouncementSerializer(serializers.Serializer): class ContestPasswordVerifySerializer(serializers.Serializer): contest_id = serializers.IntegerField() password = serializers.CharField(max_length=30, required=True) + + +class ACMContestRankSerializer(serializers.ModelSerializer): + user = UsernameSerializer() + submission_info = serializers.JSONField() + + class Meta: + model = ACMContestRank + + +class OIContestRankSerializer(serializers.ModelSerializer): + user = UsernameSerializer() + submission_info = serializers.JSONField() + + class Meta: + model = OIContestRank diff --git a/contest/views/oj.py b/contest/views/oj.py index 74653f05..5baef2e7 100644 --- a/contest/views/oj.py +++ b/contest/views/oj.py @@ -1,9 +1,14 @@ +from django.utils.timezone import now +from django.db.models import Q +from django.core.cache import cache from utils.api import APIView, validate_serializer -from account.decorators import login_required +from account.decorators import login_required, check_contest_permission -from ..models import ContestAnnouncement, Contest, ContestStatus +from ..models import ContestAnnouncement, Contest, ContestStatus, ContestRuleType +from ..models import OIContestRank, ACMContestRank from ..serializers import ContestAnnouncementSerializer from ..serializers import ContestSerializer, ContestPasswordVerifySerializer +from ..serializers import OIContestRankSerializer, ACMContestRankSerializer class ContestAnnouncementListAPI(APIView): @@ -11,7 +16,7 @@ class ContestAnnouncementListAPI(APIView): contest_id = request.GET.get("contest_id") if not contest_id: return self.error("Invalid parameter") - data = ContestAnnouncement.objects.filter(contest_id=contest_id).order_by("-create_time") + data = ContestAnnouncement.objects.filter(contest_id=contest_id) max_id = request.GET.get("max_id") if max_id: data = data.filter(id__gt=max_id) @@ -30,8 +35,20 @@ class ContestAPI(APIView): contests = Contest.objects.filter(visible=True) keyword = request.GET.get("keyword") + rule_type = request.GET.get("rule_type") + status = request.GET.get("status") if keyword: contests = contests.filter(title__contains=keyword) + if rule_type: + contests = contests.filter(rule_type=rule_type) + if status: + cur = now() + if status == ContestStatus.CONTEST_NOT_START: + contests = contests.filter(start_time__gt=cur) + elif status == ContestStatus.CONTEST_ENDED: + contests = contests.filter(end_time__lt=cur) + else: + contests = contests.filter(Q(start_time__lte=cur) & Q(end_time__gte=cur)) return self.success(self.paginate_data(request, contests, ContestSerializer)) @@ -68,3 +85,24 @@ class ContestAccessAPI(APIView): return self.success({"Access": True}) else: return self.success({"Access": False}) + + +class ContestRankAPI(APIView): + def get_rank(self): + if self.contest.contest_type == ContestRuleType.ACM: + rank = ACMContestRank.objects.filter(contest=self.contest). \ + select_related("user").order_by("-total_ac_number", "total_time") + return ACMContestRankSerializer(rank, many=True).data + else: + rank = OIContestRank.objects.filter(contest=self.contest). \ + select_related("user").order_by("-total_score") + return OIContestRankSerializer(rank, many=True).data + + @check_contest_permission + def get(self, request): + cache_key = str(self.contest.id) + "_rank_cache" + rank = cache.get(cache_key) + if not rank: + rank = self.get_rank() + cache.set(cache_key, rank) + return self.success(rank) diff --git a/judge/dispatcher.py b/judge/dispatcher.py index 53251539..7ad1dc45 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -7,11 +7,13 @@ from urllib.parse import urljoin from django.db import transaction from django.db.models import F from django_redis import get_redis_connection +from django.core.cache import cache from judge.languages import languages from account.models import User from conf.models import JudgeServer, JudgeServerToken -from problem.models import Problem, ProblemRuleType +from problem.models import Problem, ProblemRuleType, ContestProblem +from contest.models import ContestRuleType, ACMContestRank, OIContestRank from submission.models import JudgeStatus, Submission logger = logging.getLogger(__name__) @@ -33,8 +35,13 @@ class JudgeDispatcher(object): token = JudgeServerToken.objects.first().token self.token = hashlib.sha256(token.encode("utf-8")).hexdigest() self.redis_conn = get_redis_connection("JudgeQueue") - self.submission_obj = Submission.objects.get(pk=submission_id) - self.problem_obj = Problem.objects.get(pk=problem_id) + 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 = self.problem.contest + else: + self.problem = Problem.objects.get(pk=problem_id) def _request(self, url, data=None): kwargs = {"headers": {"X-Judge-Server-Token": self.token, @@ -69,59 +76,60 @@ class JudgeDispatcher(object): def judge(self, output=False): server = self.choose_judge_server() if not server: - data = {"submission_id": self.submission_obj.id, "problem_id": self.problem_obj.id} + data = {"submission_id": self.submission.id, "problem_id": self.problem.id} self.redis_conn.lpush(WAITING_QUEUE, json.dumps(data)) return - sub_config = list(filter(lambda item: self.submission_obj.language == item["name"], languages))[0] + sub_config = list(filter(lambda item: self.submission.language == item["name"], languages))[0] spj_config = {} - if self.problem_obj.spj_code: + if self.problem.spj_code: for lang in languages: - if lang["name"] == self.problem_obj.spj_language: + if lang["name"] == self.problem.spj_language: spj_config = lang["spj"] break data = { "language_config": sub_config["config"], - "src": self.submission_obj.code, - "max_cpu_time": self.problem_obj.time_limit, - "max_memory": 1024 * 1024 * self.problem_obj.memory_limit, - "test_case_id": self.problem_obj.test_case_id, + "src": self.submission.code, + "max_cpu_time": self.problem.time_limit, + "max_memory": 1024 * 1024 * self.problem.memory_limit, + "test_case_id": self.problem.test_case_id, "output": output, - "spj_version": self.problem_obj.spj_version, + "spj_version": self.problem.spj_version, "spj_config": spj_config.get("config"), "spj_compile_config": spj_config.get("compile"), - "spj_src": self.problem_obj.spj_code + "spj_src": self.problem.spj_code } - self.submission_obj.result = JudgeStatus.JUDGING - self.submission_obj.save() + self.submission.result = JudgeStatus.JUDGING + self.submission.save() # TODO: try catch resp = self._request(urljoin(server.service_url, "/judge"), data=data) - self.submission_obj.info = resp + self.submission.info = resp if resp["err"]: - self.submission_obj.result = JudgeStatus.COMPILE_ERROR - self.submission_obj.statistic_info["err_info"] = resp["data"] + self.submission.result = JudgeStatus.COMPILE_ERROR + self.submission.statistic_info["err_info"] = resp["data"] else: # 用时和内存占用保存为多个测试点中最长的那个 - self.submission_obj.statistic_info["time_cost"] = max([x["cpu_time"] for x in resp["data"]]) - self.submission_obj.statistic_info["memory_cost"] = max([x["memory"] for x in 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"]]) error_test_case = list(filter(lambda case: case["result"] != 0, resp["data"])) # 多个测试点全部正确则AC,否则 ACM模式下取第一个错误的测试点的状态, OI模式若全部错误则取第一个错误测试点状态,否则为部分正确 if not error_test_case: - self.submission_obj.result = JudgeStatus.ACCEPTED - elif self.problem_obj.rule_type == ProblemRuleType.ACM or len(error_test_case) == len(resp["data"]): - self.submission_obj.result = error_test_case[0]["result"] + self.submission.result = JudgeStatus.ACCEPTED + elif self.problem.rule_type == ProblemRuleType.ACM or len(error_test_case) == len(resp["data"]): + self.submission.result = error_test_case[0]["result"] else: - self.submission_obj.result = JudgeStatus.PARTIALLY_ACCEPTED - self.submission_obj.save() + self.submission.result = JudgeStatus.PARTIALLY_ACCEPTED + self.submission.save() self.release_judge_res(server.id) - if self.submission_obj.contest_id: - # ToDo: update contest status - pass + if self.submission.contest_id: + self.update_contest_problem_status() + self.update_contest_rank() else: self.update_problem_status() + # 至此判题结束,尝试处理任务队列中剩余的任务 process_pending_task(self.redis_conn) def compile_spj(self, service_url, src, spj_version, spj_compile_config, test_case_id): @@ -132,26 +140,88 @@ class JudgeDispatcher(object): def update_problem_status(self): with transaction.atomic(): - problem = Problem.objects.select_for_update().get(id=self.problem_obj.id) - user = User.objects.select_for_update().get(id=self.submission_obj.user_id) - # 更新提交计数器 - problem.add_submission_number() + # 更新problem计数器 + self.problem = Problem.objects.select_for_update().get(id=self.problem.id) + self.problem.add_submission_number() + if self.submission.result == JudgeStatus.ACCEPTED: + self.problem.add_ac_number() + + # 更新user profile + user = User.objects.select_for_update().get(id=self.submission.user_id) user_profile = user.userprofile user_profile.add_submission_number() - - if self.submission_obj.result == JudgeStatus.ACCEPTED: - problem.add_ac_number() - problems_status = user_profile.problems_status if "problems" not in problems_status: problems_status["problems"] = {} # 之前状态不是ac, 现在是ac了 需要更新用户ac题目数量计数器,这里需要判重 - if problems_status["problems"].get(str(problem.id), JudgeStatus.WRONG_ANSWER) != JudgeStatus.ACCEPTED: - if self.submission_obj.result == JudgeStatus.ACCEPTED: + if problems_status["problems"].get(str(self.problem.id), JudgeStatus.WRONG_ANSWER) != JudgeStatus.ACCEPTED: + if self.submission.result == JudgeStatus.ACCEPTED: user_profile.add_accepted_problem_number() - problems_status["problems"][str(problem.id)] = JudgeStatus.ACCEPTED + problems_status["problems"][str(self.problem.id)] = JudgeStatus.ACCEPTED else: - problems_status["problems"][str(problem.id)] = JudgeStatus.WRONG_ANSWER + problems_status["problems"][str(self.problem.id)] = JudgeStatus.WRONG_ANSWER user_profile.problems_status = problems_status user_profile.save(update_fields=["problems_status"]) + + def update_contest_problem_status(self): + with transaction.atomic(): + problem = ContestProblem.objects.select_for_update().get(id=self.problem.id) + problem.add_submission_number() + if self.submission.result == JudgeStatus.ACCEPTED: + problem.add_ac_number() + + def update_contest_rank(self): + if self.contest.real_time_rank: + cache.delete(str(self.contest.id) + "_rank_cache") + with transaction.atomic(): + if self.contest.rule_type == ContestRuleType.ACM: + acm_rank, _ = ACMContestRank.objects.select_for_update(). \ + get_or_create(user_id=self.submission.user_id, contest=self.contest) + self._update_acm_contest_rank(acm_rank) + else: + oi_rank, _ = OIContestRank.objects.select_for_update(). \ + get_or_create(user_id=self.submission.user_id, contest=self.contest) + self._update_oi_contest_rank(oi_rank) + + 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) + # 此题提交过 + if info: + if info["is_ac"]: + return + + rank.total_submission_number += 1 + if self.submission.result == JudgeStatus.ACCEPTED: + rank.total_ac_number += 1 + info["is_ac"] = True + info["ac_time"] = (self.submission.create_time - self.contest.start_time).total_seconds() + rank.total_time += info["ac_time"] + info["error_number"] * 20 * 60 + + if problem.total_accepted_number == 1: + info["is_first_ac"] = True + else: + info["error_number"] += 1 + + # 第一次提交 + else: + rank.total_submission_number += 1 + info = {"is_ac": False, "ac_time": 0, "error_number": 0, "is_first_ac": False} + if self.submission.result == JudgeStatus.ACCEPTED: + rank.total_ac_number += 1 + info["is_ac"] = True + info["ac_time"] = (self.submission.create_time - self.contest.start_time).total_seconds() + rank.total_time += info["ac_time"] + + if problem.total_accepted_number == 1: + info["is_first_ac"] = True + + else: + info["error_number"] = 1 + rank.submission_info[str(self.submission.problem_id)] = info + rank.save() + + def _update_oi_contest_rank(self, rank): + pass diff --git a/submission/migrations/0004_auto_20170717_1324.py b/submission/migrations/0004_auto_20170717_1324.py index ca355ae6..c8a5be33 100644 --- a/submission/migrations/0004_auto_20170717_1324.py +++ b/submission/migrations/0004_auto_20170717_1324.py @@ -12,8 +12,13 @@ class Migration(migrations.Migration): ] operations = [ + migrations.RenameField( + model_name='submission', + old_name='created_time', + new_name='create_time', + ), migrations.AlterModelOptions( name='submission', - options={'ordering': ('-created_time',)}, - ), + options={'ordering': ('-create_time',)}, + ) ] diff --git a/submission/models.py b/submission/models.py index 7e8c06f7..65d242b6 100644 --- a/submission/models.py +++ b/submission/models.py @@ -23,7 +23,7 @@ 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) - created_time = models.DateTimeField(auto_now_add=True) + create_time = models.DateTimeField(auto_now_add=True) user_id = models.IntegerField(db_index=True) code = models.TextField() result = models.IntegerField(default=JudgeStatus.PENDING) @@ -42,7 +42,7 @@ class Submission(models.Model): class Meta: db_table = "submission" - ordering = ("-created_time",) + ordering = ("-create_time",) def __str__(self): return self.id diff --git a/submission/serializers.py b/submission/serializers.py index bc330364..815d2757 100644 --- a/submission/serializers.py +++ b/submission/serializers.py @@ -8,6 +8,7 @@ class CreateSubmissionSerializer(serializers.Serializer): problem_id = serializers.IntegerField() language = serializers.ChoiceField(choices=language_names) code = serializers.CharField(max_length=20000) + contest_id = serializers.IntegerField(required=False) class SubmissionModelSerializer(serializers.ModelSerializer): @@ -32,7 +33,7 @@ class SubmissionSafeSerializer(serializers.ModelSerializer): return User.objects.get(id=obj.user_id).username -class SubmissionListSerializer(SubmissionSafeSerializer): +class SubmissionListSerializer(serializers.ModelSerializer): username = serializers.SerializerMethodField() statistic_info = serializers.JSONField() show_link = serializers.SerializerMethodField() diff --git a/submission/urls/oj.py b/submission/urls/oj.py index e569574d..d86bfa59 100644 --- a/submission/urls/oj.py +++ b/submission/urls/oj.py @@ -1,8 +1,9 @@ from django.conf.urls import url -from ..views.oj import SubmissionAPI, SubmissionListAPI +from ..views.oj import SubmissionAPI, SubmissionListAPI, ContestSubmissionListAPI urlpatterns = [ url(r"^submission/?$", SubmissionAPI.as_view(), name="submission_api"), url(r"^submissions/?$", SubmissionListAPI.as_view(), name="submission_list_api"), + url(r"^contest/submissions/?$", ContestSubmissionListAPI.as_view(), name="contest_submission_list_api"), ] diff --git a/submission/views/oj.py b/submission/views/oj.py index b9a90070..0fe8819d 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -1,17 +1,19 @@ from django_redis import get_redis_connection -from account.decorators import login_required -from problem.models import Problem, ProblemRuleType +from account.decorators import login_required, check_contest_permission +from problem.models import Problem, ProblemRuleType, ContestProblem from submission.tasks import judge_task # from judge.dispatcher import JudgeDispatcher -from utils.api import APIView, validate_serializer -from utils.throttling import TokenBucket, BucketController + from ..models import Submission from ..serializers import CreateSubmissionSerializer, SubmissionModelSerializer from ..serializers import SubmissionSafeSerializer, SubmissionListSerializer +from utils.api import APIView, validate_serializer +from utils.throttling import TokenBucket, BucketController -def _submit(response, user, problem_id, language, code, contest_id=None): + +def _submit(response, user, problem_id, language, code, contest_id): # TODO: 预设默认值,需修改 controller = BucketController(user_id=user.id, redis_conn=get_redis_connection("Throttling"), @@ -24,9 +26,11 @@ def _submit(response, user, problem_id, language, code, contest_id=None): controller.last_capacity -= 1 else: return response.error("Please wait %d seconds" % int(bucket.expected_time() + 1)) - try: - problem = Problem.objects.get(_id=problem_id) + if contest_id: + problem = ContestProblem.objects.get(_id=problem_id, visible=True) + else: + problem = Problem.objects.get(_id=problem_id, visible=True) except Problem.DoesNotExist: return response.error("Problem not exist") @@ -35,9 +39,9 @@ def _submit(response, user, problem_id, language, code, contest_id=None): code=code, problem_id=problem._id, contest_id=contest_id) - # todo 暂时保留 方便排错 - # JudgeDispatcher(submission.id, problem.id).judge() - judge_task.delay(submission.id, problem.id) + # use this for debug + # JudgeDispatcher(submission.id, problem._id).judge() + judge_task.delay(submission.id, problem._id) return response.success({"submission_id": submission.id}) @@ -46,7 +50,7 @@ class SubmissionAPI(APIView): @login_required def post(self, request): data = request.data - return _submit(self, request.user, data["problem_id"], data["language"], data["code"]) + return _submit(self, request.user, data["problem_id"], data["language"], data["code"], data.get("contest_id")) @login_required def get(self, request): @@ -71,11 +75,7 @@ class SubmissionAPI(APIView): class SubmissionListAPI(APIView): def get(self, request): - contest_id = request.GET.get("contest_id") - if contest_id: - subs = Submission.objects.filter(contest_id=contest_id) - else: - subs = Submission.objects.filter(contest_id__isnull=True) + subs = Submission.objects.filter(contest_id__isnull=True) problem_id = request.GET.get("problem_id") if problem_id: @@ -86,3 +86,18 @@ class SubmissionListAPI(APIView): data = self.paginate_data(request, subs) data["results"] = SubmissionListSerializer(data["results"], many=True, user=request.user).data return self.success(data) + + +class ContestSubmissionListAPI(APIView): + @check_contest_permission + def get(self, request): + subs = Submission.objects.filter(contest_id=self.contest.id) + problem_id = request.GET.get("problem_id") + if problem_id: + subs = subs.filter(problem_id=problem_id) + + if request.GET.get("myself") and request.GET["myself"] == "1": + subs = subs.filter(user_id=request.user.id) + data = self.paginate_data(request, subs) + data["results"] = SubmissionListSerializer(data["results"], many=True, user=request.user).data + return self.success(data) diff --git a/utils/api/api.py b/utils/api/api.py index 47a81cd4..920b827a 100644 --- a/utils/api/api.py +++ b/utils/api/api.py @@ -130,7 +130,7 @@ class APIView(View): count = query_set.count() results = object_serializer(results, many=True).data else: - count = len(query_set) + count = query_set.count() data = {"results": results, "total": count} return data