From 727fbf48d84ce52ed64033dcfdf5edb398357280 Mon Sep 17 00:00:00 2001 From: zema1 Date: Fri, 10 Nov 2017 19:40:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0contest=20ip=E9=99=90?= =?UTF-8?q?=E5=88=B6api=EF=BC=9B=20OI=20problem=E7=9A=84AC=EF=BC=8Ctotal?= =?UTF-8?q?=20count=E4=B9=9F=E7=AE=97=E5=85=A5profile=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/decorators.py | 2 +- account/models.py | 3 +++ account/views/oj.py | 2 ++ .../0008_contest_allowed_ip_ranges.py | 21 +++++++++++++++ contest/models.py | 8 +++--- contest/serializers.py | 4 ++- contest/tests.py | 1 + contest/views/admin.py | 12 +++++++++ judge/dispatcher.py | 13 ++++++++-- submission/migrations/0008_submission_ip.py | 20 ++++++++++++++ submission/models.py | 1 + submission/serializers.py | 4 +-- submission/views/oj.py | 26 ++++++++++++------- 13 files changed, 96 insertions(+), 21 deletions(-) create mode 100644 contest/migrations/0008_contest_allowed_ip_ranges.py create mode 100644 submission/migrations/0008_submission_ip.py diff --git a/account/decorators.py b/account/decorators.py index 99d2a8ff..a8164d73 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -82,7 +82,7 @@ def check_contest_permission(check_type="details"): return self.error("Contest %s doesn't exist" % contest_id) # creator or owner - if self.contest.is_contest_admin(user): + if user.is_authenticated() and user.is_contest_admin(self.contest): return func(*args, **kwargs) if self.contest.contest_type == ContestType.PASSWORD_PROTECTED_CONTEST: diff --git a/account/models.py b/account/models.py index 4f991d7e..eb92f750 100644 --- a/account/models.py +++ b/account/models.py @@ -59,6 +59,9 @@ class User(AbstractBaseUser): def can_mgmt_all_problem(self): return self.problem_permission == ProblemPermission.ALL + def is_contest_admin(self, contest): + return self.is_authenticated() and (contest.created_by == self or self.admin_type == AdminType.SUPER_ADMIN) + class Meta: db_table = "user" diff --git a/account/views/oj.py b/account/views/oj.py index eccbb3f7..a4515533 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -277,6 +277,8 @@ class UserChangePasswordAPI(APIView): class ApplyResetPasswordAPI(APIView): @validate_serializer(ApplyResetPasswordSerializer) def post(self, request): + if request.user.is_authenticated(): + return self.error("You have already logged in, are you kidding me? ") data = request.data captcha = Captcha(request) if not captcha.check(data["captcha"]): diff --git a/contest/migrations/0008_contest_allowed_ip_ranges.py b/contest/migrations/0008_contest_allowed_ip_ranges.py new file mode 100644 index 00000000..fd6c6ff7 --- /dev/null +++ b/contest/migrations/0008_contest_allowed_ip_ranges.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-10 06:57 +from __future__ import unicode_literals + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('contest', '0007_contestannouncement_visible'), + ] + + operations = [ + migrations.AddField( + model_name='contest', + name='allowed_ip_ranges', + field=django.contrib.postgres.fields.jsonb.JSONField(default=list), + ), + ] diff --git a/contest/models.py b/contest/models.py index aab9e7e1..2f07b37e 100644 --- a/contest/models.py +++ b/contest/models.py @@ -4,7 +4,7 @@ from django.utils.timezone import now from utils.models import JSONField from utils.constants import ContestStatus, ContestType -from account.models import User, AdminType +from account.models import User from utils.models import RichTextField @@ -23,6 +23,7 @@ class Contest(models.Model): created_by = models.ForeignKey(User) # 是否可见 false的话相当于删除 visible = models.BooleanField(default=True) + allowed_ip_ranges = JSONField(default=list) @property def status(self): @@ -42,14 +43,11 @@ class Contest(models.Model): return ContestType.PASSWORD_PROTECTED_CONTEST return ContestType.PUBLIC_CONTEST - def is_contest_admin(self, user): - return user.is_authenticated() and (self.created_by == user or user.admin_type == AdminType.SUPER_ADMIN) - # 是否有权查看problem 的一些统计信息 诸如submission_number, accepted_number 等 def problem_details_permission(self, user): return self.rule_type == ContestRuleType.ACM or \ self.status == ContestStatus.CONTEST_ENDED or \ - self.is_contest_admin(user) or \ + user.is_authenticated() and user.is_contest_admin(self) or \ self.real_time_rank class Meta: diff --git a/contest/serializers.py b/contest/serializers.py index ba8df287..abbdccca 100644 --- a/contest/serializers.py +++ b/contest/serializers.py @@ -13,6 +13,7 @@ class CreateConetestSeriaizer(serializers.Serializer): password = serializers.CharField(allow_blank=True, max_length=32) visible = serializers.BooleanField() real_time_rank = serializers.BooleanField() + allowed_ip_ranges = serializers.ListField(child=serializers.CharField(max_length=32), allow_empty=True) class EditConetestSeriaizer(serializers.Serializer): @@ -24,6 +25,7 @@ class EditConetestSeriaizer(serializers.Serializer): password = serializers.CharField(allow_blank=True, allow_null=True, max_length=32) visible = serializers.BooleanField() real_time_rank = serializers.BooleanField() + allowed_ip_ranges = serializers.ListField(child=serializers.CharField(max_length=32)) class ContestAdminSerializer(serializers.ModelSerializer): @@ -42,7 +44,7 @@ class ContestAdminSerializer(serializers.ModelSerializer): class ContestSerializer(ContestAdminSerializer): class Meta: model = Contest - exclude = ("password", "visible") + exclude = ("password", "visible", "allowed_ip_ranges") class ContestAnnouncementSerializer(serializers.ModelSerializer): diff --git a/contest/tests.py b/contest/tests.py index f8c5b73e..3b7b1e20 100644 --- a/contest/tests.py +++ b/contest/tests.py @@ -13,6 +13,7 @@ DEFAULT_CONTEST_DATA = {"title": "test title", "description": "test description" "end_time": timezone.localtime(timezone.now()) + timedelta(days=1), "rule_type": ContestRuleType.ACM, "password": "123", + "allowed_ip_ranges": [], "visible": True, "real_time_rank": True} diff --git a/contest/views/admin.py b/contest/views/admin.py index 6b53abd4..d5ce3478 100644 --- a/contest/views/admin.py +++ b/contest/views/admin.py @@ -1,3 +1,4 @@ +from ipaddress import ip_network import dateutil.parser from utils.api import APIView, validate_serializer @@ -21,6 +22,11 @@ class ContestAPI(APIView): return self.error("Start time must occur earlier than end time") if data.get("password") and data["password"] == "": data["password"] = None + for ip_range in data["allowed_ip_ranges"]: + try: + ip_network(ip_range, strict=False) + except ValueError: + return self.error(f"{ip_range} is not a valid cidr network") contest = Contest.objects.create(**data) return self.success(ContestAdminSerializer(contest).data) @@ -39,6 +45,12 @@ class ContestAPI(APIView): return self.error("Start time must occur earlier than end time") if not data["password"]: data["password"] = None + for ip_range in data["allowed_ip_ranges"]: + try: + ip_network(ip_range, strict=False) + except ValueError as e: + return self.error(f"{ip_range} is not a valid cidr network") + for k, v in data.items(): setattr(contest, k, v) contest.save() diff --git a/judge/dispatcher.py b/judge/dispatcher.py index 30b74443..a602bab4 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -174,8 +174,8 @@ class JudgeDispatcher(object): # update_userprofile user = User.objects.select_for_update().get(id=self.submission.user_id) user_profile = user.userprofile + user_profile.submission_number += 1 if problem.rule_type == ProblemRuleType.ACM: - user_profile.submission_number += 1 acm_problems_status = user_profile.acm_problems_status.get("problems", {}) if problem_id not in acm_problems_status: acm_problems_status[problem_id] = {"status": self.submission.result, "_id": self.problem._id} @@ -196,14 +196,23 @@ class JudgeDispatcher(object): oi_problems_status[problem_id] = {"status": self.submission.result, "_id": self.problem._id, "score": score} + if self.submission.result == JudgeStatus.ACCEPTED: + user_profile.accepted_number += 1 else: + if oi_problems_status[problem_id]["status"] == JudgeStatus.ACCEPTED and \ + self.submission.result != JudgeStatus.ACCEPTED: + user_profile.accepted_number -= 1 + elif oi_problems_status[problem_id]["status"] != JudgeStatus.ACCEPTED and \ + self.submission.result == JudgeStatus: + user_profile.accepted_number += 1 + # minus last time score, add this time score user_profile.add_score(this_time_score=score, last_time_score=oi_problems_status[problem_id]["score"]) oi_problems_status[problem_id]["score"] = score oi_problems_status[problem_id]["status"] = self.submission.result user_profile.oi_problems_status["problems"] = oi_problems_status - user_profile.save(update_fields=["oi_problems_status"]) + user_profile.save(update_fields=["submission_number", "accepted_number", "oi_problems_status"]) def update_contest_problem_status(self): if self.contest_id and self.contest.status != ContestStatus.CONTEST_UNDERWAY: diff --git a/submission/migrations/0008_submission_ip.py b/submission/migrations/0008_submission_ip.py new file mode 100644 index 00000000..e60841bd --- /dev/null +++ b/submission/migrations/0008_submission_ip.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-10 06:57 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submission', '0007_auto_20170923_1318'), + ] + + operations = [ + migrations.AddField( + model_name='submission', + name='ip', + field=models.CharField(blank=True, max_length=32, null=True), + ), + ] diff --git a/submission/models.py b/submission/models.py index fe6a9916..0c4aeda9 100644 --- a/submission/models.py +++ b/submission/models.py @@ -36,6 +36,7 @@ class Submission(models.Model): # 存储该提交所用时间和内存值,方便提交列表显示 # {time_cost: "", memory_cost: "", err_info: "", score: 0} statistic_info = JSONField(default=dict) + ip = models.CharField(max_length=32, null=True, blank=True) def check_user_permission(self, user, check_share=True): return self.user_id == user.id or \ diff --git a/submission/serializers.py b/submission/serializers.py index 21bd75db..2b67ab83 100644 --- a/submission/serializers.py +++ b/submission/serializers.py @@ -31,7 +31,7 @@ class SubmissionSafeModelSerializer(serializers.ModelSerializer): class Meta: model = Submission - exclude = ("info", "contest") + exclude = ("info", "contest", "ip") class SubmissionListSerializer(serializers.ModelSerializer): @@ -45,7 +45,7 @@ class SubmissionListSerializer(serializers.ModelSerializer): class Meta: model = Submission - exclude = ("info", "contest", "code") + exclude = ("info", "contest", "code", "ip") def get_show_link(self, obj): # 没传user或为匿名user diff --git a/submission/views/oj.py b/submission/views/oj.py index fec15cc1..a0deb0a7 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -1,3 +1,5 @@ +import ipaddress + from django.conf import settings from account.decorators import login_required, check_contest_permission from judge.tasks import judge_task @@ -54,23 +56,26 @@ class SubmissionAPI(APIView): return self.error("Contest doesn't exist.") if contest.status == ContestStatus.CONTEST_ENDED: return self.error("The contest have ended") - if contest.status == ContestStatus.CONTEST_NOT_START and not contest.is_contest_admin(request.user): - return self.error("Contest have not started") + if not request.user.is_contest_admin(contest): + if contest.status == ContestStatus.CONTEST_NOT_START: + return self.error("Contest have not started") + user_ip = ipaddress.ip_address(request.session.get("ip")) + if contest.allowed_ip_ranges: + if not any(user_ip in ipaddress.ip_network(cidr) for cidr in contest.allowed_ip_ranges): + return self.error("Your IP is not allowed in this contest") + if not contest.problem_details_permission(request.user): hide_id = True if data.get("captcha"): if not Captcha(request).check(data["captcha"]): return self.error("Invalid captcha") - error = self.throttling(request) if error: return self.error(error) try: - problem = Problem.objects.get(id=data["problem_id"], - contest_id=data.get("contest_id"), - visible=True) + problem = Problem.objects.get(id=data["problem_id"], contest_id=data.get("contest_id"), visible=True) except Problem.DoesNotExist: return self.error("Problem not exist") @@ -79,6 +84,7 @@ class SubmissionAPI(APIView): language=data["language"], code=data["code"], problem_id=problem.id, + ip=request.session["ip"], contest_id=data.get("contest_id")) # use this for debug # JudgeDispatcher(submission.id, problem.id).judge() @@ -100,10 +106,10 @@ class SubmissionAPI(APIView): if not submission.check_user_permission(request.user): return self.error("No permission for this submission") - if submission.problem.rule_type == ProblemRuleType.ACM: - submission_data = SubmissionSafeModelSerializer(submission).data - else: + if submission.problem.rule_type == ProblemRuleType.OI or request.user.is_admin_role(): submission_data = SubmissionModelSerializer(submission).data + else: + submission_data = SubmissionSafeModelSerializer(submission).data # 是否有权限取消共享 submission_data["can_unshare"] = submission.check_user_permission(request.user, check_share=False) return self.success(submission_data) @@ -145,7 +151,7 @@ class SubmissionListAPI(APIView): except Problem.DoesNotExist: return self.error("Problem doesn't exist") submissions = submissions.filter(problem=problem) - if (myself and myself == "1") and not SysOptions.submission_list_show_all: + if (myself and myself == "1") or not SysOptions.submission_list_show_all: submissions = submissions.filter(user_id=request.user.id) elif username: submissions = submissions.filter(username=username)