diff --git a/account/urls/user.py b/account/urls/user.py index 1c3ad368..921faf69 100644 --- a/account/urls/user.py +++ b/account/urls/user.py @@ -1,9 +1,10 @@ from django.conf.urls import url from ..views.user import (SSOAPI, AvatarUploadAPI, TwoFactorAuthAPI, - UserInfoAPI, UserProfileAPI) + UserNameAPI, UserInfoAPI, UserProfileAPI) urlpatterns = [ + url(r"^username/?$", UserNameAPI.as_view(), name="user_name_api"), url(r"^user/(?P\w+)/?$", UserInfoAPI.as_view(), name="user_info_api"), url(r"^profile/?$", UserProfileAPI.as_view(), name="user_profile_api"), url(r"^avatar/upload/?$", AvatarUploadAPI.as_view(), name="avatar_upload_api"), diff --git a/account/views/user.py b/account/views/user.py index d0b83930..0c0ea111 100644 --- a/account/views/user.py +++ b/account/views/user.py @@ -19,6 +19,24 @@ from ..serializers import (SSOSerializer, TwoFactorAuthCodeSerializer, EditUserProfileSerializer, AvatarUploadForm) +class UserNameAPI(APIView): + def get(self, request): + """ + Return Username to valid login status + """ + try: + user = User.objects.get(id=request.user.id) + except User.DoesNotExist: + return self.success({ + "username": "User does not exist", + "isLogin": False + }) + return self.success({ + "username": user.username, + "isLogin": True + }) + + class UserInfoAPI(APIView): # @login_required @method_decorator(ensure_csrf_cookie) diff --git a/deploy/requirements.txt b/deploy/requirements.txt index 3d8bf5ad..b843e277 100644 --- a/deploy/requirements.txt +++ b/deploy/requirements.txt @@ -11,3 +11,5 @@ celery Envelopes qrcode flake8-coding +requests +django-redis diff --git a/oj/local_settings.py b/oj/local_settings.py index a57437d5..f5212ff6 100644 --- a/oj/local_settings.py +++ b/oj/local_settings.py @@ -24,6 +24,13 @@ CACHES = { "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", } + }, + "Throttling": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://127.0.0.1:6379/3", + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + } } } diff --git a/oj/settings.py b/oj/settings.py index e7199d92..c33efe61 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -45,6 +45,7 @@ INSTALLED_APPS = ( 'problem', 'contest', 'utils', + 'submission', 'rest_framework', ) diff --git a/oj/urls.py b/oj/urls.py index 79e6b038..1e79aab5 100644 --- a/oj/urls.py +++ b/oj/urls.py @@ -10,5 +10,6 @@ urlpatterns = [ url(r"^api/", include("problem.urls.oj")), url(r"^api/admin/", include("problem.urls.admin")), url(r"^api/admin/", include("contest.urls.admin")), - url(r"^api/", include("contest.urls.oj")) + url(r"^api/", include("contest.urls.oj")), + url(r"^api/", include("submission.urls.oj")), ] diff --git a/submission/__init__.py b/submission/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/submission/models.py b/submission/models.py new file mode 100644 index 00000000..9c6d823e --- /dev/null +++ b/submission/models.py @@ -0,0 +1,42 @@ +from django.db import models +from jsonfield import JSONField + +from utils.models import RichTextField +from utils.shortcuts import rand_str + + +class JudgeStatus: + COMPILE_ERROR = -2 + WRONG_ANSWER = -1 + ACCEPTED = 0 + CPU_TIME_LIMIT_EXCEEDED = 1 + REAL_TIME_LIMIT_EXCEEDED = 2 + MEMORY_LIMIT_EXCEEDED = 3 + RUNTIME_ERROR = 4 + SYSTEM_ERROR = 5 + PENDING = 6 + JUDGING = 7 + PARTIALLY_ACCEPTED = 8 + + +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) + user_id = models.IntegerField(db_index=True) + code = RichTextField() + result = models.IntegerField(default=JudgeStatus.PENDING) + # 判题结果的详细信息 + info = JSONField(default={}) + language = models.CharField(max_length=20) + shared = models.BooleanField(default=False) + # 题目状态为 Accepted 时才会存储相关info + accepted_time = models.IntegerField(blank=True, null=True) + accepted_info = JSONField(default={}) + + class Meta: + db_table = "submission" + + def __str__(self): + return self.id diff --git a/submission/serializers.py b/submission/serializers.py new file mode 100644 index 00000000..32fe1670 --- /dev/null +++ b/submission/serializers.py @@ -0,0 +1,8 @@ +from utils.api import serializers +from judge.languages import language_names + + +class CreateSubmissionSerializer(serializers.Serializer): + problem_id = serializers.IntegerField() + language = serializers.ChoiceField(choices=language_names) + code = serializers.CharField(max_length=20000) diff --git a/submission/tasks.py b/submission/tasks.py new file mode 100644 index 00000000..ea9d6c8f --- /dev/null +++ b/submission/tasks.py @@ -0,0 +1,7 @@ +from celery import shared_task +from judge.tasks import JudgeDispatcher + + +@shared_task +def _judge(submission_obj, problem_obj): + return JudgeDispatcher(submission_obj, problem_obj).judge() diff --git a/submission/test.py b/submission/test.py new file mode 100644 index 00000000..e69de29b diff --git a/submission/urls/__init__.py b/submission/urls/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/submission/urls/admin.py b/submission/urls/admin.py new file mode 100644 index 00000000..e69de29b diff --git a/submission/urls/oj.py b/submission/urls/oj.py new file mode 100644 index 00000000..9f54c055 --- /dev/null +++ b/submission/urls/oj.py @@ -0,0 +1,9 @@ +from django.conf.urls import url + +from ..views.oj import (SubmissionAPI, SubmissionListAPI) + +urlpatterns = [ + url(r"^submission/?$", SubmissionAPI.as_view(), name="submissiob_api"), + url(r"^submissions/?$", SubmissionListAPI.as_view(), name="submission_list_api"), + url(r"^submissions/(?P\d+)/?$", SubmissionListAPI.as_view(), name="submission_list_page_api"), +] diff --git a/submission/views/__init__.py b/submission/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/submission/views/admin.py b/submission/views/admin.py new file mode 100644 index 00000000..e69de29b diff --git a/submission/views/oj.py b/submission/views/oj.py new file mode 100644 index 00000000..eb43575d --- /dev/null +++ b/submission/views/oj.py @@ -0,0 +1,169 @@ +from django.core.paginator import Paginator +from django_redis import get_redis_connection + +from account.decorators import login_required +from account.models import AdminType, User +from problem.models import Problem + +from utils.api import CSRFExemptAPIView +from utils.api import APIView, validate_serializer +from utils.shortcuts import build_query_string +from utils.throttling import TokenBucket, BucketController + +from ..models import Submission +from ..serializers import CreateSubmissionSerializer +from ..tasks import _judge + + +def _submit_code(response, user, problem_id, language, code): + controller = BucketController(user_id=user.id, + redis_conn=get_redis_connection("Throttling"), + default_capacity=30) + 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: + problem = Problem.objects.get(id=problem_id) + except Problem.DoesNotExist: + return response.error("Problem not exist") + + submission = Submission.objects.create(user_id=user.id, + language=language, + code=code, + problem_id=problem.id) + + try: + _judge.delay(submission, problem) + except Exception as e: + return response.error("Failed") + + return response.success({"submission_id": submission.id}) + + +class SubmissionAPI(APIView): + @validate_serializer(CreateSubmissionSerializer) + # @login_required + def post(self, request): + data = request.data + return _submit_code(self, request.user, data["problem_id"], data["language"], data["code"]) + + @login_required + def get(self, request): + submission_id = request.GET.get("submission_id") + if not submission_id: + return self.error("Parameter error") + try: + submission = Submission.objects.get(id=submission_id, user_id=request.user.id) + except Submission.DoesNotExist: + return self.error("Submission not exist") + + response_data = {"result": submission.result} + if submission.result == 0: + response_data["accepted_answer_time"] = submission.accepted_answer_time + return self.success(response_data) + + +class MyProblemSubmissionListAPI(APIView): + """ + 用户单个题目的全部提交列表 + """ + def get(self, request): + problem_id = request.GET.get("problem_id") + try: + problem = Problem.objects.get(id=problem_id, visible=True) + except Problem.DoesNotExist: + return self.error("Problem not exist") + + submissions = Submission.objects.filter(user_id=request.user.id, problem_id=problem.id, + contest_id__isnull=True). \ + order_by("-created_time"). \ + values("id", "result", "created_time", "accepted_time", "language") + + return self.success({"submissions": submissions, "problem": problem}) + + +class SubmissionListAPI(APIView): + """ + 所有提交的列表 + """ + def get(self, request, **kwargs): + submission_filter = {"my": None, "user_id": None} + show_all = False + page = kwargs.get("page", 1) + + user_id = request.GET.get("user_id") + if user_id and request.user.admin_type == AdminType.SUPER_ADMIN: + submission_filter["user_id"] = user_id + submissions = Submission.objects.filter(user_id=user_id, contest_id__isnull=True) + else: + show_all = True + if request.GET.get("my") == "true": + submission_filter["my"] = "true" + show_all = False + if show_all: + submissions = Submission.objects.filter(contest_id__isnull=True) + else: + submissions = Submission.objects.filter(user_id=request.user.id, contest_id__isnull=True) + + submissions = submissions.values("id", "user_id", "problem_id", "result", "created_time", + "accepted_time", "language").order_by("-created_time") + + language = request.GET.get("language") + if language: + submissions = submissions.filter(language=language) + submission_filter["language"] = language + + result = request.GET.get("result") + if result: + # TODO: 转换为数字结果 + submissions = submissions.filter(result=int(result)) + submission_filter["result"] = result + + paginator = Paginator(submissions, 20) + try: + submissions = paginator.page(int(page)) + except Exception: + return self.error("Page not exist") + + # Cache + cache_result = {"problem": {}, "user": {}} + for item in submissions: + problem_id = item["problem_id"] + if problem_id not in cache_result["problem"]: + problem = Problem.objects.get(id=problem_id) + cache_result["problem"][problem_id] = problem.title + item["title"] = cache_result["problem"][problem_id] + + user_id = item["user_id"] + if user_id not in cache_result["user"]: + user = User.objects.get(id=user_id) + cache_result["user"][user_id] = user + item["user"] = cache_result["user"][user_id] + + if item["user_id"] == request.user.id or request.user.admin_type == AdminType.SUPER_ADMIN: + item["show_link"] = True + else: + item["show_link"] = False + + previous_page = next_page = None + try: + previous_page = submissions.previous_page_number() + except Exception: + pass + try: + next_page = submissions.next_page_number() + except Exception: + pass + + return self.success({"submissions": submissions, "page": int(page), + "previous_page": previous_page, "next_page": next_page, + "start_id": int(page) * 20 - 20, + "query": build_query_string(submission_filter), + "submission_filter": submission_filter, + "show_all": show_all}) diff --git a/utils/shortcuts.py b/utils/shortcuts.py index 9962ade6..a7fcd116 100644 --- a/utils/shortcuts.py +++ b/utils/shortcuts.py @@ -44,3 +44,17 @@ def rand_str(length=32, type="lower_hex"): return random.choice("123456789abcdef") + get_random_string(length - 1, allowed_chars="0123456789abcdef") else: return random.choice("123456789") + get_random_string(length - 1, allowed_chars="0123456789") + + +def build_query_string(kv_data, ignore_none=True): + # {"a": 1, "b": "test"} -> "?a=1&b=test" + query_string = "" + for k, v in kv_data.iteritems(): + if ignore_none is True and kv_data[k] is None: + continue + if query_string != "": + query_string += "&" + else: + query_string = "?" + query_string += (k + "=" + str(v)) + return query_string diff --git a/utils/throttling.py b/utils/throttling.py new file mode 100644 index 00000000..c1fe8dce --- /dev/null +++ b/utils/throttling.py @@ -0,0 +1,91 @@ +from __future__ import print_function +import time + + +class TokenBucket: + def __init__(self, fill_rate, capacity, last_capacity, last_timestamp): + self.capacity = float(capacity) + self._left_tokens = last_capacity + self.fill_rate = float(fill_rate) + self.timestamp = last_timestamp + + def consume(self, tokens=1): + if tokens <= self.tokens: + self._left_tokens -= tokens + return True + return False + + def expected_time(self, tokens=1): + _tokens = self.tokens + tokens = max(tokens, _tokens) + return (tokens - _tokens) / self.fill_rate * 60 + + @property + def tokens(self): + if self._left_tokens < self.capacity: + now = time.time() + delta = self.fill_rate * ((now - self.timestamp) / 60) + self._left_tokens = min(self.capacity, self._left_tokens + delta) + self.timestamp = now + return self._left_tokens + + +class BucketController: + def __init__(self, user_id, redis_conn, default_capacity): + self.user_id = user_id + self.default_capacity = default_capacity + self.redis = redis_conn + self.key = "bucket_" + str(self.user_id) + + @property + def last_capacity(self): + value = self.redis.hget(self.key, "last_capacity") + if value is None: + self.last_capacity = self.default_capacity + return self.default_capacity + return int(value) + + @last_capacity.setter + def last_capacity(self, value): + self.redis.hset(self.key, "last_capacity", value) + + @property + def last_timestamp(self): + value = self.redis.hget(self.key, "last_timestamp") + if value is None: + timestamp = int(time.time()) + self.last_timestamp = timestamp + return timestamp + return int(value) + + @last_timestamp.setter + def last_timestamp(self, value): + self.redis.hset(self.key, "last_timestamp", value) + + +""" +# # Token bucket, to limit submission rate +# # Demo + +success = failure = 0 +current_user_id = 1 +token_bucket_default_capacity = 50 +token_bucket_fill_rate = 10 +for i in range(5000): + controller = BucketController(user_id=current_user_id, + redis_conn=redis.Redis(), + default_capacity=token_bucket_default_capacity) + bucket = TokenBucket(fill_rate=token_bucket_fill_rate, + capacity=token_bucket_default_capacity, + last_capacity=controller.last_capacity, + last_timestamp=controller.last_timestamp) + time.sleep(0.05) + if bucket.consume(): + success += 1 + print(i, ": Accepted") + controller.last_capacity -= 1 + else: + failure += 1 + print(i, "Dropped, time left ", bucket.expected_time()) +print(success, failure) +"""