Merge branch '2.0' of github.com:QingdaoU/OnlineJudge into 2.0

This commit is contained in:
virusdefender
2017-04-26 23:41:16 +08:00
33 changed files with 561 additions and 107 deletions

View File

@@ -12,3 +12,5 @@ script:
- flake8 .
- coverage run --include="$PWD/*" manage.py test
- coverage report
notifications:
slack: onlinejudgeteam:BzBz8UFgmS5crpiblof17K2W

View File

@@ -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()

View File

@@ -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()

8
account/tasks.py Normal file
View File

@@ -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)

View File

@@ -0,0 +1,78 @@
<table cellpadding="0" cellspacing="0" align="center" style="text-align:left;font-family:'微软雅黑','黑体',arial;"
width="742">
<tbody>
<tr>
<td>
<table cellpadding="0" cellspacing="0"
style="text-align:left;border:1px solid #50a5e6;color:#fff;font-size:18px;" width="740">
<tbody>
<tr height="39" style="background-color:#50a5e6;">
<td style="padding-left:15px;font-family:'微软雅黑','黑体',arial;">
{{ website_name }} 登录信息找回
</td>
</tr>
</tbody>
</table>
<table cellpadding="0" cellspacing="0"
style="text-align:left;border:1px solid #f0f0f0;border-top:none;color:#585858;background-color:#fafafa;"
width="740">
<tbody>
<tr height="25">
<td></td>
</tr>
<tr height="40">
<td style="padding-left:25px;padding-right:25px;font-size:18px;font-family:'微软雅黑','黑体',arial;">
Hello, {{ username }}:
</td>
</tr>
<tr height="15">
<td></td>
</tr>
<tr height="30">
<td style="padding-left:55px;padding-right:55px;font-family:'微软雅黑','黑体',arial;font-size:14px;">
您刚刚在 {{ website_name }} 申请了找回登录信息服务。
</td>
</tr>
<tr height="30">
<td style="padding-left:55px;padding-right:55px;font-family:'微软雅黑','黑体',arial;font-size:14px;">
请在<span style="color:rgb(255,0,0)">30分钟</span>内点击下面链接设置您的新密码:
</td>
</tr>
<tr height="60">
<td style="padding-left:55px;padding-right:55px;font-family:'微软雅黑','黑体',arial;font-size:14px;">
<a href="{{ link }}" target="_blank"
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;">重置密码</a>
</td>
</tr>
<tr height="10">
<td></td>
</tr>
<tr height="20">
<td style="padding-left:55px;padding-right:55px;font-family:'微软雅黑','黑体',arial;font-size:12px;">
如果上面的链接点击无效,请复制以下链接至浏览器的地址栏直接打开。
</td>
</tr>
<tr height="30">
<td style="padding-left:55px;padding-right:65px;font-family:'微软雅黑','黑体',arial;">
<a href="{{ link }}" target="_blank" style="color:#0c94de;font-size:12px;">
{{ link }}
</a>
</td>
</tr>
<tr height="20">
<td style="padding-left:55px;padding-right:55px;font-family:'微软雅黑','黑体',arial;font-size:12px;">
如果您没有提出过该申请,请忽略此邮件。有可能是其他用户误填了您的邮件地址,我们不会对你的帐户进行任何修改。
请不要向他人透露本邮件的内容,否则可能会导致您的账号被盗。
</td>
</tr>
<tr height="20">
<td></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>

View File

@@ -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):

View File

@@ -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"),
]

View File

@@ -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")
]

12
account/urls/user.py Normal file
View File

@@ -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")
]

View File

@@ -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")

View File

@@ -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")

148
account/views/user.py Normal file
View File

@@ -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")

View File

@@ -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"),
]

View File

@@ -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)

View File

@@ -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")
]

View File

@@ -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")
]

View File

@@ -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)

View File

@@ -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")
]

View File

@@ -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"),
]

View File

@@ -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()

View File

@@ -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

View File

@@ -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")

View File

@@ -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/"

View File

@@ -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/"

View File

@@ -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"]))

View File

@@ -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")),

View File

@@ -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

View File

@@ -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")
]

View File

@@ -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")
]

View File

@@ -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")

10
requirements.txt Normal file
View File

@@ -0,0 +1,10 @@
django==1.9.6
djangorestframework==3.4.0
otpauth
pillow
python-dateutil
celery
Envelopes
pytz
jsonfield
qrcode

View File

@@ -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:

View File

@@ -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"):
"""
生成指定长度的随机字符串或者数字, 可以用于密钥等安全场景