diff --git a/.travis.yml b/.travis.yml
index 51d41d6..f51b0b5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -12,3 +12,5 @@ script:
- flake8 .
- coverage run --include="$PWD/*" manage.py test
- coverage report
+notifications:
+ slack: onlinejudgeteam:BzBz8UFgmS5crpiblof17K2W
diff --git a/account/decorators.py b/account/decorators.py
index c11560d..9df97e5 100644
--- a/account/decorators.py
+++ b/account/decorators.py
@@ -1,7 +1,5 @@
import functools
-from django.utils.translation import ugettext as _
-
from utils.api import JSONResponse
from .models import ProblemPermission
@@ -22,10 +20,10 @@ class BasePermissionDecorator(object):
if self.check_permission():
if self.request.user.is_disabled:
- return self.error(_("Your account is disabled"))
+ 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 in first")
def check_permission(self):
raise NotImplementedError()
diff --git a/account/serializers.py b/account/serializers.py
index fe128f6..112d59d 100644
--- a/account/serializers.py
+++ b/account/serializers.py
@@ -44,3 +44,23 @@ class EditUserSerializer(serializers.Serializer):
open_api = serializers.BooleanField()
two_factor_auth = serializers.BooleanField()
is_disabled = serializers.BooleanField()
+
+
+class ApplyResetPasswordSerializer(serializers.Serializer):
+ email = serializers.EmailField()
+ captcha = serializers.CharField(max_length=4, min_length=4)
+
+
+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)
+
+
+class SSOSerializer(serializers.Serializer):
+ appkey = serializers.CharField(max_length=35)
+ token = serializers.CharField(max_length=40)
+
+
+class TwoFactorAuthCodeSerializer(serializers.Serializer):
+ code = serializers.IntegerField()
diff --git a/account/tasks.py b/account/tasks.py
new file mode 100644
index 0000000..0aacec9
--- /dev/null
+++ b/account/tasks.py
@@ -0,0 +1,8 @@
+from celery import shared_task
+
+from utils.shortcuts import send_email
+
+
+@shared_task
+def send_email_async(from_name, to_email, to_name, subject, content):
+ send_email(from_name, to_email, to_name, subject, content)
diff --git a/account/templates/reset_password_email.html b/account/templates/reset_password_email.html
new file mode 100644
index 0000000..5a0b591
--- /dev/null
+++ b/account/templates/reset_password_email.html
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+ |
+ {{ website_name }} 登录信息找回
+ |
+
+
+
+
+
+
+ |
+
+
+
+ |
+ Hello, {{ username }}:
+ |
+
+
+
+ |
+
+
+ |
+ 您刚刚在 {{ website_name }} 申请了找回登录信息服务。
+ |
+
+
+ |
+ 请在30分钟内点击下面链接设置您的新密码:
+ |
+
+
+ |
+ 重置密码
+ |
+
+
+ |
+
+
+ |
+ 如果上面的链接点击无效,请复制以下链接至浏览器的地址栏直接打开。
+ |
+
+
+ |
+
+ {{ link }}
+
+ |
+
+
+ |
+ 如果您没有提出过该申请,请忽略此邮件。有可能是其他用户误填了您的邮件地址,我们不会对你的帐户进行任何修改。
+ 请不要向他人透露本邮件的内容,否则可能会导致您的账号被盗。
+ |
+
+
+ |
+
+
+
+ |
+
+
+
\ No newline at end of file
diff --git a/account/tests.py b/account/tests.py
index 877b317..d4addd5 100644
--- a/account/tests.py
+++ b/account/tests.py
@@ -2,7 +2,6 @@ import time
from unittest import mock
from django.contrib import auth
-from django.utils.translation import ugettext as _
from otpauth import OtpAuth
from utils.api.tests import APIClient, APITestCase
@@ -45,7 +44,7 @@ class UserLoginAPITest(APITestCase):
def test_login_with_correct_info(self):
response = self.client.post(self.login_url,
data={"username": self.username, "password": self.password})
- self.assertDictEqual(response.data, {"error": None, "data": _("Succeeded")})
+ self.assertDictEqual(response.data, {"error": None, "data": "Succeeded"})
user = auth.get_user(self.client)
self.assertTrue(user.is_authenticated())
@@ -53,7 +52,7 @@ class UserLoginAPITest(APITestCase):
def test_login_with_wrong_info(self):
response = self.client.post(self.login_url,
data={"username": self.username, "password": "invalid_password"})
- self.assertDictEqual(response.data, {"error": "error", "data": _("Invalid username or password")})
+ self.assertDictEqual(response.data, {"error": "error", "data": "Invalid username or password"})
user = auth.get_user(self.client)
self.assertFalse(user.is_authenticated())
@@ -67,7 +66,7 @@ class UserLoginAPITest(APITestCase):
data={"username": self.username,
"password": self.password,
"tfa_code": code})
- self.assertDictEqual(response.data, {"error": None, "data": _("Succeeded")})
+ self.assertDictEqual(response.data, {"error": None, "data": "Succeeded"})
user = auth.get_user(self.client)
self.assertTrue(user.is_authenticated())
@@ -78,7 +77,7 @@ class UserLoginAPITest(APITestCase):
data={"username": self.username,
"password": self.password,
"tfa_code": "qqqqqq"})
- self.assertDictEqual(response.data, {"error": "error", "data": _("Invalid two factor verification code")})
+ self.assertDictEqual(response.data, {"error": "error", "data": "Invalid two factor verification code"})
user = auth.get_user(self.client)
self.assertFalse(user.is_authenticated())
@@ -116,7 +115,7 @@ class UserRegisterAPITest(CaptchaTest):
def test_invalid_captcha(self):
self.data["captcha"] = "****"
response = self.client.post(self.register_url, data=self.data)
- self.assertDictEqual(response.data, {"error": "error", "data": _("Invalid captcha")})
+ self.assertDictEqual(response.data, {"error": "error", "data": "Invalid captcha"})
self.data.pop("captcha")
response = self.client.post(self.register_url, data=self.data)
@@ -124,7 +123,7 @@ class UserRegisterAPITest(CaptchaTest):
def test_register_with_correct_info(self):
response = self.client.post(self.register_url, data=self.data)
- self.assertDictEqual(response.data, {"error": None, "data": _("Succeeded")})
+ self.assertDictEqual(response.data, {"error": None, "data": "Succeeded"})
def test_username_already_exists(self):
self.test_register_with_correct_info()
@@ -132,7 +131,7 @@ class UserRegisterAPITest(CaptchaTest):
self.data["captcha"] = self._set_captcha(self.client.session)
self.data["email"] = "test1@qduoj.com"
response = self.client.post(self.register_url, data=self.data)
- self.assertDictEqual(response.data, {"error": "error", "data": _("Username already exists")})
+ self.assertDictEqual(response.data, {"error": "error", "data": "Username already exists"})
def test_email_already_exists(self):
self.test_register_with_correct_info()
@@ -140,7 +139,7 @@ class UserRegisterAPITest(CaptchaTest):
self.data["captcha"] = self._set_captcha(self.client.session)
self.data["username"] = "test_user1"
response = self.client.post(self.register_url, data=self.data)
- self.assertDictEqual(response.data, {"error": "error", "data": _("Email already exists")})
+ self.assertDictEqual(response.data, {"error": "error", "data": "Email already exists"})
class UserChangePasswordAPITest(CaptchaTest):
@@ -159,19 +158,19 @@ 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 in first"})
def test_valid_ola_password(self):
self.assertTrue(self.client.login(username=self.username, password=self.old_password))
response = self.client.post(self.url, data=self.data)
- self.assertEqual(response.data, {"error": None, "data": _("Succeeded")})
+ self.assertEqual(response.data, {"error": None, "data": "Succeeded"})
self.assertTrue(self.client.login(username=self.username, password=self.new_password))
def test_invalid_old_password(self):
self.assertTrue(self.client.login(username=self.username, password=self.old_password))
self.data["old_password"] = "invalid"
response = self.client.post(self.url, data=self.data)
- self.assertEqual(response.data, {"error": "error", "data": _("Invalid old password")})
+ self.assertEqual(response.data, {"error": "error", "data": "Invalid old password"})
class AdminUserTest(APITestCase):
diff --git a/account/urls/admin.py b/account/urls/admin.py
index 372ba24..b10741e 100644
--- a/account/urls/admin.py
+++ b/account/urls/admin.py
@@ -3,5 +3,5 @@ from django.conf.urls import url
from ..views.admin import UserAdminAPI
urlpatterns = [
- url(r"^user$", UserAdminAPI.as_view(), name="user_admin_api"),
+ url(r"^user/?$", UserAdminAPI.as_view(), name="user_admin_api"),
]
diff --git a/account/urls/oj.py b/account/urls/oj.py
index 6a1ef6c..fa47e33 100644
--- a/account/urls/oj.py
+++ b/account/urls/oj.py
@@ -1,9 +1,12 @@
from django.conf.urls import url
-from ..views.oj import UserChangePasswordAPI, UserLoginAPI, UserRegisterAPI
+from ..views.oj import (ApplyResetPasswordAPI, ResetPasswordAPI,
+ UserChangePasswordAPI, UserLoginAPI, UserRegisterAPI)
urlpatterns = [
- url(r"^login$", UserLoginAPI.as_view(), name="user_login_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"^login/?$", UserLoginAPI.as_view(), name="user_login_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"),
+ url(r"^reset_password/?$", ResetPasswordAPI.as_view(), name="apply_reset_password_api")
]
diff --git a/account/urls/user.py b/account/urls/user.py
new file mode 100644
index 0000000..1676ddc
--- /dev/null
+++ b/account/urls/user.py
@@ -0,0 +1,12 @@
+from django.conf.urls import url
+
+from ..views.user import (SSOAPI, AvatarUploadAPI, TwoFactorAuthAPI,
+ UserInfoAPI, UserProfileAPI)
+
+urlpatterns = [
+ url(r"^user/?$", 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"),
+ 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 49e8093..62c5115 100644
--- a/account/views/admin.py
+++ b/account/views/admin.py
@@ -1,6 +1,5 @@
from django.core.exceptions import MultipleObjectsReturned
from django.db.models import Q
-from django.utils.translation import ugettext as _
from utils.api import APIView, validate_serializer
from utils.shortcuts import rand_str
@@ -21,21 +20,21 @@ class UserAdminAPI(APIView):
try:
user = User.objects.get(id=data["id"])
except User.DoesNotExist:
- return self.error(_("User does not exist"))
+ 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"))
+ 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"))
+ return self.error("Email already exists")
# Some old data has duplicate email
except MultipleObjectsReturned:
- return self.error(_("Email already exists"))
+ return self.error("Email already exists")
except User.DoesNotExist:
pass
@@ -85,7 +84,7 @@ class UserAdminAPI(APIView):
try:
user = User.objects.get(id=user_id)
except User.DoesNotExist:
- return self.error(_("User does not exist"))
+ return self.error("User does not exist")
return self.success(UserSerializer(user).data)
user = User.objects.all().order_by("-create_time")
diff --git a/account/views/oj.py b/account/views/oj.py
index 3e48a12..7db3bbb 100644
--- a/account/views/oj.py
+++ b/account/views/oj.py
@@ -1,15 +1,23 @@
+from datetime import timedelta
+
+from django.conf import settings
from django.contrib import auth
from django.core.exceptions import MultipleObjectsReturned
-from django.utils.translation import ugettext as _
+from django.utils.timezone import now
from otpauth import OtpAuth
+from conf.models import WebsiteConfig
from utils.api import APIView, validate_serializer
from utils.captcha import Captcha
+from utils.shortcuts import rand_str
from ..decorators import login_required
from ..models import User, UserProfile
-from ..serializers import (UserChangePasswordSerializer, UserLoginSerializer,
+from ..serializers import (ApplyResetPasswordSerializer,
+ ResetPasswordSerializer,
+ UserChangePasswordSerializer, UserLoginSerializer,
UserRegisterSerializer)
+from ..tasks import send_email_async
class UserLoginAPI(APIView):
@@ -24,7 +32,7 @@ class UserLoginAPI(APIView):
if user:
if not user.two_factor_auth:
auth.login(request, user)
- return self.success(_("Succeeded"))
+ return self.success("Succeeded")
# `tfa_code` not in post data
if user.two_factor_auth and "tfa_code" not in data:
@@ -32,11 +40,11 @@ class UserLoginAPI(APIView):
if OtpAuth(user.tfa_token).valid_totp(data["tfa_code"]):
auth.login(request, user)
- return self.success(_("Succeeded"))
+ return self.success("Succeeded")
else:
- return self.error(_("Invalid two factor verification code"))
+ return self.error("Invalid two factor verification code")
else:
- return self.error(_("Invalid username or password"))
+ return self.error("Invalid username or password")
# todo remove this, only for debug use
def get(self, request):
@@ -53,24 +61,24 @@ class UserRegisterAPI(APIView):
data = request.data
captcha = Captcha(request)
if not captcha.check(data["captcha"]):
- return self.error(_("Invalid captcha"))
+ return self.error("Invalid captcha")
try:
User.objects.get(username=data["username"])
- return self.error(_("Username already exists"))
+ return self.error("Username already exists")
except User.DoesNotExist:
pass
try:
User.objects.get(email=data["email"])
- return self.error(_("Email already exists"))
+ return self.error("Email already exists")
# Some old data has duplicate email
except MultipleObjectsReturned:
- return self.error(_("Email already exists"))
+ 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"))
+ return self.success("Succeeded")
class UserChangePasswordAPI(APIView):
@@ -83,12 +91,64 @@ class UserChangePasswordAPI(APIView):
data = request.data
captcha = Captcha(request)
if not captcha.check(data["captcha"]):
- return self.error(_("Invalid captcha"))
+ return self.error("Invalid captcha")
username = request.user.username
user = auth.authenticate(username=username, password=data["old_password"])
if user:
user.set_password(data["new_password"])
user.save()
- return self.success(_("Succeeded"))
+ return self.success("Succeeded")
else:
- return self.error(_("Invalid old password"))
+ return self.error("Invalid old password")
+
+
+class ApplyResetPasswordAPI(APIView):
+ @validate_serializer(ApplyResetPasswordSerializer)
+ 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:
+ 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:
+ 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)
+ send_email_async.delay(config.name,
+ user.email,
+ user.username,
+ config.name + " 登录信息找回邮件",
+ email_template)
+ return self.success("Succeeded")
+
+
+class ResetPasswordAPI(APIView):
+ @validate_serializer(ResetPasswordSerializer)
+ def post(self, request):
+ data = request.data
+ captcha = Captcha(request)
+ if not captcha.check(data["captcha"]):
+ return self.error("Invalid captcha")
+ try:
+ 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")
+ user.reset_password_token = None
+ user.set_password(data["password"])
+ user.save()
+ return self.success("Succeeded")
diff --git a/account/views/user.py b/account/views/user.py
new file mode 100644
index 0000000..19eb893
--- /dev/null
+++ b/account/views/user.py
@@ -0,0 +1,148 @@
+import os
+from io import StringIO
+
+import qrcode
+from django.conf import settings
+from django.http import HttpResponse
+from otpauth import OtpAuth
+
+from conf.models import WebsiteConfig
+from utils.api import APIView, validate_serializer
+from utils.shortcuts import rand_str
+
+from ..decorators import login_required
+from ..models import User
+from ..serializers import (EditUserSerializer, SSOSerializer,
+ TwoFactorAuthCodeSerializer, UserSerializer)
+
+
+class UserInfoAPI(APIView):
+ @login_required
+ def get(self, request):
+ """
+ Return user info api
+ """
+ return self.success(UserSerializer(request.user).data)
+
+
+class UserProfileAPI(APIView):
+ @login_required
+ def get(self, request):
+ """
+ Return user info api
+ """
+ return self.success(UserSerializer(request.user).data)
+
+ @validate_serializer(EditUserSerializer)
+ @login_required
+ def put(self, request):
+ data = request.data
+ user_profile = request.user.userprofile
+ if data["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(APIView):
+ def post(self, request):
+ if "file" not in request.FILES:
+ return self.error("Upload failed")
+
+ f = request.FILES["file"]
+ if f.size > 1024 * 1024:
+ return self.error("Picture too large")
+ if os.path.splitext(f.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]
+ with open(os.path.join(settings.IMAGE_UPLOAD_DIR, name), "wb") as img:
+ for chunk in request.FILES["file"]:
+ img.write(chunk)
+ 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 = StringIO()
+ 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/announcement/urls/admin.py b/announcement/urls/admin.py
index fefc160..6b9ce0f 100644
--- a/announcement/urls/admin.py
+++ b/announcement/urls/admin.py
@@ -3,5 +3,5 @@ from django.conf.urls import url
from ..views import AnnouncementAdminAPI
urlpatterns = [
- url(r"^announcement$", AnnouncementAdminAPI.as_view(), name="announcement_admin_api"),
+ url(r"^announcement/?$", AnnouncementAdminAPI.as_view(), name="announcement_admin_api"),
]
diff --git a/announcement/views.py b/announcement/views.py
index 8cbe78e..f607a7d 100644
--- a/announcement/views.py
+++ b/announcement/views.py
@@ -1,5 +1,3 @@
-from django.utils.translation import ugettext as _
-
from account.decorators import super_admin_required
from utils.api import APIView, validate_serializer
@@ -32,7 +30,7 @@ class AnnouncementAdminAPI(APIView):
try:
announcement = Announcement.objects.get(id=data["id"])
except Announcement.DoesNotExist:
- return self.error(_("Announcement does not exist"))
+ return self.error("Announcement does not exist")
announcement.title = data["title"]
announcement.content = data["content"]
@@ -52,7 +50,7 @@ class AnnouncementAdminAPI(APIView):
announcement = Announcement.objects.get(id=announcement_id)
return self.success(AnnouncementSerializer(announcement).data)
except Announcement.DoesNotExist:
- return self.error(_("Announcement does not exist"))
+ return self.error("Announcement does not exist")
announcement = Announcement.objects.all().order_by("-create_time")
if request.GET.get("visible") == "true":
announcement = announcement.filter(visible=True)
diff --git a/conf/urls/admin.py b/conf/urls/admin.py
index dcb5c6f..be24272 100644
--- a/conf/urls/admin.py
+++ b/conf/urls/admin.py
@@ -3,7 +3,7 @@ from django.conf.urls import url
from ..views import SMTPAPI, JudgeServerAPI, WebsiteConfigAPI
urlpatterns = [
- url(r"^smtp$", SMTPAPI.as_view(), name="smtp_admin_api"),
- url(r"^website$", WebsiteConfigAPI.as_view(), name="website_config_api"),
- url(r"^judge_server", JudgeServerAPI.as_view(), name="judge_server_api")
+ url(r"^smtp/?$", SMTPAPI.as_view(), name="smtp_admin_api"),
+ url(r"^website/?$", WebsiteConfigAPI.as_view(), name="website_config_api"),
+ url(r"^judge_server/?$", JudgeServerAPI.as_view(), name="judge_server_api")
]
diff --git a/conf/urls/oj.py b/conf/urls/oj.py
index b5a06e5..3e6d757 100644
--- a/conf/urls/oj.py
+++ b/conf/urls/oj.py
@@ -3,7 +3,7 @@ from django.conf.urls import url
from ..views import JudgeServerHeartbeatAPI, LanguagesAPI, WebsiteConfigAPI
urlpatterns = [
- url(r"^website$", WebsiteConfigAPI.as_view(), name="website_info_api"),
- url(r"^judge_server_heartbeat$", JudgeServerHeartbeatAPI.as_view(), name="judge_server_heartbeat_api"),
- url(r"^languages$", LanguagesAPI.as_view(), name="language_list_api")
+ url(r"^website/?$", WebsiteConfigAPI.as_view(), name="website_info_api"),
+ url(r"^judge_server_heartbeat/?$", JudgeServerHeartbeatAPI.as_view(), name="judge_server_heartbeat_api"),
+ url(r"^languages/?$", LanguagesAPI.as_view(), name="language_list_api")
]
diff --git a/contest/tests.py b/contest/tests.py
index e69de29..8a8134f 100644
--- a/contest/tests.py
+++ b/contest/tests.py
@@ -0,0 +1,107 @@
+import copy
+from datetime import datetime, timedelta
+
+from django.utils import timezone
+
+from utils.api._serializers import DateTimeTZField
+from utils.api.tests import APITestCase
+
+from .models import ContestAnnouncement, ContestRuleType
+
+DEFAULT_CONTEST_DATA = {"title": "test title", "description": "test description",
+ "start_time": timezone.localtime(timezone.now()),
+ "end_time": timezone.localtime(timezone.now()) + timedelta(days=1),
+ "rule_type": ContestRuleType.ACM,
+ "password": "123",
+ "visible": True, "real_time_rank": True}
+
+
+class ContestAPITest(APITestCase):
+ def setUp(self):
+ self.create_super_admin()
+ self.url = self.reverse("contest_api")
+ self.data = DEFAULT_CONTEST_DATA
+
+ def test_create_contest(self):
+ response = self.client.post(self.url, data=self.data)
+ self.assertSuccess(response)
+ return response
+
+ def test_update_contest(self):
+ id = self.test_create_contest().data["data"]["id"]
+ update_data = {"id": id, "title": "update title",
+ "description": "update description",
+ "password": "12345",
+ "visible": False, "real_time_rank": False}
+ data = copy.deepcopy(self.data)
+ data.update(update_data)
+ response = self.client.put(self.url, data=data)
+ self.assertSuccess(response)
+ response_data = response.data["data"]
+ datetime_tz_field = DateTimeTZField()
+ for k in data.keys():
+ if isinstance(data[k], datetime):
+ data[k] = datetime_tz_field.to_representation(data[k])
+ self.assertEqual(response_data[k], data[k])
+
+ def test_get_contests(self):
+ self.test_create_contest()
+ response = self.client.get(self.url)
+ self.assertSuccess(response)
+
+ def test_get_one_contest(self):
+ id = self.test_create_contest().data["data"]["id"]
+ response = self.client.get("{}?id={}".format(self.url, id))
+ self.assertSuccess(response)
+
+
+class ContestAnnouncementAPITest(APITestCase):
+ def setUp(self):
+ 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}
+
+ def create_contest(self):
+ url = self.reverse("contest_api")
+ data = DEFAULT_CONTEST_DATA
+ return self.client.post(url, data=data)
+
+ def test_create_contest_announcement(self):
+ response = self.client.post(self.url, data=self.data)
+ self.assertSuccess(response)
+ return response
+
+ def test_delete_contest_announcement(self):
+ id = self.test_create_contest_announcement().data["data"]["id"]
+ response = self.client.delete("{}?id={}".format(self.url, id))
+ self.assertSuccess(response)
+ self.assertFalse(ContestAnnouncement.objects.filter(id=id).exists())
+
+ def test_get_contest_announcements(self):
+ self.test_create_contest_announcement()
+ response = self.client.get(self.url)
+ self.assertSuccess(response)
+
+ def test_get_one_contest_announcement(self):
+ id = self.test_create_contest_announcement().data["data"]["id"]
+ response = self.client.get("{}?id={}".format(self.url, id))
+ self.assertSuccess(response)
+
+
+class ContestAnnouncementListAPITest(APITestCase):
+ def setUp(self):
+ self.create_super_admin()
+ self.url = self.reverse("contest_list_api")
+
+ def create_contest_announcements(self):
+ contest_id = self.client.post(self.reverse("contest_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})
+ return contest_id
+
+ def test_get_contest_announcement_list(self):
+ contest_id = self.create_contest_announcements()
+ response = self.client.get(self.url, data={"contest_id": contest_id})
+ self.assertSuccess(response)
diff --git a/contest/urls/admin.py b/contest/urls/admin.py
index d9ea9c9..2a7705a 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/announcement$", ContestAnnouncementAPI.as_view(), name="contest_announcement_admin_api")
+ url(r"^contest/?$", ContestAPI.as_view(), name="contest_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 e2e48ca..bfc80b8 100644
--- a/contest/urls/oj.py
+++ b/contest/urls/oj.py
@@ -3,5 +3,5 @@ from django.conf.urls import url
from ..views.oj import ContestAnnouncementListAPI
urlpatterns = [
- url(r"^contest$", ContestAnnouncementListAPI.as_view(), name="contest_list_api"),
+ url(r"^contest/?$", ContestAnnouncementListAPI.as_view(), name="contest_list_api"),
]
diff --git a/contest/views/admin.py b/contest/views/admin.py
index 15007ad..60bb161 100644
--- a/contest/views/admin.py
+++ b/contest/views/admin.py
@@ -20,8 +20,8 @@ class ContestAPI(APIView):
return self.error("Start time must occur earlier than end time")
if not data["password"]:
data["password"] = None
- Contest.objects.create(**data)
- return self.success()
+ contest = Contest.objects.create(**data)
+ return self.success(ContestSerializer(contest).data)
@validate_serializer(EditConetestSeriaizer)
def put(self, request):
@@ -90,7 +90,8 @@ class ContestAnnouncementAPI(APIView):
contest_announcement_id = request.GET.get("id")
if contest_announcement_id:
if request.user.is_admin():
- ContestAnnouncement.objects.filter(id=contest_announcement_id, contest__created_by=request.user).delete()
+ ContestAnnouncement.objects.filter(id=contest_announcement_id,
+ contest__created_by=request.user).delete()
else:
ContestAnnouncement.objects.filter(id=contest_announcement_id).delete()
return self.success()
diff --git a/deploy/requirements.txt b/deploy/requirements.txt
index ae2a33c..3d8bf5a 100644
--- a/deploy/requirements.txt
+++ b/deploy/requirements.txt
@@ -1,5 +1,5 @@
-Django<1.10
-djangorestframework==3.3.3
+django==1.9.6
+djangorestframework==3.4.0
pillow
jsonfield
otpauth
@@ -7,3 +7,7 @@ flake8-quotes
pytz
coverage
python-dateutil
+celery
+Envelopes
+qrcode
+flake8-coding
diff --git a/fps/parser.py b/fps/parser.py
index 44b37da..bca71b7 100644
--- a/fps/parser.py
+++ b/fps/parser.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
-import copy
import base64
+import copy
import random
import string
import xml.etree.ElementTree as ET
@@ -63,7 +63,7 @@ class FPSParser(object):
if not lang:
raise ValueError("Invalid " + tag + ", language name is missed")
problem[tag].append({"language": lang, "code": item.text})
- elif tag == 'spj':
+ elif tag == "spj":
lang = item.attrib.get("language")
if not lang:
raise ValueError("Invalid spj, language name if missed")
diff --git a/oj/local_settings.py b/oj/local_settings.py
index 902ca25..79dc44e 100644
--- a/oj/local_settings.py
+++ b/oj/local_settings.py
@@ -7,11 +7,6 @@ DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
- },
- # submission 的 name 和 engine 请勿修改,其他代码会用到
- 'submission': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': os.path.join(BASE_DIR, 'db1.sqlite3'),
}
}
@@ -33,3 +28,4 @@ ALLOWED_HOSTS = ["*"]
TEST_CASE_DIR = "/tmp"
+LOG_PATH = "log/"
diff --git a/oj/server_settings.py b/oj/server_settings.py
index e26eefa..7434ee7 100644
--- a/oj/server_settings.py
+++ b/oj/server_settings.py
@@ -12,15 +12,6 @@ DATABASES = {
'PORT': 3306,
'USER': os.environ["MYSQL_ENV_MYSQL_USER"],
'PASSWORD': os.environ["MYSQL_ENV_MYSQL_ROOT_PASSWORD"]
- },
- 'submission': {
- 'NAME': 'oj_submission',
- 'ENGINE': 'django.db.backends.mysql',
- '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"]
}
}
@@ -43,3 +34,4 @@ ALLOWED_HOSTS = ['*']
TEST_CASE_DIR = "/test_case"
+LOG_PATH = "log/"
diff --git a/oj/settings.py b/oj/settings.py
index 9e23a7b..e7199d9 100644
--- a/oj/settings.py
+++ b/oj/settings.py
@@ -104,8 +104,6 @@ STATIC_URL = '/static/'
AUTH_USER_MODEL = 'account.User'
-LOG_PATH = "log/"
-
LOGGING = {
'version': 1,
'disable_existing_loggers': True,
@@ -118,13 +116,13 @@ LOGGING = {
'django_error': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
- 'filename': LOG_PATH + 'django.log',
+ 'filename': os.path.join(LOG_PATH, 'django.log'),
'formatter': 'standard'
},
'app_info': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
- 'filename': LOG_PATH + 'app_info.log',
+ 'filename': os.path.join(LOG_PATH, 'app_info.log'),
'formatter': 'standard'
},
'console': {
@@ -152,17 +150,13 @@ LOGGING = {
},
}
-if DEBUG:
- REST_FRAMEWORK = {
- 'TEST_REQUEST_DEFAULT_FORMAT': 'json'
- }
-else:
- REST_FRAMEWORK = {
- 'TEST_REQUEST_DEFAULT_FORMAT': 'json',
- 'DEFAULT_RENDERER_CLASSES': (
- 'rest_framework.renderers.JSONRenderer',
- )
- }
+
+REST_FRAMEWORK = {
+ 'TEST_REQUEST_DEFAULT_FORMAT': 'json',
+ 'DEFAULT_RENDERER_CLASSES': (
+ 'rest_framework.renderers.JSONRenderer',
+ )
+}
# for celery
BROKER_URL = 'redis://%s:%s/%s' % (REDIS_QUEUE["host"], str(REDIS_QUEUE["port"]), str(REDIS_QUEUE["db"]))
diff --git a/oj/urls.py b/oj/urls.py
index b8cc62d..79e6b03 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/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/problem/tests.py b/problem/tests.py
index aa70af9..d80262a 100644
--- a/problem/tests.py
+++ b/problem/tests.py
@@ -6,6 +6,7 @@ from zipfile import ZipFile
from django.conf import settings
from utils.api.tests import APITestCase
+
from .models import ProblemTag
from .views.admin import TestCaseUploadAPI
diff --git a/problem/urls/admin.py b/problem/urls/admin.py
index a0e0b66..b4813c5 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 ProblemAPI, TestCaseUploadAPI, ContestProblemAPI
+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'contest/problem$', ContestProblemAPI.as_view(), name="contest_problem_api")
+ url(r"^test_case/upload/?$", TestCaseUploadAPI.as_view(), name="test_case_upload_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/urls/oj.py b/problem/urls/oj.py
index d99155a..a7613f1 100644
--- a/problem/urls/oj.py
+++ b/problem/urls/oj.py
@@ -3,5 +3,5 @@ from django.conf.urls import url
from ..views.oj import ProblemTagAPI
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")
]
diff --git a/problem/views/admin.py b/problem/views/admin.py
index 2fc8cf7..ad818ad 100644
--- a/problem/views/admin.py
+++ b/problem/views/admin.py
@@ -10,9 +10,10 @@ from contest.models import Contest
from utils.api import APIView, CSRFExemptAPIView, validate_serializer
from utils.shortcuts import rand_str
-from ..models import Problem, ProblemRuleType, ProblemTag, ContestProblem
-from ..serializers import (CreateProblemSerializer, EditProblemSerializer,
- ProblemSerializer, TestCaseUploadForm, CreateContestProblemSerializer)
+from ..models import ContestProblem, Problem, ProblemRuleType, ProblemTag
+from ..serializers import (CreateContestProblemSerializer,
+ CreateProblemSerializer, EditProblemSerializer,
+ ProblemSerializer, TestCaseUploadForm)
class TestCaseUploadAPI(CSRFExemptAPIView):
@@ -162,15 +163,15 @@ class ProblemAPI(APIView):
if problem_id:
try:
problem = Problem.objects.get(id=problem_id)
- if not user.can_mgmt_all_problem():
- problem = problem.get(created_by=request.user)
+ 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)
except Problem.DoesNotExist:
return self.error("Problem does not exist")
problems = Problem.objects.all().order_by("-create_time")
if not user.can_mgmt_all_problem():
- problems = problems.filter(created_by=request.user)
+ problems = problems.filter(created_by=user)
keyword = request.GET.get("keyword")
if keyword:
problems = problems.filter(title__contains=keyword)
@@ -185,8 +186,8 @@ class ProblemAPI(APIView):
try:
problem = Problem.objects.get(id=problem_id)
- if not user.can_mgmt_all_problem():
- problem = problem.get(created_by=request.user)
+ 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")
@@ -290,7 +291,7 @@ class ContestProblemAPI(APIView):
if problem_id:
try:
problem = ContestProblem.objects.get(id=problem_id)
- if request.user.is_admin() and problem.contest.created_by != user:
+ if user.is_admin() and problem.contest.created_by != user:
return self.error("Problem does not exist")
except ContestProblem.DoesNotExist:
return self.error("Problem does not exist")
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..9b0d805
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,10 @@
+django==1.9.6
+djangorestframework==3.4.0
+otpauth
+pillow
+python-dateutil
+celery
+Envelopes
+pytz
+jsonfield
+qrcode
\ No newline at end of file
diff --git a/run_test.py b/run_test.py
index 17e23d6..8358f12 100644
--- a/run_test.py
+++ b/run_test.py
@@ -1,12 +1,11 @@
-import sys
import getopt
import os
+import sys
opts, args = getopt.getopt(sys.argv[1:], "cm:", ["coverage=", "module="])
is_coverage = False
test_module = ""
-waf_addr = "127.0.0.1:50001"
setting = "oj.settings"
for opt, arg in opts:
diff --git a/utils/shortcuts.py b/utils/shortcuts.py
index 1dd8834..9962ade 100644
--- a/utils/shortcuts.py
+++ b/utils/shortcuts.py
@@ -2,10 +2,33 @@ import logging
import random
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"):
"""
生成指定长度的随机字符串或者数字, 可以用于密钥等安全场景