From 043f8c8b2643878d89d16ad23c2e938e96408b38 Mon Sep 17 00:00:00 2001 From: Chiaki Date: Sun, 30 Apr 2017 21:58:34 +0800 Subject: [PATCH 001/106] Change account interface --- account/serializers.py | 30 ++++++++++++++++++++- account/urls/oj.py | 3 ++- account/urls/user.py | 2 +- account/views/oj.py | 4 +-- account/views/user.py | 60 +++++++++++++++++++++++++++++------------- 5 files changed, 76 insertions(+), 23 deletions(-) diff --git a/account/serializers.py b/account/serializers.py index 112d59d..68eca7c 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -1,6 +1,8 @@ +from django import forms + from utils.api import DateTimeTZField, serializers -from .models import AdminType, ProblemPermission, User +from .models import AdminType, ProblemPermission, User, UserProfile class UserLoginSerializer(serializers.Serializer): @@ -32,6 +34,18 @@ class UserSerializer(serializers.ModelSerializer): "create_time", "last_login", "two_factor_auth", "open_api", "is_disabled"] +class UserProfileSerializer(serializers.ModelSerializer): + + class Meta: + model = UserProfile + + +class UserInfoSerializer(serializers.ModelSerializer): + + class Meta: + model = UserProfile + + class EditUserSerializer(serializers.Serializer): id = serializers.IntegerField() username = serializers.CharField(max_length=30) @@ -46,6 +60,16 @@ class EditUserSerializer(serializers.Serializer): is_disabled = serializers.BooleanField() +class EditUserProfileSerializer(serializers.Serializer): + avatar = serializers.CharField(max_length=100, allow_null=True, required=False) + blog = serializers.URLField(allow_null=True, required=False) + mood = serializers.CharField(max_length=200, allow_null=True, required=False) + phone_number = serializers.CharField(max_length=15, allow_null=True, required=False, ) + school = serializers.CharField(max_length=200, allow_null=True, required=False) + major = serializers.CharField(max_length=200, allow_null=True, required=False) + student_id = serializers.CharField(max_length=15, allow_null=True, required=False) + + class ApplyResetPasswordSerializer(serializers.Serializer): email = serializers.EmailField() captcha = serializers.CharField(max_length=4, min_length=4) @@ -64,3 +88,7 @@ class SSOSerializer(serializers.Serializer): class TwoFactorAuthCodeSerializer(serializers.Serializer): code = serializers.IntegerField() + + +class AvatarUploadForm(forms.Form): + file = forms.FileField() diff --git a/account/urls/oj.py b/account/urls/oj.py index fa47e33..36b28ce 100644 --- a/account/urls/oj.py +++ b/account/urls/oj.py @@ -8,5 +8,6 @@ urlpatterns = [ url(r"^register/?$", UserRegisterAPI.as_view(), name="user_register_api"), url(r"^change_password/?$", UserChangePasswordAPI.as_view(), name="user_change_password_api"), url(r"^apply_reset_password/?$", ApplyResetPasswordAPI.as_view(), name="apply_reset_password_api"), - url(r"^reset_password/?$", ResetPasswordAPI.as_view(), name="apply_reset_password_api") + url(r"^reset_password/?$", ResetPasswordAPI.as_view(), name="apply_reset_password_api"), + url(r"^captcha/?$", "utils.captcha.views.show_captcha", name="show_captcha"), ] diff --git a/account/urls/user.py b/account/urls/user.py index 1676ddc..1c3ad36 100644 --- a/account/urls/user.py +++ b/account/urls/user.py @@ -4,7 +4,7 @@ from ..views.user import (SSOAPI, AvatarUploadAPI, TwoFactorAuthAPI, UserInfoAPI, UserProfileAPI) urlpatterns = [ - url(r"^user/?$", UserInfoAPI.as_view(), name="user_info_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"), url(r"^sso/?$", SSOAPI.as_view(), name="sso_api"), diff --git a/account/views/oj.py b/account/views/oj.py index 7db3bbb..39219ee 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -60,8 +60,8 @@ class UserRegisterAPI(APIView): """ data = request.data captcha = Captcha(request) - if not captcha.check(data["captcha"]): - return self.error("Invalid captcha") + # if not captcha.check(data["captcha"]): + # return self.error("Invalid captcha") try: User.objects.get(username=data["username"]) return self.error("Username already exists") diff --git a/account/views/user.py b/account/views/user.py index 19eb893..727bd78 100644 --- a/account/views/user.py +++ b/account/views/user.py @@ -4,25 +4,37 @@ from io import StringIO import qrcode from django.conf import settings from django.http import HttpResponse +from django.views.decorators.csrf import ensure_csrf_cookie +from django.utils.decorators import method_decorator from otpauth import OtpAuth from conf.models import WebsiteConfig -from utils.api import APIView, validate_serializer +from utils.api import APIView, validate_serializer, CSRFExemptAPIView from utils.shortcuts import rand_str from ..decorators import login_required -from ..models import User +from ..models import User, UserProfile from ..serializers import (EditUserSerializer, SSOSerializer, - TwoFactorAuthCodeSerializer, UserSerializer) + TwoFactorAuthCodeSerializer, UserSerializer, + UserProfileSerializer, UserInfoSerializer, + EditUserProfileSerializer, AvatarUploadForm) class UserInfoAPI(APIView): - @login_required - def get(self, request): + # @login_required + @method_decorator(ensure_csrf_cookie) + def get(self, request, **kwargs): """ Return user info api """ - return self.success(UserSerializer(request.user).data) + try: + user = User.objects.get(username=kwargs["username"]) + except User.DoesNotExist: + return self.error("User does not exist") + profile = UserProfile.objects.get(user=user) + dit = UserProfileSerializer(profile).data + dit['user'] = UserSerializer(user).data + return self.success(dit) class UserProfileAPI(APIView): @@ -31,14 +43,22 @@ class UserProfileAPI(APIView): """ Return user info api """ - return self.success(UserSerializer(request.user).data) + try: + user = User.objects.get(id=request.user.id) + except User.DoesNotExist: + return self.error("User does not exist") + profile = UserProfile.objects.get(user=user) + dit = UserProfileSerializer(profile).data + dit['user'] = UserSerializer(user).data + return self.success(dit) - @validate_serializer(EditUserSerializer) + @validate_serializer(EditUserProfileSerializer) @login_required def put(self, request): data = request.data user_profile = request.user.userprofile - if data["avatar"]: + print(data) + if data.get("avatar"): user_profile.avatar = data["avatar"] else: user_profile.mood = data["mood"] @@ -52,21 +72,25 @@ class UserProfileAPI(APIView): return self.success("Succeeded") -class AvatarUploadAPI(APIView): - def post(self, request): - if "file" not in request.FILES: - return self.error("Upload failed") +class AvatarUploadAPI(CSRFExemptAPIView): + request_parsers = () - f = request.FILES["file"] - if f.size > 1024 * 1024: + def post(self, request): + form = AvatarUploadForm(request.POST, request.FILES) + if form.is_valid(): + avatar = form.cleaned_data["file"] + else: + return self.error("Upload failed") + if avatar.size > 1024 * 1024: return self.error("Picture too large") - if os.path.splitext(f.name)[-1].lower() not in [".gif", ".jpg", ".jpeg", ".bmp", ".png"]: + if os.path.splitext(avatar.name)[-1].lower() not in [".gif", ".jpg", ".jpeg", ".bmp", ".png"]: return self.error("Unsupported file format") - name = "avatar_" + rand_str(5) + os.path.splitext(f.name)[-1] + name = "avatar_" + rand_str(5) + os.path.splitext(avatar.name)[-1] with open(os.path.join(settings.IMAGE_UPLOAD_DIR, name), "wb") as img: - for chunk in request.FILES["file"]: + for chunk in avatar: img.write(chunk) + print(os.path.join(settings.IMAGE_UPLOAD_DIR, name)) return self.success({"path": "/static/upload/" + name}) From d2216195657862e81ec303e3334288743724e088 Mon Sep 17 00:00:00 2001 From: zemal Date: Mon, 1 May 2017 13:03:48 +0800 Subject: [PATCH 002/106] Add problem_list api. Fix AC/Total count bug. --- problem/models.py | 16 +++++++++++----- problem/urls/oj.py | 5 +++-- problem/views/oj.py | 42 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/problem/models.py b/problem/models.py index 17d4e5d..8b373b0 100644 --- a/problem/models.py +++ b/problem/models.py @@ -18,6 +18,12 @@ class ProblemRuleType(object): OI = "OI" +class ProblemDifficulty(object): + High = "High" + Mid = "Mid" + Low = "Low" + + class AbstractProblem(models.Model): title = models.CharField(max_length=128) # HTML @@ -49,19 +55,19 @@ class AbstractProblem(models.Model): difficulty = models.CharField(max_length=32) tags = models.ManyToManyField(ProblemTag) source = models.CharField(max_length=200, blank=True, null=True) - total_submit_number = models.IntegerField(default=0) - total_accepted_number = models.IntegerField(default=0) + total_submit_number = models.BigIntegerField(default=0) + total_accepted_number = models.BigIntegerField(default=0) class Meta: db_table = "problem" abstract = True def add_submission_number(self): - self.accepted_problem_number = models.F("total_submit_number") + 1 + self.total_submit_number=models.F("total_submit_number") + 1 self.save() def add_ac_number(self): - self.accepted_problem_number = models.F("total_accepted_number") + 1 + self.total_accepted_number=models.F("total_accepted_number") + 1 self.save() @@ -77,4 +83,4 @@ class ContestProblem(AbstractProblem): class Meta: db_table = "contest_problem" - unique_together = (("_id", "contest"), ) + unique_together = (("_id", "contest"),) diff --git a/problem/urls/oj.py b/problem/urls/oj.py index a7613f1..5e7e6be 100644 --- a/problem/urls/oj.py +++ b/problem/urls/oj.py @@ -1,7 +1,8 @@ from django.conf.urls import url -from ..views.oj import ProblemTagAPI +from ..views.oj import ProblemTagAPI, ProblemAPI urlpatterns = [ - url(r"^problem/tags/?$", ProblemTagAPI.as_view(), name="problem_tag_list_api") + url(r"^problem/tags/?$", ProblemTagAPI.as_view(), name="problem_tag_list_api"), + url(r"^problems/?$", ProblemAPI.as_view(), name="problem_list_api"), ] diff --git a/problem/views/oj.py b/problem/views/oj.py index 94496ce..c21c06a 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -1,8 +1,46 @@ +from django.db.models import Q, Count from utils.api import APIView -from ..models import ProblemTag +from ..models import ProblemTag, Problem +from ..serializers import ProblemSerializer, TagSerializer class ProblemTagAPI(APIView): def get(self, request): - return self.success([item.name for item in ProblemTag.objects.all().order_by("id")]) + tags = ProblemTag.objects.annotate(problem_number=Count("problem"))\ + .filter(problem_number__gt=0).order_by("-problem_number") + return self.success(TagSerializer(tags, many=True).data) + + +class ProblemAPI(APIView): + def get(self, request): + # 问题详情页 + problem_id = request.GET.get("id") + if problem_id: + try: + problem = Problem.objects.get(id=problem_id) + return self.success(ProblemSerializer(problem).data) + except Problem.DoesNotExist: + return self.error("Problem does not exist") + + problems = Problem.objects.filter(visible=True) + # 按照标签筛选 + tag_text = request.GET.get("tag", None) + if tag_text: + try: + tag = ProblemTag.objects.get(name=tag_text) + except ProblemTag.DoesNotExist: + return self.error("The Tag does not exist.") + problems = tag.problem_set.all().filter(visible=True) + + # 搜索的情况 + keyword = request.GET.get("keyword", "").strip() + if keyword: + problems = problems.filter(Q(title__contains=keyword) | Q(description__contains=keyword)) + + # 难度筛选 + difficulty_rank = request.GET.get('difficulty', None) + if difficulty_rank: + problems = problems.filter(difficulty=difficulty_rank) + + return self.success(self.paginate_data(request, problems, ProblemSerializer)) From a96a23da2d3fc890b47af267c1119f19a446e680 Mon Sep 17 00:00:00 2001 From: zemal Date: Mon, 1 May 2017 13:20:26 +0800 Subject: [PATCH 003/106] Fix tests. --- problem/tests.py | 8 +++++--- problem/urls/admin.py | 2 +- problem/views/oj.py | 4 +--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/problem/tests.py b/problem/tests.py index d80262a..fc9fa44 100644 --- a/problem/tests.py +++ b/problem/tests.py @@ -17,7 +17,9 @@ class ProblemTagListAPITest(APITestCase): ProblemTag.objects.create(name="name2") resp = self.client.get(self.reverse("problem_tag_list_api")) self.assertSuccess(resp) - self.assertEqual(resp.data["data"], ["name1", "name2"]) + resp_data = resp.data['data'] + self.assertEqual(resp_data[0]["name"], "name1") + self.assertEqual(resp_data[1]["name"], "name2") class TestCaseUploadAPITest(APITestCase): @@ -76,9 +78,9 @@ class TestCaseUploadAPITest(APITestCase): self.assertEqual(f.read(), name + "\n" + name + "\n" + "end") -class ProblemAPITest(APITestCase): +class ProblemAdminAPITest(APITestCase): def setUp(self): - self.url = self.reverse("problem_api") + self.url = self.reverse("problem_admin_api") self.create_super_admin() self.data = {"_id": "A-110", "title": "test", "description": "

test

", "input_description": "test", "output_description": "test", "time_limit": 1000, "memory_limit": 256, "difficulty": "Low", diff --git a/problem/urls/admin.py b/problem/urls/admin.py index b4813c5..d123f35 100644 --- a/problem/urls/admin.py +++ b/problem/urls/admin.py @@ -4,6 +4,6 @@ from ..views.admin import ContestProblemAPI, ProblemAPI, TestCaseUploadAPI urlpatterns = [ url(r"^test_case/upload/?$", TestCaseUploadAPI.as_view(), name="test_case_upload_api"), - url(r"^problem/?$", ProblemAPI.as_view(), name="problem_api"), + url(r"^problem/?$", ProblemAPI.as_view(), name="problem_admin_api"), url(r"^contest/problem/?$", ContestProblemAPI.as_view(), name="contest_problem_api") ] diff --git a/problem/views/oj.py b/problem/views/oj.py index c21c06a..bed7ed8 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -7,9 +7,7 @@ from ..serializers import ProblemSerializer, TagSerializer class ProblemTagAPI(APIView): def get(self, request): - tags = ProblemTag.objects.annotate(problem_number=Count("problem"))\ - .filter(problem_number__gt=0).order_by("-problem_number") - return self.success(TagSerializer(tags, many=True).data) + return self.success(TagSerializer(ProblemTag.objects.all(), many=True).data) class ProblemAPI(APIView): From 7a43982b7483382e5b8c80e4c4341d8af497586e Mon Sep 17 00:00:00 2001 From: zemal Date: Mon, 1 May 2017 13:30:47 +0800 Subject: [PATCH 004/106] fix ci --- problem/models.py | 4 ++-- problem/tests.py | 3 ++- problem/views/oj.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/problem/models.py b/problem/models.py index 8b373b0..61cc8f4 100644 --- a/problem/models.py +++ b/problem/models.py @@ -63,11 +63,11 @@ class AbstractProblem(models.Model): abstract = True def add_submission_number(self): - self.total_submit_number=models.F("total_submit_number") + 1 + self.total_submit_number = models.F("total_submit_number") + 1 self.save() def add_ac_number(self): - self.total_accepted_number=models.F("total_accepted_number") + 1 + self.total_accepted_number = models.F("total_accepted_number") + 1 self.save() diff --git a/problem/tests.py b/problem/tests.py index fc9fa44..0aeefb0 100644 --- a/problem/tests.py +++ b/problem/tests.py @@ -29,7 +29,8 @@ class TestCaseUploadAPITest(APITestCase): self.create_super_admin() def test_filter_file_name(self): - self.assertEqual(self.api.filter_name_list(["1.in", "1.out", "2.in", ".DS_Store"], spj=False), ["1.in", "1.out"]) + self.assertEqual(self.api.filter_name_list(["1.in", "1.out", "2.in", ".DS_Store"], spj=False), + ["1.in", "1.out"]) self.assertEqual(self.api.filter_name_list(["2.in", "2.out"], spj=False), []) self.assertEqual(self.api.filter_name_list(["1.in", "1.out", "2.in"], spj=True), ["1.in", "2.in"]) diff --git a/problem/views/oj.py b/problem/views/oj.py index bed7ed8..ffce405 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -1,4 +1,4 @@ -from django.db.models import Q, Count +from django.db.models import Q from utils.api import APIView from ..models import ProblemTag, Problem From feddec8a3fed2b3bced9592b3a60e53711054ea1 Mon Sep 17 00:00:00 2001 From: zemal Date: Mon, 1 May 2017 13:39:18 +0800 Subject: [PATCH 005/106] fix ci again. --- problem/tests.py | 2 +- problem/views/oj.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/problem/tests.py b/problem/tests.py index 0aeefb0..caa6ad6 100644 --- a/problem/tests.py +++ b/problem/tests.py @@ -17,7 +17,7 @@ class ProblemTagListAPITest(APITestCase): ProblemTag.objects.create(name="name2") resp = self.client.get(self.reverse("problem_tag_list_api")) self.assertSuccess(resp) - resp_data = resp.data['data'] + resp_data = resp.data["data"] self.assertEqual(resp_data[0]["name"], "name1") self.assertEqual(resp_data[1]["name"], "name2") diff --git a/problem/views/oj.py b/problem/views/oj.py index ffce405..611c2c9 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -37,7 +37,7 @@ class ProblemAPI(APIView): problems = problems.filter(Q(title__contains=keyword) | Q(description__contains=keyword)) # 难度筛选 - difficulty_rank = request.GET.get('difficulty', None) + difficulty_rank = request.GET.get("difficulty", None) if difficulty_rank: problems = problems.filter(difficulty=difficulty_rank) From d11f8f9bffb8582efa7e4b5d3d0e2ac2e91960df Mon Sep 17 00:00:00 2001 From: Chiaki Date: Mon, 1 May 2017 15:20:13 +0800 Subject: [PATCH 006/106] Fix python3 qrcode and some bugs --- account/urls/oj.py | 4 +++- account/views/oj.py | 6 ++++++ account/views/user.py | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/account/urls/oj.py b/account/urls/oj.py index 36b28ce..34b267c 100644 --- a/account/urls/oj.py +++ b/account/urls/oj.py @@ -1,10 +1,12 @@ from django.conf.urls import url from ..views.oj import (ApplyResetPasswordAPI, ResetPasswordAPI, - UserChangePasswordAPI, UserLoginAPI, UserRegisterAPI) + UserChangePasswordAPI, UserRegisterAPI, + UserLoginAPI, UserLogoutAPI) urlpatterns = [ url(r"^login/?$", UserLoginAPI.as_view(), name="user_login_api"), + url(r"^logout/?$", UserLogoutAPI.as_view(), name="user_logout_api"), url(r"^register/?$", UserRegisterAPI.as_view(), name="user_register_api"), url(r"^change_password/?$", UserChangePasswordAPI.as_view(), name="user_change_password_api"), url(r"^apply_reset_password/?$", ApplyResetPasswordAPI.as_view(), name="apply_reset_password_api"), diff --git a/account/views/oj.py b/account/views/oj.py index 39219ee..fdf2c31 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -52,6 +52,12 @@ class UserLoginAPI(APIView): return self.success({}) +class UserLogoutAPI(APIView): + def get(self, request): + auth.logout(request) + return self.success({}) + + class UserRegisterAPI(APIView): @validate_serializer(UserRegisterSerializer) def post(self, request): diff --git a/account/views/user.py b/account/views/user.py index 727bd78..1f9ee24 100644 --- a/account/views/user.py +++ b/account/views/user.py @@ -1,5 +1,5 @@ import os -from io import StringIO +from io import BytesIO import qrcode from django.conf import settings @@ -140,7 +140,7 @@ class TwoFactorAuthAPI(APIView): config = WebsiteConfig.objects.first() image = qrcode.make(OtpAuth(token).to_uri("totp", config.base_url, config.name)) - buf = StringIO() + buf = BytesIO() image.save(buf, "gif") return HttpResponse(buf.getvalue(), "image/gif") From 570c63100a033cabb38d6ea28748bd374e8e0a32 Mon Sep 17 00:00:00 2001 From: zemal Date: Mon, 1 May 2017 15:52:17 +0800 Subject: [PATCH 007/106] Alter IntergerField to BigIntergerField. --- problem/migrations/0004_auto_20170501_0637.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 problem/migrations/0004_auto_20170501_0637.py diff --git a/problem/migrations/0004_auto_20170501_0637.py b/problem/migrations/0004_auto_20170501_0637.py new file mode 100644 index 0000000..5d55ace --- /dev/null +++ b/problem/migrations/0004_auto_20170501_0637.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2017-05-01 06:37 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('problem', '0003_auto_20170217_0820'), + ] + + operations = [ + migrations.AlterField( + model_name='contestproblem', + name='total_accepted_number', + field=models.BigIntegerField(default=0), + ), + migrations.AlterField( + model_name='contestproblem', + name='total_submit_number', + field=models.BigIntegerField(default=0), + ), + migrations.AlterField( + model_name='problem', + name='total_accepted_number', + field=models.BigIntegerField(default=0), + ), + migrations.AlterField( + model_name='problem', + name='total_submit_number', + field=models.BigIntegerField(default=0), + ), + ] From ce5c153662525878936688c0b8acce2c00a0b933 Mon Sep 17 00:00:00 2001 From: Chiaki Date: Mon, 1 May 2017 16:06:45 +0800 Subject: [PATCH 008/106] Fix ci --- account/views/oj.py | 4 ++-- account/views/user.py | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/account/views/oj.py b/account/views/oj.py index fdf2c31..b1cef63 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -66,8 +66,8 @@ class UserRegisterAPI(APIView): """ data = request.data captcha = Captcha(request) - # if not captcha.check(data["captcha"]): - # return self.error("Invalid captcha") + if not captcha.check(data["captcha"]): + return self.error("Invalid captcha") try: User.objects.get(username=data["username"]) return self.error("Username already exists") diff --git a/account/views/user.py b/account/views/user.py index 1f9ee24..d0b8393 100644 --- a/account/views/user.py +++ b/account/views/user.py @@ -14,9 +14,8 @@ from utils.shortcuts import rand_str from ..decorators import login_required from ..models import User, UserProfile -from ..serializers import (EditUserSerializer, SSOSerializer, - TwoFactorAuthCodeSerializer, UserSerializer, - UserProfileSerializer, UserInfoSerializer, +from ..serializers import (SSOSerializer, TwoFactorAuthCodeSerializer, + UserSerializer, UserProfileSerializer, EditUserProfileSerializer, AvatarUploadForm) @@ -33,7 +32,7 @@ class UserInfoAPI(APIView): return self.error("User does not exist") profile = UserProfile.objects.get(user=user) dit = UserProfileSerializer(profile).data - dit['user'] = UserSerializer(user).data + dit["user"] = UserSerializer(user).data return self.success(dit) @@ -49,7 +48,7 @@ class UserProfileAPI(APIView): return self.error("User does not exist") profile = UserProfile.objects.get(user=user) dit = UserProfileSerializer(profile).data - dit['user'] = UserSerializer(user).data + dit["user"] = UserSerializer(user).data return self.success(dit) @validate_serializer(EditUserProfileSerializer) From 65f9c7f52b501bee4580c65b4c947560fd053bcb Mon Sep 17 00:00:00 2001 From: Chiaki Date: Mon, 8 May 2017 17:29:01 +0800 Subject: [PATCH 009/106] Add submission module --- account/urls/user.py | 3 +- account/views/user.py | 18 +++ oj/settings.py | 1 + oj/urls.py | 3 +- requirements.txt | 3 +- submission/__init__.py | 0 submission/migrations/0001_initial.py | 39 ++++++ submission/migrations/__init__.py | 0 submission/models.py | 44 +++++++ submission/serializers.py | 10 ++ submission/tasks.py | 6 + submission/test.py | 0 submission/urls/__init__.py | 0 submission/urls/admin.py | 0 submission/urls/oj.py | 9 ++ submission/views/__init__.py | 0 submission/views/admin.py | 0 submission/views/oj.py | 172 ++++++++++++++++++++++++++ utils/shortcuts.py | 14 +++ utils/throttling.py | 91 ++++++++++++++ 20 files changed, 410 insertions(+), 3 deletions(-) create mode 100644 submission/__init__.py create mode 100644 submission/migrations/0001_initial.py create mode 100644 submission/migrations/__init__.py create mode 100644 submission/models.py create mode 100644 submission/serializers.py create mode 100644 submission/tasks.py create mode 100644 submission/test.py create mode 100644 submission/urls/__init__.py create mode 100644 submission/urls/admin.py create mode 100644 submission/urls/oj.py create mode 100644 submission/views/__init__.py create mode 100644 submission/views/admin.py create mode 100644 submission/views/oj.py create mode 100644 utils/throttling.py diff --git a/account/urls/user.py b/account/urls/user.py index 1c3ad36..921faf6 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 d0b8393..0c0ea11 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/oj/settings.py b/oj/settings.py index e7199d9..c33efe6 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 79e6b03..1e79aab 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/requirements.txt b/requirements.txt index 9b0d805..eab93e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ celery Envelopes pytz jsonfield -qrcode \ No newline at end of file +qrcode +redis \ No newline at end of file diff --git a/submission/__init__.py b/submission/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/submission/migrations/0001_initial.py b/submission/migrations/0001_initial.py new file mode 100644 index 0000000..74a0559 --- /dev/null +++ b/submission/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2017-05-08 09:27 +from __future__ import unicode_literals + +from django.db import migrations, models +import jsonfield.fields +import utils.models +import utils.shortcuts + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Submission', + fields=[ + ('id', models.CharField(db_index=True, default=utils.shortcuts.rand_str, max_length=32, primary_key=True, serialize=False)), + ('contest_id', models.IntegerField(db_index=True)), + ('problem_id', models.IntegerField(db_index=True)), + ('created_time', models.DateTimeField(auto_now_add=True)), + ('user_id', models.IntegerField(db_index=True)), + ('code', utils.models.RichTextField()), + ('result', models.IntegerField(default=6)), + ('info', jsonfield.fields.JSONField()), + ('language', models.CharField(max_length=20)), + ('shared', models.BooleanField(default=False)), + ('accepted_time', models.IntegerField(blank=True, null=True)), + ('accepted_info', jsonfield.fields.JSONField()), + ], + options={ + 'db_table': 'submission', + }, + ), + ] diff --git a/submission/migrations/__init__.py b/submission/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/submission/models.py b/submission/models.py new file mode 100644 index 0000000..7cb6cc7 --- /dev/null +++ b/submission/models.py @@ -0,0 +1,44 @@ +from django.db import models +from jsonfield import JSONField + +# from judge.languages import language_names +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 + # TODO: 部分正确 + + +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) + 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() + # TODO: choice + language = models.CharField(max_length=20) + shared = models.BooleanField(default=False) + # 题目状态为 Accepted 时才会存储相关info + accepted_time = models.IntegerField(blank=True, null=True) + accepted_info = JSONField() + + 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 0000000..f5cd1a0 --- /dev/null +++ b/submission/serializers.py @@ -0,0 +1,10 @@ +from utils.api import serializers + +# from account.models import User +# from .models import Submission + + +class CreateSubmissionSerializer(serializers.Serializer): + problem_id = serializers.IntegerField() + language = serializers.CharField(max_length=20) + code = serializers.CharField(max_length=20000) diff --git a/submission/tasks.py b/submission/tasks.py new file mode 100644 index 0000000..f6802ae --- /dev/null +++ b/submission/tasks.py @@ -0,0 +1,6 @@ +from celery import shared_task + + +@shared_task +def _judge(submission_obj, problem_obj): + pass diff --git a/submission/test.py b/submission/test.py new file mode 100644 index 0000000..e69de29 diff --git a/submission/urls/__init__.py b/submission/urls/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/submission/urls/admin.py b/submission/urls/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/submission/urls/oj.py b/submission/urls/oj.py new file mode 100644 index 0000000..9f54c05 --- /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 0000000..e69de29 diff --git a/submission/views/admin.py b/submission/views/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/submission/views/oj.py b/submission/views/oj.py new file mode 100644 index 0000000..ddc2148 --- /dev/null +++ b/submission/views/oj.py @@ -0,0 +1,172 @@ +import redis + +from django.core.paginator import Paginator + +from account.decorators import login_required +from account.models import AdminType, User +from problem.models import Problem + +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=redis.Redis(), + 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: + # TODO 参数 + _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 9962ade..a7fcd11 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 0000000..c1fe8dc --- /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) +""" From 4dd52f6727fe98354246f8fe045ab7317a48b070 Mon Sep 17 00:00:00 2001 From: Chiaki Date: Mon, 8 May 2017 17:31:16 +0800 Subject: [PATCH 010/106] fix ci --- deploy/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/requirements.txt b/deploy/requirements.txt index 3d8bf5a..f186890 100644 --- a/deploy/requirements.txt +++ b/deploy/requirements.txt @@ -11,3 +11,4 @@ celery Envelopes qrcode flake8-coding +redis From 5de3adf0efc81333f346def8b0b6b245f924f91c Mon Sep 17 00:00:00 2001 From: zemal Date: Mon, 8 May 2017 20:37:54 +0800 Subject: [PATCH 011/106] JudgeDispatcher beta. --- account/models.py | 8 +-- judge/tasks.py | 130 +++++++++++++++++++++++++++++++++++++++++++ oj/local_settings.py | 17 ++++++ problem/views/oj.py | 6 +- requirements.txt | 4 +- 5 files changed, 154 insertions(+), 11 deletions(-) create mode 100644 judge/tasks.py diff --git a/account/models.py b/account/models.py index 0e53c36..1e97429 100644 --- a/account/models.py +++ b/account/models.py @@ -8,12 +8,6 @@ class AdminType(object): ADMIN = "Admin" SUPER_ADMIN = "Super Admin" - -class ProblemSolutionStatus(object): - ACCEPTED = 1 - PENDING = 2 - - class ProblemPermission(object): NONE = "None" OWN = "Own" @@ -75,7 +69,7 @@ def _random_avatar(): class UserProfile(models.Model): user = models.OneToOneField(User) # Store user problem solution status with json string format - # {"problems": {1: ProblemSolutionStatus.ACCEPTED}, "contest_problems": {20: ProblemSolutionStatus.PENDING)} + # {"problems": {1: JudgeStatus.ACCEPTED}, "contest_problems": {20: JudgeStatus.PENDING)} problems_status = JSONField(default={}) avatar = models.CharField(max_length=50, default=_random_avatar) blog = models.URLField(blank=True, null=True) diff --git a/judge/tasks.py b/judge/tasks.py new file mode 100644 index 0000000..ea33058 --- /dev/null +++ b/judge/tasks.py @@ -0,0 +1,130 @@ +import time +import json +import requests +import hashlib +import logging +from urllib.parse import urljoin + +from django.db import transaction +from django.db.models import F +from django_redis import get_redis_connection + +from judge.languages import languages +from account.models import User, UserProfile +from conf.models import JudgeServer, JudgeServerToken +from contest.models import Contest +from problem.models import Problem, ProblemRuleType +from submission.models import Submission, JudgeStatus + +logger = logging.getLogger(__name__) + +WAITING_QUEUE = "waiting_queue" + + +class JudgeDispatcher(object): + def __init__(self, submission_obj, problem_obj): + 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_obj + self.problem_obj = problem_obj + + def _request(self, url, data=None): + kwargs = {"headers": {"X-Judge-Server-Token": self.token, + "Content-Type": "application/json"}} + if data: + kwargs["data"] = json.dumps(data) + try: + return requests.post(url, **kwargs).json() + except Exception as e: + logger.error(e.with_traceback()) + + @staticmethod + def choose_judge_server(): + with transaction.atomic(): + # TODO: use more reasonable way + servers = JudgeServer.objects.select_for_update().filter( + status="normal").order_by("task_number") + if servers.exists(): + server = servers.first() + server.used_instance_number = F("task_number") + 1 + server.save() + return server + + @staticmethod + def release_judge_res(judge_server_id): + with transaction.atomic(): + # 使用原子操作, 同时因为use和release中间间隔了判题过程,需要重新查询一下 + server = JudgeServer.objects.select_for_update().get(id=judge_server_id) + server.used_instance_number = F("task_number") - 1 + server.save() + + def judge(self, output=False): + server = self.choose_judge_server() + if not server: + self.redis_conn.lpush(WAITING_QUEUE, self.submission_obj.id) + return + + language = list(filter(lambda item: self.submission_obj.language == item['name'], languages))[0] + data = {"language_config": language['config'], + "src": self.submission_obj.code, + "max_cpu_time": self.problem_obj.time_limit, + "max_memory": self.problem_obj.memory_limit, + "test_case_id": self.problem_obj.test_case_id, + "output": output} + # TODO: try catch + resp = self._request(urljoin(server.service_url, "/judge"), data=data) + self.submission_obj.info = resp + if resp['err']: + self.submission_obj.result = JudgeStatus.COMPILE_ERROR + else: + error_test_case = list(filter(lambda case: case['result'] != 0, resp)) + # 多个测试点全部正确AC,否则ACM模式下取第一个测试点状态 + if not error_test_case: + self.submission_obj.result = JudgeStatus.ACCEPTED + elif self.problem_obj.rule_tyle == ProblemRuleType.ACM: + self.submission_obj.result = error_test_case[0].result + else: + self.submission_obj.result = JudgeStatus.PARTIALLY_ACCEPTED + self.submission_obj.save() + self.release_judge_res(server.id) + + if self.submission_obj.contest_id: + # ToDo: update contest status + pass + else: + self.update_problem_status() + # 取redis中等待中的提交 + if self.redis_conn.llen(WAITING_QUEUE): + pass + + def compile_spj(self, service_url, src, spj_version, spj_compile_config, test_case_id): + data = {"src": src, "spj_version": spj_version, + "spj_compile_config": spj_compile_config, "test_case_id": test_case_id} + return self._request(service_url + "/compile_spj", data=data) + + def update_problem_status(self): + with transaction.atomic(): + problem = Problem.objects.select_for_update().get(id=self.problem_obj.problem_id) + # 更新普通题目的计数器 + problem.add_submission_number() + + # 更新用户做题状态 + user = User.objects.select_for_update().get(id=self.submission_obj.user_id) + problems_status = UserProfile.objects.get(user=user).problem_status + + if "problems" not in problems_status: + problems_status["problems"] = {} + + # 增加用户提交计数器 + user.userprofile.add_submission_number() + + # 之前状态不是ac, 现在是ac了 需要更新用户ac题目数量计数器,这里需要判重 + if problems_status["problems"].get(str(problem.id), JudgeStatus.WRONG_ANSWER) != JudgeStatus.ACCEPTED: + if self.submission_obj.result == JudgeStatus.ACCEPTED: + user.userprofile.add_accepted_problem_number() + problems_status["problems"][str(problem.id)] = JudgeStatus.ACCEPTED + else: + problems_status["problems"][str(problem.id)] = JudgeStatus.WRONG_ANSWER + user.problems_status = problems_status + user.save(update_fields=["problems_status"]) diff --git a/oj/local_settings.py b/oj/local_settings.py index 79dc44e..a57437d 100644 --- a/oj/local_settings.py +++ b/oj/local_settings.py @@ -10,6 +10,23 @@ DATABASES = { } } +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://127.0.0.1:6379/1", + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + } + }, + "JudgeQueue": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://127.0.0.1:6379/2", + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + } + } +} + REDIS_CACHE = { "host": "127.0.0.1", "port": 6379, diff --git a/problem/views/oj.py b/problem/views/oj.py index 611c2c9..2edde23 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -16,14 +16,14 @@ class ProblemAPI(APIView): problem_id = request.GET.get("id") if problem_id: try: - problem = Problem.objects.get(id=problem_id) + problem = Problem.objects.get(id=problem_id, visible=True) return self.success(ProblemSerializer(problem).data) except Problem.DoesNotExist: return self.error("Problem does not exist") problems = Problem.objects.filter(visible=True) # 按照标签筛选 - tag_text = request.GET.get("tag", None) + tag_text = request.GET.get("tag") if tag_text: try: tag = ProblemTag.objects.get(name=tag_text) @@ -37,7 +37,7 @@ class ProblemAPI(APIView): problems = problems.filter(Q(title__contains=keyword) | Q(description__contains=keyword)) # 难度筛选 - difficulty_rank = request.GET.get("difficulty", None) + difficulty_rank = request.GET.get("difficulty") if difficulty_rank: problems = problems.filter(difficulty=difficulty_rank) diff --git a/requirements.txt b/requirements.txt index 9b0d805..b48fa46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ django==1.9.6 +django-redis djangorestframework==3.4.0 otpauth pillow @@ -7,4 +8,5 @@ celery Envelopes pytz jsonfield -qrcode \ No newline at end of file +qrcode +requests From 4733eecef9b129015380c86dbc2951e202e9fcc5 Mon Sep 17 00:00:00 2001 From: zemal Date: Tue, 9 May 2017 14:47:54 +0800 Subject: [PATCH 012/106] Add migrations files --- account/models.py | 1 + judge/tasks.py | 4 +-- submission/migrations/0001_initial.py | 39 +++++++++++++++++++++++++++ submission/migrations/__init__.py | 0 submission/views/oj.py | 3 +-- 5 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 submission/migrations/0001_initial.py create mode 100644 submission/migrations/__init__.py diff --git a/account/models.py b/account/models.py index 1e97429..8802c29 100644 --- a/account/models.py +++ b/account/models.py @@ -8,6 +8,7 @@ class AdminType(object): ADMIN = "Admin" SUPER_ADMIN = "Super Admin" + class ProblemPermission(object): NONE = "None" OWN = "Own" diff --git a/judge/tasks.py b/judge/tasks.py index ea33058..1a83ce4 100644 --- a/judge/tasks.py +++ b/judge/tasks.py @@ -1,4 +1,3 @@ -import time import json import requests import hashlib @@ -12,9 +11,8 @@ from django_redis import get_redis_connection from judge.languages import languages from account.models import User, UserProfile from conf.models import JudgeServer, JudgeServerToken -from contest.models import Contest from problem.models import Problem, ProblemRuleType -from submission.models import Submission, JudgeStatus +from submission.models import JudgeStatus logger = logging.getLogger(__name__) diff --git a/submission/migrations/0001_initial.py b/submission/migrations/0001_initial.py new file mode 100644 index 0000000..42a5352 --- /dev/null +++ b/submission/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2017-05-09 06:41 +from __future__ import unicode_literals + +from django.db import migrations, models +import jsonfield.fields +import utils.models +import utils.shortcuts + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Submission', + fields=[ + ('id', models.CharField(db_index=True, default=utils.shortcuts.rand_str, max_length=32, primary_key=True, serialize=False)), + ('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', utils.models.RichTextField()), + ('result', models.IntegerField(default=6)), + ('info', jsonfield.fields.JSONField(default={})), + ('language', models.CharField(max_length=20)), + ('shared', models.BooleanField(default=False)), + ('accepted_time', models.IntegerField(blank=True, null=True)), + ('accepted_info', jsonfield.fields.JSONField(default={})), + ], + options={ + 'db_table': 'submission', + }, + ), + ] diff --git a/submission/migrations/__init__.py b/submission/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/submission/views/oj.py b/submission/views/oj.py index eb43575..c34ee5c 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -5,7 +5,6 @@ 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 @@ -40,7 +39,7 @@ def _submit_code(response, user, problem_id, language, code): try: _judge.delay(submission, problem) - except Exception as e: + except Exception: return response.error("Failed") return response.success({"submission_id": submission.id}) From 08bd591bfb4b38a35ce7f674cce1e889b9bd10ef Mon Sep 17 00:00:00 2001 From: zemal Date: Wed, 10 May 2017 17:20:52 +0800 Subject: [PATCH 013/106] =?UTF-8?q?=E4=BF=AE=E6=AD=A3dispatcher=EF=BC=8C?= =?UTF-8?q?=E4=BD=BF=E7=94=A8redis=E5=AD=98=E4=BB=BB=E5=8A=A1=E9=98=9F?= =?UTF-8?q?=E5=88=97=EF=BC=8C=E4=BF=AE=E6=AD=A3submission=E7=9A=84post?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E6=94=B9=E9=83=A8=E5=88=86settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- conf/models.py | 3 +- conf/views.py | 6 ++ judge/{tasks.py => dispatcher.py} | 83 +++++++++++-------- oj/__init__.py | 6 ++ oj/celery.py | 18 ++++ oj/db_router.py | 19 ----- oj/local_settings.py | 9 +- oj/settings.py | 2 - problem/models.py | 4 +- .../migrations/0002_auto_20170509_1203.py | 20 +++++ submission/models.py | 3 +- submission/tasks.py | 7 +- submission/views/oj.py | 60 ++++++-------- 13 files changed, 133 insertions(+), 107 deletions(-) rename judge/{tasks.py => dispatcher.py} (63%) create mode 100644 oj/celery.py delete mode 100644 oj/db_router.py create mode 100644 submission/migrations/0002_auto_20170509_1203.py diff --git a/conf/models.py b/conf/models.py index 9a88fdf..9fe3cc5 100644 --- a/conf/models.py +++ b/conf/models.py @@ -41,7 +41,8 @@ class JudgeServer(models.Model): @property def status(self): - if (timezone.now() - self.last_heartbeat).total_seconds() > 5: + # 增加一秒延时,提高对网络环境的适应性 + if (timezone.now() - self.last_heartbeat).total_seconds() > 6: return "abnormal" return "normal" diff --git a/conf/views.py b/conf/views.py index 3dc9959..bc03c01 100644 --- a/conf/views.py +++ b/conf/views.py @@ -1,9 +1,11 @@ import hashlib from django.utils import timezone +from django_redis import get_redis_connection from account.decorators import super_admin_required from judge.languages import languages, spj_languages +from judge.dispatcher import process_pending_task from utils.api import APIView, CSRFExemptAPIView, validate_serializer from utils.shortcuts import rand_str @@ -126,6 +128,10 @@ class JudgeServerHeartbeatAPI(CSRFExemptAPIView): service_url=service_url, last_heartbeat=timezone.now(), ) + # 新server上线 处理队列中的,防止没有新的提交而导致一直waiting + conn = get_redis_connection("JudgeQueue") + process_pending_task(conn) + return self.success() diff --git a/judge/tasks.py b/judge/dispatcher.py similarity index 63% rename from judge/tasks.py rename to judge/dispatcher.py index 1a83ce4..48fa33e 100644 --- a/judge/tasks.py +++ b/judge/dispatcher.py @@ -9,23 +9,32 @@ from django.db.models import F from django_redis import get_redis_connection from judge.languages import languages -from account.models import User, UserProfile +from account.models import User from conf.models import JudgeServer, JudgeServerToken from problem.models import Problem, ProblemRuleType -from submission.models import JudgeStatus +from submission.models import JudgeStatus, Submission logger = logging.getLogger(__name__) WAITING_QUEUE = "waiting_queue" +# 继续处理在队列中的问题 +def process_pending_task(redis_conn): + if redis_conn.llen(WAITING_QUEUE): + # 防止循环引入 + from submission.tasks import judge_task + data = json.loads(redis_conn.rpop(WAITING_QUEUE)) + judge_task.delay(**data) + + class JudgeDispatcher(object): - def __init__(self, submission_obj, problem_obj): + def __init__(self, submission_id, problem_id): 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_obj - self.problem_obj = problem_obj + self.submission_obj = Submission.objects.get(pk=submission_id) + self.problem_obj = Problem.objects.get(pk=problem_id) def _request(self, url, data=None): kwargs = {"headers": {"X-Judge-Server-Token": self.token, @@ -41,10 +50,10 @@ class JudgeDispatcher(object): def choose_judge_server(): with transaction.atomic(): # TODO: use more reasonable way - servers = JudgeServer.objects.select_for_update().filter( - status="normal").order_by("task_number") - if servers.exists(): - server = servers.first() + servers = JudgeServer.objects.select_for_update().all().order_by('task_number') + servers = [s for s in servers if s.status == "normal"] + if servers: + server = servers[0] server.used_instance_number = F("task_number") + 1 server.save() return server @@ -60,28 +69,31 @@ class JudgeDispatcher(object): def judge(self, output=False): server = self.choose_judge_server() if not server: - self.redis_conn.lpush(WAITING_QUEUE, self.submission_obj.id) + data = {'submission_id': self.submission_obj.id, 'problem_id': self.problem_obj.id} + self.redis_conn.lpush(WAITING_QUEUE, json.dumps(data)) return language = list(filter(lambda item: self.submission_obj.language == item['name'], languages))[0] - data = {"language_config": language['config'], - "src": self.submission_obj.code, - "max_cpu_time": self.problem_obj.time_limit, - "max_memory": self.problem_obj.memory_limit, - "test_case_id": self.problem_obj.test_case_id, - "output": output} + data = { + "language_config": language['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, + "output": output + } # TODO: try catch resp = self._request(urljoin(server.service_url, "/judge"), data=data) self.submission_obj.info = resp if resp['err']: self.submission_obj.result = JudgeStatus.COMPILE_ERROR else: - error_test_case = list(filter(lambda case: case['result'] != 0, resp)) + error_test_case = list(filter(lambda case: case['result'] != 0, resp['data'])) # 多个测试点全部正确AC,否则ACM模式下取第一个测试点状态 if not error_test_case: self.submission_obj.result = JudgeStatus.ACCEPTED - elif self.problem_obj.rule_tyle == ProblemRuleType.ACM: - self.submission_obj.result = error_test_case[0].result + elif self.problem_obj.rule_type == ProblemRuleType.ACM: + self.submission_obj.result = error_test_case[0]['result'] else: self.submission_obj.result = JudgeStatus.PARTIALLY_ACCEPTED self.submission_obj.save() @@ -92,37 +104,36 @@ class JudgeDispatcher(object): pass else: self.update_problem_status() - # 取redis中等待中的提交 - if self.redis_conn.llen(WAITING_QUEUE): - pass + process_pending_task(self.redis_conn) def compile_spj(self, service_url, src, spj_version, spj_compile_config, test_case_id): data = {"src": src, "spj_version": spj_version, - "spj_compile_config": spj_compile_config, "test_case_id": test_case_id} - return self._request(service_url + "/compile_spj", data=data) + "spj_compile_config": spj_compile_config, + "test_case_id": test_case_id} + return self._request(urljoin(service_url, "compile_spj"), data=data) def update_problem_status(self): with transaction.atomic(): - problem = Problem.objects.select_for_update().get(id=self.problem_obj.problem_id) - # 更新普通题目的计数器 - problem.add_submission_number() - - # 更新用户做题状态 + 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) - problems_status = UserProfile.objects.get(user=user).problem_status + # 更新提交计数器 + problem.add_submission_number() + 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"] = {} - # 增加用户提交计数器 - user.userprofile.add_submission_number() - # 之前状态不是ac, 现在是ac了 需要更新用户ac题目数量计数器,这里需要判重 if problems_status["problems"].get(str(problem.id), JudgeStatus.WRONG_ANSWER) != JudgeStatus.ACCEPTED: if self.submission_obj.result == JudgeStatus.ACCEPTED: - user.userprofile.add_accepted_problem_number() + user_profile.add_accepted_problem_number() problems_status["problems"][str(problem.id)] = JudgeStatus.ACCEPTED else: problems_status["problems"][str(problem.id)] = JudgeStatus.WRONG_ANSWER - user.problems_status = problems_status - user.save(update_fields=["problems_status"]) + user_profile.problems_status = problems_status + user_profile.save(update_fields=["problems_status"]) diff --git a/oj/__init__.py b/oj/__init__.py index e69de29..1ed42e9 100644 --- a/oj/__init__.py +++ b/oj/__init__.py @@ -0,0 +1,6 @@ +from __future__ import absolute_import, unicode_literals + +# Django starts so that shared_task will use this app. +from .celery import app as celery_app + +__all__ = ['celery_app'] diff --git a/oj/celery.py b/oj/celery.py new file mode 100644 index 0000000..b5edaa3 --- /dev/null +++ b/oj/celery.py @@ -0,0 +1,18 @@ +from __future__ import absolute_import, unicode_literals +import os +from celery import Celery +from django.conf import settings + +# set the default Django settings module for the 'celery' program. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oj.settings') + + +app = Celery('oj') + +# Using a string here means the worker will not have to +# pickle the object when using Windows. +app.config_from_object('django.conf:settings') + +# load task modules from all registered Django app configs. +app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) +# app.autodiscover_tasks() diff --git a/oj/db_router.py b/oj/db_router.py deleted file mode 100644 index 823d784..0000000 --- a/oj/db_router.py +++ /dev/null @@ -1,19 +0,0 @@ -class DBRouter(object): - def db_for_read(self, model, **hints): - if model._meta.app_label == "submission": - return "submission" - return "default" - - def db_for_write(self, model, **hints): - if model._meta.app_label == "submission": - return "submission" - return "default" - - def allow_relation(self, obj1, obj2, **hints): - return True - - def allow_migrate(self, db, app_label, model=None, **hints): - if app_label == "submission": - return db == app_label - else: - return db == "default" diff --git a/oj/local_settings.py b/oj/local_settings.py index f5212ff..dfad51b 100644 --- a/oj/local_settings.py +++ b/oj/local_settings.py @@ -34,16 +34,11 @@ CACHES = { } } -REDIS_CACHE = { - "host": "127.0.0.1", - "port": 6379, - "db": 1 -} - +# For celery REDIS_QUEUE = { "host": "127.0.0.1", "port": 6379, - "db": 2 + "db": 4 } DEBUG = True diff --git a/oj/settings.py b/oj/settings.py index c33efe6..c0d6cf7 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -164,8 +164,6 @@ BROKER_URL = 'redis://%s:%s/%s' % (REDIS_QUEUE["host"], str(REDIS_QUEUE["port"]) CELERY_ACCEPT_CONTENT = ["json"] CELERY_TASK_SERIALIZER = "json" -DATABASE_ROUTERS = ['oj.db_router.DBRouter'] - IMAGE_UPLOAD_DIR = os.path.join(BASE_DIR, 'upload/') # 用于限制用户恶意提交大量代码 diff --git a/problem/models.py b/problem/models.py index 61cc8f4..d0708ed 100644 --- a/problem/models.py +++ b/problem/models.py @@ -64,11 +64,11 @@ class AbstractProblem(models.Model): def add_submission_number(self): self.total_submit_number = models.F("total_submit_number") + 1 - self.save() + self.save(update_fields=['total_submit_number']) def add_ac_number(self): self.total_accepted_number = models.F("total_accepted_number") + 1 - self.save() + self.save(update_fields=['total_accepted_number']) class Problem(AbstractProblem): diff --git a/submission/migrations/0002_auto_20170509_1203.py b/submission/migrations/0002_auto_20170509_1203.py new file mode 100644 index 0000000..7ca5816 --- /dev/null +++ b/submission/migrations/0002_auto_20170509_1203.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2017-05-09 12:03 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submission', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='submission', + name='code', + field=models.TextField(), + ), + ] diff --git a/submission/models.py b/submission/models.py index 9c6d823..2687004 100644 --- a/submission/models.py +++ b/submission/models.py @@ -1,7 +1,6 @@ from django.db import models from jsonfield import JSONField -from utils.models import RichTextField from utils.shortcuts import rand_str @@ -25,7 +24,7 @@ class Submission(models.Model): problem_id = models.IntegerField(db_index=True) created_time = models.DateTimeField(auto_now_add=True) user_id = models.IntegerField(db_index=True) - code = RichTextField() + code = models.TextField() result = models.IntegerField(default=JudgeStatus.PENDING) # 判题结果的详细信息 info = JSONField(default={}) diff --git a/submission/tasks.py b/submission/tasks.py index ea9d6c8..eda9e0f 100644 --- a/submission/tasks.py +++ b/submission/tasks.py @@ -1,7 +1,8 @@ +from __future__ import absolute_import, unicode_literals from celery import shared_task -from judge.tasks import JudgeDispatcher +from judge.dispatcher import JudgeDispatcher @shared_task -def _judge(submission_obj, problem_obj): - return JudgeDispatcher(submission_obj, problem_obj).judge() +def judge_task(submission_id, problem_id): + JudgeDispatcher(submission_id, problem_id).judge() diff --git a/submission/views/oj.py b/submission/views/oj.py index c34ee5c..733e0df 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -4,53 +4,43 @@ 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 submission.tasks import judge_task 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: - return response.error("Failed") - - return response.success({"submission_id": submission.id}) class SubmissionAPI(APIView): @validate_serializer(CreateSubmissionSerializer) + # TODO: login # @login_required def post(self, request): + controller = BucketController(user_id=request.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 self.error("Please wait %d seconds" % int(bucket.expected_time() + 1)) + data = request.data - return _submit_code(self, request.user, data["problem_id"], data["language"], data["code"]) + try: + problem = Problem.objects.get(id=data['problem_id']) + except Problem.DoesNotExist: + return self.error("Problem not exist") + # TODO: user_id + submission = Submission.objects.create(user_id=1, + language=data['language'], + code=data['code'], + problem_id=problem.id) + judge_task.delay(submission.id, problem.id) + # JudgeDispatcher(submission.id, problem.id).judge() + return self.success({"submission_id": submission.id}) @login_required def get(self, request): From 219facf18535abed359d8315fb0dd48c595e234d Mon Sep 17 00:00:00 2001 From: zemal Date: Wed, 10 May 2017 17:46:59 +0800 Subject: [PATCH 014/106] Fix CI. --- conf/views.py | 8 ++++---- judge/dispatcher.py | 14 +++++++------- oj/__init__.py | 2 +- oj/celery.py | 8 ++++---- problem/models.py | 4 ++-- requirements.txt | 12 ------------ submission/views/oj.py | 6 +++--- 7 files changed, 21 insertions(+), 33 deletions(-) delete mode 100644 requirements.txt diff --git a/conf/views.py b/conf/views.py index bc03c01..8bb7797 100644 --- a/conf/views.py +++ b/conf/views.py @@ -1,11 +1,11 @@ import hashlib from django.utils import timezone -from django_redis import get_redis_connection +# from django_redis import get_redis_connection from account.decorators import super_admin_required from judge.languages import languages, spj_languages -from judge.dispatcher import process_pending_task +# from judge.dispatcher import process_pending_task from utils.api import APIView, CSRFExemptAPIView, validate_serializer from utils.shortcuts import rand_str @@ -129,8 +129,8 @@ class JudgeServerHeartbeatAPI(CSRFExemptAPIView): last_heartbeat=timezone.now(), ) # 新server上线 处理队列中的,防止没有新的提交而导致一直waiting - conn = get_redis_connection("JudgeQueue") - process_pending_task(conn) + # conn = get_redis_connection("JudgeQueue") + # process_pending_task(conn) return self.success() diff --git a/judge/dispatcher.py b/judge/dispatcher.py index 48fa33e..d3aea08 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -50,7 +50,7 @@ class JudgeDispatcher(object): def choose_judge_server(): with transaction.atomic(): # TODO: use more reasonable way - servers = JudgeServer.objects.select_for_update().all().order_by('task_number') + servers = JudgeServer.objects.select_for_update().all().order_by("task_number") servers = [s for s in servers if s.status == "normal"] if servers: server = servers[0] @@ -69,13 +69,13 @@ 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_obj.id, "problem_id": self.problem_obj.id} self.redis_conn.lpush(WAITING_QUEUE, json.dumps(data)) return - language = list(filter(lambda item: self.submission_obj.language == item['name'], languages))[0] + language = list(filter(lambda item: self.submission_obj.language == item["name"], languages))[0] data = { - "language_config": language['config'], + "language_config": language["config"], "src": self.submission_obj.code, "max_cpu_time": self.problem_obj.time_limit, "max_memory": 1024 * 1024 * self.problem_obj.memory_limit, @@ -85,15 +85,15 @@ class JudgeDispatcher(object): # TODO: try catch resp = self._request(urljoin(server.service_url, "/judge"), data=data) self.submission_obj.info = resp - if resp['err']: + if resp["err"]: self.submission_obj.result = JudgeStatus.COMPILE_ERROR else: - error_test_case = list(filter(lambda case: case['result'] != 0, resp['data'])) + error_test_case = list(filter(lambda case: case["result"] != 0, resp["data"])) # 多个测试点全部正确AC,否则ACM模式下取第一个测试点状态 if not error_test_case: self.submission_obj.result = JudgeStatus.ACCEPTED elif self.problem_obj.rule_type == ProblemRuleType.ACM: - self.submission_obj.result = error_test_case[0]['result'] + self.submission_obj.result = error_test_case[0]["result"] else: self.submission_obj.result = JudgeStatus.PARTIALLY_ACCEPTED self.submission_obj.save() diff --git a/oj/__init__.py b/oj/__init__.py index 1ed42e9..23fc183 100644 --- a/oj/__init__.py +++ b/oj/__init__.py @@ -3,4 +3,4 @@ from __future__ import absolute_import, unicode_literals # Django starts so that shared_task will use this app. from .celery import app as celery_app -__all__ = ['celery_app'] +__all__ = ["celery_app"] diff --git a/oj/celery.py b/oj/celery.py index b5edaa3..4f24c7e 100644 --- a/oj/celery.py +++ b/oj/celery.py @@ -3,15 +3,15 @@ import os from celery import Celery from django.conf import settings -# set the default Django settings module for the 'celery' program. -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oj.settings') +# set the default Django settings module for the "celery" program. +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "oj.settings") -app = Celery('oj') +app = Celery("oj") # Using a string here means the worker will not have to # pickle the object when using Windows. -app.config_from_object('django.conf:settings') +app.config_from_object("django.conf:settings") # load task modules from all registered Django app configs. app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) diff --git a/problem/models.py b/problem/models.py index d0708ed..0a8fda2 100644 --- a/problem/models.py +++ b/problem/models.py @@ -64,11 +64,11 @@ class AbstractProblem(models.Model): def add_submission_number(self): self.total_submit_number = models.F("total_submit_number") + 1 - self.save(update_fields=['total_submit_number']) + self.save(update_fields=["total_submit_number"]) def add_ac_number(self): self.total_accepted_number = models.F("total_accepted_number") + 1 - self.save(update_fields=['total_accepted_number']) + self.save(update_fields=["total_accepted_number"]) class Problem(AbstractProblem): diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index b48fa46..0000000 --- a/requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -django==1.9.6 -django-redis -djangorestframework==3.4.0 -otpauth -pillow -python-dateutil -celery -Envelopes -pytz -jsonfield -qrcode -requests diff --git a/submission/views/oj.py b/submission/views/oj.py index 733e0df..553e3ed 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -30,13 +30,13 @@ class SubmissionAPI(APIView): data = request.data try: - problem = Problem.objects.get(id=data['problem_id']) + problem = Problem.objects.get(id=data["problem_id"]) except Problem.DoesNotExist: return self.error("Problem not exist") # TODO: user_id submission = Submission.objects.create(user_id=1, - language=data['language'], - code=data['code'], + language=data["language"], + code=data["code"], problem_id=problem.id) judge_task.delay(submission.id, problem.id) # JudgeDispatcher(submission.id, problem.id).judge() From 4943f3b39e06c01f2ce98e1638152c8419290eec Mon Sep 17 00:00:00 2001 From: zemal Date: Wed, 10 May 2017 19:40:26 +0800 Subject: [PATCH 015/106] Test redis in travis-ci --- .travis.yml | 1 + conf/views.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index f51b0b5..1e3f964 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: python python: - "3.5" install: + - sudo apt-get install -qq redis-server && redis-server & - pip install -r deploy/requirements.txt - mkdir log test_case upload - cp oj/custom_settings.example.py oj/custom_settings.py diff --git a/conf/views.py b/conf/views.py index 8bb7797..bc03c01 100644 --- a/conf/views.py +++ b/conf/views.py @@ -1,11 +1,11 @@ import hashlib from django.utils import timezone -# from django_redis import get_redis_connection +from django_redis import get_redis_connection from account.decorators import super_admin_required from judge.languages import languages, spj_languages -# from judge.dispatcher import process_pending_task +from judge.dispatcher import process_pending_task from utils.api import APIView, CSRFExemptAPIView, validate_serializer from utils.shortcuts import rand_str @@ -129,8 +129,8 @@ class JudgeServerHeartbeatAPI(CSRFExemptAPIView): last_heartbeat=timezone.now(), ) # 新server上线 处理队列中的,防止没有新的提交而导致一直waiting - # conn = get_redis_connection("JudgeQueue") - # process_pending_task(conn) + conn = get_redis_connection("JudgeQueue") + process_pending_task(conn) return self.success() From bc6d80d7458d2c053e8be7fa117b7e4768a504b5 Mon Sep 17 00:00:00 2001 From: Chiaki Date: Mon, 15 May 2017 13:09:54 +0800 Subject: [PATCH 016/106] Daily commit --- submission/urls/oj.py | 6 +- submission/views/oj.py | 138 +++++++++++++++++++++++++++++------------ utils/shortcuts.py | 2 +- 3 files changed, 103 insertions(+), 43 deletions(-) diff --git a/submission/urls/oj.py b/submission/urls/oj.py index 9f54c05..947aadf 100644 --- a/submission/urls/oj.py +++ b/submission/urls/oj.py @@ -1,9 +1,11 @@ from django.conf.urls import url -from ..views.oj import (SubmissionAPI, SubmissionListAPI) +from ..views.oj import (SubmissionAPI, SubmissionListAPI, SubmissionDetailAPI) urlpatterns = [ - url(r"^submission/?$", SubmissionAPI.as_view(), name="submissiob_api"), + url(r"^submission/?$", SubmissionAPI.as_view(), name="submission_api"), + url(r"^submission/(?P\w+)/?$", SubmissionDetailAPI.as_view(), name="submission_detail_api"), url(r"^submissions/?$", SubmissionListAPI.as_view(), name="submission_list_api"), url(r"^submissions/(?P\d+)/?$", SubmissionListAPI.as_view(), name="submission_list_page_api"), + # MyProblemSubmissionListAPI ] diff --git a/submission/views/oj.py b/submission/views/oj.py index 553e3ed..1d4d937 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -8,39 +8,45 @@ from submission.tasks import judge_task 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 ..models import Submission, JudgeStatus from ..serializers import CreateSubmissionSerializer +def _submit(response, user, problem_id, language, code): + # TODO: 预设默认值,需修改 + 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) + + judge_task.delay(submission.id, problem.id) + return response.success({"submission_id": submission.id}) + + class SubmissionAPI(APIView): @validate_serializer(CreateSubmissionSerializer) # TODO: login # @login_required def post(self, request): - controller = BucketController(user_id=request.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 self.error("Please wait %d seconds" % int(bucket.expected_time() + 1)) - data = request.data - try: - problem = Problem.objects.get(id=data["problem_id"]) - except Problem.DoesNotExist: - return self.error("Problem not exist") - # TODO: user_id - submission = Submission.objects.create(user_id=1, - language=data["language"], - code=data["code"], - problem_id=problem.id) - judge_task.delay(submission.id, problem.id) - # JudgeDispatcher(submission.id, problem.id).judge() - return self.success({"submission_id": submission.id}) + return _submit(self, request.user, data["problem_id"], data["language"], data["code"]) @login_required def get(self, request): @@ -62,6 +68,7 @@ class MyProblemSubmissionListAPI(APIView): """ 用户单个题目的全部提交列表 """ + def get(self, request): problem_id = request.GET.get("problem_id") try: @@ -81,6 +88,7 @@ class SubmissionListAPI(APIView): """ 所有提交的列表 """ + def get(self, request, **kwargs): submission_filter = {"my": None, "user_id": None} show_all = False @@ -102,7 +110,6 @@ class SubmissionListAPI(APIView): 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) @@ -140,19 +147,70 @@ class SubmissionListAPI(APIView): 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 + 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}) + return self.success({"submissions": submissions.object_list, "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}) + + +def _get_submission(submission_id, user): + """ + 用户权限判断 + """ + submission = Submission.objects.get(id=submission_id) + # Super Admin / Owner / Share + if user.admin_type == AdminType.SUPER_ADMIN or submission.user_id == user.id: + return {"submission": submission, "can_share": True} + if submission.contest_id: + # 比赛部分 + pass + if submission.shared: + return {"submission": submission, "can_share": False} + else: + raise Submission.DoesNotExist + + +class SubmissionDetailAPI(APIView): + """ + 单个提交页面详情 + """ + + def get(self, request, **kwargs): + try: + result = _get_submission(kwargs["submission_id"], request.user) + submission = result["submission"] + except Submission.DoesNotExist: + return self.error("Submission not exist") + + # TODO: Contest + try: + if submission.contest_id: + # problem = ContestProblem.objects.get(id=submission.problem_id, visible=True) + pass + else: + problem = Problem.objects.get(id=submission.problem_id, visible=True) + except (Problem.DoesNotExist, ): + return self.error("Submission not exist") + + if submission.result in [JudgeStatus.COMPILE_ERROR, JudgeStatus.SYSTEM_ERROR, JudgeStatus.PENDING]: + info = submission.info + else: + info = submission.info + if "test_case" in info[0]: + info = sorted(info, key=lambda x: x["test_case"]) + + user = User.objects.get(id=submission.user_id) + return self.success({"submission": submission, "problem": problem, "info": info, + "user": user, "can_share": result["can_share"]}) diff --git a/utils/shortcuts.py b/utils/shortcuts.py index a7fcd11..38a4edb 100644 --- a/utils/shortcuts.py +++ b/utils/shortcuts.py @@ -49,7 +49,7 @@ def rand_str(length=32, type="lower_hex"): 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(): + for k, v in kv_data.items(): if ignore_none is True and kv_data[k] is None: continue if query_string != "": From 099b48497bf8f25ee67d24c98640245848eb6e40 Mon Sep 17 00:00:00 2001 From: zemal Date: Mon, 15 May 2017 16:42:15 +0800 Subject: [PATCH 017/106] =?UTF-8?q?=E6=B7=BB=E5=8A=A0submission=20status?= =?UTF-8?q?=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- judge/dispatcher.py | 4 ++++ submission/serializers.py | 9 +++++++++ submission/views/oj.py | 22 ++++++++++------------ 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/judge/dispatcher.py b/judge/dispatcher.py index d3aea08..594213b 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -3,6 +3,7 @@ import requests import hashlib import logging from urllib.parse import urljoin +from functools import reduce from django.db import transaction from django.db.models import F @@ -82,6 +83,8 @@ class JudgeDispatcher(object): "test_case_id": self.problem_obj.test_case_id, "output": output } + self.submission_obj.result = JudgeStatus.JUDGING + self.submission_obj.save() # TODO: try catch resp = self._request(urljoin(server.service_url, "/judge"), data=data) self.submission_obj.info = resp @@ -92,6 +95,7 @@ class JudgeDispatcher(object): # 多个测试点全部正确AC,否则ACM模式下取第一个测试点状态 if not error_test_case: self.submission_obj.result = JudgeStatus.ACCEPTED + self.submission_obj.accepted_time = reduce(lambda x, y: x + y["cpu_time"], resp["data"], 0) elif self.problem_obj.rule_type == ProblemRuleType.ACM: self.submission_obj.result = error_test_case[0]["result"] else: diff --git a/submission/serializers.py b/submission/serializers.py index 32fe167..da43945 100644 --- a/submission/serializers.py +++ b/submission/serializers.py @@ -1,3 +1,4 @@ +from .models import Submission from utils.api import serializers from judge.languages import language_names @@ -6,3 +7,11 @@ class CreateSubmissionSerializer(serializers.Serializer): problem_id = serializers.IntegerField() language = serializers.ChoiceField(choices=language_names) code = serializers.CharField(max_length=20000) + + +class SubmissionModelSerializer(serializers.ModelSerializer): + info = serializers.JSONField() + accepted_info = serializers.JSONField() + + class Meta: + model = Submission diff --git a/submission/views/oj.py b/submission/views/oj.py index 1d4d937..0f2f516 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -5,14 +5,15 @@ from account.decorators import login_required from account.models import AdminType, User from problem.models import Problem from submission.tasks import judge_task +# from judge.dispatcher import JudgeDispatcher from utils.api import APIView, validate_serializer from utils.shortcuts import build_query_string from utils.throttling import TokenBucket, BucketController from ..models import Submission, JudgeStatus -from ..serializers import CreateSubmissionSerializer +from ..serializers import CreateSubmissionSerializer, SubmissionModelSerializer -def _submit(response, user, problem_id, language, code): +def _submit(response, user, problem_id, language, code, contest_id=None): # TODO: 预设默认值,需修改 controller = BucketController(user_id=user.id, redis_conn=get_redis_connection("Throttling"), @@ -34,34 +35,31 @@ def _submit(response, user, problem_id, language, code): submission = Submission.objects.create(user_id=user.id, language=language, code=code, - problem_id=problem.id) - + problem_id=problem.id, + contest_id=contest_id) + # 暂时保留 方便排错 + # JudgeDispatcher(submission.id, problem.id).judge() judge_task.delay(submission.id, problem.id) return response.success({"submission_id": submission.id}) class SubmissionAPI(APIView): @validate_serializer(CreateSubmissionSerializer) - # TODO: login - # @login_required + @login_required def post(self, request): data = request.data return _submit(self, request.user, data["problem_id"], data["language"], data["code"]) @login_required def get(self, request): - submission_id = request.GET.get("submission_id") + submission_id = request.GET.get("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) + return self.success(SubmissionModelSerializer(submission).data) class MyProblemSubmissionListAPI(APIView): From 55f5601eb0e5ea78ce354df5007a8a739ca92ab1 Mon Sep 17 00:00:00 2001 From: zemal Date: Tue, 20 Jun 2017 20:35:00 +0800 Subject: [PATCH 018/106] =?UTF-8?q?=E6=94=AF=E6=8C=81spj=5Fjudge=EF=BC=9B?= =?UTF-8?q?=20AC=E6=97=B6=E9=97=B4=E4=BF=AE=E6=AD=A3=E4=B8=BA=E5=A4=9A?= =?UTF-8?q?=E4=B8=AA=E6=B5=8B=E8=AF=95=E7=82=B9=E8=BF=90=E8=A1=8C=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E4=B8=AD=E6=9C=80=E9=95=BF=E7=9A=84=E9=82=A3=E4=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- judge/dispatcher.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/judge/dispatcher.py b/judge/dispatcher.py index 594213b..bdc9a0c 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -3,7 +3,6 @@ import requests import hashlib import logging from urllib.parse import urljoin -from functools import reduce from django.db import transaction from django.db.models import F @@ -74,17 +73,28 @@ class JudgeDispatcher(object): self.redis_conn.lpush(WAITING_QUEUE, json.dumps(data)) return - language = list(filter(lambda item: self.submission_obj.language == item["name"], languages))[0] + sub_config = list(filter(lambda item: self.submission_obj.language == item["name"], languages))[0] + spj_config = {} + if self.problem_obj.spj_code: + for lang in languages: + if lang["name"] == self.problem_obj.spj_language: + spj_config = lang["spj"] + break data = { - "language_config": language["config"], + "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, - "output": output + "output": output, + "spj_version": self.problem_obj.spj_version, + "spj_config": spj_config.get("config"), + "spj_compile_config": spj_config.get("compile"), + "spj_src": self.problem_obj.spj_code } self.submission_obj.result = JudgeStatus.JUDGING self.submission_obj.save() + # TODO: try catch resp = self._request(urljoin(server.service_url, "/judge"), data=data) self.submission_obj.info = resp @@ -92,10 +102,11 @@ class JudgeDispatcher(object): self.submission_obj.result = JudgeStatus.COMPILE_ERROR else: error_test_case = list(filter(lambda case: case["result"] != 0, resp["data"])) - # 多个测试点全部正确AC,否则ACM模式下取第一个测试点状态 + # 多个测试点全部正确AC,否则 ACM模式下取第一个错误的测试点状态, OI模式对应为部分正确 if not error_test_case: self.submission_obj.result = JudgeStatus.ACCEPTED - self.submission_obj.accepted_time = reduce(lambda x, y: x + y["cpu_time"], resp["data"], 0) + # AC 用时保存为多个测试点中最长的那个 + self.submission_obj.accepted_time = max([x["cpu_time"] for x in resp["data"]]) elif self.problem_obj.rule_type == ProblemRuleType.ACM: self.submission_obj.result = error_test_case[0]["result"] else: From 78a8999b44d5561f4e467355236e23a439683726 Mon Sep 17 00:00:00 2001 From: zemal Date: Thu, 22 Jun 2017 14:10:32 +0800 Subject: [PATCH 019/106] Add contestAPI. --- contest/tests.py | 30 +++++++++++++++++++++++++----- contest/urls/admin.py | 2 +- contest/urls/oj.py | 6 ++++-- contest/views/oj.py | 20 +++++++++++++++++++- 4 files changed, 49 insertions(+), 9 deletions(-) diff --git a/contest/tests.py b/contest/tests.py index 8a8134f..93b839f 100644 --- a/contest/tests.py +++ b/contest/tests.py @@ -16,10 +16,10 @@ DEFAULT_CONTEST_DATA = {"title": "test title", "description": "test description" "visible": True, "real_time_rank": True} -class ContestAPITest(APITestCase): +class ContestAdminAPITest(APITestCase): def setUp(self): self.create_super_admin() - self.url = self.reverse("contest_api") + self.url = self.reverse("contest_admin_api") self.data = DEFAULT_CONTEST_DATA def test_create_contest(self): @@ -55,6 +55,26 @@ class ContestAPITest(APITestCase): self.assertSuccess(response) +class ContestAPITest(APITestCase): + def setUp(self): + self.create_admin() + self.url = self.reverse("contest_api") + + def create_contest(self): + url = self.reverse("contest_admin_api") + return self.client.post(url, data=DEFAULT_CONTEST_DATA) + + def test_get_contest_list(self): + self.create_contest() + response = self.client.get(self.url) + self.assertSuccess(response) + + def test_get_one_contest(self): + contest_id = self.create_contest().data["data"]["id"] + response = self.client.get("{}?id={}".format(self.url, contest_id)) + self.assertSuccess(response) + + class ContestAnnouncementAPITest(APITestCase): def setUp(self): self.create_super_admin() @@ -63,7 +83,7 @@ class ContestAnnouncementAPITest(APITestCase): self.data = {"title": "test title", "content": "test content", "contest_id": contest_id} def create_contest(self): - url = self.reverse("contest_api") + url = self.reverse("contest_admin_api") data = DEFAULT_CONTEST_DATA return self.client.post(url, data=data) @@ -92,10 +112,10 @@ class ContestAnnouncementAPITest(APITestCase): class ContestAnnouncementListAPITest(APITestCase): def setUp(self): self.create_super_admin() - self.url = self.reverse("contest_list_api") + self.url = self.reverse("contest_announcement_api") def create_contest_announcements(self): - contest_id = self.client.post(self.reverse("contest_api"), data=DEFAULT_CONTEST_DATA).data["data"]["id"] + contest_id = self.client.post(self.reverse("contest_admin_api"), data=DEFAULT_CONTEST_DATA).data["data"]["id"] url = self.reverse("contest_announcement_admin_api") self.client.post(url, data={"title": "test title1", "content": "test content1", "contest_id": contest_id}) self.client.post(url, data={"title": "test title2", "content": "test content2", "contest_id": contest_id}) diff --git a/contest/urls/admin.py b/contest/urls/admin.py index 2a7705a..5a8bc75 100644 --- a/contest/urls/admin.py +++ b/contest/urls/admin.py @@ -3,6 +3,6 @@ from django.conf.urls import url from ..views.admin import ContestAnnouncementAPI, ContestAPI urlpatterns = [ - url(r"^contest/?$", ContestAPI.as_view(), name="contest_api"), + url(r"^contest/?$", ContestAPI.as_view(), name="contest_admin_api"), url(r"^contest/announcement/?$", ContestAnnouncementAPI.as_view(), name="contest_announcement_admin_api") ] diff --git a/contest/urls/oj.py b/contest/urls/oj.py index bfc80b8..283bdff 100644 --- a/contest/urls/oj.py +++ b/contest/urls/oj.py @@ -1,7 +1,9 @@ from django.conf.urls import url -from ..views.oj import ContestAnnouncementListAPI +from ..views.oj import ContestAnnouncementListAPI, ContestListAPI urlpatterns = [ - url(r"^contest/?$", ContestAnnouncementListAPI.as_view(), name="contest_list_api"), + url(r"^contest/?$", ContestListAPI.as_view(), name="contest_api"), + url(r"^contest/announcement/?$", ContestAnnouncementListAPI.as_view(), name="contest_announcement_api"), + ] diff --git a/contest/views/oj.py b/contest/views/oj.py index e9ffe81..8bff965 100644 --- a/contest/views/oj.py +++ b/contest/views/oj.py @@ -1,7 +1,8 @@ from utils.api import APIView -from ..models import ContestAnnouncement +from ..models import ContestAnnouncement, Contest from ..serializers import ContestAnnouncementSerializer +from ..serializers import ContestSerializer class ContestAnnouncementListAPI(APIView): @@ -14,3 +15,20 @@ class ContestAnnouncementListAPI(APIView): if max_id: data = data.filter(id__gt=max_id) return self.success(ContestAnnouncementSerializer(data, many=True).data) + + +class ContestListAPI(APIView): + def get(self, request): + contest_id = request.GET.get("id") + if contest_id: + try: + contest = Contest.objects.get(id=contest_id, visible=True) + return self.success(ContestSerializer(contest).data) + except Contest.DoesNotExist: + return self.error("Contest Doesn't exist.") + + contests = Contest.objects.filter(visible=True) + keyword = request.GET.get("keyword") + if keyword: + contests = contests.filter(title__contains=keyword) + return self.success(self.paginate_data(request, contests, ContestSerializer)) From b931724c9b3243ed069fc16d97e2b3e4b7d6efe8 Mon Sep 17 00:00:00 2001 From: zemal Date: Tue, 4 Jul 2017 10:26:02 +0800 Subject: [PATCH 020/106] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BF=A1=E6=81=AFapi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/serializers.py | 1 + account/urls/user.py | 3 +-- account/views/user.py | 34 ++++++++++------------------------ utils/api/api.py | 1 + 4 files changed, 13 insertions(+), 26 deletions(-) diff --git a/account/serializers.py b/account/serializers.py index 68eca7c..6c50194 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -35,6 +35,7 @@ class UserSerializer(serializers.ModelSerializer): class UserProfileSerializer(serializers.ModelSerializer): + user = UserSerializer() class Meta: model = UserProfile diff --git a/account/urls/user.py b/account/urls/user.py index 921faf6..8ca0adf 100644 --- a/account/urls/user.py +++ b/account/urls/user.py @@ -1,11 +1,10 @@ from django.conf.urls import url from ..views.user import (SSOAPI, AvatarUploadAPI, TwoFactorAuthAPI, - UserNameAPI, UserInfoAPI, UserProfileAPI) + UserNameAPI, 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"), url(r"^sso/?$", SSOAPI.as_view(), name="sso_api"), diff --git a/account/views/user.py b/account/views/user.py index 0c0ea11..fa97a80 100644 --- a/account/views/user.py +++ b/account/views/user.py @@ -15,11 +15,12 @@ from utils.shortcuts import rand_str from ..decorators import login_required from ..models import User, UserProfile from ..serializers import (SSOSerializer, TwoFactorAuthCodeSerializer, - UserSerializer, UserProfileSerializer, + UserProfileSerializer, EditUserProfileSerializer, AvatarUploadForm) class UserNameAPI(APIView): + @method_decorator(ensure_csrf_cookie) def get(self, request): """ Return Username to valid login status @@ -37,37 +38,22 @@ class UserNameAPI(APIView): }) -class UserInfoAPI(APIView): - # @login_required - @method_decorator(ensure_csrf_cookie) - def get(self, request, **kwargs): - """ - Return user info api - """ - try: - user = User.objects.get(username=kwargs["username"]) - except User.DoesNotExist: - return self.error("User does not exist") - profile = UserProfile.objects.get(user=user) - dit = UserProfileSerializer(profile).data - dit["user"] = UserSerializer(user).data - return self.success(dit) - - class UserProfileAPI(APIView): @login_required - def get(self, request): + def get(self, request, **kwargs): """ - Return user info api + Return user info according username or user_id """ + username = request.GET.get("username") try: - user = User.objects.get(id=request.user.id) + if username: + user = User.objects.get(username=username) + else: + user = request.user except User.DoesNotExist: return self.error("User does not exist") profile = UserProfile.objects.get(user=user) - dit = UserProfileSerializer(profile).data - dit["user"] = UserSerializer(user).data - return self.success(dit) + return self.success(UserProfileSerializer(profile).data) @validate_serializer(EditUserProfileSerializer) @login_required diff --git a/utils/api/api.py b/utils/api/api.py index 78018cd..197026a 100644 --- a/utils/api/api.py +++ b/utils/api/api.py @@ -65,6 +65,7 @@ class APIView(View): for parser in self.request_parsers: if content_type.startswith(parser.content_type): break + # else means the for loop is not interrupted by break else: raise ValueError("unknown content_type '%s'" % content_type) if body: From 12ee85ef8f3ebc975f083b2193be42f5c50bcd33 Mon Sep 17 00:00:00 2001 From: zemal Date: Tue, 4 Jul 2017 17:32:50 +0800 Subject: [PATCH 021/106] =?UTF-8?q?=E4=BF=AE=E6=94=B9submission=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- submission/urls/oj.py | 6 +----- submission/views/oj.py | 42 +++++++++++++++--------------------------- 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/submission/urls/oj.py b/submission/urls/oj.py index 947aadf..4b45c47 100644 --- a/submission/urls/oj.py +++ b/submission/urls/oj.py @@ -3,9 +3,5 @@ from django.conf.urls import url from ..views.oj import (SubmissionAPI, SubmissionListAPI, SubmissionDetailAPI) urlpatterns = [ - url(r"^submission/?$", SubmissionAPI.as_view(), name="submission_api"), - url(r"^submission/(?P\w+)/?$", SubmissionDetailAPI.as_view(), name="submission_detail_api"), - url(r"^submissions/?$", SubmissionListAPI.as_view(), name="submission_list_api"), - url(r"^submissions/(?P\d+)/?$", SubmissionListAPI.as_view(), name="submission_list_page_api"), - # MyProblemSubmissionListAPI + url(r"^submissions/?$", SubmissionAPI.as_view(), name="submission_api"), ] diff --git a/submission/views/oj.py b/submission/views/oj.py index 0f2f516..8160401 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -37,7 +37,7 @@ 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) return response.success({"submission_id": submission.id}) @@ -53,40 +53,28 @@ class SubmissionAPI(APIView): @login_required def get(self, request): submission_id = request.GET.get("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") - return self.success(SubmissionModelSerializer(submission).data) + if submission_id: + try: + submission = Submission.objects.get(id=submission_id, user_id=request.user.id) + except Submission.DoesNotExist: + return self.error("Submission not exist") + return self.success(SubmissionModelSerializer(submission).data) + problem_id = request.GET.get('problem_id') + subs = Submission.objects.filter(contest_id__isnull=True) + if problem_id: + subs = subs.filter(problem_id=problem_id) -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}) + if request.GET.get('myself'): + subs = subs.filter(user_id=request.user.id) + # todo: paginate + return self.success(SubmissionModelSerializer(subs, many=True).data) class SubmissionListAPI(APIView): """ 所有提交的列表 """ - def get(self, request, **kwargs): submission_filter = {"my": None, "user_id": None} show_all = False From 91eb7b5bb6ef8dec2d2017eaace23b2a2762cba5 Mon Sep 17 00:00:00 2001 From: zemal Date: Tue, 4 Jul 2017 18:03:45 +0800 Subject: [PATCH 022/106] fix ci --- submission/urls/oj.py | 2 +- submission/views/oj.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/submission/urls/oj.py b/submission/urls/oj.py index 4b45c47..27e48bf 100644 --- a/submission/urls/oj.py +++ b/submission/urls/oj.py @@ -1,6 +1,6 @@ from django.conf.urls import url -from ..views.oj import (SubmissionAPI, SubmissionListAPI, SubmissionDetailAPI) +from ..views.oj import SubmissionAPI urlpatterns = [ url(r"^submissions/?$", SubmissionAPI.as_view(), name="submission_api"), diff --git a/submission/views/oj.py b/submission/views/oj.py index 8160401..f13e3c9 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -60,12 +60,12 @@ class SubmissionAPI(APIView): return self.error("Submission not exist") return self.success(SubmissionModelSerializer(submission).data) - problem_id = request.GET.get('problem_id') + problem_id = request.GET.get("problem_id") subs = Submission.objects.filter(contest_id__isnull=True) if problem_id: subs = subs.filter(problem_id=problem_id) - if request.GET.get('myself'): + if request.GET.get("myself"): subs = subs.filter(user_id=request.user.id) # todo: paginate return self.success(SubmissionModelSerializer(subs, many=True).data) From 62274224a97e29a27634d4c9b3436d4dde799737 Mon Sep 17 00:00:00 2001 From: zemal Date: Tue, 4 Jul 2017 20:59:25 +0800 Subject: [PATCH 023/106] =?UTF-8?q?problem=E6=94=B9=E7=94=A8=5Fid=E4=BD=9C?= =?UTF-8?q?=E4=B8=BA=E6=90=9C=E7=B4=A2=E7=94=A8=E4=B8=BB=E9=94=AE=EF=BC=9B?= =?UTF-8?q?submission=E6=9B=B4=E5=8A=A0statistic=5Finfo=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- judge/dispatcher.py | 6 +++-- problem/views/oj.py | 2 +- .../migrations/0003_auto_20170704_1243.py | 24 +++++++++++++++++++ submission/models.py | 5 ++-- submission/serializers.py | 8 ++++++- submission/views/oj.py | 11 ++++++--- 6 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 submission/migrations/0003_auto_20170704_1243.py diff --git a/judge/dispatcher.py b/judge/dispatcher.py index bdc9a0c..f96fd4c 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -101,12 +101,14 @@ class JudgeDispatcher(object): if resp["err"]: self.submission_obj.result = JudgeStatus.COMPILE_ERROR 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"]]) + 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 - # AC 用时保存为多个测试点中最长的那个 - self.submission_obj.accepted_time = max([x["cpu_time"] for x in resp["data"]]) elif self.problem_obj.rule_type == ProblemRuleType.ACM: self.submission_obj.result = error_test_case[0]["result"] else: diff --git a/problem/views/oj.py b/problem/views/oj.py index 2edde23..a687520 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -16,7 +16,7 @@ class ProblemAPI(APIView): problem_id = request.GET.get("id") if problem_id: try: - problem = Problem.objects.get(id=problem_id, visible=True) + problem = Problem.objects.get(_id=problem_id, visible=True) return self.success(ProblemSerializer(problem).data) except Problem.DoesNotExist: return self.error("Problem does not exist") diff --git a/submission/migrations/0003_auto_20170704_1243.py b/submission/migrations/0003_auto_20170704_1243.py new file mode 100644 index 0000000..58fa83c --- /dev/null +++ b/submission/migrations/0003_auto_20170704_1243.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2017-07-04 12:43 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('submission', '0002_auto_20170509_1203'), + ] + + operations = [ + migrations.RenameField( + model_name='submission', + old_name='accepted_info', + new_name='statistic_info', + ), + migrations.RemoveField( + model_name='submission', + name='accepted_time', + ), + ] diff --git a/submission/models.py b/submission/models.py index 2687004..d690cfe 100644 --- a/submission/models.py +++ b/submission/models.py @@ -30,9 +30,8 @@ class Submission(models.Model): 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={}) + # 存储该提交所用时间和内存值,方便提交列表显示 + statistic_info = JSONField(default={}) class Meta: db_table = "submission" diff --git a/submission/serializers.py b/submission/serializers.py index da43945..8b79970 100644 --- a/submission/serializers.py +++ b/submission/serializers.py @@ -1,4 +1,5 @@ from .models import Submission +from account.models import User from utils.api import serializers from judge.languages import language_names @@ -10,8 +11,13 @@ class CreateSubmissionSerializer(serializers.Serializer): class SubmissionModelSerializer(serializers.ModelSerializer): + username = serializers.SerializerMethodField() info = serializers.JSONField() - accepted_info = serializers.JSONField() + statistic_info = serializers.JSONField() class Meta: model = Submission + + @staticmethod + def get_username(obj): + return User.objects.get(id=obj.user_id).username diff --git a/submission/views/oj.py b/submission/views/oj.py index f13e3c9..57172b7 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -28,14 +28,14 @@ def _submit(response, user, problem_id, language, code, contest_id=None): return response.error("Please wait %d seconds" % int(bucket.expected_time() + 1)) try: - problem = Problem.objects.get(id=problem_id) + 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, + problem_id=problem._id, contest_id=contest_id) # todo 暂时保留 方便排错 # JudgeDispatcher(submission.id, problem.id).judge() @@ -60,8 +60,13 @@ class SubmissionAPI(APIView): return self.error("Submission not exist") return self.success(SubmissionModelSerializer(submission).data) + contest_id = request.GET.get("contest_id") + if contest_id: + subs = Submission.objects.filter(contest_id__isnull=False) + else: + subs = Submission.objects.filter(contest_id__isnull=True) + problem_id = request.GET.get("problem_id") - subs = Submission.objects.filter(contest_id__isnull=True) if problem_id: subs = subs.filter(problem_id=problem_id) From 35f6c9c4a75326667edaeab39c00d2d1ff9a3ec5 Mon Sep 17 00:00:00 2001 From: zemal Date: Wed, 5 Jul 2017 21:09:14 +0800 Subject: [PATCH 024/106] =?UTF-8?q?=E6=B7=BB=E5=8A=A0submission=5Flist=20?= =?UTF-8?q?=E5=92=8C=20submission=20details=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- submission/models.py | 6 ++ submission/serializers.py | 32 ++++++++++- submission/urls/oj.py | 5 +- submission/views/oj.py | 117 ++++++++------------------------------ 4 files changed, 65 insertions(+), 95 deletions(-) diff --git a/submission/models.py b/submission/models.py index d690cfe..74a7e87 100644 --- a/submission/models.py +++ b/submission/models.py @@ -1,5 +1,6 @@ from django.db import models from jsonfield import JSONField +from account.models import AdminType from utils.shortcuts import rand_str @@ -33,6 +34,11 @@ class Submission(models.Model): # 存储该提交所用时间和内存值,方便提交列表显示 statistic_info = JSONField(default={}) + def check_user_permission(self, user): + return self.user_id == user.id or \ + self.shared is True or \ + user.admin_type == AdminType.SUPER_ADMIN + class Meta: db_table = "submission" diff --git a/submission/serializers.py b/submission/serializers.py index 8b79970..1c58e39 100644 --- a/submission/serializers.py +++ b/submission/serializers.py @@ -11,13 +11,43 @@ class CreateSubmissionSerializer(serializers.Serializer): class SubmissionModelSerializer(serializers.ModelSerializer): - username = serializers.SerializerMethodField() info = serializers.JSONField() statistic_info = serializers.JSONField() class Meta: model = Submission + +# 不显示submission info详情的serializer +class SubmissionSafeSerializer(serializers.ModelSerializer): + username = serializers.SerializerMethodField() + statistic_info = serializers.JSONField() + + class Meta: + model = Submission + exclude = ('info', 'contest_id') + @staticmethod def get_username(obj): return User.objects.get(id=obj.user_id).username + + +class SubmissionListSerializer(SubmissionSafeSerializer): + username = serializers.SerializerMethodField() + statistic_info = serializers.JSONField() + show_link = serializers.SerializerMethodField() + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop('user', None) + super().__init__(*args, **kwargs) + + class Meta: + model = Submission + exclude = ('info', 'contest_id', 'code') + + def get_show_link(self, obj): + return obj.check_user_permission(self.user) + + @staticmethod + def get_username(obj): + return User.objects.get(id=obj.user_id).username \ No newline at end of file diff --git a/submission/urls/oj.py b/submission/urls/oj.py index 27e48bf..e569574 100644 --- a/submission/urls/oj.py +++ b/submission/urls/oj.py @@ -1,7 +1,8 @@ from django.conf.urls import url -from ..views.oj import SubmissionAPI +from ..views.oj import SubmissionAPI, SubmissionListAPI urlpatterns = [ - url(r"^submissions/?$", SubmissionAPI.as_view(), name="submission_api"), + url(r"^submission/?$", SubmissionAPI.as_view(), name="submission_api"), + url(r"^submissions/?$", SubmissionListAPI.as_view(), name="submission_list_api"), ] diff --git a/submission/views/oj.py b/submission/views/oj.py index 57172b7..1066899 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -1,16 +1,16 @@ -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 problem.models import Problem, ProblemRuleType from submission.tasks import judge_task # from judge.dispatcher import JudgeDispatcher from utils.api import APIView, validate_serializer -from utils.shortcuts import build_query_string from utils.throttling import TokenBucket, BucketController from ..models import Submission, JudgeStatus from ..serializers import CreateSubmissionSerializer, SubmissionModelSerializer +from ..serializers import SubmissionSafeSerializer, SubmissionListSerializer + def _submit(response, user, problem_id, language, code, contest_id=None): @@ -53,16 +53,29 @@ class SubmissionAPI(APIView): @login_required def get(self, request): submission_id = request.GET.get("id") - if submission_id: - try: - submission = Submission.objects.get(id=submission_id, user_id=request.user.id) - except Submission.DoesNotExist: - return self.error("Submission not exist") - return self.success(SubmissionModelSerializer(submission).data) + if not submission_id: + return self.error("Parameter id doesn't exist.") + try: + submission = Submission.objects.get(id=submission_id, user_id=request.user.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.") + # check problem'rule is ACM or IO. + if Problem.objects.filter(_id=submission.problem_id, + visible=True, + rule_type=ProblemRuleType.ACM + ).exists(): + return self.success(SubmissionSafeSerializer(submission).data) + return self.success(SubmissionModelSerializer(submission).data) + + +class SubmissionListAPI(APIView): + def get(self, request): contest_id = request.GET.get("contest_id") if contest_id: - subs = Submission.objects.filter(contest_id__isnull=False) + subs = Submission.objects.filter(contest_id=contest_id) else: subs = Submission.objects.filter(contest_id__isnull=True) @@ -73,87 +86,7 @@ class SubmissionAPI(APIView): if request.GET.get("myself"): subs = subs.filter(user_id=request.user.id) # todo: paginate - return self.success(SubmissionModelSerializer(subs, many=True).data) - - -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.object_list, "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}) + return self.success(SubmissionListSerializer(subs, many=True, user=request.user).data) def _get_submission(submission_id, user): @@ -192,7 +125,7 @@ class SubmissionDetailAPI(APIView): pass else: problem = Problem.objects.get(id=submission.problem_id, visible=True) - except (Problem.DoesNotExist, ): + except (Problem.DoesNotExist,): return self.error("Submission not exist") if submission.result in [JudgeStatus.COMPILE_ERROR, JudgeStatus.SYSTEM_ERROR, JudgeStatus.PENDING]: From e0369e68657670bb8f3c0a7baab6e4fb4ad4d2f9 Mon Sep 17 00:00:00 2001 From: zemal Date: Thu, 6 Jul 2017 16:09:38 +0800 Subject: [PATCH 025/106] =?UTF-8?q?=E4=BF=AE=E5=A4=8DOI=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E4=B8=8B=E6=B5=8B=E8=AF=95=E7=82=B9=E5=85=A8=E9=83=A8=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E7=BB=93=E6=9E=9C=E4=B9=9F=E6=98=AF=E9=83=A8=E5=88=86?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- judge/dispatcher.py | 5 +++-- submission/serializers.py | 10 ++++++---- submission/views/oj.py | 3 +-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/judge/dispatcher.py b/judge/dispatcher.py index f96fd4c..5325153 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -100,16 +100,17 @@ class JudgeDispatcher(object): self.submission_obj.info = resp if resp["err"]: self.submission_obj.result = JudgeStatus.COMPILE_ERROR + self.submission_obj.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"]]) error_test_case = list(filter(lambda case: case["result"] != 0, resp["data"])) - # 多个测试点全部正确AC,否则 ACM模式下取第一个错误的测试点状态, OI模式对应为部分正确 + # 多个测试点全部正确则AC,否则 ACM模式下取第一个错误的测试点的状态, OI模式若全部错误则取第一个错误测试点状态,否则为部分正确 if not error_test_case: self.submission_obj.result = JudgeStatus.ACCEPTED - elif self.problem_obj.rule_type == ProblemRuleType.ACM: + 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"] else: self.submission_obj.result = JudgeStatus.PARTIALLY_ACCEPTED diff --git a/submission/serializers.py b/submission/serializers.py index 1c58e39..bc33036 100644 --- a/submission/serializers.py +++ b/submission/serializers.py @@ -25,7 +25,7 @@ class SubmissionSafeSerializer(serializers.ModelSerializer): class Meta: model = Submission - exclude = ('info', 'contest_id') + exclude = ("info", "contest_id") @staticmethod def get_username(obj): @@ -38,16 +38,18 @@ class SubmissionListSerializer(SubmissionSafeSerializer): show_link = serializers.SerializerMethodField() def __init__(self, *args, **kwargs): - self.user = kwargs.pop('user', None) + self.user = kwargs.pop("user", None) super().__init__(*args, **kwargs) class Meta: model = Submission - exclude = ('info', 'contest_id', 'code') + exclude = ("info", "contest_id", "code") def get_show_link(self, obj): + if self.user.id is None: + return False return obj.check_user_permission(self.user) @staticmethod def get_username(obj): - return User.objects.get(id=obj.user_id).username \ No newline at end of file + return User.objects.get(id=obj.user_id).username diff --git a/submission/views/oj.py b/submission/views/oj.py index 1066899..7619d7c 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -12,7 +12,6 @@ from ..serializers import CreateSubmissionSerializer, SubmissionModelSerializer from ..serializers import SubmissionSafeSerializer, SubmissionListSerializer - def _submit(response, user, problem_id, language, code, contest_id=None): # TODO: 预设默认值,需修改 controller = BucketController(user_id=user.id, @@ -83,7 +82,7 @@ class SubmissionListAPI(APIView): if problem_id: subs = subs.filter(problem_id=problem_id) - if request.GET.get("myself"): + if request.GET.get("myself") and request.GET["myself"] == "1": subs = subs.filter(user_id=request.user.id) # todo: paginate return self.success(SubmissionListSerializer(subs, many=True, user=request.user).data) From 8a60ea52bbf79f071d62badd2d9d775b5a3b0589 Mon Sep 17 00:00:00 2001 From: zemal Date: Thu, 6 Jul 2017 21:08:34 +0800 Subject: [PATCH 026/106] =?UTF-8?q?=E4=BF=AE=E6=94=B9submissions=E6=88=90?= =?UTF-8?q?=E5=88=86=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- submission/models.py | 1 + submission/views/oj.py | 5 +++-- utils/api/api.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/submission/models.py b/submission/models.py index 74a7e87..bc445ec 100644 --- a/submission/models.py +++ b/submission/models.py @@ -32,6 +32,7 @@ class Submission(models.Model): language = models.CharField(max_length=20) shared = models.BooleanField(default=False) # 存储该提交所用时间和内存值,方便提交列表显示 + # {time_cost: "", memory_cost: "", err_info: "", score: 0} statistic_info = JSONField(default={}) def check_user_permission(self, user): diff --git a/submission/views/oj.py b/submission/views/oj.py index 7619d7c..028a733 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -84,8 +84,9 @@ class SubmissionListAPI(APIView): if request.GET.get("myself") and request.GET["myself"] == "1": subs = subs.filter(user_id=request.user.id) - # todo: paginate - return self.success(SubmissionListSerializer(subs, many=True, user=request.user).data) + data = self.paginate_data(request, subs) + data["results"] = SubmissionListSerializer(data["results"], many=True, user=request.user).data + return self.success(data) def _get_submission(submission_id, user): diff --git a/utils/api/api.py b/utils/api/api.py index 197026a..47a81cd 100644 --- a/utils/api/api.py +++ b/utils/api/api.py @@ -112,7 +112,7 @@ class APIView(View): if object_serializer: return object_serializer(query_set, many=True).data else: - return query_set + return {"results": query_set, "total": query_set.count()} try: limit = int(request.GET.get("limit", "100")) except ValueError: From ee2f5f5dd792c23ac0aee6d759e524d41dbec89a Mon Sep 17 00:00:00 2001 From: zemal Date: Sat, 15 Jul 2017 23:18:07 +0800 Subject: [PATCH 027/106] =?UTF-8?q?=E5=8E=BB=E6=8E=89dataTime=E7=9A=84?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=EF=BC=8C=E5=9B=A0=E4=B8=BA=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E5=8C=96=E5=90=8Emoment.js=E4=B8=8D=E8=83=BD=E8=AF=86?= =?UTF-8?q?=E5=88=AB=E4=B8=BA=E6=A0=87=E5=87=86=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contest/serializers.py | 2 +- submission/views/oj.py | 49 --------------------------------------- utils/api/_serializers.py | 2 +- 3 files changed, 2 insertions(+), 51 deletions(-) diff --git a/contest/serializers.py b/contest/serializers.py index c99c16d..b886afe 100644 --- a/contest/serializers.py +++ b/contest/serializers.py @@ -25,7 +25,7 @@ class ContestSerializer(serializers.ModelSerializer): class Meta: model = Contest - + exclude = ('password', 'visible') class EditConetestSeriaizer(serializers.Serializer): id = serializers.IntegerField() diff --git a/submission/views/oj.py b/submission/views/oj.py index 028a733..626b8c3 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -89,52 +89,3 @@ class SubmissionListAPI(APIView): return self.success(data) -def _get_submission(submission_id, user): - """ - 用户权限判断 - """ - submission = Submission.objects.get(id=submission_id) - # Super Admin / Owner / Share - if user.admin_type == AdminType.SUPER_ADMIN or submission.user_id == user.id: - return {"submission": submission, "can_share": True} - if submission.contest_id: - # 比赛部分 - pass - if submission.shared: - return {"submission": submission, "can_share": False} - else: - raise Submission.DoesNotExist - - -class SubmissionDetailAPI(APIView): - """ - 单个提交页面详情 - """ - - def get(self, request, **kwargs): - try: - result = _get_submission(kwargs["submission_id"], request.user) - submission = result["submission"] - except Submission.DoesNotExist: - return self.error("Submission not exist") - - # TODO: Contest - try: - if submission.contest_id: - # problem = ContestProblem.objects.get(id=submission.problem_id, visible=True) - pass - else: - problem = Problem.objects.get(id=submission.problem_id, visible=True) - except (Problem.DoesNotExist,): - return self.error("Submission not exist") - - if submission.result in [JudgeStatus.COMPILE_ERROR, JudgeStatus.SYSTEM_ERROR, JudgeStatus.PENDING]: - info = submission.info - else: - info = submission.info - if "test_case" in info[0]: - info = sorted(info, key=lambda x: x["test_case"]) - - user = User.objects.get(id=submission.user_id) - return self.success({"submission": submission, "problem": problem, "info": info, - "user": user, "can_share": result["can_share"]}) diff --git a/utils/api/_serializers.py b/utils/api/_serializers.py index 4cfe3f4..f841f03 100644 --- a/utils/api/_serializers.py +++ b/utils/api/_serializers.py @@ -4,7 +4,7 @@ from rest_framework import serializers class DateTimeTZField(serializers.DateTimeField): def to_representation(self, value): - self.format = "%Y-%-m-%d %H:%M:%S %Z" + # self.format = "%Y-%-m-%d %H:%M:%S %Z" value = timezone.localtime(value) return super(DateTimeTZField, self).to_representation(value) From 53d0cae8eaffaa4ecb642b2498285d535ec17c2a Mon Sep 17 00:00:00 2001 From: zemal Date: Mon, 17 Jul 2017 21:28:06 +0800 Subject: [PATCH 028/106] contest and contest_problems api. add ordering for contest and submission models --- account/decorators.py | 41 +++++++++++++++++++ contest/migrations/0004_auto_20170717_1324.py | 19 +++++++++ contest/models.py | 7 ++-- contest/serializers.py | 28 ++++++++----- contest/urls/oj.py | 4 +- contest/views/oj.py | 32 ++++++++++++--- judge/languages.py | 2 +- problem/serializers.py | 15 +++++++ problem/urls/oj.py | 3 +- problem/views/oj.py | 12 +++++- .../migrations/0004_auto_20170717_1324.py | 19 +++++++++ submission/models.py | 1 + utils/api/_serializers.py | 2 +- 13 files changed, 160 insertions(+), 25 deletions(-) create mode 100644 contest/migrations/0004_auto_20170717_1324.py create mode 100644 submission/migrations/0004_auto_20170717_1324.py diff --git a/account/decorators.py b/account/decorators.py index 9df97e5..e424e8c 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -4,6 +4,8 @@ from utils.api import JSONResponse from .models import ProblemPermission +from contest.models import Contest, ContestType, ContestStatus + class BasePermissionDecorator(object): def __init__(self, func): @@ -53,3 +55,42 @@ class problem_permission_required(admin_role_required): if self.request.user.problem_permission == ProblemPermission.NONE: return False return True + + +def check_contest_permission(func): + """ + 只供Class based view 使用,检查用户是否有权进入该contest, + 若通过验证,在view中可通过self.contest获得该contest + """ + @functools.wraps(func) + def _check_permission(*args, **kwargs): + self = args[0] + request = args[1] + user = request.user + if kwargs.get('contest_id'): + contest_id = kwargs.pop('contest_id') + else: + contest_id = request.GET.get('contest_id') + if not contest_id: + return self.error("Parameter contest_id not exist.") + + try: + # use self.contest to avoid query contest again in view. + self.contest = Contest.objects.get(id=contest_id, visible=True) + except Contest.DoesNotExist: + return self.error("Contest %s doesn't exist" % contest_id) + + if self.contest.contest_type == ContestType.PASSWORD_PROTECTED_CONTEST: + # Anonymous + if not user.is_authenticated(): + return self.error("Please login in first.") + # creator + if request.user == self.contest.created_by: + return func(*args, **kwargs) + # password error + if ("contests" not in request.session) or (self.contest.id not in request.session["contests"]): + return self.error("Password is required.") + + return func(*args, **kwargs) + + return _check_permission diff --git a/contest/migrations/0004_auto_20170717_1324.py b/contest/migrations/0004_auto_20170717_1324.py new file mode 100644 index 0000000..6a7aa09 --- /dev/null +++ b/contest/migrations/0004_auto_20170717_1324.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2017-07-17 13:24 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('contest', '0003_auto_20170217_0820'), + ] + + operations = [ + migrations.AlterModelOptions( + name='contest', + options={'ordering': ('create_time',)}, + ), + ] diff --git a/contest/models.py b/contest/models.py index 72e60cb..dc52f47 100644 --- a/contest/models.py +++ b/contest/models.py @@ -12,9 +12,9 @@ class ContestType(object): class ContestStatus(object): - CONTEST_NOT_START = "Not Started" - CONTEST_ENDED = "Ended" - CONTEST_UNDERWAY = "Underway" + CONTEST_NOT_START = "1" + CONTEST_ENDED = "-1" + CONTEST_UNDERWAY = "0" class ContestRuleType(object): @@ -58,6 +58,7 @@ class Contest(models.Model): class Meta: db_table = "contest" + ordering = ("create_time",) class ContestRank(models.Model): diff --git a/contest/serializers.py b/contest/serializers.py index b886afe..528fff4 100644 --- a/contest/serializers.py +++ b/contest/serializers.py @@ -2,6 +2,8 @@ from utils.api import DateTimeTZField, UsernameSerializer, serializers from .models import Contest, ContestAnnouncement, ContestRuleType +from problem.serializers import ContestProblemSerializer + class CreateConetestSeriaizer(serializers.Serializer): title = serializers.CharField(max_length=128) @@ -14,6 +16,17 @@ class CreateConetestSeriaizer(serializers.Serializer): real_time_rank = serializers.BooleanField() +class EditConetestSeriaizer(serializers.Serializer): + id = serializers.IntegerField() + title = serializers.CharField(max_length=128) + description = serializers.CharField() + start_time = serializers.DateTimeField() + end_time = serializers.DateTimeField() + password = serializers.CharField(allow_blank=True, allow_null=True, max_length=32) + visible = serializers.BooleanField() + real_time_rank = serializers.BooleanField() + + class ContestSerializer(serializers.ModelSerializer): start_time = DateTimeTZField() end_time = DateTimeTZField() @@ -27,16 +40,6 @@ class ContestSerializer(serializers.ModelSerializer): model = Contest exclude = ('password', 'visible') -class EditConetestSeriaizer(serializers.Serializer): - id = serializers.IntegerField() - title = serializers.CharField(max_length=128) - description = serializers.CharField() - start_time = serializers.DateTimeField() - end_time = serializers.DateTimeField() - password = serializers.CharField(allow_blank=True, allow_null=True, max_length=32) - visible = serializers.BooleanField() - real_time_rank = serializers.BooleanField() - class ContestAnnouncementSerializer(serializers.ModelSerializer): created_by = UsernameSerializer() @@ -50,3 +53,8 @@ class CreateContestAnnouncementSerializer(serializers.Serializer): title = serializers.CharField(max_length=128) content = serializers.CharField() contest_id = serializers.IntegerField() + + +class ContestPasswordVerifySerializer(serializers.Serializer): + contest_id = serializers.IntegerField() + password = serializers.CharField(max_length=30, required=True) diff --git a/contest/urls/oj.py b/contest/urls/oj.py index 283bdff..6c09343 100644 --- a/contest/urls/oj.py +++ b/contest/urls/oj.py @@ -1,9 +1,9 @@ from django.conf.urls import url -from ..views.oj import ContestAnnouncementListAPI, ContestListAPI +from ..views.oj import ContestAnnouncementListAPI, ContestAPI urlpatterns = [ - url(r"^contest/?$", ContestListAPI.as_view(), name="contest_api"), + url(r"^contest/?$", ContestAPI.as_view(), name="contest_api"), url(r"^contest/announcement/?$", ContestAnnouncementListAPI.as_view(), name="contest_announcement_api"), ] diff --git a/contest/views/oj.py b/contest/views/oj.py index 8bff965..b576fa5 100644 --- a/contest/views/oj.py +++ b/contest/views/oj.py @@ -1,8 +1,9 @@ -from utils.api import APIView +from utils.api import APIView, validate_serializer +from account.decorators import login_required from ..models import ContestAnnouncement, Contest from ..serializers import ContestAnnouncementSerializer -from ..serializers import ContestSerializer +from ..serializers import ContestSerializer, ContestPasswordVerifySerializer class ContestAnnouncementListAPI(APIView): @@ -17,18 +18,39 @@ class ContestAnnouncementListAPI(APIView): return self.success(ContestAnnouncementSerializer(data, many=True).data) -class ContestListAPI(APIView): +class ContestAPI(APIView): def get(self, request): - contest_id = request.GET.get("id") + contest_id = request.GET.get("contest_id") if contest_id: try: contest = Contest.objects.get(id=contest_id, visible=True) return self.success(ContestSerializer(contest).data) except Contest.DoesNotExist: - return self.error("Contest Doesn't exist.") + return self.error("Contest doesn't exist.") contests = Contest.objects.filter(visible=True) keyword = request.GET.get("keyword") if keyword: contests = contests.filter(title__contains=keyword) return self.success(self.paginate_data(request, contests, ContestSerializer)) + + +class ContestPasswordVerifyAPI(APIView): + @validate_serializer(ContestPasswordVerifySerializer) + @login_required + def get(self, request): + data = request.data + try: + contest = Contest.objects.get(id=data["contest_id"], visible=True, password__isnull=False) + except Contest.DoesNotExist: + return self.error("Contest %s doesn't exist." % data["contest_id"]) + if contest.password != data["password"]: + return self.error("Password doesn't match.") + + # password verify OK. + if "contests" not in request.session: + request.session["contests"] = [] + request.session["contests"].append(int(data["contest_id"])) + # https://docs.djangoproject.com/en/dev/topics/http/sessions/#when-sessions-are-saved + request.session.modified = True + return self.success(True) diff --git a/judge/languages.py b/judge/languages.py index 6002760..1c2fd30 100644 --- a/judge/languages.py +++ b/judge/languages.py @@ -99,7 +99,7 @@ _java_lang_config = { "compile_command": "/usr/bin/javac {src_path} -d {exe_dir} -encoding UTF8" }, "run": { - "command": "/usr/bin/java -cp {exe_dir} -Xss1M -XX:MaxPermSize=16M -XX:PermSize=8M -Xms16M -Xmx{max_memory}k " + "command": "/usr/bin/java -cp {exe_dir} -Xss1M -Xms16M -Xmx{max_memory}k " "-Djava.security.manager -Djava.security.policy==/etc/java_policy -Djava.awt.headless=true Main", "seccomp_rule": None, "env": ["MALLOC_ARENA_MAX=1"] diff --git a/problem/serializers.py b/problem/serializers.py index 0425f3d..ad391c1 100644 --- a/problem/serializers.py +++ b/problem/serializers.py @@ -4,6 +4,7 @@ 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): @@ -85,3 +86,17 @@ class ProblemSerializer(serializers.ModelSerializer): class Meta: model = Problem + + +class ContestProblemSerializer(serializers.ModelSerializer): + samples = serializers.JSONField() + test_case_score = serializers.JSONField() + languages = serializers.JSONField() + template = serializers.JSONField() + tags = serializers.SlugRelatedField(many=True, slug_field="name", read_only=True) + create_time = DateTimeTZField() + last_update_time = DateTimeTZField() + created_by = UsernameSerializer() + + class Meta: + model = ContestProblem \ No newline at end of file diff --git a/problem/urls/oj.py b/problem/urls/oj.py index 5e7e6be..7450302 100644 --- a/problem/urls/oj.py +++ b/problem/urls/oj.py @@ -1,8 +1,9 @@ from django.conf.urls import url -from ..views.oj import ProblemTagAPI, ProblemAPI +from ..views.oj import ProblemTagAPI, ProblemAPI, ContestProblemAPI urlpatterns = [ url(r"^problem/tags/?$", ProblemTagAPI.as_view(), name="problem_tag_list_api"), url(r"^problems/?$", ProblemAPI.as_view(), name="problem_list_api"), + url(r"^contest_problems/?$", ContestProblemAPI.as_view(), name="contest_problem_api"), ] diff --git a/problem/views/oj.py b/problem/views/oj.py index a687520..4ad6488 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -1,8 +1,9 @@ from django.db.models import Q from utils.api import APIView - -from ..models import ProblemTag, Problem +from account.decorators import login_required, check_contest_permission +from ..models import ProblemTag, Problem, ContestProblem from ..serializers import ProblemSerializer, TagSerializer +from ..serializers import ContestProblemSerializer class ProblemTagAPI(APIView): @@ -42,3 +43,10 @@ class ProblemAPI(APIView): problems = problems.filter(difficulty=difficulty_rank) return self.success(self.paginate_data(request, problems, ProblemSerializer)) + + +class ContestProblemAPI(APIView): + @check_contest_permission + def get(self, request): + contest_problems = ContestProblem.objects.filter(contest=self.contest, visible=True) + return self.success(ContestProblemSerializer(contest_problems, many=True).data) diff --git a/submission/migrations/0004_auto_20170717_1324.py b/submission/migrations/0004_auto_20170717_1324.py new file mode 100644 index 0000000..ca355ae --- /dev/null +++ b/submission/migrations/0004_auto_20170717_1324.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2017-07-17 13:24 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('submission', '0003_auto_20170704_1243'), + ] + + operations = [ + migrations.AlterModelOptions( + name='submission', + options={'ordering': ('-created_time',)}, + ), + ] diff --git a/submission/models.py b/submission/models.py index bc445ec..7e8c06f 100644 --- a/submission/models.py +++ b/submission/models.py @@ -42,6 +42,7 @@ class Submission(models.Model): class Meta: db_table = "submission" + ordering = ("-created_time",) def __str__(self): return self.id diff --git a/utils/api/_serializers.py b/utils/api/_serializers.py index f841f03..816845a 100644 --- a/utils/api/_serializers.py +++ b/utils/api/_serializers.py @@ -4,7 +4,7 @@ from rest_framework import serializers class DateTimeTZField(serializers.DateTimeField): def to_representation(self, value): - # self.format = "%Y-%-m-%d %H:%M:%S %Z" + # self.format = "%Y-%m-%d %H:%M:%S %Z" value = timezone.localtime(value) return super(DateTimeTZField, self).to_representation(value) From ee49d0a8150c277bb42eba30c0396b9283082d4c Mon Sep 17 00:00:00 2001 From: zemal Date: Tue, 18 Jul 2017 11:18:18 +0800 Subject: [PATCH 029/106] =?UTF-8?q?=E6=B7=BB=E5=8A=A0contest=E6=9D=83?= =?UTF-8?q?=E9=99=90=E9=AA=8C=E8=AF=81=E3=80=81contest=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E9=AA=8C=E8=AF=81api=20=E6=B7=BB=E5=8A=A0problem=E3=80=81conte?= =?UTF-8?q?st=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contest/serializers.py | 7 ++- contest/tests.py | 10 ++++ contest/urls/oj.py | 2 + contest/views/admin.py | 10 ++-- contest/views/oj.py | 2 +- problem/tests.py | 110 +++++++++++++++++++++++++++++++++++++---- problem/urls/admin.py | 2 +- problem/urls/oj.py | 4 +- problem/views/oj.py | 8 +++ 9 files changed, 136 insertions(+), 19 deletions(-) diff --git a/contest/serializers.py b/contest/serializers.py index 528fff4..3e842fb 100644 --- a/contest/serializers.py +++ b/contest/serializers.py @@ -27,7 +27,7 @@ class EditConetestSeriaizer(serializers.Serializer): real_time_rank = serializers.BooleanField() -class ContestSerializer(serializers.ModelSerializer): +class ContestAdminSerializer(serializers.ModelSerializer): start_time = DateTimeTZField() end_time = DateTimeTZField() create_time = DateTimeTZField() @@ -36,6 +36,11 @@ class ContestSerializer(serializers.ModelSerializer): status = serializers.CharField() contest_type = serializers.CharField() + class Meta: + model = Contest + + +class ContestSerializer(ContestAdminSerializer): class Meta: model = Contest exclude = ('password', 'visible') diff --git a/contest/tests.py b/contest/tests.py index 93b839f..7d94afe 100644 --- a/contest/tests.py +++ b/contest/tests.py @@ -74,6 +74,16 @@ class ContestAPITest(APITestCase): response = self.client.get("{}?id={}".format(self.url, contest_id)) self.assertSuccess(response) + def test_contest_password(self): + contest_id = self.create_contest().data["data"]["id"] + self.create_user("test", "test123") + url = self.reverse("contest_password_api") + resp = self.client.post(url, {"contest_id": contest_id, "password": "error_password"}) + self.assertFailed(resp) + + resp = self.client.post(url, {"contest_id": contest_id, "password": DEFAULT_CONTEST_DATA["password"]}) + self.assertSuccess(resp) + class ContestAnnouncementAPITest(APITestCase): def setUp(self): diff --git a/contest/urls/oj.py b/contest/urls/oj.py index 6c09343..eccb1f5 100644 --- a/contest/urls/oj.py +++ b/contest/urls/oj.py @@ -1,9 +1,11 @@ from django.conf.urls import url from ..views.oj import ContestAnnouncementListAPI, ContestAPI +from ..views.oj import ContestPasswordVerifyAPI urlpatterns = [ url(r"^contest/?$", ContestAPI.as_view(), name="contest_api"), + url(r"^contest/password/?$", ContestPasswordVerifyAPI.as_view(), name="contest_password_api"), url(r"^contest/announcement/?$", ContestAnnouncementListAPI.as_view(), name="contest_announcement_api"), ] diff --git a/contest/views/admin.py b/contest/views/admin.py index 60bb161..5f9a62c 100644 --- a/contest/views/admin.py +++ b/contest/views/admin.py @@ -3,7 +3,7 @@ import dateutil.parser from utils.api import APIView, validate_serializer from ..models import Contest, ContestAnnouncement -from ..serializers import (ContestAnnouncementSerializer, ContestSerializer, +from ..serializers import (ContestAnnouncementSerializer, ContestAdminSerializer, CreateConetestSeriaizer, CreateContestAnnouncementSerializer, EditConetestSeriaizer) @@ -21,7 +21,7 @@ class ContestAPI(APIView): if not data["password"]: data["password"] = None contest = Contest.objects.create(**data) - return self.success(ContestSerializer(contest).data) + return self.success(ContestAdminSerializer(contest).data) @validate_serializer(EditConetestSeriaizer) def put(self, request): @@ -41,7 +41,7 @@ class ContestAPI(APIView): for k, v in data.items(): setattr(contest, k, v) contest.save() - return self.success(ContestSerializer(contest).data) + return self.success(ContestAdminSerializer(contest).data) def get(self, request): contest_id = request.GET.get("id") @@ -50,7 +50,7 @@ class ContestAPI(APIView): contest = Contest.objects.get(id=contest_id) if request.user.is_admin() and contest.created_by != request.user: return self.error("Contest does not exist") - return self.success(ContestSerializer(contest).data) + return self.success(ContestAdminSerializer(contest).data) except Contest.DoesNotExist: return self.error("Contest does not exist") @@ -62,7 +62,7 @@ class ContestAPI(APIView): if request.user.is_admin(): contests = contests.filter(created_by=request.user) - return self.success(self.paginate_data(request, contests, ContestSerializer)) + return self.success(self.paginate_data(request, contests, ContestAdminSerializer)) class ContestAnnouncementAPI(APIView): diff --git a/contest/views/oj.py b/contest/views/oj.py index b576fa5..08daa3e 100644 --- a/contest/views/oj.py +++ b/contest/views/oj.py @@ -38,7 +38,7 @@ class ContestAPI(APIView): class ContestPasswordVerifyAPI(APIView): @validate_serializer(ContestPasswordVerifySerializer) @login_required - def get(self, request): + def post(self, request): data = request.data try: contest = Contest.objects.get(id=data["contest_id"], visible=True, password__isnull=False) diff --git a/problem/tests.py b/problem/tests.py index caa6ad6..34e0f49 100644 --- a/problem/tests.py +++ b/problem/tests.py @@ -9,6 +9,17 @@ from utils.api.tests import APITestCase from .models import ProblemTag from .views.admin import TestCaseUploadAPI +from contest.tests import DEFAULT_CONTEST_DATA + +DEFAULT_PROBLEM_DATA = {"_id": "A-110", "title": "test", "description": "

test

", "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": "

test

", "source": "test"} class ProblemTagListAPITest(APITestCase): @@ -83,15 +94,7 @@ class ProblemAdminAPITest(APITestCase): def setUp(self): self.url = self.reverse("problem_admin_api") self.create_super_admin() - self.data = {"_id": "A-110", "title": "test", "description": "

test

", "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": "

test

", "source": "test"} + self.data = DEFAULT_PROBLEM_DATA def test_create_problem(self): resp = self.client.post(self.url, data=self.data) @@ -131,3 +134,92 @@ class ProblemAdminAPITest(APITestCase): data["id"] = problem_id resp = self.client.put(self.url, data=data) self.assertSuccess(resp) + + +class ProblemAPITest(APITestCase): + def setUp(self): + self.url = self.reverse("problem_api") + self.create_admin() + + def create_problem(self): + url = self.reverse("problem_admin_api") + return self.client.post(url, data=DEFAULT_PROBLEM_DATA) + + def test_get_problem_list(self): + self.create_problem() + resp = self.client.get(self.url) + self.assertSuccess(resp) + + def get_one_problem(self): + problem_id = self.create_problem().data["data"]["_id"] + resp = self.client.get(self.url + "?id=" + str(problem_id)) + self.assertSuccess(resp) + + +class ContestProblemAdminTest(APITestCase): + def setUp(self): + self.url = self.reverse("contest_problem_admin_api") + self.create_admin() + + def create_contest(self): + url = self.reverse("contest_admin_api") + return self.client.post(url, data=DEFAULT_CONTEST_DATA) + + def test_create_contest_problem(self): + contest = self.create_contest() + data = DEFAULT_PROBLEM_DATA + data["contest_id"] = contest.data["data"]["id"] + resp = self.client.post(self.url, data=data) + self.assertSuccess(resp) + return resp + + def test_get_contest_problem(self): + contest = self.test_create_contest_problem() + contest_id = contest.data["data"]["id"] + resp = self.client.get(self.url + "?contest_id=" + str(contest_id)) + self.assertSuccess(resp) + self.assertEqual(len(resp.data["data"]), 1) + + def test_get_one_contest_problem(self): + contest = self.test_create_contest_problem() + contest_id = contest.data["data"]["id"] + resp = self.client.get(self.url + "?id=" + str(contest_id)) + self.assertSuccess(resp) + + +class ContestProblemTest(APITestCase): + def setUp(self): + self.url = self.reverse("contest_problem_api") + self.create_admin() + + url = self.reverse("contest_admin_api") + self.contest = self.client.post(url, data=DEFAULT_CONTEST_DATA) + self.data = DEFAULT_PROBLEM_DATA + self.data["contest"] = self.contest.data["data"]["id"] + url = self.reverse("contest_problem_admin_api") + self.problem = self.client.post(url, self.data) + + def test_get_contest_problem_list(self): + contest_id = self.contest.data["data"]["id"] + resp = self.client.get(self.url + "?contest_id=" + str(contest_id)) + self.assertSuccess(resp) + self.assertEqual(len(resp.data["data"]), 1) + + def test_get_one_contest_problem(self): + contest_id = self.contest.data["data"]["id"] + problem_id = self.problem.data["data"]["_id"] + resp = self.client.get("{}?contest_id={}&problem_id={}".format(self.url,contest_id, problem_id)) + self.assertSuccess(resp) + + def test_regular_user_get_contest_problem(self): + self.create_user("test", "test123") + contest_id = self.contest.data["data"]["id"] + problem_id = self.problem.data["data"]["_id"] + resp = self.client.get("{}?contest_id={}&problem_id={}".format(self.url,contest_id, problem_id)) + self.assertFailed(resp) + + url = self.reverse("contest_password_api") + self.client.post(url, {"contest_id": contest_id, "password": DEFAULT_CONTEST_DATA["password"]}) + resp = self.client.get("{}?contest_id={}&problem_id={}".format(self.url,contest_id, problem_id)) + self.assertSuccess(resp) + diff --git a/problem/urls/admin.py b/problem/urls/admin.py index d123f35..ba0feeb 100644 --- a/problem/urls/admin.py +++ b/problem/urls/admin.py @@ -5,5 +5,5 @@ from ..views.admin import ContestProblemAPI, ProblemAPI, TestCaseUploadAPI urlpatterns = [ url(r"^test_case/upload/?$", TestCaseUploadAPI.as_view(), name="test_case_upload_api"), url(r"^problem/?$", ProblemAPI.as_view(), name="problem_admin_api"), - url(r"^contest/problem/?$", ContestProblemAPI.as_view(), name="contest_problem_api") + url(r"^contest/problem/?$", ContestProblemAPI.as_view(), name="contest_problem_admin_api"), ] diff --git a/problem/urls/oj.py b/problem/urls/oj.py index 7450302..c50cafc 100644 --- a/problem/urls/oj.py +++ b/problem/urls/oj.py @@ -4,6 +4,6 @@ from ..views.oj import ProblemTagAPI, ProblemAPI, ContestProblemAPI urlpatterns = [ url(r"^problem/tags/?$", ProblemTagAPI.as_view(), name="problem_tag_list_api"), - url(r"^problems/?$", ProblemAPI.as_view(), name="problem_list_api"), - url(r"^contest_problems/?$", ContestProblemAPI.as_view(), name="contest_problem_api"), + url(r"^problem/?$", ProblemAPI.as_view(), name="problem_api"), + url(r"^contest/problem/?$", ContestProblemAPI.as_view(), name="contest_problem_api"), ] diff --git a/problem/views/oj.py b/problem/views/oj.py index 4ad6488..c273bdd 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -48,5 +48,13 @@ class ProblemAPI(APIView): class ContestProblemAPI(APIView): @check_contest_permission def get(self, request): + problem_id = request.GET.get("problem_id") + if problem_id: + try: + problem = ContestProblem.objects.get(_id=problem_id, contest=self.contest, visible=True) + except ContestProblem.DoesNotExist: + return self.error("Problem does not exist.") + return self.success(ContestProblemSerializer(problem).data) + contest_problems = ContestProblem.objects.filter(contest=self.contest, visible=True) return self.success(ContestProblemSerializer(contest_problems, many=True).data) From 8b85f861247d5a0beb6792559a880248240ae79e Mon Sep 17 00:00:00 2001 From: zemal Date: Tue, 18 Jul 2017 11:25:08 +0800 Subject: [PATCH 030/106] reformat code. --- account/decorators.py | 8 ++++---- contest/serializers.py | 4 +--- problem/serializers.py | 2 +- problem/tests.py | 7 +++---- problem/views/oj.py | 2 +- submission/views/oj.py | 5 +---- 6 files changed, 11 insertions(+), 17 deletions(-) diff --git a/account/decorators.py b/account/decorators.py index e424e8c..5497565 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -4,7 +4,7 @@ from utils.api import JSONResponse from .models import ProblemPermission -from contest.models import Contest, ContestType, ContestStatus +from contest.models import Contest, ContestType class BasePermissionDecorator(object): @@ -67,10 +67,10 @@ def check_contest_permission(func): self = args[0] request = args[1] user = request.user - if kwargs.get('contest_id'): - contest_id = kwargs.pop('contest_id') + if kwargs.get("contest_id"): + contest_id = kwargs.pop("contest_id") else: - contest_id = request.GET.get('contest_id') + contest_id = request.GET.get("contest_id") if not contest_id: return self.error("Parameter contest_id not exist.") diff --git a/contest/serializers.py b/contest/serializers.py index 3e842fb..8fb4bee 100644 --- a/contest/serializers.py +++ b/contest/serializers.py @@ -2,8 +2,6 @@ from utils.api import DateTimeTZField, UsernameSerializer, serializers from .models import Contest, ContestAnnouncement, ContestRuleType -from problem.serializers import ContestProblemSerializer - class CreateConetestSeriaizer(serializers.Serializer): title = serializers.CharField(max_length=128) @@ -43,7 +41,7 @@ class ContestAdminSerializer(serializers.ModelSerializer): class ContestSerializer(ContestAdminSerializer): class Meta: model = Contest - exclude = ('password', 'visible') + exclude = ("password", "visible") class ContestAnnouncementSerializer(serializers.ModelSerializer): diff --git a/problem/serializers.py b/problem/serializers.py index ad391c1..fff369d 100644 --- a/problem/serializers.py +++ b/problem/serializers.py @@ -99,4 +99,4 @@ class ContestProblemSerializer(serializers.ModelSerializer): created_by = UsernameSerializer() class Meta: - model = ContestProblem \ No newline at end of file + model = ContestProblem diff --git a/problem/tests.py b/problem/tests.py index 34e0f49..3bdc79c 100644 --- a/problem/tests.py +++ b/problem/tests.py @@ -208,18 +208,17 @@ class ContestProblemTest(APITestCase): def test_get_one_contest_problem(self): contest_id = self.contest.data["data"]["id"] problem_id = self.problem.data["data"]["_id"] - resp = self.client.get("{}?contest_id={}&problem_id={}".format(self.url,contest_id, problem_id)) + resp = self.client.get("{}?contest_id={}&problem_id={}".format(self.url, contest_id, problem_id)) self.assertSuccess(resp) def test_regular_user_get_contest_problem(self): self.create_user("test", "test123") contest_id = self.contest.data["data"]["id"] problem_id = self.problem.data["data"]["_id"] - resp = self.client.get("{}?contest_id={}&problem_id={}".format(self.url,contest_id, problem_id)) + resp = self.client.get("{}?contest_id={}&problem_id={}".format(self.url, contest_id, problem_id)) self.assertFailed(resp) url = self.reverse("contest_password_api") self.client.post(url, {"contest_id": contest_id, "password": DEFAULT_CONTEST_DATA["password"]}) - resp = self.client.get("{}?contest_id={}&problem_id={}".format(self.url,contest_id, problem_id)) + resp = self.client.get("{}?contest_id={}&problem_id={}".format(self.url, contest_id, problem_id)) self.assertSuccess(resp) - diff --git a/problem/views/oj.py b/problem/views/oj.py index c273bdd..8565bec 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -1,6 +1,6 @@ from django.db.models import Q from utils.api import APIView -from account.decorators import login_required, check_contest_permission +from account.decorators import check_contest_permission from ..models import ProblemTag, Problem, ContestProblem from ..serializers import ProblemSerializer, TagSerializer from ..serializers import ContestProblemSerializer diff --git a/submission/views/oj.py b/submission/views/oj.py index 626b8c3..b9a9007 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -1,13 +1,12 @@ from django_redis import get_redis_connection from account.decorators import login_required -from account.models import AdminType, User from problem.models import Problem, ProblemRuleType 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, JudgeStatus +from ..models import Submission from ..serializers import CreateSubmissionSerializer, SubmissionModelSerializer from ..serializers import SubmissionSafeSerializer, SubmissionListSerializer @@ -87,5 +86,3 @@ class SubmissionListAPI(APIView): data = self.paginate_data(request, subs) data["results"] = SubmissionListSerializer(data["results"], many=True, user=request.user).data return self.success(data) - - From 17432b4c818a48b5824f91f7ca7798963243768b Mon Sep 17 00:00:00 2001 From: zemal Date: Thu, 20 Jul 2017 15:52:11 +0800 Subject: [PATCH 031/106] =?UTF-8?q?=E6=B7=BB=E5=8A=A0contest=20access=20ap?= =?UTF-8?q?i?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/decorators.py | 7 +++++-- account/urls/user.py | 2 +- account/views/user.py | 14 +++++++++----- contest/tests.py | 25 +++++++++++++++++++++++++ contest/urls/oj.py | 3 ++- contest/views/oj.py | 20 +++++++++++++++++--- problem/views/oj.py | 2 +- 7 files changed, 60 insertions(+), 13 deletions(-) diff --git a/account/decorators.py b/account/decorators.py index 5497565..1697a50 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -4,7 +4,7 @@ from utils.api import JSONResponse from .models import ProblemPermission -from contest.models import Contest, ContestType +from contest.models import Contest, ContestType, ContestStatus class BasePermissionDecorator(object): @@ -80,12 +80,15 @@ def check_contest_permission(func): except Contest.DoesNotExist: return self.error("Contest %s doesn't exist" % contest_id) + if self.contest.status == ContestStatus.CONTEST_NOT_START and user != self.contest.created_by: + return self.error("Contest has not started yet.") + if self.contest.contest_type == ContestType.PASSWORD_PROTECTED_CONTEST: # Anonymous if not user.is_authenticated(): return self.error("Please login in first.") # creator - if request.user == self.contest.created_by: + if user == self.contest.created_by: return func(*args, **kwargs) # password error if ("contests" not in request.session) or (self.contest.id not in request.session["contests"]): diff --git a/account/urls/user.py b/account/urls/user.py index 8ca0adf..47a601b 100644 --- a/account/urls/user.py +++ b/account/urls/user.py @@ -4,7 +4,7 @@ from ..views.user import (SSOAPI, AvatarUploadAPI, TwoFactorAuthAPI, UserNameAPI, UserProfileAPI) urlpatterns = [ - url(r"^username/?$", UserNameAPI.as_view(), name="user_name_api"), + # url(r"^username/?$", UserNameAPI.as_view(), name="user_name_api"), url(r"^profile/?$", UserProfileAPI.as_view(), name="user_profile_api"), url(r"^avatar/upload/?$", AvatarUploadAPI.as_view(), name="avatar_upload_api"), url(r"^sso/?$", SSOAPI.as_view(), name="sso_api"), diff --git a/account/views/user.py b/account/views/user.py index fa97a80..9bbbf03 100644 --- a/account/views/user.py +++ b/account/views/user.py @@ -39,15 +39,19 @@ class UserNameAPI(APIView): class UserProfileAPI(APIView): - @login_required + """ + 判断是否登录, 若登录返回用户信息 + """ + @method_decorator(ensure_csrf_cookie) def get(self, request, **kwargs): - """ - Return user info according username or user_id - """ + user = request.user + if not user.is_authenticated(): + return self.success(0) + username = request.GET.get("username") try: if username: - user = User.objects.get(username=username) + user = User.objects.get(username=username, is_disabled=False) else: user = request.user except User.DoesNotExist: diff --git a/contest/tests.py b/contest/tests.py index 7d94afe..583035c 100644 --- a/contest/tests.py +++ b/contest/tests.py @@ -84,6 +84,31 @@ class ContestAPITest(APITestCase): resp = self.client.post(url, {"contest_id": contest_id, "password": DEFAULT_CONTEST_DATA["password"]}) self.assertSuccess(resp) + def test_contest_access(self): + contest_id = self.create_contest().data["data"]["id"] + self.create_user("test", "test123") + url = self.reverse("contest_access_api") + resp = self.client.get(url + "?contest_id=" + str(contest_id)) + self.assertFalse(resp.data["data"]["Access"]) + + password_url = self.reverse("contest_password_api") + resp = self.client.post(password_url, {"contest_id": contest_id, "password": DEFAULT_CONTEST_DATA["password"]}) + self.assertSuccess(resp) + resp = self.client.get(url + "?contest_id=" + str(contest_id)) + self.assertSuccess(resp) + + # def test_get_not_started_contest(self): + # contest_id = self.create_contest().data["data"]["id"] + # resp = self.client.get(self.url + "?id=" + str(contest_id)) + # self.assertSuccess(resp) + # + # self.create_user("test", "1234") + # url = self.reverse("contest_password_api") + # resp = self.client.post(url, {"contest_id": contest_id, "password": DEFAULT_CONTEST_DATA["password"]}) + # self.assertSuccess(resp) + # resp = self.client.get(self.url + "?id=" + str(contest_id)) + # self.assertFailed(resp) + class ContestAnnouncementAPITest(APITestCase): def setUp(self): diff --git a/contest/urls/oj.py b/contest/urls/oj.py index eccb1f5..42fbdf2 100644 --- a/contest/urls/oj.py +++ b/contest/urls/oj.py @@ -1,11 +1,12 @@ from django.conf.urls import url from ..views.oj import ContestAnnouncementListAPI, ContestAPI -from ..views.oj import ContestPasswordVerifyAPI +from ..views.oj import ContestPasswordVerifyAPI, ContestAccessAPI urlpatterns = [ url(r"^contest/?$", ContestAPI.as_view(), name="contest_api"), url(r"^contest/password/?$", ContestPasswordVerifyAPI.as_view(), name="contest_password_api"), url(r"^contest/announcement/?$", ContestAnnouncementListAPI.as_view(), name="contest_announcement_api"), + url(r"^contest/access/?$", ContestAccessAPI.as_view(), name="contest_access_api"), ] diff --git a/contest/views/oj.py b/contest/views/oj.py index 08daa3e..74653f0 100644 --- a/contest/views/oj.py +++ b/contest/views/oj.py @@ -1,7 +1,7 @@ from utils.api import APIView, validate_serializer from account.decorators import login_required -from ..models import ContestAnnouncement, Contest +from ..models import ContestAnnouncement, Contest, ContestStatus from ..serializers import ContestAnnouncementSerializer from ..serializers import ContestSerializer, ContestPasswordVerifySerializer @@ -20,13 +20,13 @@ class ContestAnnouncementListAPI(APIView): class ContestAPI(APIView): def get(self, request): - contest_id = request.GET.get("contest_id") + contest_id = request.GET.get("id") if contest_id: try: contest = Contest.objects.get(id=contest_id, visible=True) - return self.success(ContestSerializer(contest).data) except Contest.DoesNotExist: return self.error("Contest doesn't exist.") + return self.success(ContestSerializer(contest).data) contests = Contest.objects.filter(visible=True) keyword = request.GET.get("keyword") @@ -54,3 +54,17 @@ class ContestPasswordVerifyAPI(APIView): # https://docs.djangoproject.com/en/dev/topics/http/sessions/#when-sessions-are-saved request.session.modified = True return self.success(True) + + +class ContestAccessAPI(APIView): + @login_required + def get(self, request): + contest_id = request.GET.get("contest_id") + if not contest_id: + return self.error("Parameter contest_id not exist.") + if "contests" not in request.session: + request.session["contests"] = [] + if int(contest_id) in request.session["contests"]: + return self.success({"Access": True}) + else: + return self.success({"Access": False}) diff --git a/problem/views/oj.py b/problem/views/oj.py index 8565bec..edc1c1b 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -14,7 +14,7 @@ class ProblemTagAPI(APIView): class ProblemAPI(APIView): def get(self, request): # 问题详情页 - problem_id = request.GET.get("id") + problem_id = request.GET.get("problem_id") if problem_id: try: problem = Problem.objects.get(_id=problem_id, visible=True) From 14b850c6527b25ecf8e181ab0b9f8283e8dfb9ab Mon Sep 17 00:00:00 2001 From: zemal Date: Tue, 1 Aug 2017 16:52:48 +0800 Subject: [PATCH 032/106] =?UTF-8?q?=E5=AE=8C=E6=88=90ACM=20ContestProblem?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=88=A4=E9=A2=98=E9=80=BB=E8=BE=91=20contes?= =?UTF-8?q?t,submission=E7=AD=89=E8=A1=A8=E9=BB=98=E8=AE=A4-create=5Ftime?= =?UTF-8?q?=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/models.py | 4 - account/urls/user.py | 2 +- contest/migrations/0004_auto_20170717_1324.py | 6 +- contest/models.py | 6 +- contest/serializers.py | 17 ++ contest/views/oj.py | 44 ++++- judge/dispatcher.py | 150 +++++++++++++----- .../migrations/0004_auto_20170717_1324.py | 9 +- submission/models.py | 4 +- submission/serializers.py | 3 +- submission/urls/oj.py | 3 +- submission/views/oj.py | 47 ++++-- utils/api/api.py | 2 +- 13 files changed, 224 insertions(+), 73 deletions(-) diff --git a/account/models.py b/account/models.py index 8802c29..3563dd7 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 47a601b..8fdb7db 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 6a7aa09..617790a 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 dc52f47..ca99142 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 8fb4bee..356a150 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 74653f0..5baef2e 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 5325153..7ad1dc4 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 ca355ae..c8a5be3 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 7e8c06f..65d242b 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 bc33036..815d275 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 e569574..d86bfa5 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 b9a9007..0fe8819 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 47a81cd..920b827 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 From 7d11a596e50ffa462a5fafb7db89522d1d63c712 Mon Sep 17 00:00:00 2001 From: zemal Date: Tue, 15 Aug 2017 20:02:36 +0800 Subject: [PATCH 033/106] =?UTF-8?q?=E4=BF=AE=E6=AD=A3contest=5Fsubmission?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- judge/dispatcher.py | 12 ++++---- {submission => judge}/tasks.py | 0 submission/views/oj.py | 50 +++++++++++++++++++++------------- 3 files changed, 37 insertions(+), 25 deletions(-) rename {submission => judge}/tasks.py (100%) diff --git a/judge/dispatcher.py b/judge/dispatcher.py index 7ad1dc4..489825f 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -1,19 +1,19 @@ -import json -import requests import hashlib +import json import logging from urllib.parse import urljoin +import requests +from django.core.cache import cache 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, ContestProblem from contest.models import ContestRuleType, ACMContestRank, OIContestRank +from judge.languages import languages +from problem.models import Problem, ProblemRuleType, ContestProblem from submission.models import JudgeStatus, Submission logger = logging.getLogger(__name__) @@ -25,7 +25,7 @@ WAITING_QUEUE = "waiting_queue" def process_pending_task(redis_conn): if redis_conn.llen(WAITING_QUEUE): # 防止循环引入 - from submission.tasks import judge_task + from judge.tasks import judge_task data = json.loads(redis_conn.rpop(WAITING_QUEUE)) judge_task.delay(**data) diff --git a/submission/tasks.py b/judge/tasks.py similarity index 100% rename from submission/tasks.py rename to judge/tasks.py diff --git a/submission/views/oj.py b/submission/views/oj.py index 0fe8819..8358390 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -1,16 +1,17 @@ from django_redis import get_redis_connection from account.decorators import login_required, check_contest_permission +from judge.tasks import judge_task from problem.models import Problem, ProblemRuleType, ContestProblem -from submission.tasks import judge_task -# from judge.dispatcher import JudgeDispatcher - +from contest.models import Contest, ContestStatus +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 + +# from judge.dispatcher import JudgeDispatcher def _submit(response, user, problem_id, language, code, contest_id): @@ -50,6 +51,13 @@ class SubmissionAPI(APIView): @login_required def post(self, request): data = request.data + if data.get("contest_id"): + try: + contest = Contest.objects.get(id=data["contest_id"]) + except Contest.DoesNotExist: + return self.error("Contest doesn't exist.") + if contest.status != ContestStatus.CONTEST_UNDERWAY and request.user != contest.created_by: + return self.error("You have no permission to submit code.") return _submit(self, request.user, data["problem_id"], data["language"], data["code"], data.get("contest_id")) @login_required @@ -64,7 +72,16 @@ class SubmissionAPI(APIView): if not submission.check_user_permission(request.user): return self.error("No permission for this submission.") - # check problem'rule is ACM or IO. + 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 @@ -75,23 +92,18 @@ class SubmissionAPI(APIView): class SubmissionListAPI(APIView): def get(self, request): + if request.GET.get("contest_id"): + return self._get_contest_submission_list(request) + subs = Submission.objects.filter(contest_id__isnull=True) + return self.process_submissions(request, subs) - 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) - - -class ContestSubmissionListAPI(APIView): @check_contest_permission - def get(self, request): + def _get_contest_submission_list(self, request): subs = Submission.objects.filter(contest_id=self.contest.id) + return self.process_submissions(request, subs) + + def process_submissions(self, request, subs): problem_id = request.GET.get("problem_id") if problem_id: subs = subs.filter(problem_id=problem_id) From 0e96b7c2a8d739d40e4855a486631f97e75a6eb7 Mon Sep 17 00:00:00 2001 From: zemal Date: Tue, 15 Aug 2017 20:32:14 +0800 Subject: [PATCH 034/106] =?UTF-8?q?=E6=9B=B4=E6=8D=A2cache=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- conf/views.py | 4 +--- judge/dispatcher.py | 17 ++++++++--------- oj/settings.py | 36 ++++++++++++++++++++++++++++++++++++ submission/urls/oj.py | 5 ++--- submission/views/oj.py | 4 ++-- utils/cache.py | 6 ++++++ utils/constants.py | 2 ++ 7 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 utils/cache.py create mode 100644 utils/constants.py diff --git a/conf/views.py b/conf/views.py index bc03c01..c202421 100644 --- a/conf/views.py +++ b/conf/views.py @@ -1,7 +1,6 @@ import hashlib from django.utils import timezone -from django_redis import get_redis_connection from account.decorators import super_admin_required from judge.languages import languages, spj_languages @@ -129,8 +128,7 @@ class JudgeServerHeartbeatAPI(CSRFExemptAPIView): last_heartbeat=timezone.now(), ) # 新server上线 处理队列中的,防止没有新的提交而导致一直waiting - conn = get_redis_connection("JudgeQueue") - process_pending_task(conn) + process_pending_task() return self.success() diff --git a/judge/dispatcher.py b/judge/dispatcher.py index 489825f..501eabf 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -7,7 +7,6 @@ import requests from django.core.cache import cache from django.db import transaction from django.db.models import F -from django_redis import get_redis_connection from account.models import User from conf.models import JudgeServer, JudgeServerToken @@ -15,18 +14,18 @@ from contest.models import ContestRuleType, ACMContestRank, OIContestRank from judge.languages import languages from problem.models import Problem, ProblemRuleType, ContestProblem from submission.models import JudgeStatus, Submission +from utils.cache import judge_queue_cache +from utils.constants import CacheKey logger = logging.getLogger(__name__) -WAITING_QUEUE = "waiting_queue" - # 继续处理在队列中的问题 -def process_pending_task(redis_conn): - if redis_conn.llen(WAITING_QUEUE): +def process_pending_task(): + if judge_queue_cache.llen(CacheKey.waiting_queue): # 防止循环引入 from judge.tasks import judge_task - data = json.loads(redis_conn.rpop(WAITING_QUEUE)) + data = json.loads(judge_queue_cache.rpop(CacheKey.waiting_queue)) judge_task.delay(**data) @@ -34,7 +33,7 @@ class JudgeDispatcher(object): def __init__(self, submission_id, problem_id): token = JudgeServerToken.objects.first().token self.token = hashlib.sha256(token.encode("utf-8")).hexdigest() - self.redis_conn = get_redis_connection("JudgeQueue") + self.redis_conn = judge_queue_cache self.submission = Submission.objects.get(pk=submission_id) if self.submission.contest_id: self.problem = ContestProblem.objects.select_related("contest")\ @@ -77,7 +76,7 @@ class JudgeDispatcher(object): server = self.choose_judge_server() if not server: data = {"submission_id": self.submission.id, "problem_id": self.problem.id} - self.redis_conn.lpush(WAITING_QUEUE, json.dumps(data)) + self.redis_conn.lpush(CacheKey.waiting_queue, json.dumps(data)) return sub_config = list(filter(lambda item: self.submission.language == item["name"], languages))[0] @@ -130,7 +129,7 @@ class JudgeDispatcher(object): else: self.update_problem_status() # 至此判题结束,尝试处理任务队列中剩余的任务 - process_pending_task(self.redis_conn) + process_pending_task() def compile_spj(self, service_url, src, spj_version, spj_compile_config, test_case_id): data = {"src": src, "spj_version": spj_version, diff --git a/oj/settings.py b/oj/settings.py index c0d6cf7..e6a6524 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -159,6 +159,42 @@ REST_FRAMEWORK = { ) } +CACHE_JUDGE_QUEUE = "judge_queue" +CACHE_THROTTLING = "throttling" + + +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://127.0.0.1:6379/1", + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + } + }, + CACHE_JUDGE_QUEUE: { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://127.0.0.1:6379/2", + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + } + }, + CACHE_THROTTLING: { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://127.0.0.1:6379/3", + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + } + } +} + +# For celery +REDIS_QUEUE = { + "host": "127.0.0.1", + "port": 6379, + "db": 4 +} + + # for celery BROKER_URL = 'redis://%s:%s/%s' % (REDIS_QUEUE["host"], str(REDIS_QUEUE["port"]), str(REDIS_QUEUE["db"])) CELERY_ACCEPT_CONTENT = ["json"] diff --git a/submission/urls/oj.py b/submission/urls/oj.py index d86bfa5..30cc62f 100644 --- a/submission/urls/oj.py +++ b/submission/urls/oj.py @@ -1,9 +1,8 @@ from django.conf.urls import url -from ..views.oj import SubmissionAPI, SubmissionListAPI, ContestSubmissionListAPI +from ..views.oj import SubmissionAPI, SubmissionListAPI 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"), + url(r"^submissions/?$", SubmissionListAPI.as_view(), name="submission_list_api") ] diff --git a/submission/views/oj.py b/submission/views/oj.py index 8358390..af3584f 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -1,4 +1,3 @@ -from django_redis import get_redis_connection from account.decorators import login_required, check_contest_permission from judge.tasks import judge_task @@ -9,6 +8,7 @@ from utils.throttling import TokenBucket, BucketController from ..models import Submission from ..serializers import CreateSubmissionSerializer, SubmissionModelSerializer from ..serializers import SubmissionSafeSerializer, SubmissionListSerializer +from utils.cache import throttling_cache # from judge.dispatcher import JudgeDispatcher @@ -17,7 +17,7 @@ from ..serializers import SubmissionSafeSerializer, SubmissionListSerializer def _submit(response, user, problem_id, language, code, contest_id): # TODO: 预设默认值,需修改 controller = BucketController(user_id=user.id, - redis_conn=get_redis_connection("Throttling"), + redis_conn=throttling_cache, default_capacity=30) bucket = TokenBucket(fill_rate=10, capacity=20, last_capacity=controller.last_capacity, diff --git a/utils/cache.py b/utils/cache.py new file mode 100644 index 0000000..d72c81a --- /dev/null +++ b/utils/cache.py @@ -0,0 +1,6 @@ +from django.conf import settings +from django_redis import get_redis_connection + +judge_queue_cache = get_redis_connection(settings.CACHE_JUDGE_QUEUE) +throttling_cache = get_redis_connection(settings.CACHE_THROTTLING) +default_cache = get_redis_connection("default") diff --git a/utils/constants.py b/utils/constants.py new file mode 100644 index 0000000..66ef169 --- /dev/null +++ b/utils/constants.py @@ -0,0 +1,2 @@ +class CacheKey: + waiting_queue = "waiting_queue" From df185a233f46e40af8f62b6ec4946593559f87ab Mon Sep 17 00:00:00 2001 From: zemal Date: Tue, 15 Aug 2017 20:45:49 +0800 Subject: [PATCH 035/106] fix flake8 prompt migrations errors --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 2e0788e..3198bbc 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,7 @@ [flake8] exclude = xss_filter.py, - migrations/, + */migrations/, *settings.py max-line-length = 180 inline-quotes = " From 1587192ff9d44559ac3df277f2db9f7af6a7899a Mon Sep 17 00:00:00 2001 From: zemal Date: Tue, 15 Aug 2017 21:05:41 +0800 Subject: [PATCH 036/106] add problem_statistic_info --- judge/dispatcher.py | 42 ++++++++++--------- problem/migrations/0005_auto_20170815_1258.py | 26 ++++++++++++ problem/models.py | 3 ++ submission/views/oj.py | 12 +++--- utils/cache.py | 2 +- 5 files changed, 58 insertions(+), 27 deletions(-) create mode 100644 problem/migrations/0005_auto_20170815_1258.py diff --git a/judge/dispatcher.py b/judge/dispatcher.py index 501eabf..c64fb4d 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -14,7 +14,7 @@ from contest.models import ContestRuleType, ACMContestRank, OIContestRank from judge.languages import languages from problem.models import Problem, ProblemRuleType, ContestProblem from submission.models import JudgeStatus, Submission -from utils.cache import judge_queue_cache +from utils.cache import judge_cache from utils.constants import CacheKey logger = logging.getLogger(__name__) @@ -22,10 +22,10 @@ logger = logging.getLogger(__name__) # 继续处理在队列中的问题 def process_pending_task(): - if judge_queue_cache.llen(CacheKey.waiting_queue): + if judge_cache.llen(CacheKey.waiting_queue): # 防止循环引入 from judge.tasks import judge_task - data = json.loads(judge_queue_cache.rpop(CacheKey.waiting_queue)) + data = json.loads(judge_cache.rpop(CacheKey.waiting_queue)) judge_task.delay(**data) @@ -33,7 +33,7 @@ class JudgeDispatcher(object): def __init__(self, submission_id, problem_id): token = JudgeServerToken.objects.first().token self.token = hashlib.sha256(token.encode("utf-8")).hexdigest() - self.redis_conn = judge_queue_cache + 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")\ @@ -98,8 +98,8 @@ class JudgeDispatcher(object): "spj_compile_config": spj_config.get("compile"), "spj_src": self.problem.spj_code } - self.submission.result = JudgeStatus.JUDGING - self.submission.save() + + Submission.objects.filter(id=self.submission.id).update(result=JudgeStatus.JUDGING) # TODO: try catch resp = self._request(urljoin(server.service_url, "/judge"), data=data) @@ -123,11 +123,12 @@ class JudgeDispatcher(object): self.submission.save() self.release_judge_res(server.id) + self.update_problem_status() + if self.submission.contest_id: - self.update_contest_problem_status() self.update_contest_rank() else: - self.update_problem_status() + self.update_user_profile() # 至此判题结束,尝试处理任务队列中剩余的任务 process_pending_task() @@ -138,13 +139,21 @@ class JudgeDispatcher(object): return self._request(urljoin(service_url, "compile_spj"), data=data) def update_problem_status(self): + self.problem.add_submission_number() + if self.submission.result == JudgeStatus.ACCEPTED: + self.problem.add_ac_number() with transaction.atomic(): - # 更新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() + if self.submission.contest_id: + problem = ContestProblem.objects.select_for_update().get(_id=self.problem.id, contest_id=self.contest.id) + else: + problem = Problem.objects.select_related().get(_id=self.problem.id) + info = problem.statistic_info + info[self.submission.result] = info.get(self.submission.result, 0) + 1 + problem.statistic_info = info + problem.save(update_fields=["statistic_info"]) + def update_user_profile(self): + with transaction.atomic(): # 更新user profile user = User.objects.select_for_update().get(id=self.submission.user_id) user_profile = user.userprofile @@ -163,13 +172,6 @@ class JudgeDispatcher(object): 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") diff --git a/problem/migrations/0005_auto_20170815_1258.py b/problem/migrations/0005_auto_20170815_1258.py new file mode 100644 index 0000000..1949696 --- /dev/null +++ b/problem/migrations/0005_auto_20170815_1258.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2017-08-15 12:58 +from __future__ import unicode_literals + +from django.db import migrations +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('problem', '0004_auto_20170501_0637'), + ] + + operations = [ + migrations.AddField( + model_name='contestproblem', + name='statistic_info', + field=jsonfield.fields.JSONField(default={}), + ), + migrations.AddField( + model_name='problem', + name='statistic_info', + field=jsonfield.fields.JSONField(default={}), + ), + ] diff --git a/problem/models.py b/problem/models.py index 0a8fda2..6915ee0 100644 --- a/problem/models.py +++ b/problem/models.py @@ -57,6 +57,9 @@ class AbstractProblem(models.Model): source = models.CharField(max_length=200, blank=True, null=True) total_submit_number = models.BigIntegerField(default=0) total_accepted_number = models.BigIntegerField(default=0) + # {0: 0, 1: 0, 2: 0, 3: 0 ...} + # the first number means JudgeStatus, the second number present count + statistic_info = JSONField(default={}) class Meta: db_table = "problem" diff --git a/submission/views/oj.py b/submission/views/oj.py index af3584f..5af2897 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -95,21 +95,21 @@ class SubmissionListAPI(APIView): if request.GET.get("contest_id"): return self._get_contest_submission_list(request) - subs = Submission.objects.filter(contest_id__isnull=True) - return self.process_submissions(request, subs) + submissions = Submission.objects.filter(contest_id__isnull=True) + return self.process_submissions(request, submissions) @check_contest_permission def _get_contest_submission_list(self, request): subs = Submission.objects.filter(contest_id=self.contest.id) return self.process_submissions(request, subs) - def process_submissions(self, request, subs): + def process_submissions(self, request, submissions): problem_id = request.GET.get("problem_id") if problem_id: - subs = subs.filter(problem_id=problem_id) + submissions = submissions.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) + submissions = submissions.filter(user_id=request.user.id) + data = self.paginate_data(request, submissions) data["results"] = SubmissionListSerializer(data["results"], many=True, user=request.user).data return self.success(data) diff --git a/utils/cache.py b/utils/cache.py index d72c81a..c77131f 100644 --- a/utils/cache.py +++ b/utils/cache.py @@ -1,6 +1,6 @@ from django.conf import settings from django_redis import get_redis_connection -judge_queue_cache = get_redis_connection(settings.CACHE_JUDGE_QUEUE) +judge_cache = get_redis_connection(settings.CACHE_JUDGE_QUEUE) throttling_cache = get_redis_connection(settings.CACHE_THROTTLING) default_cache = get_redis_connection("default") From 57b75fd5117dc179849cbe05babe4e50defbb286 Mon Sep 17 00:00:00 2001 From: zemal Date: Wed, 16 Aug 2017 15:33:27 +0800 Subject: [PATCH 037/106] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dproblem=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- judge/dispatcher.py | 5 +++-- problem/serializers.py | 2 ++ submission/views/oj.py | 4 +--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/judge/dispatcher.py b/judge/dispatcher.py index c64fb4d..1bcdba2 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -25,7 +25,7 @@ def process_pending_task(): if judge_cache.llen(CacheKey.waiting_queue): # 防止循环引入 from judge.tasks import judge_task - data = json.loads(judge_cache.rpop(CacheKey.waiting_queue)) + data = json.loads(judge_cache.rpop(CacheKey.waiting_queue).decode("utf-8")) judge_task.delay(**data) @@ -148,7 +148,8 @@ class JudgeDispatcher(object): else: problem = Problem.objects.select_related().get(_id=self.problem.id) info = problem.statistic_info - info[self.submission.result] = info.get(self.submission.result, 0) + 1 + result = str(self.submission.result) + info[result] = info.get(result, 0) + 1 problem.statistic_info = info problem.save(update_fields=["statistic_info"]) diff --git a/problem/serializers.py b/problem/serializers.py index fff369d..92b0f1f 100644 --- a/problem/serializers.py +++ b/problem/serializers.py @@ -83,6 +83,7 @@ class ProblemSerializer(serializers.ModelSerializer): create_time = DateTimeTZField() last_update_time = DateTimeTZField() created_by = UsernameSerializer() + statistic_info = serializers.JSONField() class Meta: model = Problem @@ -97,6 +98,7 @@ class ContestProblemSerializer(serializers.ModelSerializer): create_time = DateTimeTZField() last_update_time = DateTimeTZField() created_by = UsernameSerializer() + statistic_info = serializers.JSONField() class Meta: model = ContestProblem diff --git a/submission/views/oj.py b/submission/views/oj.py index 5af2897..b133284 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -1,6 +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 contest.models import Contest, ContestStatus from utils.api import APIView, validate_serializer @@ -11,9 +12,6 @@ from ..serializers import SubmissionSafeSerializer, SubmissionListSerializer from utils.cache import throttling_cache -# from judge.dispatcher import JudgeDispatcher - - def _submit(response, user, problem_id, language, code, contest_id): # TODO: 预设默认值,需修改 controller = BucketController(user_id=user.id, From d1767e775d0640f3448ff3b428476c992c819100 Mon Sep 17 00:00:00 2001 From: zemal Date: Sat, 19 Aug 2017 06:10:48 +0800 Subject: [PATCH 038/106] =?UTF-8?q?=E5=AE=8C=E5=96=84captchaapi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/urls/oj.py | 4 +- utils/captcha/__init__.py | 201 ++++++++++++++++++++++++-------------- utils/captcha/views.py | 12 ++- 3 files changed, 136 insertions(+), 81 deletions(-) diff --git a/account/urls/oj.py b/account/urls/oj.py index 34b267c..f9a3c7c 100644 --- a/account/urls/oj.py +++ b/account/urls/oj.py @@ -4,6 +4,8 @@ from ..views.oj import (ApplyResetPasswordAPI, ResetPasswordAPI, UserChangePasswordAPI, UserRegisterAPI, UserLoginAPI, UserLogoutAPI) +from utils.captcha.views import CaptchaAPIView + urlpatterns = [ url(r"^login/?$", UserLoginAPI.as_view(), name="user_login_api"), url(r"^logout/?$", UserLogoutAPI.as_view(), name="user_logout_api"), @@ -11,5 +13,5 @@ urlpatterns = [ url(r"^change_password/?$", UserChangePasswordAPI.as_view(), name="user_change_password_api"), url(r"^apply_reset_password/?$", ApplyResetPasswordAPI.as_view(), name="apply_reset_password_api"), url(r"^reset_password/?$", ResetPasswordAPI.as_view(), name="apply_reset_password_api"), - url(r"^captcha/?$", "utils.captcha.views.show_captcha", name="show_captcha"), + url(r"^captcha/?$", CaptchaAPIView.as_view(), name="show_captcha"), ] diff --git a/utils/captcha/__init__.py b/utils/captcha/__init__.py index ee3fe25..e360b72 100644 --- a/utils/captcha/__init__.py +++ b/utils/captcha/__init__.py @@ -1,12 +1,9 @@ """ -Copyright 2013 TY - +Copyright 2017 TY Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,95 +12,147 @@ limitations under the License. """ import os -import time import random - -from io import BytesIO -from django.http import HttpResponse +from math import ceil +from six import BytesIO from PIL import Image, ImageDraw, ImageFont +__version__ = '0.3.3' +current_path = os.path.normpath(os.path.dirname(__file__)) class Captcha(object): def __init__(self, request): + """ something init """ - 初始化,设置各种属性 - """ - self.django_request = request - self.session_key = "_django_captcha_key" - self.captcha_expires_time = "_django_captcha_expires_time" - # 验证码图片尺寸 - self.img_width = 90 + self.django_request = request + self.session_key = '_django_captcha_key' + self.words = ["hello", "word"] + + # image size (pix) + self.img_width = 150 self.img_height = 30 - def _get_font_size(self, code): - """ - 将图片高度的80%作为字体大小 - """ + self.type = 'number' + self.mode = 'number' + + def _get_font_size(self): s1 = int(self.img_height * 0.8) - s2 = int(self.img_width / len(code)) - return int(min((s1, s2)) + max((s1, s2)) * 0.05) + s2 = int(self.img_width/len(self.code)) + return int(min((s1, s2)) + max((s1, s2))*0.05) + + def _get_words(self): + """ words list + """ + + # TODO 扩充单词表 + + if self.words: + return set(self.words) + def _set_answer(self, answer): - """ - 设置答案和过期时间 - """ self.django_request.session[self.session_key] = str(answer) - self.django_request.session[self.captcha_expires_time] = time.time() + 60 - def _make_code(self): + def _generate(self): + # 英文单词验证码 + def word(): + code = random.sample(self._get_words(), 1)[0] + self._set_answer(code) + return code + + # 数字公式验证码 + def number(): + m, n = 1, 50 + x = random.randrange(m, n) + y = random.randrange(m, n) + + r = random.randrange(0, 2) + if r == 0: + code = "%s - %s = ?" % (x, y) + z = x - y + else: + code = "%s + %s = ?" % (x, y) + z = x + y + self._set_answer(z) + return code + + fun = eval(self.mode.lower()) + return fun() + + def get(self): + """ return captcha image bytes """ - 生成随机数或随机字符串 + + # font color + self.font_color = ['black', 'darkblue', 'darkred'] + + # background color + self.background = (random.randrange(230, 255), random.randrange(230, 255), random.randrange(230, 255)) + + # font path + self.font_path = os.path.join(current_path, 'timesbi.ttf') # or Menlo.ttc + + self.django_request.session[self.session_key] = '' + im = Image.new('RGB', (self.img_width, self.img_height), self.background) + self.code = self._generate() + + # set font size automaticly + self.font_size = self._get_font_size() + + # creat + draw = ImageDraw.Draw(im) + + # draw noisy point/line + if self.mode == 'word': + c = int(8/len(self.code)*3) or 3 + elif self.mode == 'number': + c = 4 + + for i in range(random.randrange(c-2, c)): + line_color = (random.randrange(0, 255), random.randrange(0, 255), random.randrange(0, 255)) + xy = (random.randrange(0, int(self.img_width*0.2)), random.randrange(0, self.img_height), + random.randrange(int(3*self.img_width/4), self.img_width), random.randrange(0, self.img_height)) + draw.line(xy, fill=line_color, width=int(self.font_size*0.1)) + + # main part + j = int(self.font_size*0.3) + k = int(self.font_size*0.5) + x = random.randrange(j, k) + + for i in self.code: + # 上下抖动量,字数越多,上下抖动越大 + m = int(len(self.code)) + y = random.randrange(1, 3) + + if i in ('+', '=', '?'): + # 对计算符号等特殊字符放大处理 + m = ceil(self.font_size*0.8) + else: + # 字体大小变化量,字数越少,字体大小变化越多 + m = random.randrange(0, int(45 / self.font_size) + int(self.font_size/5)) + + self.font = ImageFont.truetype(self.font_path.replace('\\', '/'), self.font_size + int(ceil(m))) + draw.text((x, y), i, font=self.font, fill=random.choice(self.font_color)) + x += self.font_size*0.9 + + del x + del draw + with BytesIO() as buf: + im.save(buf, 'gif') + buf_str = buf.getvalue() + return buf_str + + def validate(self, code): + """ user input validate """ - string = random.sample("abcdefghkmnpqrstuvwxyzABCDEFGHGKMNOPQRSTUVWXYZ23456789", 4) - self._set_answer("".join(string)) - return string - def display(self): - """ - 生成验证码图片 - """ - background = (random.randrange(200, 255), random.randrange(200, 255), random.randrange(200, 255)) - code_color = (random.randrange(0, 50), random.randrange(0, 50), random.randrange(0, 50), 255) - - font_path = os.path.join(os.path.normpath(os.path.dirname(__file__)), "timesbi.ttf") - - image = Image.new("RGB", (self.img_width, self.img_height), background) - code = self._make_code() - font_size = self._get_font_size(code) - draw = ImageDraw.Draw(image) - - # x是第一个字母的x坐标 - x = random.randrange(int(font_size * 0.3), int(font_size * 0.5)) - - for i in code: - # 字符y坐标 - y = random.randrange(1, 7) - # 随机字符大小 - font = ImageFont.truetype(font_path.replace("\\", "/"), font_size + random.randrange(-3, 7)) - draw.text((x, y), i, font=font, fill=code_color) - # 随机化字符之间的距离 字符粘连可以降低识别率 - x += font_size * random.randrange(6, 8) / 10 - - buf = BytesIO() - image.save(buf, "gif") - - self.django_request.session[self.session_key] = "".join(code) - return HttpResponse(buf.getvalue(), "image/gif") - - def check(self, code): - """ - 检查用户输入的验证码是否正确 - """ - _code = self.django_request.session.get(self.session_key) or "" - if not _code: - return False - expires_time = self.django_request.session.get(self.captcha_expires_time) or 0 - # 注意 如果验证之后不清除之前的验证码的话 可能会造成重复验证的现象 - del self.django_request.session[self.session_key] - del self.django_request.session[self.captcha_expires_time] - if _code.lower() == str(code).lower() and time.time() < expires_time: - return True - else: + if not code: return False + + code = code.strip() + _code = self.django_request.session.get(self.session_key) or '' + self.django_request.session[self.session_key] = '' + return _code.lower() == str(code).lower() + diff --git a/utils/captcha/views.py b/utils/captcha/views.py index ba42059..36258d3 100644 --- a/utils/captcha/views.py +++ b/utils/captcha/views.py @@ -1,7 +1,11 @@ -from django.http import HttpResponse +from base64 import b64encode -from utils.captcha import Captcha +from . import Captcha +from ..api import APIView -def show_captcha(request): - return HttpResponse(Captcha(request).display(), content_type="image/gif") +class CaptchaAPIView(APIView): + def get(self, request): + img_prefix = "data:image/png;base64," + img = img_prefix + b64encode(Captcha(request).get()).decode("utf-8") + return self.success(img) From 0647312124757ff56aa24ebe874cc939f6e44f32 Mon Sep 17 00:00:00 2001 From: zemal Date: Sat, 19 Aug 2017 17:25:39 +0800 Subject: [PATCH 039/106] add username or email check api --- account/serializers.py | 8 ++++++-- account/urls/oj.py | 3 ++- account/views/oj.py | 25 +++++++++++++++++++++++-- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/account/serializers.py b/account/serializers.py index 6c50194..68ba2b2 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -11,11 +11,15 @@ class UserLoginSerializer(serializers.Serializer): tfa_code = serializers.CharField(min_length=6, max_length=6, required=False, allow_null=True) +class UsernameOrEmailCheckSerializer(serializers.Serializer): + username = serializers.CharField(max_length=30, required=False) + email = serializers.EmailField(max_length=30, required=False) + class UserRegisterSerializer(serializers.Serializer): username = serializers.CharField(max_length=30) password = serializers.CharField(max_length=30, min_length=6) - email = serializers.EmailField(max_length=254) - captcha = serializers.CharField(max_length=4, min_length=4) + email = serializers.EmailField(max_length=30) + captcha = serializers.CharField(max_length=4, min_length=1) class UserChangePasswordSerializer(serializers.Serializer): diff --git a/account/urls/oj.py b/account/urls/oj.py index f9a3c7c..a0b8c72 100644 --- a/account/urls/oj.py +++ b/account/urls/oj.py @@ -2,7 +2,7 @@ from django.conf.urls import url from ..views.oj import (ApplyResetPasswordAPI, ResetPasswordAPI, UserChangePasswordAPI, UserRegisterAPI, - UserLoginAPI, UserLogoutAPI) + UserLoginAPI, UserLogoutAPI, UsernameOrEmailCheck) from utils.captcha.views import CaptchaAPIView @@ -14,4 +14,5 @@ urlpatterns = [ url(r"^apply_reset_password/?$", ApplyResetPasswordAPI.as_view(), name="apply_reset_password_api"), url(r"^reset_password/?$", ResetPasswordAPI.as_view(), name="apply_reset_password_api"), url(r"^captcha/?$", CaptchaAPIView.as_view(), name="show_captcha"), + url(r"^check_username_or_email", UsernameOrEmailCheck.as_view(), name="check_username_or_email") ] diff --git a/account/views/oj.py b/account/views/oj.py index b1cef63..3bfb259 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -16,7 +16,7 @@ from ..models import User, UserProfile from ..serializers import (ApplyResetPasswordSerializer, ResetPasswordSerializer, UserChangePasswordSerializer, UserLoginSerializer, - UserRegisterSerializer) + UserRegisterSerializer, UsernameOrEmailCheckSerializer) from ..tasks import send_email_async @@ -58,6 +58,27 @@ class UserLogoutAPI(APIView): return self.success({}) +class UsernameOrEmailCheck(APIView): + @validate_serializer(UsernameOrEmailCheckSerializer) + def post(self, request): + """ + check username or email is duplicate + """ + data = request.data + # True means OK. + result = { + "username": True, + "email": True + } + if data.get("username"): + if User.objects.filter(username=data["username"]).exists(): + result["username"] = False + if data.get("email"): + if User.objects.filter(email=data["email"]).exists(): + result["email"] = False + return self.success(result) + + class UserRegisterAPI(APIView): @validate_serializer(UserRegisterSerializer) def post(self, request): @@ -66,7 +87,7 @@ class UserRegisterAPI(APIView): """ data = request.data captcha = Captcha(request) - if not captcha.check(data["captcha"]): + if not captcha.validate(data["captcha"]): return self.error("Invalid captcha") try: User.objects.get(username=data["username"]) From 3b1f02c35654e2e4ef7560c1a03b59845c34d0ca Mon Sep 17 00:00:00 2001 From: zemal Date: Sun, 20 Aug 2017 08:35:59 +0800 Subject: [PATCH 040/106] =?UTF-8?q?=E6=95=B4=E7=90=86=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/serializers.py | 2 +- account/urls/oj.py | 9 +- account/urls/user.py | 12 -- account/views/oj.py | 180 ++++++++++++++++++++++--- account/views/user.py | 179 ------------------------ oj/local_settings.py | 23 ---- oj/settings.py | 2 + oj/urls.py | 1 - utils/captcha/__init__.py | 58 ++++---- utils/management/commands/initadmin.py | 2 +- 10 files changed, 200 insertions(+), 268 deletions(-) delete mode 100644 account/urls/user.py delete mode 100644 account/views/user.py diff --git a/account/serializers.py b/account/serializers.py index 68ba2b2..69e312a 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -15,6 +15,7 @@ class UsernameOrEmailCheckSerializer(serializers.Serializer): username = serializers.CharField(max_length=30, required=False) email = serializers.EmailField(max_length=30, required=False) + class UserRegisterSerializer(serializers.Serializer): username = serializers.CharField(max_length=30) password = serializers.CharField(max_length=30, min_length=6) @@ -46,7 +47,6 @@ class UserProfileSerializer(serializers.ModelSerializer): class UserInfoSerializer(serializers.ModelSerializer): - class Meta: model = UserProfile diff --git a/account/urls/oj.py b/account/urls/oj.py index a0b8c72..1d65729 100644 --- a/account/urls/oj.py +++ b/account/urls/oj.py @@ -2,7 +2,8 @@ from django.conf.urls import url from ..views.oj import (ApplyResetPasswordAPI, ResetPasswordAPI, UserChangePasswordAPI, UserRegisterAPI, - UserLoginAPI, UserLogoutAPI, UsernameOrEmailCheck) + UserLoginAPI, UserLogoutAPI, UsernameOrEmailCheck, + SSOAPI, AvatarUploadAPI, TwoFactorAuthAPI, UserProfileAPI) from utils.captcha.views import CaptchaAPIView @@ -14,5 +15,9 @@ urlpatterns = [ url(r"^apply_reset_password/?$", ApplyResetPasswordAPI.as_view(), name="apply_reset_password_api"), url(r"^reset_password/?$", ResetPasswordAPI.as_view(), name="apply_reset_password_api"), url(r"^captcha/?$", CaptchaAPIView.as_view(), name="show_captcha"), - url(r"^check_username_or_email", UsernameOrEmailCheck.as_view(), name="check_username_or_email") + url(r"^check_username_or_email", UsernameOrEmailCheck.as_view(), name="check_username_or_email"), + url(r"^profile/?$", UserProfileAPI.as_view(), name="user_profile_api"), + url(r"^avatar/upload/?$", AvatarUploadAPI.as_view(), name="avatar_upload_api"), + url(r"^sso/?$", SSOAPI.as_view(), name="sso_api"), + url(r"^two_factor_auth/?$", TwoFactorAuthAPI.as_view(), name="two_factor_auth_api") ] diff --git a/account/urls/user.py b/account/urls/user.py deleted file mode 100644 index 8fdb7db..0000000 --- a/account/urls/user.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.conf.urls import url - -from ..views.user import (SSOAPI, AvatarUploadAPI, TwoFactorAuthAPI, - UserProfileAPI) - -urlpatterns = [ - # url(r"^username/?$", UserNameAPI.as_view(), name="user_name_api"), - url(r"^profile/?$", UserProfileAPI.as_view(), name="user_profile_api"), - url(r"^avatar/upload/?$", AvatarUploadAPI.as_view(), name="avatar_upload_api"), - url(r"^sso/?$", SSOAPI.as_view(), name="sso_api"), - url(r"^two_factor_auth/?$", TwoFactorAuthAPI.as_view(), name="two_factor_auth_api") -] diff --git a/account/views/oj.py b/account/views/oj.py index 3bfb259..351224c 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -1,13 +1,18 @@ +import os +import qrcode +from io import BytesIO from datetime import timedelta +from otpauth import OtpAuth from django.conf import settings from django.contrib import auth -from django.core.exceptions import MultipleObjectsReturned from django.utils.timezone import now -from otpauth import OtpAuth +from django.http import HttpResponse +from django.views.decorators.csrf import ensure_csrf_cookie +from django.utils.decorators import method_decorator from conf.models import WebsiteConfig -from utils.api import APIView, validate_serializer +from utils.api import APIView, validate_serializer, CSRFExemptAPIView from utils.captcha import Captcha from utils.shortcuts import rand_str @@ -17,9 +22,153 @@ from ..serializers import (ApplyResetPasswordSerializer, ResetPasswordSerializer, UserChangePasswordSerializer, UserLoginSerializer, UserRegisterSerializer, UsernameOrEmailCheckSerializer) +from ..serializers import (SSOSerializer, TwoFactorAuthCodeSerializer, + UserProfileSerializer, + EditUserProfileSerializer, AvatarUploadForm) from ..tasks import send_email_async +class UserProfileAPI(APIView): + """ + 判断是否登录, 若登录返回用户信息 + """ + @method_decorator(ensure_csrf_cookie) + def get(self, request, **kwargs): + user = request.user + if not user.is_authenticated(): + return self.success(0) + + username = request.GET.get("username") + try: + if username: + user = User.objects.get(username=username, is_disabled=False) + else: + user = request.user + except User.DoesNotExist: + return self.error("User does not exist") + profile = UserProfile.objects.get(user=user) + return self.success(UserProfileSerializer(profile).data) + + @validate_serializer(EditUserProfileSerializer) + @login_required + def put(self, request): + data = request.data + user_profile = request.user.userprofile + print(data) + if data.get("avatar"): + user_profile.avatar = data["avatar"] + else: + user_profile.mood = data["mood"] + user_profile.blog = data["blog"] + user_profile.school = data["school"] + user_profile.student_id = data["student_id"] + user_profile.phone_number = data["phone_number"] + user_profile.major = data["major"] + # Timezone & language 暂时不加 + user_profile.save() + return self.success("Succeeded") + + +class AvatarUploadAPI(CSRFExemptAPIView): + request_parsers = () + + def post(self, request): + form = AvatarUploadForm(request.POST, request.FILES) + if form.is_valid(): + avatar = form.cleaned_data["file"] + else: + return self.error("Upload failed") + if avatar.size > 1024 * 1024: + return self.error("Picture too large") + if os.path.splitext(avatar.name)[-1].lower() not in [".gif", ".jpg", ".jpeg", ".bmp", ".png"]: + return self.error("Unsupported file format") + + name = "avatar_" + rand_str(5) + os.path.splitext(avatar.name)[-1] + with open(os.path.join(settings.IMAGE_UPLOAD_DIR, name), "wb") as img: + for chunk in avatar: + img.write(chunk) + print(os.path.join(settings.IMAGE_UPLOAD_DIR, name)) + return self.success({"path": "/static/upload/" + name}) + + +class SSOAPI(APIView): + @login_required + def get(self, request): + callback = request.GET.get("callback", None) + if not callback: + return self.error("Parameter Error") + token = rand_str() + request.user.auth_token = token + request.user.save() + return self.success({"redirect_url": callback + "?token=" + token, + "callback": callback}) + + @validate_serializer(SSOSerializer) + def post(self, request): + data = request.data + try: + User.objects.get(open_api_appkey=data["appkey"]) + except User.DoesNotExist: + return self.error("Invalid appkey") + try: + user = User.objects.get(auth_token=data["token"]) + user.auth_token = None + user.save() + return self.success({"username": user.username, + "id": user.id, + "admin_type": user.admin_type, + "avatar": user.userprofile.avatar}) + except User.DoesNotExist: + return self.error("User does not exist") + + +class TwoFactorAuthAPI(APIView): + @login_required + def get(self, request): + """ + Get QR code + """ + user = request.user + if user.two_factor_auth: + return self.error("Already open 2FA") + token = rand_str() + user.tfa_token = token + user.save() + + config = WebsiteConfig.objects.first() + image = qrcode.make(OtpAuth(token).to_uri("totp", config.base_url, config.name)) + buf = BytesIO() + image.save(buf, "gif") + + return HttpResponse(buf.getvalue(), "image/gif") + + @login_required + @validate_serializer(TwoFactorAuthCodeSerializer) + def post(self, request): + """ + Open 2FA + """ + code = request.data["code"] + user = request.user + if OtpAuth(user.tfa_token).valid_totp(code): + user.two_factor_auth = True + user.save() + return self.success("Succeeded") + else: + return self.error("Invalid captcha") + + @login_required + @validate_serializer(TwoFactorAuthCodeSerializer) + def put(self, request): + code = request.data["code"] + user = request.user + if OtpAuth(user.tfa_token).valid_totp(code): + user.two_factor_auth = False + user.save() + else: + return self.error("Invalid captcha") + + class UserLoginAPI(APIView): @validate_serializer(UserLoginSerializer) def post(self, request): @@ -89,23 +238,16 @@ class UserRegisterAPI(APIView): captcha = Captcha(request) if not captcha.validate(data["captcha"]): return self.error("Invalid captcha") - try: - User.objects.get(username=data["username"]) + if User.objects.filter(username=data["username"]).exists(): return self.error("Username already exists") - except User.DoesNotExist: - pass - try: - User.objects.get(email=data["email"]) + if User.objects.filter(email=data["email"]).exists(): return self.error("Email already exists") - # Some old data has duplicate email - except MultipleObjectsReturned: - return self.error("Email already exists") - except User.DoesNotExist: - user = User.objects.create(username=data["username"], email=data["email"]) - user.set_password(data["password"]) - user.save() - UserProfile.objects.create(user=user) - return self.success("Succeeded") + + user = User.objects.create(username=data["username"], email=data["email"]) + user.set_password(data["password"]) + user.save() + UserProfile.objects.create(user=user, time_zone=settings.USER_DEFAULT_TZ) + return self.success("Succeeded") class UserChangePasswordAPI(APIView): @@ -117,7 +259,7 @@ class UserChangePasswordAPI(APIView): """ data = request.data captcha = Captcha(request) - if not captcha.check(data["captcha"]): + if not captcha.validate(data["captcha"]): return self.error("Invalid captcha") username = request.user.username user = auth.authenticate(username=username, password=data["old_password"]) diff --git a/account/views/user.py b/account/views/user.py deleted file mode 100644 index 9bbbf03..0000000 --- a/account/views/user.py +++ /dev/null @@ -1,179 +0,0 @@ -import os -from io import BytesIO - -import qrcode -from django.conf import settings -from django.http import HttpResponse -from django.views.decorators.csrf import ensure_csrf_cookie -from django.utils.decorators import method_decorator -from otpauth import OtpAuth - -from conf.models import WebsiteConfig -from utils.api import APIView, validate_serializer, CSRFExemptAPIView -from utils.shortcuts import rand_str - -from ..decorators import login_required -from ..models import User, UserProfile -from ..serializers import (SSOSerializer, TwoFactorAuthCodeSerializer, - UserProfileSerializer, - EditUserProfileSerializer, AvatarUploadForm) - - -class UserNameAPI(APIView): - @method_decorator(ensure_csrf_cookie) - 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 UserProfileAPI(APIView): - """ - 判断是否登录, 若登录返回用户信息 - """ - @method_decorator(ensure_csrf_cookie) - def get(self, request, **kwargs): - user = request.user - if not user.is_authenticated(): - return self.success(0) - - username = request.GET.get("username") - try: - if username: - user = User.objects.get(username=username, is_disabled=False) - else: - user = request.user - except User.DoesNotExist: - return self.error("User does not exist") - profile = UserProfile.objects.get(user=user) - return self.success(UserProfileSerializer(profile).data) - - @validate_serializer(EditUserProfileSerializer) - @login_required - def put(self, request): - data = request.data - user_profile = request.user.userprofile - print(data) - if data.get("avatar"): - user_profile.avatar = data["avatar"] - else: - user_profile.mood = data["mood"] - user_profile.blog = data["blog"] - user_profile.school = data["school"] - user_profile.student_id = data["student_id"] - user_profile.phone_number = data["phone_number"] - user_profile.major = data["major"] - # Timezone & language 暂时不加 - user_profile.save() - return self.success("Succeeded") - - -class AvatarUploadAPI(CSRFExemptAPIView): - request_parsers = () - - def post(self, request): - form = AvatarUploadForm(request.POST, request.FILES) - if form.is_valid(): - avatar = form.cleaned_data["file"] - else: - return self.error("Upload failed") - if avatar.size > 1024 * 1024: - return self.error("Picture too large") - if os.path.splitext(avatar.name)[-1].lower() not in [".gif", ".jpg", ".jpeg", ".bmp", ".png"]: - return self.error("Unsupported file format") - - name = "avatar_" + rand_str(5) + os.path.splitext(avatar.name)[-1] - with open(os.path.join(settings.IMAGE_UPLOAD_DIR, name), "wb") as img: - for chunk in avatar: - img.write(chunk) - print(os.path.join(settings.IMAGE_UPLOAD_DIR, name)) - return self.success({"path": "/static/upload/" + name}) - - -class SSOAPI(APIView): - @login_required - def get(self, request): - callback = request.GET.get("callback", None) - if not callback: - return self.error("Parameter Error") - token = rand_str() - request.user.auth_token = token - request.user.save() - return self.success({"redirect_url": callback + "?token=" + token, - "callback": callback}) - - @validate_serializer(SSOSerializer) - def post(self, request): - data = request.data - try: - User.objects.get(open_api_appkey=data["appkey"]) - except User.DoesNotExist: - return self.error("Invalid appkey") - try: - user = User.objects.get(auth_token=data["token"]) - user.auth_token = None - user.save() - return self.success({"username": user.username, - "id": user.id, - "admin_type": user.admin_type, - "avatar": user.userprofile.avatar}) - except User.DoesNotExist: - return self.error("User does not exist") - - -class TwoFactorAuthAPI(APIView): - @login_required - def get(self, request): - """ - Get QR code - """ - user = request.user - if user.two_factor_auth: - return self.error("Already open 2FA") - token = rand_str() - user.tfa_token = token - user.save() - - config = WebsiteConfig.objects.first() - image = qrcode.make(OtpAuth(token).to_uri("totp", config.base_url, config.name)) - buf = BytesIO() - image.save(buf, "gif") - - return HttpResponse(buf.getvalue(), "image/gif") - - @login_required - @validate_serializer(TwoFactorAuthCodeSerializer) - def post(self, request): - """ - Open 2FA - """ - code = request.data["code"] - user = request.user - if OtpAuth(user.tfa_token).valid_totp(code): - user.two_factor_auth = True - user.save() - return self.success("Succeeded") - else: - return self.error("Invalid captcha") - - @login_required - @validate_serializer(TwoFactorAuthCodeSerializer) - def put(self, request): - code = request.data["code"] - user = request.user - if OtpAuth(user.tfa_token).valid_totp(code): - user.two_factor_auth = False - user.save() - else: - return self.error("Invalid captcha") diff --git a/oj/local_settings.py b/oj/local_settings.py index dfad51b..562f1f0 100644 --- a/oj/local_settings.py +++ b/oj/local_settings.py @@ -10,29 +10,6 @@ DATABASES = { } } -CACHES = { - "default": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": "redis://127.0.0.1:6379/1", - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - } - }, - "JudgeQueue": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": "redis://127.0.0.1:6379/2", - "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", - } - } -} # For celery REDIS_QUEUE = { diff --git a/oj/settings.py b/oj/settings.py index e6a6524..5b75a83 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -97,6 +97,8 @@ USE_L10N = True USE_TZ = True +# in user's profile +USER_DEFAULT_TZ = 'Asia/Shanghai' # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.8/howto/static-files/ diff --git a/oj/urls.py b/oj/urls.py index 1e79aab..5eda628 100644 --- a/oj/urls.py +++ b/oj/urls.py @@ -3,7 +3,6 @@ from django.conf.urls import include, url urlpatterns = [ url(r"^api/", include("account.urls.oj")), url(r"^api/admin/", include("account.urls.admin")), - url(r"^api/account/", include("account.urls.user")), url(r"^api/admin/", include("announcement.urls.admin")), url(r"^api/", include("conf.urls.oj")), url(r"^api/admin/", include("conf.urls.admin")), diff --git a/utils/captcha/__init__.py b/utils/captcha/__init__.py index e360b72..ce687d5 100644 --- a/utils/captcha/__init__.py +++ b/utils/captcha/__init__.py @@ -17,30 +17,30 @@ from math import ceil from six import BytesIO from PIL import Image, ImageDraw, ImageFont -__version__ = '0.3.3' +__version__ = "0.3.3" current_path = os.path.normpath(os.path.dirname(__file__)) -class Captcha(object): +class Captcha(object): def __init__(self, request): """ something init """ self.django_request = request - self.session_key = '_django_captcha_key' + self.session_key = "_django_captcha_key" self.words = ["hello", "word"] # image size (pix) self.img_width = 150 self.img_height = 30 - self.type = 'number' - self.mode = 'number' + self.type = "number" + self.mode = "number" def _get_font_size(self): s1 = int(self.img_height * 0.8) - s2 = int(self.img_width/len(self.code)) - return int(min((s1, s2)) + max((s1, s2))*0.05) + s2 = int(self.img_width / len(self.code)) + return int(min((s1, s2)) + max((s1, s2)) * 0.05) def _get_words(self): """ words list @@ -51,7 +51,6 @@ class Captcha(object): if self.words: return set(self.words) - def _set_answer(self, answer): self.django_request.session[self.session_key] = str(answer) @@ -86,16 +85,16 @@ class Captcha(object): """ # font color - self.font_color = ['black', 'darkblue', 'darkred'] + self.font_color = ["black", "darkblue", "darkred"] # background color self.background = (random.randrange(230, 255), random.randrange(230, 255), random.randrange(230, 255)) # font path - self.font_path = os.path.join(current_path, 'timesbi.ttf') # or Menlo.ttc + self.font_path = os.path.join(current_path, "timesbi.ttf") # or Menlo.ttc - self.django_request.session[self.session_key] = '' - im = Image.new('RGB', (self.img_width, self.img_height), self.background) + self.django_request.session[self.session_key] = "" + im = Image.new("RGB", (self.img_width, self.img_height), self.background) self.code = self._generate() # set font size automaticly @@ -105,20 +104,20 @@ class Captcha(object): draw = ImageDraw.Draw(im) # draw noisy point/line - if self.mode == 'word': - c = int(8/len(self.code)*3) or 3 - elif self.mode == 'number': + if self.mode == "word": + c = int(8 / len(self.code) * 3) or 3 + elif self.mode == "number": c = 4 - for i in range(random.randrange(c-2, c)): + for i in range(random.randrange(c - 2, c)): line_color = (random.randrange(0, 255), random.randrange(0, 255), random.randrange(0, 255)) - xy = (random.randrange(0, int(self.img_width*0.2)), random.randrange(0, self.img_height), - random.randrange(int(3*self.img_width/4), self.img_width), random.randrange(0, self.img_height)) - draw.line(xy, fill=line_color, width=int(self.font_size*0.1)) + xy = (random.randrange(0, int(self.img_width * 0.2)), random.randrange(0, self.img_height), + random.randrange(int(3 * self.img_width / 4), self.img_width), random.randrange(0, self.img_height)) + draw.line(xy, fill=line_color, width=int(self.font_size * 0.1)) # main part - j = int(self.font_size*0.3) - k = int(self.font_size*0.5) + j = int(self.font_size * 0.3) + k = int(self.font_size * 0.5) x = random.randrange(j, k) for i in self.code: @@ -126,21 +125,21 @@ class Captcha(object): m = int(len(self.code)) y = random.randrange(1, 3) - if i in ('+', '=', '?'): + if i in ("+", "=", "?"): # 对计算符号等特殊字符放大处理 - m = ceil(self.font_size*0.8) + m = ceil(self.font_size * 0.8) else: # 字体大小变化量,字数越少,字体大小变化越多 - m = random.randrange(0, int(45 / self.font_size) + int(self.font_size/5)) + m = random.randrange(0, int(45 / self.font_size) + int(self.font_size / 5)) - self.font = ImageFont.truetype(self.font_path.replace('\\', '/'), self.font_size + int(ceil(m))) + self.font = ImageFont.truetype(self.font_path.replace("\\", "/"), self.font_size + int(ceil(m))) draw.text((x, y), i, font=self.font, fill=random.choice(self.font_color)) - x += self.font_size*0.9 + x += self.font_size * 0.9 del x del draw with BytesIO() as buf: - im.save(buf, 'gif') + im.save(buf, "gif") buf_str = buf.getvalue() return buf_str @@ -152,7 +151,6 @@ class Captcha(object): return False code = code.strip() - _code = self.django_request.session.get(self.session_key) or '' - self.django_request.session[self.session_key] = '' + _code = self.django_request.session.get(self.session_key) or "" + self.django_request.session[self.session_key] = "" return _code.lower() == str(code).lower() - diff --git a/utils/management/commands/initadmin.py b/utils/management/commands/initadmin.py index 5829178..bdd84ae 100644 --- a/utils/management/commands/initadmin.py +++ b/utils/management/commands/initadmin.py @@ -13,7 +13,7 @@ class Command(BaseCommand): "would you like to reset it's password?\n" "Input yes to confirm: ")) if input() == "yes": - # for dev + # todo remove this in product env # rand_password = rand_str(length=6) rand_password = "rootroot" admin.save() From 07643e2639d01021242e4cdb0a7c381624785214 Mon Sep 17 00:00:00 2001 From: zemal Date: Sun, 20 Aug 2017 20:32:07 +0800 Subject: [PATCH 041/106] =?UTF-8?q?ranklist=E7=9B=B8=E5=85=B3=E7=9A=84?= =?UTF-8?q?=E6=94=B9=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0003_userprofile_total_score.py | 25 +++++++++++ account/models.py | 20 ++++++--- account/serializers.py | 9 +++- account/urls/oj.py | 6 ++- account/views/oj.py | 20 +++++++-- judge/dispatcher.py | 41 +++++++++++++------ 6 files changed, 97 insertions(+), 24 deletions(-) create mode 100644 account/migrations/0003_userprofile_total_score.py diff --git a/account/migrations/0003_userprofile_total_score.py b/account/migrations/0003_userprofile_total_score.py new file mode 100644 index 0000000..f836b91 --- /dev/null +++ b/account/migrations/0003_userprofile_total_score.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2017-08-20 02:03 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0002_auto_20170209_1028'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='total_score', + field=models.BigIntegerField(default=0), + ), + migrations.RenameField( + model_name='userprofile', + old_name='accepted_problem_number', + new_name='accepted_number', + ), + ] diff --git a/account/models.py b/account/models.py index 3563dd7..cb71bfc 100644 --- a/account/models.py +++ b/account/models.py @@ -69,28 +69,38 @@ def _random_avatar(): class UserProfile(models.Model): user = models.OneToOneField(User) - # Store user problem solution status with json string format - # {"problems": {1: JudgeStatus.ACCEPTED}, "contest_problems": {20: JudgeStatus.PENDING)} + # Store user problem solution status with json string format, Only for problems not contest_problems + # ACM: {1: {status: JudgeStatus.ACCEPTED}} + # OI: {1: {score: 33}} problems_status = JSONField(default={}) avatar = models.CharField(max_length=50, default=_random_avatar) blog = models.URLField(blank=True, null=True) mood = models.CharField(max_length=200, blank=True, null=True) - accepted_problem_number = models.IntegerField(default=0) - submission_number = models.IntegerField(default=0) phone_number = models.CharField(max_length=15, blank=True, null=True) school = models.CharField(max_length=200, blank=True, null=True) major = models.CharField(max_length=200, blank=True, null=True) student_id = models.CharField(max_length=15, blank=True, null=True) time_zone = models.CharField(max_length=32, blank=True, null=True) language = models.CharField(max_length=32, blank=True, null=True) + # for ACM + accepted_number = models.IntegerField(default=0) + # for OI + total_score = models.BigIntegerField(default=0) + submission_number = models.IntegerField(default=0) def add_accepted_problem_number(self): - self.accepted_problem_number = models.F("accepted_problem_number") + 1 + self.accepted_number = models.F("accepted_number") + 1 self.save() def add_submission_number(self): self.submission_number = models.F("submission_number") + 1 self.save() + # 计算总分时, 应先减掉上次该题所得分数, 然后再加上本次所得分数 + def add_score(self, this_time_score, last_time_score=None): + last_time_score = last_time_score or 0 + self.total_score = models.F("total_score") - last_time_score + this_time_score + self.save() + class Meta: db_table = "user_profile" diff --git a/account/serializers.py b/account/serializers.py index 69e312a..9b10fc3 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -1,6 +1,6 @@ from django import forms -from utils.api import DateTimeTZField, serializers +from utils.api import DateTimeTZField, serializers, UsernameSerializer from .models import AdminType, ProblemPermission, User, UserProfile @@ -97,3 +97,10 @@ class TwoFactorAuthCodeSerializer(serializers.Serializer): class AvatarUploadForm(forms.Form): file = forms.FileField() + + +class RankInfoSerializer(serializers.ModelSerializer): + user = UsernameSerializer() + + class Meta: + model = UserProfile diff --git a/account/urls/oj.py b/account/urls/oj.py index 1d65729..e31a81e 100644 --- a/account/urls/oj.py +++ b/account/urls/oj.py @@ -3,7 +3,8 @@ from django.conf.urls import url from ..views.oj import (ApplyResetPasswordAPI, ResetPasswordAPI, UserChangePasswordAPI, UserRegisterAPI, UserLoginAPI, UserLogoutAPI, UsernameOrEmailCheck, - SSOAPI, AvatarUploadAPI, TwoFactorAuthAPI, UserProfileAPI) + SSOAPI, AvatarUploadAPI, TwoFactorAuthAPI, UserProfileAPI, + UserRankAPI) from utils.captcha.views import CaptchaAPIView @@ -19,5 +20,6 @@ urlpatterns = [ url(r"^profile/?$", UserProfileAPI.as_view(), name="user_profile_api"), url(r"^avatar/upload/?$", AvatarUploadAPI.as_view(), name="avatar_upload_api"), url(r"^sso/?$", SSOAPI.as_view(), name="sso_api"), - url(r"^two_factor_auth/?$", TwoFactorAuthAPI.as_view(), name="two_factor_auth_api") + url(r"^two_factor_auth/?$", TwoFactorAuthAPI.as_view(), name="two_factor_auth_api"), + url(r"^user_rank/?$", UserRankAPI.as_view(), name="user_rank_api"), ] diff --git a/account/views/oj.py b/account/views/oj.py index 351224c..1107a7d 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -18,10 +18,10 @@ from utils.shortcuts import rand_str from ..decorators import login_required from ..models import User, UserProfile -from ..serializers import (ApplyResetPasswordSerializer, - ResetPasswordSerializer, +from ..serializers import (ApplyResetPasswordSerializer, ResetPasswordSerializer, UserChangePasswordSerializer, UserLoginSerializer, - UserRegisterSerializer, UsernameOrEmailCheckSerializer) + UserRegisterSerializer, UsernameOrEmailCheckSerializer, + RankInfoSerializer) from ..serializers import (SSOSerializer, TwoFactorAuthCodeSerializer, UserProfileSerializer, EditUserProfileSerializer, AvatarUploadForm) @@ -32,6 +32,7 @@ class UserProfileAPI(APIView): """ 判断是否登录, 若登录返回用户信息 """ + @method_decorator(ensure_csrf_cookie) def get(self, request, **kwargs): user = request.user @@ -321,3 +322,16 @@ class ResetPasswordAPI(APIView): user.set_password(data["password"]) user.save() return self.success("Succeeded") + + +class UserRankAPI(APIView): + def get(self, request): + rule_type = request.GET.get("rule") + if rule_type not in ["acm", "oi"]: + rule_type = "acm" + profiles = UserProfile.objects.select_related("user").filter(submission_number__gt=0) + if rule_type == "acm": + profiles = profiles.order_by("-accepted_number", "submission_number") + else: + profiles = profiles.order_by("-total_score") + return self.success(self.paginate_data(request, profiles, RankInfoSerializer)) diff --git a/judge/dispatcher.py b/judge/dispatcher.py index 1bcdba2..9b4339d 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -40,7 +40,7 @@ class JudgeDispatcher(object): .get(_id=problem_id, contest_id=self.submission.contest_id) self.contest = self.problem.contest else: - self.problem = Problem.objects.get(pk=problem_id) + self.problem = Problem.objects.get(_id=problem_id) def _request(self, url, data=None): kwargs = {"headers": {"X-Judge-Server-Token": self.token, @@ -75,7 +75,7 @@ class JudgeDispatcher(object): 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 @@ -111,6 +111,7 @@ class JudgeDispatcher(object): # 用时和内存占用保存为多个测试点中最长的那个 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"] error_test_case = list(filter(lambda case: case["result"] != 0, resp["data"])) # 多个测试点全部正确则AC,否则 ACM模式下取第一个错误的测试点的状态, OI模式若全部错误则取第一个错误测试点状态,否则为部分正确 @@ -144,9 +145,9 @@ class JudgeDispatcher(object): self.problem.add_ac_number() with transaction.atomic(): if self.submission.contest_id: - problem = ContestProblem.objects.select_for_update().get(_id=self.problem.id, contest_id=self.contest.id) + problem = ContestProblem.objects.select_for_update().get(_id=self.problem._id, contest_id=self.contest.id) else: - problem = Problem.objects.select_related().get(_id=self.problem.id) + problem = Problem.objects.select_related().get(_id=self.problem._id) info = problem.statistic_info result = str(self.submission.result) info[result] = info.get(result, 0) + 1 @@ -155,21 +156,35 @@ class JudgeDispatcher(object): def update_user_profile(self): with transaction.atomic(): - # 更新user profile user = User.objects.select_for_update().get(id=self.submission.user_id) user_profile = user.userprofile user_profile.add_submission_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(self.problem.id), JudgeStatus.WRONG_ANSWER) != JudgeStatus.ACCEPTED: - if self.submission.result == JudgeStatus.ACCEPTED: - user_profile.add_accepted_problem_number() - problems_status["problems"][str(self.problem.id)] = JudgeStatus.ACCEPTED + problem_id = str(self.problem._id) + if self.problem.rule_type == ProblemRuleType.ACM: + if problem_id not in problems_status: + problems_status[problem_id] = {"status": self.submission.result} + if self.submission.result == JudgeStatus.ACCEPTED: + user_profile.add_accepted_problem_number() + # 以前提交过, ac了直接略过 + elif problems_status[problem_id]["status"] != JudgeStatus.ACCEPTED: + if self.submission.result == JudgeStatus.ACCEPTED: + user_profile.add_accepted_problem_number() + problems_status[problem_id]["status"] = JudgeStatus.ACCEPTED + else: + problems_status[problem_id]["status"] = self.submission.result + + else: + score = self.submission.statistic_info["score"] + if problem_id not in problems_status: + user_profile.add_score(score) + problems_status[problem_id] = {"score": score} else: - problems_status["problems"][str(self.problem.id)] = JudgeStatus.WRONG_ANSWER + # 加上本次 减掉上次的score + user_profile.add_score(score, problems_status[problem_id]["score"]) + problems_status[problem_id] = {"score": score} + user_profile.problems_status = problems_status user_profile.save(update_fields=["problems_status"]) From 99fd87dbcf49079a92cd5468ca50d5623919eef9 Mon Sep 17 00:00:00 2001 From: zemal Date: Sun, 20 Aug 2017 20:41:48 +0800 Subject: [PATCH 042/106] =?UTF-8?q?=E6=8D=A2=E5=9B=9E=E4=B9=8B=E5=89=8D?= =?UTF-8?q?=E7=9A=84capacha=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/views/oj.py | 4 +- utils/captcha/__init__.py | 170 +++++++++++++------------------------- 2 files changed, 61 insertions(+), 113 deletions(-) diff --git a/account/views/oj.py b/account/views/oj.py index 1107a7d..178337e 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -237,7 +237,7 @@ class UserRegisterAPI(APIView): """ data = request.data captcha = Captcha(request) - if not captcha.validate(data["captcha"]): + if not captcha.check(data["captcha"]): return self.error("Invalid captcha") if User.objects.filter(username=data["username"]).exists(): return self.error("Username already exists") @@ -260,7 +260,7 @@ class UserChangePasswordAPI(APIView): """ data = request.data captcha = Captcha(request) - if not captcha.validate(data["captcha"]): + if not captcha.check(data["captcha"]): return self.error("Invalid captcha") username = request.user.username user = auth.authenticate(username=username, password=data["old_password"]) diff --git a/utils/captcha/__init__.py b/utils/captcha/__init__.py index ce687d5..b88f452 100644 --- a/utils/captcha/__init__.py +++ b/utils/captcha/__init__.py @@ -1,5 +1,5 @@ """ -Copyright 2017 TY +Copyright 2013 TY Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at @@ -12,145 +12,93 @@ limitations under the License. """ import os +import time import random -from math import ceil -from six import BytesIO -from PIL import Image, ImageDraw, ImageFont -__version__ = "0.3.3" -current_path = os.path.normpath(os.path.dirname(__file__)) +from io import BytesIO +from PIL import Image, ImageDraw, ImageFont class Captcha(object): def __init__(self, request): - """ something init """ - + 初始化,设置各种属性 + """ self.django_request = request self.session_key = "_django_captcha_key" - self.words = ["hello", "word"] + self.captcha_expires_time = "_django_captcha_expires_time" - # image size (pix) - self.img_width = 150 + # 验证码图片尺寸 + self.img_width = 90 self.img_height = 30 - self.type = "number" - self.mode = "number" - - def _get_font_size(self): + def _get_font_size(self, code): + """ + 将图片高度的80%作为字体大小 + """ s1 = int(self.img_height * 0.8) - s2 = int(self.img_width / len(self.code)) + s2 = int(self.img_width / len(code)) return int(min((s1, s2)) + max((s1, s2)) * 0.05) - def _get_words(self): - """ words list - """ - - # TODO 扩充单词表 - - if self.words: - return set(self.words) - def _set_answer(self, answer): + """ + 设置答案和过期时间 + """ self.django_request.session[self.session_key] = str(answer) + self.django_request.session[self.captcha_expires_time] = time.time() + 60 - def _generate(self): - # 英文单词验证码 - def word(): - code = random.sample(self._get_words(), 1)[0] - self._set_answer(code) - return code - - # 数字公式验证码 - def number(): - m, n = 1, 50 - x = random.randrange(m, n) - y = random.randrange(m, n) - - r = random.randrange(0, 2) - if r == 0: - code = "%s - %s = ?" % (x, y) - z = x - y - else: - code = "%s + %s = ?" % (x, y) - z = x + y - self._set_answer(z) - return code - - fun = eval(self.mode.lower()) - return fun() + def _make_code(self): + """ + 生成随机数或随机字符串 + """ + string = random.sample("abcdefghkmnpqrstuvwxyzABCDEFGHGKMNOPQRSTUVWXYZ23456789", 4) + self._set_answer("".join(string)) + return string def get(self): - """ return captcha image bytes """ + 生成验证码图片,返回值为图片的bytes + """ + background = (random.randrange(200, 255), random.randrange(200, 255), random.randrange(200, 255)) + code_color = (random.randrange(0, 50), random.randrange(0, 50), random.randrange(0, 50), 255) - # font color - self.font_color = ["black", "darkblue", "darkred"] + font_path = os.path.join(os.path.normpath(os.path.dirname(__file__)), "timesbi.ttf") - # background color - self.background = (random.randrange(230, 255), random.randrange(230, 255), random.randrange(230, 255)) + image = Image.new("RGB", (self.img_width, self.img_height), background) + code = self._make_code() + font_size = self._get_font_size(code) + draw = ImageDraw.Draw(image) - # font path - self.font_path = os.path.join(current_path, "timesbi.ttf") # or Menlo.ttc + # x是第一个字母的x坐标 + x = random.randrange(int(font_size * 0.3), int(font_size * 0.5)) - self.django_request.session[self.session_key] = "" - im = Image.new("RGB", (self.img_width, self.img_height), self.background) - self.code = self._generate() + for i in code: + # 字符y坐标 + y = random.randrange(1, 7) + # 随机字符大小 + font = ImageFont.truetype(font_path.replace("\\", "/"), font_size + random.randrange(-3, 7)) + draw.text((x, y), i, font=font, fill=code_color) + # 随机化字符之间的距离 字符粘连可以降低识别率 + x += font_size * random.randrange(6, 8) / 10 - # set font size automaticly - self.font_size = self._get_font_size() - - # creat - draw = ImageDraw.Draw(im) - - # draw noisy point/line - if self.mode == "word": - c = int(8 / len(self.code) * 3) or 3 - elif self.mode == "number": - c = 4 - - for i in range(random.randrange(c - 2, c)): - line_color = (random.randrange(0, 255), random.randrange(0, 255), random.randrange(0, 255)) - xy = (random.randrange(0, int(self.img_width * 0.2)), random.randrange(0, self.img_height), - random.randrange(int(3 * self.img_width / 4), self.img_width), random.randrange(0, self.img_height)) - draw.line(xy, fill=line_color, width=int(self.font_size * 0.1)) - - # main part - j = int(self.font_size * 0.3) - k = int(self.font_size * 0.5) - x = random.randrange(j, k) - - for i in self.code: - # 上下抖动量,字数越多,上下抖动越大 - m = int(len(self.code)) - y = random.randrange(1, 3) - - if i in ("+", "=", "?"): - # 对计算符号等特殊字符放大处理 - m = ceil(self.font_size * 0.8) - else: - # 字体大小变化量,字数越少,字体大小变化越多 - m = random.randrange(0, int(45 / self.font_size) + int(self.font_size / 5)) - - self.font = ImageFont.truetype(self.font_path.replace("\\", "/"), self.font_size + int(ceil(m))) - draw.text((x, y), i, font=self.font, fill=random.choice(self.font_color)) - x += self.font_size * 0.9 - - del x - del draw + self.django_request.session[self.session_key] = "".join(code) with BytesIO() as buf: - im.save(buf, "gif") + image.save(buf, "gif") buf_str = buf.getvalue() return buf_str - def validate(self, code): - """ user input validate + def check(self, code): + """ + 检查用户输入的验证码是否正确 """ - - if not code: - return False - - code = code.strip() _code = self.django_request.session.get(self.session_key) or "" - self.django_request.session[self.session_key] = "" - return _code.lower() == str(code).lower() + if not _code: + return False + expires_time = self.django_request.session.get(self.captcha_expires_time) or 0 + # 注意 如果验证之后不清除之前的验证码的话 可能会造成重复验证的现象 + del self.django_request.session[self.session_key] + del self.django_request.session[self.captcha_expires_time] + if _code.lower() == str(code).lower() and time.time() < expires_time: + return True + else: + return False From 57ab7435afd2bd42bfdf3729342b6bd73b49e5e0 Mon Sep 17 00:00:00 2001 From: zemal Date: Wed, 23 Aug 2017 17:01:55 +0800 Subject: [PATCH 043/106] =?UTF-8?q?=E7=A7=BB=E9=99=A4time=5Fzone,=E4=BF=AE?= =?UTF-8?q?=E5=A4=8Dproblem=E8=B6=8A=E6=9D=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/models.py | 1 - account/views/oj.py | 2 +- contest/urls/oj.py | 3 ++- contest/views/oj.py | 3 ++- oj/settings.py | 5 +---- problem/serializers.py | 28 +++++++++++++++----------- problem/views/admin.py | 15 +++++++------- submission/views/oj.py | 4 ++-- utils/api/tests.py | 2 +- utils/management/commands/initadmin.py | 2 +- 10 files changed, 34 insertions(+), 31 deletions(-) diff --git a/account/models.py b/account/models.py index cb71bfc..7f020aa 100644 --- a/account/models.py +++ b/account/models.py @@ -80,7 +80,6 @@ class UserProfile(models.Model): school = models.CharField(max_length=200, blank=True, null=True) major = models.CharField(max_length=200, blank=True, null=True) student_id = models.CharField(max_length=15, blank=True, null=True) - time_zone = models.CharField(max_length=32, blank=True, null=True) language = models.CharField(max_length=32, blank=True, null=True) # for ACM accepted_number = models.IntegerField(default=0) diff --git a/account/views/oj.py b/account/views/oj.py index 178337e..b1f5798 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -247,7 +247,7 @@ class UserRegisterAPI(APIView): user = User.objects.create(username=data["username"], email=data["email"]) user.set_password(data["password"]) user.save() - UserProfile.objects.create(user=user, time_zone=settings.USER_DEFAULT_TZ) + UserProfile.objects.create(user=user) return self.success("Succeeded") diff --git a/contest/urls/oj.py b/contest/urls/oj.py index 42fbdf2..cfa12f6 100644 --- a/contest/urls/oj.py +++ b/contest/urls/oj.py @@ -2,11 +2,12 @@ from django.conf.urls import url from ..views.oj import ContestAnnouncementListAPI, ContestAPI from ..views.oj import ContestPasswordVerifyAPI, ContestAccessAPI +from ..views.oj import ContestRankAPI urlpatterns = [ url(r"^contest/?$", ContestAPI.as_view(), name="contest_api"), url(r"^contest/password/?$", ContestPasswordVerifyAPI.as_view(), name="contest_password_api"), url(r"^contest/announcement/?$", ContestAnnouncementListAPI.as_view(), name="contest_announcement_api"), url(r"^contest/access/?$", ContestAccessAPI.as_view(), name="contest_access_api"), - + url(r"^contest_rank/?$", ContestRankAPI.as_view(), name="contest_rank_api"), ] diff --git a/contest/views/oj.py b/contest/views/oj.py index 5baef2e..5e2b72a 100644 --- a/contest/views/oj.py +++ b/contest/views/oj.py @@ -89,9 +89,10 @@ class ContestAccessAPI(APIView): class ContestRankAPI(APIView): def get_rank(self): - if self.contest.contest_type == ContestRuleType.ACM: + if self.contest.rule_type == ContestRuleType.ACM: rank = ACMContestRank.objects.filter(contest=self.contest). \ select_related("user").order_by("-total_ac_number", "total_time") + print(rank) return ACMContestRankSerializer(rank, many=True).data else: rank = OIContestRank.objects.filter(contest=self.contest). \ diff --git a/oj/settings.py b/oj/settings.py index 5b75a83..a8e0267 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -61,7 +61,7 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.security.SecurityMiddleware', 'account.middleware.AdminRoleRequiredMiddleware', 'account.middleware.SessionSecurityMiddleware', - 'account.middleware.TimezoneMiddleware' + # 'account.middleware.TimezoneMiddleware' ) ROOT_URLCONF = 'oj.urls' @@ -97,9 +97,6 @@ USE_L10N = True USE_TZ = True -# in user's profile -USER_DEFAULT_TZ = 'Asia/Shanghai' - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.8/howto/static-files/ diff --git a/problem/serializers.py b/problem/serializers.py index 92b0f1f..a43e999 100644 --- a/problem/serializers.py +++ b/problem/serializers.py @@ -74,7 +74,7 @@ class TagSerializer(serializers.ModelSerializer): model = ProblemTag -class ProblemSerializer(serializers.ModelSerializer): +class BaseProblemSerializer(serializers.ModelSerializer): samples = serializers.JSONField() test_case_score = serializers.JSONField() languages = serializers.JSONField() @@ -85,20 +85,24 @@ class ProblemSerializer(serializers.ModelSerializer): created_by = UsernameSerializer() statistic_info = serializers.JSONField() + +class ProblemAdminSerializer(BaseProblemSerializer): class Meta: model = Problem -class ContestProblemSerializer(serializers.ModelSerializer): - samples = serializers.JSONField() - test_case_score = serializers.JSONField() - languages = serializers.JSONField() - template = serializers.JSONField() - tags = serializers.SlugRelatedField(many=True, slug_field="name", read_only=True) - create_time = DateTimeTZField() - last_update_time = DateTimeTZField() - created_by = UsernameSerializer() - statistic_info = serializers.JSONField() - +class ContestProblemAdminSerializer(BaseProblemSerializer): class Meta: model = ContestProblem + + +class ProblemSerializer(BaseProblemSerializer): + class Meta: + model = Problem + exclude = ("test_case_score", "test_case_id", "visible") + + +class ContestProblemSerializer(BaseProblemSerializer): + class Meta: + model = ContestProblem + exclude = ("test_case_score", "test_case_id", "visible", "is_public") diff --git a/problem/views/admin.py b/problem/views/admin.py index ad818ad..51afde8 100644 --- a/problem/views/admin.py +++ b/problem/views/admin.py @@ -13,7 +13,8 @@ from utils.shortcuts import rand_str from ..models import ContestProblem, Problem, ProblemRuleType, ProblemTag from ..serializers import (CreateContestProblemSerializer, CreateProblemSerializer, EditProblemSerializer, - ProblemSerializer, TestCaseUploadForm) + ProblemAdminSerializer, TestCaseUploadForm, + ContestProblemAdminSerializer) class TestCaseUploadAPI(CSRFExemptAPIView): @@ -154,7 +155,7 @@ class ProblemAPI(APIView): except ProblemTag.DoesNotExist: tag = ProblemTag.objects.create(name=item) problem.tags.add(tag) - return self.success(ProblemSerializer(problem).data) + return self.success(ProblemAdminSerializer(problem).data) @problem_permission_required def get(self, request): @@ -165,7 +166,7 @@ class ProblemAPI(APIView): problem = Problem.objects.get(id=problem_id) if not user.can_mgmt_all_problem() and problem.created_by != user: return self.error("Problem does not exist") - return self.success(ProblemSerializer(problem).data) + return self.success(ProblemAdminSerializer(problem).data) except Problem.DoesNotExist: return self.error("Problem does not exist") @@ -175,7 +176,7 @@ class ProblemAPI(APIView): keyword = request.GET.get("keyword") if keyword: problems = problems.filter(title__contains=keyword) - return self.success(self.paginate_data(request, problems, ProblemSerializer)) + return self.success(self.paginate_data(request, problems, ProblemAdminSerializer)) @validate_serializer(EditProblemSerializer) @problem_permission_required @@ -282,7 +283,7 @@ class ContestProblemAPI(APIView): except ProblemTag.DoesNotExist: tag = ProblemTag.objects.create(name=item) problem.tags.add(tag) - return self.success(ProblemSerializer(problem).data) + return self.success(ContestProblemAdminSerializer(problem).data) def get(self, request): problem_id = request.GET.get("id") @@ -295,7 +296,7 @@ class ContestProblemAPI(APIView): return self.error("Problem does not exist") except ContestProblem.DoesNotExist: return self.error("Problem does not exist") - return self.success(ProblemSerializer(problem).data) + return self.success(ProblemAdminSerializer(problem).data) if not contest_id: return self.error("Contest id is required") @@ -306,4 +307,4 @@ class ContestProblemAPI(APIView): keyword = request.GET.get("keyword") if keyword: problems = problems.filter(title__contains=keyword) - return self.success(self.paginate_data(request, problems, ProblemSerializer)) + return self.success(self.paginate_data(request, problems, ContestProblemAdminSerializer)) diff --git a/submission/views/oj.py b/submission/views/oj.py index b133284..8889201 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -55,7 +55,7 @@ class SubmissionAPI(APIView): except Contest.DoesNotExist: return self.error("Contest doesn't exist.") if contest.status != ContestStatus.CONTEST_UNDERWAY and request.user != contest.created_by: - return self.error("You have no permission to submit code.") + return self.error("Contest have not started or have ended, you can't submit code.") return _submit(self, request.user, data["problem_id"], data["language"], data["code"], data.get("contest_id")) @login_required @@ -64,7 +64,7 @@ class SubmissionAPI(APIView): if not submission_id: return self.error("Parameter id doesn't exist.") try: - submission = Submission.objects.get(id=submission_id, user_id=request.user.id) + submission = Submission.objects.get(id=submission_id) except Submission.DoesNotExist: return self.error("Submission doesn't exist.") if not submission.check_user_permission(request.user): diff --git a/utils/api/tests.py b/utils/api/tests.py index ff72791..9f9d997 100644 --- a/utils/api/tests.py +++ b/utils/api/tests.py @@ -11,7 +11,7 @@ class APITestCase(TestCase): def create_user(self, username, password, admin_type=AdminType.REGULAR_USER, login=True, problem_permission=ProblemPermission.NONE): user = User.objects.create(username=username, admin_type=admin_type, problem_permission=problem_permission) user.set_password(password) - UserProfile.objects.create(user=user, time_zone="Asia/Shanghai") + UserProfile.objects.create(user=user) user.save() if login: self.client.login(username=username, password=password) diff --git a/utils/management/commands/initadmin.py b/utils/management/commands/initadmin.py index bdd84ae..1ff9d6b 100644 --- a/utils/management/commands/initadmin.py +++ b/utils/management/commands/initadmin.py @@ -33,7 +33,7 @@ class Command(BaseCommand): rand_password = "rootroot" user.set_password(rand_password) user.save() - UserProfile.objects.create(user=user, time_zone="Asia/Shanghai") + UserProfile.objects.create(user=user) self.stdout.write(self.style.SUCCESS("Successfully created super admin user.\n" "Username: root\nPassword: %s\n" "Remember to change password and turn on two factors auth " From 539b45148b0529c035dc933639e0191747f9f5c8 Mon Sep 17 00:00:00 2001 From: zema1 Date: Sat, 26 Aug 2017 08:41:29 +0800 Subject: [PATCH 044/106] =?UTF-8?q?=E7=A7=BB=E9=99=A4user=20time=5Fzone,?= =?UTF-8?q?=20=E7=BB=9F=E4=B8=80=E4=BD=BF=E7=94=A8submission=5Fnumber?= =?UTF-8?q?=E5=92=8Caccepted=5Fnumber=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0004_remove_userprofile_time_zone.py | 19 ++++++++++ contest/migrations/0005_auto_20170823_0918.py | 30 ++++++++++++++++ contest/models.py | 6 ++-- contest/views/oj.py | 33 ++++++++++------- judge/dispatcher.py | 12 +++---- problem/migrations/0006_auto_20170823_0918.py | 35 +++++++++++++++++++ problem/models.py | 12 +++---- utils/constants.py | 1 + 8 files changed, 120 insertions(+), 28 deletions(-) create mode 100644 account/migrations/0004_remove_userprofile_time_zone.py create mode 100644 contest/migrations/0005_auto_20170823_0918.py create mode 100644 problem/migrations/0006_auto_20170823_0918.py diff --git a/account/migrations/0004_remove_userprofile_time_zone.py b/account/migrations/0004_remove_userprofile_time_zone.py new file mode 100644 index 0000000..97345a4 --- /dev/null +++ b/account/migrations/0004_remove_userprofile_time_zone.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2017-08-23 09:04 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0003_userprofile_total_score'), + ] + + operations = [ + migrations.RemoveField( + model_name='userprofile', + name='time_zone', + ), + ] diff --git a/contest/migrations/0005_auto_20170823_0918.py b/contest/migrations/0005_auto_20170823_0918.py new file mode 100644 index 0000000..dbf12c6 --- /dev/null +++ b/contest/migrations/0005_auto_20170823_0918.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2017-08-23 09:18 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('contest', '0004_auto_20170717_1324'), + ] + + operations = [ + migrations.RenameField( + model_name='acmcontestrank', + old_name='total_ac_number', + new_name='accepted_number', + ), + migrations.RenameField( + model_name='acmcontestrank', + old_name='total_submission_number', + new_name='submission_number', + ), + migrations.RenameField( + model_name='oicontestrank', + old_name='total_submission_number', + new_name='submission_number', + ), + ] diff --git a/contest/models.py b/contest/models.py index ca99142..bda6c6c 100644 --- a/contest/models.py +++ b/contest/models.py @@ -64,14 +64,14 @@ class Contest(models.Model): class ContestRank(models.Model): user = models.ForeignKey(User) contest = models.ForeignKey(Contest) - total_submission_number = models.IntegerField(default=0) + submission_number = models.IntegerField(default=0) class Meta: abstract = True class ACMContestRank(ContestRank): - total_ac_number = models.IntegerField(default=0) + accepted_number = models.IntegerField(default=0) # total_time is only for ACM contest total_time = ac time + none-ac times * 20 * 60 total_time = models.IntegerField(default=0) # {23: {"is_ac": True, "ac_time": 8999, "error_number": 2, "is_first_ac": True}} @@ -92,7 +92,7 @@ class OIContestRank(ContestRank): db_table = "oi_contest_rank" def update_rank(self, submission): - self.total_submission_number += 1 + self.submission_number += 1 class ContestAnnouncement(models.Model): diff --git a/contest/views/oj.py b/contest/views/oj.py index 5e2b72a..28ee1bc 100644 --- a/contest/views/oj.py +++ b/contest/views/oj.py @@ -1,7 +1,9 @@ +import pickle 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 utils.cache import default_cache +from utils.constants import CacheKey from account.decorators import login_required, check_contest_permission from ..models import ContestAnnouncement, Contest, ContestStatus, ContestRuleType @@ -90,20 +92,25 @@ class ContestAccessAPI(APIView): class ContestRankAPI(APIView): def get_rank(self): if self.contest.rule_type == ContestRuleType.ACM: - rank = ACMContestRank.objects.filter(contest=self.contest). \ - select_related("user").order_by("-total_ac_number", "total_time") - print(rank) - return ACMContestRankSerializer(rank, many=True).data + return ACMContestRank.objects.filter(contest=self.contest). \ + select_related("user").order_by("-accepted_number", "total_time") else: - rank = OIContestRank.objects.filter(contest=self.contest). \ + return 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) + if self.contest.rule_type == ContestRuleType.ACM: + model, serializer = ACMContestRank, ACMContestRankSerializer + else: + model, serializer = OIContestRank, OIContestRankSerializer + + cache_key = CacheKey.contest_rank_cache + str(self.contest.id) + qs = default_cache.get(cache_key) + if not qs: + ranks = self.get_rank() + default_cache.set(cache_key, pickle.dumps(ranks)) + else: + ranks = pickle.loads(qs) + + return self.success(self.paginate_data(request, ranks, serializer)) diff --git a/judge/dispatcher.py b/judge/dispatcher.py index 9b4339d..e21ecb4 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -210,29 +210,29 @@ class JudgeDispatcher(object): if info["is_ac"]: return - rank.total_submission_number += 1 + rank.submission_number += 1 if self.submission.result == JudgeStatus.ACCEPTED: - rank.total_ac_number += 1 + rank.accepted_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: + if problem.accepted_number == 1: info["is_first_ac"] = True else: info["error_number"] += 1 # 第一次提交 else: - rank.total_submission_number += 1 + rank.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 + rank.accepted_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: + if problem.accepted_number == 1: info["is_first_ac"] = True else: diff --git a/problem/migrations/0006_auto_20170823_0918.py b/problem/migrations/0006_auto_20170823_0918.py new file mode 100644 index 0000000..933070f --- /dev/null +++ b/problem/migrations/0006_auto_20170823_0918.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.6 on 2017-08-23 09:18 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('problem', '0005_auto_20170815_1258'), + ] + + operations = [ + migrations.RenameField( + model_name='contestproblem', + old_name='total_accepted_number', + new_name='accepted_number', + ), + migrations.RenameField( + model_name='contestproblem', + old_name='total_submit_number', + new_name='submission_number', + ), + migrations.RenameField( + model_name='problem', + old_name='total_accepted_number', + new_name='accepted_number', + ), + migrations.RenameField( + model_name='problem', + old_name='total_submit_number', + new_name='submission_number', + ), + ] diff --git a/problem/models.py b/problem/models.py index 6915ee0..c6da000 100644 --- a/problem/models.py +++ b/problem/models.py @@ -55,8 +55,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) - total_submit_number = models.BigIntegerField(default=0) - total_accepted_number = models.BigIntegerField(default=0) + submission_number = models.BigIntegerField(default=0) + accepted_number = models.BigIntegerField(default=0) # {0: 0, 1: 0, 2: 0, 3: 0 ...} # the first number means JudgeStatus, the second number present count statistic_info = JSONField(default={}) @@ -66,12 +66,12 @@ class AbstractProblem(models.Model): abstract = True def add_submission_number(self): - self.total_submit_number = models.F("total_submit_number") + 1 - self.save(update_fields=["total_submit_number"]) + self.submission_number = models.F("submission_number") + 1 + self.save(update_fields=["submission_number"]) def add_ac_number(self): - self.total_accepted_number = models.F("total_accepted_number") + 1 - self.save(update_fields=["total_accepted_number"]) + self.accepted_number = models.F("accepted_number") + 1 + self.save(update_fields=["accepted_number"]) class Problem(AbstractProblem): diff --git a/utils/constants.py b/utils/constants.py index 66ef169..86181b6 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -1,2 +1,3 @@ class CacheKey: waiting_queue = "waiting_queue" + contest_rank_cache = "contest_rank_cache_" From 1e4ede6d1a28519a4a6d403e4793a975fa18ebb0 Mon Sep 17 00:00:00 2001 From: zema1 Date: Tue, 29 Aug 2017 19:26:38 +0800 Subject: [PATCH 045/106] =?UTF-8?q?=E5=A4=A7=E5=B9=85=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E6=9F=A5=E8=AF=A2,=20=E5=8D=87?= =?UTF-8?q?=E7=BA=A7django=E8=87=B31.11=20LTS,=20=E5=8D=87=E7=BA=A7python?= =?UTF-8?q?=E8=87=B33.6.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .python-version | 2 +- account/decorators.py | 2 +- account/middleware.py | 24 +++++++++++++++---- account/views/oj.py | 2 +- contest/views/oj.py | 10 ++++---- deploy/requirements.txt | 2 +- oj/settings.py | 1 - problem/views/oj.py | 8 +++---- .../migrations/0005_submission_username.py | 21 ++++++++++++++++ submission/models.py | 1 + submission/serializers.py | 14 ++--------- submission/views/oj.py | 1 + utils/models.py | 2 -- 13 files changed, 57 insertions(+), 33 deletions(-) create mode 100644 submission/migrations/0005_submission_username.py diff --git a/.python-version b/.python-version index 1545d96..b727628 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.5.0 +3.6.2 diff --git a/account/decorators.py b/account/decorators.py index 1697a50..0b14f89 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -76,7 +76,7 @@ def check_contest_permission(func): try: # use self.contest to avoid query contest again in view. - self.contest = Contest.objects.get(id=contest_id, visible=True) + self.contest = Contest.objects.select_related("created_by").get(id=contest_id, visible=True) except Contest.DoesNotExist: return self.error("Contest %s doesn't exist" % contest_id) diff --git a/account/middleware.py b/account/middleware.py index f510fd2..dac102d 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -1,14 +1,16 @@ import time - import pytz + from django.contrib import auth from django.utils import timezone from django.utils.translation import ugettext as _ +from django.db import connection +from django.utils.deprecation import MiddlewareMixin from utils.api import JSONResponse -class SessionSecurityMiddleware(object): +class SessionSecurityMiddleware(MiddlewareMixin): def process_request(self, request): if request.user.is_authenticated() and request.user.is_admin_role(): if "last_activity" in request.session: @@ -20,15 +22,27 @@ class SessionSecurityMiddleware(object): request.session["last_activity"] = time.time() -class AdminRoleRequiredMiddleware(object): +class AdminRoleRequiredMiddleware(MiddlewareMixin): def process_request(self, request): path = request.path_info if path.startswith("/admin/") or path.startswith("/api/admin/"): - if not(request.user.is_authenticated() and request.user.is_admin_role()): + if not (request.user.is_authenticated() and request.user.is_admin_role()): return JSONResponse.response({"error": "login-required", "data": _("Please login in first")}) -class TimezoneMiddleware(object): +class TimezoneMiddleware(MiddlewareMixin): def process_request(self, request): if request.user.is_authenticated(): timezone.activate(pytz.timezone(request.user.userprofile.time_zone)) + + +class LogSqlMiddleware(MiddlewareMixin): + def process_response(self, request, response): + print("\033[94m", "#" * 30, "\033[0m") + time_threshold = 0.03 + for query in connection.queries: + if float(query["time"]) > time_threshold: + print("\033[93m", query, "\n", "-" * 30, "\033[0m") + else: + print(query, "\n", "-" * 30) + return response diff --git a/account/views/oj.py b/account/views/oj.py index b1f5798..dda41c7 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -47,7 +47,7 @@ class UserProfileAPI(APIView): user = request.user except User.DoesNotExist: return self.error("User does not exist") - profile = UserProfile.objects.get(user=user) + profile = UserProfile.objects.select_related("user").get(user=user) return self.success(UserProfileSerializer(profile).data) @validate_serializer(EditUserProfileSerializer) diff --git a/contest/views/oj.py b/contest/views/oj.py index 28ee1bc..b25019c 100644 --- a/contest/views/oj.py +++ b/contest/views/oj.py @@ -18,7 +18,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) + data = ContestAnnouncement.objects.select_related("created_by").filter(contest_id=contest_id) max_id = request.GET.get("max_id") if max_id: data = data.filter(id__gt=max_id) @@ -30,12 +30,12 @@ class ContestAPI(APIView): contest_id = request.GET.get("id") if contest_id: try: - contest = Contest.objects.get(id=contest_id, visible=True) + contest = Contest.objects.select_related("created_by").get(id=contest_id, visible=True) except Contest.DoesNotExist: return self.error("Contest doesn't exist.") return self.success(ContestSerializer(contest).data) - contests = Contest.objects.filter(visible=True) + contests = Contest.objects.select_related("created_by").filter(visible=True) keyword = request.GET.get("keyword") rule_type = request.GET.get("rule_type") status = request.GET.get("status") @@ -101,9 +101,9 @@ class ContestRankAPI(APIView): @check_contest_permission def get(self, request): if self.contest.rule_type == ContestRuleType.ACM: - model, serializer = ACMContestRank, ACMContestRankSerializer + serializer = ACMContestRankSerializer else: - model, serializer = OIContestRank, OIContestRankSerializer + serializer = OIContestRankSerializer cache_key = CacheKey.contest_rank_cache + str(self.contest.id) qs = default_cache.get(cache_key) diff --git a/deploy/requirements.txt b/deploy/requirements.txt index b843e27..1d2032c 100644 --- a/deploy/requirements.txt +++ b/deploy/requirements.txt @@ -1,4 +1,4 @@ -django==1.9.6 +django==1.11.4 djangorestframework==3.4.0 pillow jsonfield diff --git a/oj/settings.py b/oj/settings.py index a8e0267..ed06ec9 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -61,7 +61,6 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.security.SecurityMiddleware', 'account.middleware.AdminRoleRequiredMiddleware', 'account.middleware.SessionSecurityMiddleware', - # 'account.middleware.TimezoneMiddleware' ) ROOT_URLCONF = 'oj.urls' diff --git a/problem/views/oj.py b/problem/views/oj.py index edc1c1b..930e0d9 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -17,12 +17,12 @@ class ProblemAPI(APIView): problem_id = request.GET.get("problem_id") if problem_id: try: - problem = Problem.objects.get(_id=problem_id, visible=True) + problem = Problem.objects.select_related("created_by").get(_id=problem_id, visible=True) return self.success(ProblemSerializer(problem).data) except Problem.DoesNotExist: return self.error("Problem does not exist") - problems = Problem.objects.filter(visible=True) + problems = Problem.objects.select_related("created_by").filter(visible=True) # 按照标签筛选 tag_text = request.GET.get("tag") if tag_text: @@ -51,10 +51,10 @@ class ContestProblemAPI(APIView): problem_id = request.GET.get("problem_id") if problem_id: try: - problem = ContestProblem.objects.get(_id=problem_id, contest=self.contest, visible=True) + problem = ContestProblem.objects.select_related("created_by").get(_id=problem_id, contest=self.contest, visible=True) except ContestProblem.DoesNotExist: return self.error("Problem does not exist.") return self.success(ContestProblemSerializer(problem).data) - contest_problems = ContestProblem.objects.filter(contest=self.contest, visible=True) + contest_problems = ContestProblem.objects.select_related("created_by").filter(contest=self.contest, visible=True) return self.success(ContestProblemSerializer(contest_problems, many=True).data) diff --git a/submission/migrations/0005_submission_username.py b/submission/migrations/0005_submission_username.py new file mode 100644 index 0000000..46f63df --- /dev/null +++ b/submission/migrations/0005_submission_username.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-08-26 03:47 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submission', '0004_auto_20170717_1324'), + ] + + operations = [ + migrations.AddField( + model_name='submission', + name='username', + field=models.CharField(default="", max_length=30), + preserve_default=False, + ), + ] diff --git a/submission/models.py b/submission/models.py index 65d242b..45130be 100644 --- a/submission/models.py +++ b/submission/models.py @@ -25,6 +25,7 @@ class Submission(models.Model): problem_id = models.IntegerField(db_index=True) create_time = models.DateTimeField(auto_now_add=True) user_id = models.IntegerField(db_index=True) + username = models.CharField(max_length=30) code = models.TextField() result = models.IntegerField(default=JudgeStatus.PENDING) # 判题结果的详细信息 diff --git a/submission/serializers.py b/submission/serializers.py index 815d275..d2fe4ad 100644 --- a/submission/serializers.py +++ b/submission/serializers.py @@ -1,5 +1,4 @@ from .models import Submission -from account.models import User from utils.api import serializers from judge.languages import language_names @@ -21,20 +20,14 @@ class SubmissionModelSerializer(serializers.ModelSerializer): # 不显示submission info详情的serializer class SubmissionSafeSerializer(serializers.ModelSerializer): - username = serializers.SerializerMethodField() statistic_info = serializers.JSONField() class Meta: model = Submission exclude = ("info", "contest_id") - @staticmethod - def get_username(obj): - return User.objects.get(id=obj.user_id).username - class SubmissionListSerializer(serializers.ModelSerializer): - username = serializers.SerializerMethodField() statistic_info = serializers.JSONField() show_link = serializers.SerializerMethodField() @@ -47,10 +40,7 @@ class SubmissionListSerializer(serializers.ModelSerializer): exclude = ("info", "contest_id", "code") def get_show_link(self, obj): - if self.user.id is None: + # 没传user或为匿名user + if self.user is None or self.user.id is None: return False return obj.check_user_permission(self.user) - - @staticmethod - def get_username(obj): - return User.objects.get(id=obj.user_id).username diff --git a/submission/views/oj.py b/submission/views/oj.py index 8889201..27abc10 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -34,6 +34,7 @@ def _submit(response, user, problem_id, language, code, contest_id): return response.error("Problem not exist") submission = Submission.objects.create(user_id=user.id, + username=user.username, language=language, code=code, problem_id=problem._id, diff --git a/utils/models.py b/utils/models.py index a3e15b0..b651aa2 100644 --- a/utils/models.py +++ b/utils/models.py @@ -4,8 +4,6 @@ from utils.xss_filter import XssHtml class RichTextField(models.TextField): - __metaclass__ = models.SubfieldBase - def get_prep_value(self, value): if not value: value = "" From f55a242ec07a905ada556ff80236c2a55565fcb5 Mon Sep 17 00:00:00 2001 From: zema1 Date: Tue, 12 Sep 2017 11:45:17 +0800 Subject: [PATCH 046/106] Move real_name to UserProfile; Delete student_id field; Mark the problems that have submission; Alter dispatcher to adapt the changes. --- account/migrations/0005_auto_20170830_1154.py | 39 ++++++++ account/models.py | 13 +-- account/serializers.py | 10 +- account/views/oj.py | 72 ++++++-------- judge/dispatcher.py | 95 +++++++++++-------- oj/settings.py | 22 ++++- problem/models.py | 3 +- problem/views/oj.py | 28 +++++- reset_password_email.html | 0 .../migrations/0006_auto_20170830_1154.py | 20 ++++ submission/models.py | 2 +- submission/views/oj.py | 9 +- utils/captcha/__init__.py | 5 +- utils/captcha/views.py | 5 +- utils/shortcuts.py | 11 +++ 15 files changed, 221 insertions(+), 113 deletions(-) create mode 100644 account/migrations/0005_auto_20170830_1154.py create mode 100644 reset_password_email.html create mode 100644 submission/migrations/0006_auto_20170830_1154.py diff --git a/account/migrations/0005_auto_20170830_1154.py b/account/migrations/0005_auto_20170830_1154.py new file mode 100644 index 0000000..efc739d --- /dev/null +++ b/account/migrations/0005_auto_20170830_1154.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-08-30 11:54 +from __future__ import unicode_literals + +from django.db import migrations, models +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0004_remove_userprofile_time_zone'), + ] + + operations = [ + migrations.RenameField( + model_name='userprofile', + old_name='problems_status', + new_name='acm_problems_status', + ), + migrations.AddField( + model_name='userprofile', + name='oi_problems_status', + field=jsonfield.fields.JSONField(default={}), + ), + migrations.RemoveField( + model_name='user', + name='real_name', + ), + migrations.RemoveField( + model_name='userprofile', + name='student_id', + ), + migrations.AddField( + model_name='userprofile', + name='real_name', + field=models.CharField(max_length=30, blank=True, null=True), + ), + ] diff --git a/account/models.py b/account/models.py index 7f020aa..a94e6a4 100644 --- a/account/models.py +++ b/account/models.py @@ -24,7 +24,6 @@ class UserManager(models.Manager): class User(AbstractBaseUser): username = models.CharField(max_length=30, unique=True) - real_name = models.CharField(max_length=30, null=True) email = models.EmailField(max_length=254, null=True) create_time = models.DateTimeField(auto_now_add=True, null=True) # One of UserType @@ -69,17 +68,19 @@ def _random_avatar(): class UserProfile(models.Model): user = models.OneToOneField(User) - # Store user problem solution status with json string format, Only for problems not contest_problems - # ACM: {1: {status: JudgeStatus.ACCEPTED}} - # OI: {1: {score: 33}} - problems_status = JSONField(default={}) + # Store user problem solution status with json string format + # {problems: {1: JudgeStatus.ACCEPTED}, contest_problems: {1: JudgeStatus.ACCEPTED}}, record problem_id and status + acm_problems_status = JSONField(default={}) + # {problems: {1: 33}, contest_problems: {1: 44}, record problem_id and score + oi_problems_status = JSONField(default={}) + + real_name = models.CharField(max_length=30, blank=True, null=True) avatar = models.CharField(max_length=50, default=_random_avatar) blog = models.URLField(blank=True, null=True) mood = models.CharField(max_length=200, blank=True, null=True) phone_number = models.CharField(max_length=15, blank=True, null=True) school = models.CharField(max_length=200, blank=True, null=True) major = models.CharField(max_length=200, blank=True, null=True) - student_id = models.CharField(max_length=15, blank=True, null=True) language = models.CharField(max_length=32, blank=True, null=True) # for ACM accepted_number = models.IntegerField(default=0) diff --git a/account/serializers.py b/account/serializers.py index 9b10fc3..9917f71 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -35,18 +35,23 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ["id", "username", "real_name", "email", "admin_type", "problem_permission", + fields = ["id", "username", "email", "admin_type", "problem_permission", "create_time", "last_login", "two_factor_auth", "open_api", "is_disabled"] class UserProfileSerializer(serializers.ModelSerializer): user = UserSerializer() + acm_problems_status = serializers.JSONField() + oi_problems_status = serializers.JSONField() class Meta: model = UserProfile class UserInfoSerializer(serializers.ModelSerializer): + acm_problems_status = serializers.JSONField() + oi_problems_status = serializers.JSONField() + class Meta: model = UserProfile @@ -54,7 +59,6 @@ class UserInfoSerializer(serializers.ModelSerializer): class EditUserSerializer(serializers.Serializer): id = serializers.IntegerField() username = serializers.CharField(max_length=30) - real_name = serializers.CharField(max_length=30) password = serializers.CharField(max_length=30, min_length=6, allow_blank=True, required=False, default=None) email = serializers.EmailField(max_length=254) admin_type = serializers.ChoiceField(choices=(AdminType.REGULAR_USER, AdminType.ADMIN, AdminType.SUPER_ADMIN)) @@ -66,13 +70,13 @@ class EditUserSerializer(serializers.Serializer): class EditUserProfileSerializer(serializers.Serializer): + real_name = serializers.CharField(max_length=30) avatar = serializers.CharField(max_length=100, allow_null=True, required=False) blog = serializers.URLField(allow_null=True, required=False) mood = serializers.CharField(max_length=200, allow_null=True, required=False) phone_number = serializers.CharField(max_length=15, allow_null=True, required=False, ) school = serializers.CharField(max_length=200, allow_null=True, required=False) major = serializers.CharField(max_length=200, allow_null=True, required=False) - student_id = serializers.CharField(max_length=15, allow_null=True, required=False) class ApplyResetPasswordSerializer(serializers.Serializer): diff --git a/account/views/oj.py b/account/views/oj.py index dda41c7..f567c56 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -1,20 +1,19 @@ import os import qrcode -from io import BytesIO from datetime import timedelta from otpauth import OtpAuth from django.conf import settings from django.contrib import auth from django.utils.timezone import now -from django.http import HttpResponse from django.views.decorators.csrf import ensure_csrf_cookie from django.utils.decorators import method_decorator +from django.template.loader import render_to_string from conf.models import WebsiteConfig from utils.api import APIView, validate_serializer, CSRFExemptAPIView from utils.captcha import Captcha -from utils.shortcuts import rand_str +from utils.shortcuts import rand_str, img2base64 from ..decorators import login_required from ..models import User, UserProfile @@ -29,16 +28,14 @@ from ..tasks import send_email_async class UserProfileAPI(APIView): - """ - 判断是否登录, 若登录返回用户信息 - """ - @method_decorator(ensure_csrf_cookie) def get(self, request, **kwargs): + """ + 判断是否登录, 若登录返回用户信息 + """ user = request.user if not user.is_authenticated(): return self.success(0) - username = request.GET.get("username") try: if username: @@ -55,19 +52,10 @@ class UserProfileAPI(APIView): def put(self, request): data = request.data user_profile = request.user.userprofile - print(data) - if data.get("avatar"): - user_profile.avatar = data["avatar"] - else: - user_profile.mood = data["mood"] - user_profile.blog = data["blog"] - user_profile.school = data["school"] - user_profile.student_id = data["student_id"] - user_profile.phone_number = data["phone_number"] - user_profile.major = data["major"] - # Timezone & language 暂时不加 + for k, v in data.items(): + setattr(user_profile, k, v) user_profile.save() - return self.success("Succeeded") + return self.success(UserProfileSerializer(user_profile).data) class AvatarUploadAPI(CSRFExemptAPIView): @@ -137,11 +125,9 @@ class TwoFactorAuthAPI(APIView): user.save() config = WebsiteConfig.objects.first() - image = qrcode.make(OtpAuth(token).to_uri("totp", config.base_url, config.name)) - buf = BytesIO() - image.save(buf, "gif") - - return HttpResponse(buf.getvalue(), "image/gif") + label = f"{config.name_shortcut}:{user.username}@{config.base_url}" + image = qrcode.make(OtpAuth(token).to_uri("totp", label, config.name)) + return self.success(img2base64(image)) @login_required @validate_serializer(TwoFactorAuthCodeSerializer) @@ -215,17 +201,17 @@ class UsernameOrEmailCheck(APIView): check username or email is duplicate """ data = request.data - # True means OK. + # True means already exist. result = { - "username": True, - "email": True + "username": False, + "email": False } if data.get("username"): if User.objects.filter(username=data["username"]).exists(): - result["username"] = False + result["username"] = True if data.get("email"): if User.objects.filter(email=data["email"]).exists(): - result["email"] = False + result["email"] = True return self.success(result) @@ -259,9 +245,6 @@ class UserChangePasswordAPI(APIView): User change password api """ data = request.data - captcha = Captcha(request) - if not captcha.check(data["captcha"]): - return self.error("Invalid captcha") username = request.user.username user = auth.authenticate(username=username, password=data["old_password"]) if user: @@ -284,24 +267,23 @@ class ApplyResetPasswordAPI(APIView): user = User.objects.get(email=data["email"]) except User.DoesNotExist: return self.error("User does not exist") - if user.reset_password_token_expire_time and 0 < ( - user.reset_password_token_expire_time - now()).total_seconds() < 20 * 60: + if user.reset_password_token_expire_time and \ + 0 < int((user.reset_password_token_expire_time - now()).total_seconds()) < 20 * 60: return self.error("You can only reset password once per 20 minutes") user.reset_password_token = rand_str() - user.reset_password_token_expire_time = now() + timedelta(minutes=20) user.save() - email_template = open("reset_password_email.html", "w", - encoding="utf-8").read() - email_template = email_template.replace("{{ username }}", user.username). \ - replace("{{ website_name }}", settings.WEBSITE_INFO["website_name"]). \ - replace("{{ link }}", settings.WEBSITE_INFO["url"] + "/reset_password/t/" + - user.reset_password_token) + render_data = { + "username": user.username, + "website_name": config.name, + "link": f"{config.base_url}/reset-password/{user.reset_password_token}" + } + email_html = render_to_string('reset_password_email.html', render_data) send_email_async.delay(config.name, user.email, user.username, config.name + " 登录信息找回邮件", - email_template) + email_html) return self.success("Succeeded") @@ -316,8 +298,8 @@ class ResetPasswordAPI(APIView): user = User.objects.get(reset_password_token=data["token"]) except User.DoesNotExist: return self.error("Token dose not exist") - if 0 < (user.reset_password_token_expire_time - now()).total_seconds() < 30 * 60: - return self.error("Token expired") + if int((user.reset_password_token_expire_time - now()).total_seconds()) < 0: + return self.error("Token have expired") user.reset_password_token = None user.set_password(data["password"]) user.save() diff --git a/judge/dispatcher.py b/judge/dispatcher.py index e21ecb4..edf2e2f 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -36,7 +36,7 @@ class JudgeDispatcher(object): 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")\ + self.problem = ContestProblem.objects.select_related("contest") \ .get(_id=problem_id, contest_id=self.submission.contest_id) self.contest = self.problem.contest else: @@ -114,7 +114,8 @@ class JudgeDispatcher(object): # todo OI statistic_info["score"] error_test_case = list(filter(lambda case: case["result"] != 0, resp["data"])) - # 多个测试点全部正确则AC,否则 ACM模式下取第一个错误的测试点的状态, OI模式若全部错误则取第一个错误测试点状态,否则为部分正确 + # ACM模式下,多个测试点全部正确则AC,否则取第一个错误的测试点的状态 + # OI模式下, 若多个测试点全部正确则AC, 若全部错误则取第一个错误测试点状态,否则为部分正确 if not error_test_case: self.submission.result = JudgeStatus.ACCEPTED elif self.problem.rule_type == ProblemRuleType.ACM or len(error_test_case) == len(resp["data"]): @@ -125,11 +126,9 @@ class JudgeDispatcher(object): self.release_judge_res(server.id) self.update_problem_status() - if self.submission.contest_id: self.update_contest_rank() - else: - self.update_user_profile() + # 至此判题结束,尝试处理任务队列中剩余的任务 process_pending_task() @@ -140,53 +139,71 @@ class JudgeDispatcher(object): return self._request(urljoin(service_url, "compile_spj"), data=data) def update_problem_status(self): - self.problem.add_submission_number() - if self.submission.result == JudgeStatus.ACCEPTED: - self.problem.add_ac_number() with transaction.atomic(): + # prepare problem and user_profile if self.submission.contest_id: - problem = ContestProblem.objects.select_for_update().get(_id=self.problem._id, contest_id=self.contest.id) + problem = ContestProblem.objects.select_for_update().get(contest_id=self.contest.id, + _id=self.problem._id) else: - problem = Problem.objects.select_related().get(_id=self.problem._id) - info = problem.statistic_info - result = str(self.submission.result) - info[result] = info.get(result, 0) + 1 - problem.statistic_info = info - problem.save(update_fields=["statistic_info"]) - - def update_user_profile(self): - with transaction.atomic(): - user = User.objects.select_for_update().get(id=self.submission.user_id) + problem = Problem.objects.select_for_update().get(_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 - user_profile.add_submission_number() - problems_status = user_profile.problems_status + if self.submission.contest_id: + key = "contest_problems" + else: + key = "problems" + acm_problems_status = user_profile.acm_problems_status.get(key, {}) + oi_problems_status = user_profile.oi_problems_status.get(key, {}) + + # 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: + user_profile.submission_number += 1 + if self.submission.result == JudgeStatus.ACCEPTED: + user_profile.accepted_number += 1 + problem.submission_number += 1 + if self.submission.result == JudgeStatus.ACCEPTED: + problem.accepted_number += 1 problem_id = str(self.problem._id) if self.problem.rule_type == ProblemRuleType.ACM: - if problem_id not in problems_status: - problems_status[problem_id] = {"status": self.submission.result} + # update acm problem info + result = str(self.submission.result) + problem_info[result] = problem_info.get(result, 0) + 1 + problem.statistic_info = problem_info + + # update user_profile + if problem_id not in acm_problems_status: + acm_problems_status[problem_id] = self.submission.result + # skip if the problem has been accepted + elif acm_problems_status[problem_id] != JudgeStatus.ACCEPTED: if self.submission.result == JudgeStatus.ACCEPTED: - user_profile.add_accepted_problem_number() - # 以前提交过, ac了直接略过 - elif problems_status[problem_id]["status"] != JudgeStatus.ACCEPTED: - if self.submission.result == JudgeStatus.ACCEPTED: - user_profile.add_accepted_problem_number() - problems_status[problem_id]["status"] = JudgeStatus.ACCEPTED + acm_problems_status[problem_id] = JudgeStatus.ACCEPTED else: - problems_status[problem_id]["status"] = self.submission.result + acm_problems_status[problem_id] = self.submission.result + user_profile.acm_problems_status[key] = acm_problems_status else: + # update oi problem info score = self.submission.statistic_info["score"] - if problem_id not in problems_status: - user_profile.add_score(score) - problems_status[problem_id] = {"score": score} - else: - # 加上本次 减掉上次的score - user_profile.add_score(score, problems_status[problem_id]["score"]) - problems_status[problem_id] = {"score": score} + problem_info[score] = problem_info.get(score, 0) + 1 + problem.statistic_info = problem_info - user_profile.problems_status = problems_status - user_profile.save(update_fields=["problems_status"]) + # update user_profile + if problem_id not in oi_problems_status: + user_profile.add_score(score) + 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]) + 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"]) def update_contest_rank(self): if self.contest.real_time_rank: diff --git a/oj/settings.py b/oj/settings.py index ed06ec9..1d4983d 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -23,7 +23,6 @@ if ENV == "local": elif ENV == "server": from .server_settings import * - BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -80,9 +79,26 @@ TEMPLATES = [ }, }, ] - WSGI_APPLICATION = 'oj.wsgi.application' +# Password validation +# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + # Internationalization # https://docs.djangoproject.com/en/1.8/topics/i18n/ @@ -105,7 +121,7 @@ AUTH_USER_MODEL = 'account.User' LOGGING = { 'version': 1, - 'disable_existing_loggers': True, + 'disable_existing_loggers': False, 'formatters': { 'standard': { 'format': '%(asctime)s [%(threadName)s:%(thread)d] [%(name)s:%(lineno)d] [%(module)s:%(funcName)s] [%(levelname)s]- %(message)s'} diff --git a/problem/models.py b/problem/models.py index c6da000..c446317 100644 --- a/problem/models.py +++ b/problem/models.py @@ -57,8 +57,7 @@ class AbstractProblem(models.Model): source = models.CharField(max_length=200, blank=True, null=True) submission_number = models.BigIntegerField(default=0) accepted_number = models.BigIntegerField(default=0) - # {0: 0, 1: 0, 2: 0, 3: 0 ...} - # the first number means JudgeStatus, the second number present count + # ACM rule_type: {JudgeStatus.ACCEPTED: 3, JudgeStaus.WRONG_ANSWER: 11}, the number means count statistic_info = JSONField(default={}) class Meta: diff --git a/problem/views/oj.py b/problem/views/oj.py index 930e0d9..5d3f2fb 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -1,10 +1,10 @@ from django.db.models import Q from utils.api import APIView from account.decorators import check_contest_permission -from ..models import ProblemTag, Problem, ContestProblem +from ..models import ProblemTag, Problem, ContestProblem, ProblemRuleType from ..serializers import ProblemSerializer, TagSerializer from ..serializers import ContestProblemSerializer - +from contest.models import ContestRuleType class ProblemTagAPI(APIView): def get(self, request): @@ -41,8 +41,18 @@ class ProblemAPI(APIView): difficulty_rank = request.GET.get("difficulty") if difficulty_rank: problems = problems.filter(difficulty=difficulty_rank) - - return self.success(self.paginate_data(request, problems, ProblemSerializer)) + # 根据profile 为做过的题目添加标记 + data = self.paginate_data(request, problems, ProblemSerializer) + if request.user.id: + profile = request.user.userprofile + acm_problems_status = profile.acm_problems_status.get("problems", {}) + oi_problems_status = profile.oi_problems_status.get("problems", {}) + for problem in data["results"]: + if problem["rule_type"] == ProblemRuleType.ACM: + problem["my_status"] = acm_problems_status.get(problem["_id"], None) + else: + problem["my_status"] = oi_problems_status.get(problem["_id"], None) + return self.success(data) class ContestProblemAPI(APIView): @@ -57,4 +67,14 @@ class ContestProblemAPI(APIView): return self.success(ContestProblemSerializer(problem).data) contest_problems = ContestProblem.objects.select_related("created_by").filter(contest=self.contest, visible=True) + # 根据profile, 为做过的题目添加标记 + data = ContestProblemSerializer(contest_problems, many=True).data + if request.user.id: + profile = request.user.userprofile + if self.contest.rule_type == ContestRuleType.ACM: + problems_status = profile.acm_problems_status.get("contest_problems", {}) + else: + problems_status = profile.oi_problems_status.get("contest_problems", {}) + for problem in data: + problem["my_status"] = problems_status.get(problem["_id"], None) return self.success(ContestProblemSerializer(contest_problems, many=True).data) diff --git a/reset_password_email.html b/reset_password_email.html new file mode 100644 index 0000000..e69de29 diff --git a/submission/migrations/0006_auto_20170830_1154.py b/submission/migrations/0006_auto_20170830_1154.py new file mode 100644 index 0000000..675cc86 --- /dev/null +++ b/submission/migrations/0006_auto_20170830_1154.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-08-30 11:54 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submission', '0005_submission_username'), + ] + + operations = [ + migrations.AlterField( + model_name='submission', + name='result', + field=models.IntegerField(db_index=True, default=6), + ), + ] diff --git a/submission/models.py b/submission/models.py index 45130be..f16f3a4 100644 --- a/submission/models.py +++ b/submission/models.py @@ -27,7 +27,7 @@ class Submission(models.Model): user_id = models.IntegerField(db_index=True) username = models.CharField(max_length=30) code = models.TextField() - result = models.IntegerField(default=JudgeStatus.PENDING) + result = models.IntegerField(db_index=True, default=JudgeStatus.PENDING) # 判题结果的详细信息 info = JSONField(default={}) language = models.CharField(max_length=20) diff --git a/submission/views/oj.py b/submission/views/oj.py index 27abc10..d6e88e0 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -1,7 +1,7 @@ from account.decorators import login_required, check_contest_permission from judge.tasks import judge_task -# from judge.dispatcher import JudgeDispatcher +from judge.dispatcher import JudgeDispatcher from problem.models import Problem, ProblemRuleType, ContestProblem from contest.models import Contest, ContestStatus from utils.api import APIView, validate_serializer @@ -104,11 +104,14 @@ class SubmissionListAPI(APIView): def process_submissions(self, request, submissions): problem_id = request.GET.get("problem_id") + myself = request.GET.get("myself") + result = request.GET.get("result") if problem_id: submissions = submissions.filter(problem_id=problem_id) - - if request.GET.get("myself") and request.GET["myself"] == "1": + if myself and myself == "1": submissions = submissions.filter(user_id=request.user.id) + if result: + submissions = submissions.filter(result=result) data = self.paginate_data(request, submissions) data["results"] = SubmissionListSerializer(data["results"], many=True, user=request.user).data return self.success(data) diff --git a/utils/captcha/__init__.py b/utils/captcha/__init__.py index b88f452..e645e46 100644 --- a/utils/captcha/__init__.py +++ b/utils/captcha/__init__.py @@ -82,10 +82,7 @@ class Captcha(object): x += font_size * random.randrange(6, 8) / 10 self.django_request.session[self.session_key] = "".join(code) - with BytesIO() as buf: - image.save(buf, "gif") - buf_str = buf.getvalue() - return buf_str + return image def check(self, code): """ diff --git a/utils/captcha/views.py b/utils/captcha/views.py index 36258d3..1c6c654 100644 --- a/utils/captcha/views.py +++ b/utils/captcha/views.py @@ -2,10 +2,9 @@ from base64 import b64encode from . import Captcha from ..api import APIView +from ..shortcuts import img2base64 class CaptchaAPIView(APIView): def get(self, request): - img_prefix = "data:image/png;base64," - img = img_prefix + b64encode(Captcha(request).get()).decode("utf-8") - return self.success(img) + return self.success(img2base64(Captcha(request).get())) diff --git a/utils/shortcuts.py b/utils/shortcuts.py index 38a4edb..634579a 100644 --- a/utils/shortcuts.py +++ b/utils/shortcuts.py @@ -1,5 +1,7 @@ import logging import random +from io import BytesIO +from base64 import b64encode from django.utils.crypto import get_random_string from envelopes import Envelope @@ -58,3 +60,12 @@ def build_query_string(kv_data, ignore_none=True): query_string = "?" query_string += (k + "=" + str(v)) return query_string + + +def img2base64(img): + with BytesIO() as buf: + img.save(buf, "gif") + buf_str = buf.getvalue() + img_prefix = "data:image/png;base64," + b64_str = img_prefix + b64encode(buf_str).decode("utf-8") + return b64_str \ No newline at end of file From a3ca8b23364a2ae3473e9796f9b9eb1ccd19e9ff Mon Sep 17 00:00:00 2001 From: zema1 Date: Wed, 13 Sep 2017 22:37:57 +0800 Subject: [PATCH 047/106] Use signals to save ip, user_agent, last_login in sessions --- .travis.yml | 2 +- account/__init__.py | 1 + account/apps.py | 9 +++++++++ account/migrations/0006_user_session_keys.py | 21 ++++++++++++++++++++ account/models.py | 1 + account/signals.py | 21 ++++++++++++++++++++ account/tests.py | 1 - account/views/admin.py | 1 - account/views/oj.py | 7 ++++--- oj/settings.py | 9 ++++----- problem/tests.py | 2 +- problem/views/oj.py | 11 ++++++++-- submission/views/oj.py | 2 +- utils/captcha/__init__.py | 1 - utils/captcha/views.py | 2 -- utils/shortcuts.py | 2 +- 16 files changed, 74 insertions(+), 19 deletions(-) create mode 100644 account/apps.py create mode 100644 account/migrations/0006_user_session_keys.py create mode 100644 account/signals.py diff --git a/.travis.yml b/.travis.yml index 1e3f964..b0ba885 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python python: - - "3.5" + - "3.6" install: - sudo apt-get install -qq redis-server && redis-server & - pip install -r deploy/requirements.txt diff --git a/account/__init__.py b/account/__init__.py index e69de29..d35b9f0 100644 --- a/account/__init__.py +++ b/account/__init__.py @@ -0,0 +1 @@ +default_app_config = 'account.apps.ProfilesConfig' \ No newline at end of file diff --git a/account/apps.py b/account/apps.py new file mode 100644 index 0000000..1336a18 --- /dev/null +++ b/account/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class ProfilesConfig(AppConfig): + name = "account" + verbose_name = "account" + + def ready(self): + import account.signals diff --git a/account/migrations/0006_user_session_keys.py b/account/migrations/0006_user_session_keys.py new file mode 100644 index 0000000..4a0282a --- /dev/null +++ b/account/migrations/0006_user_session_keys.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-16 06:22 +from __future__ import unicode_literals + +from django.db import migrations +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0005_auto_20170830_1154'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='session_keys', + field=jsonfield.fields.JSONField(default=[]), + ), + ] diff --git a/account/models.py b/account/models.py index a94e6a4..1876968 100644 --- a/account/models.py +++ b/account/models.py @@ -35,6 +35,7 @@ class User(AbstractBaseUser): auth_token = models.CharField(max_length=40, null=True) two_factor_auth = models.BooleanField(default=False) tfa_token = models.CharField(max_length=40, null=True) + session_keys = JSONField(default=[]) # open api key open_api = models.BooleanField(default=False) open_api_appkey = models.CharField(max_length=35, null=True) diff --git a/account/signals.py b/account/signals.py new file mode 100644 index 0000000..4d14970 --- /dev/null +++ b/account/signals.py @@ -0,0 +1,21 @@ +from django.utils.timezone import now +from django.dispatch import receiver +from django.contrib.auth.signals import user_logged_in, user_logged_out + + +@receiver(user_logged_in) +def add_user_session(sender, request, user, **kwargs): + request.session["ip"] = request.META.get('REMOTE_ADDR', '') + request.session["user_agent"] = request.META.get('HTTP_USER_AGENT', '') + request.session["last_login"] = now() + if request.session.session_key not in user.session_keys: + user.session_keys.append(request.session.session_key) + user.save() + + +@receiver(user_logged_out) +def delete_user_session(sender, request, user, **kwargs): + # user may be None + if user and request.session.session_key in user.session_keys: + user.session_keys.remove(request.session.session_key) + user.save() diff --git a/account/tests.py b/account/tests.py index d4addd5..e37867d 100644 --- a/account/tests.py +++ b/account/tests.py @@ -194,7 +194,6 @@ class AdminUserTest(APITestCase): resp_data = response.data["data"] self.assertEqual(resp_data["username"], self.username) self.assertEqual(resp_data["email"], "test@qq.com") - self.assertEqual(resp_data["real_name"], "test_name") self.assertEqual(resp_data["open_api"], True) self.assertEqual(resp_data["two_factor_auth"], False) self.assertEqual(resp_data["is_disabled"], False) diff --git a/account/views/admin.py b/account/views/admin.py index 62c5115..83f4a93 100644 --- a/account/views/admin.py +++ b/account/views/admin.py @@ -39,7 +39,6 @@ class UserAdminAPI(APIView): pass user.username = data["username"] - user.real_name = data["real_name"] user.email = data["email"] user.admin_type = data["admin_type"] user.is_disabled = data["is_disabled"] diff --git a/account/views/oj.py b/account/views/oj.py index f567c56..bc00ea1 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -5,6 +5,7 @@ from otpauth import OtpAuth from django.conf import settings from django.contrib import auth +from importlib import import_module from django.utils.timezone import now from django.views.decorators.csrf import ensure_csrf_cookie from django.utils.decorators import method_decorator @@ -267,8 +268,8 @@ class ApplyResetPasswordAPI(APIView): user = User.objects.get(email=data["email"]) except User.DoesNotExist: return self.error("User does not exist") - if user.reset_password_token_expire_time and \ - 0 < int((user.reset_password_token_expire_time - now()).total_seconds()) < 20 * 60: + if user.reset_password_token_expire_time and 0 < int( + (user.reset_password_token_expire_time - now()).total_seconds()) < 20 * 60: return self.error("You can only reset password once per 20 minutes") user.reset_password_token = rand_str() user.reset_password_token_expire_time = now() + timedelta(minutes=20) @@ -278,7 +279,7 @@ class ApplyResetPasswordAPI(APIView): "website_name": config.name, "link": f"{config.base_url}/reset-password/{user.reset_password_token}" } - email_html = render_to_string('reset_password_email.html', render_data) + email_html = render_to_string("reset_password_email.html", render_data) send_email_async.delay(config.name, user.email, user.username, diff --git a/oj/settings.py b/oj/settings.py index 1d4983d..7851914 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -33,10 +33,11 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) INSTALLED_APPS = ( 'django.contrib.auth', - 'django.contrib.contenttypes', 'django.contrib.sessions', + 'django.contrib.contenttypes', 'django.contrib.messages', 'django.contrib.staticfiles', + 'rest_framework', 'account', 'announcement', @@ -45,8 +46,6 @@ INSTALLED_APPS = ( 'contest', 'utils', 'submission', - - 'rest_framework', ) MIDDLEWARE_CLASSES = ( @@ -54,14 +53,14 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', 'account.middleware.AdminRoleRequiredMiddleware', 'account.middleware.SessionSecurityMiddleware', + # 'account.middleware.LogSqlMiddleware', ) - +SESSION_ENGINE = 'django.contrib.sessions.backends.cache' ROOT_URLCONF = 'oj.urls' TEMPLATES = [ diff --git a/problem/tests.py b/problem/tests.py index 3bdc79c..7979127 100644 --- a/problem/tests.py +++ b/problem/tests.py @@ -147,7 +147,7 @@ class ProblemAPITest(APITestCase): def test_get_problem_list(self): self.create_problem() - resp = self.client.get(self.url) + resp = self.client.get(f"{self.url}?limit=10") self.assertSuccess(resp) def get_one_problem(self): diff --git a/problem/views/oj.py b/problem/views/oj.py index 5d3f2fb..40fb021 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -6,6 +6,7 @@ from ..serializers import ProblemSerializer, TagSerializer from ..serializers import ContestProblemSerializer from contest.models import ContestRuleType + class ProblemTagAPI(APIView): def get(self, request): return self.success(TagSerializer(ProblemTag.objects.all(), many=True).data) @@ -22,6 +23,10 @@ class ProblemAPI(APIView): except Problem.DoesNotExist: return self.error("Problem does not exist") + limit = request.GET.get("limit") + if not limit: + return self.error("Limit is needed") + problems = Problem.objects.select_related("created_by").filter(visible=True) # 按照标签筛选 tag_text = request.GET.get("tag") @@ -61,12 +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, visible=True) + problem = ContestProblem.objects.select_related("created_by").get(_id=problem_id, contest=self.contest, + visible=True) except ContestProblem.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 = ContestProblem.objects.select_related("created_by").filter(contest=self.contest, + visible=True) # 根据profile, 为做过的题目添加标记 data = ContestProblemSerializer(contest_problems, many=True).data if request.user.id: diff --git a/submission/views/oj.py b/submission/views/oj.py index d6e88e0..f4d276d 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -1,7 +1,7 @@ from account.decorators import login_required, check_contest_permission from judge.tasks import judge_task -from judge.dispatcher import JudgeDispatcher +# from judge.dispatcher import JudgeDispatcher from problem.models import Problem, ProblemRuleType, ContestProblem from contest.models import Contest, ContestStatus from utils.api import APIView, validate_serializer diff --git a/utils/captcha/__init__.py b/utils/captcha/__init__.py index e645e46..8b8375c 100644 --- a/utils/captcha/__init__.py +++ b/utils/captcha/__init__.py @@ -15,7 +15,6 @@ import os import time import random -from io import BytesIO from PIL import Image, ImageDraw, ImageFont diff --git a/utils/captcha/views.py b/utils/captcha/views.py index 1c6c654..4642e32 100644 --- a/utils/captcha/views.py +++ b/utils/captcha/views.py @@ -1,5 +1,3 @@ -from base64 import b64encode - from . import Captcha from ..api import APIView from ..shortcuts import img2base64 diff --git a/utils/shortcuts.py b/utils/shortcuts.py index 634579a..e2cc470 100644 --- a/utils/shortcuts.py +++ b/utils/shortcuts.py @@ -68,4 +68,4 @@ def img2base64(img): buf_str = buf.getvalue() img_prefix = "data:image/png;base64," b64_str = img_prefix + b64encode(buf_str).decode("utf-8") - return b64_str \ No newline at end of file + return b64_str From 1ee0596a3aa66dacdd35e045508141c25bfefcaf Mon Sep 17 00:00:00 2001 From: zema1 Date: Sat, 16 Sep 2017 10:38:49 +0800 Subject: [PATCH 048/106] add session management api; add more unit tests for account module --- .flake8 | 1 + account/__init__.py | 2 +- account/signals.py | 4 +- account/tests.py | 226 +++++++++++++++++++++++++++++++++++++- account/urls/oj.py | 6 +- account/views/oj.py | 75 ++++++++++++- utils/api/_serializers.py | 4 +- utils/api/tests.py | 10 +- utils/shortcuts.py | 9 ++ 9 files changed, 320 insertions(+), 17 deletions(-) diff --git a/.flake8 b/.flake8 index 3198bbc..d64dbd5 100644 --- a/.flake8 +++ b/.flake8 @@ -3,6 +3,7 @@ exclude = xss_filter.py, */migrations/, *settings.py + */apps.py max-line-length = 180 inline-quotes = " no-accept-encodings = True diff --git a/account/__init__.py b/account/__init__.py index d35b9f0..28aa769 100644 --- a/account/__init__.py +++ b/account/__init__.py @@ -1 +1 @@ -default_app_config = 'account.apps.ProfilesConfig' \ No newline at end of file +default_app_config = "account.apps.ProfilesConfig" diff --git a/account/signals.py b/account/signals.py index 4d14970..0a15370 100644 --- a/account/signals.py +++ b/account/signals.py @@ -5,8 +5,8 @@ from django.contrib.auth.signals import user_logged_in, user_logged_out @receiver(user_logged_in) def add_user_session(sender, request, user, **kwargs): - request.session["ip"] = request.META.get('REMOTE_ADDR', '') - request.session["user_agent"] = request.META.get('HTTP_USER_AGENT', '') + request.session["ip"] = request.META.get("REMOTE_ADDR", "") + request.session["user_agent"] = request.META.get("HTTP_USER_AGENT", "") request.session["last_login"] = now() if request.session.session_key not in user.session_keys: user.session_keys.append(request.session.session_key) diff --git a/account/tests.py b/account/tests.py index e37867d..34da106 100644 --- a/account/tests.py +++ b/account/tests.py @@ -1,7 +1,9 @@ import time from unittest import mock +from datetime import timedelta from django.contrib import auth +from django.utils.timezone import now from otpauth import OtpAuth from utils.api.tests import APIClient, APITestCase @@ -28,6 +30,40 @@ class PermissionDecoratorTest(APITestCase): pass +class DuplicateUserCheckAPITest(APITestCase): + def setUp(self): + self.create_user("test", "test123", login=False) + self.url = self.reverse("check_username_or_email") + + def test_duplicate_username(self): + resp = self.client.post(self.url, data={"username": "test"}) + data = resp.data["data"] + self.assertEqual(data["username"], True) + + def test_ok_username(self): + resp = self.client.post(self.url, data={"username": "test1"}) + data = resp.data["data"] + self.assertEqual(data["username"], False) + + +class TFARequiredCheckAPITest(APITestCase): + def setUp(self): + self.url = self.reverse("tfa_required_check") + self.create_user("test", "test123", login=False) + + def test_not_required_tfa(self): + resp = self.client.post(self.url, data={"username": "test"}) + self.assertSuccess(resp) + self.assertEqual(resp.data["data"]["result"], False) + + def test_required_tfa(self): + user = User.objects.first() + user.two_factor_auth = True + user.save() + resp = self.client.post(self.url, data={"username": "test"}) + self.assertEqual(resp.data["data"]["result"], True) + + class UserLoginAPITest(APITestCase): def setUp(self): self.username = self.password = "test" @@ -87,7 +123,7 @@ class UserLoginAPITest(APITestCase): response = self.client.post(self.login_url, data={"username": self.username, "password": self.password}) - self.assertDictEqual(response.data, {"error": None, "data": "tfa_required"}) + self.assertDictEqual(response.data, {"error": "error", "data": "tfa_required"}) user = auth.get_user(self.client) self.assertFalse(user.is_authenticated()) @@ -142,6 +178,160 @@ class UserRegisterAPITest(CaptchaTest): self.assertDictEqual(response.data, {"error": "error", "data": "Email already exists"}) +class SessionManagementAPITest(APITestCase): + def setUp(self): + self.create_user("test", "test123") + self.url = self.reverse("session_management_api") + + def test_get_sessions(self): + resp = self.client.get(self.url) + self.assertSuccess(resp) + data = resp.data["data"] + self.assertEqual(len(data), 1) + + def test_delete_session_key(self): + # resp = self.client.delete(self.url, data={"session_key": self.client.session.session_key}) + resp = self.client.delete(self.url + "?session_key=" + self.client.session.session_key) + self.assertSuccess(resp) + + def test_delete_session_with_invalid_key(self): + resp = self.client.delete(self.url + "?session_key=aaaaaaaaaa") + self.assertDictEqual(resp.data, {"error": "error", "data": "Invalid session_key"}) + + +class UserProfileAPITest(APITestCase): + def setUp(self): + self.url = self.reverse("user_profile_api") + + def test_get_profile_without_login(self): + resp = self.client.get(self.url) + self.assertDictEqual(resp.data, {"error": None, "data": 0}) + + def test_get_profile(self): + self.create_user("test", "test123") + resp = self.client.get(self.url) + self.assertSuccess(resp) + + def test_update_profile(self): + self.create_user("test", "test123") + update_data = {"real_name": "zemal", "submission_number": 233} + resp = self.client.put(self.url, data=update_data) + self.assertSuccess(resp) + data = resp.data["data"] + self.assertEqual(data["real_name"], "zemal") + self.assertEqual(data["submission_number"], 0) + + +class TwoFactorAuthAPITest(APITestCase): + def setUp(self): + self.url = self.reverse("two_factor_auth_api") + self.create_user("test", "test123") + self.create_website_config() + + def _get_tfa_code(self): + user = User.objects.first() + code = OtpAuth(user.tfa_token).totp() + if len(str(code)) < 6: + code = (6 - len(str(code))) * "0" + str(code) + return code + + def test_get_image(self): + resp = self.client.get(self.url) + self.assertSuccess(resp) + + def test_open_tfa_with_invalid_code(self): + self.test_get_image() + resp = self.client.post(self.url, data={"code": "000000"}) + self.assertDictEqual(resp.data, {"error": "error", "data": "Invalid code"}) + + def test_open_tfa_with_correct_code(self): + self.test_get_image() + code = self._get_tfa_code() + resp = self.client.post(self.url, data={"code": code}) + self.assertSuccess(resp) + user = User.objects.first() + self.assertEqual(user.two_factor_auth, True) + + def test_close_tfa_with_invalid_code(self): + self.test_open_tfa_with_correct_code() + resp = self.client.post(self.url, data={"code": "000000"}) + self.assertDictEqual(resp.data, {"error": "error", "data": "Invalid code"}) + + def test_close_tfa_with_correct_code(self): + self.test_open_tfa_with_correct_code() + code = self._get_tfa_code() + resp = self.client.put(self.url, data={"code": code}) + self.assertSuccess(resp) + user = User.objects.first() + self.assertEqual(user.two_factor_auth, False) + + +@mock.patch("account.views.oj.send_email_async.delay") +class ApplyResetPasswordAPITest(CaptchaTest): + def setUp(self): + self.create_user("test", "test123", login=False) + user = User.objects.first() + user.email = "test@oj.com" + user.save() + self.url = self.reverse("apply_reset_password_api") + self.create_website_config() + self.data = {"email": "test@oj.com", "captcha": self._set_captcha(self.client.session)} + + def _refresh_captcha(self): + self.data["captcha"] = self._set_captcha(self.client.session) + + def test_apply_reset_password(self, send_email_delay): + resp = self.client.post(self.url, data=self.data) + self.assertSuccess(resp) + send_email_delay.assert_called() + + def test_apply_reset_password_twice_in_20_mins(self, send_email_delay): + self.test_apply_reset_password() + send_email_delay.reset_mock() + self._refresh_captcha() + resp = self.client.post(self.url, data=self.data) + self.assertDictEqual(resp.data, {"error": "error", "data": "You can only reset password once per 20 minutes"}) + send_email_delay.assert_not_called() + + def test_apply_reset_password_again_after_20_mins(self, send_email_delay): + self.test_apply_reset_password() + user = User.objects.first() + user.reset_password_token_expire_time = now() - timedelta(minutes=21) + user.save() + self._refresh_captcha() + self.test_apply_reset_password() + + +class ResetPasswordAPITest(CaptchaTest): + def setUp(self): + self.create_user("test", "test123", login=False) + self.url = self.reverse("reset_password_api") + user = User.objects.first() + user.reset_password_token = "online_judge?" + user.reset_password_token_expire_time = now() + timedelta(minutes=20) + user.save() + self.data = {"token": user.reset_password_token, + "captcha": self._set_captcha(self.client.session), + "password": "test456"} + + def test_reset_password_with_correct_token(self): + resp = self.client.post(self.url, data=self.data) + self.assertSuccess(resp) + self.assertTrue(self.client.login(username="test", password="test456")) + + def test_reset_password_with_invalid_token(self): + self.data["token"] = "aaaaaaaaaaa" + resp = self.client.post(self.url, data=self.data) + self.assertDictEqual(resp.data, {"error": "error", "data": "Token dose not exist"}) + + def test_reset_password_with_expired_token(self): + user = User.objects.first() + user.reset_password_token_expire_time = now() - timedelta(seconds=30) + user.save() + resp = self.client.post(self.url, data=self.data) + self.assertDictEqual(resp.data, {"error": "error", "data": "Token have expired"}) + + class UserChangePasswordAPITest(CaptchaTest): def setUp(self): self.client = APIClient() @@ -248,3 +438,37 @@ class AdminUserTest(APITestCase): # if `openapi_app_key` is not None, the value is not changed self.assertTrue(resp_data["open_api"]) self.assertEqual(User.objects.get(id=self.regular_user.id).open_api_appkey, key) + + +class UserRankAPITest(APITestCase): + def setUp(self): + self.url = self.reverse("user_rank_api") + self.create_user("test1", "test123", login=False) + self.create_user("test2", "test123", login=False) + test1 = User.objects.get(username="test1") + profile1 = test1.userprofile + profile1.submission_number = 10 + profile1.accepted_number = 10 + profile1.total_score = 240 + profile1.save() + + test2 = User.objects.get(username="test2") + profile2 = test2.userprofile + profile2.submission_number = 15 + profile2.accepted_number = 10 + profile2.total_score = 700 + profile2.save() + + def test_get_acm_rank(self): + resp = self.client.get(self.url, data={"rule": "acm"}) + self.assertSuccess(resp) + data = resp.data["data"] + self.assertEqual(data[0]["user"]["username"], "test1") + self.assertEqual(data[1]["user"]["username"], "test2") + + def test_get_oi_rank(self): + resp = self.client.get(self.url, data={"rule": "oi"}) + self.assertSuccess(resp) + data = resp.data["data"] + self.assertEqual(data[0]["user"]["username"], "test2") + self.assertEqual(data[1]["user"]["username"], "test1") diff --git a/account/urls/oj.py b/account/urls/oj.py index e31a81e..a9bf642 100644 --- a/account/urls/oj.py +++ b/account/urls/oj.py @@ -4,7 +4,7 @@ from ..views.oj import (ApplyResetPasswordAPI, ResetPasswordAPI, UserChangePasswordAPI, UserRegisterAPI, UserLoginAPI, UserLogoutAPI, UsernameOrEmailCheck, SSOAPI, AvatarUploadAPI, TwoFactorAuthAPI, UserProfileAPI, - UserRankAPI) + UserRankAPI, CheckTFARequiredAPI, SessionManagementAPI) from utils.captcha.views import CaptchaAPIView @@ -14,12 +14,14 @@ urlpatterns = [ url(r"^register/?$", UserRegisterAPI.as_view(), name="user_register_api"), url(r"^change_password/?$", UserChangePasswordAPI.as_view(), name="user_change_password_api"), url(r"^apply_reset_password/?$", ApplyResetPasswordAPI.as_view(), name="apply_reset_password_api"), - url(r"^reset_password/?$", ResetPasswordAPI.as_view(), name="apply_reset_password_api"), + url(r"^reset_password/?$", ResetPasswordAPI.as_view(), name="reset_password_api"), url(r"^captcha/?$", CaptchaAPIView.as_view(), name="show_captcha"), url(r"^check_username_or_email", UsernameOrEmailCheck.as_view(), name="check_username_or_email"), url(r"^profile/?$", UserProfileAPI.as_view(), name="user_profile_api"), url(r"^avatar/upload/?$", AvatarUploadAPI.as_view(), name="avatar_upload_api"), url(r"^sso/?$", SSOAPI.as_view(), name="sso_api"), + url(r"^tfa_required/?$", CheckTFARequiredAPI.as_view(), name="tfa_required_check"), url(r"^two_factor_auth/?$", TwoFactorAuthAPI.as_view(), name="two_factor_auth_api"), url(r"^user_rank/?$", UserRankAPI.as_view(), name="user_rank_api"), + url(r"^sessions/?$", SessionManagementAPI.as_view(), name="session_management_api") ] diff --git a/account/views/oj.py b/account/views/oj.py index bc00ea1..e88ce9e 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -14,7 +14,7 @@ from django.template.loader import render_to_string from conf.models import WebsiteConfig from utils.api import APIView, validate_serializer, CSRFExemptAPIView from utils.captcha import Captcha -from utils.shortcuts import rand_str, img2base64 +from utils.shortcuts import rand_str, img2base64, datetime2str from ..decorators import login_required from ..models import User, UserProfile @@ -77,7 +77,6 @@ class AvatarUploadAPI(CSRFExemptAPIView): with open(os.path.join(settings.IMAGE_UPLOAD_DIR, name), "wb") as img: for chunk in avatar: img.write(chunk) - print(os.path.join(settings.IMAGE_UPLOAD_DIR, name)) return self.success({"path": "/static/upload/" + name}) @@ -126,7 +125,7 @@ class TwoFactorAuthAPI(APIView): user.save() config = WebsiteConfig.objects.first() - label = f"{config.name_shortcut}:{user.username}@{config.base_url}" + label = f"{config.name_shortcut}:{user.username}" image = qrcode.make(OtpAuth(token).to_uri("totp", label, config.name)) return self.success(img2base64(image)) @@ -143,18 +142,38 @@ class TwoFactorAuthAPI(APIView): user.save() return self.success("Succeeded") else: - return self.error("Invalid captcha") + return self.error("Invalid code") @login_required @validate_serializer(TwoFactorAuthCodeSerializer) def put(self, request): code = request.data["code"] user = request.user + if not user.two_factor_auth: + return self.error("Other session have disabled TFA") if OtpAuth(user.tfa_token).valid_totp(code): user.two_factor_auth = False user.save() + return self.success("Succeeded") else: - return self.error("Invalid captcha") + return self.error("Invalid code") + + +class CheckTFARequiredAPI(APIView): + @validate_serializer(UsernameOrEmailCheckSerializer) + def post(self, request): + """ + Check TFA is required + """ + data = request.data + result = False + if data.get("username"): + try: + user = User.objects.get(username=data["username"]) + result = user.two_factor_auth + except User.DoesNotExist: + pass + return self.success({"result": result}) class UserLoginAPI(APIView): @@ -173,7 +192,7 @@ class UserLoginAPI(APIView): # `tfa_code` not in post data if user.two_factor_auth and "tfa_code" not in data: - return self.success("tfa_required") + return self.error("tfa_required") if OtpAuth(user.tfa_token).valid_totp(data["tfa_code"]): auth.login(request, user) @@ -302,11 +321,55 @@ class ResetPasswordAPI(APIView): if int((user.reset_password_token_expire_time - now()).total_seconds()) < 0: return self.error("Token have expired") user.reset_password_token = None + user.two_factor_auth = False user.set_password(data["password"]) user.save() return self.success("Succeeded") +class SessionManagementAPI(APIView): + @login_required + def get(self, request): + engine = import_module(settings.SESSION_ENGINE) + SessionStore = engine.SessionStore + current_session = request.COOKIES.get(settings.SESSION_COOKIE_NAME) + session_keys = request.user.session_keys + result = [] + modified = False + for key in session_keys[:]: + session = SessionStore(key) + # session does not exist or is expiry + if not session._session: + session_keys.remove(key) + modified = True + continue + + s = {} + if current_session == key: + s["current_session"] = True + s["ip"] = session["ip"] + s["user_agent"] = session["user_agent"] + s["last_login"] = datetime2str(session["last_login"]) + s["session_key"] = key + result.append(s) + if modified: + request.user.save() + return self.success(result) + + @login_required + def delete(self, request): + session_key = request.GET.get("session_key") + if not session_key: + return self.error("Parameter Error") + request.session.delete(session_key) + if session_key in request.user.session_keys: + request.user.session_keys.remove(session_key) + request.user.save() + return self.success("Succeeded") + else: + return self.error("Invalid session_key") + + class UserRankAPI(APIView): def get(self, request): rule_type = request.GET.get("rule") diff --git a/utils/api/_serializers.py b/utils/api/_serializers.py index 816845a..737a965 100644 --- a/utils/api/_serializers.py +++ b/utils/api/_serializers.py @@ -1,11 +1,9 @@ -from django.utils import timezone from rest_framework import serializers class DateTimeTZField(serializers.DateTimeField): def to_representation(self, value): - # self.format = "%Y-%m-%d %H:%M:%S %Z" - value = timezone.localtime(value) + # value = timezone.localtime(value) return super(DateTimeTZField, self).to_representation(value) diff --git a/utils/api/tests.py b/utils/api/tests.py index 9f9d997..3d9cc30 100644 --- a/utils/api/tests.py +++ b/utils/api/tests.py @@ -3,12 +3,14 @@ from django.test.testcases import TestCase from rest_framework.test import APIClient from account.models import AdminType, ProblemPermission, User, UserProfile +from conf.models import WebsiteConfig class APITestCase(TestCase): client_class = APIClient - def create_user(self, username, password, admin_type=AdminType.REGULAR_USER, login=True, problem_permission=ProblemPermission.NONE): + def create_user(self, username, password, admin_type=AdminType.REGULAR_USER, login=True, + problem_permission=ProblemPermission.NONE): user = User.objects.create(username=username, admin_type=admin_type, problem_permission=problem_permission) user.set_password(password) UserProfile.objects.create(user=user) @@ -18,13 +20,17 @@ class APITestCase(TestCase): return user def create_admin(self, username="admin", password="admin", login=True): - return self.create_user(username=username, password=password, admin_type=AdminType.ADMIN, problem_permission=ProblemPermission.OWN, + return self.create_user(username=username, password=password, admin_type=AdminType.ADMIN, + problem_permission=ProblemPermission.OWN, login=login) def create_super_admin(self, username="root", password="root", login=True): return self.create_user(username=username, password=password, admin_type=AdminType.SUPER_ADMIN, problem_permission=ProblemPermission.ALL, login=login) + def create_website_config(self): + return WebsiteConfig.objects.create() + def reverse(self, url_name): return reverse(url_name) diff --git a/utils/shortcuts.py b/utils/shortcuts.py index e2cc470..01ddd80 100644 --- a/utils/shortcuts.py +++ b/utils/shortcuts.py @@ -69,3 +69,12 @@ def img2base64(img): img_prefix = "data:image/png;base64," b64_str = img_prefix + b64encode(buf_str).decode("utf-8") return b64_str + + +def datetime2str(value, format="iso-8601"): + if format.lower() == "iso-8601": + value = value.isoformat() + if value.endswith("+00:00"): + value = value[:-6] + "Z" + return value + return value.strftime(format) From 034ad59f2eba73c7167fa41d41e60359efe0d332 Mon Sep 17 00:00:00 2001 From: zema1 Date: Tue, 19 Sep 2017 19:10:50 +0800 Subject: [PATCH 049/106] support avatar upload; use middleware to operate session data. --- account/__init__.py | 1 - account/apps.py | 9 --------- account/middleware.py | 25 ++++++++++++++++++----- account/migrations/0001_initial.py | 2 +- account/models.py | 8 ++++---- account/serializers.py | 14 ++++++------- account/signals.py | 21 -------------------- account/tests.py | 10 ++++++---- account/urls/oj.py | 2 +- account/views/oj.py | 32 ++++++++++++++++++++---------- oj/local_settings.py | 4 ++++ oj/settings.py | 4 +++- utils/shortcuts.py | 5 +++++ 13 files changed, 72 insertions(+), 65 deletions(-) delete mode 100644 account/apps.py delete mode 100644 account/signals.py diff --git a/account/__init__.py b/account/__init__.py index 28aa769..e69de29 100644 --- a/account/__init__.py +++ b/account/__init__.py @@ -1 +0,0 @@ -default_app_config = "account.apps.ProfilesConfig" diff --git a/account/apps.py b/account/apps.py deleted file mode 100644 index 1336a18..0000000 --- a/account/apps.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.apps import AppConfig - - -class ProfilesConfig(AppConfig): - name = "account" - verbose_name = "account" - - def ready(self): - import account.signals diff --git a/account/middleware.py b/account/middleware.py index dac102d..48a6942 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -12,16 +12,31 @@ from utils.api import JSONResponse class SessionSecurityMiddleware(MiddlewareMixin): def process_request(self, request): - if request.user.is_authenticated() and request.user.is_admin_role(): - if "last_activity" in request.session: - # 24 hours passed since last visit - if time.time() - request.session["last_activity"] >= 24 * 60 * 60: + if request.user.is_authenticated(): + if "last_activity" in request.session and request.user.is_admin_role(): + # 24 hours passed since last visit, 86400 = 24 * 60 * 60 + if time.time() - request.session["last_activity"] >= 86400: auth.logout(request) return JSONResponse.response({"error": "login-required", "data": _("Please login in first")}) - # update last active time request.session["last_activity"] = time.time() +class SessionRecordMiddleware(MiddlewareMixin): + def process_request(self, request): + if request.user.is_authenticated(): + session = request.session + ip = request.META.get("REMOTE_ADDR", "") + user_agent = request.META.get("HTTP_USER_AGENT", "") + _ip = session.setdefault("ip", ip) + _user_agent = session.setdefault("user_agent", user_agent) + if ip != _ip or user_agent != _user_agent: + session.modified = True + user_sessions = request.user.session_keys + if request.session.session_key not in user_sessions: + user_sessions.append(session.session_key) + request.user.save() + + class AdminRoleRequiredMiddleware(MiddlewareMixin): def process_request(self, request): path = request.path_info diff --git a/account/migrations/0001_initial.py b/account/migrations/0001_initial.py index c6de9c3..a96776e 100644 --- a/account/migrations/0001_initial.py +++ b/account/migrations/0001_initial.py @@ -50,7 +50,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('problems_status', jsonfield.fields.JSONField(default={})), - ('avatar', models.CharField(default=account.models._random_avatar, max_length=50)), + ('avatar', models.CharField(default=account.models._default_avatar, max_length=50)), ('blog', models.URLField(blank=True, null=True)), ('mood', models.CharField(blank=True, max_length=200, null=True)), ('accepted_problem_number', models.IntegerField(default=0)), diff --git a/account/models.py b/account/models.py index 1876968..6bf4a19 100644 --- a/account/models.py +++ b/account/models.py @@ -1,4 +1,5 @@ from django.contrib.auth.models import AbstractBaseUser +from django.conf import settings from django.db import models from jsonfield import JSONField @@ -62,9 +63,8 @@ class User(AbstractBaseUser): db_table = "user" -def _random_avatar(): - import random - return "/static/img/avatar/avatar-" + str(random.randint(1, 20)) + ".png" +def _default_avatar(): + return f"/{settings.IMAGE_UPLOAD_DIR}/default.png" class UserProfile(models.Model): @@ -76,7 +76,7 @@ class UserProfile(models.Model): oi_problems_status = JSONField(default={}) real_name = models.CharField(max_length=30, blank=True, null=True) - avatar = models.CharField(max_length=50, default=_random_avatar) + avatar = models.CharField(max_length=50, default=_default_avatar) blog = models.URLField(blank=True, null=True) mood = models.CharField(max_length=200, blank=True, null=True) phone_number = models.CharField(max_length=15, blank=True, null=True) diff --git a/account/serializers.py b/account/serializers.py index 9917f71..2b71ceb 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -70,13 +70,13 @@ class EditUserSerializer(serializers.Serializer): class EditUserProfileSerializer(serializers.Serializer): - real_name = serializers.CharField(max_length=30) - avatar = serializers.CharField(max_length=100, allow_null=True, required=False) - blog = serializers.URLField(allow_null=True, required=False) - mood = serializers.CharField(max_length=200, allow_null=True, required=False) - phone_number = serializers.CharField(max_length=15, allow_null=True, required=False, ) - school = serializers.CharField(max_length=200, allow_null=True, required=False) - major = serializers.CharField(max_length=200, allow_null=True, required=False) + real_name = serializers.CharField(max_length=30, allow_blank=True) + avatar = serializers.CharField(max_length=100, allow_blank=True, required=False) + blog = serializers.URLField(allow_blank=True, required=False) + mood = serializers.CharField(max_length=200, allow_blank=True, required=False) + phone_number = serializers.CharField(max_length=15, allow_blank=True, required=False, ) + school = serializers.CharField(max_length=200, allow_blank=True, required=False) + major = serializers.CharField(max_length=200, allow_blank=True, required=False) class ApplyResetPasswordSerializer(serializers.Serializer): diff --git a/account/signals.py b/account/signals.py deleted file mode 100644 index 0a15370..0000000 --- a/account/signals.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.utils.timezone import now -from django.dispatch import receiver -from django.contrib.auth.signals import user_logged_in, user_logged_out - - -@receiver(user_logged_in) -def add_user_session(sender, request, user, **kwargs): - request.session["ip"] = request.META.get("REMOTE_ADDR", "") - request.session["user_agent"] = request.META.get("HTTP_USER_AGENT", "") - request.session["last_login"] = now() - if request.session.session_key not in user.session_keys: - user.session_keys.append(request.session.session_key) - user.save() - - -@receiver(user_logged_out) -def delete_user_session(sender, request, user, **kwargs): - # user may be None - if user and request.session.session_key in user.session_keys: - user.session_keys.remove(request.session.session_key) - user.save() diff --git a/account/tests.py b/account/tests.py index 34da106..e648e76 100644 --- a/account/tests.py +++ b/account/tests.py @@ -182,6 +182,9 @@ class SessionManagementAPITest(APITestCase): def setUp(self): self.create_user("test", "test123") self.url = self.reverse("session_management_api") + # launch a request to provide session data + login_url = self.reverse("user_login_api") + self.client.post(login_url, data={"username": "test", "password": "test123"}) def test_get_sessions(self): resp = self.client.get(self.url) @@ -189,10 +192,9 @@ class SessionManagementAPITest(APITestCase): data = resp.data["data"] self.assertEqual(len(data), 1) - def test_delete_session_key(self): - # resp = self.client.delete(self.url, data={"session_key": self.client.session.session_key}) - resp = self.client.delete(self.url + "?session_key=" + self.client.session.session_key) - self.assertSuccess(resp) + # def test_delete_session_key(self): + # resp = self.client.delete(self.url + "?session_key=" + self.session_key) + # self.assertSuccess(resp) def test_delete_session_with_invalid_key(self): resp = self.client.delete(self.url + "?session_key=aaaaaaaaaa") diff --git a/account/urls/oj.py b/account/urls/oj.py index a9bf642..b80b34e 100644 --- a/account/urls/oj.py +++ b/account/urls/oj.py @@ -18,7 +18,7 @@ urlpatterns = [ url(r"^captcha/?$", CaptchaAPIView.as_view(), name="show_captcha"), url(r"^check_username_or_email", UsernameOrEmailCheck.as_view(), name="check_username_or_email"), url(r"^profile/?$", UserProfileAPI.as_view(), name="user_profile_api"), - url(r"^avatar/upload/?$", AvatarUploadAPI.as_view(), name="avatar_upload_api"), + url(r"^upload_avatar/?$", AvatarUploadAPI.as_view(), name="avatar_upload_api"), url(r"^sso/?$", SSOAPI.as_view(), name="sso_api"), url(r"^tfa_required/?$", CheckTFARequiredAPI.as_view(), name="tfa_required_check"), url(r"^two_factor_auth/?$", TwoFactorAuthAPI.as_view(), name="two_factor_auth_api"), diff --git a/account/views/oj.py b/account/views/oj.py index e88ce9e..06d5c9e 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -12,9 +12,9 @@ from django.utils.decorators import method_decorator from django.template.loader import render_to_string from conf.models import WebsiteConfig -from utils.api import APIView, validate_serializer, CSRFExemptAPIView +from utils.api import APIView, validate_serializer from utils.captcha import Captcha -from utils.shortcuts import rand_str, img2base64, datetime2str +from utils.shortcuts import rand_str, img2base64, timestamp2utcstr from ..decorators import login_required from ..models import User, UserProfile @@ -59,7 +59,7 @@ class UserProfileAPI(APIView): return self.success(UserProfileSerializer(user_profile).data) -class AvatarUploadAPI(CSRFExemptAPIView): +class AvatarUploadAPI(APIView): request_parsers = () def post(self, request): @@ -67,17 +67,26 @@ class AvatarUploadAPI(CSRFExemptAPIView): if form.is_valid(): avatar = form.cleaned_data["file"] else: - return self.error("Upload failed") - if avatar.size > 1024 * 1024: - return self.error("Picture too large") - if os.path.splitext(avatar.name)[-1].lower() not in [".gif", ".jpg", ".jpeg", ".bmp", ".png"]: + return self.error("Invalid file content") + # 2097152 = 2 * 1024 * 1024 = 2MB + if avatar.size > 2097152: + return self.error("Picture is too large") + suffix = os.path.splitext(avatar.name)[-1].lower() + if suffix not in [".gif", ".jpg", ".jpeg", ".bmp", ".png"]: return self.error("Unsupported file format") - name = "avatar_" + rand_str(5) + os.path.splitext(avatar.name)[-1] - with open(os.path.join(settings.IMAGE_UPLOAD_DIR, name), "wb") as img: + name = rand_str(10) + suffix + with open(os.path.join(settings.IMAGE_UPLOAD_DIR_ABS, name), "wb") as img: for chunk in avatar: img.write(chunk) - return self.success({"path": "/static/upload/" + name}) + user_profile = request.user.userprofile + _, old_avatar = os.path.split(user_profile.avatar) + if old_avatar != "default.png": + os.remove(os.path.join(settings.IMAGE_UPLOAD_DIR_ABS, old_avatar)) + + user_profile.avatar = f"/{settings.IMAGE_UPLOAD_DIR}/{name}" + user_profile.save() + return self.success("Succeeded") class SSOAPI(APIView): @@ -333,6 +342,7 @@ class SessionManagementAPI(APIView): engine = import_module(settings.SESSION_ENGINE) SessionStore = engine.SessionStore current_session = request.COOKIES.get(settings.SESSION_COOKIE_NAME) + current_session = request.session.session_key session_keys = request.user.session_keys result = [] modified = False @@ -349,7 +359,7 @@ class SessionManagementAPI(APIView): s["current_session"] = True s["ip"] = session["ip"] s["user_agent"] = session["user_agent"] - s["last_login"] = datetime2str(session["last_login"]) + s["last_activity"] = timestamp2utcstr(session["last_activity"]) s["session_key"] = key result.append(s) if modified: diff --git a/oj/local_settings.py b/oj/local_settings.py index 562f1f0..cee68f2 100644 --- a/oj/local_settings.py +++ b/oj/local_settings.py @@ -24,4 +24,8 @@ ALLOWED_HOSTS = ["*"] TEST_CASE_DIR = "/tmp" +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, "static"), +] + LOG_PATH = "log/" diff --git a/oj/settings.py b/oj/settings.py index 7851914..eef91a6 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -58,6 +58,7 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.security.SecurityMiddleware', 'account.middleware.AdminRoleRequiredMiddleware', 'account.middleware.SessionSecurityMiddleware', + 'account.middleware.SessionRecordMiddleware', # 'account.middleware.LogSqlMiddleware', ) SESSION_ENGINE = 'django.contrib.sessions.backends.cache' @@ -213,7 +214,8 @@ BROKER_URL = 'redis://%s:%s/%s' % (REDIS_QUEUE["host"], str(REDIS_QUEUE["port"]) CELERY_ACCEPT_CONTENT = ["json"] CELERY_TASK_SERIALIZER = "json" -IMAGE_UPLOAD_DIR = os.path.join(BASE_DIR, 'upload/') +IMAGE_UPLOAD_DIR = 'static/avatar' +IMAGE_UPLOAD_DIR_ABS = os.path.join(BASE_DIR, IMAGE_UPLOAD_DIR) # 用于限制用户恶意提交大量代码 TOKEN_BUCKET_DEFAULT_CAPACITY = 50 diff --git a/utils/shortcuts.py b/utils/shortcuts.py index 01ddd80..525eef0 100644 --- a/utils/shortcuts.py +++ b/utils/shortcuts.py @@ -1,5 +1,6 @@ import logging import random +import datetime from io import BytesIO from base64 import b64encode @@ -78,3 +79,7 @@ def datetime2str(value, format="iso-8601"): value = value[:-6] + "Z" return value return value.strftime(format) + + +def timestamp2utcstr(value): + return datetime.datetime.utcfromtimestamp(value).isoformat() From e9c734481560909a47820d4d1dd9d9f5d3e98fb9 Mon Sep 17 00:00:00 2001 From: zema1 Date: Fri, 22 Sep 2017 16:41:29 +0800 Subject: [PATCH 050/106] adjust account fields, cache the website_config --- account/migrations/0007_auto_20170920_0254.py | 30 +++++++++++++++++++ account/models.py | 4 +-- account/serializers.py | 2 +- account/tests.py | 19 ++++++++++++ account/views/oj.py | 22 +++++++++++++- conf/tests.py | 2 +- conf/views.py | 15 ++++++++-- problem/views/oj.py | 6 ++-- utils/constants.py | 1 + 9 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 account/migrations/0007_auto_20170920_0254.py diff --git a/account/migrations/0007_auto_20170920_0254.py b/account/migrations/0007_auto_20170920_0254.py new file mode 100644 index 0000000..896d0d8 --- /dev/null +++ b/account/migrations/0007_auto_20170920_0254.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-20 02:54 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0006_user_session_keys'), + ] + + operations = [ + migrations.RenameField( + model_name='userprofile', + old_name='phone_number', + new_name='github', + ), + migrations.AlterField( + model_name='userprofile', + name='avatar', + field=models.CharField(default='/static/avatar/default.png', max_length=50), + ), + migrations.AlterField( + model_name='userprofile', + name='github', + field=models.CharField(blank=True, max_length=50, null=True), + ), + ] diff --git a/account/models.py b/account/models.py index 6bf4a19..b909f93 100644 --- a/account/models.py +++ b/account/models.py @@ -76,10 +76,10 @@ class UserProfile(models.Model): oi_problems_status = JSONField(default={}) real_name = models.CharField(max_length=30, blank=True, null=True) - avatar = models.CharField(max_length=50, default=_default_avatar) + avatar = models.CharField(max_length=50, default=_default_avatar()) blog = models.URLField(blank=True, null=True) mood = models.CharField(max_length=200, blank=True, null=True) - phone_number = models.CharField(max_length=15, blank=True, null=True) + github = models.CharField(max_length=50, blank=True, null=True) school = models.CharField(max_length=200, blank=True, null=True) major = models.CharField(max_length=200, blank=True, null=True) language = models.CharField(max_length=32, blank=True, null=True) diff --git a/account/serializers.py b/account/serializers.py index 2b71ceb..8345fc6 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -74,7 +74,7 @@ class EditUserProfileSerializer(serializers.Serializer): avatar = serializers.CharField(max_length=100, allow_blank=True, required=False) blog = serializers.URLField(allow_blank=True, required=False) mood = serializers.CharField(max_length=200, allow_blank=True, required=False) - phone_number = serializers.CharField(max_length=15, allow_blank=True, required=False, ) + github = serializers.CharField(max_length=50, allow_blank=True, required=False) school = serializers.CharField(max_length=200, allow_blank=True, required=False) major = serializers.CharField(max_length=200, allow_blank=True, required=False) diff --git a/account/tests.py b/account/tests.py index e648e76..edf25b8 100644 --- a/account/tests.py +++ b/account/tests.py @@ -8,8 +8,11 @@ from otpauth import OtpAuth from utils.api.tests import APIClient, APITestCase from utils.shortcuts import rand_str +from utils.cache import default_cache +from utils.constants import CacheKey from .models import AdminType, ProblemPermission, User +from conf.models import WebsiteConfig class PermissionDecoratorTest(APITestCase): @@ -128,6 +131,13 @@ class UserLoginAPITest(APITestCase): user = auth.get_user(self.client) self.assertFalse(user.is_authenticated()) + def test_user_disabled(self): + self.user.is_disabled = True + self.user.save() + resp = self.client.post(self.login_url, data={"username": self.username, + "password": self.password}) + self.assertDictEqual(resp.data, {"error": "error", "data": "Your account have been disabled"}) + class CaptchaTest(APITestCase): def _set_captcha(self, session): @@ -147,6 +157,15 @@ class UserRegisterAPITest(CaptchaTest): self.data = {"username": "test_user", "password": "testuserpassword", "real_name": "real_name", "email": "test@qduoj.com", "captcha": self._set_captcha(self.client.session)} + # clea cache in redis + default_cache.delete(CacheKey.website_config) + + def test_website_config_limit(self): + website = WebsiteConfig.objects.create() + website.allow_register = False + website.save() + resp = self.client.post(self.register_url, data=self.data) + self.assertDictEqual(resp.data, {"error": "error", "data": "Register have been disabled by admin"}) def test_invalid_captcha(self): self.data["captcha"] = "****" diff --git a/account/views/oj.py b/account/views/oj.py index 06d5c9e..5b74ca8 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -1,5 +1,6 @@ import os import qrcode +import pickle from datetime import timedelta from otpauth import OtpAuth @@ -15,6 +16,8 @@ from conf.models import WebsiteConfig from utils.api import APIView, validate_serializer from utils.captcha import Captcha from utils.shortcuts import rand_str, img2base64, timestamp2utcstr +from utils.cache import default_cache +from utils.constants import CacheKey from ..decorators import login_required from ..models import User, UserProfile @@ -62,6 +65,7 @@ class UserProfileAPI(APIView): class AvatarUploadAPI(APIView): request_parsers = () + @login_required def post(self, request): form = AvatarUploadForm(request.POST, request.FILES) if form.is_valid(): @@ -195,6 +199,8 @@ class UserLoginAPI(APIView): user = auth.authenticate(username=data["username"], password=data["password"]) # None is returned if username or password is wrong if user: + if user.is_disabled: + return self.error("Your account have been disabled") if not user.two_factor_auth: auth.login(request, user) return self.success("Succeeded") @@ -250,6 +256,18 @@ class UserRegisterAPI(APIView): """ User register api """ + config = default_cache.get(CacheKey.website_config) + if config: + config = pickle.loads(config) + else: + config = WebsiteConfig.objects.first() + if not config: + config = WebsiteConfig.objects.create() + default_cache.set(CacheKey.website_config, pickle.dumps(config)) + + if not config.allow_register: + return self.error("Register have been disabled by admin") + data = request.data captcha = Captcha(request) if not captcha.check(data["captcha"]): @@ -385,7 +403,9 @@ class UserRankAPI(APIView): rule_type = request.GET.get("rule") if rule_type not in ["acm", "oi"]: rule_type = "acm" - profiles = UserProfile.objects.select_related("user").filter(submission_number__gt=0) + profiles = UserProfile.objects.select_related("user")\ + .filter(submission_number__gt=0)\ + .exclude(user__is_disabled=True) if rule_type == "acm": profiles = profiles.order_by("-accepted_number", "submission_number") else: diff --git a/conf/tests.py b/conf/tests.py index fd05b43..ae9ee15 100644 --- a/conf/tests.py +++ b/conf/tests.py @@ -76,7 +76,7 @@ class WebsiteConfigAPITest(APITestCase): url = self.reverse("website_info_api") resp = self.client.get(url) self.assertSuccess(resp) - self.assertEqual(resp.data["data"]["name_shortcut"], "oj") + self.assertEqual(resp.data["data"]["name_shortcut"], "test oj") class JudgeServerHeartbeatTest(APITestCase): diff --git a/conf/views.py b/conf/views.py index c202421..814c062 100644 --- a/conf/views.py +++ b/conf/views.py @@ -1,4 +1,5 @@ import hashlib +import pickle from django.utils import timezone @@ -7,6 +8,8 @@ from judge.languages import languages, spj_languages from judge.dispatcher import process_pending_task from utils.api import APIView, CSRFExemptAPIView, validate_serializer from utils.shortcuts import rand_str +from utils.cache import default_cache +from utils.constants import CacheKey from .models import JudgeServer, JudgeServerToken, SMTPConfig, WebsiteConfig from .serializers import (CreateEditWebsiteConfigSerializer, @@ -57,9 +60,14 @@ class SMTPTestAPI(APIView): class WebsiteConfigAPI(APIView): def get(self, request): - config = WebsiteConfig.objects.first() - if not config: - config = WebsiteConfig.objects.create() + config = default_cache.get(CacheKey.website_config) + if config: + config = pickle.loads(config) + else: + config = WebsiteConfig.objects.first() + if not config: + config = WebsiteConfig.objects.create() + default_cache.set(CacheKey.website_config, pickle.dumps(config)) return self.success(WebsiteConfigSerializer(config).data) @validate_serializer(CreateEditWebsiteConfigSerializer) @@ -68,6 +76,7 @@ class WebsiteConfigAPI(APIView): data = request.data WebsiteConfig.objects.all().delete() config = WebsiteConfig.objects.create(**data) + default_cache.set(CacheKey.website_config, pickle.dumps(config)) return self.success(WebsiteConfigSerializer(config).data) diff --git a/problem/views/oj.py b/problem/views/oj.py index 40fb021..1a724f8 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -43,9 +43,9 @@ class ProblemAPI(APIView): problems = problems.filter(Q(title__contains=keyword) | Q(description__contains=keyword)) # 难度筛选 - difficulty_rank = request.GET.get("difficulty") - if difficulty_rank: - problems = problems.filter(difficulty=difficulty_rank) + difficulty = request.GET.get("difficulty") + if difficulty: + problems = problems.filter(difficulty=difficulty) # 根据profile 为做过的题目添加标记 data = self.paginate_data(request, problems, ProblemSerializer) if request.user.id: diff --git a/utils/constants.py b/utils/constants.py index 86181b6..b11aa21 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -1,3 +1,4 @@ class CacheKey: waiting_queue = "waiting_queue" contest_rank_cache = "contest_rank_cache_" + website_config = "website_config" From 51c229a2c5f74e5afa8551a288115b010c865f41 Mon Sep 17 00:00:00 2001 From: zema1 Date: Sun, 24 Sep 2017 09:48:17 +0800 Subject: [PATCH 051/106] merge problem and contest_problem --- contest/models.py | 4 +- judge/dispatcher.py | 67 ++++++++++++------- problem/migrations/0008_auto_20170923_1318.py | 66 ++++++++++++++++++ problem/models.py | 27 +++----- problem/serializers.py | 7 +- problem/views/admin.py | 29 ++++---- problem/views/oj.py | 12 ++-- .../migrations/0007_auto_20170923_1318.py | 36 ++++++++++ submission/models.py | 6 +- submission/serializers.py | 6 +- submission/test.py | 0 submission/tests.py | 57 ++++++++++++++++ submission/views/oj.py | 43 +++++------- 13 files changed, 263 insertions(+), 97 deletions(-) create mode 100644 problem/migrations/0008_auto_20170923_1318.py create mode 100644 submission/migrations/0007_auto_20170923_1318.py delete mode 100644 submission/test.py create mode 100644 submission/tests.py diff --git a/contest/models.py b/contest/models.py index bda6c6c..80ac92a 100644 --- a/contest/models.py +++ b/contest/models.py @@ -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: diff --git a/judge/dispatcher.py b/judge/dispatcher.py index edf2e2f..1488621 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -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() diff --git a/problem/migrations/0008_auto_20170923_1318.py b/problem/migrations/0008_auto_20170923_1318.py new file mode 100644 index 0000000..4f5bb99 --- /dev/null +++ b/problem/migrations/0008_auto_20170923_1318.py @@ -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', + ), + ] diff --git a/problem/models.py b/problem/models.py index c446317..0e9c5e2 100644 --- a/problem/models.py +++ b/problem/models.py @@ -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"),) diff --git a/problem/serializers.py b/problem/serializers.py index a43e999..d9e4919 100644 --- a/problem/serializers.py +++ b/problem/serializers.py @@ -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") diff --git a/problem/views/admin.py b/problem/views/admin.py index 51afde8..572e2fb 100644 --- a/problem/views/admin.py +++ b/problem/views/admin.py @@ -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") diff --git a/problem/views/oj.py b/problem/views/oj.py index 1a724f8..6eb8467 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -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, - visible=True) - except ContestProblem.DoesNotExist: + problem = Problem.objects.select_related("created_by").get(_id=problem_id, + contest=self.contest, + visible=True) + 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: diff --git a/submission/migrations/0007_auto_20170923_1318.py b/submission/migrations/0007_auto_20170923_1318.py new file mode 100644 index 0000000..d498e4c --- /dev/null +++ b/submission/migrations/0007_auto_20170923_1318.py @@ -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', + ), + ] diff --git a/submission/models.py b/submission/models.py index f16f3a4..f4cb2f2 100644 --- a/submission/models.py +++ b/submission/models.py @@ -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) diff --git a/submission/serializers.py b/submission/serializers.py index d2fe4ad..1c5493d 100644 --- a/submission/serializers.py +++ b/submission/serializers.py @@ -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 diff --git a/submission/test.py b/submission/test.py deleted file mode 100644 index e69de29..0000000 diff --git a/submission/tests.py b/submission/tests.py new file mode 100644 index 0000000..4c474ba --- /dev/null +++ b/submission/tests.py @@ -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": "

test

", "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": "

test

", "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() diff --git a/submission/views/oj.py b/submission/views/oj.py index f4d276d..11c76e9 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -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: From 2a91fd5e9fe315928d362fdb8b2b67d70a97ccef Mon Sep 17 00:00:00 2001 From: zema1 Date: Fri, 29 Sep 2017 21:58:20 +0800 Subject: [PATCH 052/106] fix bugs due to problem id --- account/tests.py | 2 +- account/views/oj.py | 2 +- conf/tests.py | 7 ++++++- judge/dispatcher.py | 21 +++++++++++++-------- problem/views/oj.py | 13 +++++++------ submission/serializers.py | 2 ++ submission/tests.py | 2 +- submission/views/oj.py | 30 +++++++++++++++++++++++------- 8 files changed, 54 insertions(+), 25 deletions(-) diff --git a/account/tests.py b/account/tests.py index edf25b8..7331a0e 100644 --- a/account/tests.py +++ b/account/tests.py @@ -226,7 +226,7 @@ class UserProfileAPITest(APITestCase): def test_get_profile_without_login(self): resp = self.client.get(self.url) - self.assertDictEqual(resp.data, {"error": None, "data": 0}) + self.assertDictEqual(resp.data, {"error": None, "data": {}}) def test_get_profile(self): self.create_user("test", "test123") diff --git a/account/views/oj.py b/account/views/oj.py index 5b74ca8..cfb9472 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -39,7 +39,7 @@ class UserProfileAPI(APIView): """ user = request.user if not user.is_authenticated(): - return self.success(0) + return self.success({}) username = request.GET.get("username") try: if username: diff --git a/conf/tests.py b/conf/tests.py index ae9ee15..1694c21 100644 --- a/conf/tests.py +++ b/conf/tests.py @@ -3,6 +3,8 @@ import hashlib from django.utils import timezone from utils.api.tests import APITestCase +from utils.cache import default_cache +from utils.constants import CacheKey from .models import JudgeServer, JudgeServerToken, SMTPConfig @@ -76,7 +78,10 @@ class WebsiteConfigAPITest(APITestCase): url = self.reverse("website_info_api") resp = self.client.get(url) self.assertSuccess(resp) - self.assertEqual(resp.data["data"]["name_shortcut"], "test oj") + self.assertEqual(resp.data["data"]["name_shortcut"], "oj") + + def tearDown(self): + default_cache.delete(CacheKey.website_config) class JudgeServerHeartbeatTest(APITestCase): diff --git a/judge/dispatcher.py b/judge/dispatcher.py index 1488621..8ab6688 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -4,17 +4,16 @@ import logging from urllib.parse import urljoin import requests -from django.core.cache import cache from django.db import transaction from django.db.models import F from account.models import User from conf.models import JudgeServer, JudgeServerToken -from contest.models import ContestRuleType, ACMContestRank, OIContestRank +from contest.models import ContestRuleType, ACMContestRank, OIContestRank, ContestStatus from judge.languages import languages from problem.models import Problem, ProblemRuleType from submission.models import JudgeStatus, Submission -from utils.cache import judge_cache +from utils.cache import judge_cache, default_cache from utils.constants import CacheKey logger = logging.getLogger(__name__) @@ -126,6 +125,7 @@ class JudgeDispatcher(object): if resp["err"]: self.submission.result = JudgeStatus.COMPILE_ERROR self.submission.statistic_info["err_info"] = resp["data"] + self.submission.statistic_info["score"] = 0 else: self._compute_statistic_info(resp["data"]) error_test_case = list(filter(lambda case: case["result"] != 0, resp["data"])) @@ -154,6 +154,9 @@ class JudgeDispatcher(object): return self._request(urljoin(service_url, "compile_spj"), data=data) def update_problem_status(self): + if self.contest_id and self.contest.status != ContestStatus.CONTEST_UNDERWAY: + logger.info("Contest debug mode, id: " + str(self.contest_id) + ", submission id: " + self.submission.id) + return with transaction.atomic(): # prepare problem and user_profile problem = Problem.objects.select_for_update().get(contest_id=self.contest_id, id=self.problem.id) @@ -168,15 +171,15 @@ class JudgeDispatcher(object): oi_problems_status = user_profile.oi_problems_status.get(key, {}) # update submission and accepted number counter + problem.submission_number += 1 + if self.submission.result == JudgeStatus.ACCEPTED: + problem.accepted_number += 1 # 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.contest_id: user_profile.submission_number += 1 if self.submission.result == JudgeStatus.ACCEPTED: user_profile.accepted_number += 1 - problem.submission_number += 1 - if self.submission.result == JudgeStatus.ACCEPTED: - problem.accepted_number += 1 problem_id = str(self.problem.id) if self.problem.rule_type == ProblemRuleType.ACM: @@ -217,8 +220,10 @@ class JudgeDispatcher(object): "submission_number", "accepted_number", "acm_problems_status", "oi_problems_status"]) def update_contest_rank(self): + if self.contest_id and self.contest.status != ContestStatus.CONTEST_UNDERWAY: + return if self.contest.real_time_rank: - cache.delete(str(self.contest.id) + "_rank_cache") + default_cache.delete(CacheKey.contest_rank_cache + str(self.contest_id)) with transaction.atomic(): if self.contest.rule_type == ContestRuleType.ACM: acm_rank, _ = ACMContestRank.objects.select_for_update(). \ @@ -232,7 +237,7 @@ class JudgeDispatcher(object): def _update_acm_contest_rank(self, rank): info = rank.submission_info.get(str(self.submission.problem_id)) # 因前面更改过,这里需要重新获取 - problem = Problem.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"]: diff --git a/problem/views/oj.py b/problem/views/oj.py index 6eb8467..f5b03f8 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -18,7 +18,8 @@ class ProblemAPI(APIView): problem_id = request.GET.get("problem_id") if problem_id: try: - problem = Problem.objects.select_related("created_by").get(_id=problem_id, visible=True) + problem = Problem.objects.select_related("created_by")\ + .get(_id=problem_id, contest_id__isnull=True, visible=True) return self.success(ProblemSerializer(problem).data) except Problem.DoesNotExist: return self.error("Problem does not exist") @@ -27,7 +28,7 @@ class ProblemAPI(APIView): if not limit: return self.error("Limit is needed") - problems = Problem.objects.select_related("created_by").filter(visible=True) + problems = Problem.objects.select_related("created_by").filter(contest_id__isnull=True, visible=True) # 按照标签筛选 tag_text = request.GET.get("tag") if tag_text: @@ -54,9 +55,9 @@ class ProblemAPI(APIView): oi_problems_status = profile.oi_problems_status.get("problems", {}) for problem in data["results"]: if problem["rule_type"] == ProblemRuleType.ACM: - problem["my_status"] = acm_problems_status.get(problem["_id"], None) + problem["my_status"] = acm_problems_status.get(str(problem["id"]), None) else: - problem["my_status"] = oi_problems_status.get(problem["_id"], None) + problem["my_status"] = oi_problems_status.get(str(problem["id"]), None) return self.success(data) @@ -83,5 +84,5 @@ class ContestProblemAPI(APIView): else: problems_status = profile.oi_problems_status.get("contest_problems", {}) for problem in data: - problem["my_status"] = problems_status.get(problem["_id"], None) - return self.success(ContestProblemSerializer(contest_problems, many=True).data) + problem["my_status"] = problems_status.get(str(problem["id"]), None) + return self.success(data) diff --git a/submission/serializers.py b/submission/serializers.py index 1c5493d..ae8c3a6 100644 --- a/submission/serializers.py +++ b/submission/serializers.py @@ -20,6 +20,7 @@ class SubmissionModelSerializer(serializers.ModelSerializer): # 不显示submission info的serializer, 用于ACM rule_type class SubmissionSafeSerializer(serializers.ModelSerializer): + problem = serializers.SlugRelatedField(read_only=True, slug_field="_id") statistic_info = serializers.JSONField() class Meta: @@ -28,6 +29,7 @@ class SubmissionSafeSerializer(serializers.ModelSerializer): class SubmissionListSerializer(serializers.ModelSerializer): + problem = serializers.SlugRelatedField(read_only=True, slug_field="_id") statistic_info = serializers.JSONField() show_link = serializers.SerializerMethodField() diff --git a/submission/tests.py b/submission/tests.py index 4c474ba..ca4ffa5 100644 --- a/submission/tests.py +++ b/submission/tests.py @@ -16,7 +16,7 @@ DEFAULT_PROBLEM_DATA = {"_id": "110", "title": "test", "description": "

testtest

", "source": "test"} DEFAULT_SUBMISSION_DATA = { - "problem_id": "110", + "problem_id": "1", "user_id": 1, "username": "test", "code": "xxxxxxxxxxxxxx", diff --git a/submission/views/oj.py b/submission/views/oj.py index 11c76e9..e2c59f2 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -1,3 +1,4 @@ +from account.models import AdminType from account.decorators import login_required, check_contest_permission from judge.tasks import judge_task # from judge.dispatcher import JudgeDispatcher @@ -25,7 +26,7 @@ def _submit(response, user, problem_id, language, code, contest_id): return response.error("Please wait %d seconds" % int(bucket.expected_time() + 1)) try: - problem = Problem.objects.get(_id=problem_id, + problem = Problem.objects.get(id=problem_id, contest_id=contest_id, visible=True) except Problem.DoesNotExist: @@ -53,15 +54,17 @@ class SubmissionAPI(APIView): contest = Contest.objects.get(id=data["contest_id"]) except Contest.DoesNotExist: return self.error("Contest doesn't exist.") - if contest.status != ContestStatus.CONTEST_UNDERWAY and request.user != contest.created_by: - return self.error("Contest have not started or have ended, you can't submit code.") + if contest.status == ContestStatus.CONTEST_ENDED: + return self.error("The contest have ended") + if contest.status == ContestStatus.CONTEST_NOT_START and request.user != contest.created_by: + return self.error("Contest have not started") return _submit(self, request.user, data["problem_id"], data["language"], data["code"], data.get("contest_id")) @login_required def get(self, request): submission_id = request.GET.get("id") if not submission_id: - return self.error("Parameter id doesn't exist.") + return self.error("Parameter id do esn't exist.") try: submission = Submission.objects.select_related("problem").get(id=submission_id) except Submission.DoesNotExist: @@ -86,8 +89,21 @@ class SubmissionListAPI(APIView): @check_contest_permission def _get_contest_submission_list(self, request): - subs = Submission.objects.filter(contest_id=self.contest.id) - return self.process_submissions(request, subs) + contest = self.contest + # todo OI mode + submissions = Submission.objects.filter(contest_id=contest.id) + # filter the test submissions submitted before contest start + if contest.status != ContestStatus.CONTEST_NOT_START: + print(contest.start_time) + submissions = submissions.filter(create_time__gte=contest.start_time) + + # 封榜的时候只能看到自己的提交 + if not contest.real_time_rank: + if request.user and not ( + request.user.admin_type == AdminType.SUPER_ADMIN or request.user == contest.created_by): + submissions = submissions.filter(user_id=request.user.id) + + return self.process_submissions(request, submissions) def process_submissions(self, request, submissions): problem_id = request.GET.get("problem_id") @@ -98,7 +114,7 @@ class SubmissionListAPI(APIView): problem = Problem.objects.get(_id=problem_id, visible=True) except Problem.DoesNotExist: return self.error("Problem doesn't exist") - submissions = problem.submission_set.all() + submissions = submissions.filter(problem=problem) if myself and myself == "1": submissions = submissions.filter(user_id=request.user.id) if result: From d650252a1a45aad1300de3f43d845567f946192f Mon Sep 17 00:00:00 2001 From: zema1 Date: Sat, 30 Sep 2017 10:26:54 +0800 Subject: [PATCH 053/106] separate contest submission and regular submission --- account/decorators.py | 9 +++--- contest/models.py | 5 ++- contest/tests.py | 18 ++--------- contest/views/admin.py | 2 +- problem/tests.py | 37 +++++++++++++--------- problem/views/oj.py | 2 +- submission/tests.py | 33 ++++++++++++-------- submission/urls/oj.py | 5 +-- submission/views/oj.py | 69 ++++++++++++++++++++++++++---------------- 9 files changed, 103 insertions(+), 77 deletions(-) diff --git a/account/decorators.py b/account/decorators.py index 0b14f89..f47c4a0 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -80,16 +80,17 @@ def check_contest_permission(func): except Contest.DoesNotExist: return self.error("Contest %s doesn't exist" % contest_id) - if self.contest.status == ContestStatus.CONTEST_NOT_START and user != self.contest.created_by: + # creator or owner + if self.contest.is_contest_admin(user): + return func(*args, **kwargs) + + if self.contest.status == ContestStatus.CONTEST_NOT_START: return self.error("Contest has not started yet.") if self.contest.contest_type == ContestType.PASSWORD_PROTECTED_CONTEST: # Anonymous if not user.is_authenticated(): return self.error("Please login in first.") - # creator - if user == self.contest.created_by: - return func(*args, **kwargs) # password error if ("contests" not in request.session) or (self.contest.id not in request.session["contests"]): return self.error("Password is required.") diff --git a/contest/models.py b/contest/models.py index 80ac92a..3383d17 100644 --- a/contest/models.py +++ b/contest/models.py @@ -2,7 +2,7 @@ from django.db import models from django.utils.timezone import now from jsonfield import JSONField -from account.models import User +from account.models import User, AdminType from utils.models import RichTextField @@ -56,6 +56,9 @@ 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) + class Meta: db_table = "contest" ordering = ("-create_time",) diff --git a/contest/tests.py b/contest/tests.py index 583035c..c481bec 100644 --- a/contest/tests.py +++ b/contest/tests.py @@ -74,17 +74,17 @@ class ContestAPITest(APITestCase): response = self.client.get("{}?id={}".format(self.url, contest_id)) self.assertSuccess(response) - def test_contest_password(self): + def test_regular_user_validate_contest_password(self): contest_id = self.create_contest().data["data"]["id"] self.create_user("test", "test123") url = self.reverse("contest_password_api") resp = self.client.post(url, {"contest_id": contest_id, "password": "error_password"}) - self.assertFailed(resp) + self.assertDictEqual(resp.data, {"error": "error", "data": "Password doesn't match."}) resp = self.client.post(url, {"contest_id": contest_id, "password": DEFAULT_CONTEST_DATA["password"]}) self.assertSuccess(resp) - def test_contest_access(self): + def test_regular_user_access_contest(self): contest_id = self.create_contest().data["data"]["id"] self.create_user("test", "test123") url = self.reverse("contest_access_api") @@ -97,18 +97,6 @@ class ContestAPITest(APITestCase): resp = self.client.get(url + "?contest_id=" + str(contest_id)) self.assertSuccess(resp) - # def test_get_not_started_contest(self): - # contest_id = self.create_contest().data["data"]["id"] - # resp = self.client.get(self.url + "?id=" + str(contest_id)) - # self.assertSuccess(resp) - # - # self.create_user("test", "1234") - # url = self.reverse("contest_password_api") - # resp = self.client.post(url, {"contest_id": contest_id, "password": DEFAULT_CONTEST_DATA["password"]}) - # self.assertSuccess(resp) - # resp = self.client.get(self.url + "?id=" + str(contest_id)) - # self.assertFailed(resp) - class ContestAnnouncementAPITest(APITestCase): def setUp(self): diff --git a/contest/views/admin.py b/contest/views/admin.py index 5f9a62c..37244fa 100644 --- a/contest/views/admin.py +++ b/contest/views/admin.py @@ -18,7 +18,7 @@ class ContestAPI(APIView): data["created_by"] = request.user if data["end_time"] <= data["start_time"]: return self.error("Start time must occur earlier than end time") - if not data["password"]: + if data.get("password") and data["password"] == "": data["password"] = None contest = Contest.objects.create(**data) return self.success(ContestAdminSerializer(contest).data) diff --git a/problem/tests.py b/problem/tests.py index 7979127..4e081ff 100644 --- a/problem/tests.py +++ b/problem/tests.py @@ -1,6 +1,7 @@ import copy import os import shutil +from datetime import timedelta from zipfile import ZipFile from django.conf import settings @@ -9,6 +10,7 @@ from utils.api.tests import APITestCase from .models import ProblemTag from .views.admin import TestCaseUploadAPI +from contest.models import Contest from contest.tests import DEFAULT_CONTEST_DATA DEFAULT_PROBLEM_DATA = {"_id": "A-110", "title": "test", "description": "

test

", "input_description": "test", @@ -193,32 +195,37 @@ class ContestProblemTest(APITestCase): self.create_admin() url = self.reverse("contest_admin_api") - self.contest = self.client.post(url, data=DEFAULT_CONTEST_DATA) - self.data = DEFAULT_PROBLEM_DATA - self.data["contest"] = self.contest.data["data"]["id"] + contest_data = copy.deepcopy(DEFAULT_CONTEST_DATA) + contest_data["password"] = "" + contest_data["start_time"] = contest_data["start_time"] + timedelta(hours=1) + self.contest = self.client.post(url, data=contest_data).data["data"] + + problem_data = copy.deepcopy(DEFAULT_PROBLEM_DATA) + problem_data["contest"] = self.contest["id"] url = self.reverse("contest_problem_admin_api") - self.problem = self.client.post(url, self.data) + self.problem = self.client.post(url, problem_data).data["data"] def test_get_contest_problem_list(self): - contest_id = self.contest.data["data"]["id"] + contest_id = self.contest["id"] resp = self.client.get(self.url + "?contest_id=" + str(contest_id)) self.assertSuccess(resp) self.assertEqual(len(resp.data["data"]), 1) def test_get_one_contest_problem(self): - contest_id = self.contest.data["data"]["id"] - problem_id = self.problem.data["data"]["_id"] + contest_id = self.contest["id"] + problem_id = self.problem["_id"] resp = self.client.get("{}?contest_id={}&problem_id={}".format(self.url, contest_id, problem_id)) self.assertSuccess(resp) - def test_regular_user_get_contest_problem(self): + def test_regular_user_get_not_started_contest_problem(self): self.create_user("test", "test123") - contest_id = self.contest.data["data"]["id"] - problem_id = self.problem.data["data"]["_id"] - resp = self.client.get("{}?contest_id={}&problem_id={}".format(self.url, contest_id, problem_id)) - self.assertFailed(resp) + resp = self.client.get(self.url + "?contest_id=" + str(self.contest["id"])) + self.assertDictEqual(resp.data, {"error": "error", "data": "Contest has not started yet."}) - url = self.reverse("contest_password_api") - self.client.post(url, {"contest_id": contest_id, "password": DEFAULT_CONTEST_DATA["password"]}) - resp = self.client.get("{}?contest_id={}&problem_id={}".format(self.url, contest_id, problem_id)) + def test_reguar_user_get_started_contest_problem(self): + self.create_user("test", "test123") + contest = Contest.objects.first() + contest.start_time = contest.start_time - timedelta(hours=1) + contest.save() + resp = self.client.get(self.url + "?contest_id=" + str(self.contest["id"])) self.assertSuccess(resp) diff --git a/problem/views/oj.py b/problem/views/oj.py index f5b03f8..0a6b066 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -77,7 +77,7 @@ class ContestProblemAPI(APIView): 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: + if request.user.is_authenticated() and self.contest.rule_type != ContestRuleType.OI: profile = request.user.userprofile if self.contest.rule_type == ContestRuleType.ACM: problems_status = profile.acm_problems_status.get("contest_problems", {}) diff --git a/submission/tests.py b/submission/tests.py index ca4ffa5..c734882 100644 --- a/submission/tests.py +++ b/submission/tests.py @@ -5,7 +5,7 @@ from .models import Submission from problem.models import Problem, ProblemTag from utils.api.tests import APITestCase -DEFAULT_PROBLEM_DATA = {"_id": "110", "title": "test", "description": "

test

", "input_description": "test", +DEFAULT_PROBLEM_DATA = {"_id": "A-110", "title": "test", "description": "

test

", "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", @@ -25,13 +25,28 @@ DEFAULT_SUBMISSION_DATA = { "language": "C", "statistic_info": {} } +# todo contest submission -class SubmissionListTest(APITestCase): +class SubmissionPrepare(APITestCase): + def _create_problem_and_submission(self): + user = self.create_admin("test", "test123", login=False) + problem_data = deepcopy(DEFAULT_PROBLEM_DATA) + problem_data.pop("tags") + problem_data["created_by"] = user + self.problem = Problem.objects.create(**problem_data) + for tag in DEFAULT_PROBLEM_DATA["tags"]: + tag = ProblemTag.objects.create(name=tag) + self.problem.tags.add(tag) + self.problem.save() + self.submission = Submission.objects.create(**DEFAULT_SUBMISSION_DATA) + + +class SubmissionListTest(SubmissionPrepare): def setUp(self): + self._create_problem_and_submission() 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"}) @@ -39,16 +54,10 @@ class SubmissionListTest(APITestCase): @mock.patch("submission.views.oj.judge_task.delay") -class SubmissionAPITest(APITestCase): +class SubmissionAPITest(SubmissionPrepare): 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._create_problem_and_submission() + self.user = self.create_user("123", "test123") self.url = self.reverse("submission_api") def test_create_submission(self, judge_task): diff --git a/submission/urls/oj.py b/submission/urls/oj.py index 30cc62f..f2a7703 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"^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 e2c59f2..274418a 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -1,9 +1,8 @@ -from account.models import AdminType 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 -from contest.models import Contest, ContestStatus +from contest.models import Contest, ContestStatus, ContestRuleType from utils.api import APIView, validate_serializer from utils.throttling import TokenBucket, BucketController from ..models import Submission @@ -64,7 +63,7 @@ class SubmissionAPI(APIView): def get(self, request): submission_id = request.GET.get("id") if not submission_id: - return self.error("Parameter id do esn't exist.") + return self.error("Parameter id doesn't exist.") try: submission = Submission.objects.select_related("problem").get(id=submission_id) except Submission.DoesNotExist: @@ -82,36 +81,15 @@ class SubmissionListAPI(APIView): 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) + return self.error("Parameter error") submissions = Submission.objects.filter(contest_id__isnull=True) - return self.process_submissions(request, submissions) - - @check_contest_permission - def _get_contest_submission_list(self, request): - contest = self.contest - # todo OI mode - submissions = Submission.objects.filter(contest_id=contest.id) - # filter the test submissions submitted before contest start - if contest.status != ContestStatus.CONTEST_NOT_START: - print(contest.start_time) - submissions = submissions.filter(create_time__gte=contest.start_time) - - # 封榜的时候只能看到自己的提交 - if not contest.real_time_rank: - if request.user and not ( - request.user.admin_type == AdminType.SUPER_ADMIN or request.user == contest.created_by): - submissions = submissions.filter(user_id=request.user.id) - - return self.process_submissions(request, submissions) - - def process_submissions(self, request, submissions): problem_id = request.GET.get("problem_id") myself = request.GET.get("myself") result = request.GET.get("result") if problem_id: try: - problem = Problem.objects.get(_id=problem_id, visible=True) + problem = Problem.objects.get(_id=problem_id, contest_id__isnull=True, visible=True) except Problem.DoesNotExist: return self.error("Problem doesn't exist") submissions = submissions.filter(problem=problem) @@ -122,3 +100,42 @@ class SubmissionListAPI(APIView): data = self.paginate_data(request, submissions) 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): + if not request.GET.get("limit"): + return self.error("Limit is needed") + + contest = self.contest + if contest.rule_type == ContestRuleType.OI and not contest.is_contest_admin(request.user): + return self.error("No permission for OI contest submissions") + + submissions = Submission.objects.filter(contest_id=contest.id) + problem_id = request.GET.get("problem_id") + myself = request.GET.get("myself") + result = request.GET.get("result") + if problem_id: + try: + problem = Problem.objects.get(_id=problem_id, contest_id=contest.id, visible=True) + except Problem.DoesNotExist: + return self.error("Problem doesn't exist") + submissions = submissions.filter(problem=problem) + + if myself and myself == "1": + submissions = submissions.filter(user_id=request.user.id) + if result: + submissions = submissions.filter(result=result) + + # filter the test submissions submitted before contest start + if contest.status != ContestStatus.CONTEST_NOT_START: + submissions = submissions.filter(create_time__gte=contest.start_time) + + # 封榜的时候只能看到自己的提交 + if not contest.real_time_rank and not contest.is_contest_admin(request.user): + submissions = submissions.filter(user_id=request.user.id) + + data = self.paginate_data(request, submissions) + data["results"] = SubmissionListSerializer(data["results"], many=True, user=request.user).data + return self.success(data) From 9990cf647a2d03521eb4c09b0c0bbba8f7b599f7 Mon Sep 17 00:00:00 2001 From: virusdefender Date: Mon, 2 Oct 2017 03:54:34 +0800 Subject: [PATCH 054/106] =?UTF-8?q?=E4=BD=BF=E7=94=A8=20SysOptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/tasks.py | 29 ++++- account/views/oj.py | 22 ++-- conf/models.py | 32 ------ conf/serializers.py | 23 +--- conf/tests.py | 26 ++--- conf/views.py | 78 ++++--------- judge/dispatcher.py | 8 +- oj/settings.py | 1 + options/__init__.py | 0 options/migrations/0001_initial.py | 25 ++++ options/migrations/__init__.py | 0 options/models.py | 7 ++ options/options.py | 179 +++++++++++++++++++++++++++++ options/tests.py | 1 + options/views.py | 1 + utils/api/__init__.py | 2 +- utils/api/tests.py | 4 - utils/constants.py | 1 + utils/shortcuts.py | 30 +---- 19 files changed, 297 insertions(+), 172 deletions(-) create mode 100644 options/__init__.py create mode 100644 options/migrations/0001_initial.py create mode 100644 options/migrations/__init__.py create mode 100644 options/models.py create mode 100644 options/options.py create mode 100644 options/tests.py create mode 100644 options/views.py diff --git a/account/tasks.py b/account/tasks.py index 0aacec9..3e7c1d2 100644 --- a/account/tasks.py +++ b/account/tasks.py @@ -1,6 +1,31 @@ -from celery import shared_task +import logging -from utils.shortcuts import send_email +from celery import shared_task +from envelopes import Envelope + +from options.options import SysOptions + +logger = logging.getLogger(__name__) + + +def send_email(from_name, to_email, to_name, subject, content): + smtp = SysOptions.smtp_config + if not smtp: + return + envlope = Envelope(from_addr=(smtp["email"], from_name), + to_addr=(to_email, to_name), + subject=subject, + html_body=content) + try: + envlope.send(smtp["server"], + login=smtp["email"], + password=smtp["password"], + port=smtp["port"], + tls=smtp["tls"]) + return True + except Exception as e: + logger.exception(e) + return False @shared_task diff --git a/account/views/oj.py b/account/views/oj.py index cfb9472..140c0e9 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -1,24 +1,23 @@ import os -import qrcode import pickle from datetime import timedelta -from otpauth import OtpAuth +from importlib import import_module +import qrcode from django.conf import settings from django.contrib import auth -from importlib import import_module +from django.template.loader import render_to_string +from django.utils.decorators import method_decorator from django.utils.timezone import now from django.views.decorators.csrf import ensure_csrf_cookie -from django.utils.decorators import method_decorator -from django.template.loader import render_to_string +from otpauth import OtpAuth -from conf.models import WebsiteConfig +from options.options import SysOptions from utils.api import APIView, validate_serializer -from utils.captcha import Captcha -from utils.shortcuts import rand_str, img2base64, timestamp2utcstr from utils.cache import default_cache +from utils.captcha import Captcha from utils.constants import CacheKey - +from utils.shortcuts import rand_str, img2base64, timestamp2utcstr from ..decorators import login_required from ..models import User, UserProfile from ..serializers import (ApplyResetPasswordSerializer, ResetPasswordSerializer, @@ -137,9 +136,8 @@ class TwoFactorAuthAPI(APIView): user.tfa_token = token user.save() - config = WebsiteConfig.objects.first() - label = f"{config.name_shortcut}:{user.username}" - image = qrcode.make(OtpAuth(token).to_uri("totp", label, config.name)) + label = f"{SysOptions.website_name_shortcut}:{user.username}" + image = qrcode.make(OtpAuth(token).to_uri("totp", label, SysOptions.website_name)) return self.success(img2base64(image)) @login_required diff --git a/conf/models.py b/conf/models.py index 9fe3cc5..86248db 100644 --- a/conf/models.py +++ b/conf/models.py @@ -2,31 +2,6 @@ from django.db import models from django.utils import timezone -class SMTPConfig(models.Model): - server = models.CharField(max_length=128) - port = models.IntegerField(default=25) - email = models.CharField(max_length=128) - password = models.CharField(max_length=128) - tls = models.BooleanField() - - class Meta: - db_table = "smtp_config" - - -class WebsiteConfig(models.Model): - base_url = models.CharField(max_length=128, default="http://127.0.0.1") - name = models.CharField(max_length=32, default="Online Judge") - name_shortcut = models.CharField(max_length=32, default="oj") - footer = models.TextField(default="Online Judge Footer") - # allow register - allow_register = models.BooleanField(default=True) - # submission list show all user's submission - submission_list_show_all = models.BooleanField(default=True) - - class Meta: - db_table = "website_config" - - class JudgeServer(models.Model): hostname = models.CharField(max_length=64) ip = models.CharField(max_length=32, blank=True, null=True) @@ -48,10 +23,3 @@ class JudgeServer(models.Model): class Meta: db_table = "judge_server" - - -class JudgeServerToken(models.Model): - token = models.CharField(max_length=32) - - class Meta: - db_table = "judge_server_token" diff --git a/conf/serializers.py b/conf/serializers.py index 59b7203..09f9940 100644 --- a/conf/serializers.py +++ b/conf/serializers.py @@ -1,6 +1,6 @@ from utils.api import DateTimeTZField, serializers -from .models import JudgeServer, SMTPConfig, WebsiteConfig +from .models import JudgeServer class EditSMTPConfigSerializer(serializers.Serializer): @@ -15,31 +15,19 @@ class CreateSMTPConfigSerializer(EditSMTPConfigSerializer): password = serializers.CharField(max_length=128) -class SMTPConfigSerializer(serializers.ModelSerializer): - class Meta: - model = SMTPConfig - exclude = ["id", "password"] - - class TestSMTPConfigSerializer(serializers.Serializer): email = serializers.EmailField() class CreateEditWebsiteConfigSerializer(serializers.Serializer): - base_url = serializers.CharField(max_length=128) - name = serializers.CharField(max_length=32) - name_shortcut = serializers.CharField(max_length=32) - footer = serializers.CharField(max_length=1024) + website_base_url = serializers.CharField(max_length=128) + website_name = serializers.CharField(max_length=32) + website_name_shortcut = serializers.CharField(max_length=32) + website_footer = serializers.CharField(max_length=1024) allow_register = serializers.BooleanField() submission_list_show_all = serializers.BooleanField() -class WebsiteConfigSerializer(serializers.ModelSerializer): - class Meta: - model = WebsiteConfig - exclude = ["id"] - - class JudgeServerSerializer(serializers.ModelSerializer): create_time = DateTimeTZField() last_heartbeat = DateTimeTZField() @@ -47,6 +35,7 @@ class JudgeServerSerializer(serializers.ModelSerializer): class Meta: model = JudgeServer + fields = "__all__" class JudgeServerHeartbeatSerializer(serializers.Serializer): diff --git a/conf/tests.py b/conf/tests.py index 1694c21..eff8cfd 100644 --- a/conf/tests.py +++ b/conf/tests.py @@ -2,11 +2,11 @@ import hashlib from django.utils import timezone +from options.options import SysOptions from utils.api.tests import APITestCase from utils.cache import default_cache from utils.constants import CacheKey - -from .models import JudgeServer, JudgeServerToken, SMTPConfig +from .models import JudgeServer class SMTPConfigTest(APITestCase): @@ -29,10 +29,6 @@ class SMTPConfigTest(APITestCase): "tls": True} resp = self.client.put(self.url, data=data) self.assertSuccess(resp) - smtp = SMTPConfig.objects.first() - self.assertEqual(smtp.password, self.password) - self.assertEqual(smtp.server, "smtp1.test.com") - self.assertEqual(smtp.email, "test2@test.com") def test_edit_without_password1(self): self.test_create_smtp_config() @@ -40,7 +36,6 @@ class SMTPConfigTest(APITestCase): "tls": True, "password": ""} resp = self.client.put(self.url, data=data) self.assertSuccess(resp) - self.assertEqual(SMTPConfig.objects.first().password, self.password) def test_edit_with_password(self): self.test_create_smtp_config() @@ -48,18 +43,14 @@ class SMTPConfigTest(APITestCase): "tls": True, "password": "newpassword"} resp = self.client.put(self.url, data=data) self.assertSuccess(resp) - smtp = SMTPConfig.objects.first() - self.assertEqual(smtp.password, "newpassword") - self.assertEqual(smtp.server, "smtp1.test.com") - self.assertEqual(smtp.email, "test2@test.com") class WebsiteConfigAPITest(APITestCase): def test_create_website_config(self): self.create_super_admin() url = self.reverse("website_config_api") - data = {"base_url": "http://test.com", "name": "test name", - "name_shortcut": "test oj", "footer": "test", + data = {"website_base_url": "http://test.com", "website_name": "test name", + "website_name_shortcut": "test oj", "website_footer": "test", "allow_register": True, "submission_list_show_all": False} resp = self.client.post(url, data=data) self.assertSuccess(resp) @@ -67,8 +58,8 @@ class WebsiteConfigAPITest(APITestCase): def test_edit_website_config(self): self.create_super_admin() url = self.reverse("website_config_api") - data = {"base_url": "http://test.com", "name": "test name", - "name_shortcut": "test oj", "footer": "test", + data = {"website_base_url": "http://test.com", "website_name": "test name", + "website_name_shortcut": "test oj", "website_footer": "test", "allow_register": True, "submission_list_show_all": False} resp = self.client.post(url, data=data) self.assertSuccess(resp) @@ -78,7 +69,6 @@ class WebsiteConfigAPITest(APITestCase): url = self.reverse("website_info_api") resp = self.client.get(url) self.assertSuccess(resp) - self.assertEqual(resp.data["data"]["name_shortcut"], "oj") def tearDown(self): default_cache.delete(CacheKey.website_config) @@ -91,7 +81,7 @@ class JudgeServerHeartbeatTest(APITestCase): "cpu": 90.5, "memory": 80.3, "action": "heartbeat"} self.token = "test" self.hashed_token = hashlib.sha256(self.token.encode("utf-8")).hexdigest() - JudgeServerToken.objects.create(token=self.token) + SysOptions.judge_server_token = self.token def test_new_heartbeat(self): resp = self.client.post(self.url, data=self.data, **{"HTTP_X_JUDGE_SERVER_TOKEN": self.hashed_token}) @@ -127,11 +117,9 @@ class JudgeServerAPITest(APITestCase): self.create_super_admin() def test_get_judge_server(self): - self.assertFalse(JudgeServerToken.objects.exists()) resp = self.client.get(self.url) self.assertSuccess(resp) self.assertEqual(len(resp.data["data"]["servers"]), 1) - self.assertEqual(JudgeServerToken.objects.first().token, resp.data["data"]["token"]) def test_delete_judge_server(self): resp = self.client.delete(self.url + "?hostname=testhostname") diff --git a/conf/views.py b/conf/views.py index 814c062..f09972c 100644 --- a/conf/views.py +++ b/conf/views.py @@ -1,54 +1,45 @@ import hashlib -import pickle from django.utils import timezone from account.decorators import super_admin_required -from judge.languages import languages, spj_languages from judge.dispatcher import process_pending_task +from judge.languages import languages, spj_languages +from options.options import SysOptions from utils.api import APIView, CSRFExemptAPIView, validate_serializer -from utils.shortcuts import rand_str -from utils.cache import default_cache -from utils.constants import CacheKey - -from .models import JudgeServer, JudgeServerToken, SMTPConfig, WebsiteConfig +from .models import JudgeServer from .serializers import (CreateEditWebsiteConfigSerializer, CreateSMTPConfigSerializer, EditSMTPConfigSerializer, JudgeServerHeartbeatSerializer, - JudgeServerSerializer, SMTPConfigSerializer, - TestSMTPConfigSerializer, WebsiteConfigSerializer) + JudgeServerSerializer, TestSMTPConfigSerializer) class SMTPAPI(APIView): @super_admin_required def get(self, request): - smtp = SMTPConfig.objects.first() + smtp = SysOptions.smtp_config if not smtp: return self.success(None) - return self.success(SMTPConfigSerializer(smtp).data) + smtp.pop("password") + return self.success(smtp) @validate_serializer(CreateSMTPConfigSerializer) @super_admin_required def post(self, request): - SMTPConfig.objects.all().delete() - smtp = SMTPConfig.objects.create(**request.data) - return self.success(SMTPConfigSerializer(smtp).data) + SysOptions.smtp_config = request.data + return self.success() @validate_serializer(EditSMTPConfigSerializer) @super_admin_required def put(self, request): + smtp = SysOptions.smtp_config data = request.data - smtp = SMTPConfig.objects.first() - if not smtp: - return self.error("SMTP config is missing") - smtp.server = data["server"] - smtp.port = data["port"] - smtp.email = data["email"] - smtp.tls = data["tls"] - if data.get("password"): - smtp.password = data["password"] - smtp.save() - return self.success(SMTPConfigSerializer(smtp).data) + for item in ["server", "port", "email", "tls"]: + smtp[item] = data[item] + if "password" in data: + smtp["password"] = data["password"] + SysOptions.smtp_config = smtp + return self.success() class SMTPTestAPI(APIView): @@ -60,37 +51,24 @@ class SMTPTestAPI(APIView): class WebsiteConfigAPI(APIView): def get(self, request): - config = default_cache.get(CacheKey.website_config) - if config: - config = pickle.loads(config) - else: - config = WebsiteConfig.objects.first() - if not config: - config = WebsiteConfig.objects.create() - default_cache.set(CacheKey.website_config, pickle.dumps(config)) - return self.success(WebsiteConfigSerializer(config).data) + ret = {key: getattr(SysOptions, key) for key in + ["website_base_url", "website_name", "website_name_shortcut", + "website_footer", "allow_register", "submission_list_show_all"]} + return self.success(ret) @validate_serializer(CreateEditWebsiteConfigSerializer) @super_admin_required def post(self, request): - data = request.data - WebsiteConfig.objects.all().delete() - config = WebsiteConfig.objects.create(**data) - default_cache.set(CacheKey.website_config, pickle.dumps(config)) - return self.success(WebsiteConfigSerializer(config).data) + for k, v in request.data.items(): + setattr(SysOptions, k, v) + return self.success() class JudgeServerAPI(APIView): @super_admin_required def get(self, request): - judge_server_token = JudgeServerToken.objects.first() - if not judge_server_token: - token = rand_str(12) - JudgeServerToken.objects.create(token=token) - else: - token = judge_server_token.token servers = JudgeServer.objects.all().order_by("-last_heartbeat") - return self.success({"token": token, + return self.success({"token": SysOptions.judge_server_token, "servers": JudgeServerSerializer(servers, many=True).data}) @super_admin_required @@ -104,15 +82,9 @@ class JudgeServerAPI(APIView): class JudgeServerHeartbeatAPI(CSRFExemptAPIView): @validate_serializer(JudgeServerHeartbeatSerializer) def post(self, request): - judge_server_token = JudgeServerToken.objects.first() - if not judge_server_token: - token = rand_str(12) - JudgeServerToken.objects.create(token=token) - else: - token = judge_server_token.token data = request.data client_token = request.META.get("HTTP_X_JUDGE_SERVER_TOKEN") - if hashlib.sha256(token.encode("utf-8")).hexdigest() != client_token: + if hashlib.sha256(SysOptions.judge_server_token.encode("utf-8")).hexdigest() != client_token: return self.error("Invalid token") service_url = data.get("service_url") diff --git a/judge/dispatcher.py b/judge/dispatcher.py index 8ab6688..597ed7d 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -8,9 +8,10 @@ from django.db import transaction from django.db.models import F from account.models import User -from conf.models import JudgeServer, JudgeServerToken +from conf.models import JudgeServer from contest.models import ContestRuleType, ACMContestRank, OIContestRank, ContestStatus from judge.languages import languages +from options.options import SysOptions from problem.models import Problem, ProblemRuleType from submission.models import JudgeStatus, Submission from utils.cache import judge_cache, default_cache @@ -30,8 +31,7 @@ def process_pending_task(): class JudgeDispatcher(object): def __init__(self, submission_id, problem_id): - token = JudgeServerToken.objects.first().token - self.token = hashlib.sha256(token.encode("utf-8")).hexdigest() + self.token = hashlib.sha256(SysOptions.judge_server_token.encode("utf-8")).hexdigest() self.redis_conn = judge_cache self.submission = Submission.objects.get(pk=submission_id) self.contest_id = self.submission.contest_id @@ -50,7 +50,7 @@ class JudgeDispatcher(object): try: return requests.post(url, **kwargs).json() except Exception as e: - logger.error(e.with_traceback()) + logger.exception(e) @staticmethod def choose_judge_server(): diff --git a/oj/settings.py b/oj/settings.py index eef91a6..4633cf0 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -46,6 +46,7 @@ INSTALLED_APPS = ( 'contest', 'utils', 'submission', + 'options', ) MIDDLEWARE_CLASSES = ( diff --git a/options/__init__.py b/options/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/options/migrations/0001_initial.py b/options/migrations/0001_initial.py new file mode 100644 index 0000000..db40e1e --- /dev/null +++ b/options/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.3 on 2017-10-01 19:19 +from __future__ import unicode_literals + +import jsonfield.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='SysOptions', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(db_index=True, max_length=128, unique=True)), + ('value', jsonfield.fields.JSONField()), + ], + ), + ] diff --git a/options/migrations/__init__.py b/options/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/options/models.py b/options/models.py new file mode 100644 index 0000000..6d9ac75 --- /dev/null +++ b/options/models.py @@ -0,0 +1,7 @@ +from django.db import models +from jsonfield import JSONField + + +class SysOptions(models.Model): + key = models.CharField(max_length=128, unique=True, db_index=True) + value = JSONField() diff --git a/options/options.py b/options/options.py new file mode 100644 index 0000000..51beb53 --- /dev/null +++ b/options/options.py @@ -0,0 +1,179 @@ +from django.core.cache import cache +from django.db import transaction, IntegrityError + +from utils.constants import CacheKey +from utils.shortcuts import rand_str +from .models import SysOptions as SysOptionsModel + + +class OptionKeys: + website_base_url = "website_base_url" + website_name = "website_name" + website_name_shortcut = "website_name_shortcut" + website_footer = "website_footer" + allow_register = "allow_register" + submission_list_show_all = "submission_list_show_all" + smtp_config = "smtp_config" + judge_server_token = "judge_server_token" + + +class OptionDefaultValue: + website_base_url = "http://127.0.0.1" + website_name = "Online Judge" + website_name_shortcut = "oj" + website_footer = "Online Judge Footer" + allow_register = True + submission_list_show_all = True + smtp_config = {} + judge_server_token = rand_str + + +class _SysOptionsMeta(type): + @classmethod + def _set_cache(mcs, option_key, option_value): + cache.set(f"{CacheKey.option}:{option_key}", option_value, timeout=60) + + @classmethod + def _del_cache(mcs, option_key): + cache.delete(f"{CacheKey.option}:{option_key}") + + @classmethod + def _get_keys(cls): + return [key for key in OptionKeys.__dict__ if not key.startswith("__")] + + def rebuild_cache(cls): + for key in cls._get_keys(): + # get option 的时候会写 cache 的 + cls._get_option(key, use_cache=False) + + @classmethod + def _init_option(mcs): + for item in mcs._get_keys(): + if not SysOptionsModel.objects.filter(key=item).exists(): + default_value = getattr(OptionDefaultValue, item) + if callable(default_value): + default_value = default_value() + try: + SysOptionsModel.objects.create(key=item, value=default_value) + except IntegrityError: + pass + + @classmethod + def _get_option(mcs, option_key, use_cache=True): + try: + if use_cache: + option = cache.get(f"{CacheKey.option}:{option_key}") + if option: + return option + option = SysOptionsModel.objects.get(key=option_key) + value = option.value + mcs._set_cache(option_key, value) + return value + except SysOptionsModel.DoesNotExist: + mcs._init_option() + return mcs._get_option(option_key, use_cache=use_cache) + + @classmethod + def _set_option(mcs, option_key: str, option_value): + try: + with transaction.atomic(): + option = SysOptionsModel.objects.select_for_update().get(key=option_key) + option.value = option_value + option.save() + mcs._del_cache(option_key) + except SysOptionsModel.DoesNotExist: + mcs._init_option() + mcs._set_option(option_key, option_value) + + @classmethod + def _increment(mcs, option_key): + try: + with transaction.atomic(): + option = SysOptionsModel.objects.select_for_update().get(key=option_key) + value = option.value + 1 + option.value = value + option.save() + mcs._del_cache(option_key) + except SysOptionsModel.DoesNotExist: + mcs._init_option() + return mcs._increment(option_key) + + @classmethod + def set_options(mcs, options): + for key, value in options: + mcs._set_option(key, value) + + @classmethod + def get_options(mcs, keys): + result = {} + for key in keys: + result[key] = mcs._get_option(key) + return result + + @property + def website_base_url(cls): + return cls._get_option(OptionKeys.website_base_url) + + @website_base_url.setter + def website_base_url(cls, value): + cls._set_option(OptionKeys.website_base_url, value) + + @property + def website_name(cls): + return cls._get_option(OptionKeys.website_name) + + @website_name.setter + def website_name(cls, value): + cls._set_option(OptionKeys.website_name, value) + + @property + def website_name_shortcut(cls): + return cls._get_option(OptionKeys.website_name_shortcut) + + @website_name_shortcut.setter + def website_name_shortcut(cls, value): + cls._set_option(OptionKeys.website_name_shortcut, value) + + @property + def website_footer(cls): + return cls._get_option(OptionKeys.website_footer) + + @website_footer.setter + def website_footer(cls, value): + cls._set_option(OptionKeys.website_footer, value) + + @property + def allow_register(cls): + return cls._get_option(OptionKeys.allow_register) + + @allow_register.setter + def allow_register(cls, value): + cls._set_option(OptionKeys.allow_register, value) + + @property + def submission_list_show_all(cls): + return cls._get_option(OptionKeys.submission_list_show_all) + + @submission_list_show_all.setter + def submission_list_show_all(cls, value): + cls._set_option(OptionKeys.submission_list_show_all, value) + + @property + def smtp_config(cls): + return cls._get_option(OptionKeys.smtp_config) + + @smtp_config.setter + def smtp_config(cls, value): + cls._set_option(OptionKeys.smtp_config, value) + + @property + def judge_server_token(cls): + return cls._get_option(OptionKeys.judge_server_token) + + @judge_server_token.setter + def judge_server_token(cls, value): + cls._set_option(OptionKeys.judge_server_token, value) + + +class SysOptions(metaclass=_SysOptionsMeta): + pass diff --git a/options/tests.py b/options/tests.py new file mode 100644 index 0000000..a39b155 --- /dev/null +++ b/options/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/options/views.py b/options/views.py new file mode 100644 index 0000000..60f00ef --- /dev/null +++ b/options/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/utils/api/__init__.py b/utils/api/__init__.py index dedbe3a..9384481 100644 --- a/utils/api/__init__.py +++ b/utils/api/__init__.py @@ -1,2 +1,2 @@ -from .api import * # NOQA from ._serializers import * # NOQA +from .api import * # NOQA diff --git a/utils/api/tests.py b/utils/api/tests.py index 3d9cc30..4b485c9 100644 --- a/utils/api/tests.py +++ b/utils/api/tests.py @@ -3,7 +3,6 @@ from django.test.testcases import TestCase from rest_framework.test import APIClient from account.models import AdminType, ProblemPermission, User, UserProfile -from conf.models import WebsiteConfig class APITestCase(TestCase): @@ -28,9 +27,6 @@ class APITestCase(TestCase): return self.create_user(username=username, password=password, admin_type=AdminType.SUPER_ADMIN, problem_permission=ProblemPermission.ALL, login=login) - def create_website_config(self): - return WebsiteConfig.objects.create() - def reverse(self, url_name): return reverse(url_name) diff --git a/utils/constants.py b/utils/constants.py index b11aa21..14f13af 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -2,3 +2,4 @@ class CacheKey: waiting_queue = "waiting_queue" contest_rank_cache = "contest_rank_cache_" website_config = "website_config" + option = "option" diff --git a/utils/shortcuts.py b/utils/shortcuts.py index 525eef0..8fc1ffa 100644 --- a/utils/shortcuts.py +++ b/utils/shortcuts.py @@ -1,35 +1,9 @@ -import logging -import random import datetime -from io import BytesIO +import random from base64 import b64encode +from io import BytesIO from django.utils.crypto import get_random_string -from envelopes import Envelope - -from conf.models import SMTPConfig - -logger = logging.getLogger(__name__) - - -def send_email(from_name, to_email, to_name, subject, content): - smtp = SMTPConfig.objects.first() - if not smtp: - return - envlope = Envelope(from_addr=(smtp.email, from_name), - to_addr=(to_email, to_name), - subject=subject, - html_body=content) - try: - envlope.send(smtp.server, - login=smtp.email, - password=smtp.password, - port=smtp.port, - tls=smtp.tls) - return True - except Exception as e: - logger.exception(e) - return False def rand_str(length=32, type="lower_hex"): From edb32eaf7bd589ed3d08aade16cf2fa839ff3e35 Mon Sep 17 00:00:00 2001 From: virusdefender Date: Mon, 2 Oct 2017 04:33:43 +0800 Subject: [PATCH 055/106] tiny work --- account/middleware.py | 21 ++------------------- account/views/oj.py | 1 - contest/models.py | 9 +++------ contest/views/oj.py | 37 +++++++++++++++---------------------- oj/settings.py | 1 - utils/api/api.py | 2 +- 6 files changed, 21 insertions(+), 50 deletions(-) diff --git a/account/middleware.py b/account/middleware.py index 48a6942..9141d53 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -10,22 +10,11 @@ from django.utils.deprecation import MiddlewareMixin from utils.api import JSONResponse -class SessionSecurityMiddleware(MiddlewareMixin): - def process_request(self, request): - if request.user.is_authenticated(): - if "last_activity" in request.session and request.user.is_admin_role(): - # 24 hours passed since last visit, 86400 = 24 * 60 * 60 - if time.time() - request.session["last_activity"] >= 86400: - auth.logout(request) - return JSONResponse.response({"error": "login-required", "data": _("Please login in first")}) - request.session["last_activity"] = time.time() - - class SessionRecordMiddleware(MiddlewareMixin): def process_request(self, request): if request.user.is_authenticated(): session = request.session - ip = request.META.get("REMOTE_ADDR", "") + ip = request.META.get("HTTP_X_REAL_IP", "UNKNOWN IP") user_agent = request.META.get("HTTP_USER_AGENT", "") _ip = session.setdefault("ip", ip) _user_agent = session.setdefault("user_agent", user_agent) @@ -42,13 +31,7 @@ class AdminRoleRequiredMiddleware(MiddlewareMixin): path = request.path_info if path.startswith("/admin/") or path.startswith("/api/admin/"): if not (request.user.is_authenticated() and request.user.is_admin_role()): - return JSONResponse.response({"error": "login-required", "data": _("Please login in first")}) - - -class TimezoneMiddleware(MiddlewareMixin): - def process_request(self, request): - if request.user.is_authenticated(): - timezone.activate(pytz.timezone(request.user.userprofile.time_zone)) + return JSONResponse.response({"error": "login-required", "data": "Please login in first"}) class LogSqlMiddleware(MiddlewareMixin): diff --git a/account/views/oj.py b/account/views/oj.py index 140c0e9..aad7c7d 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -357,7 +357,6 @@ class SessionManagementAPI(APIView): def get(self, request): engine = import_module(settings.SESSION_ENGINE) SessionStore = engine.SessionStore - current_session = request.COOKIES.get(settings.SESSION_COOKIE_NAME) current_session = request.session.session_key session_keys = request.user.session_keys result = [] diff --git a/contest/models.py b/contest/models.py index 3383d17..38b2356 100644 --- a/contest/models.py +++ b/contest/models.py @@ -64,7 +64,7 @@ class Contest(models.Model): ordering = ("-create_time",) -class ContestRank(models.Model): +class AbstractContestRank(models.Model): user = models.ForeignKey(User) contest = models.ForeignKey(Contest) submission_number = models.IntegerField(default=0) @@ -73,7 +73,7 @@ class ContestRank(models.Model): abstract = True -class ACMContestRank(ContestRank): +class ACMContestRank(AbstractContestRank): accepted_number = models.IntegerField(default=0) # total_time is only for ACM contest total_time = ac time + none-ac times * 20 * 60 total_time = models.IntegerField(default=0) @@ -85,7 +85,7 @@ class ACMContestRank(ContestRank): db_table = "acm_contest_rank" -class OIContestRank(ContestRank): +class OIContestRank(AbstractContestRank): total_score = models.IntegerField(default=0) # {23: 333}} # key is problem id, value is current score @@ -94,9 +94,6 @@ class OIContestRank(ContestRank): class Meta: db_table = "oi_contest_rank" - def update_rank(self, submission): - self.submission_number += 1 - class ContestAnnouncement(models.Model): contest = models.ForeignKey(Contest) diff --git a/contest/views/oj.py b/contest/views/oj.py index b25019c..a319513 100644 --- a/contest/views/oj.py +++ b/contest/views/oj.py @@ -1,6 +1,6 @@ import pickle 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 utils.cache import default_cache from utils.constants import CacheKey @@ -32,7 +32,7 @@ class ContestAPI(APIView): try: contest = Contest.objects.select_related("created_by").get(id=contest_id, visible=True) except Contest.DoesNotExist: - return self.error("Contest doesn't exist.") + return self.error("Contest does not exist") return self.success(ContestSerializer(contest).data) contests = Contest.objects.select_related("created_by").filter(visible=True) @@ -50,7 +50,7 @@ class ContestAPI(APIView): 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)) + contests = contests.filter(start_time__lte=cur, end_time__gte=cur) return self.success(self.paginate_data(request, contests, ContestSerializer)) @@ -62,14 +62,14 @@ class ContestPasswordVerifyAPI(APIView): try: contest = Contest.objects.get(id=data["contest_id"], visible=True, password__isnull=False) except Contest.DoesNotExist: - return self.error("Contest %s doesn't exist." % data["contest_id"]) + return self.error("Contest does not exist") if contest.password != data["password"]: - return self.error("Password doesn't match.") + return self.error("Wrong password") # password verify OK. - if "contests" not in request.session: - request.session["contests"] = [] - request.session["contests"].append(int(data["contest_id"])) + if "accessible_contests" not in request.session: + request.session["accessible_contests"] = [] + request.session["contests"].append(contest.id) # https://docs.djangoproject.com/en/dev/topics/http/sessions/#when-sessions-are-saved request.session.modified = True return self.success(True) @@ -80,13 +80,8 @@ class ContestAccessAPI(APIView): def get(self, request): contest_id = request.GET.get("contest_id") if not contest_id: - return self.error("Parameter contest_id not exist.") - if "contests" not in request.session: - request.session["contests"] = [] - if int(contest_id) in request.session["contests"]: - return self.success({"Access": True}) - else: - return self.success({"Access": False}) + return self.error() + return self.success({"access": int(contest_id) in request.session.get("accessible_contests", [])}) class ContestRankAPI(APIView): @@ -105,12 +100,10 @@ class ContestRankAPI(APIView): else: serializer = OIContestRankSerializer - cache_key = CacheKey.contest_rank_cache + str(self.contest.id) - qs = default_cache.get(cache_key) + cache_key = f"{CacheKey.contest_rank_cache}:{self.contest.id}" + qs = cache.get(cache_key) if not qs: - ranks = self.get_rank() - default_cache.set(cache_key, pickle.dumps(ranks)) - else: - ranks = pickle.loads(qs) + qs = self.get_rank() + cache.set(cache_key, qs) - return self.success(self.paginate_data(request, ranks, serializer)) + return self.success(self.paginate_data(request, qs, serializer)) diff --git a/oj/settings.py b/oj/settings.py index 4633cf0..672bc6f 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -58,7 +58,6 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', 'account.middleware.AdminRoleRequiredMiddleware', - 'account.middleware.SessionSecurityMiddleware', 'account.middleware.SessionRecordMiddleware', # 'account.middleware.LogSqlMiddleware', ) diff --git a/utils/api/api.py b/utils/api/api.py index 920b827..f49b039 100644 --- a/utils/api/api.py +++ b/utils/api/api.py @@ -79,7 +79,7 @@ class APIView(View): def success(self, data=None): return self.response({"error": None, "data": data}) - def error(self, msg, err="error"): + def error(self, msg="error", err="error"): return self.response({"error": err, "data": msg}) def _serializer_error_to_str(self, errors): From a324d55364ca9857d5cabf541b3470a3afbd3a07 Mon Sep 17 00:00:00 2001 From: virusdefender Date: Mon, 2 Oct 2017 05:16:14 +0800 Subject: [PATCH 056/106] tiny work --- account/models.py | 33 +++++------- account/serializers.py | 54 +++++++++---------- account/urls/oj.py | 1 - account/views/oj.py | 101 ++++++++++-------------------------- announcement/models.py | 2 +- announcement/serializers.py | 8 +-- conf/models.py | 6 +-- conf/serializers.py | 12 ++--- contest/models.py | 17 +----- contest/views/oj.py | 6 +-- utils/constants.py | 23 ++++++++ utils/xss_filter.py | 9 ++-- 12 files changed, 111 insertions(+), 161 deletions(-) diff --git a/account/models.py b/account/models.py index b909f93..2db0e55 100644 --- a/account/models.py +++ b/account/models.py @@ -24,22 +24,22 @@ class UserManager(models.Manager): class User(AbstractBaseUser): - username = models.CharField(max_length=30, unique=True) - email = models.EmailField(max_length=254, null=True) + username = models.CharField(max_length=32, unique=True) + email = models.EmailField(max_length=64, null=True) create_time = models.DateTimeField(auto_now_add=True, null=True) # One of UserType - admin_type = models.CharField(max_length=24, default=AdminType.REGULAR_USER) - problem_permission = models.CharField(max_length=24, default=ProblemPermission.NONE) - reset_password_token = models.CharField(max_length=40, null=True) + admin_type = models.CharField(max_length=32, default=AdminType.REGULAR_USER) + problem_permission = models.CharField(max_length=32, default=ProblemPermission.NONE) + reset_password_token = models.CharField(max_length=32, null=True) reset_password_token_expire_time = models.DateTimeField(null=True) # SSO auth token - auth_token = models.CharField(max_length=40, null=True) + auth_token = models.CharField(max_length=32, null=True) two_factor_auth = models.BooleanField(default=False) - tfa_token = models.CharField(max_length=40, null=True) + tfa_token = models.CharField(max_length=32, null=True) session_keys = JSONField(default=[]) # open api key open_api = models.BooleanField(default=False) - open_api_appkey = models.CharField(max_length=35, null=True) + open_api_appkey = models.CharField(max_length=32, null=True) is_disabled = models.BooleanField(default=False) USERNAME_FIELD = "username" @@ -63,10 +63,6 @@ class User(AbstractBaseUser): db_table = "user" -def _default_avatar(): - return f"/{settings.IMAGE_UPLOAD_DIR}/default.png" - - class UserProfile(models.Model): user = models.OneToOneField(User) # Store user problem solution status with json string format @@ -75,14 +71,13 @@ class UserProfile(models.Model): # {problems: {1: 33}, contest_problems: {1: 44}, record problem_id and score oi_problems_status = JSONField(default={}) - real_name = models.CharField(max_length=30, blank=True, null=True) - avatar = models.CharField(max_length=50, default=_default_avatar()) + real_name = models.CharField(max_length=32, blank=True, null=True) + avatar = models.CharField(max_length=256, default=f"{settings.IMAGE_UPLOAD_DIR}/default.png") blog = models.URLField(blank=True, null=True) - mood = models.CharField(max_length=200, blank=True, null=True) - github = models.CharField(max_length=50, blank=True, null=True) - school = models.CharField(max_length=200, blank=True, null=True) - major = models.CharField(max_length=200, blank=True, null=True) - language = models.CharField(max_length=32, blank=True, null=True) + mood = models.CharField(max_length=256, blank=True, null=True) + github = models.CharField(max_length=64, blank=True, null=True) + school = models.CharField(max_length=64, blank=True, null=True) + major = models.CharField(max_length=64, blank=True, null=True) # for ACM accepted_number = models.IntegerField(default=0) # for OI diff --git a/account/serializers.py b/account/serializers.py index 8345fc6..1b29170 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -6,27 +6,27 @@ from .models import AdminType, ProblemPermission, User, UserProfile class UserLoginSerializer(serializers.Serializer): - username = serializers.CharField(max_length=30) - password = serializers.CharField(max_length=30) - tfa_code = serializers.CharField(min_length=6, max_length=6, required=False, allow_null=True) + username = serializers.CharField() + password = serializers.CharField() + tfa_code = serializers.CharField(required=False, allow_null=True) class UsernameOrEmailCheckSerializer(serializers.Serializer): - username = serializers.CharField(max_length=30, required=False) - email = serializers.EmailField(max_length=30, required=False) + username = serializers.CharField(required=False) + email = serializers.EmailField(required=False) class UserRegisterSerializer(serializers.Serializer): - username = serializers.CharField(max_length=30) - password = serializers.CharField(max_length=30, min_length=6) - email = serializers.EmailField(max_length=30) - captcha = serializers.CharField(max_length=4, min_length=1) + username = serializers.CharField(max_length=32) + password = serializers.CharField(min_length=6) + email = serializers.EmailField(max_length=64) + captcha = serializers.CharField() class UserChangePasswordSerializer(serializers.Serializer): old_password = serializers.CharField() - new_password = serializers.CharField(max_length=30, min_length=6) - captcha = serializers.CharField(max_length=4, min_length=4) + new_password = serializers.CharField(min_length=6) + captcha = serializers.CharField() class UserSerializer(serializers.ModelSerializer): @@ -58,9 +58,9 @@ class UserInfoSerializer(serializers.ModelSerializer): class EditUserSerializer(serializers.Serializer): id = serializers.IntegerField() - username = serializers.CharField(max_length=30) - password = serializers.CharField(max_length=30, min_length=6, allow_blank=True, required=False, default=None) - email = serializers.EmailField(max_length=254) + username = serializers.CharField(max_length=32) + password = serializers.CharField(min_length=6, allow_blank=True, required=False, default=None) + email = serializers.EmailField(max_length=64) admin_type = serializers.ChoiceField(choices=(AdminType.REGULAR_USER, AdminType.ADMIN, AdminType.SUPER_ADMIN)) problem_permission = serializers.ChoiceField(choices=(ProblemPermission.NONE, ProblemPermission.OWN, ProblemPermission.ALL)) @@ -70,29 +70,29 @@ class EditUserSerializer(serializers.Serializer): class EditUserProfileSerializer(serializers.Serializer): - real_name = serializers.CharField(max_length=30, allow_blank=True) - avatar = serializers.CharField(max_length=100, allow_blank=True, required=False) - blog = serializers.URLField(allow_blank=True, required=False) - mood = serializers.CharField(max_length=200, allow_blank=True, required=False) - github = serializers.CharField(max_length=50, allow_blank=True, required=False) - school = serializers.CharField(max_length=200, allow_blank=True, required=False) - major = serializers.CharField(max_length=200, allow_blank=True, required=False) + real_name = serializers.CharField(max_length=32, allow_blank=True) + avatar = serializers.CharField(max_length=256, allow_blank=True, required=False) + blog = serializers.URLField(max_length=256, allow_blank=True, required=False) + mood = serializers.CharField(max_length=256, allow_blank=True, required=False) + github = serializers.CharField(max_length=64, allow_blank=True, required=False) + school = serializers.CharField(max_length=64, allow_blank=True, required=False) + major = serializers.CharField(max_length=64, allow_blank=True, required=False) class ApplyResetPasswordSerializer(serializers.Serializer): email = serializers.EmailField() - captcha = serializers.CharField(max_length=4, min_length=4) + captcha = serializers.CharField() class ResetPasswordSerializer(serializers.Serializer): - token = serializers.CharField(min_length=1, max_length=40) - password = serializers.CharField(min_length=6, max_length=30) - captcha = serializers.CharField(max_length=4, min_length=4) + token = serializers.CharField() + password = serializers.CharField(min_length=6) + captcha = serializers.CharField() class SSOSerializer(serializers.Serializer): - appkey = serializers.CharField(max_length=35) - token = serializers.CharField(max_length=40) + appkey = serializers.CharField() + token = serializers.CharField() class TwoFactorAuthCodeSerializer(serializers.Serializer): diff --git a/account/urls/oj.py b/account/urls/oj.py index b80b34e..cc71683 100644 --- a/account/urls/oj.py +++ b/account/urls/oj.py @@ -19,7 +19,6 @@ urlpatterns = [ url(r"^check_username_or_email", UsernameOrEmailCheck.as_view(), name="check_username_or_email"), url(r"^profile/?$", UserProfileAPI.as_view(), name="user_profile_api"), url(r"^upload_avatar/?$", AvatarUploadAPI.as_view(), name="avatar_upload_api"), - url(r"^sso/?$", SSOAPI.as_view(), name="sso_api"), url(r"^tfa_required/?$", CheckTFARequiredAPI.as_view(), name="tfa_required_check"), url(r"^two_factor_auth/?$", TwoFactorAuthAPI.as_view(), name="two_factor_auth_api"), url(r"^user_rank/?$", UserRankAPI.as_view(), name="user_rank_api"), diff --git a/account/views/oj.py b/account/views/oj.py index aad7c7d..1225556 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -12,11 +12,10 @@ from django.utils.timezone import now from django.views.decorators.csrf import ensure_csrf_cookie from otpauth import OtpAuth +from utils.constants import ContestRuleType from options.options import SysOptions from utils.api import APIView, validate_serializer -from utils.cache import default_cache from utils.captcha import Captcha -from utils.constants import CacheKey from utils.shortcuts import rand_str, img2base64, timestamp2utcstr from ..decorators import login_required from ..models import User, UserProfile @@ -38,7 +37,7 @@ class UserProfileAPI(APIView): """ user = request.user if not user.is_authenticated(): - return self.success({}) + return self.success() username = request.GET.get("username") try: if username: @@ -47,8 +46,7 @@ class UserProfileAPI(APIView): user = request.user except User.DoesNotExist: return self.error("User does not exist") - profile = UserProfile.objects.select_related("user").get(user=user) - return self.success(UserProfileSerializer(profile).data) + return self.success(UserProfileSerializer(user.userprofile).data) @validate_serializer(EditUserProfileSerializer) @login_required @@ -71,8 +69,7 @@ class AvatarUploadAPI(APIView): avatar = form.cleaned_data["file"] else: return self.error("Invalid file content") - # 2097152 = 2 * 1024 * 1024 = 2MB - if avatar.size > 2097152: + if avatar.size > 2 * 1024 * 1024: return self.error("Picture is too large") suffix = os.path.splitext(avatar.name)[-1].lower() if suffix not in [".gif", ".jpg", ".jpeg", ".bmp", ".png"]: @@ -83,46 +80,12 @@ class AvatarUploadAPI(APIView): for chunk in avatar: img.write(chunk) user_profile = request.user.userprofile - _, old_avatar = os.path.split(user_profile.avatar) - if old_avatar != "default.png": - os.remove(os.path.join(settings.IMAGE_UPLOAD_DIR_ABS, old_avatar)) - user_profile.avatar = f"/{settings.IMAGE_UPLOAD_DIR}/{name}" + user_profile.avatar = f"{settings.IMAGE_UPLOAD_DIR}/{name}" user_profile.save() return self.success("Succeeded") -class SSOAPI(APIView): - @login_required - def get(self, request): - callback = request.GET.get("callback", None) - if not callback: - return self.error("Parameter Error") - token = rand_str() - request.user.auth_token = token - request.user.save() - return self.success({"redirect_url": callback + "?token=" + token, - "callback": callback}) - - @validate_serializer(SSOSerializer) - def post(self, request): - data = request.data - try: - User.objects.get(open_api_appkey=data["appkey"]) - except User.DoesNotExist: - return self.error("Invalid appkey") - try: - user = User.objects.get(auth_token=data["token"]) - user.auth_token = None - user.save() - return self.success({"username": user.username, - "id": user.id, - "admin_type": user.admin_type, - "avatar": user.userprofile.avatar}) - except User.DoesNotExist: - return self.error("User does not exist") - - class TwoFactorAuthAPI(APIView): @login_required def get(self, request): @@ -131,7 +94,7 @@ class TwoFactorAuthAPI(APIView): """ user = request.user if user.two_factor_auth: - return self.error("Already open 2FA") + return self.error("2FA is already turned on") token = rand_str() user.tfa_token = token user.save() @@ -161,7 +124,7 @@ class TwoFactorAuthAPI(APIView): code = request.data["code"] user = request.user if not user.two_factor_auth: - return self.error("Other session have disabled TFA") + return self.error("2FA is already turned off") if OtpAuth(user.tfa_token).valid_totp(code): user.two_factor_auth = False user.save() @@ -198,7 +161,7 @@ class UserLoginAPI(APIView): # None is returned if username or password is wrong if user: if user.is_disabled: - return self.error("Your account have been disabled") + return self.error("Your account has been disabled") if not user.two_factor_auth: auth.login(request, user) return self.success("Succeeded") @@ -218,13 +181,13 @@ class UserLoginAPI(APIView): # todo remove this, only for debug use def get(self, request): auth.login(request, auth.authenticate(username=request.GET["username"], password=request.GET["password"])) - return self.success({}) + return self.success() class UserLogoutAPI(APIView): def get(self, request): auth.logout(request) - return self.success({}) + return self.success() class UsernameOrEmailCheck(APIView): @@ -240,11 +203,9 @@ class UsernameOrEmailCheck(APIView): "email": False } if data.get("username"): - if User.objects.filter(username=data["username"]).exists(): - result["username"] = True + result["username"] = User.objects.filter(username=data["username"]).exists() if data.get("email"): - if User.objects.filter(email=data["email"]).exists(): - result["email"] = True + result["email"] = User.objects.filter(email=data["email"]).exists() return self.success(result) @@ -254,17 +215,9 @@ class UserRegisterAPI(APIView): """ User register api """ - config = default_cache.get(CacheKey.website_config) - if config: - config = pickle.loads(config) - else: - config = WebsiteConfig.objects.first() - if not config: - config = WebsiteConfig.objects.create() - default_cache.set(CacheKey.website_config, pickle.dumps(config)) - if not config.allow_register: - return self.error("Register have been disabled by admin") + if not SysOptions.allow_register: + return self.error("Register function has been disabled by admin") data = request.data captcha = Captcha(request) @@ -293,6 +246,7 @@ class UserChangePasswordAPI(APIView): username = request.user.username user = auth.authenticate(username=username, password=data["old_password"]) if user: + # TODO: check tfa? user.set_password(data["new_password"]) user.save() return self.success("Succeeded") @@ -305,7 +259,6 @@ class ApplyResetPasswordAPI(APIView): def post(self, request): data = request.data captcha = Captcha(request) - config = WebsiteConfig.objects.first() if not captcha.check(data["captcha"]): return self.error("Invalid captcha") try: @@ -320,14 +273,14 @@ class ApplyResetPasswordAPI(APIView): user.save() render_data = { "username": user.username, - "website_name": config.name, - "link": f"{config.base_url}/reset-password/{user.reset_password_token}" + "website_name": SysOptions.website_name, + "link": f"{SysOptions.website_base_url}/reset-password/{user.reset_password_token}" } email_html = render_to_string("reset_password_email.html", render_data) - send_email_async.delay(config.name, + send_email_async.delay(SysOptions.website_name, user.email, user.username, - config.name + " 登录信息找回邮件", + f"{SysOptions.website_name} 登录信息找回邮件", email_html) return self.success("Succeeded") @@ -342,9 +295,9 @@ class ResetPasswordAPI(APIView): try: user = User.objects.get(reset_password_token=data["token"]) except User.DoesNotExist: - return self.error("Token dose not exist") - if int((user.reset_password_token_expire_time - now()).total_seconds()) < 0: - return self.error("Token have expired") + return self.error("Token does not exist") + if user.reset_password_token_expire_time < now(): + return self.error("Token has expired") user.reset_password_token = None user.two_factor_auth = False user.set_password(data["password"]) @@ -356,13 +309,13 @@ class SessionManagementAPI(APIView): @login_required def get(self, request): engine = import_module(settings.SESSION_ENGINE) - SessionStore = engine.SessionStore + session_store = engine.SessionStore current_session = request.session.session_key session_keys = request.user.session_keys result = [] modified = False for key in session_keys[:]: - session = SessionStore(key) + session = session_store(key) # session does not exist or is expiry if not session._session: session_keys.remove(key) @@ -398,12 +351,12 @@ class SessionManagementAPI(APIView): class UserRankAPI(APIView): def get(self, request): rule_type = request.GET.get("rule") - if rule_type not in ["acm", "oi"]: - rule_type = "acm" + if rule_type not in ContestRuleType.choices(): + rule_type = ContestRuleType.ACM profiles = UserProfile.objects.select_related("user")\ .filter(submission_number__gt=0)\ .exclude(user__is_disabled=True) - if rule_type == "acm": + if rule_type == ContestRuleType.ACM: profiles = profiles.order_by("-accepted_number", "submission_number") else: profiles = profiles.order_by("-total_score") diff --git a/announcement/models.py b/announcement/models.py index 186d4ea..49f57b8 100644 --- a/announcement/models.py +++ b/announcement/models.py @@ -5,7 +5,7 @@ from utils.models import RichTextField class Announcement(models.Model): - title = models.CharField(max_length=50) + title = models.CharField(max_length=64) # HTML content = RichTextField() create_time = models.DateTimeField(auto_now_add=True) diff --git a/announcement/serializers.py b/announcement/serializers.py index 0c0becc..b660a61 100644 --- a/announcement/serializers.py +++ b/announcement/serializers.py @@ -5,8 +5,8 @@ from .models import Announcement class CreateAnnouncementSerializer(serializers.Serializer): - title = serializers.CharField(max_length=50) - content = serializers.CharField(max_length=10000) + title = serializers.CharField(max_length=64) + content = serializers.CharField(max_length=1024 * 1024 * 8) visible = serializers.BooleanField() @@ -21,6 +21,6 @@ class AnnouncementSerializer(serializers.ModelSerializer): class EditAnnouncementSerializer(serializers.Serializer): id = serializers.IntegerField() - title = serializers.CharField(max_length=50) - content = serializers.CharField(max_length=10000) + title = serializers.CharField(max_length=64) + content = serializers.CharField(max_length=1024 * 1024 * 8) visible = serializers.BooleanField() diff --git a/conf/models.py b/conf/models.py index 86248db..4c6348d 100644 --- a/conf/models.py +++ b/conf/models.py @@ -3,16 +3,16 @@ from django.utils import timezone class JudgeServer(models.Model): - hostname = models.CharField(max_length=64) + hostname = models.CharField(max_length=128) ip = models.CharField(max_length=32, blank=True, null=True) - judger_version = models.CharField(max_length=24) + judger_version = models.CharField(max_length=32) cpu_core = models.IntegerField() memory_usage = models.FloatField() cpu_usage = models.FloatField() last_heartbeat = models.DateTimeField() create_time = models.DateTimeField(auto_now_add=True) task_number = models.IntegerField(default=0) - service_url = models.CharField(max_length=128, blank=True, null=True) + service_url = models.CharField(max_length=256, blank=True, null=True) @property def status(self): diff --git a/conf/serializers.py b/conf/serializers.py index 09f9940..7f0cf57 100644 --- a/conf/serializers.py +++ b/conf/serializers.py @@ -21,9 +21,9 @@ class TestSMTPConfigSerializer(serializers.Serializer): class CreateEditWebsiteConfigSerializer(serializers.Serializer): website_base_url = serializers.CharField(max_length=128) - website_name = serializers.CharField(max_length=32) - website_name_shortcut = serializers.CharField(max_length=32) - website_footer = serializers.CharField(max_length=1024) + website_name = serializers.CharField(max_length=64) + website_name_shortcut = serializers.CharField(max_length=64) + website_footer = serializers.CharField(max_length=1024 * 1024) allow_register = serializers.BooleanField() submission_list_show_all = serializers.BooleanField() @@ -39,10 +39,10 @@ class JudgeServerSerializer(serializers.ModelSerializer): class JudgeServerHeartbeatSerializer(serializers.Serializer): - hostname = serializers.CharField(max_length=64) - judger_version = serializers.CharField(max_length=24) + hostname = serializers.CharField(max_length=128) + judger_version = serializers.CharField(max_length=32) cpu_core = serializers.IntegerField(min_value=1) memory = serializers.FloatField(min_value=0, max_value=100) cpu = serializers.FloatField(min_value=0, max_value=100) action = serializers.ChoiceField(choices=("heartbeat", )) - service_url = serializers.CharField(max_length=128, required=False) + service_url = serializers.CharField(max_length=256, required=False) diff --git a/contest/models.py b/contest/models.py index 38b2356..d66d400 100644 --- a/contest/models.py +++ b/contest/models.py @@ -2,26 +2,11 @@ from django.db import models from django.utils.timezone import now from jsonfield import JSONField +from utils.constants import ContestStatus, ContestRuleType, ContestType from account.models import User, AdminType from utils.models import RichTextField -class ContestType(object): - PUBLIC_CONTEST = "Public" - PASSWORD_PROTECTED_CONTEST = "Password Protected" - - -class ContestStatus(object): - CONTEST_NOT_START = "1" - CONTEST_ENDED = "-1" - CONTEST_UNDERWAY = "0" - - -class ContestRuleType(object): - ACM = "ACM" - OI = "OI" - - class Contest(models.Model): title = models.CharField(max_length=40) description = RichTextField() diff --git a/contest/views/oj.py b/contest/views/oj.py index a319513..c4fd4fc 100644 --- a/contest/views/oj.py +++ b/contest/views/oj.py @@ -1,13 +1,11 @@ -import pickle from django.utils.timezone import now from django.core.cache import cache from utils.api import APIView, validate_serializer -from utils.cache import default_cache from utils.constants import CacheKey from account.decorators import login_required, check_contest_permission -from ..models import ContestAnnouncement, Contest, ContestStatus, ContestRuleType -from ..models import OIContestRank, ACMContestRank +from utils.constants import ContestRuleType, ContestType, ContestStatus +from ..models import ContestAnnouncement, Contest, OIContestRank, ACMContestRank from ..serializers import ContestAnnouncementSerializer from ..serializers import ContestSerializer, ContestPasswordVerifySerializer from ..serializers import OIContestRankSerializer, ACMContestRankSerializer diff --git a/utils/constants.py b/utils/constants.py index 14f13af..be7057a 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -1,3 +1,26 @@ +class Choices: + @classmethod + def choices(cls): + d = cls.__dict__ + return [d[item] for item in d.keys() if not item.startswith("__")] + + +class ContestType: + PUBLIC_CONTEST = "Public" + PASSWORD_PROTECTED_CONTEST = "Password Protected" + + +class ContestStatus: + CONTEST_NOT_START = "1" + CONTEST_ENDED = "-1" + CONTEST_UNDERWAY = "0" + + +class ContestRuleType(Choices): + ACM = "ACM" + OI = "OI" + + class CacheKey: waiting_queue = "waiting_queue" contest_rank_cache = "contest_rank_cache_" diff --git a/utils/xss_filter.py b/utils/xss_filter.py index d29495b..34d65a8 100644 --- a/utils/xss_filter.py +++ b/utils/xss_filter.py @@ -26,11 +26,8 @@ Cannot defense xss in browser which is belowed IE7 浏览器版本:IE7+ 或其他浏览器,无法防御IE6及以下版本浏览器中的XSS """ import re - -try: - from html.parser import HTMLParser -except: - from HTMLParser import HTMLParser +import copy +from html.parser import HTMLParser class XssHtml(HTMLParser): @@ -163,7 +160,7 @@ class XssHtml(HTMLParser): else: other = [] if attrs: - for (key, value) in attrs.items(): + for key, value in copy.deepcopy(attrs).items(): if key not in self.common_attrs + other: del attrs[key] return attrs From 93bd77d8d83249bbcc5b3908977de66fd03459d0 Mon Sep 17 00:00:00 2001 From: virusdefender Date: Fri, 6 Oct 2017 17:46:14 +0800 Subject: [PATCH 057/106] bug fixes --- account/middleware.py | 18 +++-------- account/migrations/0001_initial.py | 2 +- account/models.py | 2 +- account/serializers.py | 2 +- account/tests.py | 12 ++----- account/urls/oj.py | 2 +- account/views/oj.py | 10 +++--- contest/models.py | 3 +- contest/views/oj.py | 2 +- judge/dispatcher.py | 28 +++++++--------- oj/local_settings.py | 8 +++-- oj/settings.py | 51 ++++++++++++------------------ options/options.py | 8 ++--- problem/serializers.py | 2 ++ submission/views/oj.py | 4 +-- utils/cache.py | 31 +++++++++++++++--- 16 files changed, 91 insertions(+), 94 deletions(-) diff --git a/account/middleware.py b/account/middleware.py index 9141d53..b674346 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -1,10 +1,5 @@ -import time -import pytz - -from django.contrib import auth -from django.utils import timezone -from django.utils.translation import ugettext as _ from django.db import connection +from django.utils.timezone import now from django.utils.deprecation import MiddlewareMixin from utils.api import JSONResponse @@ -14,14 +9,11 @@ class SessionRecordMiddleware(MiddlewareMixin): def process_request(self, request): if request.user.is_authenticated(): session = request.session - ip = request.META.get("HTTP_X_REAL_IP", "UNKNOWN IP") - user_agent = request.META.get("HTTP_USER_AGENT", "") - _ip = session.setdefault("ip", ip) - _user_agent = session.setdefault("user_agent", user_agent) - if ip != _ip or user_agent != _user_agent: - session.modified = True + session["user_agent"] = request.META.get("HTTP_USER_AGENT", "") + session["ip"] = request.META.get("HTTP_X_REAL_IP", "UNKNOWN IP") + session["last_activity"] = now() user_sessions = request.user.session_keys - if request.session.session_key not in user_sessions: + if session.session_key not in user_sessions: user_sessions.append(session.session_key) request.user.save() diff --git a/account/migrations/0001_initial.py b/account/migrations/0001_initial.py index a96776e..e1e588e 100644 --- a/account/migrations/0001_initial.py +++ b/account/migrations/0001_initial.py @@ -50,7 +50,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('problems_status', jsonfield.fields.JSONField(default={})), - ('avatar', models.CharField(default=account.models._default_avatar, max_length=50)), + ('avatar', models.CharField(default="default.png", max_length=50)), ('blog', models.URLField(blank=True, null=True)), ('mood', models.CharField(blank=True, max_length=200, null=True)), ('accepted_problem_number', models.IntegerField(default=0)), diff --git a/account/models.py b/account/models.py index 2db0e55..3ba07d9 100644 --- a/account/models.py +++ b/account/models.py @@ -72,7 +72,7 @@ class UserProfile(models.Model): oi_problems_status = JSONField(default={}) real_name = models.CharField(max_length=32, blank=True, null=True) - avatar = models.CharField(max_length=256, default=f"{settings.IMAGE_UPLOAD_DIR}/default.png") + avatar = models.CharField(max_length=256, default=f"/{settings.IMAGE_UPLOAD_DIR}/default.png") blog = models.URLField(blank=True, null=True) mood = models.CharField(max_length=256, blank=True, null=True) github = models.CharField(max_length=64, blank=True, null=True) diff --git a/account/serializers.py b/account/serializers.py index 1b29170..aa9675a 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -26,7 +26,6 @@ class UserRegisterSerializer(serializers.Serializer): class UserChangePasswordSerializer(serializers.Serializer): old_password = serializers.CharField() new_password = serializers.CharField(min_length=6) - captcha = serializers.CharField() class UserSerializer(serializers.ModelSerializer): @@ -46,6 +45,7 @@ class UserProfileSerializer(serializers.ModelSerializer): class Meta: model = UserProfile + fields = "__all__" class UserInfoSerializer(serializers.ModelSerializer): diff --git a/account/tests.py b/account/tests.py index 7331a0e..906ef28 100644 --- a/account/tests.py +++ b/account/tests.py @@ -8,11 +8,9 @@ from otpauth import OtpAuth from utils.api.tests import APIClient, APITestCase from utils.shortcuts import rand_str -from utils.cache import default_cache -from utils.constants import CacheKey +from options.options import SysOptions from .models import AdminType, ProblemPermission, User -from conf.models import WebsiteConfig class PermissionDecoratorTest(APITestCase): @@ -157,13 +155,9 @@ class UserRegisterAPITest(CaptchaTest): self.data = {"username": "test_user", "password": "testuserpassword", "real_name": "real_name", "email": "test@qduoj.com", "captcha": self._set_captcha(self.client.session)} - # clea cache in redis - default_cache.delete(CacheKey.website_config) def test_website_config_limit(self): - website = WebsiteConfig.objects.create() - website.allow_register = False - website.save() + SysOptions.allow_register = False resp = self.client.post(self.register_url, data=self.data) self.assertDictEqual(resp.data, {"error": "error", "data": "Register have been disabled by admin"}) @@ -247,7 +241,6 @@ class TwoFactorAuthAPITest(APITestCase): def setUp(self): self.url = self.reverse("two_factor_auth_api") self.create_user("test", "test123") - self.create_website_config() def _get_tfa_code(self): user = User.objects.first() @@ -295,7 +288,6 @@ class ApplyResetPasswordAPITest(CaptchaTest): user.email = "test@oj.com" user.save() self.url = self.reverse("apply_reset_password_api") - self.create_website_config() self.data = {"email": "test@oj.com", "captcha": self._set_captcha(self.client.session)} def _refresh_captcha(self): diff --git a/account/urls/oj.py b/account/urls/oj.py index cc71683..aacbb59 100644 --- a/account/urls/oj.py +++ b/account/urls/oj.py @@ -3,7 +3,7 @@ from django.conf.urls import url from ..views.oj import (ApplyResetPasswordAPI, ResetPasswordAPI, UserChangePasswordAPI, UserRegisterAPI, UserLoginAPI, UserLogoutAPI, UsernameOrEmailCheck, - SSOAPI, AvatarUploadAPI, TwoFactorAuthAPI, UserProfileAPI, + AvatarUploadAPI, TwoFactorAuthAPI, UserProfileAPI, UserRankAPI, CheckTFARequiredAPI, SessionManagementAPI) from utils.captcha.views import CaptchaAPIView diff --git a/account/views/oj.py b/account/views/oj.py index 1225556..5f72156 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -1,5 +1,4 @@ import os -import pickle from datetime import timedelta from importlib import import_module @@ -16,15 +15,14 @@ from utils.constants import ContestRuleType from options.options import SysOptions from utils.api import APIView, validate_serializer from utils.captcha import Captcha -from utils.shortcuts import rand_str, img2base64, timestamp2utcstr +from utils.shortcuts import rand_str, img2base64, datetime2str from ..decorators import login_required from ..models import User, UserProfile from ..serializers import (ApplyResetPasswordSerializer, ResetPasswordSerializer, UserChangePasswordSerializer, UserLoginSerializer, UserRegisterSerializer, UsernameOrEmailCheckSerializer, RankInfoSerializer) -from ..serializers import (SSOSerializer, TwoFactorAuthCodeSerializer, - UserProfileSerializer, +from ..serializers import (TwoFactorAuthCodeSerializer, UserProfileSerializer, EditUserProfileSerializer, AvatarUploadForm) from ..tasks import send_email_async @@ -81,7 +79,7 @@ class AvatarUploadAPI(APIView): img.write(chunk) user_profile = request.user.userprofile - user_profile.avatar = f"{settings.IMAGE_UPLOAD_DIR}/{name}" + user_profile.avatar = f"/{settings.IMAGE_UPLOAD_DIR}/{name}" user_profile.save() return self.success("Succeeded") @@ -327,7 +325,7 @@ class SessionManagementAPI(APIView): s["current_session"] = True s["ip"] = session["ip"] s["user_agent"] = session["user_agent"] - s["last_activity"] = timestamp2utcstr(session["last_activity"]) + s["last_activity"] = datetime2str(session["last_activity"]) s["session_key"] = key result.append(s) if modified: diff --git a/contest/models.py b/contest/models.py index d66d400..10581cc 100644 --- a/contest/models.py +++ b/contest/models.py @@ -1,8 +1,9 @@ +from utils.constants import ContestRuleType # noqa from django.db import models from django.utils.timezone import now from jsonfield import JSONField -from utils.constants import ContestStatus, ContestRuleType, ContestType +from utils.constants import ContestStatus, ContestType from account.models import User, AdminType from utils.models import RichTextField diff --git a/contest/views/oj.py b/contest/views/oj.py index c4fd4fc..6811ef6 100644 --- a/contest/views/oj.py +++ b/contest/views/oj.py @@ -4,7 +4,7 @@ from utils.api import APIView, validate_serializer from utils.constants import CacheKey from account.decorators import login_required, check_contest_permission -from utils.constants import ContestRuleType, ContestType, ContestStatus +from utils.constants import ContestRuleType, ContestStatus from ..models import ContestAnnouncement, Contest, OIContestRank, ACMContestRank from ..serializers import ContestAnnouncementSerializer from ..serializers import ContestSerializer, ContestPasswordVerifySerializer diff --git a/judge/dispatcher.py b/judge/dispatcher.py index 597ed7d..29b4475 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -14,7 +14,7 @@ from judge.languages import languages from options.options import SysOptions from problem.models import Problem, ProblemRuleType from submission.models import JudgeStatus, Submission -from utils.cache import judge_cache, default_cache +from utils.cache import cache from utils.constants import CacheKey logger = logging.getLogger(__name__) @@ -22,31 +22,28 @@ logger = logging.getLogger(__name__) # 继续处理在队列中的问题 def process_pending_task(): - if judge_cache.llen(CacheKey.waiting_queue): + if cache.llen(CacheKey.waiting_queue): # 防止循环引入 from judge.tasks import judge_task - data = json.loads(judge_cache.rpop(CacheKey.waiting_queue).decode("utf-8")) + data = json.loads(cache.rpop(CacheKey.waiting_queue).decode("utf-8")) judge_task.delay(**data) class JudgeDispatcher(object): def __init__(self, submission_id, problem_id): self.token = hashlib.sha256(SysOptions.judge_server_token.encode("utf-8")).hexdigest() - self.redis_conn = judge_cache - self.submission = Submission.objects.get(pk=submission_id) + self.submission = Submission.objects.get(id=submission_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.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) def _request(self, url, data=None): - kwargs = {"headers": {"X-Judge-Server-Token": self.token, - "Content-Type": "application/json"}} + kwargs = {"headers": {"X-Judge-Server-Token": self.token}} if data: - kwargs["data"] = json.dumps(data) + kwargs["json"] = data try: return requests.post(url, **kwargs).json() except Exception as e: @@ -55,7 +52,6 @@ class JudgeDispatcher(object): @staticmethod def choose_judge_server(): with transaction.atomic(): - # TODO: use more reasonable way servers = JudgeServer.objects.select_for_update().all().order_by("task_number") servers = [s for s in servers if s.status == "normal"] if servers: @@ -65,10 +61,10 @@ class JudgeDispatcher(object): return server @staticmethod - def release_judge_res(judge_server_id): + def release_judge_server(judge_server_id): with transaction.atomic(): # 使用原子操作, 同时因为use和release中间间隔了判题过程,需要重新查询一下 - server = JudgeServer.objects.select_for_update().get(id=judge_server_id) + server = JudgeServer.objects.get(id=judge_server_id) server.used_instance_number = F("task_number") - 1 server.save() @@ -94,7 +90,7 @@ class JudgeDispatcher(object): server = self.choose_judge_server() if not server: data = {"submission_id": self.submission.id, "problem_id": self.problem.id} - self.redis_conn.lpush(CacheKey.waiting_queue, json.dumps(data)) + cache.lpush(CacheKey.waiting_queue, json.dumps(data)) return sub_config = list(filter(lambda item: self.submission.language == item["name"], languages))[0] @@ -138,7 +134,7 @@ class JudgeDispatcher(object): else: self.submission.result = JudgeStatus.PARTIALLY_ACCEPTED self.submission.save() - self.release_judge_res(server.id) + self.release_judge_server(server.id) self.update_problem_status() if self.contest_id: @@ -223,7 +219,7 @@ class JudgeDispatcher(object): if self.contest_id and self.contest.status != ContestStatus.CONTEST_UNDERWAY: return if self.contest.real_time_rank: - default_cache.delete(CacheKey.contest_rank_cache + str(self.contest_id)) + cache.delete(CacheKey.contest_rank_cache + str(self.contest_id)) with transaction.atomic(): if self.contest.rule_type == ContestRuleType.ACM: acm_rank, _ = ACMContestRank.objects.select_for_update(). \ diff --git a/oj/local_settings.py b/oj/local_settings.py index cee68f2..bbe2398 100644 --- a/oj/local_settings.py +++ b/oj/local_settings.py @@ -5,8 +5,12 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'HOST': '127.0.0.1', + 'PORT': 5433, + 'NAME': "onlinejudge", + 'USER': "onlinejudge", + 'PASSWORD': 'onlinejudge' } } diff --git a/oj/settings.py b/oj/settings.py index 672bc6f..ac972c7 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -61,7 +61,6 @@ MIDDLEWARE_CLASSES = ( 'account.middleware.SessionRecordMiddleware', # 'account.middleware.LogSqlMiddleware', ) -SESSION_ENGINE = 'django.contrib.sessions.backends.cache' ROOT_URLCONF = 'oj.urls' TEMPLATES = [ @@ -166,41 +165,33 @@ LOGGING = { } -REST_FRAMEWORK = { - 'TEST_REQUEST_DEFAULT_FORMAT': 'json', - 'DEFAULT_RENDERER_CLASSES': ( - 'rest_framework.renderers.JSONRenderer', - ) -} +REDIS_URL = "redis://127.0.0.1:6379" -CACHE_JUDGE_QUEUE = "judge_queue" -CACHE_THROTTLING = "throttling" +def redis_config(db): + def make_key(key, key_prefix, version): + return key + + return { + "BACKEND": "utils.cache.MyRedisCache", + "LOCATION": f"{REDIS_URL}/{db}", + "TIMEOUT": None, + "KEY_PREFIX": "", + "KEY_FUNCTION": make_key + } CACHES = { - "default": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": "redis://127.0.0.1:6379/1", - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - } - }, - CACHE_JUDGE_QUEUE: { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": "redis://127.0.0.1:6379/2", - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - } - }, - CACHE_THROTTLING: { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": "redis://127.0.0.1:6379/3", - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - } - } + "default": redis_config(db=1) } + +CELERY_RESULT_BACKEND = CELERY_BROKER_URL = f"{REDIS_URL}/2" +CELERY_TASK_SOFT_TIME_LIMIT = CELERY_TASK_TIME_LIMIT = 180 + +SESSION_ENGINE = "django.contrib.sessions.backends.cache" +SESSION_CACHE_ALIAS = "default" + + # For celery REDIS_QUEUE = { "host": "127.0.0.1", diff --git a/options/options.py b/options/options.py index 51beb53..b2d76f1 100644 --- a/options/options.py +++ b/options/options.py @@ -113,15 +113,15 @@ class _SysOptionsMeta(type): @property def website_base_url(cls): return cls._get_option(OptionKeys.website_base_url) - + @website_base_url.setter def website_base_url(cls, value): cls._set_option(OptionKeys.website_base_url, value) - + @property def website_name(cls): return cls._get_option(OptionKeys.website_name) - + @website_name.setter def website_name(cls, value): cls._set_option(OptionKeys.website_name, value) @@ -173,7 +173,7 @@ class _SysOptionsMeta(type): @judge_server_token.setter def judge_server_token(cls, value): cls._set_option(OptionKeys.judge_server_token, value) - + class SysOptions(metaclass=_SysOptionsMeta): pass diff --git a/problem/serializers.py b/problem/serializers.py index d9e4919..4856b6d 100644 --- a/problem/serializers.py +++ b/problem/serializers.py @@ -71,6 +71,7 @@ class CreateContestProblemSerializer(CreateOrEditProblemSerializer): class TagSerializer(serializers.ModelSerializer): class Meta: model = ProblemTag + fields = "__all__" class BaseProblemSerializer(serializers.ModelSerializer): @@ -88,6 +89,7 @@ class BaseProblemSerializer(serializers.ModelSerializer): class ProblemAdminSerializer(BaseProblemSerializer): class Meta: model = Problem + fields = "__all__" class ContestProblemAdminSerializer(BaseProblemSerializer): diff --git a/submission/views/oj.py b/submission/views/oj.py index 274418a..e9672a0 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -5,16 +5,16 @@ from problem.models import Problem, ProblemRuleType from contest.models import Contest, ContestStatus, ContestRuleType from utils.api import APIView, validate_serializer from utils.throttling import TokenBucket, BucketController +from utils.cache import cache from ..models import Submission from ..serializers import CreateSubmissionSerializer, SubmissionModelSerializer from ..serializers import SubmissionSafeSerializer, SubmissionListSerializer -from utils.cache import throttling_cache def _submit(response, user, problem_id, language, code, contest_id): # TODO: 预设默认值,需修改 controller = BucketController(user_id=user.id, - redis_conn=throttling_cache, + redis_conn=cache, default_capacity=30) bucket = TokenBucket(fill_rate=10, capacity=20, last_capacity=controller.last_capacity, diff --git a/utils/cache.py b/utils/cache.py index c77131f..ed9059b 100644 --- a/utils/cache.py +++ b/utils/cache.py @@ -1,6 +1,27 @@ -from django.conf import settings -from django_redis import get_redis_connection +from django.core.cache import cache, caches # noqa +from django.conf import settings # noqa -judge_cache = get_redis_connection(settings.CACHE_JUDGE_QUEUE) -throttling_cache = get_redis_connection(settings.CACHE_THROTTLING) -default_cache = get_redis_connection("default") +from django_redis.cache import RedisCache +from django_redis.client.default import DefaultClient + + +class MyRedisClient(DefaultClient): + def __getattr__(self, item): + client = self.get_client(write=True) + return getattr(client, item) + + def redis_incr(self, key, count=1): + """ + django 默认的 incr 在 key 不存在时候会抛异常 + """ + client = self.get_client(write=True) + return client.incr(key, count) + + +class MyRedisCache(RedisCache): + def __init__(self, server, params): + super().__init__(server, params) + self._client_cls = MyRedisClient + + def __getattr__(self, item): + return getattr(self.client, item) From 080ecf1bcf81dc9f206ada138e130c0ebc595337 Mon Sep 17 00:00:00 2001 From: zema1 Date: Wed, 11 Oct 2017 21:43:29 +0800 Subject: [PATCH 058/106] migrate to postgres json field --- account/migrations/0008_auto_20171011_1214.py | 105 ++++++++++++++++++ account/models.py | 23 ++-- .../migrations/0002_auto_20171011_1214.py | 20 ++++ conf/migrations/0002_auto_20171011_1214.py | 39 +++++++ contest/migrations/0006_auto_20171011_1214.py | 26 +++++ contest/models.py | 6 +- judge/dispatcher.py | 18 +-- options/migrations/0002_auto_20171011_1214.py | 21 ++++ options/models.py | 2 +- problem/migrations/0009_auto_20171011_1214.py | 41 +++++++ problem/models.py | 4 +- problem/views/oj.py | 4 +- .../migrations/0008_auto_20171011_1214.py | 26 +++++ submission/models.py | 6 +- utils/models.py | 1 + 15 files changed, 315 insertions(+), 27 deletions(-) create mode 100644 account/migrations/0008_auto_20171011_1214.py create mode 100644 announcement/migrations/0002_auto_20171011_1214.py create mode 100644 conf/migrations/0002_auto_20171011_1214.py create mode 100644 contest/migrations/0006_auto_20171011_1214.py create mode 100644 options/migrations/0002_auto_20171011_1214.py create mode 100644 problem/migrations/0009_auto_20171011_1214.py create mode 100644 submission/migrations/0008_auto_20171011_1214.py diff --git a/account/migrations/0008_auto_20171011_1214.py b/account/migrations/0008_auto_20171011_1214.py new file mode 100644 index 0000000..7426a1f --- /dev/null +++ b/account/migrations/0008_auto_20171011_1214.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-11 12:14 +from __future__ import unicode_literals + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0007_auto_20170920_0254'), + ] + + operations = [ + migrations.RemoveField( + model_name='userprofile', + name='language', + ), + migrations.AlterField( + model_name='user', + name='admin_type', + field=models.CharField(default='Regular User', max_length=32), + ), + migrations.AlterField( + model_name='user', + name='auth_token', + field=models.CharField(max_length=32, null=True), + ), + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(max_length=64, null=True), + ), + migrations.AlterField( + model_name='user', + name='open_api_appkey', + field=models.CharField(max_length=32, null=True), + ), + migrations.AlterField( + model_name='user', + name='problem_permission', + field=models.CharField(default='None', max_length=32), + ), + migrations.AlterField( + model_name='user', + name='reset_password_token', + field=models.CharField(max_length=32, null=True), + ), + migrations.AlterField( + model_name='user', + name='session_keys', + field=django.contrib.postgres.fields.jsonb.JSONField(default=list), + ), + migrations.AlterField( + model_name='user', + name='tfa_token', + field=models.CharField(max_length=32, null=True), + ), + migrations.AlterField( + model_name='user', + name='username', + field=models.CharField(max_length=32, unique=True), + ), + migrations.AlterField( + model_name='userprofile', + name='acm_problems_status', + field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), + ), + migrations.AlterField( + model_name='userprofile', + name='avatar', + field=models.CharField(default='/static/avatar/default.png', max_length=256), + ), + migrations.AlterField( + model_name='userprofile', + name='github', + field=models.CharField(blank=True, max_length=64, null=True), + ), + migrations.AlterField( + model_name='userprofile', + name='major', + field=models.CharField(blank=True, max_length=64, null=True), + ), + migrations.AlterField( + model_name='userprofile', + name='mood', + field=models.CharField(blank=True, max_length=256, null=True), + ), + migrations.AlterField( + model_name='userprofile', + name='oi_problems_status', + field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), + ), + migrations.AlterField( + model_name='userprofile', + name='real_name', + field=models.CharField(blank=True, max_length=32, null=True), + ), + migrations.AlterField( + model_name='userprofile', + name='school', + field=models.CharField(blank=True, max_length=64, null=True), + ), + ] diff --git a/account/models.py b/account/models.py index 3ba07d9..0b0e6ca 100644 --- a/account/models.py +++ b/account/models.py @@ -1,7 +1,7 @@ from django.contrib.auth.models import AbstractBaseUser from django.conf import settings from django.db import models -from jsonfield import JSONField +from utils.models import JSONField class AdminType(object): @@ -36,7 +36,7 @@ class User(AbstractBaseUser): auth_token = models.CharField(max_length=32, null=True) two_factor_auth = models.BooleanField(default=False) tfa_token = models.CharField(max_length=32, null=True) - session_keys = JSONField(default=[]) + session_keys = JSONField(default=list) # open api key open_api = models.BooleanField(default=False) open_api_appkey = models.CharField(max_length=32, null=True) @@ -65,11 +65,20 @@ class User(AbstractBaseUser): class UserProfile(models.Model): user = models.OneToOneField(User) - # Store user problem solution status with json string format - # {problems: {1: JudgeStatus.ACCEPTED}, contest_problems: {1: JudgeStatus.ACCEPTED}}, record problem_id and status - acm_problems_status = JSONField(default={}) - # {problems: {1: 33}, contest_problems: {1: 44}, record problem_id and score - oi_problems_status = JSONField(default={}) + # acm_problems_status examples: + # { + # "problems": { + # "1": { + # "status": JudgeStatus.ACCEPTED, + # "_id": "1000" + # } + # }, + # "contest_problems": { + # } + # } + acm_problems_status = JSONField(default=dict) + # like acm_problems_status, merely add "score" field + oi_problems_status = JSONField(default=dict) real_name = models.CharField(max_length=32, blank=True, null=True) avatar = models.CharField(max_length=256, default=f"/{settings.IMAGE_UPLOAD_DIR}/default.png") diff --git a/announcement/migrations/0002_auto_20171011_1214.py b/announcement/migrations/0002_auto_20171011_1214.py new file mode 100644 index 0000000..ffe4c96 --- /dev/null +++ b/announcement/migrations/0002_auto_20171011_1214.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-11 12:14 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('announcement', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='announcement', + name='title', + field=models.CharField(max_length=64), + ), + ] diff --git a/conf/migrations/0002_auto_20171011_1214.py b/conf/migrations/0002_auto_20171011_1214.py new file mode 100644 index 0000000..ef355b5 --- /dev/null +++ b/conf/migrations/0002_auto_20171011_1214.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-11 12:14 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('conf', '0001_initial'), + ] + + operations = [ + migrations.DeleteModel( + name='JudgeServerToken', + ), + migrations.DeleteModel( + name='SMTPConfig', + ), + migrations.DeleteModel( + name='WebsiteConfig', + ), + migrations.AlterField( + model_name='judgeserver', + name='hostname', + field=models.CharField(max_length=128), + ), + migrations.AlterField( + model_name='judgeserver', + name='judger_version', + field=models.CharField(max_length=32), + ), + migrations.AlterField( + model_name='judgeserver', + name='service_url', + field=models.CharField(blank=True, max_length=256, null=True), + ), + ] diff --git a/contest/migrations/0006_auto_20171011_1214.py b/contest/migrations/0006_auto_20171011_1214.py new file mode 100644 index 0000000..d429742 --- /dev/null +++ b/contest/migrations/0006_auto_20171011_1214.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-11 12:14 +from __future__ import unicode_literals + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('contest', '0005_auto_20170823_0918'), + ] + + operations = [ + migrations.AlterField( + model_name='acmcontestrank', + name='submission_info', + field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), + ), + migrations.AlterField( + model_name='oicontestrank', + name='submission_info', + field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), + ), + ] diff --git a/contest/models.py b/contest/models.py index 10581cc..4251998 100644 --- a/contest/models.py +++ b/contest/models.py @@ -1,7 +1,7 @@ from utils.constants import ContestRuleType # noqa from django.db import models from django.utils.timezone import now -from jsonfield import JSONField +from utils.models import JSONField from utils.constants import ContestStatus, ContestType from account.models import User, AdminType @@ -65,7 +65,7 @@ class ACMContestRank(AbstractContestRank): total_time = models.IntegerField(default=0) # {23: {"is_ac": True, "ac_time": 8999, "error_number": 2, "is_first_ac": True}} # key is problem id - submission_info = JSONField(default={}) + submission_info = JSONField(default=dict) class Meta: db_table = "acm_contest_rank" @@ -75,7 +75,7 @@ class OIContestRank(AbstractContestRank): total_score = models.IntegerField(default=0) # {23: 333}} # key is problem id, value is current score - submission_info = JSONField(default={}) + submission_info = JSONField(default=dict) class Meta: db_table = "oi_contest_rank" diff --git a/judge/dispatcher.py b/judge/dispatcher.py index 29b4475..e457b29 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -186,13 +186,10 @@ class JudgeDispatcher(object): # update user_profile if problem_id not in acm_problems_status: - acm_problems_status[problem_id] = self.submission.result + acm_problems_status[problem_id] = {"status": self.submission.result, "_id": self.problem._id} # skip if the problem has been accepted - elif acm_problems_status[problem_id] != JudgeStatus.ACCEPTED: - if self.submission.result == JudgeStatus.ACCEPTED: - acm_problems_status[problem_id] = JudgeStatus.ACCEPTED - else: - acm_problems_status[problem_id] = self.submission.result + elif acm_problems_status[problem_id]["status"] != JudgeStatus.ACCEPTED: + acm_problems_status[problem_id]["status"] = self.submission.result user_profile.acm_problems_status[key] = acm_problems_status else: @@ -204,11 +201,14 @@ class JudgeDispatcher(object): # update user_profile if problem_id not in oi_problems_status: user_profile.add_score(score) - oi_problems_status[problem_id] = score + oi_problems_status[problem_id] = {"status": self.submission.result, + "_id": self.problem._id, + "score": score} else: # minus last time score, add this time score - user_profile.add_score(this_time_score=score, last_time_score=oi_problems_status[problem_id]) - oi_problems_status[problem_id] = 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[key] = oi_problems_status problem.save(update_fields=["submission_number", "accepted_number", "statistic_info"]) diff --git a/options/migrations/0002_auto_20171011_1214.py b/options/migrations/0002_auto_20171011_1214.py new file mode 100644 index 0000000..ee52ffa --- /dev/null +++ b/options/migrations/0002_auto_20171011_1214.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-11 12:14 +from __future__ import unicode_literals + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('options', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='sysoptions', + name='value', + field=django.contrib.postgres.fields.jsonb.JSONField(), + ), + ] diff --git a/options/models.py b/options/models.py index 6d9ac75..04dee5e 100644 --- a/options/models.py +++ b/options/models.py @@ -1,5 +1,5 @@ from django.db import models -from jsonfield import JSONField +from utils.models import JSONField class SysOptions(models.Model): diff --git a/problem/migrations/0009_auto_20171011_1214.py b/problem/migrations/0009_auto_20171011_1214.py new file mode 100644 index 0000000..7073b8f --- /dev/null +++ b/problem/migrations/0009_auto_20171011_1214.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-11 12:14 +from __future__ import unicode_literals + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('problem', '0008_auto_20170923_1318'), + ] + + operations = [ + migrations.AlterField( + model_name='problem', + name='languages', + field=django.contrib.postgres.fields.jsonb.JSONField(), + ), + migrations.AlterField( + model_name='problem', + name='samples', + field=django.contrib.postgres.fields.jsonb.JSONField(), + ), + migrations.AlterField( + model_name='problem', + name='statistic_info', + field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), + ), + migrations.AlterField( + model_name='problem', + name='template', + field=django.contrib.postgres.fields.jsonb.JSONField(), + ), + migrations.AlterField( + model_name='problem', + name='test_case_score', + field=django.contrib.postgres.fields.jsonb.JSONField(), + ), + ] diff --git a/problem/models.py b/problem/models.py index 0e9c5e2..72f3fc9 100644 --- a/problem/models.py +++ b/problem/models.py @@ -1,5 +1,5 @@ from django.db import models -from jsonfield import JSONField +from utils.models import JSONField from account.models import User from contest.models import Contest @@ -66,7 +66,7 @@ class Problem(models.Model): 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 - statistic_info = JSONField(default={}) + statistic_info = JSONField(default=dict) class Meta: db_table = "problem" diff --git a/problem/views/oj.py b/problem/views/oj.py index 0a6b066..6764249 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -55,9 +55,9 @@ class ProblemAPI(APIView): oi_problems_status = profile.oi_problems_status.get("problems", {}) for problem in data["results"]: if problem["rule_type"] == ProblemRuleType.ACM: - problem["my_status"] = acm_problems_status.get(str(problem["id"]), None) + problem["my_status"] = acm_problems_status.get(str(problem["id"]), {}).get("status") else: - problem["my_status"] = oi_problems_status.get(str(problem["id"]), None) + problem["my_status"] = oi_problems_status.get(str(problem["id"]), {}).get("status") return self.success(data) diff --git a/submission/migrations/0008_auto_20171011_1214.py b/submission/migrations/0008_auto_20171011_1214.py new file mode 100644 index 0000000..1c585d8 --- /dev/null +++ b/submission/migrations/0008_auto_20171011_1214.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-11 12:14 +from __future__ import unicode_literals + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('submission', '0007_auto_20170923_1318'), + ] + + operations = [ + migrations.AlterField( + model_name='submission', + name='info', + field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), + ), + migrations.AlterField( + model_name='submission', + name='statistic_info', + field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), + ), + ] diff --git a/submission/models.py b/submission/models.py index f4cb2f2..ef40893 100644 --- a/submission/models.py +++ b/submission/models.py @@ -1,5 +1,5 @@ from django.db import models -from jsonfield import JSONField +from utils.models import JSONField from account.models import AdminType from problem.models import Problem from contest.models import Contest @@ -31,12 +31,12 @@ class Submission(models.Model): code = models.TextField() result = models.IntegerField(db_index=True, default=JudgeStatus.PENDING) # 判题结果的详细信息 - info = JSONField(default={}) + info = JSONField(default=dict) language = models.CharField(max_length=20) shared = models.BooleanField(default=False) # 存储该提交所用时间和内存值,方便提交列表显示 # {time_cost: "", memory_cost: "", err_info: "", score: 0} - statistic_info = JSONField(default={}) + statistic_info = JSONField(default=dict) def check_user_permission(self, user): return self.user_id == user.id or \ diff --git a/utils/models.py b/utils/models.py index b651aa2..3c11452 100644 --- a/utils/models.py +++ b/utils/models.py @@ -1,3 +1,4 @@ +from django.contrib.postgres.fields import JSONField # NOQA from django.db import models from utils.xss_filter import XssHtml From 2c5a1e42bf5bc866067edccc9aa30b278c73a9f2 Mon Sep 17 00:00:00 2001 From: zema1 Date: Sun, 15 Oct 2017 18:36:55 +0800 Subject: [PATCH 059/106] support share submission --- account/models.py | 4 +++ problem/models.py | 2 +- problem/views/oj.py | 56 ++++++++++++++++++++++++++------------- submission/models.py | 10 ++++--- submission/serializers.py | 9 +++++-- submission/views/oj.py | 39 ++++++++++++++++++++------- 6 files changed, 85 insertions(+), 35 deletions(-) diff --git a/account/models.py b/account/models.py index 0b0e6ca..6ef7f71 100644 --- a/account/models.py +++ b/account/models.py @@ -74,6 +74,10 @@ class UserProfile(models.Model): # } # }, # "contest_problems": { + # "1": { + # "status": JudgeStatus.ACCEPTED, + # "_id": "1000" + # } # } # } acm_problems_status = JSONField(default=dict) diff --git a/problem/models.py b/problem/models.py index 72f3fc9..9053793 100644 --- a/problem/models.py +++ b/problem/models.py @@ -65,7 +65,7 @@ class Problem(models.Model): 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 + # {JudgeStatus.ACCEPTED: 3, JudgeStaus.WRONG_ANSWER: 11}, the number means count statistic_info = JSONField(default=dict) class Meta: diff --git a/problem/views/oj.py b/problem/views/oj.py index 6764249..321b1bb 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -13,6 +13,25 @@ class ProblemTagAPI(APIView): class ProblemAPI(APIView): + @staticmethod + def _add_problem_status(request, queryset_values): + if request.user.is_authenticated(): + profile = request.user.userprofile + acm_problems_status = profile.acm_problems_status.get("problems", {}) + oi_problems_status = profile.oi_problems_status.get("problems", {}) + # paginate data + results = queryset_values.get("results") + if results: + problems = results + else: + problems = [queryset_values,] + + for problem in problems: + if problem["rule_type"] == ProblemRuleType.ACM: + problem["my_status"] = acm_problems_status.get(str(problem["id"]), {}).get("status") + else: + problem["my_status"] = oi_problems_status.get(str(problem["id"]), {}).get("status") + def get(self, request): # 问题详情页 problem_id = request.GET.get("problem_id") @@ -20,7 +39,9 @@ class ProblemAPI(APIView): try: problem = Problem.objects.select_related("created_by")\ .get(_id=problem_id, contest_id__isnull=True, visible=True) - return self.success(ProblemSerializer(problem).data) + problem_data = ProblemSerializer(problem).data + self._add_problem_status(request, problem_data) + return self.success(problem_data) except Problem.DoesNotExist: return self.error("Problem does not exist") @@ -49,19 +70,21 @@ class ProblemAPI(APIView): problems = problems.filter(difficulty=difficulty) # 根据profile 为做过的题目添加标记 data = self.paginate_data(request, problems, ProblemSerializer) - if request.user.id: - profile = request.user.userprofile - acm_problems_status = profile.acm_problems_status.get("problems", {}) - oi_problems_status = profile.oi_problems_status.get("problems", {}) - for problem in data["results"]: - if problem["rule_type"] == ProblemRuleType.ACM: - problem["my_status"] = acm_problems_status.get(str(problem["id"]), {}).get("status") - else: - problem["my_status"] = oi_problems_status.get(str(problem["id"]), {}).get("status") + self._add_problem_status(request, data) return self.success(data) class ContestProblemAPI(APIView): + def _add_problem_status(self, request, queryset_values): + if request.user.is_authenticated() and self.contest.rule_type != ContestRuleType.OI: + profile = request.user.userprofile + if self.contest.rule_type == ContestRuleType.ACM: + problems_status = profile.acm_problems_status.get("contest_problems", {}) + else: + problems_status = profile.oi_problems_status.get("contest_problems", {}) + for problem in queryset_values: + problem["my_status"] = problems_status.get(str(problem["id"]), {}).get("status") + @check_contest_permission def get(self, request): problem_id = request.GET.get("problem_id") @@ -72,17 +95,12 @@ class ContestProblemAPI(APIView): visible=True) except Problem.DoesNotExist: return self.error("Problem does not exist.") - return self.success(ContestProblemSerializer(problem).data) + problem_data = ContestProblemSerializer(problem).data + self._add_problem_status(request, problem_data) + return self.success(problem_data) 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.is_authenticated() and self.contest.rule_type != ContestRuleType.OI: - profile = request.user.userprofile - if self.contest.rule_type == ContestRuleType.ACM: - problems_status = profile.acm_problems_status.get("contest_problems", {}) - else: - problems_status = profile.oi_problems_status.get("contest_problems", {}) - for problem in data: - problem["my_status"] = problems_status.get(str(problem["id"]), None) + self._add_problem_status(request, data) return self.success(data) diff --git a/submission/models.py b/submission/models.py index ef40893..7f046a0 100644 --- a/submission/models.py +++ b/submission/models.py @@ -30,7 +30,7 @@ class Submission(models.Model): username = models.CharField(max_length=30) code = models.TextField() result = models.IntegerField(db_index=True, default=JudgeStatus.PENDING) - # 判题结果的详细信息 + # 从JudgeServer返回的判题详情 info = JSONField(default=dict) language = models.CharField(max_length=20) shared = models.BooleanField(default=False) @@ -38,10 +38,12 @@ class Submission(models.Model): # {time_cost: "", memory_cost: "", err_info: "", score: 0} statistic_info = JSONField(default=dict) - def check_user_permission(self, user): + def check_user_permission(self, user, check_share=True): return self.user_id == user.id or \ - self.shared is True or \ - user.admin_type == AdminType.SUPER_ADMIN + (check_share and self.shared is True) or \ + user.is_super_admin() or \ + user.can_mgmt_all_problem() or \ + self.problem.created_by_id == user.id class Meta: db_table = "submission" diff --git a/submission/serializers.py b/submission/serializers.py index ae8c3a6..66a517b 100644 --- a/submission/serializers.py +++ b/submission/serializers.py @@ -10,6 +10,11 @@ class CreateSubmissionSerializer(serializers.Serializer): contest_id = serializers.IntegerField(required=False) +class ShareSubmissionSerializer(serializers.Serializer): + id = serializers.CharField() + shared = serializers.BooleanField() + + class SubmissionModelSerializer(serializers.ModelSerializer): info = serializers.JSONField() statistic_info = serializers.JSONField() @@ -19,7 +24,7 @@ class SubmissionModelSerializer(serializers.ModelSerializer): # 不显示submission info的serializer, 用于ACM rule_type -class SubmissionSafeSerializer(serializers.ModelSerializer): +class SubmissionSafeModelSerializer(serializers.ModelSerializer): problem = serializers.SlugRelatedField(read_only=True, slug_field="_id") statistic_info = serializers.JSONField() @@ -43,6 +48,6 @@ class SubmissionListSerializer(serializers.ModelSerializer): def get_show_link(self, obj): # 没传user或为匿名user - if self.user is None or self.user.id is None: + if self.user is None or not self.user.is_authenticated(): return False return obj.check_user_permission(self.user) diff --git a/submission/views/oj.py b/submission/views/oj.py index e9672a0..c4bfe21 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -7,8 +7,9 @@ from utils.api import APIView, validate_serializer from utils.throttling import TokenBucket, BucketController from utils.cache import cache from ..models import Submission -from ..serializers import CreateSubmissionSerializer, SubmissionModelSerializer -from ..serializers import SubmissionSafeSerializer, SubmissionListSerializer +from ..serializers import (CreateSubmissionSerializer, SubmissionModelSerializer, + ShareSubmissionSerializer) +from ..serializers import SubmissionSafeModelSerializer, SubmissionListSerializer def _submit(response, user, problem_id, language, code, contest_id): @@ -63,17 +64,37 @@ class SubmissionAPI(APIView): def get(self, request): submission_id = request.GET.get("id") if not submission_id: - return self.error("Parameter id doesn't exist.") + return self.error("Parameter id doesn't exist") try: submission = Submission.objects.select_related("problem").get(id=submission_id) except Submission.DoesNotExist: - return self.error("Submission doesn't exist.") + return self.error("Submission doesn't exist") if not submission.check_user_permission(request.user): - return self.error("No permission for this submission.") + return self.error("No permission for this submission") if submission.problem.rule_type == ProblemRuleType.ACM: - return self.success(SubmissionSafeSerializer(submission).data) - return self.success(SubmissionModelSerializer(submission).data) + submission_data = SubmissionSafeModelSerializer(submission).data + else: + submission_data = SubmissionModelSerializer(submission).data + # 是否有权限取消共享 + submission_data["can_unshare"] = submission.check_user_permission(request.user, check_share=False) + return self.success(submission_data) + + @validate_serializer(ShareSubmissionSerializer) + @login_required + def put(self, request): + try: + submission = Submission.objects.select_related("problem")\ + .get(id=request.data["id"], contest__isnull=True) + except Submission.DoesNotExist: + return self.error("Submission doesn't exist") + if not submission.check_user_permission(request.user, check_share=False): + return self.error("No permission to share the submission") + if submission.contest and submission.contest.status == ContestStatus.CONTEST_UNDERWAY: + return self.error("Can not share submission during a contest going") + submission.shared = request.data["shared"] + submission.save(update_fields=["shared"]) + return self.success() class SubmissionListAPI(APIView): @@ -83,7 +104,7 @@ class SubmissionListAPI(APIView): if request.GET.get("contest_id"): return self.error("Parameter error") - submissions = Submission.objects.filter(contest_id__isnull=True) + submissions = Submission.objects.filter(contest_id__isnull=True).select_related("problem__created_by") problem_id = request.GET.get("problem_id") myself = request.GET.get("myself") result = request.GET.get("result") @@ -112,7 +133,7 @@ class ContestSubmissionListAPI(APIView): if contest.rule_type == ContestRuleType.OI and not contest.is_contest_admin(request.user): return self.error("No permission for OI contest submissions") - submissions = Submission.objects.filter(contest_id=contest.id) + submissions = Submission.objects.filter(contest_id=contest.id).select_related("problem__created_by") problem_id = request.GET.get("problem_id") myself = request.GET.get("myself") result = request.GET.get("result") From f5566148bce13b91d651e4089f2e1e970de05335 Mon Sep 17 00:00:00 2001 From: zema1 Date: Mon, 16 Oct 2017 09:45:29 +0800 Subject: [PATCH 060/106] =?UTF-8?q?=E5=AE=8C=E5=96=84OI=E7=BB=86=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/decorators.py | 3 ++- contest/models.py | 8 ++++++++ contest/views/oj.py | 10 ++++++---- judge/dispatcher.py | 27 +++++++++++---------------- problem/serializers.py | 1 + problem/views/oj.py | 17 ++++++++--------- submission/views/oj.py | 13 +++++++------ utils/constants.py | 2 +- 8 files changed, 44 insertions(+), 37 deletions(-) diff --git a/account/decorators.py b/account/decorators.py index f47c4a0..b352366 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -92,7 +92,8 @@ def check_contest_permission(func): if not user.is_authenticated(): return self.error("Please login in first.") # password error - if ("contests" not in request.session) or (self.contest.id not in request.session["contests"]): + if ("accessible_contests" not in request.session) or \ + (self.contest.id not in request.session["accessible_contests"]): return self.error("Password is required.") return func(*args, **kwargs) diff --git a/contest/models.py b/contest/models.py index 4251998..eb1c88a 100644 --- a/contest/models.py +++ b/contest/models.py @@ -45,6 +45,14 @@ class Contest(models.Model): def is_contest_admin(self, user): return user.is_authenticated() and (self.created_by == user or user.admin_type == AdminType.SUPER_ADMIN) + def check_oi_permission(self, user): + if self.status != ContestStatus.CONTEST_ENDED and self.real_time_rank == False: + if self.is_contest_admin(user): + return True + else: + return False + return True + class Meta: db_table = "contest" ordering = ("-create_time",) diff --git a/contest/views/oj.py b/contest/views/oj.py index 6811ef6..1e787ce 100644 --- a/contest/views/oj.py +++ b/contest/views/oj.py @@ -67,7 +67,7 @@ class ContestPasswordVerifyAPI(APIView): # password verify OK. if "accessible_contests" not in request.session: request.session["accessible_contests"] = [] - request.session["contests"].append(contest.id) + request.session["accessible_contests"].append(contest.id) # https://docs.djangoproject.com/en/dev/topics/http/sessions/#when-sessions-are-saved request.session.modified = True return self.success(True) @@ -93,10 +93,12 @@ class ContestRankAPI(APIView): @check_contest_permission def get(self, request): - if self.contest.rule_type == ContestRuleType.ACM: - serializer = ACMContestRankSerializer - else: + if self.contest.rule_type == ContestRuleType.OI: + if not self.contest.check_oi_permission(request.user): + return self.error("You have no permission for ranks now") serializer = OIContestRankSerializer + else: + serializer = ACMContestRankSerializer cache_key = f"{CacheKey.contest_rank_cache}:{self.contest.id}" qs = cache.get(cache_key) diff --git a/judge/dispatcher.py b/judge/dispatcher.py index e457b29..829a43d 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -156,7 +156,6 @@ class JudgeDispatcher(object): with transaction.atomic(): # prepare problem and user_profile 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.contest_id: @@ -165,25 +164,25 @@ class JudgeDispatcher(object): key = "problems" acm_problems_status = user_profile.acm_problems_status.get(key, {}) oi_problems_status = user_profile.oi_problems_status.get(key, {}) + problem_id = str(self.problem.id) + problem_info = problem.statistic_info + + # update problem info + result = str(self.submission.result) + problem_info[result] = problem_info.get(result, 0) + 1 + problem.statistic_info = problem_info # update submission and accepted number counter problem.submission_number += 1 if self.submission.result == JudgeStatus.ACCEPTED: problem.accepted_number += 1 - # 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 + # submission in a contest will not be counted in user profile if not self.contest_id: user_profile.submission_number += 1 if self.submission.result == JudgeStatus.ACCEPTED: user_profile.accepted_number += 1 - problem_id = str(self.problem.id) if self.problem.rule_type == ProblemRuleType.ACM: - # update acm problem info - result = str(self.submission.result) - problem_info[result] = problem_info.get(result, 0) + 1 - problem.statistic_info = problem_info - # update user_profile if problem_id not in acm_problems_status: acm_problems_status[problem_id] = {"status": self.submission.result, "_id": self.problem._id} @@ -193,12 +192,8 @@ class JudgeDispatcher(object): user_profile.acm_problems_status[key] = acm_problems_status else: - # update oi problem info - score = self.submission.statistic_info["score"] - problem_info[score] = problem_info.get(score, 0) + 1 - problem.statistic_info = problem_info - # update user_profile + score = self.submission.statistic_info["score"] if problem_id not in oi_problems_status: user_profile.add_score(score) oi_problems_status[problem_id] = {"status": self.submission.result, @@ -218,8 +213,8 @@ class JudgeDispatcher(object): def update_contest_rank(self): if self.contest_id and self.contest.status != ContestStatus.CONTEST_UNDERWAY: return - if self.contest.real_time_rank: - cache.delete(CacheKey.contest_rank_cache + str(self.contest_id)) + if self.contest.rule_type == ContestRuleType.OI or self.contest.real_time_rank: + cache.delete(f"{CacheKey.contest_rank_cache}:{self.contest.id}") with transaction.atomic(): if self.contest.rule_type == ContestRuleType.ACM: acm_rank, _ = ACMContestRank.objects.select_for_update(). \ diff --git a/problem/serializers.py b/problem/serializers.py index 4856b6d..0e44710 100644 --- a/problem/serializers.py +++ b/problem/serializers.py @@ -95,6 +95,7 @@ class ProblemAdminSerializer(BaseProblemSerializer): class ContestProblemAdminSerializer(BaseProblemSerializer): class Meta: model = Problem + fields = "__all__" class ProblemSerializer(BaseProblemSerializer): diff --git a/problem/views/oj.py b/problem/views/oj.py index 321b1bb..3410ae1 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -21,11 +21,10 @@ class ProblemAPI(APIView): oi_problems_status = profile.oi_problems_status.get("problems", {}) # paginate data results = queryset_values.get("results") - if results: + if results is not None: problems = results else: problems = [queryset_values,] - for problem in problems: if problem["rule_type"] == ProblemRuleType.ACM: problem["my_status"] = acm_problems_status.get(str(problem["id"]), {}).get("status") @@ -53,11 +52,7 @@ class ProblemAPI(APIView): # 按照标签筛选 tag_text = request.GET.get("tag") if tag_text: - try: - tag = ProblemTag.objects.get(name=tag_text) - except ProblemTag.DoesNotExist: - return self.error("The Tag does not exist.") - problems = tag.problem_set.all().filter(visible=True) + problems = problems.filter(tags__name=tag_text) # 搜索的情况 keyword = request.GET.get("keyword", "").strip() @@ -76,7 +71,11 @@ class ProblemAPI(APIView): class ContestProblemAPI(APIView): def _add_problem_status(self, request, queryset_values): - if request.user.is_authenticated() and self.contest.rule_type != ContestRuleType.OI: + print("checking") + if self.contest.rule_type == ContestRuleType.OI and not self.contest.check_oi_permission(request.user): + return + print('here') + if request.user.is_authenticated(): profile = request.user.userprofile if self.contest.rule_type == ContestRuleType.ACM: problems_status = profile.acm_problems_status.get("contest_problems", {}) @@ -96,7 +95,7 @@ class ContestProblemAPI(APIView): except Problem.DoesNotExist: return self.error("Problem does not exist.") problem_data = ContestProblemSerializer(problem).data - self._add_problem_status(request, problem_data) + self._add_problem_status(request, [problem_data,]) return self.success(problem_data) contest_problems = Problem.objects.select_related("created_by").filter(contest=self.contest, visible=True) diff --git a/submission/views/oj.py b/submission/views/oj.py index c4bfe21..4cf6a8c 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -84,14 +84,13 @@ class SubmissionAPI(APIView): @login_required def put(self, request): try: - submission = Submission.objects.select_related("problem")\ - .get(id=request.data["id"], contest__isnull=True) + submission = Submission.objects.select_related("problem").get(id=request.data["id"]) except Submission.DoesNotExist: return self.error("Submission doesn't exist") if not submission.check_user_permission(request.user, check_share=False): return self.error("No permission to share the submission") if submission.contest and submission.contest.status == ContestStatus.CONTEST_UNDERWAY: - return self.error("Can not share submission during a contest going") + return self.error("Can not share submission now") submission.shared = request.data["shared"] submission.save(update_fields=["shared"]) return self.success() @@ -130,7 +129,7 @@ class ContestSubmissionListAPI(APIView): return self.error("Limit is needed") contest = self.contest - if contest.rule_type == ContestRuleType.OI and not contest.is_contest_admin(request.user): + if not contest.check_oi_permission(request.user): return self.error("No permission for OI contest submissions") submissions = Submission.objects.filter(contest_id=contest.id).select_related("problem__created_by") @@ -154,9 +153,11 @@ class ContestSubmissionListAPI(APIView): submissions = submissions.filter(create_time__gte=contest.start_time) # 封榜的时候只能看到自己的提交 - if not contest.real_time_rank and not contest.is_contest_admin(request.user): - submissions = submissions.filter(user_id=request.user.id) + if contest.rule_type == ContestRuleType.ACM: + if not contest.real_time_rank and not contest.is_contest_admin(request.user): + submissions = submissions.filter(user_id=request.user.id) data = self.paginate_data(request, submissions) data["results"] = SubmissionListSerializer(data["results"], many=True, user=request.user).data return self.success(data) + diff --git a/utils/constants.py b/utils/constants.py index be7057a..390d568 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -23,6 +23,6 @@ class ContestRuleType(Choices): class CacheKey: waiting_queue = "waiting_queue" - contest_rank_cache = "contest_rank_cache_" + contest_rank_cache = "contest_rank_cache" website_config = "website_config" option = "option" From d8bf33a12d68e688c2df8a066f72a71bc1bc96f0 Mon Sep 17 00:00:00 2001 From: zema1 Date: Sat, 21 Oct 2017 10:51:35 +0800 Subject: [PATCH 061/106] fix tests --- account/tests.py | 15 ++++---- conf/tests.py | 4 -- contest/tests.py | 4 +- oj/settings.py | 6 +++ problem/tests.py | 84 ++++++++++++++++++++++++++++-------------- problem/views/admin.py | 1 + problem/views/oj.py | 2 - submission/tests.py | 10 +++-- 8 files changed, 80 insertions(+), 46 deletions(-) diff --git a/account/tests.py b/account/tests.py index 906ef28..75515ce 100644 --- a/account/tests.py +++ b/account/tests.py @@ -11,6 +11,7 @@ from utils.shortcuts import rand_str from options.options import SysOptions from .models import AdminType, ProblemPermission, User +from utils.constants import ContestRuleType class PermissionDecoratorTest(APITestCase): @@ -134,7 +135,7 @@ class UserLoginAPITest(APITestCase): self.user.save() resp = self.client.post(self.login_url, data={"username": self.username, "password": self.password}) - self.assertDictEqual(resp.data, {"error": "error", "data": "Your account have been disabled"}) + self.assertDictEqual(resp.data, {"error": "error", "data": "Your account has been disabled"}) class CaptchaTest(APITestCase): @@ -159,7 +160,7 @@ class UserRegisterAPITest(CaptchaTest): def test_website_config_limit(self): SysOptions.allow_register = False resp = self.client.post(self.register_url, data=self.data) - self.assertDictEqual(resp.data, {"error": "error", "data": "Register have been disabled by admin"}) + self.assertDictEqual(resp.data, {"error": "error", "data": "Register function has been disabled by admin"}) def test_invalid_captcha(self): self.data["captcha"] = "****" @@ -220,7 +221,7 @@ class UserProfileAPITest(APITestCase): def test_get_profile_without_login(self): resp = self.client.get(self.url) - self.assertDictEqual(resp.data, {"error": None, "data": {}}) + self.assertDictEqual(resp.data, {"error": None, "data": None}) def test_get_profile(self): self.create_user("test", "test123") @@ -335,14 +336,14 @@ class ResetPasswordAPITest(CaptchaTest): def test_reset_password_with_invalid_token(self): self.data["token"] = "aaaaaaaaaaa" resp = self.client.post(self.url, data=self.data) - self.assertDictEqual(resp.data, {"error": "error", "data": "Token dose not exist"}) + self.assertDictEqual(resp.data, {"error": "error", "data": "Token does not exist"}) def test_reset_password_with_expired_token(self): user = User.objects.first() user.reset_password_token_expire_time = now() - timedelta(seconds=30) user.save() resp = self.client.post(self.url, data=self.data) - self.assertDictEqual(resp.data, {"error": "error", "data": "Token have expired"}) + self.assertDictEqual(resp.data, {"error": "error", "data": "Token has expired"}) class UserChangePasswordAPITest(CaptchaTest): @@ -473,14 +474,14 @@ class UserRankAPITest(APITestCase): profile2.save() def test_get_acm_rank(self): - resp = self.client.get(self.url, data={"rule": "acm"}) + resp = self.client.get(self.url, data={"rule": ContestRuleType.ACM}) self.assertSuccess(resp) data = resp.data["data"] self.assertEqual(data[0]["user"]["username"], "test1") self.assertEqual(data[1]["user"]["username"], "test2") def test_get_oi_rank(self): - resp = self.client.get(self.url, data={"rule": "oi"}) + resp = self.client.get(self.url, data={"rule": ContestRuleType.OI}) self.assertSuccess(resp) data = resp.data["data"] self.assertEqual(data[0]["user"]["username"], "test2") diff --git a/conf/tests.py b/conf/tests.py index eff8cfd..5906b07 100644 --- a/conf/tests.py +++ b/conf/tests.py @@ -4,7 +4,6 @@ from django.utils import timezone from options.options import SysOptions from utils.api.tests import APITestCase -from utils.cache import default_cache from utils.constants import CacheKey from .models import JudgeServer @@ -70,9 +69,6 @@ class WebsiteConfigAPITest(APITestCase): resp = self.client.get(url) self.assertSuccess(resp) - def tearDown(self): - default_cache.delete(CacheKey.website_config) - class JudgeServerHeartbeatTest(APITestCase): def setUp(self): diff --git a/contest/tests.py b/contest/tests.py index c481bec..df05df8 100644 --- a/contest/tests.py +++ b/contest/tests.py @@ -79,7 +79,7 @@ class ContestAPITest(APITestCase): self.create_user("test", "test123") url = self.reverse("contest_password_api") resp = self.client.post(url, {"contest_id": contest_id, "password": "error_password"}) - self.assertDictEqual(resp.data, {"error": "error", "data": "Password doesn't match."}) + self.assertDictEqual(resp.data, {"error": "error", "data": "Wrong password"}) resp = self.client.post(url, {"contest_id": contest_id, "password": DEFAULT_CONTEST_DATA["password"]}) self.assertSuccess(resp) @@ -89,7 +89,7 @@ class ContestAPITest(APITestCase): self.create_user("test", "test123") url = self.reverse("contest_access_api") resp = self.client.get(url + "?contest_id=" + str(contest_id)) - self.assertFalse(resp.data["data"]["Access"]) + self.assertFalse(resp.data["data"]["access"]) password_url = self.reverse("contest_password_api") resp = self.client.post(password_url, {"contest_id": contest_id, "password": DEFAULT_CONTEST_DATA["password"]}) diff --git a/oj/settings.py b/oj/settings.py index ac972c7..950c426 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -163,6 +163,12 @@ LOGGING = { } }, } +REST_FRAMEWORK = { + 'TEST_REQUEST_DEFAULT_FORMAT': 'json', + 'DEFAULT_RENDERER_CLASSES': ( + 'rest_framework.renderers.JSONRenderer', + ) +} REDIS_URL = "redis://127.0.0.1:6379" diff --git a/problem/tests.py b/problem/tests.py index 4e081ff..7efa668 100644 --- a/problem/tests.py +++ b/problem/tests.py @@ -1,4 +1,5 @@ import copy +import hashlib import os import shutil from datetime import timedelta @@ -9,6 +10,7 @@ from django.conf import settings from utils.api.tests import APITestCase from .models import ProblemTag +from .models import Problem, ProblemRuleType from .views.admin import TestCaseUploadAPI from contest.models import Contest from contest.tests import DEFAULT_CONTEST_DATA @@ -23,6 +25,40 @@ DEFAULT_PROBLEM_DATA = {"_id": "A-110", "title": "test", "description": "

test "input_size": 0, "score": 0}], "rule_type": "ACM", "hint": "

test

", "source": "test"} +class ProblemCreateTestBase(APITestCase): + @staticmethod + def add_problem(problem_data, created_by): + data = copy.deepcopy(problem_data) + if data["spj"]: + if not data["spj_language"] or not data["spj_code"]: + raise ValueError("Invalid spj") + data["spj_version"] = hashlib.md5((data["spj_language"] + ":" + data["spj_code"]).encode("utf-8")).hexdigest() + else: + 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: + raise ValueError("invalid score") + else: + total_score += item["score"] + data["total_score"] = total_score + data["created_by"] = created_by + tags = data.pop("tags") + + data["languages"] = list(data["languages"]) + + problem = Problem.objects.create(**data) + + for item in tags: + try: + tag = ProblemTag.objects.get(name=item) + except ProblemTag.DoesNotExist: + tag = ProblemTag.objects.create(name=item) + problem.tags.add(tag) + return problem + class ProblemTagListAPITest(APITestCase): def test_get_tag_list(self): @@ -96,7 +132,7 @@ class ProblemAdminAPITest(APITestCase): def setUp(self): self.url = self.reverse("problem_admin_api") self.create_super_admin() - self.data = DEFAULT_PROBLEM_DATA + self.data = copy.deepcopy(DEFAULT_PROBLEM_DATA) def test_create_problem(self): resp = self.client.post(self.url, data=self.data) @@ -138,23 +174,19 @@ class ProblemAdminAPITest(APITestCase): self.assertSuccess(resp) -class ProblemAPITest(APITestCase): +class ProblemAPITest(ProblemCreateTestBase): def setUp(self): self.url = self.reverse("problem_api") - self.create_admin() - - def create_problem(self): - url = self.reverse("problem_admin_api") - return self.client.post(url, data=DEFAULT_PROBLEM_DATA) + admin = self.create_admin(login=False) + self.problem = self.add_problem(DEFAULT_PROBLEM_DATA, admin) + self.create_user("test", "test123") def test_get_problem_list(self): - self.create_problem() resp = self.client.get(f"{self.url}?limit=10") self.assertSuccess(resp) def get_one_problem(self): - problem_id = self.create_problem().data["data"]["_id"] - resp = self.client.get(self.url + "?id=" + str(problem_id)) + resp = self.client.get(self.url + "?id=" + self.problem._id) self.assertSuccess(resp) @@ -169,51 +201,49 @@ class ContestProblemAdminTest(APITestCase): def test_create_contest_problem(self): contest = self.create_contest() - data = DEFAULT_PROBLEM_DATA + data = copy.deepcopy(DEFAULT_PROBLEM_DATA) data["contest_id"] = contest.data["data"]["id"] resp = self.client.post(self.url, data=data) self.assertSuccess(resp) - return resp + return contest, resp def test_get_contest_problem(self): - contest = self.test_create_contest_problem() + contest, contest_problem = self.test_create_contest_problem() contest_id = contest.data["data"]["id"] resp = self.client.get(self.url + "?contest_id=" + str(contest_id)) self.assertSuccess(resp) self.assertEqual(len(resp.data["data"]), 1) def test_get_one_contest_problem(self): - contest = self.test_create_contest_problem() + contest, contest_problem = self.test_create_contest_problem() contest_id = contest.data["data"]["id"] - resp = self.client.get(self.url + "?id=" + str(contest_id)) + problem_id = contest_problem.data["data"]["id"] + resp = self.client.get(f"{self.url}?contest_id={contest_id}&id={problem_id}") self.assertSuccess(resp) -class ContestProblemTest(APITestCase): +class ContestProblemTest(ProblemCreateTestBase): def setUp(self): - self.url = self.reverse("contest_problem_api") - self.create_admin() - + admin = self.create_admin() url = self.reverse("contest_admin_api") contest_data = copy.deepcopy(DEFAULT_CONTEST_DATA) contest_data["password"] = "" contest_data["start_time"] = contest_data["start_time"] + timedelta(hours=1) self.contest = self.client.post(url, data=contest_data).data["data"] + self.problem = self.add_problem(DEFAULT_PROBLEM_DATA, admin) + self.problem.contest_id = self.contest["id"] + self.problem.save() + self.url = self.reverse("contest_problem_api") - problem_data = copy.deepcopy(DEFAULT_PROBLEM_DATA) - problem_data["contest"] = self.contest["id"] - url = self.reverse("contest_problem_admin_api") - self.problem = self.client.post(url, problem_data).data["data"] - - def test_get_contest_problem_list(self): + def test_admin_get_contest_problem_list(self): contest_id = self.contest["id"] resp = self.client.get(self.url + "?contest_id=" + str(contest_id)) self.assertSuccess(resp) self.assertEqual(len(resp.data["data"]), 1) - def test_get_one_contest_problem(self): + def test_admin_get_one_contest_problem(self): contest_id = self.contest["id"] - problem_id = self.problem["_id"] + problem_id = self.problem._id resp = self.client.get("{}?contest_id={}&problem_id={}".format(self.url, contest_id, problem_id)) self.assertSuccess(resp) diff --git a/problem/views/admin.py b/problem/views/admin.py index 572e2fb..00f46c4 100644 --- a/problem/views/admin.py +++ b/problem/views/admin.py @@ -223,6 +223,7 @@ class ProblemAPI(APIView): data["total_score"] = total_score # todo check filename and score info tags = data.pop("tags") + data["languages"] = list(data["languages"]) for k, v in data.items(): setattr(problem, k, v) diff --git a/problem/views/oj.py b/problem/views/oj.py index 3410ae1..330e933 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -71,10 +71,8 @@ class ProblemAPI(APIView): class ContestProblemAPI(APIView): def _add_problem_status(self, request, queryset_values): - print("checking") if self.contest.rule_type == ContestRuleType.OI and not self.contest.check_oi_permission(request.user): return - print('here') if request.user.is_authenticated(): profile = request.user.userprofile if self.contest.rule_type == ContestRuleType.ACM: diff --git a/submission/tests.py b/submission/tests.py index c734882..fdcd12e 100644 --- a/submission/tests.py +++ b/submission/tests.py @@ -32,14 +32,16 @@ class SubmissionPrepare(APITestCase): def _create_problem_and_submission(self): user = self.create_admin("test", "test123", login=False) problem_data = deepcopy(DEFAULT_PROBLEM_DATA) - problem_data.pop("tags") + tags = problem_data.pop("tags") problem_data["created_by"] = user self.problem = Problem.objects.create(**problem_data) - for tag in DEFAULT_PROBLEM_DATA["tags"]: + for tag in tags: tag = ProblemTag.objects.create(name=tag) self.problem.tags.add(tag) self.problem.save() - self.submission = Submission.objects.create(**DEFAULT_SUBMISSION_DATA) + self.submission_data = deepcopy(DEFAULT_SUBMISSION_DATA) + self.submission_data["problem_id"] = self.problem.id + self.submission = Submission.objects.create(**self.submission_data) class SubmissionListTest(SubmissionPrepare): @@ -61,6 +63,6 @@ class SubmissionAPITest(SubmissionPrepare): self.url = self.reverse("submission_api") def test_create_submission(self, judge_task): - resp = self.client.post(self.url, DEFAULT_SUBMISSION_DATA) + resp = self.client.post(self.url, self.submission_data) self.assertSuccess(resp) judge_task.assert_called() From 5d03ec5aabd0528dc12715e1f0b14b5f7d7c5091 Mon Sep 17 00:00:00 2001 From: zema1 Date: Sat, 21 Oct 2017 20:39:39 +0800 Subject: [PATCH 062/106] add pick one api --- account/middleware.py | 2 +- conf/tests.py | 1 - contest/models.py | 2 +- deploy/Dockerfile | 2 +- deploy/requirements.txt | 1 - judge/dispatcher.py | 100 +++++++++++------- problem/migrations/0009_auto_20171011_1214.py | 4 + problem/models.py | 1 + problem/tests.py | 8 +- problem/urls/oj.py | 3 +- problem/views/oj.py | 17 ++- submission/models.py | 1 - submission/views/oj.py | 9 +- 13 files changed, 96 insertions(+), 55 deletions(-) diff --git a/account/middleware.py b/account/middleware.py index b674346..1ef78cc 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -10,7 +10,7 @@ class SessionRecordMiddleware(MiddlewareMixin): if request.user.is_authenticated(): session = request.session session["user_agent"] = request.META.get("HTTP_USER_AGENT", "") - session["ip"] = request.META.get("HTTP_X_REAL_IP", "UNKNOWN IP") + session["ip"] = request.META.get("HTTP_X_REAL_IP", request.META.get("REMOTE_ADDR")) session["last_activity"] = now() user_sessions = request.user.session_keys if session.session_key not in user_sessions: diff --git a/conf/tests.py b/conf/tests.py index 5906b07..cd83444 100644 --- a/conf/tests.py +++ b/conf/tests.py @@ -4,7 +4,6 @@ from django.utils import timezone from options.options import SysOptions from utils.api.tests import APITestCase -from utils.constants import CacheKey from .models import JudgeServer diff --git a/contest/models.py b/contest/models.py index eb1c88a..08a3bab 100644 --- a/contest/models.py +++ b/contest/models.py @@ -46,7 +46,7 @@ class Contest(models.Model): return user.is_authenticated() and (self.created_by == user or user.admin_type == AdminType.SUPER_ADMIN) def check_oi_permission(self, user): - if self.status != ContestStatus.CONTEST_ENDED and self.real_time_rank == False: + if self.status != ContestStatus.CONTEST_ENDED and not self.real_time_rank: if self.is_contest_admin(user): return True else: diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 6f3d17d..ce70ae4 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.5 +FROM python:3.6 ADD requirements.txt /tmp RUN pip install -r /tmp/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple WORKDIR /app diff --git a/deploy/requirements.txt b/deploy/requirements.txt index 1d2032c..f6bb6fe 100644 --- a/deploy/requirements.txt +++ b/deploy/requirements.txt @@ -1,7 +1,6 @@ django==1.11.4 djangorestframework==3.4.0 pillow -jsonfield otpauth flake8-quotes pytz diff --git a/judge/dispatcher.py b/judge/dispatcher.py index 829a43d..199e627 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -136,9 +136,11 @@ class JudgeDispatcher(object): self.submission.save() self.release_judge_server(server.id) - self.update_problem_status() if self.contest_id: + self.update_contest_problem_status() self.update_contest_rank() + else: + self.update_problem_status() # 至此判题结束,尝试处理任务队列中剩余的任务 process_pending_task() @@ -150,65 +152,91 @@ class JudgeDispatcher(object): return self._request(urljoin(service_url, "compile_spj"), data=data) def update_problem_status(self): - if self.contest_id and self.contest.status != ContestStatus.CONTEST_UNDERWAY: - logger.info("Contest debug mode, id: " + str(self.contest_id) + ", submission id: " + self.submission.id) - return + result = str(self.submission.result) + problem_id = str(self.problem.id) with transaction.atomic(): - # prepare problem and user_profile + # update problem status problem = Problem.objects.select_for_update().get(contest_id=self.contest_id, id=self.problem.id) - user = User.objects.select_for_update().select_for_update("userprofile").get(id=self.submission.user_id) - user_profile = user.userprofile - if self.contest_id: - key = "contest_problems" - else: - key = "problems" - acm_problems_status = user_profile.acm_problems_status.get(key, {}) - oi_problems_status = user_profile.oi_problems_status.get(key, {}) - problem_id = str(self.problem.id) - problem_info = problem.statistic_info - - # update problem info - result = str(self.submission.result) - problem_info[result] = problem_info.get(result, 0) + 1 - problem.statistic_info = problem_info - - # update submission and accepted number counter problem.submission_number += 1 if self.submission.result == JudgeStatus.ACCEPTED: problem.accepted_number += 1 - # submission in a contest will not be counted in user profile - if not self.contest_id: + problem_info = problem.statistic_info + problem_info[result] = problem_info.get(result, 0) + 1 + problem.save(update_fields=["accepted_number", "submission_number", "statistic_info"]) + + # update_userprofile + user = User.objects.select_for_update().get(id=self.submission.user_id) + user_profile = user.userprofile + if problem.rule_type == ProblemRuleType.ACM: user_profile.submission_number += 1 if self.submission.result == JudgeStatus.ACCEPTED: user_profile.accepted_number += 1 - - if self.problem.rule_type == ProblemRuleType.ACM: - # update user_profile + 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} - # skip if the problem has been accepted elif acm_problems_status[problem_id]["status"] != JudgeStatus.ACCEPTED: acm_problems_status[problem_id]["status"] = self.submission.result - user_profile.acm_problems_status[key] = acm_problems_status + user_profile.acm_problems_status["problems"] = acm_problems_status + user_profile.save(update_fields=["submission_number", "accepted_number", "acm_problems_status"]) else: - # update user_profile + oi_problems_status = user_profile.oi_problems_status.get("problems", {}) score = self.submission.statistic_info["score"] if problem_id not in oi_problems_status: user_profile.add_score(score) oi_problems_status[problem_id] = {"status": self.submission.result, - "_id": self.problem._id, - "score": score} + "_id": self.problem._id, + "score": score} else: # 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"]) + 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[key] = oi_problems_status + user_profile.oi_problems_status["problems"] = oi_problems_status + user_profile.save(update_fields=["oi_problems_status"]) + def update_contest_problem_status(self): + if self.contest_id and self.contest.status != ContestStatus.CONTEST_UNDERWAY: + logger.info("Contest debug mode, id: " + str(self.contest_id) + ", submission id: " + self.submission.id) + return + with transaction.atomic(): + user = User.objects.select_for_update().select_related("userprofile").get(id=self.submission.user_id) + user_profile = user.userprofile + problem_id = str(self.problem.id) + if self.contest.rule_type == ContestRuleType.ACM: + contest_problems_status = user_profile.acm_problems_status.get("contest_problems", {}) + if problem_id not in contest_problems_status: + contest_problems_status[problem_id] = {"status": self.submission.result, "_id": self.problem._id} + elif contest_problems_status[problem_id]["status"] != JudgeStatus.ACCEPTED: + contest_problems_status[problem_id]["status"] = self.submission.result + else: + # 如果已AC, 直接跳过 不计入任何计数器 + return + user_profile.acm_problems_status["contest_problems"] = contest_problems_status + user_profile.save(update_fields=["acm_problems_status"]) + + elif self.contest.rule_type == ContestRuleType.OI: + contest_problems_status = user_profile.oi_problems_status.get("contest_problems", {}) + score = self.submission.statistic_info["score"] + if problem_id not in contest_problems_status: + contest_problems_status[problem_id] = {"status": self.submission.result, + "_id": self.problem._id, + "score": score} + else: + contest_problems_status[problem_id]["score"] = score + contest_problems_status[problem_id]["status"] = self.submission.result + user_profile.oi_problems_status["contest_problems"] = contest_problems_status + user_profile.save(update_fields=["oi_problems_status"]) + + problem = Problem.objects.select_for_update().get(contest_id=self.contest_id, id=self.problem.id) + result = str(self.submission.result) + problem_info = problem.statistic_info + problem_info[result] = problem_info.get(result, 0) + 1 + problem.submission_number += 1 + if self.submission.result == JudgeStatus.ACCEPTED: + problem.accepted_number += 1 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"]) def update_contest_rank(self): if self.contest_id and self.contest.status != ContestStatus.CONTEST_UNDERWAY: diff --git a/problem/migrations/0009_auto_20171011_1214.py b/problem/migrations/0009_auto_20171011_1214.py index 7073b8f..e219ff2 100644 --- a/problem/migrations/0009_auto_20171011_1214.py +++ b/problem/migrations/0009_auto_20171011_1214.py @@ -38,4 +38,8 @@ class Migration(migrations.Migration): name='test_case_score', field=django.contrib.postgres.fields.jsonb.JSONField(), ), + migrations.AlterModelOptions( + name='problem', + options={'ordering': ('create_time',)}, + ), ] diff --git a/problem/models.py b/problem/models.py index 9053793..6fb1c79 100644 --- a/problem/models.py +++ b/problem/models.py @@ -71,6 +71,7 @@ class Problem(models.Model): class Meta: db_table = "problem" unique_together = (("_id", "contest"),) + ordering = ("create_time",) def add_submission_number(self): self.submission_number = models.F("submission_number") + 1 diff --git a/problem/tests.py b/problem/tests.py index 7efa668..072abd4 100644 --- a/problem/tests.py +++ b/problem/tests.py @@ -25,6 +25,7 @@ DEFAULT_PROBLEM_DATA = {"_id": "A-110", "title": "test", "description": "

test "input_size": 0, "score": 0}], "rule_type": "ACM", "hint": "

test

", "source": "test"} + class ProblemCreateTestBase(APITestCase): @staticmethod def add_problem(problem_data, created_by): @@ -32,7 +33,8 @@ class ProblemCreateTestBase(APITestCase): if data["spj"]: if not data["spj_language"] or not data["spj_code"]: raise ValueError("Invalid spj") - data["spj_version"] = hashlib.md5((data["spj_language"] + ":" + data["spj_code"]).encode("utf-8")).hexdigest() + data["spj_version"] = hashlib.md5( + (data["spj_language"] + ":" + data["spj_code"]).encode("utf-8")).hexdigest() else: data["spj_language"] = None data["spj_code"] = None @@ -215,7 +217,7 @@ class ContestProblemAdminTest(APITestCase): self.assertEqual(len(resp.data["data"]), 1) def test_get_one_contest_problem(self): - contest, contest_problem = self.test_create_contest_problem() + contest, contest_problem = self.test_create_contest_problem() contest_id = contest.data["data"]["id"] problem_id = contest_problem.data["data"]["id"] resp = self.client.get(f"{self.url}?contest_id={contest_id}&id={problem_id}") @@ -233,7 +235,7 @@ class ContestProblemTest(ProblemCreateTestBase): self.problem = self.add_problem(DEFAULT_PROBLEM_DATA, admin) self.problem.contest_id = self.contest["id"] self.problem.save() - self.url = self.reverse("contest_problem_api") + self.url = self.reverse("contest_problem_api") def test_admin_get_contest_problem_list(self): contest_id = self.contest["id"] diff --git a/problem/urls/oj.py b/problem/urls/oj.py index c50cafc..f7cd3ae 100644 --- a/problem/urls/oj.py +++ b/problem/urls/oj.py @@ -1,9 +1,10 @@ from django.conf.urls import url -from ..views.oj import ProblemTagAPI, ProblemAPI, ContestProblemAPI +from ..views.oj import ProblemTagAPI, ProblemAPI, ContestProblemAPI, PickOneAPI urlpatterns = [ url(r"^problem/tags/?$", ProblemTagAPI.as_view(), name="problem_tag_list_api"), url(r"^problem/?$", ProblemAPI.as_view(), name="problem_api"), + url(r"^pickone/?$", PickOneAPI.as_view(), name="pick_one_api"), url(r"^contest/problem/?$", ContestProblemAPI.as_view(), name="contest_problem_api"), ] diff --git a/problem/views/oj.py b/problem/views/oj.py index 330e933..2f54ed5 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -1,3 +1,4 @@ +import random from django.db.models import Q from utils.api import APIView from account.decorators import check_contest_permission @@ -12,6 +13,15 @@ class ProblemTagAPI(APIView): return self.success(TagSerializer(ProblemTag.objects.all(), many=True).data) +class PickOneAPI(APIView): + def get(self, request): + problems = Problem.objects.filter(contest_id__isnull=True, visible=True) + count = problems.count() + if count == 0: + return self.error("No problem to pick") + return self.success(problems[random.randint(0, count - 1)]._id) + + class ProblemAPI(APIView): @staticmethod def _add_problem_status(request, queryset_values): @@ -24,7 +34,7 @@ class ProblemAPI(APIView): if results is not None: problems = results else: - problems = [queryset_values,] + problems = [queryset_values, ] for problem in problems: if problem["rule_type"] == ProblemRuleType.ACM: problem["my_status"] = acm_problems_status.get(str(problem["id"]), {}).get("status") @@ -36,7 +46,7 @@ class ProblemAPI(APIView): problem_id = request.GET.get("problem_id") if problem_id: try: - problem = Problem.objects.select_related("created_by")\ + problem = Problem.objects.select_related("created_by") \ .get(_id=problem_id, contest_id__isnull=True, visible=True) problem_data = ProblemSerializer(problem).data self._add_problem_status(request, problem_data) @@ -93,9 +103,8 @@ class ContestProblemAPI(APIView): except Problem.DoesNotExist: return self.error("Problem does not exist.") problem_data = ContestProblemSerializer(problem).data - self._add_problem_status(request, [problem_data,]) + self._add_problem_status(request, [problem_data, ]) return self.success(problem_data) - contest_problems = Problem.objects.select_related("created_by").filter(contest=self.contest, visible=True) # 根据profile, 为做过的题目添加标记 data = ContestProblemSerializer(contest_problems, many=True).data diff --git a/submission/models.py b/submission/models.py index 7f046a0..fe6a991 100644 --- a/submission/models.py +++ b/submission/models.py @@ -1,6 +1,5 @@ from django.db import models from utils.models import JSONField -from account.models import AdminType from problem.models import Problem from contest.models import Contest diff --git a/submission/views/oj.py b/submission/views/oj.py index 4cf6a8c..2430a20 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -1,6 +1,6 @@ from account.decorators import login_required, check_contest_permission -from judge.tasks import judge_task -# from judge.dispatcher import JudgeDispatcher +# from judge.tasks import judge_task +from judge.dispatcher import JudgeDispatcher from problem.models import Problem, ProblemRuleType from contest.models import Contest, ContestStatus, ContestRuleType from utils.api import APIView, validate_serializer @@ -39,8 +39,8 @@ def _submit(response, user, problem_id, language, code, contest_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}) @@ -160,4 +160,3 @@ class ContestSubmissionListAPI(APIView): data = self.paginate_data(request, submissions) data["results"] = SubmissionListSerializer(data["results"], many=True, user=request.user).data return self.success(data) - From 1b0952cd0d6e32cdf7e86535793546a65e0ba035 Mon Sep 17 00:00:00 2001 From: zema1 Date: Mon, 23 Oct 2017 10:47:26 +0800 Subject: [PATCH 063/106] update settings --- deploy/Dockerfile | 13 ++- deploy/nginx.conf | 110 ++++++++++++++++++++++ deploy/requirements.txt | 1 + deploy/run.sh | 22 +++++ deploy/supervisor.conf | 45 +++++++++ oj/{local_settings.py => dev_settings.py} | 10 +- oj/production_settings.py | 28 ++++++ oj/server_settings.py | 37 -------- oj/settings.py | 41 +++----- submission/views/oj.py | 8 +- 10 files changed, 233 insertions(+), 82 deletions(-) create mode 100644 deploy/nginx.conf create mode 100644 deploy/run.sh create mode 100644 deploy/supervisor.conf rename oj/{local_settings.py => dev_settings.py} (86%) create mode 100644 oj/production_settings.py delete mode 100644 oj/server_settings.py diff --git a/deploy/Dockerfile b/deploy/Dockerfile index ce70ae4..4b8bfce 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -1,5 +1,10 @@ -FROM python:3.6 +FROM python:3.6-alpine3.6 + ADD requirements.txt /tmp -RUN pip install -r /tmp/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple -WORKDIR /app -CMD python manage.py runserver 0.0.0.0:8085 +RUN apk add --no-cache --virtual .build-deps build-base jpeg-dev zlib-dev postgresql-dev && \ + pip install -r /tmp/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple && \ + apk del .build-deps --purge + +Volume ["/app"] + +CMD sh /app/deploy/run.sh diff --git a/deploy/nginx.conf b/deploy/nginx.conf new file mode 100644 index 0000000..8209807 --- /dev/null +++ b/deploy/nginx.conf @@ -0,0 +1,110 @@ +user nginx; + +# Set number of worker processes automatically based on number of CPU cores. +worker_processes auto; + +# Enables the use of JIT for regular expressions to speed-up their processing. +pcre_jit on; + +# Configures default error logger. +error_log /dev/stderr warn; + +# use supervisor to monitor +daemon off; + +# set pid path +pid /tmp/nginx.pid; + +# Includes files with directives to load dynamic modules. +include /etc/nginx/modules/*.conf; + + +events { + # The maximum number of simultaneous connections that can be opened by + # a worker process. + worker_connections 1024; +} + +http { + # Includes mapping of file name extensions to MIME types of responses + # and defines the default type. + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Name servers used to resolve names of upstream servers into addresses. + # It's also needed when using tcpsocket and udpsocket in Lua modules. + #resolver 208.67.222.222 208.67.220.220; + + # Don't tell nginx version to clients. + server_tokens off; + + # Specifies the maximum accepted body size of a client request, as + # indicated by the request header Content-Length. If the stated content + # length is greater than this size, then the client receives the HTTP + # error code 413. Set to 0 to disable. + client_max_body_size 50m; + + # Timeout for keep-alive connections. Server will close connections after + # this time. + keepalive_timeout 65; + + # Sendfile copies data between one FD and other from within the kernel, + # which is more efficient than read() + write(). + sendfile on; + + # Don't buffer data-sends (disable Nagle algorithm). + # Good for sending frequent small bursts of data in real time. + tcp_nodelay on; + + # Causes nginx to attempt to send its HTTP response head in one packet, + # instead of using partial frames. + #tcp_nopush on; + + + # Path of the file with Diffie-Hellman parameters for EDH ciphers. + #ssl_dhparam /etc/ssl/nginx/dh2048.pem; + + # Specifies that our cipher suits should be preferred over client ciphers. + ssl_prefer_server_ciphers on; + + # Enables a shared SSL cache with size that can hold around 8000 sessions. + ssl_session_cache shared:SSL:2m; + + + # Enable gzipping of responses. + gzip on; + gzip_types application/javascript text/css; + + # Set the Vary HTTP header as defined in the RFC 2616. + gzip_vary on; + + # Enable checking the existence of precompressed files. + #gzip_static on; + + + # Specifies the main log format. + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + # Sets the path, format, and configuration for a buffered log write. + # access_log /var/log/nginx/access.log main; + access_log off + + server { + listen 80 default_server; + server_name _; + keetalive_timeout 5; + + location /static/avatar { + expires max; + alias /app/static/avatar; + } + location / { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; + + proxy_pass http://127.0.0.1:8080; + } + } +} diff --git a/deploy/requirements.txt b/deploy/requirements.txt index f6bb6fe..70aa2e5 100644 --- a/deploy/requirements.txt +++ b/deploy/requirements.txt @@ -12,3 +12,4 @@ qrcode flake8-coding requests django-redis +psycopg2 diff --git a/deploy/run.sh b/deploy/run.sh new file mode 100644 index 0000000..fbc4b3c --- /dev/null +++ b/deploy/run.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +BASE=/app + +if [ ! -f "$BASE/custom_settings.py" ]; then + echo SECRET_KEY=\"$(cat /dev/urandom | head -1 | md5sum | head -c 32)\" >> /app/oj/custom_settings.py +fi + +if [ ! -d "$BASE/log" ]; then + mkdir -p $BASE/log +fi + +cd $BASE +find . -name "*.pyc" -delete + +python manage.py migrate +if [ $? -ne 0 ]; then + echo "Can't start server" + exit 1 +fi +python manage.py initadmin +python manage.py runserver 0.0.0.0:8080 diff --git a/deploy/supervisor.conf b/deploy/supervisor.conf new file mode 100644 index 0000000..55f5313 --- /dev/null +++ b/deploy/supervisor.conf @@ -0,0 +1,45 @@ +[supervisord] +logfile=/app/log/supervisord.log +logfile_maxbytes=10MB +logfile_backups=10 +loglevel=info +pidfile=/tmp/supervisord.pid +nodaemon=true +childlogdir=/app/log/ + +[supervisorctl] +serverurl=unix:///tmp/supervisor.sock + +[program:gunicorn] +command=gunicorn oj.wsgi --user nobody -b 127.0.0.1:8080 --reload -w `grep -c ^processor /proc/cpuinfo` +directory=/app/ +stdout_logfile=/app/log/gunicorn.log +stderr_logfile=/app/log/gunicorn.log +autostart=true +autorestart=true +startsecs=5 +stopwaitsecs = 5 +killasgroup=true + +[program:task_queue] +command=celery -A oj worker -l warning +directory=/app/ +user=nobody +stdout_logfile=/app/log/celery.log +stderr_logfile=/app/log/celery.log +autostart=true +autorestart=true +startsecs=5 +stopwaitsecs = 5 +killasgroup=true + +[program:nginx] +command=nginx -c /app/deploy/nginx.conf +directory=/app/ +stdout_logfile=/app/log/nginx.log +stderr_logfile=/app/log/nginx.log +autostart=true +autorestart=true +startsecs=5 +stopwaitsecs = 5 +killasgroup=true \ No newline at end of file diff --git a/oj/local_settings.py b/oj/dev_settings.py similarity index 86% rename from oj/local_settings.py rename to oj/dev_settings.py index bbe2398..cb40938 100644 --- a/oj/local_settings.py +++ b/oj/dev_settings.py @@ -14,14 +14,12 @@ DATABASES = { } } - -# For celery -REDIS_QUEUE = { +REDIS_CONF = { "host": "127.0.0.1", - "port": 6379, - "db": 4 + "port": "6379" } + DEBUG = True ALLOWED_HOSTS = ["*"] @@ -31,5 +29,3 @@ TEST_CASE_DIR = "/tmp" STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static"), ] - -LOG_PATH = "log/" diff --git a/oj/production_settings.py b/oj/production_settings.py new file mode 100644 index 0000000..7d89276 --- /dev/null +++ b/oj/production_settings.py @@ -0,0 +1,28 @@ +import os + + +def get_env(name, default=""): + return os.environ.get(name, default) + + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'HOST': get_env("POSTGRESQL_HOST", "postgresql"), + 'PORT': get_env("POSTGRESQL_PORT", "5433"), + 'NAME': get_env("POSTGRESQL_DBNAME"), + 'USER': get_env("POSTGRESQL_USER"), + 'PASSWORD': get_env("POSTGRESQL_PASSWORD") + } +} + +REDIS_CONF = { + "host": get_env("REDIS_HOST", "redis"), + "port": get_env("REDIS_PORT", "6379") +} + +DEBUG = False + +ALLOWED_HOSTS = ['*'] + +TEST_CASE_DIR = "/test_case" diff --git a/oj/server_settings.py b/oj/server_settings.py deleted file mode 100644 index 7434ee7..0000000 --- a/oj/server_settings.py +++ /dev/null @@ -1,37 +0,0 @@ -# coding=utf-8 -import os - -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': "oj", - 'CONN_MAX_AGE': 0.1, - 'HOST': os.environ["MYSQL_PORT_3306_TCP_ADDR"], - 'PORT': 3306, - 'USER': os.environ["MYSQL_ENV_MYSQL_USER"], - 'PASSWORD': os.environ["MYSQL_ENV_MYSQL_ROOT_PASSWORD"] - } -} - -REDIS_CACHE = { - "host": os.environ["REDIS_PORT_6379_TCP_ADDR"], - "port": 6379, - "db": 1 -} - -REDIS_QUEUE = { - "host": os.environ["REDIS_PORT_6379_TCP_ADDR"], - "port": 6379, - "db": 2 -} - -DEBUG = False - -ALLOWED_HOSTS = ['*'] - - -TEST_CASE_DIR = "/test_case" - -LOG_PATH = "log/" diff --git a/oj/settings.py b/oj/settings.py index 950c426..a5fe7db 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -1,8 +1,7 @@ -# coding=utf-8 """ Django settings for oj project. -Generated by 'django-admin startproject' using Django 1.8. +Generated by 'django-admin startproject' using Django 1.11. For more information on this file, see https://docs.djangoproject.com/en/1.8/topics/settings/ @@ -10,27 +9,19 @@ https://docs.djangoproject.com/en/1.8/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.8/ref/settings/ """ -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os from .custom_settings import * -# 判断运行环境 -ENV = os.environ.get("oj_env", "local") - -if ENV == "local": - from .local_settings import * -elif ENV == "server": - from .server_settings import * +if os.environ.get("NODE_ENV") == "production": + from .production_settings import * +else: + from .dev_settings import * BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ - -# Application definition - +# Applications INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.sessions', @@ -117,6 +108,7 @@ USE_TZ = True STATIC_URL = '/static/' AUTH_USER_MODEL = 'account.User' +LOG_PATH = "log/" LOGGING = { 'version': 1, @@ -170,9 +162,7 @@ REST_FRAMEWORK = { ) } - -REDIS_URL = "redis://127.0.0.1:6379" - +REDIS_URL = "redis://%s:%s" % (REDIS_CONF["host"], REDIS_CONF["port"]) def redis_config(db): def make_key(key, key_prefix, version): @@ -191,23 +181,14 @@ CACHES = { } -CELERY_RESULT_BACKEND = CELERY_BROKER_URL = f"{REDIS_URL}/2" -CELERY_TASK_SOFT_TIME_LIMIT = CELERY_TASK_TIME_LIMIT = 180 SESSION_ENGINE = "django.contrib.sessions.backends.cache" SESSION_CACHE_ALIAS = "default" -# For celery -REDIS_QUEUE = { - "host": "127.0.0.1", - "port": 6379, - "db": 4 -} - - -# for celery -BROKER_URL = 'redis://%s:%s/%s' % (REDIS_QUEUE["host"], str(REDIS_QUEUE["port"]), str(REDIS_QUEUE["db"])) +CELERY_RESULT_BACKEND = f"{REDIS_URL}/2" +BROKER_URL = f"{REDIS_URL}/3" +CELERY_TASK_SOFT_TIME_LIMIT = CELERY_TASK_TIME_LIMIT = 180 CELERY_ACCEPT_CONTENT = ["json"] CELERY_TASK_SERIALIZER = "json" diff --git a/submission/views/oj.py b/submission/views/oj.py index 2430a20..613bd38 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -1,6 +1,6 @@ from account.decorators import login_required, check_contest_permission -# from judge.tasks import judge_task -from judge.dispatcher import JudgeDispatcher +from judge.tasks import judge_task +# from judge.dispatcher import JudgeDispatcher from problem.models import Problem, ProblemRuleType from contest.models import Contest, ContestStatus, ContestRuleType from utils.api import APIView, validate_serializer @@ -39,8 +39,8 @@ def _submit(response, user, problem_id, language, code, contest_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}) From e8841eae820cf62fc8241af848972d04c9173312 Mon Sep 17 00:00:00 2001 From: zema1 Date: Mon, 23 Oct 2017 20:59:44 +0800 Subject: [PATCH 064/106] add dockerfiles --- account/templates/reset_password_email.html | 13 +++++----- deploy/Dockerfile | 13 ++++++---- deploy/nginx.conf | 5 ++-- deploy/requirements.txt | 2 ++ deploy/run.sh | 24 ++++++++++++++++--- deploy/supervisor.conf | 6 ++--- oj/dev_settings.py | 2 ++ oj/production_settings.py | 13 +++++----- oj/settings.py | 3 +-- options/migrations/0001_initial.py | 6 ++--- options/migrations/0002_auto_20171011_1214.py | 21 ---------------- reset_password_email.html | 0 utils/management/commands/initadmin.py | 24 +++++++++---------- 13 files changed, 67 insertions(+), 65 deletions(-) delete mode 100644 options/migrations/0002_auto_20171011_1214.py delete mode 100644 reset_password_email.html diff --git a/account/templates/reset_password_email.html b/account/templates/reset_password_email.html index 5a0b591..b2f76f8 100644 --- a/account/templates/reset_password_email.html +++ b/account/templates/reset_password_email.html @@ -8,7 +8,7 @@ - {{ website_name }} 登录信息找回 + {{ website_name }} @@ -32,18 +32,18 @@ - 您刚刚在 {{ website_name }} 申请了找回登录信息服务。 + We received a request to reset your password for {{ website_name }}. - 请在30分钟内点击下面链接设置您的新密码: + You can use the following link to reset your password in 20 minutes. 重置密码 + style="color: rgb(255,255,255);text-decoration: none;display: block;min-height: 39px;width: 158px;line-height: 39px;background-color:rgb(80,165,230);font-size:20px;text-align:center;">Reset Password @@ -51,7 +51,7 @@ - 如果上面的链接点击无效,请复制以下链接至浏览器的地址栏直接打开。 + If the button above doesn't work, please copy the following link to your browser and press enter. @@ -63,8 +63,7 @@ - 如果您没有提出过该申请,请忽略此邮件。有可能是其他用户误填了您的邮件地址,我们不会对你的帐户进行任何修改。 - 请不要向他人透露本邮件的内容,否则可能会导致您的账号被盗。 + If you did not ask that, please ignore this email. It will expire and become useless in 20 minutes. diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 4b8bfce..7f819ae 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -1,10 +1,13 @@ FROM python:3.6-alpine3.6 -ADD requirements.txt /tmp -RUN apk add --no-cache --virtual .build-deps build-base jpeg-dev zlib-dev postgresql-dev && \ - pip install -r /tmp/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple && \ - apk del .build-deps --purge +ENV OJ_ENV production +RUN apk add --no-cache nginx supervisor jpeg-dev zlib-dev postgresql-dev freetype-dev -Volume ["/app"] +ADD requirements.txt /tmp +RUN apk add --no-cache build-base && \ + pip install --no-cache-dir -r /tmp/requirements.txt -i https://pypi.doubanio.com/simple && \ + apk del build-base --purge + +VOLUME [ "/app" ] CMD sh /app/deploy/run.sh diff --git a/deploy/nginx.conf b/deploy/nginx.conf index 8209807..bdaf1de 100644 --- a/deploy/nginx.conf +++ b/deploy/nginx.conf @@ -46,7 +46,7 @@ http { # Timeout for keep-alive connections. Server will close connections after # this time. - keepalive_timeout 65; + keepalive_timeout 10; # Sendfile copies data between one FD and other from within the kernel, # which is more efficient than read() + write(). @@ -89,12 +89,11 @@ http { # Sets the path, format, and configuration for a buffered log write. # access_log /var/log/nginx/access.log main; - access_log off + access_log off; server { listen 80 default_server; server_name _; - keetalive_timeout 5; location /static/avatar { expires max; diff --git a/deploy/requirements.txt b/deploy/requirements.txt index 70aa2e5..559cce8 100644 --- a/deploy/requirements.txt +++ b/deploy/requirements.txt @@ -13,3 +13,5 @@ flake8-coding requests django-redis psycopg2 +gunicorn +jsonfield diff --git a/deploy/run.sh b/deploy/run.sh index fbc4b3c..8082c21 100644 --- a/deploy/run.sh +++ b/deploy/run.sh @@ -12,11 +12,29 @@ fi cd $BASE find . -name "*.pyc" -delete +chown -R nobody:nogroup $BASE/log +# wait for postgresql start +sleep 5 + +n=0 +while [ $n -lt 3 ] +do python manage.py migrate if [ $? -ne 0 ]; then - echo "Can't start server" - exit 1 + echo "Can't start server, try again in 3 seconds.." + sleep 3 + let "n+=1" + continue fi python manage.py initadmin -python manage.py runserver 0.0.0.0:8080 +break + +done + +if [ $n -eq 3 ]; then + echo "Can't start server, please check log file for details." + exit 1 +fi + +exec supervisord -c /app/deploy/supervisor.conf diff --git a/deploy/supervisor.conf b/deploy/supervisor.conf index 55f5313..1bbc52f 100644 --- a/deploy/supervisor.conf +++ b/deploy/supervisor.conf @@ -11,7 +11,7 @@ childlogdir=/app/log/ serverurl=unix:///tmp/supervisor.sock [program:gunicorn] -command=gunicorn oj.wsgi --user nobody -b 127.0.0.1:8080 --reload -w `grep -c ^processor /proc/cpuinfo` +command=sh -c "gunicorn oj.wsgi --user nobody -b 127.0.0.1:8080 --reload -w `grep -c ^processor /proc/cpuinfo`" directory=/app/ stdout_logfile=/app/log/gunicorn.log stderr_logfile=/app/log/gunicorn.log @@ -21,7 +21,7 @@ startsecs=5 stopwaitsecs = 5 killasgroup=true -[program:task_queue] +[program:celery] command=celery -A oj worker -l warning directory=/app/ user=nobody @@ -42,4 +42,4 @@ autostart=true autorestart=true startsecs=5 stopwaitsecs = 5 -killasgroup=true \ No newline at end of file +killasgroup=true diff --git a/oj/dev_settings.py b/oj/dev_settings.py index cb40938..85719f6 100644 --- a/oj/dev_settings.py +++ b/oj/dev_settings.py @@ -26,6 +26,8 @@ ALLOWED_HOSTS = ["*"] TEST_CASE_DIR = "/tmp" +LOG_PATH = "/tmp/" + STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static"), ] diff --git a/oj/production_settings.py b/oj/production_settings.py index 7d89276..7b1b1e1 100644 --- a/oj/production_settings.py +++ b/oj/production_settings.py @@ -8,11 +8,11 @@ def get_env(name, default=""): DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'HOST': get_env("POSTGRESQL_HOST", "postgresql"), - 'PORT': get_env("POSTGRESQL_PORT", "5433"), - 'NAME': get_env("POSTGRESQL_DBNAME"), - 'USER': get_env("POSTGRESQL_USER"), - 'PASSWORD': get_env("POSTGRESQL_PASSWORD") + 'HOST': get_env("POSTGRES_HOST", "postgres"), + 'PORT': get_env("POSTGRES_PORT", "5433"), + 'NAME': get_env("POSTGRES_DB"), + 'USER': get_env("POSTGRES_USER"), + 'PASSWORD': get_env("POSTGRES_PASSWORD") } } @@ -25,4 +25,5 @@ DEBUG = False ALLOWED_HOSTS = ['*'] -TEST_CASE_DIR = "/test_case" +TEST_CASE_DIR = "/app/test_case" +LOG_PATH = "log/" diff --git a/oj/settings.py b/oj/settings.py index a5fe7db..47f0b78 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -13,7 +13,7 @@ import os from .custom_settings import * -if os.environ.get("NODE_ENV") == "production": +if os.environ.get("OJ_ENV") == "production": from .production_settings import * else: from .dev_settings import * @@ -108,7 +108,6 @@ USE_TZ = True STATIC_URL = '/static/' AUTH_USER_MODEL = 'account.User' -LOG_PATH = "log/" LOGGING = { 'version': 1, diff --git a/options/migrations/0001_initial.py b/options/migrations/0001_initial.py index db40e1e..a109c91 100644 --- a/options/migrations/0001_initial.py +++ b/options/migrations/0001_initial.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.3 on 2017-10-01 19:19 +# Generated by Django 1.11.4 on 2017-10-23 08:11 from __future__ import unicode_literals -import jsonfield.fields +import django.contrib.postgres.fields.jsonb from django.db import migrations, models @@ -19,7 +19,7 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('key', models.CharField(db_index=True, max_length=128, unique=True)), - ('value', jsonfield.fields.JSONField()), + ('value', django.contrib.postgres.fields.jsonb.JSONField()), ], ), ] diff --git a/options/migrations/0002_auto_20171011_1214.py b/options/migrations/0002_auto_20171011_1214.py deleted file mode 100644 index ee52ffa..0000000 --- a/options/migrations/0002_auto_20171011_1214.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.4 on 2017-10-11 12:14 -from __future__ import unicode_literals - -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('options', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='sysoptions', - name='value', - field=django.contrib.postgres.fields.jsonb.JSONField(), - ), - ] diff --git a/reset_password_email.html b/reset_password_email.html deleted file mode 100644 index e69de29..0000000 diff --git a/utils/management/commands/initadmin.py b/utils/management/commands/initadmin.py index 1ff9d6b..c29463a 100644 --- a/utils/management/commands/initadmin.py +++ b/utils/management/commands/initadmin.py @@ -1,3 +1,4 @@ +import os from django.core.management.base import BaseCommand from account.models import AdminType, ProblemPermission, User, UserProfile @@ -9,18 +10,17 @@ class Command(BaseCommand): try: admin = User.objects.get(username="root") if admin.admin_type == AdminType.SUPER_ADMIN: - self.stdout.write(self.style.WARNING("Super admin user 'root' already exists, " - "would you like to reset it's password?\n" - "Input yes to confirm: ")) - if input() == "yes": - # todo remove this in product env - # rand_password = rand_str(length=6) - rand_password = "rootroot" - admin.save() - self.stdout.write(self.style.SUCCESS("Successfully created super admin user password.\n" - "Username: root\nPassword: %s\n" - "Remember to change password and turn on two factors auth " - "after installation." % rand_password)) + if os.environ.get("OJ_ENV") != "production": + self.stdout.write(self.style.WARNING("Super admin user 'root' already exists, " + "would you like to reset it's password?\n" + "Input yes to confirm: ")) + if input() == "yes": + rand_password = "rootroot" + admin.save() + self.stdout.write(self.style.SUCCESS("Successfully created super admin user password.\n" + "Username: root\nPassword: %s\n" + "Remember to change password and turn on two factors auth " + "after installation." % rand_password)) else: self.stdout.write(self.style.SUCCESS("Nothing happened")) else: From b694000ab937fc1f3e6cfca44444e33c28d5c039 Mon Sep 17 00:00:00 2001 From: zema1 Date: Tue, 24 Oct 2017 21:14:29 +0800 Subject: [PATCH 065/106] update dockerfile and settings --- account/models.py | 2 +- account/views/oj.py | 4 +- deploy/Dockerfile | 2 +- deploy/nginx.conf | 109 -------------------------------------- deploy/run.sh | 3 +- deploy/supervisor.conf | 23 +++----- oj/dev_settings.py | 3 ++ oj/production_settings.py | 7 ++- oj/settings.py | 3 -- 9 files changed, 19 insertions(+), 137 deletions(-) delete mode 100644 deploy/nginx.conf diff --git a/account/models.py b/account/models.py index 6ef7f71..92e3df3 100644 --- a/account/models.py +++ b/account/models.py @@ -85,7 +85,7 @@ class UserProfile(models.Model): oi_problems_status = JSONField(default=dict) real_name = models.CharField(max_length=32, blank=True, null=True) - avatar = models.CharField(max_length=256, default=f"/{settings.IMAGE_UPLOAD_DIR}/default.png") + avatar = models.CharField(max_length=256, default=f"{settings.AVATAR_URI_PREFIX}/default.png") blog = models.URLField(blank=True, null=True) mood = models.CharField(max_length=256, blank=True, null=True) github = models.CharField(max_length=64, blank=True, null=True) diff --git a/account/views/oj.py b/account/views/oj.py index 5f72156..3cccd36 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -74,12 +74,12 @@ class AvatarUploadAPI(APIView): return self.error("Unsupported file format") name = rand_str(10) + suffix - with open(os.path.join(settings.IMAGE_UPLOAD_DIR_ABS, name), "wb") as img: + with open(os.path.join(settings.AVATAR_UPLOAD_DIR, name), "wb") as img: for chunk in avatar: img.write(chunk) user_profile = request.user.userprofile - user_profile.avatar = f"/{settings.IMAGE_UPLOAD_DIR}/{name}" + user_profile.avatar = f"{settings.AVATAR_URI_PREFIX}/{name}" user_profile.save() return self.success("Succeeded") diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 7f819ae..55af522 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -1,7 +1,7 @@ FROM python:3.6-alpine3.6 ENV OJ_ENV production -RUN apk add --no-cache nginx supervisor jpeg-dev zlib-dev postgresql-dev freetype-dev +RUN apk add --no-cache supervisor jpeg-dev zlib-dev postgresql-dev freetype-dev ADD requirements.txt /tmp RUN apk add --no-cache build-base && \ diff --git a/deploy/nginx.conf b/deploy/nginx.conf deleted file mode 100644 index bdaf1de..0000000 --- a/deploy/nginx.conf +++ /dev/null @@ -1,109 +0,0 @@ -user nginx; - -# Set number of worker processes automatically based on number of CPU cores. -worker_processes auto; - -# Enables the use of JIT for regular expressions to speed-up their processing. -pcre_jit on; - -# Configures default error logger. -error_log /dev/stderr warn; - -# use supervisor to monitor -daemon off; - -# set pid path -pid /tmp/nginx.pid; - -# Includes files with directives to load dynamic modules. -include /etc/nginx/modules/*.conf; - - -events { - # The maximum number of simultaneous connections that can be opened by - # a worker process. - worker_connections 1024; -} - -http { - # Includes mapping of file name extensions to MIME types of responses - # and defines the default type. - include /etc/nginx/mime.types; - default_type application/octet-stream; - - # Name servers used to resolve names of upstream servers into addresses. - # It's also needed when using tcpsocket and udpsocket in Lua modules. - #resolver 208.67.222.222 208.67.220.220; - - # Don't tell nginx version to clients. - server_tokens off; - - # Specifies the maximum accepted body size of a client request, as - # indicated by the request header Content-Length. If the stated content - # length is greater than this size, then the client receives the HTTP - # error code 413. Set to 0 to disable. - client_max_body_size 50m; - - # Timeout for keep-alive connections. Server will close connections after - # this time. - keepalive_timeout 10; - - # Sendfile copies data between one FD and other from within the kernel, - # which is more efficient than read() + write(). - sendfile on; - - # Don't buffer data-sends (disable Nagle algorithm). - # Good for sending frequent small bursts of data in real time. - tcp_nodelay on; - - # Causes nginx to attempt to send its HTTP response head in one packet, - # instead of using partial frames. - #tcp_nopush on; - - - # Path of the file with Diffie-Hellman parameters for EDH ciphers. - #ssl_dhparam /etc/ssl/nginx/dh2048.pem; - - # Specifies that our cipher suits should be preferred over client ciphers. - ssl_prefer_server_ciphers on; - - # Enables a shared SSL cache with size that can hold around 8000 sessions. - ssl_session_cache shared:SSL:2m; - - - # Enable gzipping of responses. - gzip on; - gzip_types application/javascript text/css; - - # Set the Vary HTTP header as defined in the RFC 2616. - gzip_vary on; - - # Enable checking the existence of precompressed files. - #gzip_static on; - - - # Specifies the main log format. - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - # Sets the path, format, and configuration for a buffered log write. - # access_log /var/log/nginx/access.log main; - access_log off; - - server { - listen 80 default_server; - server_name _; - - location /static/avatar { - expires max; - alias /app/static/avatar; - } - location / { - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Host $host; - - proxy_pass http://127.0.0.1:8080; - } - } -} diff --git a/deploy/run.sh b/deploy/run.sh index 8082c21..34ef16b 100644 --- a/deploy/run.sh +++ b/deploy/run.sh @@ -12,7 +12,6 @@ fi cd $BASE find . -name "*.pyc" -delete -chown -R nobody:nogroup $BASE/log # wait for postgresql start sleep 5 @@ -29,7 +28,6 @@ if [ $? -ne 0 ]; then fi python manage.py initadmin break - done if [ $n -eq 3 ]; then @@ -37,4 +35,5 @@ if [ $n -eq 3 ]; then exit 1 fi +chown -R nobody:nogroup /data/log /data/test_case /data/avatar exec supervisord -c /app/deploy/supervisor.conf diff --git a/deploy/supervisor.conf b/deploy/supervisor.conf index 1bbc52f..fc248d8 100644 --- a/deploy/supervisor.conf +++ b/deploy/supervisor.conf @@ -5,16 +5,16 @@ logfile_backups=10 loglevel=info pidfile=/tmp/supervisord.pid nodaemon=true -childlogdir=/app/log/ +childlogdir=/data/log/ [supervisorctl] serverurl=unix:///tmp/supervisor.sock [program:gunicorn] -command=sh -c "gunicorn oj.wsgi --user nobody -b 127.0.0.1:8080 --reload -w `grep -c ^processor /proc/cpuinfo`" +command=sh -c "gunicorn oj.wsgi --user nobody -b 0.0.0.0:8080 --reload -w `grep -c ^processor /proc/cpuinfo`" directory=/app/ -stdout_logfile=/app/log/gunicorn.log -stderr_logfile=/app/log/gunicorn.log +stdout_logfile=/data/log/gunicorn.log +stderr_logfile=/data/log/gunicorn.log autostart=true autorestart=true startsecs=5 @@ -25,19 +25,8 @@ killasgroup=true command=celery -A oj worker -l warning directory=/app/ user=nobody -stdout_logfile=/app/log/celery.log -stderr_logfile=/app/log/celery.log -autostart=true -autorestart=true -startsecs=5 -stopwaitsecs = 5 -killasgroup=true - -[program:nginx] -command=nginx -c /app/deploy/nginx.conf -directory=/app/ -stdout_logfile=/app/log/nginx.log -stderr_logfile=/app/log/nginx.log +stdout_logfile=/data/log/celery.log +stderr_logfile=/data/log/celery.log autostart=true autorestart=true startsecs=5 diff --git a/oj/dev_settings.py b/oj/dev_settings.py index 85719f6..15ae239 100644 --- a/oj/dev_settings.py +++ b/oj/dev_settings.py @@ -28,6 +28,9 @@ TEST_CASE_DIR = "/tmp" LOG_PATH = "/tmp/" +AVATAR_URI_PREFIX = "/static/avatar" +AVATAR_UPLOAD_DIR = f"{BASE_DIR}{AVATAR_URI_PREFIX}" + STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static"), ] diff --git a/oj/production_settings.py b/oj/production_settings.py index 7b1b1e1..56dbc49 100644 --- a/oj/production_settings.py +++ b/oj/production_settings.py @@ -25,5 +25,8 @@ DEBUG = False ALLOWED_HOSTS = ['*'] -TEST_CASE_DIR = "/app/test_case" -LOG_PATH = "log/" +AVATAR_URI_PREFIX = "/static/avatar" +AVATAR_UPLOAD_DIR = "/data/avatar" + +TEST_CASE_DIR = "/data/test_case" +LOG_PATH = "/data/log" diff --git a/oj/settings.py b/oj/settings.py index 47f0b78..81f55d3 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -191,9 +191,6 @@ CELERY_TASK_SOFT_TIME_LIMIT = CELERY_TASK_TIME_LIMIT = 180 CELERY_ACCEPT_CONTENT = ["json"] CELERY_TASK_SERIALIZER = "json" -IMAGE_UPLOAD_DIR = 'static/avatar' -IMAGE_UPLOAD_DIR_ABS = os.path.join(BASE_DIR, IMAGE_UPLOAD_DIR) - # 用于限制用户恶意提交大量代码 TOKEN_BUCKET_DEFAULT_CAPACITY = 50 From 728373a5ff95b3a338bdbde70c607f50c810fee6 Mon Sep 17 00:00:00 2001 From: zema1 Date: Fri, 27 Oct 2017 18:36:29 +0800 Subject: [PATCH 066/106] =?UTF-8?q?=E5=AE=8C=E5=96=84contest=E6=9D=83?= =?UTF-8?q?=E9=99=90=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/decorators.py | 81 ++++++++++++++++-------------- account/serializers.py | 9 +++- account/tests.py | 6 +-- account/urls/oj.py | 3 +- account/views/oj.py | 34 ++++++++++--- contest/models.py | 13 +++-- contest/tests.py | 29 +++++------ contest/urls/oj.py | 4 +- contest/views/oj.py | 21 ++++---- deploy/run.sh | 2 +- oj/settings.py | 2 +- problem/serializers.py | 9 +++- problem/tests.py | 22 ++++----- problem/views/oj.py | 22 +++++---- submission/serializers.py | 1 + submission/views/oj.py | 100 +++++++++++++++++++++++--------------- utils/api/api.py | 14 ++---- utils/api/tests.py | 4 +- utils/throttling.py | 5 +- 19 files changed, 219 insertions(+), 162 deletions(-) diff --git a/account/decorators.py b/account/decorators.py index b352366..4521530 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -4,7 +4,7 @@ from utils.api import JSONResponse from .models import ProblemPermission -from contest.models import Contest, ContestType, ContestStatus +from contest.models import Contest, ContestType, ContestStatus, ContestRuleType class BasePermissionDecorator(object): @@ -25,7 +25,7 @@ class BasePermissionDecorator(object): return self.error("Your account is disabled") return self.func(*args, **kwargs) else: - return self.error("Please login in first") + return self.error("Please login first") def check_permission(self): raise NotImplementedError() @@ -57,45 +57,54 @@ class problem_permission_required(admin_role_required): return True -def check_contest_permission(func): +def check_contest_permission(check_type="details"): """ - 只供Class based view 使用,检查用户是否有权进入该contest, + 只供Class based view 使用,检查用户是否有权进入该contest, check_type 可选 details, problems, ranks, submissions 若通过验证,在view中可通过self.contest获得该contest """ - @functools.wraps(func) - def _check_permission(*args, **kwargs): - self = args[0] - request = args[1] - user = request.user - if kwargs.get("contest_id"): - contest_id = kwargs.pop("contest_id") - else: - contest_id = request.GET.get("contest_id") - if not contest_id: - return self.error("Parameter contest_id not exist.") - try: - # use self.contest to avoid query contest again in view. - self.contest = Contest.objects.select_related("created_by").get(id=contest_id, visible=True) - except Contest.DoesNotExist: - return self.error("Contest %s doesn't exist" % contest_id) + def decorator(func): + def _check_permission(*args, **kwargs): + self = args[0] + request = args[1] + user = request.user + if kwargs.get("contest_id"): + contest_id = kwargs.pop("contest_id") + else: + contest_id = request.GET.get("contest_id") + if not contest_id: + return self.error("Parameter contest_id not exist.") + + try: + # use self.contest to avoid query contest again in view. + self.contest = Contest.objects.select_related("created_by").get(id=contest_id, visible=True) + except Contest.DoesNotExist: + return self.error("Contest %s doesn't exist" % contest_id) + + # creator or owner + if self.contest.is_contest_admin(user): + return func(*args, **kwargs) + + if self.contest.contest_type == ContestType.PASSWORD_PROTECTED_CONTEST: + # Anonymous + if not user.is_authenticated(): + return self.error("Please login first.") + # password error + if ("accessible_contests" not in request.session) or \ + (self.contest.id not in request.session["accessible_contests"]): + return self.error("Password is required.") + + # regular use get contest problems, ranks etc. before contest started + if self.contest.status == ContestStatus.CONTEST_NOT_START and check_type != "details": + return self.error("Contest has not started yet.") + + # check is user have permission to get ranks, submissions OI Contest + if self.contest.status == ContestStatus.CONTEST_UNDERWAY and self.contest.rule_type == ContestRuleType.OI: + if not self.contest.real_time_rank and (check_type == "ranks" or check_type == "submissions"): + return self.error(f"No permission to get {check_type}") - # creator or owner - if self.contest.is_contest_admin(user): return func(*args, **kwargs) - if self.contest.status == ContestStatus.CONTEST_NOT_START: - return self.error("Contest has not started yet.") + return _check_permission - if self.contest.contest_type == ContestType.PASSWORD_PROTECTED_CONTEST: - # Anonymous - if not user.is_authenticated(): - return self.error("Please login in first.") - # password error - if ("accessible_contests" not in request.session) or \ - (self.contest.id not in request.session["accessible_contests"]): - return self.error("Password is required.") - - return func(*args, **kwargs) - - return _check_permission + return decorator diff --git a/account/serializers.py b/account/serializers.py index aa9675a..9c2cd15 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -8,7 +8,7 @@ from .models import AdminType, ProblemPermission, User, UserProfile class UserLoginSerializer(serializers.Serializer): username = serializers.CharField() password = serializers.CharField() - tfa_code = serializers.CharField(required=False, allow_null=True) + tfa_code = serializers.CharField(required=False, allow_blank=True) class UsernameOrEmailCheckSerializer(serializers.Serializer): @@ -26,6 +26,13 @@ class UserRegisterSerializer(serializers.Serializer): class UserChangePasswordSerializer(serializers.Serializer): old_password = serializers.CharField() new_password = serializers.CharField(min_length=6) + tfa_code = serializers.CharField(required=False, allow_blank=True) + + +class UserChangeEmailSerializer(serializers.Serializer): + password = serializers.CharField() + new_email = serializers.EmailField(max_length=64) + tfa_code = serializers.CharField(required=False, allow_blank=True) class UserSerializer(serializers.ModelSerializer): diff --git a/account/tests.py b/account/tests.py index 75515ce..ccc8b1a 100644 --- a/account/tests.py +++ b/account/tests.py @@ -362,7 +362,7 @@ class UserChangePasswordAPITest(CaptchaTest): def test_login_required(self): response = self.client.post(self.url, data=self.data) - self.assertEqual(response.data, {"error": "permission-denied", "data": "Please login in first"}) + self.assertEqual(response.data, {"error": "permission-denied", "data": "Please login first"}) def test_valid_ola_password(self): self.assertTrue(self.client.login(username=self.username, password=self.old_password)) @@ -476,13 +476,13 @@ class UserRankAPITest(APITestCase): def test_get_acm_rank(self): resp = self.client.get(self.url, data={"rule": ContestRuleType.ACM}) self.assertSuccess(resp) - data = resp.data["data"] + data = resp.data["data"]["results"] self.assertEqual(data[0]["user"]["username"], "test1") self.assertEqual(data[1]["user"]["username"], "test2") def test_get_oi_rank(self): resp = self.client.get(self.url, data={"rule": ContestRuleType.OI}) self.assertSuccess(resp) - data = resp.data["data"] + data = resp.data["data"]["results"] self.assertEqual(data[0]["user"]["username"], "test2") self.assertEqual(data[1]["user"]["username"], "test1") diff --git a/account/urls/oj.py b/account/urls/oj.py index aacbb59..0af54a5 100644 --- a/account/urls/oj.py +++ b/account/urls/oj.py @@ -1,7 +1,7 @@ from django.conf.urls import url from ..views.oj import (ApplyResetPasswordAPI, ResetPasswordAPI, - UserChangePasswordAPI, UserRegisterAPI, + UserChangePasswordAPI, UserRegisterAPI, UserChangeEmailAPI, UserLoginAPI, UserLogoutAPI, UsernameOrEmailCheck, AvatarUploadAPI, TwoFactorAuthAPI, UserProfileAPI, UserRankAPI, CheckTFARequiredAPI, SessionManagementAPI) @@ -13,6 +13,7 @@ urlpatterns = [ url(r"^logout/?$", UserLogoutAPI.as_view(), name="user_logout_api"), url(r"^register/?$", UserRegisterAPI.as_view(), name="user_register_api"), url(r"^change_password/?$", UserChangePasswordAPI.as_view(), name="user_change_password_api"), + url(r"^change_email/?$", UserChangeEmailAPI.as_view(), name="user_change_email"), url(r"^apply_reset_password/?$", ApplyResetPasswordAPI.as_view(), name="apply_reset_password_api"), url(r"^reset_password/?$", ResetPasswordAPI.as_view(), name="reset_password_api"), url(r"^captcha/?$", CaptchaAPIView.as_view(), name="show_captcha"), diff --git a/account/views/oj.py b/account/views/oj.py index 3cccd36..385399c 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -21,7 +21,7 @@ from ..models import User, UserProfile from ..serializers import (ApplyResetPasswordSerializer, ResetPasswordSerializer, UserChangePasswordSerializer, UserLoginSerializer, UserRegisterSerializer, UsernameOrEmailCheckSerializer, - RankInfoSerializer) + RankInfoSerializer, UserChangeEmailSerializer) from ..serializers import (TwoFactorAuthCodeSerializer, UserProfileSerializer, EditUserProfileSerializer, AvatarUploadForm) from ..tasks import send_email_async @@ -176,11 +176,6 @@ class UserLoginAPI(APIView): else: return self.error("Invalid username or password") - # todo remove this, only for debug use - def get(self, request): - auth.login(request, auth.authenticate(username=request.GET["username"], password=request.GET["password"])) - return self.success() - class UserLogoutAPI(APIView): def get(self, request): @@ -233,6 +228,27 @@ class UserRegisterAPI(APIView): return self.success("Succeeded") +class UserChangeEmailAPI(APIView): + @validate_serializer(UserChangeEmailSerializer) + @login_required + def post(self, request): + data = request.data + user = auth.authenticate(username=request.user.username, password=data["password"]) + if user: + if user.two_factor_auth: + if "tfa_code" not in data: + return self.error("tfa_required") + if not OtpAuth(user.tfa_token).valid_totp(data["tfa_code"]): + return self.error("Invalid two factor verification code") + if User.objects.filter(email=data["new_email"]).exists(): + return self.error("The email is owned by other account") + user.email = data["new_email"] + user.save() + return self.success("Succeeded") + else: + return self.error("Wrong password") + + class UserChangePasswordAPI(APIView): @validate_serializer(UserChangePasswordSerializer) @login_required @@ -244,7 +260,11 @@ class UserChangePasswordAPI(APIView): username = request.user.username user = auth.authenticate(username=username, password=data["old_password"]) if user: - # TODO: check tfa? + if user.two_factor_auth: + if "tfa_code" not in data: + return self.error("tfa_required") + if not OtpAuth(user.tfa_token).valid_totp(data["tfa_code"]): + return self.error("Invalid two factor verification code") user.set_password(data["new_password"]) user.save() return self.success("Succeeded") diff --git a/contest/models.py b/contest/models.py index 08a3bab..adfa3a7 100644 --- a/contest/models.py +++ b/contest/models.py @@ -45,13 +45,12 @@ class Contest(models.Model): def is_contest_admin(self, user): return user.is_authenticated() and (self.created_by == user or user.admin_type == AdminType.SUPER_ADMIN) - def check_oi_permission(self, user): - if self.status != ContestStatus.CONTEST_ENDED and not self.real_time_rank: - if self.is_contest_admin(user): - return True - else: - return False - return True + # 是否有权查看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 \ + self.real_time_rank class Meta: db_table = "contest" diff --git a/contest/tests.py b/contest/tests.py index df05df8..635c326 100644 --- a/contest/tests.py +++ b/contest/tests.py @@ -58,43 +58,40 @@ class ContestAdminAPITest(APITestCase): class ContestAPITest(APITestCase): def setUp(self): self.create_admin() - self.url = self.reverse("contest_api") - - def create_contest(self): url = self.reverse("contest_admin_api") - return self.client.post(url, data=DEFAULT_CONTEST_DATA) + self.contest = self.client.post(url, data=DEFAULT_CONTEST_DATA).data["data"] + self.url = self.reverse("contest_api") + "?contest_id=" + str(self.contest["id"]) def test_get_contest_list(self): - self.create_contest() - response = self.client.get(self.url) + url = self.reverse("contest_list_api") + response = self.client.get(url + "?limit=10") self.assertSuccess(response) + self.assertEqual(len(response.data["data"]["results"]), 1) def test_get_one_contest(self): - contest_id = self.create_contest().data["data"]["id"] - response = self.client.get("{}?id={}".format(self.url, contest_id)) - self.assertSuccess(response) + resp = self.client.get(self.url) + self.assertSuccess(resp) def test_regular_user_validate_contest_password(self): - contest_id = self.create_contest().data["data"]["id"] self.create_user("test", "test123") url = self.reverse("contest_password_api") - resp = self.client.post(url, {"contest_id": contest_id, "password": "error_password"}) + resp = self.client.post(url, {"contest_id": self.contest["id"], "password": "error_password"}) self.assertDictEqual(resp.data, {"error": "error", "data": "Wrong password"}) - resp = self.client.post(url, {"contest_id": contest_id, "password": DEFAULT_CONTEST_DATA["password"]}) + resp = self.client.post(url, {"contest_id": self.contest["id"], "password": DEFAULT_CONTEST_DATA["password"]}) self.assertSuccess(resp) def test_regular_user_access_contest(self): - contest_id = self.create_contest().data["data"]["id"] self.create_user("test", "test123") url = self.reverse("contest_access_api") - resp = self.client.get(url + "?contest_id=" + str(contest_id)) + resp = self.client.get(url + "?contest_id=" + str(self.contest["id"])) self.assertFalse(resp.data["data"]["access"]) password_url = self.reverse("contest_password_api") - resp = self.client.post(password_url, {"contest_id": contest_id, "password": DEFAULT_CONTEST_DATA["password"]}) + resp = self.client.post(password_url, + {"contest_id": self.contest["id"], "password": DEFAULT_CONTEST_DATA["password"]}) self.assertSuccess(resp) - resp = self.client.get(url + "?contest_id=" + str(contest_id)) + resp = self.client.get(self.url) self.assertSuccess(resp) diff --git a/contest/urls/oj.py b/contest/urls/oj.py index cfa12f6..9e94fa5 100644 --- a/contest/urls/oj.py +++ b/contest/urls/oj.py @@ -1,10 +1,12 @@ from django.conf.urls import url -from ..views.oj import ContestAnnouncementListAPI, ContestAPI +from ..views.oj import ContestAnnouncementListAPI from ..views.oj import ContestPasswordVerifyAPI, ContestAccessAPI +from ..views.oj import ContestListAPI, ContestAPI from ..views.oj import ContestRankAPI urlpatterns = [ + url(r"^contests/?$", ContestListAPI.as_view(), name="contest_list_api"), url(r"^contest/?$", ContestAPI.as_view(), name="contest_api"), url(r"^contest/password/?$", ContestPasswordVerifyAPI.as_view(), name="contest_password_api"), url(r"^contest/announcement/?$", ContestAnnouncementListAPI.as_view(), name="contest_announcement_api"), diff --git a/contest/views/oj.py b/contest/views/oj.py index 1e787ce..342d30c 100644 --- a/contest/views/oj.py +++ b/contest/views/oj.py @@ -12,6 +12,7 @@ from ..serializers import OIContestRankSerializer, ACMContestRankSerializer class ContestAnnouncementListAPI(APIView): + @check_contest_permission(check_type="announcements") def get(self, request): contest_id = request.GET.get("contest_id") if not contest_id: @@ -24,15 +25,13 @@ class ContestAnnouncementListAPI(APIView): class ContestAPI(APIView): + @check_contest_permission(check_type="details") def get(self, request): - contest_id = request.GET.get("id") - if contest_id: - try: - contest = Contest.objects.select_related("created_by").get(id=contest_id, visible=True) - except Contest.DoesNotExist: - return self.error("Contest does not exist") - return self.success(ContestSerializer(contest).data) + return self.success(ContestSerializer(self.contest).data) + +class ContestListAPI(APIView): + def get(self, request): contests = Contest.objects.select_related("created_by").filter(visible=True) keyword = request.GET.get("keyword") rule_type = request.GET.get("rule_type") @@ -49,7 +48,8 @@ class ContestAPI(APIView): contests = contests.filter(end_time__lt=cur) else: contests = contests.filter(start_time__lte=cur, end_time__gte=cur) - return self.success(self.paginate_data(request, contests, ContestSerializer)) + data = self.paginate_data(request, contests, ContestSerializer) + return self.success(data) class ContestPasswordVerifyAPI(APIView): @@ -91,11 +91,9 @@ class ContestRankAPI(APIView): return OIContestRank.objects.filter(contest=self.contest). \ select_related("user").order_by("-total_score") - @check_contest_permission + @check_contest_permission(check_type="ranks") def get(self, request): if self.contest.rule_type == ContestRuleType.OI: - if not self.contest.check_oi_permission(request.user): - return self.error("You have no permission for ranks now") serializer = OIContestRankSerializer else: serializer = ACMContestRankSerializer @@ -105,5 +103,4 @@ class ContestRankAPI(APIView): if not qs: qs = self.get_rank() cache.set(cache_key, qs) - return self.success(self.paginate_data(request, qs, serializer)) diff --git a/deploy/run.sh b/deploy/run.sh index 34ef16b..e992e96 100644 --- a/deploy/run.sh +++ b/deploy/run.sh @@ -14,7 +14,7 @@ cd $BASE find . -name "*.pyc" -delete # wait for postgresql start -sleep 5 +sleep 6 n=0 while [ $n -lt 3 ] diff --git a/oj/settings.py b/oj/settings.py index 81f55d3..c25a9b9 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -192,7 +192,7 @@ CELERY_ACCEPT_CONTENT = ["json"] CELERY_TASK_SERIALIZER = "json" # 用于限制用户恶意提交大量代码 -TOKEN_BUCKET_DEFAULT_CAPACITY = 50 +TOKEN_BUCKET_DEFAULT_CAPACITY = 20 # 单位:每分钟 TOKEN_BUCKET_FILL_RATE = 2 diff --git a/problem/serializers.py b/problem/serializers.py index 0e44710..47e9a53 100644 --- a/problem/serializers.py +++ b/problem/serializers.py @@ -107,4 +107,11 @@ class ProblemSerializer(BaseProblemSerializer): class ContestProblemSerializer(BaseProblemSerializer): class Meta: model = Problem - exclude = ("test_case_score", "test_case_id", "visible", "is_public") + exclude = ("test_case_score", "test_case_id", "visible", "is_public", "difficulty") + + +class ContestProblemSafeSerializer(BaseProblemSerializer): + class Meta: + model = Problem + exclude = ("test_case_score", "test_case_id", "visible", "is_public", "difficulty" + "submission_number", "accepted_number", "statistic_info") diff --git a/problem/tests.py b/problem/tests.py index 072abd4..8ff2017 100644 --- a/problem/tests.py +++ b/problem/tests.py @@ -196,30 +196,26 @@ class ContestProblemAdminTest(APITestCase): def setUp(self): self.url = self.reverse("contest_problem_admin_api") self.create_admin() - - def create_contest(self): - url = self.reverse("contest_admin_api") - return self.client.post(url, data=DEFAULT_CONTEST_DATA) + self.contest = self.client.post(self.reverse("contest_admin_api"), data=DEFAULT_CONTEST_DATA).data["data"] def test_create_contest_problem(self): - contest = self.create_contest() data = copy.deepcopy(DEFAULT_PROBLEM_DATA) - data["contest_id"] = contest.data["data"]["id"] + data["contest_id"] = self.contest["id"] resp = self.client.post(self.url, data=data) self.assertSuccess(resp) - return contest, resp + return resp.data["data"] def test_get_contest_problem(self): - contest, contest_problem = self.test_create_contest_problem() - contest_id = contest.data["data"]["id"] + self.test_create_contest_problem() + contest_id = self.contest["id"] resp = self.client.get(self.url + "?contest_id=" + str(contest_id)) self.assertSuccess(resp) - self.assertEqual(len(resp.data["data"]), 1) + self.assertEqual(len(resp.data["data"]["results"]), 1) def test_get_one_contest_problem(self): - contest, contest_problem = self.test_create_contest_problem() - contest_id = contest.data["data"]["id"] - problem_id = contest_problem.data["data"]["id"] + contest_problem = self.test_create_contest_problem() + contest_id = self.contest["id"] + problem_id = contest_problem["id"] resp = self.client.get(f"{self.url}?contest_id={contest_id}&id={problem_id}") self.assertSuccess(resp) diff --git a/problem/views/oj.py b/problem/views/oj.py index 2f54ed5..0cc9abd 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -4,7 +4,7 @@ from utils.api import APIView from account.decorators import check_contest_permission from ..models import ProblemTag, Problem, ProblemRuleType from ..serializers import ProblemSerializer, TagSerializer -from ..serializers import ContestProblemSerializer +from ..serializers import ContestProblemSerializer, ContestProblemSafeSerializer from contest.models import ContestRuleType @@ -81,8 +81,6 @@ class ProblemAPI(APIView): class ContestProblemAPI(APIView): def _add_problem_status(self, request, queryset_values): - if self.contest.rule_type == ContestRuleType.OI and not self.contest.check_oi_permission(request.user): - return if request.user.is_authenticated(): profile = request.user.userprofile if self.contest.rule_type == ContestRuleType.ACM: @@ -92,7 +90,7 @@ class ContestProblemAPI(APIView): for problem in queryset_values: problem["my_status"] = problems_status.get(str(problem["id"]), {}).get("status") - @check_contest_permission + @check_contest_permission(check_type="problems") def get(self, request): problem_id = request.GET.get("problem_id") if problem_id: @@ -102,11 +100,17 @@ class ContestProblemAPI(APIView): visible=True) except Problem.DoesNotExist: return self.error("Problem does not exist.") - problem_data = ContestProblemSerializer(problem).data - self._add_problem_status(request, [problem_data, ]) + if self.contest.problem_details_permission(request.user): + problem_data = ContestProblemSerializer(problem).data + self._add_problem_status(request, [problem_data, ]) + else: + problem_data = ContestProblemSafeSerializer(problem).data return self.success(problem_data) + contest_problems = Problem.objects.select_related("created_by").filter(contest=self.contest, visible=True) - # 根据profile, 为做过的题目添加标记 - data = ContestProblemSerializer(contest_problems, many=True).data - self._add_problem_status(request, data) + if self.contest.problem_details_permission(request.user): + data = ContestProblemSerializer(contest_problems, many=True).data + self._add_problem_status(request, data) + else: + data = ContestProblemSafeSerializer(contest_problems, many=True).data return self.success(data) diff --git a/submission/serializers.py b/submission/serializers.py index 66a517b..21bd75d 100644 --- a/submission/serializers.py +++ b/submission/serializers.py @@ -8,6 +8,7 @@ class CreateSubmissionSerializer(serializers.Serializer): language = serializers.ChoiceField(choices=language_names) code = serializers.CharField(max_length=20000) contest_id = serializers.IntegerField(required=False) + captcha = serializers.CharField(required=False) class ShareSubmissionSerializer(serializers.Serializer): diff --git a/submission/views/oj.py b/submission/views/oj.py index 613bd38..d29b383 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -1,3 +1,4 @@ +from django.conf import settings from account.decorators import login_required, check_contest_permission from judge.tasks import judge_task # from judge.dispatcher import JudgeDispatcher @@ -5,6 +6,7 @@ from problem.models import Problem, ProblemRuleType from contest.models import Contest, ContestStatus, ContestRuleType from utils.api import APIView, validate_serializer from utils.throttling import TokenBucket, BucketController +from utils.captcha import Captcha from utils.cache import cache from ..models import Submission from ..serializers import (CreateSubmissionSerializer, SubmissionModelSerializer, @@ -12,43 +14,38 @@ from ..serializers import (CreateSubmissionSerializer, SubmissionModelSerializer from ..serializers import SubmissionSafeModelSerializer, SubmissionListSerializer -def _submit(response, user, problem_id, language, code, contest_id): - # TODO: 预设默认值,需修改 - controller = BucketController(user_id=user.id, - redis_conn=cache, - 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, - contest_id=contest_id, - visible=True) - except Problem.DoesNotExist: - return response.error("Problem not exist") - - submission = Submission.objects.create(user_id=user.id, - username=user.username, - language=language, - code=code, - 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) - return response.success({"submission_id": submission.id}) - - class SubmissionAPI(APIView): + def throttling(self, request): + user_controller = BucketController(factor=request.user.id, + redis_conn=cache, + default_capacity=settings.TOKEN_BUCKET_DEFAULT_CAPACITY) + user_bucket = TokenBucket(fill_rate=settings.TOKEN_BUCKET_FILL_RATE, + capacity=settings.TOKEN_BUCKET_DEFAULT_CAPACITY, + last_capacity=user_controller.last_capacity, + last_timestamp=user_controller.last_timestamp) + if user_bucket.consume(): + user_controller.last_capacity -= 1 + else: + return "Please wait %d seconds" % int(user_bucket.expected_time() + 1) + + ip_controller = BucketController(factor=request.session["ip"], + redis_conn=cache, + default_capacity=settings.TOKEN_BUCKET_DEFAULT_CAPACITY * 3) + + ip_bucket = TokenBucket(fill_rate=settings.TOKEN_BUCKET_FILL_RATE * 3, + capacity=settings.TOKEN_BUCKET_DEFAULT_CAPACITY * 3, + last_capacity=ip_controller.last_capacity, + last_timestamp=ip_controller.last_timestamp) + if ip_bucket.consume(): + ip_controller.last_capacity -= 1 + else: + return "Captcha is required" + @validate_serializer(CreateSubmissionSerializer) @login_required def post(self, request): data = request.data + hide_id = False if data.get("contest_id"): try: contest = Contest.objects.get(id=data["contest_id"]) @@ -56,9 +53,39 @@ 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 request.user != contest.created_by: + if contest.status == ContestStatus.CONTEST_NOT_START and not contest.is_contest_admin(request.user): return self.error("Contest have not started") - return _submit(self, request.user, data["problem_id"], data["language"], data["code"], data.get("contest_id")) + if not contest.problem_details_permission(): + 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) + except Problem.DoesNotExist: + return self.error("Problem not exist") + + submission = Submission.objects.create(user_id=request.user.id, + username=request.user.username, + language=data["language"], + code=data["code"], + problem_id=problem.id, + contest_id=data.get("contest_id")) + # use this for debug + # JudgeDispatcher(submission.id, problem.id).judge() + judge_task.delay(submission.id, problem.id) + if hide_id: + return self.success() + else: + return self.success({"submission_id": submission.id}) @login_required def get(self, request): @@ -123,15 +150,12 @@ class SubmissionListAPI(APIView): class ContestSubmissionListAPI(APIView): - @check_contest_permission + @check_contest_permission(check_type="submissions") def get(self, request): if not request.GET.get("limit"): return self.error("Limit is needed") contest = self.contest - if not contest.check_oi_permission(request.user): - return self.error("No permission for OI contest submissions") - submissions = Submission.objects.filter(contest_id=contest.id).select_related("problem__created_by") problem_id = request.GET.get("problem_id") myself = request.GET.get("myself") diff --git a/utils/api/api.py b/utils/api/api.py index f49b039..09671d2 100644 --- a/utils/api/api.py +++ b/utils/api/api.py @@ -107,18 +107,12 @@ class APIView(View): :param object_serializer: 用来序列化query set, 如果为None, 则直接对query set切片 :return: """ - need_paginate = request.GET.get("limit", None) - if need_paginate is None: - if object_serializer: - return object_serializer(query_set, many=True).data - else: - return {"results": query_set, "total": query_set.count()} try: - limit = int(request.GET.get("limit", "100")) + limit = int(request.GET.get("limit", "10")) except ValueError: - limit = 100 - if limit < 0: - limit = 100 + limit = 10 + if limit < 0 or limit > 100: + limit = 10 try: offset = int(request.GET.get("offset", "0")) except ValueError: diff --git a/utils/api/tests.py b/utils/api/tests.py index 4b485c9..b47ceae 100644 --- a/utils/api/tests.py +++ b/utils/api/tests.py @@ -27,8 +27,8 @@ class APITestCase(TestCase): return self.create_user(username=username, password=password, admin_type=AdminType.SUPER_ADMIN, problem_permission=ProblemPermission.ALL, login=login) - def reverse(self, url_name): - return reverse(url_name) + def reverse(self, url_name, *args, **kwargs): + return reverse(url_name, *args, **kwargs) def assertSuccess(self, response): if not response.data["error"] is None: diff --git a/utils/throttling.py b/utils/throttling.py index c1fe8dc..7c5f54a 100644 --- a/utils/throttling.py +++ b/utils/throttling.py @@ -31,11 +31,10 @@ class TokenBucket: class BucketController: - def __init__(self, user_id, redis_conn, default_capacity): - self.user_id = user_id + def __init__(self, factor, redis_conn, default_capacity): self.default_capacity = default_capacity self.redis = redis_conn - self.key = "bucket_" + str(self.user_id) + self.key = "bucket_" + str(factor) @property def last_capacity(self): From f0655ee305c79600ea6dbb9ff98d4a978d7dc447 Mon Sep 17 00:00:00 2001 From: zema1 Date: Mon, 30 Oct 2017 15:07:52 +0800 Subject: [PATCH 067/106] =?UTF-8?q?=E5=85=A8=E5=B1=80=E7=9A=84announcement?= =?UTF-8?q?=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- announcement/models.py | 1 + announcement/urls/admin.py | 2 +- announcement/urls/oj.py | 7 +++++++ announcement/views/__init__.py | 0 announcement/{views.py => views/admin.py} | 6 +++--- announcement/views/oj.py | 10 ++++++++++ contest/views/admin.py | 7 +++++-- oj/urls.py | 1 + 8 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 announcement/urls/oj.py create mode 100644 announcement/views/__init__.py rename announcement/{views.py => views/admin.py} (91%) create mode 100644 announcement/views/oj.py diff --git a/announcement/models.py b/announcement/models.py index 49f57b8..c7252e9 100644 --- a/announcement/models.py +++ b/announcement/models.py @@ -15,3 +15,4 @@ class Announcement(models.Model): class Meta: db_table = "announcement" + ordering = ('-create_time',) diff --git a/announcement/urls/admin.py b/announcement/urls/admin.py index 6b9ce0f..09673e6 100644 --- a/announcement/urls/admin.py +++ b/announcement/urls/admin.py @@ -1,6 +1,6 @@ from django.conf.urls import url -from ..views import AnnouncementAdminAPI +from ..views.admin import AnnouncementAdminAPI urlpatterns = [ url(r"^announcement/?$", AnnouncementAdminAPI.as_view(), name="announcement_admin_api"), diff --git a/announcement/urls/oj.py b/announcement/urls/oj.py new file mode 100644 index 0000000..71bcf3a --- /dev/null +++ b/announcement/urls/oj.py @@ -0,0 +1,7 @@ +from django.conf.urls import url + +from ..views.oj import AnnouncementAPI + +urlpatterns = [ + url(r"^announcement/?$", AnnouncementAPI.as_view(), name="announcement_admin_api"), +] diff --git a/announcement/views/__init__.py b/announcement/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/announcement/views.py b/announcement/views/admin.py similarity index 91% rename from announcement/views.py rename to announcement/views/admin.py index f607a7d..86d5430 100644 --- a/announcement/views.py +++ b/announcement/views/admin.py @@ -1,9 +1,9 @@ from account.decorators import super_admin_required from utils.api import APIView, validate_serializer -from .models import Announcement -from .serializers import (AnnouncementSerializer, CreateAnnouncementSerializer, - EditAnnouncementSerializer) +from announcement.models import Announcement +from announcement.serializers import (AnnouncementSerializer, CreateAnnouncementSerializer, + EditAnnouncementSerializer) class AnnouncementAdminAPI(APIView): diff --git a/announcement/views/oj.py b/announcement/views/oj.py new file mode 100644 index 0000000..1176c36 --- /dev/null +++ b/announcement/views/oj.py @@ -0,0 +1,10 @@ +from utils.api import APIView + +from announcement.models import Announcement +from announcement.serializers import AnnouncementSerializer + + +class AnnouncementAPI(APIView): + def get(self, request): + announcements = Announcement.objects.filter(visible=True) + return self.success(self.paginate_data(request, announcements, AnnouncementSerializer)) diff --git a/contest/views/admin.py b/contest/views/admin.py index 37244fa..58e8c7b 100644 --- a/contest/views/admin.py +++ b/contest/views/admin.py @@ -110,10 +110,13 @@ class ContestAnnouncementAPI(APIView): except ContestAnnouncement.DoesNotExist: return self.error("Contest announcement does not exist") - contest_announcements = ContestAnnouncement.objects.all().order_by("-create_time") + contest_id = request.GET.get("contest_id") + if not contest_id: + return self.error("Paramater error") + contest_announcements = ContestAnnouncement.objects.filter(contest_id=contest_id) if request.user.is_admin(): contest_announcements = contest_announcements.filter(created_by=request.user) keyword = request.GET.get("keyword") if keyword: contest_announcements = contest_announcements.filter(title__contains=keyword) - return self.success(self.paginate_data(request, contest_announcements, ContestAnnouncementSerializer)) + return self.success(ContestAnnouncementSerializer(contest_announcements, many=True).data) diff --git a/oj/urls.py b/oj/urls.py index 5eda628..3a2570b 100644 --- a/oj/urls.py +++ b/oj/urls.py @@ -3,6 +3,7 @@ from django.conf.urls import include, url urlpatterns = [ url(r"^api/", include("account.urls.oj")), url(r"^api/admin/", include("account.urls.admin")), + url(r"^api/", include("announcement.urls.oj")), url(r"^api/admin/", include("announcement.urls.admin")), url(r"^api/", include("conf.urls.oj")), url(r"^api/admin/", include("conf.urls.admin")), From aa4240790be82e36174cc239636fe2717f03b308 Mon Sep 17 00:00:00 2001 From: zema1 Date: Tue, 31 Oct 2017 16:33:25 +0800 Subject: [PATCH 068/106] fix many bugs --- account/views/oj.py | 5 ++--- announcement/models.py | 2 +- contest/models.py | 2 +- contest/tests.py | 4 ++-- judge/dispatcher.py | 17 ++++++++++---- oj/dev_settings.py | 2 +- oj/production_settings.py | 1 + oj/settings.py | 47 +++++++++++++++++++++++---------------- options/options.py | 8 ++++++- problem/serializers.py | 2 +- problem/views/admin.py | 2 +- submission/views/oj.py | 5 ++++- 12 files changed, 62 insertions(+), 35 deletions(-) diff --git a/account/views/oj.py b/account/views/oj.py index 385399c..de1df67 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -372,10 +372,9 @@ class UserRankAPI(APIView): if rule_type not in ContestRuleType.choices(): rule_type = ContestRuleType.ACM profiles = UserProfile.objects.select_related("user")\ - .filter(submission_number__gt=0)\ .exclude(user__is_disabled=True) if rule_type == ContestRuleType.ACM: - profiles = profiles.order_by("-accepted_number", "submission_number") + profiles = profiles.filter(submission_number__gt=0).order_by("-accepted_number", "submission_number") else: - profiles = profiles.order_by("-total_score") + profiles = profiles.filter(total_score__gt=0).order_by("-total_score") return self.success(self.paginate_data(request, profiles, RankInfoSerializer)) diff --git a/announcement/models.py b/announcement/models.py index c7252e9..a19b06c 100644 --- a/announcement/models.py +++ b/announcement/models.py @@ -15,4 +15,4 @@ class Announcement(models.Model): class Meta: db_table = "announcement" - ordering = ('-create_time',) + ordering = ("-create_time",) diff --git a/contest/models.py b/contest/models.py index adfa3a7..1ed8ff9 100644 --- a/contest/models.py +++ b/contest/models.py @@ -54,7 +54,7 @@ class Contest(models.Model): class Meta: db_table = "contest" - ordering = ("-create_time",) + ordering = ("-start_time",) class AbstractContestRank(models.Model): diff --git a/contest/tests.py b/contest/tests.py index 635c326..9ce9890 100644 --- a/contest/tests.py +++ b/contest/tests.py @@ -95,7 +95,7 @@ class ContestAPITest(APITestCase): self.assertSuccess(resp) -class ContestAnnouncementAPITest(APITestCase): +class ContestAnnouncementAdminAPITest(APITestCase): def setUp(self): self.create_super_admin() self.url = self.reverse("contest_announcement_admin_api") @@ -120,7 +120,7 @@ class ContestAnnouncementAPITest(APITestCase): def test_get_contest_announcements(self): self.test_create_contest_announcement() - response = self.client.get(self.url) + response = self.client.get(self.url + "?contest_id=" + str(self.data["contest_id"])) self.assertSuccess(response) def test_get_one_contest_announcement(self): diff --git a/judge/dispatcher.py b/judge/dispatcher.py index 199e627..3ae1d66 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -6,6 +6,7 @@ from urllib.parse import urljoin import requests from django.db import transaction from django.db.models import F +from django.conf import settings from account.models import User from conf.models import JudgeServer @@ -79,7 +80,10 @@ class JudgeDispatcher(object): try: for i in range(len(resp_data)): if resp_data[i]["result"] == JudgeStatus.ACCEPTED: - score += self.problem.test_case_score[i]["score"] + resp_data[i]["score"] = self.problem.test_case_score[i]["score"] + score += resp_data[i]["score"] + else: + resp_data[i]["score"] = 0 except IndexError: logger.error(f"Index Error raised when summing up the score in problem {self.problem.id}") self.submission.statistic_info["score"] = 0 @@ -115,8 +119,11 @@ class JudgeDispatcher(object): Submission.objects.filter(id=self.submission.id).update(result=JudgeStatus.JUDGING) - # TODO: try catch - resp = self._request(urljoin(server.service_url, "/judge"), data=data) + service_url = server.service_url + # not set service_url, it should be a linked container + if not service_url: + service_url = settings.DEFAULT_JUDGE_SERVER_SERVICE_URL + resp = self._request(urljoin(service_url, "/judge"), data=data) self.submission.info = resp if resp["err"]: self.submission.result = JudgeStatus.COMPILE_ERROR @@ -201,7 +208,7 @@ class JudgeDispatcher(object): logger.info("Contest debug mode, id: " + str(self.contest_id) + ", submission id: " + self.submission.id) return with transaction.atomic(): - user = User.objects.select_for_update().select_related("userprofile").get(id=self.submission.user_id) + user = User.objects.select_for_update().get(id=self.submission.user_id) user_profile = user.userprofile problem_id = str(self.problem.id) if self.contest.rule_type == ContestRuleType.ACM: @@ -298,5 +305,7 @@ class JudgeDispatcher(object): last_score = rank.submission_info.get(problem_id) if last_score: rank.total_score = rank.total_score - last_score + current_score + else: + rank.total_score = rank.total_score + current_score rank.submission_info[problem_id] = current_score rank.save() diff --git a/oj/dev_settings.py b/oj/dev_settings.py index 15ae239..448bc8f 100644 --- a/oj/dev_settings.py +++ b/oj/dev_settings.py @@ -26,7 +26,7 @@ ALLOWED_HOSTS = ["*"] TEST_CASE_DIR = "/tmp" -LOG_PATH = "/tmp/" +LOG_PATH = f"{BASE_DIR}/log/" AVATAR_URI_PREFIX = "/static/avatar" AVATAR_UPLOAD_DIR = f"{BASE_DIR}{AVATAR_URI_PREFIX}" diff --git a/oj/production_settings.py b/oj/production_settings.py index 56dbc49..bf8b3f8 100644 --- a/oj/production_settings.py +++ b/oj/production_settings.py @@ -30,3 +30,4 @@ AVATAR_UPLOAD_DIR = "/data/avatar" TEST_CASE_DIR = "/data/test_case" LOG_PATH = "/data/log" +DEFAULT_JUDGE_SERVER_SERVICE_URL = "http://judge-server:8080/" diff --git a/oj/settings.py b/oj/settings.py index c25a9b9..9c9ac49 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -10,6 +10,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.8/ref/settings/ """ import os +from copy import deepcopy from .custom_settings import * @@ -20,17 +21,17 @@ else: BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - # Applications -INSTALLED_APPS = ( +VENDOR_APPS = ( 'django.contrib.auth', 'django.contrib.sessions', 'django.contrib.contenttypes', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', - - 'account', +) +LOCAL_APPS = ( + 'account', 'announcement', 'conf', 'problem', @@ -38,8 +39,11 @@ INSTALLED_APPS = ( 'utils', 'submission', 'options', + 'judge', ) +INSTALLED_APPS = VENDOR_APPS + LOCAL_APPS + MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', @@ -115,17 +119,16 @@ LOGGING = { 'formatters': { 'standard': { 'format': '%(asctime)s [%(threadName)s:%(thread)d] [%(name)s:%(lineno)d] [%(module)s:%(funcName)s] [%(levelname)s]- %(message)s'} - # 日志格式 }, 'handlers': { 'django_error': { - 'level': 'DEBUG', + 'level': 'WARNING', 'class': 'logging.handlers.RotatingFileHandler', 'filename': os.path.join(LOG_PATH, 'django.log'), 'formatter': 'standard' }, 'app_info': { - 'level': 'DEBUG', + 'level': 'INFO', 'class': 'logging.handlers.RotatingFileHandler', 'filename': os.path.join(LOG_PATH, 'app_info.log'), 'formatter': 'standard' @@ -137,23 +140,30 @@ LOGGING = { } }, 'loggers': { - 'app_info': { - 'handlers': ['app_info', "console"], - 'level': 'DEBUG', - 'propagate': True - }, 'django.request': { 'handlers': ['django_error', 'console'], - 'level': 'DEBUG', + 'level': 'WARNING', + 'propagate': True, + }, + 'django.server': { + 'handlers': ['django_error', 'console'], + 'level': 'ERROR', 'propagate': True, }, 'django.db.backends': { - 'handlers': ['console'], - 'level': 'ERROR', + 'handlers': ['django_error', 'console'], + 'level': 'WARNING', 'propagate': True, - } + }, }, } +app_logger = { + 'handlers': ['app_info', 'console'], + 'level': 'DEBUG', + 'propagate': False +} +LOGGING["loggers"].update({app: deepcopy(app_logger) for app in LOCAL_APPS}) + REST_FRAMEWORK = { 'TEST_REQUEST_DEFAULT_FORMAT': 'json', 'DEFAULT_RENDERER_CLASSES': ( @@ -163,6 +173,7 @@ REST_FRAMEWORK = { REDIS_URL = "redis://%s:%s" % (REDIS_CONF["host"], REDIS_CONF["port"]) + def redis_config(db): def make_key(key, key_prefix, version): return key @@ -175,16 +186,14 @@ def redis_config(db): "KEY_FUNCTION": make_key } + CACHES = { "default": redis_config(db=1) } - - SESSION_ENGINE = "django.contrib.sessions.backends.cache" SESSION_CACHE_ALIAS = "default" - CELERY_RESULT_BACKEND = f"{REDIS_URL}/2" BROKER_URL = f"{REDIS_URL}/3" CELERY_TASK_SOFT_TIME_LIMIT = CELERY_TASK_TIME_LIMIT = 180 diff --git a/options/options.py b/options/options.py index b2d76f1..7d8b9a9 100644 --- a/options/options.py +++ b/options/options.py @@ -1,3 +1,4 @@ +import os from django.core.cache import cache from django.db import transaction, IntegrityError @@ -6,6 +7,11 @@ from utils.shortcuts import rand_str from .models import SysOptions as SysOptionsModel +def default_token(): + token = os.environ.get("JUDGE_SERVER_TOKEN") + return token if token else rand_str() + + class OptionKeys: website_base_url = "website_base_url" website_name = "website_name" @@ -25,7 +31,7 @@ class OptionDefaultValue: allow_register = True submission_list_show_all = True smtp_config = {} - judge_server_token = rand_str + judge_server_token = default_token class _SysOptionsMeta(type): diff --git a/problem/serializers.py b/problem/serializers.py index 47e9a53..671ed88 100644 --- a/problem/serializers.py +++ b/problem/serializers.py @@ -113,5 +113,5 @@ class ContestProblemSerializer(BaseProblemSerializer): class ContestProblemSafeSerializer(BaseProblemSerializer): class Meta: model = Problem - exclude = ("test_case_score", "test_case_id", "visible", "is_public", "difficulty" + exclude = ("test_case_score", "test_case_id", "visible", "is_public", "difficulty", "submission_number", "accepted_number", "statistic_info") diff --git a/problem/views/admin.py b/problem/views/admin.py index 00f46c4..1d26cbe 100644 --- a/problem/views/admin.py +++ b/problem/views/admin.py @@ -76,7 +76,7 @@ class TestCaseUploadAPI(CSRFExemptAPIView): content = zip_file.read(item).replace(b"\r\n", b"\n") size_cache[item] = len(content) if item.endswith(".out"): - md5_cache[item] = hashlib.md5(content).hexdigest() + md5_cache[item] = hashlib.md5(content.rstrip()).hexdigest() f.write(content) test_case_info = {"spj": spj, "test_cases": {}} diff --git a/submission/views/oj.py b/submission/views/oj.py index d29b383..e66aa5b 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -55,7 +55,7 @@ class SubmissionAPI(APIView): 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 contest.problem_details_permission(): + if not contest.problem_details_permission(request.user): hide_id = True if data.get("captcha"): @@ -110,6 +110,9 @@ class SubmissionAPI(APIView): @validate_serializer(ShareSubmissionSerializer) @login_required def put(self, request): + """ + share submission + """ try: submission = Submission.objects.select_related("problem").get(id=request.data["id"]) except Submission.DoesNotExist: From 8e026d771107ea317ca0de2002c2ccfb5db01d1e Mon Sep 17 00:00:00 2001 From: zema1 Date: Tue, 31 Oct 2017 20:47:47 +0800 Subject: [PATCH 069/106] =?UTF-8?q?=E5=90=88=E5=B9=B6=E9=83=A8=E5=88=86mig?= =?UTF-8?q?rations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0003_userprofile_total_score.py | 4 +++ .../0004_remove_userprofile_time_zone.py | 19 ------------ account/migrations/0005_auto_20170830_1154.py | 2 +- account/migrations/0006_user_session_keys.py | 17 ++++++++++- account/migrations/0007_auto_20170920_0254.py | 30 ------------------- account/migrations/0008_auto_20171011_1214.py | 2 +- .../migrations/0002_auto_20171011_1214.py | 4 +++ contest/migrations/0006_auto_20171011_1214.py | 4 +++ oj/settings.py | 4 +-- .../migrations/0002_auto_20170509_1203.py | 18 +++++++++++ .../migrations/0003_auto_20170704_1243.py | 24 --------------- .../migrations/0004_auto_20170717_1324.py | 24 --------------- .../migrations/0005_submission_username.py | 2 +- .../migrations/0007_auto_20170923_1318.py | 11 +++++++ .../migrations/0008_auto_20171011_1214.py | 26 ---------------- 15 files changed, 62 insertions(+), 129 deletions(-) delete mode 100644 account/migrations/0004_remove_userprofile_time_zone.py delete mode 100644 account/migrations/0007_auto_20170920_0254.py delete mode 100644 submission/migrations/0003_auto_20170704_1243.py delete mode 100644 submission/migrations/0004_auto_20170717_1324.py delete mode 100644 submission/migrations/0008_auto_20171011_1214.py diff --git a/account/migrations/0003_userprofile_total_score.py b/account/migrations/0003_userprofile_total_score.py index f836b91..f7efe88 100644 --- a/account/migrations/0003_userprofile_total_score.py +++ b/account/migrations/0003_userprofile_total_score.py @@ -22,4 +22,8 @@ class Migration(migrations.Migration): old_name='accepted_problem_number', new_name='accepted_number', ), + migrations.RemoveField( + model_name='userprofile', + name='time_zone', + ) ] diff --git a/account/migrations/0004_remove_userprofile_time_zone.py b/account/migrations/0004_remove_userprofile_time_zone.py deleted file mode 100644 index 97345a4..0000000 --- a/account/migrations/0004_remove_userprofile_time_zone.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.6 on 2017-08-23 09:04 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('account', '0003_userprofile_total_score'), - ] - - operations = [ - migrations.RemoveField( - model_name='userprofile', - name='time_zone', - ), - ] diff --git a/account/migrations/0005_auto_20170830_1154.py b/account/migrations/0005_auto_20170830_1154.py index efc739d..1ba8a94 100644 --- a/account/migrations/0005_auto_20170830_1154.py +++ b/account/migrations/0005_auto_20170830_1154.py @@ -9,7 +9,7 @@ import jsonfield.fields class Migration(migrations.Migration): dependencies = [ - ('account', '0004_remove_userprofile_time_zone'), + ('account', '0003_userprofile_total_score'), ] operations = [ diff --git a/account/migrations/0006_user_session_keys.py b/account/migrations/0006_user_session_keys.py index 4a0282a..6dc991a 100644 --- a/account/migrations/0006_user_session_keys.py +++ b/account/migrations/0006_user_session_keys.py @@ -2,7 +2,7 @@ # Generated by Django 1.11.4 on 2017-09-16 06:22 from __future__ import unicode_literals -from django.db import migrations +from django.db import migrations, models import jsonfield.fields @@ -18,4 +18,19 @@ class Migration(migrations.Migration): name='session_keys', field=jsonfield.fields.JSONField(default=[]), ), + migrations.RenameField( + model_name='userprofile', + old_name='phone_number', + new_name='github', + ), + migrations.AlterField( + model_name='userprofile', + name='avatar', + field=models.CharField(default='/static/avatar/default.png', max_length=50), + ), + migrations.AlterField( + model_name='userprofile', + name='github', + field=models.CharField(blank=True, max_length=50, null=True), + ), ] diff --git a/account/migrations/0007_auto_20170920_0254.py b/account/migrations/0007_auto_20170920_0254.py deleted file mode 100644 index 896d0d8..0000000 --- a/account/migrations/0007_auto_20170920_0254.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.4 on 2017-09-20 02:54 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('account', '0006_user_session_keys'), - ] - - operations = [ - migrations.RenameField( - model_name='userprofile', - old_name='phone_number', - new_name='github', - ), - migrations.AlterField( - model_name='userprofile', - name='avatar', - field=models.CharField(default='/static/avatar/default.png', max_length=50), - ), - migrations.AlterField( - model_name='userprofile', - name='github', - field=models.CharField(blank=True, max_length=50, null=True), - ), - ] diff --git a/account/migrations/0008_auto_20171011_1214.py b/account/migrations/0008_auto_20171011_1214.py index 7426a1f..f27cac8 100644 --- a/account/migrations/0008_auto_20171011_1214.py +++ b/account/migrations/0008_auto_20171011_1214.py @@ -9,7 +9,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('account', '0007_auto_20170920_0254'), + ('account', '0006_user_session_keys'), ] operations = [ diff --git a/announcement/migrations/0002_auto_20171011_1214.py b/announcement/migrations/0002_auto_20171011_1214.py index ffe4c96..e2d5abe 100644 --- a/announcement/migrations/0002_auto_20171011_1214.py +++ b/announcement/migrations/0002_auto_20171011_1214.py @@ -17,4 +17,8 @@ class Migration(migrations.Migration): name='title', field=models.CharField(max_length=64), ), + migrations.AlterModelOptions( + name='announcement', + options={'ordering': ('-create_time',)}, + ), ] diff --git a/contest/migrations/0006_auto_20171011_1214.py b/contest/migrations/0006_auto_20171011_1214.py index d429742..0134a5b 100644 --- a/contest/migrations/0006_auto_20171011_1214.py +++ b/contest/migrations/0006_auto_20171011_1214.py @@ -23,4 +23,8 @@ class Migration(migrations.Migration): name='submission_info', field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), ), + migrations.AlterModelOptions( + name='contest', + options={'ordering': ('-start_time',)}, + ), ] diff --git a/oj/settings.py b/oj/settings.py index 9c9ac49..9d8ea61 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -12,13 +12,13 @@ https://docs.djangoproject.com/en/1.8/ref/settings/ import os from copy import deepcopy -from .custom_settings import * - if os.environ.get("OJ_ENV") == "production": from .production_settings import * else: from .dev_settings import * +from .custom_settings import * + BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Applications diff --git a/submission/migrations/0002_auto_20170509_1203.py b/submission/migrations/0002_auto_20170509_1203.py index 7ca5816..78dcbe9 100644 --- a/submission/migrations/0002_auto_20170509_1203.py +++ b/submission/migrations/0002_auto_20170509_1203.py @@ -17,4 +17,22 @@ class Migration(migrations.Migration): name='code', field=models.TextField(), ), + migrations.RenameField( + model_name='submission', + old_name='accepted_info', + new_name='statistic_info', + ), + migrations.RemoveField( + model_name='submission', + name='accepted_time', + ), + migrations.RenameField( + model_name='submission', + old_name='created_time', + new_name='create_time', + ), + migrations.AlterModelOptions( + name='submission', + options={'ordering': ('-create_time',)}, + ) ] diff --git a/submission/migrations/0003_auto_20170704_1243.py b/submission/migrations/0003_auto_20170704_1243.py deleted file mode 100644 index 58fa83c..0000000 --- a/submission/migrations/0003_auto_20170704_1243.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.6 on 2017-07-04 12:43 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('submission', '0002_auto_20170509_1203'), - ] - - operations = [ - migrations.RenameField( - model_name='submission', - old_name='accepted_info', - new_name='statistic_info', - ), - migrations.RemoveField( - model_name='submission', - name='accepted_time', - ), - ] diff --git a/submission/migrations/0004_auto_20170717_1324.py b/submission/migrations/0004_auto_20170717_1324.py deleted file mode 100644 index c8a5be3..0000000 --- a/submission/migrations/0004_auto_20170717_1324.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.6 on 2017-07-17 13:24 -from __future__ import unicode_literals - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('submission', '0003_auto_20170704_1243'), - ] - - operations = [ - migrations.RenameField( - model_name='submission', - old_name='created_time', - new_name='create_time', - ), - migrations.AlterModelOptions( - name='submission', - options={'ordering': ('-create_time',)}, - ) - ] diff --git a/submission/migrations/0005_submission_username.py b/submission/migrations/0005_submission_username.py index 46f63df..68a3243 100644 --- a/submission/migrations/0005_submission_username.py +++ b/submission/migrations/0005_submission_username.py @@ -8,7 +8,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('submission', '0004_auto_20170717_1324'), + ('submission', '0002_auto_20170509_1203'), ] operations = [ diff --git a/submission/migrations/0007_auto_20170923_1318.py b/submission/migrations/0007_auto_20170923_1318.py index d498e4c..6356680 100644 --- a/submission/migrations/0007_auto_20170923_1318.py +++ b/submission/migrations/0007_auto_20170923_1318.py @@ -2,6 +2,7 @@ # Generated by Django 1.11.4 on 2017-09-23 13:18 from __future__ import unicode_literals +import django.contrib.postgres.fields.jsonb from django.db import migrations, models import django.db.models.deletion @@ -33,4 +34,14 @@ class Migration(migrations.Migration): old_name='problem_id', new_name='problem', ), + migrations.AlterField( + model_name='submission', + name='info', + field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), + ), + migrations.AlterField( + model_name='submission', + name='statistic_info', + field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), + ), ] diff --git a/submission/migrations/0008_auto_20171011_1214.py b/submission/migrations/0008_auto_20171011_1214.py deleted file mode 100644 index 1c585d8..0000000 --- a/submission/migrations/0008_auto_20171011_1214.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.4 on 2017-10-11 12:14 -from __future__ import unicode_literals - -import django.contrib.postgres.fields.jsonb -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('submission', '0007_auto_20170923_1318'), - ] - - operations = [ - migrations.AlterField( - model_name='submission', - name='info', - field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), - ), - migrations.AlterField( - model_name='submission', - name='statistic_info', - field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), - ), - ] From 91d17bd3e89cd412bfa6ed1e05ba06d2cc103921 Mon Sep 17 00:00:00 2001 From: zema1 Date: Tue, 31 Oct 2017 21:08:06 +0800 Subject: [PATCH 070/106] add postgresql to travis CI --- .travis.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b0ba885..7782d02 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,13 @@ language: python python: - "3.6" +services: + - redis-server + - docker +before_install: + - docker pull postgres:10 + - docker run -it -d -e POSTGRES_DB=onlinejudge -e POSTGRES_USER=onlinejudge -e POSTGRES_PASSWORD=onlinejudge -p 127.0.0.1:5433:5432 postgres:10 install: - - sudo apt-get install -qq redis-server && redis-server & - pip install -r deploy/requirements.txt - mkdir log test_case upload - cp oj/custom_settings.example.py oj/custom_settings.py @@ -10,6 +15,7 @@ install: - python manage.py migrate - python manage.py initadmin script: + - docker ps -a - flake8 . - coverage run --include="$PWD/*" manage.py test - coverage report From 225d68b413735a5544f3f094525890f57d883ebb Mon Sep 17 00:00:00 2001 From: zema1 Date: Wed, 1 Nov 2017 18:35:27 +0800 Subject: [PATCH 071/106] tiny work --- problem/views/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/problem/views/admin.py b/problem/views/admin.py index 1d26cbe..3acd4b5 100644 --- a/problem/views/admin.py +++ b/problem/views/admin.py @@ -173,7 +173,7 @@ class ProblemAPI(APIView): except Problem.DoesNotExist: return self.error("Problem does not exist") - problems = Problem.objects.all().order_by("-create_time") + problems = Problem.objects.filter(contest_id__isnull=True).order_by("-create_time") if not user.can_mgmt_all_problem(): problems = problems.filter(created_by=user) keyword = request.GET.get("keyword") From b86ebf0ed7d9b12ba663f15a56ecca9cc10dd61d Mon Sep 17 00:00:00 2001 From: zema1 Date: Wed, 1 Nov 2017 22:25:14 +0800 Subject: [PATCH 072/106] =?UTF-8?q?=E9=A2=98=E7=9B=AEAC=E5=90=8E=E4=B8=8D?= =?UTF-8?q?=E8=AE=A1=E5=85=A5AC=E8=AE=A1=E6=95=B0=E5=99=A8=EF=BC=9B=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=9B=BE=E7=89=87=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/serializers.py | 4 ++-- account/views/oj.py | 6 +++--- deploy/run.sh | 2 +- judge/dispatcher.py | 6 ++++-- oj/dev_settings.py | 3 +++ oj/production_settings.py | 3 +++ oj/settings.py | 2 +- oj/urls.py | 1 + utils/urls.py | 7 +++++++ utils/views.py | 44 +++++++++++++++++++++++++++++++++++++++ 10 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 utils/urls.py create mode 100644 utils/views.py diff --git a/account/serializers.py b/account/serializers.py index 9c2cd15..f144767 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -106,8 +106,8 @@ class TwoFactorAuthCodeSerializer(serializers.Serializer): code = serializers.IntegerField() -class AvatarUploadForm(forms.Form): - file = forms.FileField() +class ImageUploadForm(forms.Form): + image = forms.FileField() class RankInfoSerializer(serializers.ModelSerializer): diff --git a/account/views/oj.py b/account/views/oj.py index de1df67..abbad3d 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -23,7 +23,7 @@ from ..serializers import (ApplyResetPasswordSerializer, ResetPasswordSerializer UserRegisterSerializer, UsernameOrEmailCheckSerializer, RankInfoSerializer, UserChangeEmailSerializer) from ..serializers import (TwoFactorAuthCodeSerializer, UserProfileSerializer, - EditUserProfileSerializer, AvatarUploadForm) + EditUserProfileSerializer, ImageUploadForm) from ..tasks import send_email_async @@ -62,9 +62,9 @@ class AvatarUploadAPI(APIView): @login_required def post(self, request): - form = AvatarUploadForm(request.POST, request.FILES) + form = ImageUploadForm(request.POST, request.FILES) if form.is_valid(): - avatar = form.cleaned_data["file"] + avatar = form.cleaned_data["image"] else: return self.error("Invalid file content") if avatar.size > 2 * 1024 * 1024: diff --git a/deploy/run.sh b/deploy/run.sh index e992e96..c95fe35 100644 --- a/deploy/run.sh +++ b/deploy/run.sh @@ -35,5 +35,5 @@ if [ $n -eq 3 ]; then exit 1 fi -chown -R nobody:nogroup /data/log /data/test_case /data/avatar +chown -R nobody:nogroup /data/log /data/test_case /data/avatar /data/upload exec supervisord -c /app/deploy/supervisor.conf diff --git a/judge/dispatcher.py b/judge/dispatcher.py index 3ae1d66..30b7444 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -176,13 +176,15 @@ class JudgeDispatcher(object): user_profile = user.userprofile if problem.rule_type == ProblemRuleType.ACM: user_profile.submission_number += 1 - if self.submission.result == JudgeStatus.ACCEPTED: - user_profile.accepted_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} + if self.submission.result == JudgeStatus.ACCEPTED: + user_profile.accepted_number += 1 elif acm_problems_status[problem_id]["status"] != JudgeStatus.ACCEPTED: acm_problems_status[problem_id]["status"] = self.submission.result + if self.submission.result == JudgeStatus.ACCEPTED: + user_profile.accepted_number += 1 user_profile.acm_problems_status["problems"] = acm_problems_status user_profile.save(update_fields=["submission_number", "accepted_number", "acm_problems_status"]) diff --git a/oj/dev_settings.py b/oj/dev_settings.py index 448bc8f..724a5dc 100644 --- a/oj/dev_settings.py +++ b/oj/dev_settings.py @@ -31,6 +31,9 @@ LOG_PATH = f"{BASE_DIR}/log/" AVATAR_URI_PREFIX = "/static/avatar" AVATAR_UPLOAD_DIR = f"{BASE_DIR}{AVATAR_URI_PREFIX}" +UPLOAD_PREFIX = "/static/upload" +UPLOAD_DIR = f"{BASE_DIR}{UPLOAD_PREFIX}" + STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static"), ] diff --git a/oj/production_settings.py b/oj/production_settings.py index bf8b3f8..3f9dee6 100644 --- a/oj/production_settings.py +++ b/oj/production_settings.py @@ -28,6 +28,9 @@ ALLOWED_HOSTS = ['*'] AVATAR_URI_PREFIX = "/static/avatar" AVATAR_UPLOAD_DIR = "/data/avatar" +UPLOAD_PREFIX = "/static/upload" +UPLOAD_DIR = "/data/upload" + TEST_CASE_DIR = "/data/test_case" LOG_PATH = "/data/log" DEFAULT_JUDGE_SERVER_SERVICE_URL = "http://judge-server:8080/" diff --git a/oj/settings.py b/oj/settings.py index 9d8ea61..fd216dd 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -31,7 +31,7 @@ VENDOR_APPS = ( 'rest_framework', ) LOCAL_APPS = ( - 'account', + 'account', 'announcement', 'conf', 'problem', diff --git a/oj/urls.py b/oj/urls.py index 3a2570b..ffe526f 100644 --- a/oj/urls.py +++ b/oj/urls.py @@ -12,4 +12,5 @@ urlpatterns = [ url(r"^api/admin/", include("contest.urls.admin")), url(r"^api/", include("contest.urls.oj")), url(r"^api/", include("submission.urls.oj")), + url(r"^api/admin/", include("utils.urls")), ] diff --git a/utils/urls.py b/utils/urls.py new file mode 100644 index 0000000..ca9fb0f --- /dev/null +++ b/utils/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url + +from .views import SimditorImageUploadAPIView + +urlpatterns = [ + url(r"^upload_image/?$", SimditorImageUploadAPIView.as_view(), name="upload_image") +] diff --git a/utils/views.py b/utils/views.py new file mode 100644 index 0000000..c3e3861 --- /dev/null +++ b/utils/views.py @@ -0,0 +1,44 @@ +import os +from django.conf import settings +from account.serializers import ImageUploadForm +from utils.shortcuts import rand_str +from utils.api import CSRFExemptAPIView +import logging + +logger = logging.getLogger(__name__) + + +class SimditorImageUploadAPIView(CSRFExemptAPIView): + request_parsers = () + + def post(self, request): + form = ImageUploadForm(request.POST, request.FILES) + if form.is_valid(): + img = form.cleaned_data["image"] + else: + return self.response({ + "success": False, + "msg": "Upload failed", + "file_path": ""}) + + suffix = os.path.splitext(img.name)[-1].lower() + if suffix not in [".gif", ".jpg", ".jpeg", ".bmp", ".png"]: + return self.response({ + "success": False, + "msg": "Unsupported file format", + "file_path": ""}) + img_name = rand_str(10) + suffix + try: + with open(os.path.join(settings.UPLOAD_DIR, img_name), "wb") as imgFile: + for chunk in img: + imgFile.write(chunk) + except IOError as e: + logger.error(e) + return self.response({ + "success": True, + "msg": "Upload Error", + "file_path": f"{settings.UPLOAD_PREFIX}/{img_name}"}) + return self.response({ + "success": True, + "msg": "Success", + "file_path": f"{settings.UPLOAD_PREFIX}/{img_name}"}) From 70f52b6f2718a548be6422ff35097b5f58426049 Mon Sep 17 00:00:00 2001 From: zema1 Date: Thu, 2 Nov 2017 15:29:08 +0800 Subject: [PATCH 073/106] =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=90=8D=E4=B8=8D?= =?UTF-8?q?=E5=8C=BA=E5=88=86=E5=A4=A7=E5=B0=8F=E5=86=99=EF=BC=9B=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=9B=B4=E6=96=B0problem=E6=97=B6=E7=9A=84?= =?UTF-8?q?=E4=B8=80=E4=BA=9B=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/models.py | 2 +- account/views/oj.py | 7 +- oj/settings.py | 9 +-- problem/serializers.py | 7 +- problem/views/admin.py | 156 +++++++++++++++++++++++------------------ 5 files changed, 103 insertions(+), 78 deletions(-) diff --git a/account/models.py b/account/models.py index 92e3df3..4f991d7 100644 --- a/account/models.py +++ b/account/models.py @@ -20,7 +20,7 @@ class UserManager(models.Manager): use_in_migrations = True def get_by_natural_key(self, username): - return self.get(**{self.model.USERNAME_FIELD: username}) + return self.get(**{f"{self.model.USERNAME_FIELD}__iexact": username}) class User(AbstractBaseUser): diff --git a/account/views/oj.py b/account/views/oj.py index abbad3d..76d363f 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -198,7 +198,7 @@ class UsernameOrEmailCheck(APIView): if data.get("username"): result["username"] = User.objects.filter(username=data["username"]).exists() if data.get("email"): - result["email"] = User.objects.filter(email=data["email"]).exists() + result["email"] = User.objects.filter(email=data["email"].lower()).exists() return self.success(result) @@ -218,9 +218,9 @@ class UserRegisterAPI(APIView): return self.error("Invalid captcha") if User.objects.filter(username=data["username"]).exists(): return self.error("Username already exists") + data["email"] = data["email"].lower() if User.objects.filter(email=data["email"]).exists(): return self.error("Email already exists") - user = User.objects.create(username=data["username"], email=data["email"]) user.set_password(data["password"]) user.save() @@ -240,6 +240,7 @@ class UserChangeEmailAPI(APIView): return self.error("tfa_required") if not OtpAuth(user.tfa_token).valid_totp(data["tfa_code"]): return self.error("Invalid two factor verification code") + data["new_email"] = data["new_email"].lower() if User.objects.filter(email=data["new_email"]).exists(): return self.error("The email is owned by other account") user.email = data["new_email"] @@ -280,7 +281,7 @@ class ApplyResetPasswordAPI(APIView): if not captcha.check(data["captcha"]): return self.error("Invalid captcha") try: - user = User.objects.get(email=data["email"]) + user = User.objects.get(email__iexact=data["email"]) except User.DoesNotExist: return self.error("User does not exist") if user.reset_password_token_expire_time and 0 < int( diff --git a/oj/settings.py b/oj/settings.py index fd216dd..0c6e768 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -118,7 +118,9 @@ LOGGING = { 'disable_existing_loggers': False, 'formatters': { 'standard': { - 'format': '%(asctime)s [%(threadName)s:%(thread)d] [%(name)s:%(lineno)d] [%(module)s:%(funcName)s] [%(levelname)s]- %(message)s'} + 'format': '%(asctime)s [%(threadName)s:%(thread)d] [%(name)s:%(lineno)d] [%(module)s:%(funcName)s] [%(levelname)s]- %(message)s', + 'datefmt': '%Y-%m-%d %H:%M:%S' + } }, 'handlers': { 'django_error': { @@ -145,11 +147,6 @@ LOGGING = { 'level': 'WARNING', 'propagate': True, }, - 'django.server': { - 'handlers': ['django_error', 'console'], - 'level': 'ERROR', - 'propagate': True, - }, 'django.db.backends': { 'handlers': ['django_error', 'console'], 'level': 'WARNING', diff --git a/problem/serializers.py b/problem/serializers.py index 671ed88..b9ebfd0 100644 --- a/problem/serializers.py +++ b/problem/serializers.py @@ -39,7 +39,7 @@ class CreateOrEditProblemSerializer(serializers.Serializer): input_description = serializers.CharField() output_description = serializers.CharField() samples = serializers.ListField(child=CreateSampleSerializer(), allow_empty=False) - test_case_id = serializers.CharField(min_length=32, max_length=32) + test_case_id = serializers.CharField(max_length=32) test_case_score = serializers.ListField(child=CreateTestCaseScoreSerializer(), allow_empty=False) time_limit = serializers.IntegerField(min_value=1, max_value=1000 * 60) memory_limit = serializers.IntegerField(min_value=1, max_value=1024) @@ -68,6 +68,11 @@ class CreateContestProblemSerializer(CreateOrEditProblemSerializer): contest_id = serializers.IntegerField() +class EditContestProblemSerializer(CreateOrEditProblemSerializer): + id = serializers.IntegerField() + contest_id = serializers.IntegerField() + + class TagSerializer(serializers.ModelSerializer): class Meta: model = ProblemTag diff --git a/problem/views/admin.py b/problem/views/admin.py index 3acd4b5..af7708e 100644 --- a/problem/views/admin.py +++ b/problem/views/admin.py @@ -12,7 +12,7 @@ from utils.shortcuts import rand_str from ..models import Problem, ProblemRuleType, ProblemTag from ..serializers import (CreateContestProblemSerializer, ContestProblemAdminSerializer, - CreateProblemSerializer, EditProblemSerializer, + CreateProblemSerializer, EditProblemSerializer, EditContestProblemSerializer, ProblemAdminSerializer, TestCaseUploadForm) @@ -109,26 +109,14 @@ class TestCaseUploadAPI(CSRFExemptAPIView): return self.success({"id": test_case_id, "info": ret, "hint": hint, "spj": spj}) -class ProblemAPI(APIView): - @validate_serializer(CreateProblemSerializer) - @problem_permission_required - def post(self, request): +class ProblemBase(APIView): + def common_checks(self, request): data = request.data - - _id = data["_id"] - if _id: - try: - Problem.objects.get(_id=_id) - return self.error("Display ID already exists") - except Problem.DoesNotExist: - pass - else: - data["_id"] = rand_str(8) - if data["spj"]: if not data["spj_language"] or not data["spj_code"]: - return self.error("Invalid spj") - data["spj_version"] = hashlib.md5((data["spj_language"] + ":" + data["spj_code"]).encode("utf-8")).hexdigest() + return "Invalid spj" + data["spj_version"] = hashlib.md5( + (data["spj_language"] + ":" + data["spj_code"]).encode("utf-8")).hexdigest() else: data["spj_language"] = None data["spj_code"] = None @@ -136,21 +124,33 @@ class ProblemAPI(APIView): total_score = 0 for item in data["test_case_score"]: if item["score"] <= 0: - return self.error("Invalid score") + return "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") - data["languages"] = list(data["languages"]) - problem = Problem.objects.create(**data) +class ProblemAPI(ProblemBase): + @validate_serializer(CreateProblemSerializer) + @problem_permission_required + def post(self, request): + data = request.data + + _id = data["_id"] if not _id: - problem._id = str(problem.id) - problem.save() + return self.error("Display ID is required") + if Problem.objects.filter(_id=_id, contest_id__isnull=True).exists(): + return self.error("Display ID already exists") + + error_info = self.common_checks(request) + if error_info: + return self.error(error_info) + + # todo check filename and score info + tags = data.pop("tags") + problem = Problem.objects.create(**data) for item in tags: try: @@ -196,31 +196,14 @@ class ProblemAPI(APIView): return self.error("Problem does not exist") _id = data["_id"] - if _id: - try: - Problem.objects.exclude(id=problem_id).get(_id=_id) - return self.error("Display ID already exists") - except Problem.DoesNotExist: - pass - else: - data["_id"] = str(problem_id) + if not _id: + return self.error("Display ID is required") + if Problem.objects.exclude(id=problem_id).filter(_id=_id, contest_id__isnull=True).exists(): + return self.error("Display ID already exists") - if data["spj"]: - if not data["spj_language"] or not data["spj_code"]: - return self.error("Invalid spj") - data["spj_version"] = hashlib.md5((data["spj_language"] + ":" + data["spj_code"]).encode("utf-8")).hexdigest() - else: - 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 + error_info = self.common_checks(request) + if error_info: + return self.error(error_info) # todo check filename and score info tags = data.pop("tags") data["languages"] = list(data["languages"]) @@ -240,11 +223,11 @@ class ProblemAPI(APIView): return self.success() -class ContestProblemAPI(APIView): +class ContestProblemAPI(ProblemBase): @validate_serializer(CreateContestProblemSerializer) + @problem_permission_required def post(self, request): data = request.data - try: contest = Contest.objects.get(id=data.pop("contest_id")) if request.user.is_admin() and contest.created_by != request.user: @@ -257,30 +240,18 @@ class ContestProblemAPI(APIView): _id = data["_id"] if not _id: - return self.error("Display id is required for contest problem") + return self.error("Display ID is required") if Problem.objects.filter(_id=_id, contest=contest).exists(): return self.error("Duplicate Display id") - if data["spj"]: - if not data["spj_language"] or not data["spj_code"]: - return self.error("Invalid spj") - data["spj_version"] = hashlib.md5((data["spj_language"] + ":" + data["spj_code"]).encode("utf-8")).hexdigest() - else: - data["spj_language"] = None - data["spj_code"] = None + error_info = self.common_checks(request) + if error_info: + return self.error(error_info) - if data["rule_type"] == ProblemRuleType.OI: - for item in data["test_case_score"]: - if item["score"] <= 0: - return self.error("Invalid score") # todo check filename and score info - - data["created_by"] = request.user data["contest"] = contest tags = data.pop("tags") - data["languages"] = list(data["languages"]) - problem = Problem.objects.create(**data) for item in tags: @@ -291,6 +262,7 @@ class ContestProblemAPI(APIView): problem.tags.add(tag) return self.success(ContestProblemAdminSerializer(problem).data) + @problem_permission_required def get(self, request): problem_id = request.GET.get("id") contest_id = request.GET.get("contest_id") @@ -314,3 +286,53 @@ class ContestProblemAPI(APIView): if keyword: problems = problems.filter(title__contains=keyword) return self.success(self.paginate_data(request, problems, ContestProblemAdminSerializer)) + + @validate_serializer(EditContestProblemSerializer) + @problem_permission_required + def put(self, request): + data = request.data + try: + contest = Contest.objects.get(id=data.pop("contest_id")) + if request.user.is_admin() and contest.created_by != request.user: + return self.error("Contest does not exist") + except Contest.DoesNotExist: + return self.error("Contest does not exist") + + if data["rule_type"] != contest.rule_type: + return self.error("Invalid rule type") + + problem_id = data.pop("id") + user = request.user + + try: + problem = Problem.objects.get(id=problem_id, contest=contest) + if not user.can_mgmt_all_problem() and problem.created_by != user: + return self.error("Problem does not exist") + except Problem.DoesNotExist: + return self.error("Problem does not exist") + + _id = data["_id"] + if not _id: + return self.error("Display ID is required") + if Problem.objects.exclude(id=problem_id).filter(_id=_id, contest=contest).exists(): + return self.error("Display ID already exists") + + error_info = self.common_checks(request) + if error_info: + return self.error(error_info) + # todo check filename and score info + tags = data.pop("tags") + data["languages"] = list(data["languages"]) + + for k, v in data.items(): + setattr(problem, k, v) + problem.save() + + problem.tags.remove(*problem.tags.all()) + for tag in tags: + try: + tag = ProblemTag.objects.get(name=tag) + except ProblemTag.DoesNotExist: + tag = ProblemTag.objects.create(name=tag) + problem.tags.add(tag) + return self.success() From cec27407e190029c0feda47f00bccbac91204624 Mon Sep 17 00:00:00 2001 From: zema1 Date: Thu, 2 Nov 2017 21:37:47 +0800 Subject: [PATCH 074/106] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dcpp=E5=BC=95=E7=94=A8?= =?UTF-8?q?bits/stdc++=E6=97=B6=E7=9A=84=E7=BC=96=E8=AF=91=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=8C=20=E6=94=AF=E6=8C=81=E5=85=AC=E5=BC=80?= =?UTF-8?q?=E6=AF=94=E8=B5=9B=E9=A2=98=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- judge/languages.py | 6 +++--- problem/urls/admin.py | 3 ++- problem/views/admin.py | 28 +++++++++++++++++++++++++++- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/judge/languages.py b/judge/languages.py index 1c2fd30..2a4281d 100644 --- a/judge/languages.py +++ b/judge/languages.py @@ -23,7 +23,7 @@ int main() { "exe_name": "main", "max_cpu_time": 3000, "max_real_time": 5000, - "max_memory": 128 * 1024 * 1024, + "max_memory": 256 * 1024 * 1024, "compile_command": "/usr/bin/gcc -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c99 {src_path} -lm -o {exe_path}", }, "run": { @@ -59,7 +59,7 @@ _cpp_lang_config = { "exe_name": "main", "max_cpu_time": 3000, "max_real_time": 5000, - "max_memory": 128 * 1024 * 1024, + "max_memory": 256 * 1024 * 1024, "compile_command": "/usr/bin/g++ -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c++11 {src_path} -lm -o {exe_path}", }, "run": { @@ -100,7 +100,7 @@ _java_lang_config = { }, "run": { "command": "/usr/bin/java -cp {exe_dir} -Xss1M -Xms16M -Xmx{max_memory}k " - "-Djava.security.manager -Djava.security.policy==/etc/java_policy -Djava.awt.headless=true Main", + "-Djava.security.manager -Djava.security.policy=/etc/java_policy -Djava.awt.headless=true Main", "seccomp_rule": None, "env": ["MALLOC_ARENA_MAX=1"] } diff --git a/problem/urls/admin.py b/problem/urls/admin.py index ba0feeb..7cc9afa 100644 --- a/problem/urls/admin.py +++ b/problem/urls/admin.py @@ -1,9 +1,10 @@ from django.conf.urls import url -from ..views.admin import ContestProblemAPI, ProblemAPI, TestCaseUploadAPI +from ..views.admin import ContestProblemAPI, ProblemAPI, TestCaseUploadAPI, MakeContestProblemPublicAPIView urlpatterns = [ url(r"^test_case/upload/?$", TestCaseUploadAPI.as_view(), name="test_case_upload_api"), url(r"^problem/?$", ProblemAPI.as_view(), name="problem_admin_api"), url(r"^contest/problem/?$", ContestProblemAPI.as_view(), name="contest_problem_admin_api"), + url(r"^contest_problem/make_public/?$", MakeContestProblemPublicAPIView.as_view(), name="make_public_api"), ] diff --git a/problem/views/admin.py b/problem/views/admin.py index af7708e..c0c3d3d 100644 --- a/problem/views/admin.py +++ b/problem/views/admin.py @@ -305,7 +305,7 @@ class ContestProblemAPI(ProblemBase): user = request.user try: - problem = Problem.objects.get(id=problem_id, contest=contest) + problem = Problem.objects.get(id=problem_id) if not user.can_mgmt_all_problem() and problem.created_by != user: return self.error("Problem does not exist") except Problem.DoesNotExist: @@ -336,3 +336,29 @@ class ContestProblemAPI(ProblemBase): tag = ProblemTag.objects.create(name=tag) problem.tags.add(tag) return self.success() + + +class MakeContestProblemPublicAPIView(APIView): + @problem_permission_required + def post(self, request): + problem_id = request.data.get("problem_id") + if not problem_id: + return self.error("problem_id is required") + try: + problem = Problem.objects.get(id=problem_id) + except Problem.DoesNotExist: + return self.error("Problem does not exist") + if not problem.contest or problem.is_public: + return self.error("Alreay be a public problem") + problem.is_public = True + problem.save() + # https://docs.djangoproject.com/en/1.11/topics/db/queries/#copying-model-instances + tags = problem.tags.all() + problem.pk = None + problem.contest = None + problem.submission_number = problem.accepted_number = 0 + problem.statistic_info = {} + problem.save() + problem.tags.set(tags) + return self.success() + From 37d6dd84eea2a62cade67c28eb6592096aedde1f Mon Sep 17 00:00:00 2001 From: zema1 Date: Mon, 6 Nov 2017 19:05:21 +0800 Subject: [PATCH 075/106] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dcontest=20announcemen?= =?UTF-8?q?t=E7=9A=84=E4=B8=80=E4=BA=9B=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/decorators.py | 2 +- account/serializers.py | 14 ++++++------- .../0007_contestannouncement_visible.py | 20 +++++++++++++++++++ contest/models.py | 1 + contest/serializers.py | 10 +++++++++- contest/tests.py | 4 ++-- contest/views/admin.py | 20 ++++++++++++++++++- contest/views/oj.py | 12 ++++++++--- judge/languages.py | 2 +- problem/views/admin.py | 1 - submission/views/oj.py | 6 ++++++ utils/api/api.py | 2 +- 12 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 contest/migrations/0007_contestannouncement_visible.py diff --git a/account/decorators.py b/account/decorators.py index 4521530..99d2a8f 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -73,7 +73,7 @@ def check_contest_permission(check_type="details"): else: contest_id = request.GET.get("contest_id") if not contest_id: - return self.error("Parameter contest_id not exist.") + return self.error("Parameter contest_id doesn't exist.") try: # use self.contest to avoid query contest again in view. diff --git a/account/serializers.py b/account/serializers.py index f144767..c07e6ca 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -77,13 +77,13 @@ class EditUserSerializer(serializers.Serializer): class EditUserProfileSerializer(serializers.Serializer): - real_name = serializers.CharField(max_length=32, allow_blank=True) - avatar = serializers.CharField(max_length=256, allow_blank=True, required=False) - blog = serializers.URLField(max_length=256, allow_blank=True, required=False) - mood = serializers.CharField(max_length=256, allow_blank=True, required=False) - github = serializers.CharField(max_length=64, allow_blank=True, required=False) - school = serializers.CharField(max_length=64, allow_blank=True, required=False) - major = serializers.CharField(max_length=64, allow_blank=True, required=False) + real_name = serializers.CharField(max_length=32, allow_null=True, required=False) + avatar = serializers.CharField(max_length=256, allow_null=True, allow_blank=True, required=False) + blog = serializers.URLField(max_length=256, allow_null=True, allow_blank=True, required=False) + mood = serializers.CharField(max_length=256, allow_null=True, allow_blank=True, required=False) + github = serializers.CharField(max_length=64, allow_null=True, allow_blank=True, required=False) + school = serializers.CharField(max_length=64, allow_null=True, allow_blank=True, required=False) + major = serializers.CharField(max_length=64, allow_null=True, allow_blank=True, required=False) class ApplyResetPasswordSerializer(serializers.Serializer): diff --git a/contest/migrations/0007_contestannouncement_visible.py b/contest/migrations/0007_contestannouncement_visible.py new file mode 100644 index 0000000..679874f --- /dev/null +++ b/contest/migrations/0007_contestannouncement_visible.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-06 09:02 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contest', '0006_auto_20171011_1214'), + ] + + operations = [ + migrations.AddField( + model_name='contestannouncement', + name='visible', + field=models.BooleanField(default=True), + ), + ] diff --git a/contest/models.py b/contest/models.py index 1ed8ff9..aab9e7e 100644 --- a/contest/models.py +++ b/contest/models.py @@ -93,6 +93,7 @@ class ContestAnnouncement(models.Model): title = models.CharField(max_length=128) content = RichTextField() created_by = models.ForeignKey(User) + visible = models.BooleanField(default=True) create_time = models.DateTimeField(auto_now_add=True) class Meta: diff --git a/contest/serializers.py b/contest/serializers.py index 356a150..ba8df28 100644 --- a/contest/serializers.py +++ b/contest/serializers.py @@ -54,9 +54,17 @@ class ContestAnnouncementSerializer(serializers.ModelSerializer): class CreateContestAnnouncementSerializer(serializers.Serializer): + contest_id = serializers.IntegerField() title = serializers.CharField(max_length=128) content = serializers.CharField() - contest_id = serializers.IntegerField() + visible = serializers.BooleanField() + + +class EditContestAnnouncementSerializer(serializers.Serializer): + id = serializers.IntegerField() + title = serializers.CharField(max_length=128, required=False) + content = serializers.CharField(required=False, allow_blank=True) + visible = serializers.BooleanField(required=False) class ContestPasswordVerifySerializer(serializers.Serializer): diff --git a/contest/tests.py b/contest/tests.py index 9ce9890..f8c5b73 100644 --- a/contest/tests.py +++ b/contest/tests.py @@ -60,7 +60,7 @@ class ContestAPITest(APITestCase): self.create_admin() url = self.reverse("contest_admin_api") self.contest = self.client.post(url, data=DEFAULT_CONTEST_DATA).data["data"] - self.url = self.reverse("contest_api") + "?contest_id=" + str(self.contest["id"]) + self.url = self.reverse("contest_api") + "?id=" + str(self.contest["id"]) def test_get_contest_list(self): url = self.reverse("contest_list_api") @@ -100,7 +100,7 @@ class ContestAnnouncementAdminAPITest(APITestCase): self.create_super_admin() self.url = self.reverse("contest_announcement_admin_api") contest_id = self.create_contest().data["data"]["id"] - self.data = {"title": "test title", "content": "test content", "contest_id": contest_id} + self.data = {"title": "test title", "content": "test content", "contest_id": contest_id, "visible": True} def create_contest(self): url = self.reverse("contest_admin_api") diff --git a/contest/views/admin.py b/contest/views/admin.py index 58e8c7b..6b53abd 100644 --- a/contest/views/admin.py +++ b/contest/views/admin.py @@ -6,7 +6,8 @@ from ..models import Contest, ContestAnnouncement from ..serializers import (ContestAnnouncementSerializer, ContestAdminSerializer, CreateConetestSeriaizer, CreateContestAnnouncementSerializer, - EditConetestSeriaizer) + EditConetestSeriaizer, + EditContestAnnouncementSerializer) class ContestAPI(APIView): @@ -83,6 +84,23 @@ class ContestAnnouncementAPI(APIView): announcement = ContestAnnouncement.objects.create(**data) return self.success(ContestAnnouncementSerializer(announcement).data) + @validate_serializer(EditContestAnnouncementSerializer) + def put(self, request): + """ + update contest_announcement + """ + data = request.data + try: + contest_announcement = ContestAnnouncement.objects.get(id=data.pop("id")) + if request.user.is_admin() and contest_announcement.created_by != request.user: + return self.error("Contest announcement does not exist") + except ContestAnnouncement.DoesNotExist: + return self.error("Contest announcement does not exist") + for k, v in data.items(): + setattr(contest_announcement, k, v) + contest_announcement.save() + return self.success() + def delete(self, request): """ Delete one contest_announcement. diff --git a/contest/views/oj.py b/contest/views/oj.py index 342d30c..d50e0e9 100644 --- a/contest/views/oj.py +++ b/contest/views/oj.py @@ -17,7 +17,7 @@ class ContestAnnouncementListAPI(APIView): contest_id = request.GET.get("contest_id") if not contest_id: return self.error("Invalid parameter") - data = ContestAnnouncement.objects.select_related("created_by").filter(contest_id=contest_id) + data = ContestAnnouncement.objects.select_related("created_by").filter(contest_id=contest_id, visible=True) max_id = request.GET.get("max_id") if max_id: data = data.filter(id__gt=max_id) @@ -25,9 +25,15 @@ class ContestAnnouncementListAPI(APIView): class ContestAPI(APIView): - @check_contest_permission(check_type="details") def get(self, request): - return self.success(ContestSerializer(self.contest).data) + id = request.GET.get("id") + if not id: + return self.error("Invalid parameter") + try: + contest = Contest.objects.get(id=id) + return self.success(ContestSerializer(contest).data) + except Contest.DoesNotExist: + return self.error("Contest does not exist") class ContestListAPI(APIView): diff --git a/judge/languages.py b/judge/languages.py index 2a4281d..cc5f09b 100644 --- a/judge/languages.py +++ b/judge/languages.py @@ -59,7 +59,7 @@ _cpp_lang_config = { "exe_name": "main", "max_cpu_time": 3000, "max_real_time": 5000, - "max_memory": 256 * 1024 * 1024, + "max_memory": 512 * 1024 * 1024, "compile_command": "/usr/bin/g++ -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c++11 {src_path} -lm -o {exe_path}", }, "run": { diff --git a/problem/views/admin.py b/problem/views/admin.py index c0c3d3d..7d1ac67 100644 --- a/problem/views/admin.py +++ b/problem/views/admin.py @@ -361,4 +361,3 @@ class MakeContestProblemPublicAPIView(APIView): problem.save() problem.tags.set(tags) return self.success() - diff --git a/submission/views/oj.py b/submission/views/oj.py index e66aa5b..b0751c3 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -137,6 +137,7 @@ class SubmissionListAPI(APIView): problem_id = request.GET.get("problem_id") myself = request.GET.get("myself") result = request.GET.get("result") + username = request.GET.get("username") if problem_id: try: problem = Problem.objects.get(_id=problem_id, contest_id__isnull=True, visible=True) @@ -145,6 +146,8 @@ class SubmissionListAPI(APIView): submissions = submissions.filter(problem=problem) if myself and myself == "1": submissions = submissions.filter(user_id=request.user.id) + elif username: + submissions = submissions.filter(username=username) if result: submissions = submissions.filter(result=result) data = self.paginate_data(request, submissions) @@ -163,6 +166,7 @@ class ContestSubmissionListAPI(APIView): problem_id = request.GET.get("problem_id") myself = request.GET.get("myself") result = request.GET.get("result") + username = request.GET.get("username") if problem_id: try: problem = Problem.objects.get(_id=problem_id, contest_id=contest.id, visible=True) @@ -172,6 +176,8 @@ class ContestSubmissionListAPI(APIView): if myself and myself == "1": submissions = submissions.filter(user_id=request.user.id) + elif username: + submissions = submissions.filter(username=username) if result: submissions = submissions.filter(result=result) diff --git a/utils/api/api.py b/utils/api/api.py index 09671d2..e33daaf 100644 --- a/utils/api/api.py +++ b/utils/api/api.py @@ -111,7 +111,7 @@ class APIView(View): limit = int(request.GET.get("limit", "10")) except ValueError: limit = 10 - if limit < 0 or limit > 100: + if limit < 0 or limit > 250: limit = 10 try: offset = int(request.GET.get("offset", "0")) From c16543c8307498b9004689541e9ad105c7318281 Mon Sep 17 00:00:00 2001 From: zema1 Date: Mon, 6 Nov 2017 21:45:52 +0800 Subject: [PATCH 076/106] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dtest=5Fcase=E6=8E=92?= =?UTF-8?q?=E5=BA=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/views/admin.py | 18 +++--------------- announcement/views/admin.py | 7 +++---- problem/views/admin.py | 6 +++--- utils/shortcuts.py | 6 ++++++ 4 files changed, 15 insertions(+), 22 deletions(-) diff --git a/account/views/admin.py b/account/views/admin.py index 83f4a93..94af4b9 100644 --- a/account/views/admin.py +++ b/account/views/admin.py @@ -21,22 +21,10 @@ class UserAdminAPI(APIView): user = User.objects.get(id=data["id"]) except User.DoesNotExist: return self.error("User does not exist") - try: - user = User.objects.get(username=data["username"]) - if user.id != data["id"]: - return self.error("Username already exists") - except User.DoesNotExist: - pass - - try: - user = User.objects.get(email=data["email"]) - if user.id != data["id"]: - return self.error("Email already exists") - # Some old data has duplicate email - except MultipleObjectsReturned: + if User.objects.filter(username=data["username"]).exclude(id=user.id).exists(): + return self.error("Username already exists") + if User.objects.filter(email=data["email"].lower()).exclude(id=user.id).exists(): return self.error("Email already exists") - except User.DoesNotExist: - pass user.username = data["username"] user.email = data["email"] diff --git a/announcement/views/admin.py b/announcement/views/admin.py index 86d5430..58d3578 100644 --- a/announcement/views/admin.py +++ b/announcement/views/admin.py @@ -28,13 +28,12 @@ class AnnouncementAdminAPI(APIView): """ data = request.data try: - announcement = Announcement.objects.get(id=data["id"]) + announcement = Announcement.objects.get(id=data.pop("id")) except Announcement.DoesNotExist: return self.error("Announcement does not exist") - announcement.title = data["title"] - announcement.content = data["content"] - announcement.visible = data["visible"] + for k, v in data.items(): + setattr(announcement, k, v) announcement.save() return self.success(AnnouncementSerializer(announcement).data) diff --git a/problem/views/admin.py b/problem/views/admin.py index 7d1ac67..a7e24db 100644 --- a/problem/views/admin.py +++ b/problem/views/admin.py @@ -8,7 +8,7 @@ from django.conf import settings from account.decorators import problem_permission_required from contest.models import Contest from utils.api import APIView, CSRFExemptAPIView, validate_serializer -from utils.shortcuts import rand_str +from utils.shortcuts import rand_str, natural_sort_key from ..models import Problem, ProblemRuleType, ProblemTag from ..serializers import (CreateContestProblemSerializer, ContestProblemAdminSerializer, @@ -30,7 +30,7 @@ class TestCaseUploadAPI(CSRFExemptAPIView): prefix += 1 continue else: - return sorted(ret) + return sorted(ret, key=natural_sort_key) else: while True: in_name = str(prefix) + ".in" @@ -41,7 +41,7 @@ class TestCaseUploadAPI(CSRFExemptAPIView): prefix += 1 continue else: - return sorted(ret) + return sorted(ret, key=natural_sort_key) @problem_permission_required def post(self, request): diff --git a/utils/shortcuts.py b/utils/shortcuts.py index 8fc1ffa..9340564 100644 --- a/utils/shortcuts.py +++ b/utils/shortcuts.py @@ -1,3 +1,4 @@ +import re import datetime import random from base64 import b64encode @@ -57,3 +58,8 @@ def datetime2str(value, format="iso-8601"): def timestamp2utcstr(value): return datetime.datetime.utcfromtimestamp(value).isoformat() + + +def natural_sort_key(s, _nsre=re.compile(r"(\d+)")): + return [int(text) if text.isdigit() else text.lower() + for text in re.split(_nsre, s)] From 2d00ed802dfb812a64ef1f5a459110b2c19f00a8 Mon Sep 17 00:00:00 2001 From: zema1 Date: Tue, 7 Nov 2017 19:04:41 +0800 Subject: [PATCH 077/106] =?UTF-8?q?=E6=B7=BB=E5=8A=A0initinstall=E5=91=BD?= =?UTF-8?q?=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/views/admin.py | 1 - deploy/run.sh | 2 +- problem/serializers.py | 5 +++++ problem/views/admin.py | 15 ++++++++++----- submission/views/oj.py | 3 ++- utils/management/commands/initadmin.py | 22 ++++++++++------------ utils/management/commands/initinstall.py | 17 +++++++++++++++++ 7 files changed, 45 insertions(+), 20 deletions(-) create mode 100644 utils/management/commands/initinstall.py diff --git a/account/views/admin.py b/account/views/admin.py index 94af4b9..6ae3baa 100644 --- a/account/views/admin.py +++ b/account/views/admin.py @@ -1,4 +1,3 @@ -from django.core.exceptions import MultipleObjectsReturned from django.db.models import Q from utils.api import APIView, validate_serializer diff --git a/deploy/run.sh b/deploy/run.sh index c95fe35..c8ca9d5 100644 --- a/deploy/run.sh +++ b/deploy/run.sh @@ -26,7 +26,7 @@ if [ $? -ne 0 ]; then let "n+=1" continue fi -python manage.py initadmin +python manage.py initinstall break done diff --git a/problem/serializers.py b/problem/serializers.py index b9ebfd0..e78eb55 100644 --- a/problem/serializers.py +++ b/problem/serializers.py @@ -120,3 +120,8 @@ class ContestProblemSafeSerializer(BaseProblemSerializer): model = Problem exclude = ("test_case_score", "test_case_id", "visible", "is_public", "difficulty", "submission_number", "accepted_number", "statistic_info") + + +class ContestProblemMakePublicSerializer(serializers.Serializer): + id = serializers.IntegerField() + display_id = serializers.CharField(max_length=32) diff --git a/problem/views/admin.py b/problem/views/admin.py index a7e24db..cf94447 100644 --- a/problem/views/admin.py +++ b/problem/views/admin.py @@ -13,7 +13,7 @@ from utils.shortcuts import rand_str, natural_sort_key from ..models import Problem, ProblemRuleType, ProblemTag from ..serializers import (CreateContestProblemSerializer, ContestProblemAdminSerializer, CreateProblemSerializer, EditProblemSerializer, EditContestProblemSerializer, - ProblemAdminSerializer, TestCaseUploadForm) + ProblemAdminSerializer, TestCaseUploadForm, ContestProblemMakePublicSerializer) class TestCaseUploadAPI(CSRFExemptAPIView): @@ -339,15 +339,19 @@ class ContestProblemAPI(ProblemBase): class MakeContestProblemPublicAPIView(APIView): + @validate_serializer(ContestProblemMakePublicSerializer) @problem_permission_required def post(self, request): - problem_id = request.data.get("problem_id") - if not problem_id: - return self.error("problem_id is required") + data = request.data + display_id = data.get("display_id") + if Problem.objects.filter(_id=display_id, contest_id__isnull=True).exists(): + return self.error("Duplicate display ID") + try: - problem = Problem.objects.get(id=problem_id) + problem = Problem.objects.get(id=data["id"]) except Problem.DoesNotExist: return self.error("Problem does not exist") + if not problem.contest or problem.is_public: return self.error("Alreay be a public problem") problem.is_public = True @@ -356,6 +360,7 @@ class MakeContestProblemPublicAPIView(APIView): tags = problem.tags.all() problem.pk = None problem.contest = None + problem._id = display_id problem.submission_number = problem.accepted_number = 0 problem.statistic_info = {} problem.save() diff --git a/submission/views/oj.py b/submission/views/oj.py index b0751c3..fec15cc 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -4,6 +4,7 @@ from judge.tasks import judge_task # from judge.dispatcher import JudgeDispatcher from problem.models import Problem, ProblemRuleType from contest.models import Contest, ContestStatus, ContestRuleType +from options.options import SysOptions from utils.api import APIView, validate_serializer from utils.throttling import TokenBucket, BucketController from utils.captcha import Captcha @@ -144,7 +145,7 @@ class SubmissionListAPI(APIView): except Problem.DoesNotExist: return self.error("Problem doesn't exist") submissions = submissions.filter(problem=problem) - if myself and myself == "1": + if (myself and myself == "1") and not SysOptions.submission_list_show_all: submissions = submissions.filter(user_id=request.user.id) elif username: submissions = submissions.filter(username=username) diff --git a/utils/management/commands/initadmin.py b/utils/management/commands/initadmin.py index c29463a..78e8a45 100644 --- a/utils/management/commands/initadmin.py +++ b/utils/management/commands/initadmin.py @@ -1,4 +1,3 @@ -import os from django.core.management.base import BaseCommand from account.models import AdminType, ProblemPermission, User, UserProfile @@ -10,17 +9,16 @@ class Command(BaseCommand): try: admin = User.objects.get(username="root") if admin.admin_type == AdminType.SUPER_ADMIN: - if os.environ.get("OJ_ENV") != "production": - self.stdout.write(self.style.WARNING("Super admin user 'root' already exists, " - "would you like to reset it's password?\n" - "Input yes to confirm: ")) - if input() == "yes": - rand_password = "rootroot" - admin.save() - self.stdout.write(self.style.SUCCESS("Successfully created super admin user password.\n" - "Username: root\nPassword: %s\n" - "Remember to change password and turn on two factors auth " - "after installation." % rand_password)) + self.stdout.write(self.style.WARNING("Super admin user 'root' already exists, " + "would you like to reset it's password?\n" + "Input yes to confirm: ")) + if input() == "yes": + rand_password = "rootroot" + admin.save() + self.stdout.write(self.style.SUCCESS("Successfully created super admin user password.\n" + "Username: root\nPassword: %s\n" + "Remember to change password and turn on two factors auth " + "after installation." % rand_password)) else: self.stdout.write(self.style.SUCCESS("Nothing happened")) else: diff --git a/utils/management/commands/initinstall.py b/utils/management/commands/initinstall.py new file mode 100644 index 0000000..bdb0f0a --- /dev/null +++ b/utils/management/commands/initinstall.py @@ -0,0 +1,17 @@ +import os +from account.models import User +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + def handle(self, *args, **options): + if User.objects.exists(): + self.stdout.write(self.style.WARNING("Nothing happened\n")) + return + try: + if os.system("python manage.py initadmin") != 0: + self.stdout.write(self.style.ERROR("Failed to execute command 'initadmin'")) + exit(1) + self.stdout.write(self.style.SUCCESS("Done")) + except Exception as e: + self.stdout.write(self.style.ERROR("Failed to initialize, error: " + str(e))) From 343eff1c514ecfeb3d38fbf0e1d50e4705ad73bf Mon Sep 17 00:00:00 2001 From: zema1 Date: Wed, 8 Nov 2017 21:56:39 +0800 Subject: [PATCH 078/106] =?UTF-8?q?admin=E4=BF=AE=E6=94=B9username?= =?UTF-8?q?=E5=90=8Eupdate=20submissions=EF=BC=9B=20problem=20id=20refresh?= =?UTF-8?q?=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/urls/oj.py | 4 +++- account/views/admin.py | 4 ++++ account/views/oj.py | 19 +++++++++++++++++++ contest/views/oj.py | 4 ++-- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/account/urls/oj.py b/account/urls/oj.py index 0af54a5..c1915f4 100644 --- a/account/urls/oj.py +++ b/account/urls/oj.py @@ -4,7 +4,8 @@ from ..views.oj import (ApplyResetPasswordAPI, ResetPasswordAPI, UserChangePasswordAPI, UserRegisterAPI, UserChangeEmailAPI, UserLoginAPI, UserLogoutAPI, UsernameOrEmailCheck, AvatarUploadAPI, TwoFactorAuthAPI, UserProfileAPI, - UserRankAPI, CheckTFARequiredAPI, SessionManagementAPI) + UserRankAPI, CheckTFARequiredAPI, SessionManagementAPI, + ProfileProblemDisplayIDRefreshAPI) from utils.captcha.views import CaptchaAPIView @@ -19,6 +20,7 @@ urlpatterns = [ url(r"^captcha/?$", CaptchaAPIView.as_view(), name="show_captcha"), url(r"^check_username_or_email", UsernameOrEmailCheck.as_view(), name="check_username_or_email"), url(r"^profile/?$", UserProfileAPI.as_view(), name="user_profile_api"), + url(r"^profile/fresh_display_id", ProfileProblemDisplayIDRefreshAPI.as_view(), name="display_id_fresh"), url(r"^upload_avatar/?$", AvatarUploadAPI.as_view(), name="avatar_upload_api"), url(r"^tfa_required/?$", CheckTFARequiredAPI.as_view(), name="tfa_required_check"), url(r"^two_factor_auth/?$", TwoFactorAuthAPI.as_view(), name="two_factor_auth_api"), diff --git a/account/views/admin.py b/account/views/admin.py index 6ae3baa..9d1359f 100644 --- a/account/views/admin.py +++ b/account/views/admin.py @@ -1,5 +1,6 @@ from django.db.models import Q +from submission.models import Submission from utils.api import APIView, validate_serializer from utils.shortcuts import rand_str @@ -25,6 +26,7 @@ class UserAdminAPI(APIView): if User.objects.filter(email=data["email"].lower()).exclude(id=user.id).exists(): return self.error("Email already exists") + pre_username = user.username user.username = data["username"] user.email = data["email"] user.admin_type = data["admin_type"] @@ -58,6 +60,8 @@ class UserAdminAPI(APIView): user.two_factor_auth = data["two_factor_auth"] user.save() + if pre_username != user.username: + Submission.objects.filter(username=pre_username).update(username=user.username) return self.success(UserSerializer(user).data) @super_admin_required diff --git a/account/views/oj.py b/account/views/oj.py index 76d363f..50aa9db 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -11,6 +11,7 @@ from django.utils.timezone import now from django.views.decorators.csrf import ensure_csrf_cookie from otpauth import OtpAuth +from problem.models import Problem from utils.constants import ContestRuleType from options.options import SysOptions from utils.api import APIView, validate_serializer @@ -379,3 +380,21 @@ class UserRankAPI(APIView): else: profiles = profiles.filter(total_score__gt=0).order_by("-total_score") return self.success(self.paginate_data(request, profiles, RankInfoSerializer)) + + +class ProfileProblemDisplayIDRefreshAPI(APIView): + @login_required + def get(self, request): + profile = request.user.userprofile + acm_problems = profile.acm_problems_status["problems"] + oi_problems = profile.oi_problems_status["problems"] + ids = list(acm_problems.keys()) + list(oi_problems.keys()) + display_ids = Problem.objects.filter(id__in=ids).values_list("_id", flat=True) + id_map = dict(zip(ids, display_ids)) + print(id_map) + for k, v in acm_problems.items(): + v["_id"] = id_map[k] + for k, v in oi_problems.items(): + v["_id"] = id_map[k] + profile.save(update_fields=["acm_problems_status", "oi_problems_status"]) + return self.success() diff --git a/contest/views/oj.py b/contest/views/oj.py index d50e0e9..612ebd4 100644 --- a/contest/views/oj.py +++ b/contest/views/oj.py @@ -16,7 +16,7 @@ class ContestAnnouncementListAPI(APIView): def get(self, request): contest_id = request.GET.get("contest_id") if not contest_id: - return self.error("Invalid parameter") + return self.error("Invalid parameter, contest_id is required") data = ContestAnnouncement.objects.select_related("created_by").filter(contest_id=contest_id, visible=True) max_id = request.GET.get("max_id") if max_id: @@ -28,7 +28,7 @@ class ContestAPI(APIView): def get(self, request): id = request.GET.get("id") if not id: - return self.error("Invalid parameter") + return self.error("Invalid parameter, id is required") try: contest = Contest.objects.get(id=id) return self.success(ContestSerializer(contest).data) From 48f65d1a1434a75f4755ca92253d1dbd152032b6 Mon Sep 17 00:00:00 2001 From: zema1 Date: Thu, 9 Nov 2017 11:21:41 +0800 Subject: [PATCH 079/106] fix error in refresh displayID --- account/views/oj.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/account/views/oj.py b/account/views/oj.py index 50aa9db..eccbb3f 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -385,13 +385,19 @@ class UserRankAPI(APIView): class ProfileProblemDisplayIDRefreshAPI(APIView): @login_required def get(self, request): + user_id = request.GET.get("user_id") + if not user_id: + return self.error("Invalid parameter, user_id is required") + if request.user.id != user_id: + return self.error("Only user self can require a refresh") profile = request.user.userprofile - acm_problems = profile.acm_problems_status["problems"] - oi_problems = profile.oi_problems_status["problems"] + acm_problems = profile.acm_problems_status.get("problems", {}) + oi_problems = profile.oi_problems_status.get("problems", {}) ids = list(acm_problems.keys()) + list(oi_problems.keys()) + if not ids: + return self.success() display_ids = Problem.objects.filter(id__in=ids).values_list("_id", flat=True) id_map = dict(zip(ids, display_ids)) - print(id_map) for k, v in acm_problems.items(): v["_id"] = id_map[k] for k, v in oi_problems.items(): From 727fbf48d84ce52ed64033dcfdf5edb398357280 Mon Sep 17 00:00:00 2001 From: zema1 Date: Fri, 10 Nov 2017 19:40:54 +0800 Subject: [PATCH 080/106] =?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 99d2a8f..a8164d7 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 4f991d7..eb92f75 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 eccbb3f..a451553 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 0000000..fd6c6ff --- /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 aab9e7e..2f07b37 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 ba8df28..abbdccc 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 f8c5b73..3b7b1e2 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 6b53abd..d5ce347 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 30b7444..a602bab 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 0000000..e60841b --- /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 fe6a991..0c4aeda 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 21bd75d..2b67ab8 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 fec15cc..a0deb0a 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) From ffd594349263146758b6542a0d617112c1c89cca Mon Sep 17 00:00:00 2001 From: zema1 Date: Mon, 13 Nov 2017 21:29:27 +0800 Subject: [PATCH 081/106] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dsample=E7=A9=BA?= =?UTF-8?q?=E6=A0=BC=E8=A2=AB=E5=90=83=E6=8E=89=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=9B=20=E4=BF=AE=E5=A4=8Dtest=5Fid=20=E4=B8=8D=E5=AF=B9?= =?UTF-8?q?=E5=BA=94=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contest/views/oj.py | 5 ++++- judge/dispatcher.py | 3 ++- problem/serializers.py | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/contest/views/oj.py b/contest/views/oj.py index 612ebd4..66889d5 100644 --- a/contest/views/oj.py +++ b/contest/views/oj.py @@ -2,6 +2,7 @@ from django.utils.timezone import now from django.core.cache import cache from utils.api import APIView, validate_serializer from utils.constants import CacheKey +from utils.shortcuts import datetime2str from account.decorators import login_required, check_contest_permission from utils.constants import ContestRuleType, ContestStatus @@ -31,9 +32,11 @@ class ContestAPI(APIView): return self.error("Invalid parameter, id is required") try: contest = Contest.objects.get(id=id) - return self.success(ContestSerializer(contest).data) except Contest.DoesNotExist: return self.error("Contest does not exist") + data = ContestSerializer(contest).data + data["now"] = datetime2str(now()) + return self.success(data) class ContestListAPI(APIView): diff --git a/judge/dispatcher.py b/judge/dispatcher.py index a602bab..4011e4c 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -124,12 +124,13 @@ class JudgeDispatcher(object): if not service_url: service_url = settings.DEFAULT_JUDGE_SERVER_SERVICE_URL resp = self._request(urljoin(service_url, "/judge"), data=data) - self.submission.info = resp if resp["err"]: self.submission.result = JudgeStatus.COMPILE_ERROR self.submission.statistic_info["err_info"] = resp["data"] self.submission.statistic_info["score"] = 0 else: + resp["data"].sort(key=lambda x: int(x["test_case"])) + self.submission.info = resp self._compute_statistic_info(resp["data"]) error_test_case = list(filter(lambda case: case["result"] != 0, resp["data"])) # ACM模式下,多个测试点全部正确则AC,否则取第一个错误的测试点的状态 diff --git a/problem/serializers.py b/problem/serializers.py index e78eb55..529d3f5 100644 --- a/problem/serializers.py +++ b/problem/serializers.py @@ -12,8 +12,8 @@ class TestCaseUploadForm(forms.Form): class CreateSampleSerializer(serializers.Serializer): - input = serializers.CharField() - output = serializers.CharField() + input = serializers.CharField(trim_whitespace=False) + output = serializers.CharField(trim_whitespace=False) class CreateTestCaseScoreSerializer(serializers.Serializer): From 4dc1e2687b372044ce320c1d7424a192cf227145 Mon Sep 17 00:00:00 2001 From: zema1 Date: Tue, 14 Nov 2017 21:06:33 +0800 Subject: [PATCH 082/106] test_case download api --- problem/tests.py | 6 +++--- problem/urls/admin.py | 4 ++-- problem/views/admin.py | 27 ++++++++++++++++++++++++++- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/problem/tests.py b/problem/tests.py index 8ff2017..7595260 100644 --- a/problem/tests.py +++ b/problem/tests.py @@ -11,7 +11,7 @@ from utils.api.tests import APITestCase from .models import ProblemTag from .models import Problem, ProblemRuleType -from .views.admin import TestCaseUploadAPI +from .views.admin import TestCaseAPI from contest.models import Contest from contest.tests import DEFAULT_CONTEST_DATA @@ -75,8 +75,8 @@ class ProblemTagListAPITest(APITestCase): class TestCaseUploadAPITest(APITestCase): def setUp(self): - self.api = TestCaseUploadAPI() - self.url = self.reverse("test_case_upload_api") + self.api = TestCaseAPI() + self.url = self.reverse("test_case_api") self.create_super_admin() def test_filter_file_name(self): diff --git a/problem/urls/admin.py b/problem/urls/admin.py index 7cc9afa..5dbc444 100644 --- a/problem/urls/admin.py +++ b/problem/urls/admin.py @@ -1,9 +1,9 @@ from django.conf.urls import url -from ..views.admin import ContestProblemAPI, ProblemAPI, TestCaseUploadAPI, MakeContestProblemPublicAPIView +from ..views.admin import ContestProblemAPI, ProblemAPI, TestCaseAPI, MakeContestProblemPublicAPIView urlpatterns = [ - url(r"^test_case/upload/?$", TestCaseUploadAPI.as_view(), name="test_case_upload_api"), + url(r"^test_case/?$", TestCaseAPI.as_view(), name="test_case_api"), url(r"^problem/?$", ProblemAPI.as_view(), name="problem_admin_api"), url(r"^contest/problem/?$", ContestProblemAPI.as_view(), name="contest_problem_admin_api"), url(r"^contest_problem/make_public/?$", MakeContestProblemPublicAPIView.as_view(), name="make_public_api"), diff --git a/problem/views/admin.py b/problem/views/admin.py index cf94447..d9ae3f2 100644 --- a/problem/views/admin.py +++ b/problem/views/admin.py @@ -2,8 +2,10 @@ import hashlib import json import os import zipfile +from wsgiref.util import FileWrapper from django.conf import settings +from django.http import StreamingHttpResponse from account.decorators import problem_permission_required from contest.models import Contest @@ -16,7 +18,7 @@ from ..serializers import (CreateContestProblemSerializer, ContestProblemAdminSe ProblemAdminSerializer, TestCaseUploadForm, ContestProblemMakePublicSerializer) -class TestCaseUploadAPI(CSRFExemptAPIView): +class TestCaseAPI(CSRFExemptAPIView): request_parsers = () def filter_name_list(self, name_list, spj): @@ -43,6 +45,29 @@ class TestCaseUploadAPI(CSRFExemptAPIView): else: return sorted(ret, key=natural_sort_key) + @problem_permission_required + def get(self, request): + problem_id = request.GET.get("problem_id") + if not problem_id: + return self.error("Parameter error, problem_id is required") + try: + problem = Problem.objects.get(id=problem_id) + except Problem.DoesNotExist: + return self.error("Problem does not exists") + + test_case_dir = os.path.join(settings.TEST_CASE_DIR, problem.test_case_id) + if not os.path.isdir(test_case_dir): + return self.error("Test case does not exists") + name_list = self.filter_name_list(os.listdir(test_case_dir), problem.spj) + name_list.append("info") + file_name = os.path.join(test_case_dir, problem.test_case_id) + with zipfile.ZipFile(file_name, "w") as file: + for test_case in name_list: + file.write(f"{test_case_dir}/{test_case}", test_case) + response = StreamingHttpResponse(FileWrapper(open(file_name, "rb")), content_type="application/zip") + response["Content-Disposition"] = f"attachment; filename=problem_{problem.id}_test_cases.zip" + return response + @problem_permission_required def post(self, request): form = TestCaseUploadForm(request.POST, request.FILES) From 7d1f9452cff95a20353e7468118d09aff87baf22 Mon Sep 17 00:00:00 2001 From: zema1 Date: Wed, 15 Nov 2017 10:32:31 +0800 Subject: [PATCH 083/106] fix search problem --- problem/views/oj.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/problem/views/oj.py b/problem/views/oj.py index 0cc9abd..5d1efb4 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -67,7 +67,7 @@ class ProblemAPI(APIView): # 搜索的情况 keyword = request.GET.get("keyword", "").strip() if keyword: - problems = problems.filter(Q(title__contains=keyword) | Q(description__contains=keyword)) + problems = problems.filter(Q(title__icontains=keyword) | Q(_id__icontains=keyword)) # 难度筛选 difficulty = request.GET.get("difficulty") From 334b67488aede3cc196b904097687a291c853202 Mon Sep 17 00:00:00 2001 From: zema1 Date: Thu, 16 Nov 2017 22:12:17 +0800 Subject: [PATCH 084/106] =?UTF-8?q?=E6=B7=BB=E5=8A=A0SPJ=E7=BC=96=E8=AF=91?= =?UTF-8?q?API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/views/admin.py | 6 +- judge/dispatcher.py | 60 ++++++++++++------- .../migrations/0010_problem_spj_compile_ok.py | 20 +++++++ problem/models.py | 1 + problem/serializers.py | 8 +++ problem/urls/admin.py | 2 + problem/views/admin.py | 30 +++++++++- submission/views/oj.py | 4 +- 8 files changed, 104 insertions(+), 27 deletions(-) create mode 100644 problem/migrations/0010_problem_spj_compile_ok.py diff --git a/account/views/admin.py b/account/views/admin.py index 9d1359f..7b9259e 100644 --- a/account/views/admin.py +++ b/account/views/admin.py @@ -81,7 +81,7 @@ class UserAdminAPI(APIView): keyword = request.GET.get("keyword", None) if keyword: - user = user.filter(Q(username__contains=keyword) | - Q(real_name__contains=keyword) | - Q(email__contains=keyword)) + user = user.filter(Q(username__icontains=keyword) | + Q(userprofile__real_name__icontains=keyword) | + Q(email__icontains=keyword)) return self.success(self.paginate_data(request, user, UserSerializer)) diff --git a/judge/dispatcher.py b/judge/dispatcher.py index 4011e4c..f108f1f 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -11,7 +11,7 @@ from django.conf import settings from account.models import User from conf.models import JudgeServer from contest.models import ContestRuleType, ACMContestRank, OIContestRank, ContestStatus -from judge.languages import languages +from judge.languages import languages, spj_languages from options.options import SysOptions from problem.models import Problem, ProblemRuleType from submission.models import JudgeStatus, Submission @@ -30,16 +30,9 @@ def process_pending_task(): judge_task.delay(**data) -class JudgeDispatcher(object): - def __init__(self, submission_id, problem_id): +class DispatcherBase(object): + def __init__(self): self.token = hashlib.sha256(SysOptions.judge_server_token.encode("utf-8")).hexdigest() - self.submission = Submission.objects.get(id=submission_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) def _request(self, url, data=None): kwargs = {"headers": {"X-Judge-Server-Token": self.token}} @@ -69,6 +62,39 @@ class JudgeDispatcher(object): server.used_instance_number = F("task_number") - 1 server.save() + +class SPJCompiler(DispatcherBase): + def __init__(self, spj_code, spj_version, spj_language): + super().__init__() + spj_compile_config = list(filter(lambda config: spj_language == config["name"], spj_languages))[0]["spj"][ + "compile"] + self.data = { + "src": spj_code, + "spj_version": spj_version, + "spj_compile_config": spj_compile_config + } + + def compile_spj(self): + server = self.choose_judge_server() + if not server: + return "No available judge_server" + result = self._request(urljoin(server.service_url, "compile_spj"), data=self.data) + self.release_judge_server(server.id) + if result["err"]: + return result["data"] + + +class JudgeDispatcher(DispatcherBase): + def __init__(self, submission_id, problem_id): + super().__init__() + self.submission = Submission.objects.get(id=submission_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) + def _compute_statistic_info(self, resp_data): # 用时和内存占用保存为多个测试点中最长的那个 self.submission.statistic_info["time_cost"] = max([x["cpu_time"] for x in resp_data]) @@ -90,7 +116,7 @@ class JudgeDispatcher(object): return self.submission.statistic_info["score"] = score - def judge(self, output=False): + def judge(self, output=True): server = self.choose_judge_server() if not server: data = {"submission_id": self.submission.id, "problem_id": self.problem.id} @@ -100,7 +126,7 @@ class JudgeDispatcher(object): sub_config = list(filter(lambda item: self.submission.language == item["name"], languages))[0] spj_config = {} if self.problem.spj_code: - for lang in languages: + for lang in spj_languages: if lang["name"] == self.problem.spj_language: spj_config = lang["spj"] break @@ -153,12 +179,6 @@ class JudgeDispatcher(object): # 至此判题结束,尝试处理任务队列中剩余的任务 process_pending_task() - def compile_spj(self, service_url, src, spj_version, spj_compile_config, test_case_id): - data = {"src": src, "spj_version": spj_version, - "spj_compile_config": spj_compile_config, - "test_case_id": test_case_id} - return self._request(urljoin(service_url, "compile_spj"), data=data) - def update_problem_status(self): result = str(self.submission.result) problem_id = str(self.problem.id) @@ -201,10 +221,10 @@ class JudgeDispatcher(object): user_profile.accepted_number += 1 else: if oi_problems_status[problem_id]["status"] == JudgeStatus.ACCEPTED and \ - self.submission.result != JudgeStatus.ACCEPTED: + self.submission.result != JudgeStatus.ACCEPTED: user_profile.accepted_number -= 1 elif oi_problems_status[problem_id]["status"] != JudgeStatus.ACCEPTED and \ - self.submission.result == JudgeStatus: + self.submission.result == JudgeStatus: user_profile.accepted_number += 1 # minus last time score, add this time score diff --git a/problem/migrations/0010_problem_spj_compile_ok.py b/problem/migrations/0010_problem_spj_compile_ok.py new file mode 100644 index 0000000..0df1b36 --- /dev/null +++ b/problem/migrations/0010_problem_spj_compile_ok.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-16 12:42 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('problem', '0009_auto_20171011_1214'), + ] + + operations = [ + migrations.AddField( + model_name='problem', + name='spj_compile_ok', + field=models.BooleanField(default=False), + ), + ] diff --git a/problem/models.py b/problem/models.py index 6fb1c79..5116f4b 100644 --- a/problem/models.py +++ b/problem/models.py @@ -56,6 +56,7 @@ class Problem(models.Model): spj_language = models.CharField(max_length=32, blank=True, null=True) spj_code = models.TextField(blank=True, null=True) spj_version = models.CharField(max_length=32, blank=True, null=True) + spj_compile_ok = models.BooleanField(default=False) rule_type = models.CharField(max_length=32) visible = models.BooleanField(default=True) difficulty = models.CharField(max_length=32) diff --git a/problem/serializers.py b/problem/serializers.py index 529d3f5..d035e75 100644 --- a/problem/serializers.py +++ b/problem/serializers.py @@ -79,6 +79,12 @@ class TagSerializer(serializers.ModelSerializer): fields = "__all__" +class CompileSPJSerializer(serializers.Serializer): + id = serializers.IntegerField() + spj_language = serializers.ChoiceField(choices=spj_language_names) + spj_code = serializers.CharField() + + class BaseProblemSerializer(serializers.ModelSerializer): samples = serializers.JSONField() test_case_score = serializers.JSONField() @@ -125,3 +131,5 @@ class ContestProblemSafeSerializer(BaseProblemSerializer): class ContestProblemMakePublicSerializer(serializers.Serializer): id = serializers.IntegerField() display_id = serializers.CharField(max_length=32) + + diff --git a/problem/urls/admin.py b/problem/urls/admin.py index 5dbc444..d4f9974 100644 --- a/problem/urls/admin.py +++ b/problem/urls/admin.py @@ -1,9 +1,11 @@ from django.conf.urls import url from ..views.admin import ContestProblemAPI, ProblemAPI, TestCaseAPI, MakeContestProblemPublicAPIView +from ..views.admin import CompileSPJAPI urlpatterns = [ url(r"^test_case/?$", TestCaseAPI.as_view(), name="test_case_api"), + url(r"^compile_spj/?$", CompileSPJAPI.as_view(), name="compile_spj"), url(r"^problem/?$", ProblemAPI.as_view(), name="problem_admin_api"), url(r"^contest/problem/?$", ContestProblemAPI.as_view(), name="contest_problem_admin_api"), url(r"^contest_problem/make_public/?$", MakeContestProblemPublicAPIView.as_view(), name="make_public_api"), diff --git a/problem/views/admin.py b/problem/views/admin.py index d9ae3f2..6fc5451 100644 --- a/problem/views/admin.py +++ b/problem/views/admin.py @@ -8,12 +8,13 @@ from django.conf import settings from django.http import StreamingHttpResponse from account.decorators import problem_permission_required +from judge.dispatcher import SPJCompiler from contest.models import Contest from utils.api import APIView, CSRFExemptAPIView, validate_serializer from utils.shortcuts import rand_str, natural_sort_key from ..models import Problem, ProblemRuleType, ProblemTag -from ..serializers import (CreateContestProblemSerializer, ContestProblemAdminSerializer, +from ..serializers import (CreateContestProblemSerializer, ContestProblemAdminSerializer, CompileSPJSerializer, CreateProblemSerializer, EditProblemSerializer, EditContestProblemSerializer, ProblemAdminSerializer, TestCaseUploadForm, ContestProblemMakePublicSerializer) @@ -60,7 +61,7 @@ class TestCaseAPI(CSRFExemptAPIView): return self.error("Test case does not exists") name_list = self.filter_name_list(os.listdir(test_case_dir), problem.spj) name_list.append("info") - file_name = os.path.join(test_case_dir, problem.test_case_id) + file_name = os.path.join(test_case_dir, problem.test_case_id + ".zip") with zipfile.ZipFile(file_name, "w") as file: for test_case in name_list: file.write(f"{test_case_dir}/{test_case}", test_case) @@ -134,6 +135,31 @@ class TestCaseAPI(CSRFExemptAPIView): return self.success({"id": test_case_id, "info": ret, "hint": hint, "spj": spj}) +class CompileSPJAPI(APIView): + @validate_serializer(CompileSPJSerializer) + @problem_permission_required + def post(self, request): + data = request.data + try: + problem = Problem.objects.get(pk=data["id"]) + except Problem.DoesNotExist: + return self.error("Problem does not exist") + spj_version = rand_str(8) + problem.spj = True + problem.spj_version = spj_version + problem.spj_language = data["spj_language"] + problem.spj_code = data["spj_code"] + error = SPJCompiler(data["spj_code"], spj_version, data["spj_language"]).compile_spj() + if error: + problem.spj_compile_ok = False + problem.save() + return self.error(error) + else: + problem.spj_compile_ok = True + problem.save() + return self.success() + + class ProblemBase(APIView): def common_checks(self, request): data = request.data diff --git a/submission/views/oj.py b/submission/views/oj.py index a0deb0a..a3371ff 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -154,7 +154,7 @@ class SubmissionListAPI(APIView): 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) + submissions = submissions.filter(username__icontains=username) if result: submissions = submissions.filter(result=result) data = self.paginate_data(request, submissions) @@ -184,7 +184,7 @@ class ContestSubmissionListAPI(APIView): if myself and myself == "1": submissions = submissions.filter(user_id=request.user.id) elif username: - submissions = submissions.filter(username=username) + submissions = submissions.filter(username__icontains=username) if result: submissions = submissions.filter(result=result) From e8b06f0487c523ea92bc7392850288ff2a8cbf8b Mon Sep 17 00:00:00 2001 From: zema1 Date: Sat, 18 Nov 2017 08:07:03 +0800 Subject: [PATCH 085/106] add generate user api --- account/models.py | 2 +- account/serializers.py | 9 +++++ account/urls/admin.py | 3 +- account/views/admin.py | 87 ++++++++++++++++++++++++++++++++++++++++- deploy/requirements.txt | 1 + judge/dispatcher.py | 4 +- problem/serializers.py | 4 +- problem/tests.py | 2 +- problem/views/admin.py | 33 ++++++++++------ 9 files changed, 123 insertions(+), 22 deletions(-) diff --git a/account/models.py b/account/models.py index eb92f75..ceac9af 100644 --- a/account/models.py +++ b/account/models.py @@ -67,7 +67,7 @@ class User(AbstractBaseUser): class UserProfile(models.Model): - user = models.OneToOneField(User) + user = models.OneToOneField(User, on_delete=models.CASCADE) # acm_problems_status examples: # { # "problems": { diff --git a/account/serializers.py b/account/serializers.py index c07e6ca..93f0498 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -35,6 +35,15 @@ class UserChangeEmailSerializer(serializers.Serializer): tfa_code = serializers.CharField(required=False, allow_blank=True) +class GenerateUserSerializer(serializers.Serializer): + prefix = serializers.CharField(max_length=16, allow_blank=True) + suffix = serializers.CharField(max_length=16, allow_blank=True) + number_from = serializers.IntegerField() + number_to = serializers.IntegerField() + default_email = serializers.CharField(max_length=64) + password_length = serializers.IntegerField(required=False, max_value=16) + + class UserSerializer(serializers.ModelSerializer): create_time = DateTimeTZField() last_login = DateTimeTZField() diff --git a/account/urls/admin.py b/account/urls/admin.py index b10741e..5826ae2 100644 --- a/account/urls/admin.py +++ b/account/urls/admin.py @@ -1,7 +1,8 @@ from django.conf.urls import url -from ..views.admin import UserAdminAPI +from ..views.admin import UserAdminAPI, GenerateUserAPI urlpatterns = [ url(r"^user/?$", UserAdminAPI.as_view(), name="user_admin_api"), + url(r"^generate_user/?$", GenerateUserAPI.as_view(), name="generate_user_api"), ] diff --git a/account/views/admin.py b/account/views/admin.py index 7b9259e..4ae11ad 100644 --- a/account/views/admin.py +++ b/account/views/admin.py @@ -1,12 +1,16 @@ +import os +import re +import xlsxwriter from django.db.models import Q +from django.http import HttpResponse from submission.models import Submission from utils.api import APIView, validate_serializer from utils.shortcuts import rand_str from ..decorators import super_admin_required -from ..models import AdminType, ProblemPermission, User -from ..serializers import EditUserSerializer, UserSerializer +from ..models import AdminType, ProblemPermission, User, UserProfile +from ..serializers import EditUserSerializer, UserSerializer, GenerateUserSerializer class UserAdminAPI(APIView): @@ -85,3 +89,82 @@ class UserAdminAPI(APIView): Q(userprofile__real_name__icontains=keyword) | Q(email__icontains=keyword)) return self.success(self.paginate_data(request, user, UserSerializer)) + + def delete_one(self, user_id): + try: + user = User.objects.get(id=user_id) + except User.DoesNotExist: + return f"User {user_id} does not exist" + profile = user.userprofile + if profile.submission_number: + return f"Can't delete the user {user_id} as he/she has submissions" + user.delete() + + @super_admin_required + def delete(self, request): + id = request.GET.get("id") + if not id: + return self.error("Invalid Parameter, user_id is required") + for user_id in id.split(","): + if user_id: + error = self.delete_one(user_id) + if error: + return self.error(error) + return self.success() + + +class GenerateUserAPI(APIView): + @super_admin_required + def get(self, request): + """ + download users excel + """ + file_id = request.GET.get("file_id") + if not file_id: + return self.error("Invalid Parameter, file_id is required") + if not re.match(r"[a-zA-Z0-9]+", file_id): + return self.error("Illegal file_id") + file_path = f"/tmp/{file_id}.xlsx" + if not os.path.isfile(file_path): + return self.error("File does not exist") + with open(file_path, "rb") as f: + raw_data = f.read() + os.remove(file_path) + response = HttpResponse(raw_data) + response["Content-Disposition"] = f"attachment; filename=users.xlsx" + response["Content-Type"] = "application/xlsx" + return response + + @validate_serializer(GenerateUserSerializer) + @super_admin_required + def post(self, request): + data = request.data + number_max_length = max(len(str(data["number_from"])), len(str(data["number_to"]))) + if number_max_length + len(data["prefix"]) + len(data["suffix"]) > 32: + return self.error("Username should not more than 32 characters") + if data["number_from"] >= data["number_to"]: + return self.error("Start number must be lower than end number") + + password_length = data.get("password_length", 8) + default_email = data.get("default_email") + + file_id = rand_str(8) + filename = f"/tmp/{file_id}.xlsx" + workbook = xlsxwriter.Workbook(filename) + worksheet = workbook.add_worksheet() + worksheet.set_column("A:B", 20) + worksheet.write("A1", "Username") + worksheet.write("B1", "Password") + i = 1 + for number in range(data["number_from"], data["number_to"] + 1): + username = f"{data['prefix']}{number}{data['suffix']}" + password = rand_str(password_length) + user = User.objects.create(username=username, email=default_email) + user.set_password(password) + user.save() + UserProfile.objects.create(user=user) + worksheet.write_string(i, 0, username) + worksheet.write_string(i, 1, password) + i += 1 + workbook.close() + return self.success(file_id) diff --git a/deploy/requirements.txt b/deploy/requirements.txt index 559cce8..3d83ebf 100644 --- a/deploy/requirements.txt +++ b/deploy/requirements.txt @@ -15,3 +15,4 @@ django-redis psycopg2 gunicorn jsonfield +XlsxWriter diff --git a/judge/dispatcher.py b/judge/dispatcher.py index f108f1f..7c04029 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -221,10 +221,10 @@ class JudgeDispatcher(DispatcherBase): user_profile.accepted_number += 1 else: if oi_problems_status[problem_id]["status"] == JudgeStatus.ACCEPTED and \ - self.submission.result != JudgeStatus.ACCEPTED: + self.submission.result != JudgeStatus.ACCEPTED: user_profile.accepted_number -= 1 elif oi_problems_status[problem_id]["status"] != JudgeStatus.ACCEPTED and \ - self.submission.result == JudgeStatus: + self.submission.result == JudgeStatus: user_profile.accepted_number += 1 # minus last time score, add this time score diff --git a/problem/serializers.py b/problem/serializers.py index d035e75..57a034c 100644 --- a/problem/serializers.py +++ b/problem/serializers.py @@ -49,6 +49,7 @@ class CreateOrEditProblemSerializer(serializers.Serializer): spj = serializers.BooleanField() spj_language = serializers.ChoiceField(choices=spj_language_names, allow_blank=True, allow_null=True) spj_code = serializers.CharField(allow_blank=True, allow_null=True) + spj_compile_ok = serializers.BooleanField(default=False) visible = serializers.BooleanField() difficulty = serializers.ChoiceField(choices=[Difficulty.LOW, Difficulty.MID, Difficulty.HIGH]) tags = serializers.ListField(child=serializers.CharField(max_length=32), allow_empty=False) @@ -80,7 +81,6 @@ class TagSerializer(serializers.ModelSerializer): class CompileSPJSerializer(serializers.Serializer): - id = serializers.IntegerField() spj_language = serializers.ChoiceField(choices=spj_language_names) spj_code = serializers.CharField() @@ -131,5 +131,3 @@ class ContestProblemSafeSerializer(BaseProblemSerializer): class ContestProblemMakePublicSerializer(serializers.Serializer): id = serializers.IntegerField() display_id = serializers.CharField(max_length=32) - - diff --git a/problem/tests.py b/problem/tests.py index 7595260..dbd01b7 100644 --- a/problem/tests.py +++ b/problem/tests.py @@ -19,7 +19,7 @@ DEFAULT_PROBLEM_DATA = {"_id": "A-110", "title": "test", "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", + "spj_code": "", "spj_compile_ok": True, "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}], diff --git a/problem/views/admin.py b/problem/views/admin.py index 6fc5451..36fe655 100644 --- a/problem/views/admin.py +++ b/problem/views/admin.py @@ -1,6 +1,7 @@ import hashlib import json import os +import shutil import zipfile from wsgiref.util import FileWrapper @@ -10,6 +11,7 @@ from django.http import StreamingHttpResponse from account.decorators import problem_permission_required from judge.dispatcher import SPJCompiler from contest.models import Contest +from submission.models import Submission from utils.api import APIView, CSRFExemptAPIView, validate_serializer from utils.shortcuts import rand_str, natural_sort_key @@ -140,23 +142,11 @@ class CompileSPJAPI(APIView): @problem_permission_required def post(self, request): data = request.data - try: - problem = Problem.objects.get(pk=data["id"]) - except Problem.DoesNotExist: - return self.error("Problem does not exist") spj_version = rand_str(8) - problem.spj = True - problem.spj_version = spj_version - problem.spj_language = data["spj_language"] - problem.spj_code = data["spj_code"] error = SPJCompiler(data["spj_code"], spj_version, data["spj_language"]).compile_spj() if error: - problem.spj_compile_ok = False - problem.save() return self.error(error) else: - problem.spj_compile_ok = True - problem.save() return self.success() @@ -166,6 +156,8 @@ class ProblemBase(APIView): if data["spj"]: if not data["spj_language"] or not data["spj_code"]: return "Invalid spj" + if not data["spj_compile_ok"]: + return "SPJ code must be compiled successfully" data["spj_version"] = hashlib.md5( (data["spj_language"] + ":" + data["spj_code"]).encode("utf-8")).hexdigest() else: @@ -182,6 +174,23 @@ class ProblemBase(APIView): data["created_by"] = request.user data["languages"] = list(data["languages"]) + @problem_permission_required + def delete(self, request): + id = request.GET.get("id") + if not id: + return self.error("Invalid parameter, id is requred") + try: + problem = Problem.objects.get(id=id) + except Problem.DoesNotExist: + return self.error("Problem does not exists") + if Submission.objects.filter(problem=problem).exists(): + return self.error("Can't delete the problem as it has submissions") + d = os.path.join(settings.TEST_CASE_DIR, problem.test_case_id) + if os.path.isdir(d): + shutil.rmtree(d, ignore_errors=True) + problem.delete() + return self.success() + class ProblemAPI(ProblemBase): @validate_serializer(CreateProblemSerializer) From ba9b609fe4f2a48589e2c34748a5ef26d777dea6 Mon Sep 17 00:00:00 2001 From: zema1 Date: Sat, 18 Nov 2017 20:49:00 +0800 Subject: [PATCH 086/106] =?UTF-8?q?=E5=8E=BB=E6=8E=89=E6=9B=B4=E6=96=B0dis?= =?UTF-8?q?playID=E6=97=B6=E7=9A=84userid=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/views/oj.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/account/views/oj.py b/account/views/oj.py index a451553..4af6b82 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -387,11 +387,6 @@ class UserRankAPI(APIView): class ProfileProblemDisplayIDRefreshAPI(APIView): @login_required def get(self, request): - user_id = request.GET.get("user_id") - if not user_id: - return self.error("Invalid parameter, user_id is required") - if request.user.id != user_id: - return self.error("Only user self can require a refresh") profile = request.user.userprofile acm_problems = profile.acm_problems_status.get("problems", {}) oi_problems = profile.oi_problems_status.get("problems", {}) From 2b4fb4f368bbff1af241dbd08374c93c6c0ac739 Mon Sep 17 00:00:00 2001 From: zema1 Date: Wed, 22 Nov 2017 20:06:16 +0800 Subject: [PATCH 087/106] import users api --- account/serializers.py | 5 +++++ account/views/admin.py | 42 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/account/serializers.py b/account/serializers.py index 93f0498..945e096 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -44,6 +44,11 @@ class GenerateUserSerializer(serializers.Serializer): password_length = serializers.IntegerField(required=False, max_value=16) +class ImportUserSeralizer(serializers.Serializer): + users = serializers.ListField( + child=serializers.ListField(child=serializers.CharField(max_length=64))) + + class UserSerializer(serializers.ModelSerializer): create_time = DateTimeTZField() last_login = DateTimeTZField() diff --git a/account/views/admin.py b/account/views/admin.py index 4ae11ad..49f58f0 100644 --- a/account/views/admin.py +++ b/account/views/admin.py @@ -11,9 +11,34 @@ from utils.shortcuts import rand_str from ..decorators import super_admin_required from ..models import AdminType, ProblemPermission, User, UserProfile from ..serializers import EditUserSerializer, UserSerializer, GenerateUserSerializer +from ..serializers import ImportUserSeralizer class UserAdminAPI(APIView): + @validate_serializer(ImportUserSeralizer) + @super_admin_required + def post(self, request): + data = request.data["users"] + omitted_count = created_count = get_count = 0 + for user_data in data: + if len(user_data) != 3: + omitted_count += 1 + continue + user, created = User.objects.get_or_create(username=user_data[0]) + user.set_password(user_data[1]) + user.email = user_data[2] + user.save() + if created: + UserProfile.objects.create(user=user) + created_count += 1 + else: + get_count += 1 + return self.success({ + "omitted_count": omitted_count, + "created_count": created_count, + "get_count": get_count + }) + @validate_serializer(EditUserSerializer) @super_admin_required def put(self, request): @@ -156,15 +181,26 @@ class GenerateUserAPI(APIView): worksheet.write("A1", "Username") worksheet.write("B1", "Password") i = 1 + created_count = 0 + get_count = 0 for number in range(data["number_from"], data["number_to"] + 1): username = f"{data['prefix']}{number}{data['suffix']}" password = rand_str(password_length) - user = User.objects.create(username=username, email=default_email) + user, created = User.objects.get_or_create(username=username) + user.email = default_email user.set_password(password) user.save() - UserProfile.objects.create(user=user) + if created: + UserProfile.objects.create(user=user) + created_count += 1 + else: + get_count += 1 worksheet.write_string(i, 0, username) worksheet.write_string(i, 1, password) i += 1 workbook.close() - return self.success(file_id) + return self.success({ + "file_id": file_id, + "created_count": created_count, + "get_count": get_count + }) From 45953b8f80ef02b7420fefb2ca03e3984dc7db19 Mon Sep 17 00:00:00 2001 From: zema1 Date: Thu, 23 Nov 2017 19:11:12 +0800 Subject: [PATCH 088/106] submission exists api --- .travis.yml | 2 +- account/decorators.py | 4 ++-- account/views/admin.py | 4 ++-- run_test.py | 2 +- submission/urls/oj.py | 3 ++- submission/views/oj.py | 9 +++++++++ 6 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7782d02..25b3ba1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ install: script: - docker ps -a - flake8 . - - coverage run --include="$PWD/*" manage.py test + - coverage run --source='.' manage.py test - coverage report notifications: slack: onlinejudgeteam:BzBz8UFgmS5crpiblof17K2W diff --git a/account/decorators.py b/account/decorators.py index a8164d7..9371059 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -94,11 +94,11 @@ def check_contest_permission(check_type="details"): (self.contest.id not in request.session["accessible_contests"]): return self.error("Password is required.") - # regular use get contest problems, ranks etc. before contest started + # regular user get contest problems, ranks etc. before contest started if self.contest.status == ContestStatus.CONTEST_NOT_START and check_type != "details": return self.error("Contest has not started yet.") - # check is user have permission to get ranks, submissions OI Contest + # check does user have permission to get ranks, submissions OI Contest if self.contest.status == ContestStatus.CONTEST_UNDERWAY and self.contest.rule_type == ContestRuleType.OI: if not self.contest.real_time_rank and (check_type == "ranks" or check_type == "submissions"): return self.error(f"No permission to get {check_type}") diff --git a/account/views/admin.py b/account/views/admin.py index 49f58f0..b25ccb4 100644 --- a/account/views/admin.py +++ b/account/views/admin.py @@ -21,7 +21,7 @@ class UserAdminAPI(APIView): data = request.data["users"] omitted_count = created_count = get_count = 0 for user_data in data: - if len(user_data) != 3: + if len(user_data) != 3 or len(user_data[0]) > 32: omitted_count += 1 continue user, created = User.objects.get_or_create(username=user_data[0]) @@ -167,7 +167,7 @@ class GenerateUserAPI(APIView): number_max_length = max(len(str(data["number_from"])), len(str(data["number_to"]))) if number_max_length + len(data["prefix"]) + len(data["suffix"]) > 32: return self.error("Username should not more than 32 characters") - if data["number_from"] >= data["number_to"]: + if data["number_from"] > data["number_to"]: return self.error("Start number must be lower than end number") password_length = data.get("password_length", 8) diff --git a/run_test.py b/run_test.py index 8358f12..2613ca7 100644 --- a/run_test.py +++ b/run_test.py @@ -21,7 +21,7 @@ print("running flake8...") if os.system("flake8 --statistics ."): exit() -ret = os.system("coverage run ./manage.py test {module} --settings={setting}".format(module=test_module, setting=setting)) +ret = os.system("coverage run --source='.' ./manage.py test {module} --settings={setting}".format(module=test_module, setting=setting)) if not ret and is_coverage: os.system("coverage html && open htmlcov/index.html") diff --git a/submission/urls/oj.py b/submission/urls/oj.py index f2a7703..49116b9 100644 --- a/submission/urls/oj.py +++ b/submission/urls/oj.py @@ -1,9 +1,10 @@ from django.conf.urls import url -from ..views.oj import SubmissionAPI, SubmissionListAPI, ContestSubmissionListAPI +from ..views.oj import SubmissionAPI, SubmissionListAPI, ContestSubmissionListAPI, SubmissionExistsAPI urlpatterns = [ url(r"^submission/?$", SubmissionAPI.as_view(), name="submission_api"), url(r"^submissions/?$", SubmissionListAPI.as_view(), name="submission_list_api"), + url(r"^submission_exists/?$", SubmissionExistsAPI.as_view(), name="submission_exists"), 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 a3371ff..88a5c27 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -200,3 +200,12 @@ class ContestSubmissionListAPI(APIView): data = self.paginate_data(request, submissions) data["results"] = SubmissionListSerializer(data["results"], many=True, user=request.user).data return self.success(data) + + +class SubmissionExistsAPI(APIView): + def get(self, request): + if not request.GET.get("problem_id"): + return self.error("Parameter error, problem_id is required") + return self.success(request.user.is_authenticated and + Submission.objects.filter(problem_id=request.GET["problem_id"], + user_id=request.user.id).exists()) From a87d73393d6df10703708db5f8abee4ed34ebff8 Mon Sep 17 00:00:00 2001 From: zema1 Date: Thu, 23 Nov 2017 21:12:37 +0800 Subject: [PATCH 089/106] =?UTF-8?q?=E8=A1=A5=E5=85=A8account=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .travis.yml | 2 +- account/tests.py | 192 +++++++++++++++++++++++++++++++++-------- account/urls/oj.py | 2 +- account/views/admin.py | 2 +- account/views/oj.py | 2 +- run_test.py | 2 +- 6 files changed, 162 insertions(+), 40 deletions(-) diff --git a/.travis.yml b/.travis.yml index 25b3ba1..7782d02 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ install: script: - docker ps -a - flake8 . - - coverage run --source='.' manage.py test + - coverage run --include="$PWD/*" manage.py test - coverage report notifications: slack: onlinejudgeteam:BzBz8UFgmS5crpiblof17K2W diff --git a/account/tests.py b/account/tests.py index ccc8b1a..4279c96 100644 --- a/account/tests.py +++ b/account/tests.py @@ -1,6 +1,7 @@ import time from unittest import mock from datetime import timedelta +from copy import deepcopy from django.contrib import auth from django.utils.timezone import now @@ -34,18 +35,32 @@ class PermissionDecoratorTest(APITestCase): class DuplicateUserCheckAPITest(APITestCase): def setUp(self): - self.create_user("test", "test123", login=False) + user = self.create_user("test", "test123", login=False) + user.email = "test@test.com" + user.save() self.url = self.reverse("check_username_or_email") def test_duplicate_username(self): resp = self.client.post(self.url, data={"username": "test"}) data = resp.data["data"] self.assertEqual(data["username"], True) + resp = self.client.post(self.url, data={"username": "Test"}) + self.assertEqual(resp.data["data"]["username"], True) def test_ok_username(self): resp = self.client.post(self.url, data={"username": "test1"}) data = resp.data["data"] - self.assertEqual(data["username"], False) + self.assertFalse(data["username"]) + + def test_duplicate_email(self): + resp = self.client.post(self.url, data={"email": "test@test.com"}) + self.assertEqual(resp.data["data"]["email"], True) + resp = self.client.post(self.url, data={"email": "Test@Test.com"}) + self.assertTrue(resp.data["data"]["email"]) + + def test_ok_email(self): + resp = self.client.post(self.url, data={"email": "aa@test.com"}) + self.assertFalse(resp.data["data"]["email"]) class TFARequiredCheckAPITest(APITestCase): @@ -87,6 +102,12 @@ class UserLoginAPITest(APITestCase): user = auth.get_user(self.client) self.assertTrue(user.is_authenticated()) + def test_login_with_correct_info_upper_username(self): + resp = self.client.post(self.login_url, data={"username": self.username.upper(), "password": self.password}) + self.assertDictEqual(resp.data, {"error": None, "data": "Succeeded"}) + user = auth.get_user(self.client) + self.assertTrue(user.is_authenticated()) + def test_login_with_wrong_info(self): response = self.client.post(self.login_url, data={"username": self.username, "password": "invalid_password"}) @@ -346,19 +367,48 @@ class ResetPasswordAPITest(CaptchaTest): self.assertDictEqual(resp.data, {"error": "error", "data": "Token has expired"}) -class UserChangePasswordAPITest(CaptchaTest): +class UserChangeEmailAPITest(APITestCase): + def setUp(self): + self.url = self.reverse("user_change_email_api") + self.user = self.create_user("test", "test123") + self.new_mail = "test@oj.com" + self.data = {"password": "test123", "new_email": self.new_mail} + + def test_change_email_success(self): + resp = self.client.post(self.url, data=self.data) + self.assertSuccess(resp) + + def test_wrong_password(self): + self.data["password"] = "aaaa" + resp = self.client.post(self.url, data=self.data) + self.assertDictEqual(resp.data, {"error": "error", "data": "Wrong password"}) + + def test_duplicate_email(self): + u = self.create_user("aa", "bb", login=False) + u.email = self.new_mail + u.save() + resp = self.client.post(self.url, data=self.data) + self.assertDictEqual(resp.data, {"error": "error", "data": "The email is owned by other account"}) + + +class UserChangePasswordAPITest(APITestCase): def setUp(self): - self.client = APIClient() self.url = self.reverse("user_change_password_api") # Create user at first self.username = "test_user" self.old_password = "testuserpassword" self.new_password = "new_password" - self.create_user(username=self.username, password=self.old_password, login=False) + self.user = self.create_user(username=self.username, password=self.old_password, login=False) - self.data = {"old_password": self.old_password, "new_password": self.new_password, - "captcha": self._set_captcha(self.client.session)} + self.data = {"old_password": self.old_password, "new_password": self.new_password} + + def _get_tfa_code(self): + user = User.objects.first() + code = OtpAuth(user.tfa_token).totp() + if len(str(code)) < 6: + code = (6 - len(str(code))) * "0" + str(code) + return code def test_login_required(self): response = self.client.post(self.url, data=self.data) @@ -376,6 +426,58 @@ class UserChangePasswordAPITest(CaptchaTest): response = self.client.post(self.url, data=self.data) self.assertEqual(response.data, {"error": "error", "data": "Invalid old password"}) + def test_tfa_code_required(self): + self.user.two_factor_auth = True + self.user.tfa_token = "tfa_token" + self.user.save() + self.assertTrue(self.client.login(username=self.username, password=self.old_password)) + self.data["tfa_code"] = rand_str(6) + resp = self.client.post(self.url, data=self.data) + self.assertEqual(resp.data, {"error": "error", "data": "Invalid two factor verification code"}) + + self.data["tfa_code"] = self._get_tfa_code() + resp = self.client.post(self.url, data=self.data) + self.assertSuccess(resp) + + +class UserRankAPITest(APITestCase): + def setUp(self): + self.url = self.reverse("user_rank_api") + self.create_user("test1", "test123", login=False) + self.create_user("test2", "test123", login=False) + test1 = User.objects.get(username="test1") + profile1 = test1.userprofile + profile1.submission_number = 10 + profile1.accepted_number = 10 + profile1.total_score = 240 + profile1.save() + + test2 = User.objects.get(username="test2") + profile2 = test2.userprofile + profile2.submission_number = 15 + profile2.accepted_number = 10 + profile2.total_score = 700 + profile2.save() + + def test_get_acm_rank(self): + resp = self.client.get(self.url, data={"rule": ContestRuleType.ACM}) + self.assertSuccess(resp) + data = resp.data["data"]["results"] + self.assertEqual(data[0]["user"]["username"], "test1") + self.assertEqual(data[1]["user"]["username"], "test2") + + def test_get_oi_rank(self): + resp = self.client.get(self.url, data={"rule": ContestRuleType.OI}) + self.assertSuccess(resp) + data = resp.data["data"]["results"] + self.assertEqual(data[0]["user"]["username"], "test2") + self.assertEqual(data[1]["user"]["username"], "test1") + + +class ProfileProblemDisplayIDRefreshAPITest(APITestCase): + def setUp(self): + pass + class AdminUserTest(APITestCase): def setUp(self): @@ -453,36 +555,56 @@ class AdminUserTest(APITestCase): self.assertTrue(resp_data["open_api"]) self.assertEqual(User.objects.get(id=self.regular_user.id).open_api_appkey, key) + def test_import_users(self): + data = {"users": [["user1", "pass1", "eami1@e.com"], + ["user1", "pass1", "eami1@e.com"], + ["user2", "pass2"], ["user3", "pass3", "eamil3@e.com"]] + } + resp = self.client.post(self.url, data) + self.assertSuccess(resp) + self.assertDictEqual(resp.data["data"], {"omitted_count": 1, + "created_count": 2, + "get_count": 1}) + # successfully created 2 users + self.assertEqual(User.objects.all().count(), 4) -class UserRankAPITest(APITestCase): + def test_delete_users(self): + self.test_import_users() + user_ids = User.objects.filter(username__in=["user1", "user3"]).values_list("id", flat=True) + user_ids = ",".join([str(id) for id in user_ids]) + resp = self.client.delete(self.url + "?id=" + user_ids) + self.assertSuccess(resp) + self.assertEqual(User.objects.all().count(), 2) + + +class GenerateUserAPITest(APITestCase): def setUp(self): - self.url = self.reverse("user_rank_api") - self.create_user("test1", "test123", login=False) - self.create_user("test2", "test123", login=False) - test1 = User.objects.get(username="test1") - profile1 = test1.userprofile - profile1.submission_number = 10 - profile1.accepted_number = 10 - profile1.total_score = 240 - profile1.save() + self.create_super_admin() + self.url = self.reverse("generate_user_api") + self.data = { + "number_from": 100, "number_to": 105, + "prefix": "pre", "suffix": "suf", + "default_email": "test@test.com", + "password_length": 8 + } - test2 = User.objects.get(username="test2") - profile2 = test2.userprofile - profile2.submission_number = 15 - profile2.accepted_number = 10 - profile2.total_score = 700 - profile2.save() + def test_error_case(self): + data = deepcopy(self.data) + data["prefix"] = "t" * 16 + data["suffix"] = "s" * 14 + resp = self.client.post(self.url, data=data) + self.assertEqual(resp.data["data"], "Username should not more than 32 characters") - def test_get_acm_rank(self): - resp = self.client.get(self.url, data={"rule": ContestRuleType.ACM}) + data2 = deepcopy(self.data) + data2["number_from"] = 106 + resp = self.client.post(self.url, data=data2) + self.assertEqual(resp.data["data"], "Start number must be lower than end number") + + @mock.patch("account.views.admin.xlsxwriter.Workbook") + def test_generate_user_success(self, mock_workbook): + resp = self.client.post(self.url, data=self.data) self.assertSuccess(resp) - data = resp.data["data"]["results"] - self.assertEqual(data[0]["user"]["username"], "test1") - self.assertEqual(data[1]["user"]["username"], "test2") - - def test_get_oi_rank(self): - resp = self.client.get(self.url, data={"rule": ContestRuleType.OI}) - self.assertSuccess(resp) - data = resp.data["data"]["results"] - self.assertEqual(data[0]["user"]["username"], "test2") - self.assertEqual(data[1]["user"]["username"], "test1") + mock_workbook.assert_called() + data = resp.data["data"] + self.assertEqual(data["created_count"], 6) + self.assertEqual(data["get_count"], 0) diff --git a/account/urls/oj.py b/account/urls/oj.py index c1915f4..a92dd5b 100644 --- a/account/urls/oj.py +++ b/account/urls/oj.py @@ -14,7 +14,7 @@ urlpatterns = [ url(r"^logout/?$", UserLogoutAPI.as_view(), name="user_logout_api"), url(r"^register/?$", UserRegisterAPI.as_view(), name="user_register_api"), url(r"^change_password/?$", UserChangePasswordAPI.as_view(), name="user_change_password_api"), - url(r"^change_email/?$", UserChangeEmailAPI.as_view(), name="user_change_email"), + url(r"^change_email/?$", UserChangeEmailAPI.as_view(), name="user_change_email_api"), url(r"^apply_reset_password/?$", ApplyResetPasswordAPI.as_view(), name="apply_reset_password_api"), url(r"^reset_password/?$", ResetPasswordAPI.as_view(), name="reset_password_api"), url(r"^captcha/?$", CaptchaAPIView.as_view(), name="show_captcha"), diff --git a/account/views/admin.py b/account/views/admin.py index b25ccb4..ad731b5 100644 --- a/account/views/admin.py +++ b/account/views/admin.py @@ -129,7 +129,7 @@ class UserAdminAPI(APIView): def delete(self, request): id = request.GET.get("id") if not id: - return self.error("Invalid Parameter, user_id is required") + return self.error("Invalid Parameter, id is required") for user_id in id.split(","): if user_id: error = self.delete_one(user_id) diff --git a/account/views/oj.py b/account/views/oj.py index 4af6b82..344a419 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -197,7 +197,7 @@ class UsernameOrEmailCheck(APIView): "email": False } if data.get("username"): - result["username"] = User.objects.filter(username=data["username"]).exists() + result["username"] = User.objects.filter(username=data["username"].lower()).exists() if data.get("email"): result["email"] = User.objects.filter(email=data["email"].lower()).exists() return self.success(result) diff --git a/run_test.py b/run_test.py index 2613ca7..cb4c630 100644 --- a/run_test.py +++ b/run_test.py @@ -21,7 +21,7 @@ print("running flake8...") if os.system("flake8 --statistics ."): exit() -ret = os.system("coverage run --source='.' ./manage.py test {module} --settings={setting}".format(module=test_module, setting=setting)) +ret = os.system("coverage run --include=\"$PWD/*\" manage.py test {module} --settings={setting}".format(module=test_module, setting=setting)) if not ret and is_coverage: os.system("coverage html && open htmlcov/index.html") From 4e80ac94920421f55f498f79d23b562d23b73c62 Mon Sep 17 00:00:00 2001 From: zema1 Date: Thu, 23 Nov 2017 22:00:58 +0800 Subject: [PATCH 090/106] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AF=AF=E5=88=A0?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/views/admin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/account/views/admin.py b/account/views/admin.py index ad731b5..062fdcf 100644 --- a/account/views/admin.py +++ b/account/views/admin.py @@ -120,8 +120,7 @@ class UserAdminAPI(APIView): user = User.objects.get(id=user_id) except User.DoesNotExist: return f"User {user_id} does not exist" - profile = user.userprofile - if profile.submission_number: + if Submission.objects.filter(user_id=user_id).exists(): return f"Can't delete the user {user_id} as he/she has submissions" user.delete() From a1eed315b4bd8d3344b9f7610e471791ffd5495a Mon Sep 17 00:00:00 2001 From: zema1 Date: Fri, 24 Nov 2017 10:27:34 +0800 Subject: [PATCH 091/106] =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=9D=9E=E6=AF=94?= =?UTF-8?q?=E8=B5=9Bsubmission=E7=9A=84=E9=87=8D=E5=88=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- oj/urls.py | 3 ++- submission/urls/admin.py | 7 +++++++ submission/views/admin.py | 24 ++++++++++++++++++++++++ submission/views/oj.py | 2 +- 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/oj/urls.py b/oj/urls.py index ffe526f..c626656 100644 --- a/oj/urls.py +++ b/oj/urls.py @@ -9,8 +9,9 @@ urlpatterns = [ url(r"^api/admin/", include("conf.urls.admin")), 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/admin/", include("contest.urls.admin")), url(r"^api/", include("submission.urls.oj")), + url(r"^api/admin/", include("submission.urls.admin")), url(r"^api/admin/", include("utils.urls")), ] diff --git a/submission/urls/admin.py b/submission/urls/admin.py index e69de29..bf86022 100644 --- a/submission/urls/admin.py +++ b/submission/urls/admin.py @@ -0,0 +1,7 @@ +from django.conf.urls import url + +from ..views.admin import SubmissionRejudgeAPI + +urlpatterns = [ + url(r"^submission/rejudge?$", SubmissionRejudgeAPI.as_view(), name="submission_rejudge_api"), +] diff --git a/submission/views/admin.py b/submission/views/admin.py index e69de29..679360b 100644 --- a/submission/views/admin.py +++ b/submission/views/admin.py @@ -0,0 +1,24 @@ +from account.decorators import super_admin_required +from judge.tasks import judge_task +# from judge.dispatcher import JudgeDispatcher +from utils.api import APIView +from ..models import Submission, JudgeStatus + + +class SubmissionRejudgeAPI(APIView): + @super_admin_required + def get(self, request): + id = request.GET.get("id") + if not id: + return self.error("Paramater error, id is required") + try: + submission = Submission.objects.select_related("problem").get(id=id, contest_id__isnull=True) + except Submission.DoesNotExist: + return self.error("Submission does not exists") + submission.result = JudgeStatus.PENDING + submission.info = {} + submission.statistic_info = {} + submission.save() + + judge_task.delay(submission.id, submission.problem.id) + return self.success() diff --git a/submission/views/oj.py b/submission/views/oj.py index 88a5c27..8514c13 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -206,6 +206,6 @@ class SubmissionExistsAPI(APIView): def get(self, request): if not request.GET.get("problem_id"): return self.error("Parameter error, problem_id is required") - return self.success(request.user.is_authenticated and + return self.success(request.user.is_authenticated() and Submission.objects.filter(problem_id=request.GET["problem_id"], user_id=request.user.id).exists()) From 7cc33d07011b4f8400b418f4ffbb295ec342db47 Mon Sep 17 00:00:00 2001 From: virusdefender Date: Fri, 24 Nov 2017 22:26:56 +0800 Subject: [PATCH 092/106] use bulk_create and transcation for importing user --- account/tests.py | 17 +++++++++++------ account/views/admin.py | 38 +++++++++++++++++++++----------------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/account/tests.py b/account/tests.py index 4279c96..bf53727 100644 --- a/account/tests.py +++ b/account/tests.py @@ -557,20 +557,25 @@ class AdminUserTest(APITestCase): def test_import_users(self): data = {"users": [["user1", "pass1", "eami1@e.com"], - ["user1", "pass1", "eami1@e.com"], - ["user2", "pass2"], ["user3", "pass3", "eamil3@e.com"]] + ["user2", "pass3", "eamil3@e.com"]] } resp = self.client.post(self.url, data) self.assertSuccess(resp) - self.assertDictEqual(resp.data["data"], {"omitted_count": 1, - "created_count": 2, - "get_count": 1}) # successfully created 2 users self.assertEqual(User.objects.all().count(), 4) + def test_import_duplicate_user(self): + data = {"users": [["user1", "pass1", "eami1@e.com"], + ["user1", "pass1", "eami1@e.com"]] + } + resp = self.client.post(self.url, data) + self.assertFailed(resp, "DETAIL: Key (username)=(user1) already exists.") + # no user is created + self.assertEqual(User.objects.all().count(), 2) + def test_delete_users(self): self.test_import_users() - user_ids = User.objects.filter(username__in=["user1", "user3"]).values_list("id", flat=True) + user_ids = User.objects.filter(username__in=["user1", "user2"]).values_list("id", flat=True) user_ids = ",".join([str(id) for id in user_ids]) resp = self.client.delete(self.url + "?id=" + user_ids) self.assertSuccess(resp) diff --git a/account/views/admin.py b/account/views/admin.py index 062fdcf..f0b93c6 100644 --- a/account/views/admin.py +++ b/account/views/admin.py @@ -1,8 +1,11 @@ import os import re import xlsxwriter + +from django.db import transaction, IntegrityError from django.db.models import Q from django.http import HttpResponse +from django.contrib.auth.hashers import make_password from submission.models import Submission from utils.api import APIView, validate_serializer @@ -18,26 +21,27 @@ class UserAdminAPI(APIView): @validate_serializer(ImportUserSeralizer) @super_admin_required def post(self, request): + """ + Generate user + """ data = request.data["users"] - omitted_count = created_count = get_count = 0 + + user_list = [] for user_data in data: if len(user_data) != 3 or len(user_data[0]) > 32: - omitted_count += 1 - continue - user, created = User.objects.get_or_create(username=user_data[0]) - user.set_password(user_data[1]) - user.email = user_data[2] - user.save() - if created: - UserProfile.objects.create(user=user) - created_count += 1 - else: - get_count += 1 - return self.success({ - "omitted_count": omitted_count, - "created_count": created_count, - "get_count": get_count - }) + return self.error(f"Error occurred while processing data '{user_data}'") + user_list.append(User(username=user_data[0], password=make_password(user_data[1]), email=user_data[2])) + + try: + with transaction.atomic(): + ret = User.objects.bulk_create(user_list) + UserProfile.objects.bulk_create([UserProfile(user=user) for user in ret]) + return self.success() + except IntegrityError as e: + # Extract detail from exception message + # duplicate key value violates unique constraint "user_username_key" + # DETAIL: Key (username)=(root11) already exists. + return self.error(str(e).split("\n")[1]) @validate_serializer(EditUserSerializer) @super_admin_required From 9889ac5b4adc8ab65b099e85db13afa20feda7ac Mon Sep 17 00:00:00 2001 From: virusdefender Date: Fri, 24 Nov 2017 23:29:40 +0800 Subject: [PATCH 093/106] fix directory traversal --- account/views/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account/views/admin.py b/account/views/admin.py index f0b93c6..3e8fb47 100644 --- a/account/views/admin.py +++ b/account/views/admin.py @@ -150,7 +150,7 @@ class GenerateUserAPI(APIView): file_id = request.GET.get("file_id") if not file_id: return self.error("Invalid Parameter, file_id is required") - if not re.match(r"[a-zA-Z0-9]+", file_id): + if not re.match(r"^[a-zA-Z0-9]+$", file_id): return self.error("Illegal file_id") file_path = f"/tmp/{file_id}.xlsx" if not os.path.isfile(file_path): From 2d038c7bcc2161a9b39c4cba3f3f29aae52bee65 Mon Sep 17 00:00:00 2001 From: virusdefender Date: Fri, 24 Nov 2017 23:30:17 +0800 Subject: [PATCH 094/106] use bulk_create and transaction for user generator --- account/serializers.py | 3 +-- account/tests.py | 4 +--- account/views/admin.py | 48 ++++++++++++++++++++---------------------- oj/dev_settings.py | 2 +- 4 files changed, 26 insertions(+), 31 deletions(-) diff --git a/account/serializers.py b/account/serializers.py index 945e096..d346677 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -40,8 +40,7 @@ class GenerateUserSerializer(serializers.Serializer): suffix = serializers.CharField(max_length=16, allow_blank=True) number_from = serializers.IntegerField() number_to = serializers.IntegerField() - default_email = serializers.CharField(max_length=64) - password_length = serializers.IntegerField(required=False, max_value=16) + password_length = serializers.IntegerField(max_value=16, default=8) class ImportUserSeralizer(serializers.Serializer): diff --git a/account/tests.py b/account/tests.py index bf53727..43a09fe 100644 --- a/account/tests.py +++ b/account/tests.py @@ -1,4 +1,5 @@ import time + from unittest import mock from datetime import timedelta from copy import deepcopy @@ -610,6 +611,3 @@ class GenerateUserAPITest(APITestCase): resp = self.client.post(self.url, data=self.data) self.assertSuccess(resp) mock_workbook.assert_called() - data = resp.data["data"] - self.assertEqual(data["created_count"], 6) - self.assertEqual(data["get_count"], 0) diff --git a/account/views/admin.py b/account/views/admin.py index 3e8fb47..b1ef688 100644 --- a/account/views/admin.py +++ b/account/views/admin.py @@ -173,9 +173,6 @@ class GenerateUserAPI(APIView): if data["number_from"] > data["number_to"]: return self.error("Start number must be lower than end number") - password_length = data.get("password_length", 8) - default_email = data.get("default_email") - file_id = rand_str(8) filename = f"/tmp/{file_id}.xlsx" workbook = xlsxwriter.Workbook(filename) @@ -184,26 +181,27 @@ class GenerateUserAPI(APIView): worksheet.write("A1", "Username") worksheet.write("B1", "Password") i = 1 - created_count = 0 - get_count = 0 + + user_list = [] for number in range(data["number_from"], data["number_to"] + 1): - username = f"{data['prefix']}{number}{data['suffix']}" - password = rand_str(password_length) - user, created = User.objects.get_or_create(username=username) - user.email = default_email - user.set_password(password) - user.save() - if created: - UserProfile.objects.create(user=user) - created_count += 1 - else: - get_count += 1 - worksheet.write_string(i, 0, username) - worksheet.write_string(i, 1, password) - i += 1 - workbook.close() - return self.success({ - "file_id": file_id, - "created_count": created_count, - "get_count": get_count - }) + raw_password = rand_str(data["password_length"]) + user = User(username=f"{data['prefix']}{number}{data['suffix']}", password=make_password(raw_password)) + user.raw_password = raw_password + user_list.append(user) + + try: + with transaction.atomic(): + + ret = User.objects.bulk_create(user_list) + UserProfile.objects.bulk_create([UserProfile(user=user) for user in ret]) + for item in user_list: + worksheet.write_string(i, 0, item.username) + worksheet.write_string(i, 1, item.raw_password) + i += 1 + workbook.close() + return self.success({"file_id": file_id}) + except IntegrityError as e: + # Extract detail from exception message + # duplicate key value violates unique constraint "user_username_key" + # DETAIL: Key (username)=(root11) already exists. + return self.error(str(e).split("\n")[1]) diff --git a/oj/dev_settings.py b/oj/dev_settings.py index 724a5dc..5d9cb9e 100644 --- a/oj/dev_settings.py +++ b/oj/dev_settings.py @@ -7,7 +7,7 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'HOST': '127.0.0.1', - 'PORT': 5433, + 'PORT': 5432, 'NAME': "onlinejudge", 'USER': "onlinejudge", 'PASSWORD': 'onlinejudge' From 6d08011e2d864ee71dacc55762b04751c8a9ad47 Mon Sep 17 00:00:00 2001 From: virusdefender Date: Sat, 25 Nov 2017 03:37:40 +0800 Subject: [PATCH 095/106] deploy script --- .gitignore | 23 ++++++------ data/log/.gitkeep | 0 data/public/avatar/default.png | Bin 0 -> 16219 bytes data/public/upload/.gitkeep | 0 data/ssl/.gitkeep | 0 data/testcase/.gitkeep | 0 deploy/Dockerfile | 7 ++-- deploy/oj.conf | 62 +++++++++++++++++++++++++++++++++ deploy/run.sh | 37 +++++++------------- deploy/supervisor.conf | 28 ++++++++++----- oj/dev_settings.py | 16 ++------- oj/production_settings.py | 16 +++------ oj/settings.py | 14 +++++++- 13 files changed, 127 insertions(+), 76 deletions(-) create mode 100644 data/log/.gitkeep create mode 100644 data/public/avatar/default.png create mode 100644 data/public/upload/.gitkeep create mode 100644 data/ssl/.gitkeep create mode 100644 data/testcase/.gitkeep create mode 100644 deploy/oj.conf diff --git a/.gitignore b/.gitignore index e9a0324..822575d 100644 --- a/.gitignore +++ b/.gitignore @@ -54,21 +54,18 @@ db.db #*.out *.sqlite3 .DS_Store -log/ -static/release/css -static/release/js -static/release/img -static/src/upload_image/* build.txt tmp/ -test_case/ -release/ -upload/ custom_settings.py -docker-compose.yml *.zip -rsyncd.passwd -node_modules/ -update.sh -ssh.sh +data/log/* +!data/log/.gitkeep +data/testcase/* +!data/testcase/.gitkeep +data/ssl/* +!data/ssl/.gitkeep +data/static/upload/* +!data/static/upload/.gitkeep +data/static/avatar/* +!data/static/avatar/default.png diff --git a/data/log/.gitkeep b/data/log/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/public/avatar/default.png b/data/public/avatar/default.png new file mode 100644 index 0000000000000000000000000000000000000000..97f349564f0eefb2b07297ec77309597175c2b49 GIT binary patch literal 16219 zcmeHucRbba`~T}44mxIK6DrEc%E)$- zTa63&1#{6-xD6G5Wn2ay@GWK3WFV+4hWN+=0Y0B|R5S#Gh|lAGU;z)zogrFdS=O&Do}O&P1keUpVF=eDCM;f)*Yv4x zy~XduL`L#ym;{D!4hAWbzd8?rKi4sdOb~{4N@cbmFP#wg0ECZ$C~)JZvj`!UPrKjt zDo(7AXJHcHfGkiOo+m`^xeSBCHW~$`iUN#I!Dk2s5kVWcrA)mc=$Y8 z^~kp@6}!>*4>?Y`O&do1^aJj5*>xN!`VB;%`3G0YC-mOWMP-`_N@wof@;ZzXr%Teqn9-G6_#*7+lo zw7+eMS#y=^v&EjY`n8{H!)5`M$A`P)3*E2nx|4-=mUGg^;yJW!|19+hn`5D0Lq)HH z9gYvSz4RJNJXvJPAuPqmt_qhSMOnn}b45<=z#GqA!V zh>K)`LD0%vPEKy9_^vpiBy8but-1R3Er&-WCAKLjw1n5@yI&zTd%BVXRd(Y><&U}x zO&h)OZ%;myh|LDGBnO3@_Z3y}$L9S|3(Rnu{WO%L!F+hM)w4Q~r@ixMx!;zbt6*`^ zjal0NyVU;N#b3Q|gBj`ukC&x34E!ZRe{X!d1WcbdD9hWIjg8ITfAxE<%g$2YRiB-e zq(<-U&dCO^I9>F^UvqW#q}|9T%@r_}qH;q6LxVFZpVUxe-|*r*AMK%eE~Z$H zCGMJ6pw=v%Z}vhY!|}Kt^#o;$P`>!?0#+8e6$(a2$XN9 zqv!gG0xZyRe{+7Q+QBSQMezWirHk;%=KgDN>Q6|FQAgi9rHc&0wy!RUIAXJII^yf2 zNC~&!=*CsJ{Tj0Or=Wm;@V(8E*h~t+%xxj|Jt2q5?`)-9DVK})eOjCn8XauD{uwE| zKhzu$kcdwE!qVmd`|j%nL2#@AzBs9u4Nd69>*_V}p{|h{mgJW{iL+Vma#Y?~X0h{iQblcUh=V=L zQ)9+Xa?G0j^-N9e^-D~=u-7kYZ#fl^CQ;flVENMpTch%-8q|<#2$e_2tVqg z9j~%`J<;UHAT}=p+a?%XN8`!51do!*zhR!9AH%7DJWr{$(#hQCxYg;F;2rQ=;)%ci z`+Rt^%S74zr=-kM-;1yh6WG%`g>v6-``{iRz#t3l#wwf=ly!QOY(bcizf-d|oTt0Ar`!1A@o@2ISCYVle1lHx+v~45 zFq_AIAT(}~p|+3WpA;pej!iwgI)=>M!@gisVZ*y`bM^jsj6nPi1bm2I;E?JAPW>gWNb4_CqN$ixZ#TRB z($iO=jNK!BQ6m+ATavn=;?{&^n%|>a_TM2ceq9vB08*2M2%065!F>%!wP=W+kDX@H z-qfwNm2o`WS=BQ#>U_hp!OFYA%I*te3GJ@6ASNcBcr%s2r9Z=bT#J{3b!a|bh@k%S zd*bz_#c&SM7!HV{|KkT&wb#OK#%piHJ;+>HRbkxs#u~iJf1P`uiGL?U=573!wLU!j zH==kso1b(hRoAb6mbCa&;4J=YxTG5Rt>@PjZ9f~@oanc+fO(_ICq=(_6yV_@k2T z$a}$JhUDn{wYk%8f^`wT7 zln{AaAl&2kcc+i#z$1SRt!sNz*!G4cnhXk~yL0P)FahcJ?5gV;sjQos7;V&{c>cn# zr_BCVK4GFNGD+s7>I9ny1&p+;Bap?X0gzM*&<*V~ z-!8IbHE(HcoyXSL&48UfH&#~8ftRk=b75~+^nsyIOEQjOn z@4H;Qyw^ojJt;+)vhbQZd*CP%nh{3;464D;#SfY+_YoDE?{~;n`g6$LZig!c-q)(5 zz{kAejvY-Fw7$2s&|RLRk@kr3NCbvd417w?JXYuSYYA~74*v0->yMlsJX9BThb^a7 zVbCxmJH<=5k(}@T=EGlonS|Eo+pZ%2vs)dj+}YchEksDG zN5Pe!F^f2|xCFN4RLp4Cky1ioE#Gb$0JC3dXAWS!4d$Xkzg-cw8+$k3IU|^Sq#4iMy7=wscJg#AZ}ZVTU=_+=(v}HOQsfXnErseR9zbOeTa8EJFCt8v ze60ag6P|wq=WLcqC|cAYr`XG4G||bL1wP-_^L5;MPmCgLKACe-&=?lGbwH{I4zEqd zLri_6l;M^=MUp0qC9=a|%nl#DPnvHliSwc0>nXq8O9j#{)Cc=-I~%a4M~l(`P1VQy zB^Q~0_KG940HYq0-Wc0aJUq!W;D0>{&o<#ozdPIh?2>VT225uYN?5hTDST0&9F=G& zDZ?XR^q<{UW_Qup-xKwgUlMt9ZjzMc4;duz@WEKJ^9OZ|LUInGSex%;`f`-wTuATR zJ4L2yx1de0W}j%_LWbydj4G@wEPGOgxK$QWfxhgHGi~P%r-R9DwmDH-3|7l${{Jxv z=l0LRa)6HTntf&c{I@g^OcI{vMkI0enAt6;>>5tJ4eq0H>Z|sOFYvpxdAvTtwAp`E zdEHtNo(roi#U*^^(J(oxeGrgIp)LB8eJ74|i@oPp72v3QdRD*yUggOPSnq8Cceu0A z&5*%|oYM`g_gFLX^7683;Vm@9UkrDMp#;i7kEaZ<_v*>YT>mAu=*Bp)!?so9OVjX$u;}l}yaH%fKByRPK38s6fthX8C(xQ8yi@QQ~etbshvU z(*(h_ra#AB$DyrInee-Gx4@RKdFO;r2Aul&oE(X2f@!ia5~7x086OW78h(QD^)um5 z#dz@I4sR12wqQ_ko`Vo6s|L%u<9ob4oGoVru}?ysyIXh|sC0}a5l z*lTRZDma{5Vu)`CDkCbC$|Hmzz5v+E<#IyY8qhZY=EfU+ZuL-+FsH`R~r$^g6*!%z%rh4e#HDW8nZ+K44?#zu|b52)dVQ)7>hp2wfvXTCrJaYHCgc z+9H+-tQQmj;%?NPy+wNcLIVrVF{tB+z_9ns==<=VTrJZz2VS)JifS*^v3o>-TM6Q; z`jJLF1(4@B{khnE()Z~-%;0L85#)pVM{{u+Va4neUWj;N5zlpVNRg2kP$XeaCyy9c z%d3EQ?I4uqp+Ep*vdol^Mp7OH?a=R;I(y^L_6fJA)$1hSpI$6Qz@O_+AMI}md;hWe zuQ}d)_;4GSI9+8vszFiKxczzw4DK>|J@b0vn^cotOW^LNr-Bp!f*2v8scO3o(+y6N z@)LttpCJHzxIgLvmg%;PPSKra2_5uo}o7N;ArK$$;z z>az}r1@KAIhq(4PjA z2tVkc`1j3ETECf@xo2GKoV9rQ<#^0{c)k1T`zF6b0;Ipi?llp!Bky={_9g*O!N)^g zfW4rf-)pb*D#qTL^1UzVvxARmD?{P9xy6)EdHN-(r1XLvAVZ(`dNse3nUDQBSWu*$ zqaGNJ$wk_B*@5jID&brSmKAot47AhjTr|GG%X2m^kC-_wc- zO>6W$0Y5AbN^Q~|nzyqezcV~w10RUJD@XSpT zdKscUU!|vHmu}AIe~Zh!PBVZ@(1z$y5fKq90D$gn&a0%&a_=(nBjVwjT3T8K0l@?c zO^LpxK>rLLLw$Tp`ML!wASsVDAewXb)ieM6f^8^U?X}S{amNdCV9~TdlZ^1f^hWb0 zX8=p!_pJK6Pa7qzw0}dM6Sd&ph9nlJ-bFCkf|X=pI~$Ov_vMDfWzk&O-uY#7;_Z3JXNl++`Upx zN5^!O6lL!W4fiK4drr3A8NxTYLKkq%1ui)x}GiABn$$bqJ8`%C}P`yxs%VEAx_to4K3D8!#T+g2540ia=eGC4Kk!uFHVgv|-#;V<1FUTJ>Ei z)eqG2O^^R#i2y>7m&x+k{axj;W-5JEz$4`%BHkrGMbVj#cAr}sgG06QfpU%5a~H3n z>8@R8Utd#*KPvua$>~oTe}vKENfQ98(Gk7`{Np%KHr0>_Q8bGxqGGE9#|qd4T*$fk zOnWep3c6Pq6+MXKcclF###uw?_C*u5%}-B`ao)$>pGEy4V-7$koe|+WblEskkw)>k zZ3k609JtXTU_pg@!ijh6j`RedIxNHBAJIXP`CA3b&7Hd7QfQHHBz=bDqv=0>C2z|` z-=ma7Ma2tUwRZV7RAf93oR_M&yGuGM8+pU5>APUF>KaFKV-BkXjW-6xKL7~;v{e0& zlo0tK1BLSiOcEnHPVDCbU;2byYlDHHB!s1;rXB42BMB;`ATgQQ-xi#z?gD6fG08v}ra^bTmt?!DhXUW&lHy5SlJ zfL(`RfC~U5uCvT(yC$%1<^TY(Rt``mNJ*YDA_DFCHl3SBN=o4j$az$t4pnbh;KGTR z$8TGJkiR79Z;9f!J1lw_ykufQcPXc98a<(IGpTXBJh<1sD4~zKNjB;IZ<&t7 zVVXzWBNf*DpH_d!J2xyoQ2DWA26B(F&0IiD8g6Cs%A_2sHg4dc^AP+QXWO6E@!_V! z-}RKYS(rBr0|DS4Ul}c5K0Y7wo;v0gUYm)iP$Ef=*xO8nm@2?e?lDf)?$T3w{#kks zIH=EKm5&o$0=^k5H&(~X7A=9>DkQvQ_dpB~z}o3oh2YeBx+s4U=ee-P)00EPU5g8N zBD?W)SRm>y@992tuF>w;@G8Kq*(SVXl(Fz?hp8$ym6xw}5?EhZQg`+7`W*wW(WNxq z_uie`QN;g9B=Vklnl>sjvuS^ULAaicV4jtLpD?U%^O0bZ2BI9KHII$fQM?q$lg%%@ z+4(YmOuntvrW=YR1BNF;loZ8jf3&}tHeO_0&4Ou1TIK9o_h%c*$jGQ46N-F*DQLOk z0|>APQ+&N>|GlPHjJ$SNAILehD(daNbCL}h4{@)Nk$6vdB^MAxO@n`KS>uI zd@r|*S>7)nJl|O6yUU6@9x+tO{%EAsf`>LANu;nz)$1bVcThn}ce9QDmi-%=sd@3~ z&BMN+Gh&TcR{4eg$uLMhR(GcYG^Ah>O?M>C?BXTve);cc)vZ-j`VROzg>JJXAdD&Oa6=Zw9*^R(k)6rJqg*5RV`sZx@_jOyGhi6lyu z-Y||DSZj^btnE~j-`u%RP>{0gRHIMzuYJwgIIk*^u1YvCRYOScH@mdW0J}c_09!*HbdM1xm9Kt5F57d$-&$#I=xz6yttrmZYX*F*>{fgS_8J4 zf>_azqT}B(9~%kP$SEj3Q9nthu^lc>0hC`i8Ka0f2<$zV5E6%Mq+X`=?%mY9rnVro zu(qVsoFA`gVPsiQd*a0vT`0l#&n0;%yDsi{7y z_4h6=gd?7WpL>u@bAyMcYP8()HRp3HYpHtW2Lv)~Rb~yIeVt#v+4la#U}zz>1LkjR-3zB3Ne= z%f-1(`DcvQ0AN?*+!Ek+sUgIP-+W6jp*x@yhB4?na04ik)d$1G!lLRLt1a1wO9Fuj zIRF-;3@{M;3YA8`4P3H~P^^9xF7=&BnqMxiv(fqffna7CExS8k3NXbXH{owDEG01? zo$ADs{k*g`)jS1wkWW9=XM*&d0-0DscR#EfHN4%IDWdfQ;Kk&w=G0R;Fv?-d|F8&G zB1sl{ydYfs@Hw%hG_$uqm^zJS+UM}09%k6)69|>{Og#})Tr0q-ny!lG9^M7;Lgi5+ z8L-)%Z^Tjv@86XNp`?;1)RNu$MIv2uW0qIa0ZrC4F5It_#@>obaocNpcQklynJ5SA zx#zbln7?aZEC*=-#7cBBj7LwuI$m9(_C};n>TpG%`g9shd(YVM5G2CC4}_|7;4obM zXk1G^LqK9fCLzaS5=sAOukK? zWt&o5{RW(ppk({S<3RH?&T{^)pQ-7iNdZ5Ry=i{nl~1Wvb#)Gc$)!r?za-YDhT7QV zD`enrR-fv~$huvn%B8tsU_MdjCXjG>>&JYacMP&OAplu*pnX4mQ$sXt`AGNJetw^` zXYF-WompUEMa6*B@gHsdOr6Gi+<)@E#%ZKUxxHos2x44F{!>lfpH2{uyKAX6SR9e{7h0bkN z3>0{$zmHEb&z4h6cG1l&$hLS$dihKF<~u1~-Y@+M<+b?6t6+bE~`54iU`j5lf<_AhOKw&^_^W-hQtt zxr}COuh>+N^r&ae<+coO60lQx%`YMIkS&vwKJ!GM=Y6e_9NbtK`+$Ri(}#U zWOt8Ovw4I>Dy36A?Mr4N;mz8p^I=-Woo6j zw#r5&y*Af;1;nEK6#ja_lyXVOyQ|$bJRPI>b9cbJe|--G-&H^ZYrZeQD+}Eiq-ouMvhqe4ijnRD3{AqVwMRk1F?7qr@ARw(qo-{!HO4iM^aXhacy9`L5_x z4x_@nvivc6o4H#p|5c5idU>bl$s_{9kDs3YR{4ZpSnG=pvC#(uw!|;cJJ)mE&tO3t zMQY?-xLfCHVWHaPON;NV+nnj_92B-NKC^VU(Z$9!U%v+A9@=ay)Se&7pFn)q z0u|!c^Q|tMFJ)F`>-iBTr9p{v_O2$1IcN*}-!C8fJzk9iEJjAsa%tEX-?26v|E ziiJ>jlK=<%Ma#{}Zx=Fu0?P&dkXp0lJ{$hyq$+Hpz0-W|9j!q0{E*uqwh^wD~-@x$RJKOZf&E7N_h!6jOZVs1a@LMuUI0;6*Ds7Uk0vFlorw*%2~oCAD{ z`r4ofaq&KVw@Y4o4pl6uEP4zY>2Qb4@*;E!bjC+rSH}|OW@%;6A%#LO-cmjlnaf(( zOzH2`%;L_YXrw2ayAVoV=}M3M1Gz97;7fHA<{pGx>kK&BXfLEX>Yk5GDVq9LI{K2^ z*rXuI{P~o*rm=aR?LZ#=^PSmDncd&jx101vZxyW!-M*EDF5TnWJt4q|5~MFcfVo=2 zF7_XpW@82@zJa(#bU}3Iv0ZP$L4cQW$U9xBsH;MV0Y$VFYdw07$$ygv>asxfb$%^r zJ{jXC$hC_b9WZ4Mkjhf?03mp-s<*hLyvfEv{5gLeh2_x#QJlnhPowrQ0 z&{iD=tr^RLVtcla)?iQ<465vqBQ~j^#SA(9Z27UO!D+yL2^8cxGqbgf@G)I*RQ7?j zzg`~K*aP7y@n`rRdV)OG0B|VKK=^AvNsu+)cLccAkZ@tz zwo3_p=di?$JNu)%l?&RH$?ltTpW}&0MAh-IH;D4Hy2j%57``3oePlcQk-lKodnF0< zqtuH<PZAP%D@fiP<+z$|I7%BEsuwjheAgY;?1i2z2cti)jQw$&>v&$}f9$>1 z`6}dBXV+J~+C3Gv3)`v~^8nmw2~5u}rKNOnnV1jJn0&90TZl?lnuq2B-UA?x`U;@fEUT_c@l>J@_Q_Ujuv~d&>9HnrpxRD!nWyDP9TNP$WU4L z5a*iG`C@n{(?8G_y;Z^Fwcn| z(Z4|R-XZ2Eq7Iz;Rp_s&yH6zoNyKQ-_^wdbXuXGnafQ`$ytGXnp*H!eJWNtUTUzJ+m>U@u>IonLD4isi%BbRHCbQsV z)9q~}l`O!Ri=0VTJz{o^lnY;|KDxNAjPz~q#JA^oSBw?GPCxZGkGbx)l1r;LSeEZI z;o3q6)< zjo$-+$wR7O{4fvL!V*LR9zV7;|EQ}hFmrk+zNJFdL{gfX zm{q#8!S5+O=U_t;l1c+$Xk`@@H5VJ@;J){+GMbSW1eGb=`Fcfl6XZ(!9r8YwSQt?m z5C4W4g%_rBdHI~|Yc+a%Y5Tuhy5qfk9^#XO@*idyg(Y*%pg?si{zOYOAXhi*s(e%_ zs1nz}KOJ1G$v?>(Ht07S?BRT+o2t+#p5>tPb=F!@yS0*$odr=5y9_!Vit)`yqDxrQ z$f0Z^Eby;&_&P;QbGRu74qjfb;0lr_(7_rs^0g=xITzgoBy!22Hm{mBq0q7QW%c>R zO03LcU)t9^mtVL37$2y~UtYJ+Ma-RxCa`J}``nA%nuN?FpaD*?ED6BTiXn*%5(3~! z-cdrw=Ufxgaqgi#B_a4LONbDdN(e2gOT%kAH+UQO{0O__&jAkoO`dVd+?q6IXb3{hn*h8U zosd@MwOWg&M8P=Hw~{BN@e9J9G&;uc2oA z?I8m^2&99{RbhYkSHqFs&{z3*7}R|4NkG0&ML-_xDk{Xv(7sx^=&cDM-eK+~?s)4w z%&6HNc<*)$tzUw4c%t1+7tW4X!U6&+pib-=BRD-fg$F{X?M{7YTVwUh=wE~w2N*h! zoKvyuQMFURYOVj-0msU@^a>%&;2${5oJv#hXcR>Lg52EnP2w^s5BG(pyB#PJdA00URj zMa^G2-2P=5;pOxy^UIFVH@qVUmGsim-i4mDG@Tw7Gn`D}-uCAF7mk3Jd&8-?M`Pif+qte`*VK|d$z|4H zBOoarfB%r!fdH8fAw4s(fEy?TMKFkuY+}IHboLeh{T0xZG)bGrActGm?{H@{T_zYZ z7yfbKsSDTf50L#foX72YIvTEDY*MBacMXOWL2()n=N_cdBhk?kXDYTX7YNvm+KJ2b z!nR97_>HM}I!ftlO^07;dR)@}5eH7=RE1*82V25gy&4IoJDy)%1r@-C3un`(z(FCP z60llutVpaF!*K3?GYC*VmcI712ypVRsgjRDuQDEH6r;lU)zH*b9WLPnYzwP`&^ZbK z&8FZ)rN~2iVkFWQbOEzsefEF4^n;m8OdH>W+Dj5OUg#yWS!XGSMX26&`5wq9e{kdM z2j)Q4dj*xYOZbQS%x0qlU4W$0F(t^{i+sL&j@}rj<@cd(DQSK?NjO?ff68SFw_$VO zAY9#KS|bXa#AB)@QEta+5?&v;z&9B9BxiccWnAwNM0?BG(z!9;KmGVrhbjda3WV_0 zod$$ZHK?>0g8rO_C+QxNSOA<2K#j&^IPLEc{b|s|GO71ugA4=qUJ1h^coos&e73JO zq?I5@$K#pIueC-v?c0MQxNS=iBAXTc_ag4XbX$}8?-M{##BDcg)uY`U0B;OzYNdXy zHUQ@hN&%1>2UR4P`8D(KFCLYUJfaj(WP#hj)1a|Fhzfesf|=P1t*s5}YYUL>&f=$N zhC*ojq*4Loe84bXSV^BA2|y%qg_;tH3#VY~4?yqthkcw-Fz^6&N6@(X7+AG+f-mKL zkE-I&1x)6^8QK9-l@+@+mI;K5qC$Y4phtI}k_i>P=%KdF@H8!fK#f5AwYW#Iq~8JM z7H$-TJbH@-#P|w83WCuuv_Au&@hM${xljsGw1JB_c|nMLP1InS z_x8{ELB8BukZoT^NOzI|;q2c7=wxCzKd8RqYe6EB4Ri$C1Q^ilRSw7p#{Ue^Yv(no z=|vI~!_0ZEUvb$m2F>3)tJPEYd~vsh3bdg_`^|yv8h?4o4TgZq+0;^hPE3lFAN_wO z`PN!-D_#DJGu`X0ed9@zz@1;iJ;kPt5gY&Bq+A)OURo*Be7PR)ygxn(Id*_VJ*(-= z|6|~TTe|sTvtL(_cjT{^?UNrTnPwOOAG11BqI$%u@ zUooUZHVtm#o9~J*;DKk57%z}2!IECjMKaN3nI51 zCf_Vv{nxVzxDu>H-{+ZCIWjGN4>x~Nj*JJ6ouW+(oXtyraDHc8Z77&VXDfgyOoKCPiu20K= zR|;q$)sSq>gD%j)X$webY|G!Xz^Hm3&HPlA<*0h<#SQkvNr?8k@sB;WP@*rNr=mP} zQA|@z{~l~_KGAFp)VG2q-I_wqrZyj=^;rIv1@S2lsLva@uZ&oLZo$?M|C-qov@wip z3njc_XzyXIBHZiOkVg2<`I4U9Te=o3>vr3D`#ckuD=-5z+8hMKmVY)y$5{$WZI8d# zI9<8?N)fWT*ba& z{QGDtxIndfy0b_QzRkuq`s}J6!m}^<%f2GN5)>HvtH-xR} zy?cM?pG!#MS_eIv+MKc`GXY}5Nx4ksAVw5|m#FkWAN0QVBFW!vsRx1=q;Pe!i-aw* zuu@P^8Y;KcsCSwrsQP;@A|;p_SKbFbvBm-sUtvwC(5$%$SEsT2(Q*bT*(sjEEx*1g z17RM*vTHY5w1xtIDcGOS_D}qAr0lJW05255ueDueMf*u*#^6jk}K9arN4We@}EvpZ*DMc7X^Sc8^HI7Wv3b-|jT-PiyixUB$E zCSNXdq5rGuxupw@F z<_%6YQB+kG-%QJd@Ak@WZ-v9m0VXO08ngpmAF_=+yBv!5aOIGtfxNiEB4ZjzK6>e& zH(B&+-<+W(G>ZwM;`1Kpol&Ez&Y(UGHr2T2jWL3WInBsIw8e{}3^)miAaw5G*y`-Q zs&s7l*zN^9)GEBpby@51cz3Fd>#zQHASJ+iPP)N;66OOa>ObBW@UVJCMx&r}3})Vu zz37U&a27C;RtkykcO>#nt{*QyWZRz|?FZT?=Ag}DoI0YYDbY6@#ORD1&I`pN8dKY# z(wdqPHXBZ}kbmztRnq4Xc(04UsG8`XSq3)0gW?n()={!&6o+cC9B4yQ0!Y5}h7sV+ z?$Qb7W7n(Brr$$a1^x&jrcq_Po;egp>@69-4BJ^5jbq(gm+Ya#c_u7J`qz*Jn6C3p zcE^mJMz8_UxHUrgR}mtSx!?4Fw!+Ng6^p+~0I$*mBFVsCU51+jd%U1c#(f?j+RxGl zOIN?2g*WzCIfNj2?u$4jr4R^XC`g=x^Vxx{91wK123`SrX22_UN38o%K4;h3!NCHl zr%H(O*J8q#V8A=|ffnD3qUDYWs+vQX)}cWuJ9BeAS$ zPQ1856ru=xERJO~sizgL2>@Y zoWO4c?yy&KFgDz4$;Rwd9ps@Xk{Yx~CL z#`=Q#yE~wp{?|!BW7M~b-&Z&{q~m@m%91-S^}clnIYviP2=N&~zzR+~+2l74##~&N zov#mvra?|MO2DDyK(`wNj$(+D-Pjk+K!a<;&xxRXL2|;s`eskLG*N#G>Uz>(;a3d| zTiEUAz;_8Z|Fhkrv7sb%_dpY4m?ZxFDLfpsy4J`i;Kmy8Xx4fn+~R$#eLx;8gA z871BQ3C`35m1$4!_l2hB!@)Zd6$jD=jc`u?nQ_ntJN2;g?>9e>>ZuPJ(WT)+TM7Gm#95Z{j8 z9KqQhs&RYhf~fTWvjx=fV9mO?rkn>p$3ly$_S4_X8N(cNdqpCfN zM;%3Q5H$tt?$GEl_6&R7lg0-UY9ZsG%^>A+hMya)(JKO0q+*n$LDd+Ap zJ1E07?LM--q7L*O19#HFjTJd~`S{GXqNo(%&?&PSU+1eoh5=>}_@^YNE?a!tJm`M_ Do5vI_ literal 0 HcmV?d00001 diff --git a/data/public/upload/.gitkeep b/data/public/upload/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/ssl/.gitkeep b/data/ssl/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/testcase/.gitkeep b/data/testcase/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 55af522..d7416a2 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -3,11 +3,10 @@ FROM python:3.6-alpine3.6 ENV OJ_ENV production RUN apk add --no-cache supervisor jpeg-dev zlib-dev postgresql-dev freetype-dev -ADD requirements.txt /tmp -RUN apk add --no-cache build-base && \ +ADD deploy/requirements.txt /tmp +RUN apk add --update --no-cache build-base nginx openssl && \ pip install --no-cache-dir -r /tmp/requirements.txt -i https://pypi.doubanio.com/simple && \ apk del build-base --purge -VOLUME [ "/app" ] - +ADD . /app CMD sh /app/deploy/run.sh diff --git a/deploy/oj.conf b/deploy/oj.conf new file mode 100644 index 0000000..504de81 --- /dev/null +++ b/deploy/oj.conf @@ -0,0 +1,62 @@ +user nobody; +daemon off; +pid /tmp/nginx.pid; +worker_processes auto; +pcre_jit on; +error_log /data/log/nginx_error.log warn; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + server_tokens off; + keepalive_timeout 65; + sendfile on; + tcp_nodelay on; + + gzip on; + gzip_vary on; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /app/data/log/nginx_access.log main; + + upstream backend { + server 127.0.0.1:8080; + keepalive 32; + } + + server { + listen 8000 default_server; + server_name _; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + client_max_body_size 200M; + + location /public { + root /app/data; + } + + location /api { + proxy_pass http://backend; + proxy_set_header Host $host; + } + + location /admin { + root /app/dist/admin; + try_files $uri $uri/ /index.html =404; + } + + location / { + root /app/dist; + try_files $uri $uri/ /index.html =404; + } + } + +} + diff --git a/deploy/run.sh b/deploy/run.sh index c8ca9d5..64a13b5 100644 --- a/deploy/run.sh +++ b/deploy/run.sh @@ -1,39 +1,28 @@ #!/bin/bash BASE=/app +DATA=$BASE/data -if [ ! -f "$BASE/custom_settings.py" ]; then - echo SECRET_KEY=\"$(cat /dev/urandom | head -1 | md5sum | head -c 32)\" >> /app/oj/custom_settings.py +if [ ! -f "$BASE/oj/custom_settings.py" ]; then + echo SECRET_KEY=\"$(cat /dev/urandom | head -1 | md5sum | head -c 32)\" >> $BASE/oj/custom_settings.py fi -if [ ! -d "$BASE/log" ]; then - mkdir -p $BASE/log -fi +mkdir -p $DATA/log $DATA/testcase $DATA/public/upload cd $BASE -find . -name "*.pyc" -delete - -# wait for postgresql start -sleep 6 n=0 -while [ $n -lt 3 ] +while [ $n -lt 5 ] do -python manage.py migrate -if [ $? -ne 0 ]; then - echo "Can't start server, try again in 3 seconds.." - sleep 3 - let "n+=1" - continue -fi -python manage.py initinstall -break + python manage.py migrate --no-input && + python manage.py initinstall && + break + n=$(($n+1)) + echo "Failed to migrate, going to retry..." + sleep 8 done -if [ $n -eq 3 ]; then - echo "Can't start server, please check log file for details." - exit 1 -fi +cp $BASE/deploy/oj.conf /etc/nginx/conf.d/default.conf -chown -R nobody:nogroup /data/log /data/test_case /data/avatar /data/upload +chown -R nobody:nogroup $DATA $BASE/dist exec supervisord -c /app/deploy/supervisor.conf diff --git a/deploy/supervisor.conf b/deploy/supervisor.conf index fc248d8..f36cf83 100644 --- a/deploy/supervisor.conf +++ b/deploy/supervisor.conf @@ -1,20 +1,32 @@ [supervisord] -logfile=/app/log/supervisord.log +logfile=/app/data/log/supervisord.log logfile_maxbytes=10MB logfile_backups=10 loglevel=info pidfile=/tmp/supervisord.pid nodaemon=true -childlogdir=/data/log/ +childlogdir=/app/data/log/ [supervisorctl] serverurl=unix:///tmp/supervisor.sock -[program:gunicorn] -command=sh -c "gunicorn oj.wsgi --user nobody -b 0.0.0.0:8080 --reload -w `grep -c ^processor /proc/cpuinfo`" +[program:nginx] +command=nginx -c /app/deploy/oj.conf directory=/app/ -stdout_logfile=/data/log/gunicorn.log -stderr_logfile=/data/log/gunicorn.log +stdout_logfile=/app/data/log/nginx.log +stderr_logfile=/app/data/log/nginx.log +autostart=true +autorestart=true +startsecs=5 +stopwaitsecs = 5 +killasgroup=true + +[program:gunicorn] +command=sh -c "gunicorn oj.wsgi --user nobody -b 127.0.0.1:8080 --reload -w `grep -c ^processor /proc/cpuinfo`" +directory=/app/ +user=nobody +stdout_logfile=/app/data/log/gunicorn.log +stderr_logfile=/app/data/log/gunicorn.log autostart=true autorestart=true startsecs=5 @@ -25,8 +37,8 @@ killasgroup=true command=celery -A oj worker -l warning directory=/app/ user=nobody -stdout_logfile=/data/log/celery.log -stderr_logfile=/data/log/celery.log +stdout_logfile=/app/data/log/celery.log +stderr_logfile=/app/data/log/celery.log autostart=true autorestart=true startsecs=5 diff --git a/oj/dev_settings.py b/oj/dev_settings.py index 5d9cb9e..5c75a0b 100644 --- a/oj/dev_settings.py +++ b/oj/dev_settings.py @@ -7,7 +7,7 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'HOST': '127.0.0.1', - 'PORT': 5432, + 'PORT': 5433, 'NAME': "onlinejudge", 'USER': "onlinejudge", 'PASSWORD': 'onlinejudge' @@ -24,16 +24,4 @@ DEBUG = True ALLOWED_HOSTS = ["*"] -TEST_CASE_DIR = "/tmp" - -LOG_PATH = f"{BASE_DIR}/log/" - -AVATAR_URI_PREFIX = "/static/avatar" -AVATAR_UPLOAD_DIR = f"{BASE_DIR}{AVATAR_URI_PREFIX}" - -UPLOAD_PREFIX = "/static/upload" -UPLOAD_DIR = f"{BASE_DIR}{UPLOAD_PREFIX}" - -STATICFILES_DIRS = [ - os.path.join(BASE_DIR, "static"), -] +DATA_DIR = f"{BASE_DIR}/data" diff --git a/oj/production_settings.py b/oj/production_settings.py index 3f9dee6..026c53a 100644 --- a/oj/production_settings.py +++ b/oj/production_settings.py @@ -8,8 +8,8 @@ def get_env(name, default=""): DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'HOST': get_env("POSTGRES_HOST", "postgres"), - 'PORT': get_env("POSTGRES_PORT", "5433"), + 'HOST': get_env("POSTGRES_HOST", "oj-postgres"), + 'PORT': get_env("POSTGRES_PORT", "5432"), 'NAME': get_env("POSTGRES_DB"), 'USER': get_env("POSTGRES_USER"), 'PASSWORD': get_env("POSTGRES_PASSWORD") @@ -17,7 +17,7 @@ DATABASES = { } REDIS_CONF = { - "host": get_env("REDIS_HOST", "redis"), + "host": get_env("REDIS_HOST", "oj-redis"), "port": get_env("REDIS_PORT", "6379") } @@ -25,12 +25,4 @@ DEBUG = False ALLOWED_HOSTS = ['*'] -AVATAR_URI_PREFIX = "/static/avatar" -AVATAR_UPLOAD_DIR = "/data/avatar" - -UPLOAD_PREFIX = "/static/upload" -UPLOAD_DIR = "/data/upload" - -TEST_CASE_DIR = "/data/test_case" -LOG_PATH = "/data/log" -DEFAULT_JUDGE_SERVER_SERVICE_URL = "http://judge-server:8080/" +DATA_DIR = "/data" diff --git a/oj/settings.py b/oj/settings.py index 0c6e768..e1de79b 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -109,10 +109,22 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.8/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = '/storage/' AUTH_USER_MODEL = 'account.User' +TEST_CASE_DIR = os.path.join(DATA_DIR, "testcase") +LOG_PATH = os.path.join(DATA_DIR, "log") + +AVATAR_URI_PREFIX = "/public/avatar" +AVATAR_UPLOAD_DIR = f"{DATA_DIR}{AVATAR_URI_PREFIX}" + +UPLOAD_PREFIX = "/public/upload" +UPLOAD_DIR = f"{DATA_DIR}{UPLOAD_PREFIX}" + +STATICFILES_DIRS = [os.path.join(DATA_DIR, "public")] + + LOGGING = { 'version': 1, 'disable_existing_loggers': False, From cf40deb97c81e0a793001b69154b86efa66e2753 Mon Sep 17 00:00:00 2001 From: virusdefender Date: Sat, 25 Nov 2017 12:30:00 +0800 Subject: [PATCH 096/106] add ssl cert --- .gitignore | 4 +- data/{testcase => test_case}/.gitkeep | 0 deploy/nginx/common.conf | 20 +++++++++ deploy/nginx/nginx.conf | 56 ++++++++++++++++++++++++ deploy/oj.conf | 62 --------------------------- deploy/run.sh | 22 ++++++---- deploy/supervisor.conf | 2 +- oj/settings.py | 2 +- 8 files changed, 93 insertions(+), 75 deletions(-) rename data/{testcase => test_case}/.gitkeep (100%) create mode 100644 deploy/nginx/common.conf create mode 100644 deploy/nginx/nginx.conf delete mode 100644 deploy/oj.conf diff --git a/.gitignore b/.gitignore index 822575d..66e0910 100644 --- a/.gitignore +++ b/.gitignore @@ -61,8 +61,8 @@ custom_settings.py data/log/* !data/log/.gitkeep -data/testcase/* -!data/testcase/.gitkeep +data/test_case/* +!data/test_case/.gitkeep data/ssl/* !data/ssl/.gitkeep data/static/upload/* diff --git a/data/testcase/.gitkeep b/data/test_case/.gitkeep similarity index 100% rename from data/testcase/.gitkeep rename to data/test_case/.gitkeep diff --git a/deploy/nginx/common.conf b/deploy/nginx/common.conf new file mode 100644 index 0000000..478e288 --- /dev/null +++ b/deploy/nginx/common.conf @@ -0,0 +1,20 @@ +location /public { + root /app/data; +} + +location /api { + proxy_pass http://backend; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + client_max_body_size 200M; +} + +location /admin { + root /app/dist/admin; + try_files $uri $uri/ /index.html =404; +} + +location / { + root /app/dist; + try_files $uri $uri/ /index.html =404; +} \ No newline at end of file diff --git a/deploy/nginx/nginx.conf b/deploy/nginx/nginx.conf new file mode 100644 index 0000000..54eb7ed --- /dev/null +++ b/deploy/nginx/nginx.conf @@ -0,0 +1,56 @@ +user nobody; +daemon off; +pid /tmp/nginx.pid; +worker_processes auto; +pcre_jit on; +error_log /data/log/nginx_error.log warn; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + server_tokens off; + keepalive_timeout 65; + sendfile on; + tcp_nodelay on; + + gzip on; + gzip_vary on; + gzip_types application/javascript text/css; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /data/log/nginx_access.log main; + + upstream backend { + server 127.0.0.1:8080; + keepalive 32; + } + + server { + listen 8000 default_server; + server_name _; + + include common.conf; + } + + server { + listen 1443 ssl http2 default_server; + server_name _; + ssl_certificate /data/ssl/server.crt; + ssl_certificate_key /data/ssl/server.key; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + + include common.conf; + } + +} + diff --git a/deploy/oj.conf b/deploy/oj.conf deleted file mode 100644 index 504de81..0000000 --- a/deploy/oj.conf +++ /dev/null @@ -1,62 +0,0 @@ -user nobody; -daemon off; -pid /tmp/nginx.pid; -worker_processes auto; -pcre_jit on; -error_log /data/log/nginx_error.log warn; - -events { - worker_connections 1024; -} - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - server_tokens off; - keepalive_timeout 65; - sendfile on; - tcp_nodelay on; - - gzip on; - gzip_vary on; - - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /app/data/log/nginx_access.log main; - - upstream backend { - server 127.0.0.1:8080; - keepalive 32; - } - - server { - listen 8000 default_server; - server_name _; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Host $http_host; - client_max_body_size 200M; - - location /public { - root /app/data; - } - - location /api { - proxy_pass http://backend; - proxy_set_header Host $host; - } - - location /admin { - root /app/dist/admin; - try_files $uri $uri/ /index.html =404; - } - - location / { - root /app/dist; - try_files $uri $uri/ /index.html =404; - } - } - -} - diff --git a/deploy/run.sh b/deploy/run.sh index 64a13b5..356ee94 100644 --- a/deploy/run.sh +++ b/deploy/run.sh @@ -1,15 +1,21 @@ #!/bin/bash -BASE=/app -DATA=$BASE/data +APP=/app +DATA=/data -if [ ! -f "$BASE/oj/custom_settings.py" ]; then - echo SECRET_KEY=\"$(cat /dev/urandom | head -1 | md5sum | head -c 32)\" >> $BASE/oj/custom_settings.py +if [ ! -f "$APP/oj/custom_settings.py" ]; then + echo SECRET_KEY=\"$(cat /dev/urandom | head -1 | md5sum | head -c 32)\" >> $APP/oj/custom_settings.py fi -mkdir -p $DATA/log $DATA/testcase $DATA/public/upload +mkdir -p $DATA/log $DATA/ssl $DATA/test_case $DATA/public/upload -cd $BASE +SSL="$DATA/ssl" +if [ ! -f "$SSL/server.key" ]; then + openssl req -x509 -newkey rsa:2048 -keyout "$SSL/server.key" -out "$SSL/server.crt" -days 1000 \ + -subj "/C=CN/ST=Beijing/L=Beijing/O=Beijing OnlineJudge Technology Co., Ltd./OU=Service Infrastructure Department/CN=`hostname`" -nodes +fi + +cd $APP n=0 while [ $n -lt 5 ] @@ -22,7 +28,5 @@ do sleep 8 done -cp $BASE/deploy/oj.conf /etc/nginx/conf.d/default.conf - -chown -R nobody:nogroup $DATA $BASE/dist +chown -R nobody:nogroup $DATA $APP/dist exec supervisord -c /app/deploy/supervisor.conf diff --git a/deploy/supervisor.conf b/deploy/supervisor.conf index f36cf83..c9e5032 100644 --- a/deploy/supervisor.conf +++ b/deploy/supervisor.conf @@ -11,7 +11,7 @@ childlogdir=/app/data/log/ serverurl=unix:///tmp/supervisor.sock [program:nginx] -command=nginx -c /app/deploy/oj.conf +command=nginx -c /app/deploy/nginx/nginx.conf directory=/app/ stdout_logfile=/app/data/log/nginx.log stderr_logfile=/app/data/log/nginx.log diff --git a/oj/settings.py b/oj/settings.py index e1de79b..52c3f2c 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -113,7 +113,7 @@ STATIC_URL = '/storage/' AUTH_USER_MODEL = 'account.User' -TEST_CASE_DIR = os.path.join(DATA_DIR, "testcase") +TEST_CASE_DIR = os.path.join(DATA_DIR, "test_case") LOG_PATH = os.path.join(DATA_DIR, "log") AVATAR_URI_PREFIX = "/public/avatar" From 1f9eca8b7d32979124f3ec5487112a12a4dd25c9 Mon Sep 17 00:00:00 2001 From: virusdefender Date: Sat, 25 Nov 2017 12:54:29 +0800 Subject: [PATCH 097/106] fix initadmin script --- .travis.yml | 2 -- deploy/run.sh | 2 +- utils/management/commands/initadmin.py | 38 -------------------- utils/management/commands/initinstall.py | 17 --------- utils/management/commands/inituser.py | 44 ++++++++++++++++++++++++ 5 files changed, 45 insertions(+), 58 deletions(-) delete mode 100644 utils/management/commands/initadmin.py delete mode 100644 utils/management/commands/initinstall.py create mode 100644 utils/management/commands/inituser.py diff --git a/.travis.yml b/.travis.yml index 7782d02..5a01bfa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,11 +9,9 @@ before_install: - docker run -it -d -e POSTGRES_DB=onlinejudge -e POSTGRES_USER=onlinejudge -e POSTGRES_PASSWORD=onlinejudge -p 127.0.0.1:5433:5432 postgres:10 install: - pip install -r deploy/requirements.txt - - mkdir log test_case upload - cp oj/custom_settings.example.py oj/custom_settings.py - echo "SECRET_KEY=\"`cat /dev/urandom | head -1 | md5sum | head -c 32`\"" >> oj/custom_settings.py - python manage.py migrate - - python manage.py initadmin script: - docker ps -a - flake8 . diff --git a/deploy/run.sh b/deploy/run.sh index 356ee94..bcf7476 100644 --- a/deploy/run.sh +++ b/deploy/run.sh @@ -21,7 +21,7 @@ n=0 while [ $n -lt 5 ] do python manage.py migrate --no-input && - python manage.py initinstall && + python manage.py inituser --username=root --password=rootroot --action=create_super_admin && break n=$(($n+1)) echo "Failed to migrate, going to retry..." diff --git a/utils/management/commands/initadmin.py b/utils/management/commands/initadmin.py deleted file mode 100644 index 78e8a45..0000000 --- a/utils/management/commands/initadmin.py +++ /dev/null @@ -1,38 +0,0 @@ -from django.core.management.base import BaseCommand - -from account.models import AdminType, ProblemPermission, User, UserProfile -from utils.shortcuts import rand_str # NOQA - - -class Command(BaseCommand): - def handle(self, *args, **options): - try: - admin = User.objects.get(username="root") - if admin.admin_type == AdminType.SUPER_ADMIN: - self.stdout.write(self.style.WARNING("Super admin user 'root' already exists, " - "would you like to reset it's password?\n" - "Input yes to confirm: ")) - if input() == "yes": - rand_password = "rootroot" - admin.save() - self.stdout.write(self.style.SUCCESS("Successfully created super admin user password.\n" - "Username: root\nPassword: %s\n" - "Remember to change password and turn on two factors auth " - "after installation." % rand_password)) - else: - self.stdout.write(self.style.SUCCESS("Nothing happened")) - else: - self.stdout.write(self.style.ERROR("User 'root' is not super admin.")) - except User.DoesNotExist: - user = User.objects.create(username="root", email="root@oj.com", admin_type=AdminType.SUPER_ADMIN, - problem_permission=ProblemPermission.ALL) - # for dev - # rand_password = rand_str(length=6) - rand_password = "rootroot" - user.set_password(rand_password) - user.save() - UserProfile.objects.create(user=user) - self.stdout.write(self.style.SUCCESS("Successfully created super admin user.\n" - "Username: root\nPassword: %s\n" - "Remember to change password and turn on two factors auth " - "after installation." % rand_password)) diff --git a/utils/management/commands/initinstall.py b/utils/management/commands/initinstall.py deleted file mode 100644 index bdb0f0a..0000000 --- a/utils/management/commands/initinstall.py +++ /dev/null @@ -1,17 +0,0 @@ -import os -from account.models import User -from django.core.management.base import BaseCommand - - -class Command(BaseCommand): - def handle(self, *args, **options): - if User.objects.exists(): - self.stdout.write(self.style.WARNING("Nothing happened\n")) - return - try: - if os.system("python manage.py initadmin") != 0: - self.stdout.write(self.style.ERROR("Failed to execute command 'initadmin'")) - exit(1) - self.stdout.write(self.style.SUCCESS("Done")) - except Exception as e: - self.stdout.write(self.style.ERROR("Failed to initialize, error: " + str(e))) diff --git a/utils/management/commands/inituser.py b/utils/management/commands/inituser.py new file mode 100644 index 0000000..c3f0827 --- /dev/null +++ b/utils/management/commands/inituser.py @@ -0,0 +1,44 @@ +from django.core.management.base import BaseCommand + +from account.models import AdminType, ProblemPermission, User, UserProfile +from utils.shortcuts import rand_str # NOQA + + +class Command(BaseCommand): + def add_arguments(self, parser): + parser.add_argument("--username", type=str) + parser.add_argument("--password", type=str) + parser.add_argument("--action", type=str) + + def handle(self, *args, **options): + username = options["username"] + password = options["password"] + action = options["action"] + + if not(username and password and action): + self.stdout.write(self.style.ERROR("Invalid args")) + exit(1) + + if action == "create_super_admin": + if User.objects.filter(username=username).exists(): + self.stdout.write(self.style.SUCCESS(f"User {username} exists, operation ignored")) + exit() + + user = User.objects.create(username=username, admin_type=AdminType.SUPER_ADMIN, + problem_permission=ProblemPermission.ALL) + user.set_password(password) + user.save() + UserProfile.objects.create(user=user) + + self.stdout.write(self.style.SUCCESS("User created")) + elif action == "reset": + try: + user = User.objects.get(username=username) + user.set_password(password) + user.save() + self.stdout.write(self.style.SUCCESS(f"Password is rested")) + except User.DoesNotExist: + self.stdout.write(self.style.ERROR(f"User {username} doesnot exist, operation ignored")) + exit(1) + else: + raise ValueError("Invalid action") From 05475fb161189bfc858e6cd287d2b727ab1d0967 Mon Sep 17 00:00:00 2001 From: virusdefender Date: Sat, 25 Nov 2017 13:11:24 +0800 Subject: [PATCH 098/106] download frontend dist from github release --- deploy/Dockerfile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/deploy/Dockerfile b/deploy/Dockerfile index d7416a2..b7f7997 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -4,9 +4,14 @@ ENV OJ_ENV production RUN apk add --no-cache supervisor jpeg-dev zlib-dev postgresql-dev freetype-dev ADD deploy/requirements.txt /tmp -RUN apk add --update --no-cache build-base nginx openssl && \ - pip install --no-cache-dir -r /tmp/requirements.txt -i https://pypi.doubanio.com/simple && \ +RUN printf "https://mirrors.tuna.tsinghua.edu.cn/alpine/v3.6/community/\nhttps://mirrors.tuna.tsinghua.edu.cn/alpine/v3.6/main/" > /etc/apk/repositories && \ + apk add --update --no-cache build-base nginx openssl curl unzip && \ + pip install --no-cache-dir -r /tmp/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple && \ apk del build-base --purge ADD . /app +WORKDIR /app +RUN curl -L $(curl -s https://api.github.com/repos/QingdaoU/OnlineJudgeFE/releases/latest | grep /dist.zip | cut -d '"' -f 4) -o dist.zip && \ + unzip dist.zip && \ + rm dist.zip CMD sh /app/deploy/run.sh From 7fce29cb717b0b758bf090a7d93c9b99cfe9562c Mon Sep 17 00:00:00 2001 From: virusdefender Date: Sat, 25 Nov 2017 15:47:56 +0800 Subject: [PATCH 099/106] fix static file path --- .gitignore | 8 ++++---- oj/settings.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 66e0910..3a8cb90 100644 --- a/.gitignore +++ b/.gitignore @@ -65,7 +65,7 @@ data/test_case/* !data/test_case/.gitkeep data/ssl/* !data/ssl/.gitkeep -data/static/upload/* -!data/static/upload/.gitkeep -data/static/avatar/* -!data/static/avatar/default.png +data/public/upload/* +!data/public/upload/.gitkeep +data/public/avatar/* +!data/public/avatar/default.png diff --git a/oj/settings.py b/oj/settings.py index 52c3f2c..d1ab211 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -109,7 +109,7 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.8/howto/static-files/ -STATIC_URL = '/storage/' +STATIC_URL = '/public/' AUTH_USER_MODEL = 'account.User' From 79717c82b1869bab0fa6139c81843911854467d9 Mon Sep 17 00:00:00 2001 From: virusdefender Date: Sat, 25 Nov 2017 19:12:37 +0800 Subject: [PATCH 100/106] move Dockerfile --- deploy/Dockerfile => Dockerfile | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) rename deploy/Dockerfile => Dockerfile (70%) diff --git a/deploy/Dockerfile b/Dockerfile similarity index 70% rename from deploy/Dockerfile rename to Dockerfile index b7f7997..9e3f69c 100644 --- a/deploy/Dockerfile +++ b/Dockerfile @@ -1,16 +1,14 @@ FROM python:3.6-alpine3.6 ENV OJ_ENV production -RUN apk add --no-cache supervisor jpeg-dev zlib-dev postgresql-dev freetype-dev - -ADD deploy/requirements.txt /tmp -RUN printf "https://mirrors.tuna.tsinghua.edu.cn/alpine/v3.6/community/\nhttps://mirrors.tuna.tsinghua.edu.cn/alpine/v3.6/main/" > /etc/apk/repositories && \ - apk add --update --no-cache build-base nginx openssl curl unzip && \ - pip install --no-cache-dir -r /tmp/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple && \ - apk del build-base --purge ADD . /app WORKDIR /app + +RUN printf "https://mirrors.tuna.tsinghua.edu.cn/alpine/v3.6/community/\nhttps://mirrors.tuna.tsinghua.edu.cn/alpine/v3.6/main/" > /etc/apk/repositories && \ + apk add --update --no-cache build-base nginx openssl curl unzip supervisor jpeg-dev zlib-dev postgresql-dev freetype-dev && \ + pip install --no-cache-dir -r /app/deploy/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple && \ + apk del build-base --purge RUN curl -L $(curl -s https://api.github.com/repos/QingdaoU/OnlineJudgeFE/releases/latest | grep /dist.zip | cut -d '"' -f 4) -o dist.zip && \ unzip dist.zip && \ rm dist.zip From 00eb3b19674dffb57b5102320b0a2cbba30758b8 Mon Sep 17 00:00:00 2001 From: virusdefender Date: Sat, 25 Nov 2017 21:47:51 +0800 Subject: [PATCH 101/106] add api to reset openapi appkey and related middleware --- account/middleware.py | 12 ++++++++++++ account/tests.py | 16 ++++++++++++++++ account/urls/oj.py | 5 +++-- account/views/oj.py | 12 ++++++++++++ oj/settings.py | 1 + 5 files changed, 44 insertions(+), 2 deletions(-) diff --git a/account/middleware.py b/account/middleware.py index 1ef78cc..245c32a 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -3,6 +3,18 @@ from django.utils.timezone import now from django.utils.deprecation import MiddlewareMixin from utils.api import JSONResponse +from account.models import User + + +class APITokenAuthMiddleware(MiddlewareMixin): + def process_request(self, request): + appkey = request.META.get("HTTP_APPKEY") + if appkey: + try: + request.user = User.objects.get(open_api_appkey=appkey, open_api=True, is_disabled=False) + request.csrf_processing_done = True + except User.DoesNotExist: + pass class SessionRecordMiddleware(MiddlewareMixin): diff --git a/account/tests.py b/account/tests.py index 43a09fe..97880aa 100644 --- a/account/tests.py +++ b/account/tests.py @@ -611,3 +611,19 @@ class GenerateUserAPITest(APITestCase): resp = self.client.post(self.url, data=self.data) self.assertSuccess(resp) mock_workbook.assert_called() + + +class OpenAPIAppkeyAPITest(APITestCase): + def setUp(self): + self.user = self.create_super_admin() + self.url = self.reverse("open_api_appkey_api") + + def test_reset_appkey(self): + resp = self.client.post(self.url, data={}) + self.assertFailed(resp) + + self.user.open_api = True + self.user.save() + resp = self.client.post(self.url, data={}) + self.assertSuccess(resp) + self.assertEqual(resp.data["data"]["appkey"], User.objects.get(username=self.user.username).open_api_appkey) diff --git a/account/urls/oj.py b/account/urls/oj.py index a92dd5b..1b26e14 100644 --- a/account/urls/oj.py +++ b/account/urls/oj.py @@ -5,7 +5,7 @@ from ..views.oj import (ApplyResetPasswordAPI, ResetPasswordAPI, UserLoginAPI, UserLogoutAPI, UsernameOrEmailCheck, AvatarUploadAPI, TwoFactorAuthAPI, UserProfileAPI, UserRankAPI, CheckTFARequiredAPI, SessionManagementAPI, - ProfileProblemDisplayIDRefreshAPI) + ProfileProblemDisplayIDRefreshAPI, OpenAPIAppkeyAPI) from utils.captcha.views import CaptchaAPIView @@ -25,5 +25,6 @@ urlpatterns = [ url(r"^tfa_required/?$", CheckTFARequiredAPI.as_view(), name="tfa_required_check"), url(r"^two_factor_auth/?$", TwoFactorAuthAPI.as_view(), name="two_factor_auth_api"), url(r"^user_rank/?$", UserRankAPI.as_view(), name="user_rank_api"), - url(r"^sessions/?$", SessionManagementAPI.as_view(), name="session_management_api") + url(r"^sessions/?$", SessionManagementAPI.as_view(), name="session_management_api"), + url(r"^open_api_appkey/?$", OpenAPIAppkeyAPI.as_view(), name="open_api_appkey_api"), ] diff --git a/account/views/oj.py b/account/views/oj.py index 344a419..5f25817 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -401,3 +401,15 @@ class ProfileProblemDisplayIDRefreshAPI(APIView): v["_id"] = id_map[k] profile.save(update_fields=["acm_problems_status", "oi_problems_status"]) return self.success() + + +class OpenAPIAppkeyAPI(APIView): + @login_required + def post(self, request): + user = request.user + if not user.open_api: + return self.error("Permission denied") + api_appkey = rand_str() + user.open_api_appkey = api_appkey + user.save() + return self.success({"appkey": api_appkey}) diff --git a/oj/settings.py b/oj/settings.py index d1ab211..43aa1d6 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -49,6 +49,7 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'account.middleware.APITokenAuthMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', From 1bf497364881523d4f66a7e5d6f3b0d9a89ec028 Mon Sep 17 00:00:00 2001 From: virusdefender Date: Sun, 26 Nov 2017 10:40:56 +0800 Subject: [PATCH 102/106] fix deploy issues - make supervisorctl happy - fix avatar path - refine logging config - add nginx buffer path --- deploy/nginx/common.conf | 2 +- deploy/nginx/nginx.conf | 1 + deploy/run.sh | 2 +- deploy/{supervisor.conf => supervisord.conf} | 24 +++--- oj/settings.py | 78 ++++++++------------ 5 files changed, 50 insertions(+), 57 deletions(-) rename deploy/{supervisor.conf => supervisord.conf} (60%) diff --git a/deploy/nginx/common.conf b/deploy/nginx/common.conf index 478e288..ecfd251 100644 --- a/deploy/nginx/common.conf +++ b/deploy/nginx/common.conf @@ -1,5 +1,5 @@ location /public { - root /app/data; + root /data; } location /api { diff --git a/deploy/nginx/nginx.conf b/deploy/nginx/nginx.conf index 54eb7ed..0942890 100644 --- a/deploy/nginx/nginx.conf +++ b/deploy/nginx/nginx.conf @@ -20,6 +20,7 @@ http { gzip on; gzip_vary on; gzip_types application/javascript text/css; + client_body_temp_path /tmp 1 2; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' diff --git a/deploy/run.sh b/deploy/run.sh index bcf7476..91ac2e4 100644 --- a/deploy/run.sh +++ b/deploy/run.sh @@ -29,4 +29,4 @@ do done chown -R nobody:nogroup $DATA $APP/dist -exec supervisord -c /app/deploy/supervisor.conf +exec supervisord -c /app/deploy/supervisord.conf diff --git a/deploy/supervisor.conf b/deploy/supervisord.conf similarity index 60% rename from deploy/supervisor.conf rename to deploy/supervisord.conf index c9e5032..6b23166 100644 --- a/deploy/supervisor.conf +++ b/deploy/supervisord.conf @@ -1,20 +1,26 @@ [supervisord] -logfile=/app/data/log/supervisord.log +logfile=/data/log/supervisord.log logfile_maxbytes=10MB logfile_backups=10 loglevel=info pidfile=/tmp/supervisord.pid nodaemon=true -childlogdir=/app/data/log/ +childlogdir=/data/log/ + +[inet_http_server] +port=127.0.0.1:9005 + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface [supervisorctl] -serverurl=unix:///tmp/supervisor.sock +serverurl=http://127.0.0.1:9005 [program:nginx] command=nginx -c /app/deploy/nginx/nginx.conf directory=/app/ -stdout_logfile=/app/data/log/nginx.log -stderr_logfile=/app/data/log/nginx.log +stdout_logfile=/data/log/nginx.log +stderr_logfile=/data/log/nginx.log autostart=true autorestart=true startsecs=5 @@ -25,8 +31,8 @@ killasgroup=true command=sh -c "gunicorn oj.wsgi --user nobody -b 127.0.0.1:8080 --reload -w `grep -c ^processor /proc/cpuinfo`" directory=/app/ user=nobody -stdout_logfile=/app/data/log/gunicorn.log -stderr_logfile=/app/data/log/gunicorn.log +stdout_logfile=/data/log/gunicorn.log +stderr_logfile=/data/log/gunicorn.log autostart=true autorestart=true startsecs=5 @@ -37,8 +43,8 @@ killasgroup=true command=celery -A oj worker -l warning directory=/app/ user=nobody -stdout_logfile=/app/data/log/celery.log -stderr_logfile=/app/data/log/celery.log +stdout_logfile=/data/log/celery.log +stderr_logfile=/data/log/celery.log autostart=true autorestart=true startsecs=5 diff --git a/oj/settings.py b/oj/settings.py index 43aa1d6..40335a1 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -125,54 +125,40 @@ UPLOAD_DIR = f"{DATA_DIR}{UPLOAD_PREFIX}" STATICFILES_DIRS = [os.path.join(DATA_DIR, "public")] - LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'standard': { - 'format': '%(asctime)s [%(threadName)s:%(thread)d] [%(name)s:%(lineno)d] [%(module)s:%(funcName)s] [%(levelname)s]- %(message)s', - 'datefmt': '%Y-%m-%d %H:%M:%S' - } - }, - 'handlers': { - 'django_error': { - 'level': 'WARNING', - 'class': 'logging.handlers.RotatingFileHandler', - 'filename': os.path.join(LOG_PATH, 'django.log'), - 'formatter': 'standard' - }, - 'app_info': { - 'level': 'INFO', - 'class': 'logging.handlers.RotatingFileHandler', - 'filename': os.path.join(LOG_PATH, 'app_info.log'), - 'formatter': 'standard' - }, - 'console': { - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'formatter': 'standard' - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['django_error', 'console'], - 'level': 'WARNING', - 'propagate': True, - }, - 'django.db.backends': { - 'handlers': ['django_error', 'console'], - 'level': 'WARNING', - 'propagate': True, - }, - }, + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'standard': { + 'format': '[%(asctime)s] - [%(levelname)s] - [%(name)s:%(lineno)d] - %(message)s', + 'datefmt': '%Y-%m-%d %H:%M:%S' + } + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'standard' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['console'], + 'level': 'ERROR', + 'propagate': True, + }, + 'django.db.backends': { + 'handlers': ['console'], + 'level': 'ERROR', + 'propagate': True, + }, + '': { + 'handlers': ['console'], + 'level': 'WARNING', + 'propagate': True, + } + }, } -app_logger = { - 'handlers': ['app_info', 'console'], - 'level': 'DEBUG', - 'propagate': False -} -LOGGING["loggers"].update({app: deepcopy(app_logger) for app in LOCAL_APPS}) REST_FRAMEWORK = { 'TEST_REQUEST_DEFAULT_FORMAT': 'json', From 324535474eb2997f43230b429bfa180933e75975 Mon Sep 17 00:00:00 2001 From: virusdefender Date: Sun, 26 Nov 2017 13:30:07 +0800 Subject: [PATCH 103/106] fix template mark --- judge/languages.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/judge/languages.py b/judge/languages.py index cc5f09b..e7f35ff 100644 --- a/judge/languages.py +++ b/judge/languages.py @@ -1,7 +1,7 @@ _c_lang_config = { - "template": """//PREPEND START + "template": """//PREPEND BEGIN #include //PREPEND END @@ -12,7 +12,7 @@ int add(int a, int b) { } //TEMPLATE END -//APPEND START +//APPEND BEGIN int main() { printf("%d", add(1, 2)); return 0; @@ -48,12 +48,23 @@ _c_lang_spj_config = { } _cpp_lang_config = { - "template": """/*--PREPEND START--*/ -/*--PREPEND END--*/ -/*--TEMPLATE BEGIN--*/ -/*--TEMPLATE END--*/ -/*--APPEND START--*/ -/*--APPEND END--*/""", + "template": """//PREPEND BEGIN +#include +//PREPEND END + +//TEMPLATE BEGIN +int add(int a, int b) { + // Please fill this blank + return ___________; +} +//TEMPLATE END + +//APPEND BEGIN +int main() { + std::cout << add(1, 2); + return 0; +} +//APPEND END""", "compile": { "src_name": "main.cpp", "exe_name": "main", From 945ea5e4e04b121f710b5c62b3d912386892e95c Mon Sep 17 00:00:00 2001 From: virusdefender Date: Sun, 26 Nov 2017 13:42:38 +0800 Subject: [PATCH 104/106] parse problem code template --- judge/dispatcher.py | 13 +++++++-- problem/serializers.py | 63 +++++++++++++++++++++++++++++++++++++++++- problem/tests.py | 46 +++++++++++++++++++++++++++++- problem/utils.py | 10 +++++++ 4 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 problem/utils.py diff --git a/judge/dispatcher.py b/judge/dispatcher.py index 7c04029..0f5e3cb 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -14,6 +14,7 @@ from contest.models import ContestRuleType, ACMContestRank, OIContestRank, Conte from judge.languages import languages, spj_languages from options.options import SysOptions from problem.models import Problem, ProblemRuleType +from problem.utils import parse_problem_template from submission.models import JudgeStatus, Submission from utils.cache import cache from utils.constants import CacheKey @@ -123,16 +124,24 @@ class JudgeDispatcher(DispatcherBase): cache.lpush(CacheKey.waiting_queue, json.dumps(data)) return - sub_config = list(filter(lambda item: self.submission.language == item["name"], languages))[0] + language = self.submission.language + sub_config = list(filter(lambda item: language == item["name"], languages))[0] spj_config = {} if self.problem.spj_code: for lang in spj_languages: if lang["name"] == self.problem.spj_language: spj_config = lang["spj"] break + + if language in self.problem.template: + template = parse_problem_template(self.problem.template[language]) + code = f"{template['prepend']}\n{self.submission.code}\n{template['append']}" + else: + code = self.submission.code + data = { "language_config": sub_config["config"], - "src": self.submission.code, + "src": code, "max_cpu_time": self.problem.time_limit, "max_memory": 1024 * 1024 * self.problem.memory_limit, "test_case_id": self.problem.test_case_id, diff --git a/problem/serializers.py b/problem/serializers.py index 57a034c..46b2e22 100644 --- a/problem/serializers.py +++ b/problem/serializers.py @@ -4,6 +4,7 @@ from judge.languages import language_names, spj_language_names from utils.api import DateTimeTZField, UsernameSerializer, serializers from .models import Problem, ProblemRuleType, ProblemTag +from .utils import parse_problem_template class TestCaseUploadForm(forms.Form): @@ -110,9 +111,18 @@ class ContestProblemAdminSerializer(BaseProblemSerializer): class ProblemSerializer(BaseProblemSerializer): + template = serializers.SerializerMethodField() + + def get_template(self, obj): + ret = {} + for lang, code in obj.template.items(): + ret[lang] = parse_problem_template(code)["template"] + return ret + class Meta: model = Problem - exclude = ("contest", "test_case_score", "test_case_id", "visible", "is_public") + exclude = ("contest", "test_case_score", "test_case_id", "visible", "is_public", + "template", "spj_code", "spj_version", "spj_compile_ok") class ContestProblemSerializer(BaseProblemSerializer): @@ -131,3 +141,54 @@ class ContestProblemSafeSerializer(BaseProblemSerializer): class ContestProblemMakePublicSerializer(serializers.Serializer): id = serializers.IntegerField() display_id = serializers.CharField(max_length=32) + + +class ExportProblemSerializer(serializers.ModelSerializer): + description = serializers.SerializerMethodField() + input_description = serializers.SerializerMethodField() + output_description = serializers.SerializerMethodField() + test_case_score = serializers.SerializerMethodField() + hint = serializers.SerializerMethodField() + time_limit = serializers.SerializerMethodField() + memory_limit = serializers.SerializerMethodField() + spj = serializers.SerializerMethodField() + template = serializers.SerializerMethodField() + + def get_description(self, obj): + return {"format": "html", "value": obj.description} + + def get_input_description(self, obj): + return {"format": "html", "value": obj.input_description} + + def get_output_description(self, obj): + return {"format": "html", "value": obj.output_description} + + def get_hint(self, obj): + return {"format": "html", "value": obj.hint} + + def get_test_case_score(self, obj): + return obj.test_case_score if obj.rule_type == ProblemRuleType.OI else [] + + def get_time_limit(self, obj): + return {"unit": "ms", "value": obj.time_limit} + + def get_memory_limit(self, obj): + return {"unit": "MB", "value": obj.memory_limit} + + def get_spj(self, obj): + return {"enabled": obj.spj, + "code": obj.spj_code if obj.spj else None, + "language": obj.spj_language if obj.spj else None} + + def get_template(self, obj): + ret = {} + for k, v in obj.template.items(): + ret[k] = parse_problem_template(v) + return ret + + class Meta: + model = Problem + fields = ("_id", "title", "description", + "input_description", "output_description", + "test_case_score", "hint", "time_limit", "memory_limit", "samples", + "template", "spj", "rule_type", "source", "template") diff --git a/problem/tests.py b/problem/tests.py index dbd01b7..93cd058 100644 --- a/problem/tests.py +++ b/problem/tests.py @@ -11,10 +11,13 @@ from utils.api.tests import APITestCase from .models import ProblemTag from .models import Problem, ProblemRuleType -from .views.admin import TestCaseAPI from contest.models import Contest from contest.tests import DEFAULT_CONTEST_DATA +from .views.admin import TestCaseAPI +from .utils import parse_problem_template + + DEFAULT_PROBLEM_DATA = {"_id": "A-110", "title": "test", "description": "

test

", "input_description": "test", "output_description": "test", "time_limit": 1000, "memory_limit": 256, "difficulty": "Low", "visible": True, "tags": ["test"], "languages": ["C", "C++", "Java", "Python2"], "template": {}, @@ -257,3 +260,44 @@ class ContestProblemTest(ProblemCreateTestBase): contest.save() resp = self.client.get(self.url + "?contest_id=" + str(self.contest["id"])) self.assertSuccess(resp) + + +class ParseProblemTemplateTest(APITestCase): + def test_parse(self): + template_str = """ +//PREPEND BEGIN +aaa +//PREPEND END + +//TEMPLATE BEGIN +bbb +//TEMPLATE END + +//APPEND BEGIN +ccc +//APPEND END +""" + + ret = parse_problem_template(template_str) + self.assertEqual(ret["prepend"], "aaa\n") + self.assertEqual(ret["template"], "bbb\n") + self.assertEqual(ret["append"], "ccc\n") + + def test_parse1(self): + template_str = """ +//PREPEND BEGIN +aaa +//PREPEND END + +//APPEND BEGIN +ccc +//APPEND END +//APPEND BEGIN +ddd +//APPEND END +""" + + ret = parse_problem_template(template_str) + self.assertEqual(ret["prepend"], "aaa\n") + self.assertEqual(ret["template"], "") + self.assertEqual(ret["append"], "ccc\n") diff --git a/problem/utils.py b/problem/utils.py new file mode 100644 index 0000000..f824309 --- /dev/null +++ b/problem/utils.py @@ -0,0 +1,10 @@ +import re + + +def parse_problem_template(template_str): + prepend = re.findall("//PREPEND BEGIN\n([\s\S]+?)//PREPEND END", template_str) + template = re.findall("//TEMPLATE BEGIN\n([\s\S]+?)//TEMPLATE END", template_str) + append = re.findall("//APPEND BEGIN\n([\s\S]+?)//APPEND END", template_str) + return {"prepend": prepend[0] if prepend else "", + "template": template[0] if template else "", + "append": append[0] if append else ""} From 86ca138b59a6c621f12a1be757aa9e2d8b2a5d11 Mon Sep 17 00:00:00 2001 From: virusdefender Date: Sun, 26 Nov 2017 15:55:33 +0800 Subject: [PATCH 105/106] copy default avatar --- deploy/run.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deploy/run.sh b/deploy/run.sh index 91ac2e4..b710cba 100644 --- a/deploy/run.sh +++ b/deploy/run.sh @@ -7,7 +7,7 @@ if [ ! -f "$APP/oj/custom_settings.py" ]; then echo SECRET_KEY=\"$(cat /dev/urandom | head -1 | md5sum | head -c 32)\" >> $APP/oj/custom_settings.py fi -mkdir -p $DATA/log $DATA/ssl $DATA/test_case $DATA/public/upload +mkdir -p $DATA/log $DATA/ssl $DATA/test_case $DATA/public/upload $DATA/public/avatar SSL="$DATA/ssl" if [ ! -f "$SSL/server.key" ]; then @@ -28,5 +28,6 @@ do sleep 8 done +cp data/public/avatar/default.png /data/public/avatar chown -R nobody:nogroup $DATA $APP/dist exec supervisord -c /app/deploy/supervisord.conf From 5cac51007c33876aa19d134577cad1afb13d73a1 Mon Sep 17 00:00:00 2001 From: zema1 Date: Tue, 28 Nov 2017 16:20:29 +0800 Subject: [PATCH 106/106] =?UTF-8?q?=E5=AE=8C=E5=96=84contest=E5=92=8Cannou?= =?UTF-8?q?ncement=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- account/decorators.py | 2 +- account/migrations/0009_auto_20171125_1514.py | 20 +++++++++++ account/views/admin.py | 5 ++- announcement/tests.py | 11 ++++++ announcement/urls/oj.py | 2 +- contest/tests.py | 36 +++++++++++++------ 6 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 account/migrations/0009_auto_20171125_1514.py diff --git a/account/decorators.py b/account/decorators.py index 9371059..4cbb15a 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -98,7 +98,7 @@ def check_contest_permission(check_type="details"): if self.contest.status == ContestStatus.CONTEST_NOT_START and check_type != "details": return self.error("Contest has not started yet.") - # check does user have permission to get ranks, submissions OI Contest + # check does user have permission to get ranks, submissions in OI Contest if self.contest.status == ContestStatus.CONTEST_UNDERWAY and self.contest.rule_type == ContestRuleType.OI: if not self.contest.real_time_rank and (check_type == "ranks" or check_type == "submissions"): return self.error(f"No permission to get {check_type}") diff --git a/account/migrations/0009_auto_20171125_1514.py b/account/migrations/0009_auto_20171125_1514.py new file mode 100644 index 0000000..b476b78 --- /dev/null +++ b/account/migrations/0009_auto_20171125_1514.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-11-25 15:14 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0008_auto_20171011_1214'), + ] + + operations = [ + migrations.AlterField( + model_name='userprofile', + name='avatar', + field=models.CharField(default='/public/avatar/default.png', max_length=256), + ), + ] diff --git a/account/views/admin.py b/account/views/admin.py index b1ef688..171fa37 100644 --- a/account/views/admin.py +++ b/account/views/admin.py @@ -22,7 +22,7 @@ class UserAdminAPI(APIView): @super_admin_required def post(self, request): """ - Generate user + Import User """ data = request.data["users"] @@ -166,6 +166,9 @@ class GenerateUserAPI(APIView): @validate_serializer(GenerateUserSerializer) @super_admin_required def post(self, request): + """ + Generate User + """ data = request.data number_max_length = max(len(str(data["number_from"])), len(str(data["number_to"]))) if number_max_length + len(data["prefix"]) + len(data["suffix"]) > 32: diff --git a/announcement/tests.py b/announcement/tests.py index dd702ba..98caa1c 100644 --- a/announcement/tests.py +++ b/announcement/tests.py @@ -35,3 +35,14 @@ class AnnouncementAdminTest(APITestCase): resp = self.client.delete(self.url + "?id=" + str(id)) self.assertSuccess(resp) self.assertFalse(Announcement.objects.filter(id=id).exists()) + + +class AnnouncementAPITest(APITestCase): + def setUp(self): + self.user = self.create_super_admin() + Announcement.objects.create(title="title", content="content", visible=True, created_by=self.user) + self.url = self.reverse("announcement_api") + + def test_get_announcement_list(self): + resp = self.client.get(self.url) + self.assertSuccess(resp) diff --git a/announcement/urls/oj.py b/announcement/urls/oj.py index 71bcf3a..67178b0 100644 --- a/announcement/urls/oj.py +++ b/announcement/urls/oj.py @@ -3,5 +3,5 @@ from django.conf.urls import url from ..views.oj import AnnouncementAPI urlpatterns = [ - url(r"^announcement/?$", AnnouncementAPI.as_view(), name="announcement_admin_api"), + url(r"^announcement/?$", AnnouncementAPI.as_view(), name="announcement_api"), ] diff --git a/contest/tests.py b/contest/tests.py index 3b7b1e2..965ee8d 100644 --- a/contest/tests.py +++ b/contest/tests.py @@ -6,7 +6,7 @@ from django.utils import timezone from utils.api._serializers import DateTimeTZField from utils.api.tests import APITestCase -from .models import ContestAnnouncement, ContestRuleType +from .models import ContestAnnouncement, ContestRuleType, Contest DEFAULT_CONTEST_DATA = {"title": "test title", "description": "test description", "start_time": timezone.localtime(timezone.now()), @@ -21,13 +21,18 @@ class ContestAdminAPITest(APITestCase): def setUp(self): self.create_super_admin() self.url = self.reverse("contest_admin_api") - self.data = DEFAULT_CONTEST_DATA + self.data = copy.deepcopy(DEFAULT_CONTEST_DATA) def test_create_contest(self): response = self.client.post(self.url, data=self.data) self.assertSuccess(response) return response + def test_create_contest_with_invalid_cidr(self): + self.data["allowed_ip_ranges"] = ["127.0.0"] + resp = self.client.post(self.url, data=self.data) + self.assertTrue(resp.data["data"].endswith("is not a valid cidr network")) + def test_update_contest(self): id = self.test_create_contest().data["data"]["id"] update_data = {"id": id, "title": "update title", @@ -58,10 +63,9 @@ class ContestAdminAPITest(APITestCase): class ContestAPITest(APITestCase): def setUp(self): - self.create_admin() - url = self.reverse("contest_admin_api") - self.contest = self.client.post(url, data=DEFAULT_CONTEST_DATA).data["data"] - self.url = self.reverse("contest_api") + "?id=" + str(self.contest["id"]) + user = self.create_admin() + self.contest = Contest.objects.create(created_by=user, **DEFAULT_CONTEST_DATA) + self.url = self.reverse("contest_api") + "?id=" + str(self.contest.id) def test_get_contest_list(self): url = self.reverse("contest_list_api") @@ -76,21 +80,21 @@ class ContestAPITest(APITestCase): def test_regular_user_validate_contest_password(self): self.create_user("test", "test123") url = self.reverse("contest_password_api") - resp = self.client.post(url, {"contest_id": self.contest["id"], "password": "error_password"}) + resp = self.client.post(url, {"contest_id": self.contest.id, "password": "error_password"}) self.assertDictEqual(resp.data, {"error": "error", "data": "Wrong password"}) - resp = self.client.post(url, {"contest_id": self.contest["id"], "password": DEFAULT_CONTEST_DATA["password"]}) + resp = self.client.post(url, {"contest_id": self.contest.id, "password": DEFAULT_CONTEST_DATA["password"]}) self.assertSuccess(resp) def test_regular_user_access_contest(self): self.create_user("test", "test123") url = self.reverse("contest_access_api") - resp = self.client.get(url + "?contest_id=" + str(self.contest["id"])) + resp = self.client.get(url + "?contest_id=" + str(self.contest.id)) self.assertFalse(resp.data["data"]["access"]) password_url = self.reverse("contest_password_api") resp = self.client.post(password_url, - {"contest_id": self.contest["id"], "password": DEFAULT_CONTEST_DATA["password"]}) + {"contest_id": self.contest.id, "password": DEFAULT_CONTEST_DATA["password"]}) self.assertSuccess(resp) resp = self.client.get(self.url) self.assertSuccess(resp) @@ -146,3 +150,15 @@ class ContestAnnouncementListAPITest(APITestCase): contest_id = self.create_contest_announcements() response = self.client.get(self.url, data={"contest_id": contest_id}) self.assertSuccess(response) + + +class ContestRankAPITest(APITestCase): + def setUp(self): + user = self.create_admin() + self.acm_contest = Contest.objects.create(created_by=user, **DEFAULT_CONTEST_DATA) + self.create_user("test", "test123") + self.url = self.reverse("contest_rank_api") + + def get_contest_rank(self): + resp = self.client.get(self.url + "?contest_id=" + self.acm_contest.id) + self.assertSuccess(resp)