Accept Merge Request #336 mr new-arch-dev : (new-arch -> dev)
Merge Request: mr new-arch-dev Created By: @virusdefender Accepted By: @virusdefender URL: https://coding.net/u/virusdefender/p/qduoj/git/merge/336
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -52,7 +52,7 @@ db.db
|
|||||||
#redis dump
|
#redis dump
|
||||||
*.rdb
|
*.rdb
|
||||||
#*.out
|
#*.out
|
||||||
db.sqlite3
|
*.sqlite3
|
||||||
.DS_Store
|
.DS_Store
|
||||||
log/
|
log/
|
||||||
static/release/css
|
static/release/css
|
||||||
|
|||||||
25
account/migrations/0016_auto_20151211_2230.py
Normal file
25
account/migrations/0016_auto_20151211_2230.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9 on 2015-12-11 14:30
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('account', '0015_userprofile_student_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='tfa_token',
|
||||||
|
field=models.CharField(blank=True, max_length=10, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='two_factor_auth',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
20
account/migrations/0017_auto_20151212_2139.py
Normal file
20
account/migrations/0017_auto_20151212_2139.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9 on 2015-12-12 13:39
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('account', '0016_auto_20151211_2230'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='tfa_token',
|
||||||
|
field=models.CharField(blank=True, max_length=40, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -40,6 +40,9 @@ class User(AbstractBaseUser):
|
|||||||
reset_password_token_create_time = models.DateTimeField(blank=True, null=True)
|
reset_password_token_create_time = models.DateTimeField(blank=True, null=True)
|
||||||
# 论坛授权token
|
# 论坛授权token
|
||||||
auth_token = models.CharField(max_length=40, blank=True, null=True)
|
auth_token = models.CharField(max_length=40, blank=True, null=True)
|
||||||
|
# 是否开启两步验证
|
||||||
|
two_factor_auth = models.BooleanField(default=False)
|
||||||
|
tfa_token = models.CharField(max_length=40, blank=True, null=True)
|
||||||
|
|
||||||
USERNAME_FIELD = 'username'
|
USERNAME_FIELD = 'username'
|
||||||
REQUIRED_FIELDS = []
|
REQUIRED_FIELDS = []
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from .models import User, UserProfile
|
|||||||
class UserLoginSerializer(serializers.Serializer):
|
class UserLoginSerializer(serializers.Serializer):
|
||||||
username = serializers.CharField(max_length=30)
|
username = serializers.CharField(max_length=30)
|
||||||
password = serializers.CharField(max_length=30)
|
password = serializers.CharField(max_length=30)
|
||||||
captcha = serializers.CharField(min_length=4, max_length=4)
|
tfa_code = serializers.CharField(min_length=6, max_length=6, required=False)
|
||||||
|
|
||||||
|
|
||||||
class UsernameCheckSerializer(serializers.Serializer):
|
class UsernameCheckSerializer(serializers.Serializer):
|
||||||
@@ -84,3 +84,7 @@ class UserProfileSerializer(serializers.ModelSerializer):
|
|||||||
model = UserProfile
|
model = UserProfile
|
||||||
fields = ["avatar", "blog", "mood", "hduoj_username", "bestcoder_username", "codeforces_username",
|
fields = ["avatar", "blog", "mood", "hduoj_username", "bestcoder_username", "codeforces_username",
|
||||||
"rank", "accepted_number", "submissions_number", "problems_status", "phone_number", "school", "student_id"]
|
"rank", "accepted_number", "submissions_number", "problems_status", "phone_number", "school", "student_id"]
|
||||||
|
|
||||||
|
|
||||||
|
class TwoFactorAuthCodeSerializer(serializers.Serializer):
|
||||||
|
code = serializers.IntegerField()
|
||||||
|
|||||||
8
account/tasks.py
Normal file
8
account/tasks.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
from celery import shared_task
|
||||||
|
from utils.mail import send_email
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def _send_email(from_name, to_email, to_name, subject, content):
|
||||||
|
send_email(from_name, to_email, to_name, subject, content)
|
||||||
119
account/views.py
119
account/views.py
@@ -1,11 +1,13 @@
|
|||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
import codecs
|
import codecs
|
||||||
|
import qrcode
|
||||||
|
import StringIO
|
||||||
from django import http
|
from django import http
|
||||||
from django.contrib import auth
|
from django.contrib import auth
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponse
|
||||||
from django.core.exceptions import MultipleObjectsReturned
|
from django.core.exceptions import MultipleObjectsReturned
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
||||||
@@ -14,7 +16,9 @@ from rest_framework.response import Response
|
|||||||
from utils.shortcuts import (serializer_invalid_response, error_response,
|
from utils.shortcuts import (serializer_invalid_response, error_response,
|
||||||
success_response, error_page, paginate, rand_str)
|
success_response, error_page, paginate, rand_str)
|
||||||
from utils.captcha import Captcha
|
from utils.captcha import Captcha
|
||||||
from utils.mail import send_email
|
from utils.otp_auth import OtpAuth
|
||||||
|
|
||||||
|
from .tasks import _send_email
|
||||||
|
|
||||||
from .decorators import login_required
|
from .decorators import login_required
|
||||||
from .models import User, UserProfile
|
from .models import User, UserProfile
|
||||||
@@ -23,7 +27,8 @@ from .serializers import (UserLoginSerializer, UserRegisterSerializer,
|
|||||||
UserChangePasswordSerializer,
|
UserChangePasswordSerializer,
|
||||||
UserSerializer, EditUserSerializer,
|
UserSerializer, EditUserSerializer,
|
||||||
ApplyResetPasswordSerializer, ResetPasswordSerializer,
|
ApplyResetPasswordSerializer, ResetPasswordSerializer,
|
||||||
SSOSerializer, EditUserProfileSerializer, UserProfileSerializer)
|
SSOSerializer, EditUserProfileSerializer,
|
||||||
|
UserProfileSerializer, TwoFactorAuthCodeSerializer)
|
||||||
|
|
||||||
from .decorators import super_admin_required
|
from .decorators import super_admin_required
|
||||||
|
|
||||||
@@ -38,14 +43,23 @@ class UserLoginAPIView(APIView):
|
|||||||
serializer = UserLoginSerializer(data=request.data)
|
serializer = UserLoginSerializer(data=request.data)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
data = serializer.data
|
data = serializer.data
|
||||||
captcha = Captcha(request)
|
print data
|
||||||
if not captcha.check(data["captcha"]):
|
|
||||||
return error_response(u"验证码错误")
|
|
||||||
user = auth.authenticate(username=data["username"], password=data["password"])
|
user = auth.authenticate(username=data["username"], password=data["password"])
|
||||||
# 用户名或密码错误的话 返回None
|
# 用户名或密码错误的话 返回None
|
||||||
if user:
|
if user:
|
||||||
auth.login(request, user)
|
if not user.two_factor_auth:
|
||||||
return success_response(u"登录成功")
|
auth.login(request, user)
|
||||||
|
return success_response(u"登录成功")
|
||||||
|
|
||||||
|
# 没有输入两步验证的验证码
|
||||||
|
if user.two_factor_auth and "tfa_code" not in data:
|
||||||
|
return success_response("tfa_required")
|
||||||
|
|
||||||
|
if OtpAuth(user.tfa_token).valid_totp(data["tfa_code"]):
|
||||||
|
auth.login(request, user)
|
||||||
|
return success_response(u"登录成功")
|
||||||
|
else:
|
||||||
|
return error_response(u"验证码错误")
|
||||||
else:
|
else:
|
||||||
return error_response(u"用户名或密码错误")
|
return error_response(u"用户名或密码错误")
|
||||||
else:
|
else:
|
||||||
@@ -63,7 +77,7 @@ def index_page(request):
|
|||||||
return render(request, "oj/index.html")
|
return render(request, "oj/index.html")
|
||||||
|
|
||||||
if request.META.get('HTTP_REFERER') or request.GET.get("index"):
|
if request.META.get('HTTP_REFERER') or request.GET.get("index"):
|
||||||
return render(request, "oj/index.html")
|
return render(request, "oj/index.html")
|
||||||
else:
|
else:
|
||||||
return http.HttpResponseRedirect('/problems/')
|
return http.HttpResponseRedirect('/problems/')
|
||||||
|
|
||||||
@@ -151,9 +165,9 @@ class EmailCheckAPIView(APIView):
|
|||||||
检测邮箱是否存在,用状态码标识结果
|
检测邮箱是否存在,用状态码标识结果
|
||||||
---
|
---
|
||||||
"""
|
"""
|
||||||
#这里是为了适应前端表单验证空间的要求
|
# 这里是为了适应前端表单验证空间的要求
|
||||||
reset = request.GET.get("reset", None)
|
reset = request.GET.get("reset", None)
|
||||||
#如果reset为true说明该请求是重置密码页面发出的,要返回的状态码应正好相反
|
# 如果reset为true说明该请求是重置密码页面发出的,要返回的状态码应正好相反
|
||||||
if reset:
|
if reset:
|
||||||
existed = 200
|
existed = 200
|
||||||
does_not_existed = 400
|
does_not_existed = 400
|
||||||
@@ -287,22 +301,25 @@ class ApplyResetPasswordAPIView(APIView):
|
|||||||
user = User.objects.get(email=data["email"])
|
user = User.objects.get(email=data["email"])
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
return error_response(u"用户不存在")
|
return error_response(u"用户不存在")
|
||||||
if user.reset_password_token_create_time and (now() - user.reset_password_token_create_time).total_seconds() < 20 * 60:
|
if user.reset_password_token_create_time and (
|
||||||
|
now() - user.reset_password_token_create_time).total_seconds() < 20 * 60:
|
||||||
return error_response(u"20分钟内只能找回一次密码")
|
return error_response(u"20分钟内只能找回一次密码")
|
||||||
user.reset_password_token = rand_str()
|
user.reset_password_token = rand_str()
|
||||||
user.reset_password_token_create_time = now()
|
user.reset_password_token_create_time = now()
|
||||||
user.save()
|
user.save()
|
||||||
email_template = codecs.open(settings.TEMPLATES[0]["DIRS"][0] + "utils/reset_password_email.html", "r", "utf-8").read()
|
email_template = codecs.open(settings.TEMPLATES[0]["DIRS"][0] + "utils/reset_password_email.html", "r",
|
||||||
|
"utf-8").read()
|
||||||
|
|
||||||
email_template = email_template.replace("{{ username }}", user.username).\
|
email_template = email_template.replace("{{ username }}", user.username). \
|
||||||
replace("{{ website_name }}", settings.WEBSITE_INFO["website_name"]).\
|
replace("{{ website_name }}", settings.WEBSITE_INFO["website_name"]). \
|
||||||
replace("{{ link }}", request.scheme + "://" + request.META['HTTP_HOST'] + "/reset_password/t/" + user.reset_password_token)
|
replace("{{ link }}", request.scheme + "://" + request.META[
|
||||||
|
'HTTP_HOST'] + "/reset_password/t/" + user.reset_password_token)
|
||||||
|
|
||||||
send_email(settings.WEBSITE_INFO["website_name"],
|
_send_email.delay(settings.WEBSITE_INFO["website_name"],
|
||||||
user.email,
|
user.email,
|
||||||
user.username,
|
user.username,
|
||||||
settings.WEBSITE_INFO["website_name"] + u" 登录信息找回邮件",
|
settings.WEBSITE_INFO["website_name"] + u" 登录信息找回邮件",
|
||||||
email_template)
|
email_template)
|
||||||
return success_response(u"邮件发送成功,请前往您的邮箱查收")
|
return success_response(u"邮件发送成功,请前往您的邮箱查收")
|
||||||
else:
|
else:
|
||||||
return serializer_invalid_response(serializer)
|
return serializer_invalid_response(serializer)
|
||||||
@@ -350,7 +367,10 @@ class SSOAPIView(APIView):
|
|||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
try:
|
try:
|
||||||
user = User.objects.get(auth_token=serializer.data["token"])
|
user = User.objects.get(auth_token=serializer.data["token"])
|
||||||
return success_response({"username": user.username})
|
user.auth_token = None
|
||||||
|
user.save()
|
||||||
|
return success_response(
|
||||||
|
{"username": user.username, "admin_type": user.admin_type, "avatar": user.userprofile.avatar})
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
return error_response(u"用户不存在")
|
return error_response(u"用户不存在")
|
||||||
else:
|
else:
|
||||||
@@ -364,7 +384,8 @@ class SSOAPIView(APIView):
|
|||||||
token = rand_str()
|
token = rand_str()
|
||||||
request.user.auth_token = token
|
request.user.auth_token = token
|
||||||
request.user.save()
|
request.user.save()
|
||||||
return render(request, "oj/account/sso.html", {"redirect_url": callback + "?token=" + token, "callback": callback})
|
return render(request, "oj/account/sso.html",
|
||||||
|
{"redirect_url": callback + "?token=" + token, "callback": callback})
|
||||||
|
|
||||||
|
|
||||||
def reset_password_page(request, token):
|
def reset_password_page(request, token):
|
||||||
@@ -375,3 +396,55 @@ def reset_password_page(request, token):
|
|||||||
if (now() - user.reset_password_token_create_time).total_seconds() > 30 * 60:
|
if (now() - user.reset_password_token_create_time).total_seconds() > 30 * 60:
|
||||||
return error_page(request, u"链接已过期")
|
return error_page(request, u"链接已过期")
|
||||||
return render(request, "oj/account/reset_password.html", {"user": user})
|
return render(request, "oj/account/reset_password.html", {"user": user})
|
||||||
|
|
||||||
|
|
||||||
|
class TwoFactorAuthAPIView(APIView):
|
||||||
|
@login_required
|
||||||
|
def get(self, request):
|
||||||
|
"""
|
||||||
|
获取绑定二维码
|
||||||
|
"""
|
||||||
|
user = request.user
|
||||||
|
if user.two_factor_auth:
|
||||||
|
return error_response(u"已经开启两步验证了")
|
||||||
|
token = rand_str()
|
||||||
|
user.tfa_token = token
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
image = qrcode.make(OtpAuth(token).to_uri("totp", settings.WEBSITE_INFO["url"], "OnlineJudgeAdmin"))
|
||||||
|
buf = StringIO.StringIO()
|
||||||
|
image.save(buf, 'gif')
|
||||||
|
|
||||||
|
return HttpResponse(buf.getvalue(), 'image/gif')
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def post(self, request):
|
||||||
|
"""
|
||||||
|
开启两步验证
|
||||||
|
"""
|
||||||
|
serializer = TwoFactorAuthCodeSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
code = serializer.data["code"]
|
||||||
|
user = request.user
|
||||||
|
if OtpAuth(user.tfa_token).valid_totp(code):
|
||||||
|
user.two_factor_auth = True
|
||||||
|
user.save()
|
||||||
|
return success_response(u"开启两步验证成功")
|
||||||
|
else:
|
||||||
|
return error_response(u"验证码错误")
|
||||||
|
else:
|
||||||
|
return serializer_invalid_response(serializer)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def put(self, request):
|
||||||
|
serializer = TwoFactorAuthCodeSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
user = request.user
|
||||||
|
code = serializer.data["code"]
|
||||||
|
if OtpAuth(user.tfa_token).valid_totp(code):
|
||||||
|
user.two_factor_auth = False
|
||||||
|
user.save()
|
||||||
|
else:
|
||||||
|
return error_response(u"验证码错误")
|
||||||
|
else:
|
||||||
|
return serializer_invalid_response(serializer)
|
||||||
|
|||||||
@@ -110,7 +110,8 @@ class ContestRank(models.Model):
|
|||||||
# 之前已经提交过,但是是错误的,这次提交是正确的。错误的题目不计入罚时
|
# 之前已经提交过,但是是错误的,这次提交是正确的。错误的题目不计入罚时
|
||||||
self.total_time += (info["ac_time"] + info["error_number"] * 20 * 60)
|
self.total_time += (info["ac_time"] + info["error_number"] * 20 * 60)
|
||||||
problem = ContestProblem.objects.get(id=submission.problem_id)
|
problem = ContestProblem.objects.get(id=submission.problem_id)
|
||||||
if problem.total_accepted_number == 0:
|
# 更新题目计数器在前 所以是1
|
||||||
|
if problem.total_accepted_number == 1:
|
||||||
info["is_first_ac"] = True
|
info["is_first_ac"] = True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -128,7 +129,7 @@ class ContestRank(models.Model):
|
|||||||
self.total_time += info["ac_time"]
|
self.total_time += info["ac_time"]
|
||||||
problem = ContestProblem.objects.get(id=submission.problem_id)
|
problem = ContestProblem.objects.get(id=submission.problem_id)
|
||||||
|
|
||||||
if problem.total_accepted_number == 0:
|
if problem.total_accepted_number == 1:
|
||||||
info["is_first_ac"] = True
|
info["is_first_ac"] = True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ ENV PYTHONBUFFERED 1
|
|||||||
RUN mkdir -p /code/log /code/test_case /code/upload
|
RUN mkdir -p /code/log /code/test_case /code/upload
|
||||||
WORKDIR /code
|
WORKDIR /code
|
||||||
ADD requirements.txt /code/
|
ADD requirements.txt /code/
|
||||||
RUN pip install -r requirements.txt
|
RUN pip install -i http://pypi.douban.com/simple -r requirements.txt --trusted-host pypi.douban.com
|
||||||
EXPOSE 8010
|
EXPOSE 8010
|
||||||
CMD supervisord
|
CMD supervisord
|
||||||
|
|||||||
@@ -11,4 +11,6 @@ supervisor
|
|||||||
pillow
|
pillow
|
||||||
jsonfield
|
jsonfield
|
||||||
Envelopes
|
Envelopes
|
||||||
huey
|
celery
|
||||||
|
django-celery
|
||||||
|
qrcode
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[program:mq]
|
[program:task_queue]
|
||||||
|
|
||||||
command=python manage.py run_huey
|
command=python manage.py celeryd -B -l DEBUG
|
||||||
|
|
||||||
directory=/code/
|
directory=/code/
|
||||||
user=root
|
user=root
|
||||||
|
|||||||
25
judge_dispatcher/migrations/0003_auto_20151223_0029.py
Normal file
25
judge_dispatcher/migrations/0003_auto_20151223_0029.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('judge_dispatcher', '0002_auto_20151207_2310'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='judgeserver',
|
||||||
|
name='create_time',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='judgeserver',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(default='judger', max_length=30),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -3,6 +3,7 @@ from django.db import models
|
|||||||
|
|
||||||
|
|
||||||
class JudgeServer(models.Model):
|
class JudgeServer(models.Model):
|
||||||
|
name = models.CharField(max_length=30)
|
||||||
ip = models.GenericIPAddressField()
|
ip = models.GenericIPAddressField()
|
||||||
port = models.IntegerField()
|
port = models.IntegerField()
|
||||||
# 这个服务器最大可能运行的判题实例数量
|
# 这个服务器最大可能运行的判题实例数量
|
||||||
@@ -14,6 +15,7 @@ class JudgeServer(models.Model):
|
|||||||
lock = models.BooleanField(default=False)
|
lock = models.BooleanField(default=False)
|
||||||
# status 为 false 的时候代表不使用这个服务器
|
# status 为 false 的时候代表不使用这个服务器
|
||||||
status = models.BooleanField(default=True)
|
status = models.BooleanField(default=True)
|
||||||
|
create_time = models.DateTimeField(auto_now_add=True, blank=True, null=True)
|
||||||
|
|
||||||
def use_judge_instance(self):
|
def use_judge_instance(self):
|
||||||
# 因为use 和 release 中间是判题时间,可能这个 model 的数据已经被修改了,所以不能直接使用self.xxx,否则取到的是旧数据
|
# 因为use 和 release 中间是判题时间,可能这个 model 的数据已经被修改了,所以不能直接使用self.xxx,否则取到的是旧数据
|
||||||
|
|||||||
29
judge_dispatcher/serializers.py
Normal file
29
judge_dispatcher/serializers.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
import json
|
||||||
|
from rest_framework import serializers
|
||||||
|
from .models import JudgeServer
|
||||||
|
|
||||||
|
|
||||||
|
class CreateJudgesSerializer(serializers.Serializer):
|
||||||
|
name = serializers.CharField(max_length=30)
|
||||||
|
ip = serializers.IPAddressField()
|
||||||
|
port = serializers.IntegerField()
|
||||||
|
# 这个服务器最大可能运行的判题实例数量
|
||||||
|
max_instance_number = serializers.IntegerField()
|
||||||
|
token = serializers.CharField(max_length=30)
|
||||||
|
|
||||||
|
|
||||||
|
class EditJudgesSerializer(serializers.Serializer):
|
||||||
|
id = serializers.IntegerField()
|
||||||
|
name = serializers.CharField(max_length=30)
|
||||||
|
ip = serializers.IPAddressField()
|
||||||
|
port = serializers.IntegerField()
|
||||||
|
# 这个服务器最大可能运行的判题实例数量
|
||||||
|
max_instance_number = serializers.IntegerField()
|
||||||
|
token = serializers.CharField(max_length=30)
|
||||||
|
status = serializers.BooleanField()
|
||||||
|
|
||||||
|
|
||||||
|
class JudgesSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = JudgeServer
|
||||||
@@ -32,7 +32,7 @@ class JudgeDispatcher(object):
|
|||||||
if servers.exists():
|
if servers.exists():
|
||||||
return servers.first()
|
return servers.first()
|
||||||
|
|
||||||
def judge(self, is_waiting_task=False):
|
def judge(self):
|
||||||
self.submission.judge_start_time = int(time.time() * 1000)
|
self.submission.judge_start_time = int(time.time() * 1000)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
@@ -89,9 +89,9 @@ class JudgeDispatcher(object):
|
|||||||
submission = Submission.objects.get(id=waiting_submission.submission_id)
|
submission = Submission.objects.get(id=waiting_submission.submission_id)
|
||||||
waiting_submission.delete()
|
waiting_submission.delete()
|
||||||
|
|
||||||
_judge(submission, time_limit=waiting_submission.time_limit,
|
_judge.delay(submission, time_limit=waiting_submission.time_limit,
|
||||||
memory_limit=waiting_submission.memory_limit, test_case_id=waiting_submission.test_case_id,
|
memory_limit=waiting_submission.memory_limit, test_case_id=waiting_submission.test_case_id,
|
||||||
is_waiting_task=True)
|
is_waiting_task=True)
|
||||||
|
|
||||||
def update_problem_status(self):
|
def update_problem_status(self):
|
||||||
problem = Problem.objects.get(id=self.submission.problem_id)
|
problem = Problem.objects.get(id=self.submission.problem_id)
|
||||||
@@ -113,13 +113,14 @@ class JudgeDispatcher(object):
|
|||||||
# 普通题目的话,到这里就结束了
|
# 普通题目的话,到这里就结束了
|
||||||
|
|
||||||
def update_contest_problem_status(self):
|
def update_contest_problem_status(self):
|
||||||
# 能运行到这里的都是比赛题目
|
# 能运行到这里的都是比赛题目a
|
||||||
contest = Contest.objects.get(id=self.submission.contest_id)
|
contest = Contest.objects.get(id=self.submission.contest_id)
|
||||||
if contest.status != CONTEST_UNDERWAY:
|
if contest.status != CONTEST_UNDERWAY:
|
||||||
logger.info("Contest debug mode, id: " + str(contest.id) + ", submission id: " + self.submission.id)
|
logger.info("Contest debug mode, id: " + str(contest.id) + ", submission id: " + self.submission.id)
|
||||||
return
|
return
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
contest_problem = ContestProblem.objects.select_for_update().get(contest=contest, id=self.submission.problem_id)
|
contest_problem = ContestProblem.objects.select_for_update().get(contest=contest,
|
||||||
|
id=self.submission.problem_id)
|
||||||
|
|
||||||
contest_problem.add_submission_number()
|
contest_problem.add_submission_number()
|
||||||
|
|
||||||
|
|||||||
71
judge_dispatcher/views.py
Normal file
71
judge_dispatcher/views.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from account.decorators import super_admin_required
|
||||||
|
from utils.shortcuts import success_response, serializer_invalid_response, error_response, paginate
|
||||||
|
from .serializers import CreateJudgesSerializer, JudgesSerializer, EditJudgesSerializer
|
||||||
|
from .models import JudgeServer
|
||||||
|
|
||||||
|
|
||||||
|
class AdminJudgeServerAPIView(APIView):
|
||||||
|
@super_admin_required
|
||||||
|
def post(self, request):
|
||||||
|
"""
|
||||||
|
添加判题服务器 json api接口
|
||||||
|
---
|
||||||
|
request_serializer: CreateJudgesSerializer
|
||||||
|
response_serializer: JudgesSerializer
|
||||||
|
"""
|
||||||
|
serializer = CreateJudgesSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
data = serializer.data
|
||||||
|
judge_server = JudgeServer.objects.create(name=data["name"], ip=data["ip"], port=data["port"],
|
||||||
|
max_instance_number=data["max_instance_number"],
|
||||||
|
token=data["token"],
|
||||||
|
created_by=request.user)
|
||||||
|
return success_response(JudgesSerializer(judge_server).data)
|
||||||
|
else:
|
||||||
|
return serializer_invalid_response(serializer)
|
||||||
|
|
||||||
|
@super_admin_required
|
||||||
|
def put(self, request):
|
||||||
|
"""
|
||||||
|
修改判题服务器信息 json api接口
|
||||||
|
---
|
||||||
|
request_serializer: EditJudgesSerializer
|
||||||
|
response_serializer: JudgesSerializer
|
||||||
|
"""
|
||||||
|
serializer = EditJudgesSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
data = serializer.data
|
||||||
|
try:
|
||||||
|
judge_server = JudgeServer.objects.get(pk=data["id"])
|
||||||
|
except JudgeServer.DoesNotExist:
|
||||||
|
return error_response(u"此判题服务器不存在!")
|
||||||
|
|
||||||
|
judge_server.name = data["name"]
|
||||||
|
judge_server.ip = data["ip"]
|
||||||
|
judge_server.port = data["port"]
|
||||||
|
judge_server.max_instance_number = data["max_instance_number"]
|
||||||
|
judge_server.token = data["token"]
|
||||||
|
judge_server.status = data["status"]
|
||||||
|
judge_server.save()
|
||||||
|
return success_response(JudgesSerializer(judge_server).data)
|
||||||
|
else:
|
||||||
|
return serializer_invalid_response(serializer)
|
||||||
|
|
||||||
|
@super_admin_required
|
||||||
|
def get(self, request):
|
||||||
|
"""
|
||||||
|
获取全部判题服务器
|
||||||
|
"""
|
||||||
|
judge_server_id = request.GET.get("judge_server_id", None)
|
||||||
|
if judge_server_id:
|
||||||
|
try:
|
||||||
|
judge_server = JudgeServer.objects.get(id=judge_server_id)
|
||||||
|
except JudgeServer.DoesNotExist:
|
||||||
|
return error_response(u"判题服务器不存在")
|
||||||
|
return success_response(JudgesSerializer(judge_server).data)
|
||||||
|
judge_server = JudgeServer.objects.all()
|
||||||
|
|
||||||
|
return paginate(request, judge_server, JudgesSerializer)
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# coding=utf-8
|
|
||||||
import redis
|
|
||||||
import datetime
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
from judge.result import result
|
|
||||||
from django.conf import settings
|
|
||||||
from utils.shortcuts import success_response
|
|
||||||
from submission.models import Submission
|
|
||||||
|
|
||||||
|
|
||||||
class QueueLengthMonitorAPIView(APIView):
|
|
||||||
def get(self, request):
|
|
||||||
r = redis.Redis(host=settings.redis_config["host"], port=settings.redis_config["port"], db=settings.redis_config["db"])
|
|
||||||
waiting_number = r.get("judge_queue_length")
|
|
||||||
if waiting_number is None:
|
|
||||||
waiting_number = 0
|
|
||||||
now = datetime.datetime.now()
|
|
||||||
return success_response({"time": ":".join([str(now.hour), str(now.minute), str(now.second)]),
|
|
||||||
"count": waiting_number})
|
|
||||||
@@ -7,3 +7,8 @@
|
|||||||
|___/ |___/ |_|
|
|___/ |___/ |_|
|
||||||
https://github.com/QingdaoU/OnlineJudge
|
https://github.com/QingdaoU/OnlineJudge
|
||||||
"""
|
"""
|
||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
# This will make sure the app is always imported when
|
||||||
|
# Django starts so that shared_task will use this app.
|
||||||
|
from .celery import app as celery_app
|
||||||
19
oj/celery.py
Normal file
19
oj/celery.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from __future__ import absolute_import
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from celery import Celery
|
||||||
|
|
||||||
|
# set the default Django settings module for the 'celery' program.
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oj.settings')
|
||||||
|
|
||||||
|
from django.conf import 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)
|
||||||
@@ -28,6 +28,12 @@ REDIS_QUEUE = {
|
|||||||
"db": 2
|
"db": 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# for celery
|
||||||
|
BROKER_URL = 'redis://%s:%s/%s' % (REDIS_QUEUE["host"], str(REDIS_QUEUE["port"]), str(REDIS_QUEUE["db"]))
|
||||||
|
ACCEPT_CONTENT = ['json']
|
||||||
|
|
||||||
|
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
ALLOWED_HOSTS = []
|
ALLOWED_HOSTS = []
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ REDIS_QUEUE = {
|
|||||||
"db": 2
|
"db": 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# for celery
|
||||||
|
BROKER_URL = 'redis://%s:%s/%s' % (REDIS_QUEUE["host"], str(REDIS_QUEUE["port"]), str(REDIS_QUEUE["db"]))
|
||||||
|
ACCEPT_CONTENT = ['json']
|
||||||
|
|
||||||
|
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['*']
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ https://docs.djangoproject.com/en/1.8/topics/settings/
|
|||||||
For the full list of settings and their values, see
|
For the full list of settings and their values, see
|
||||||
https://docs.djangoproject.com/en/1.8/ref/settings/
|
https://docs.djangoproject.com/en/1.8/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
from __future__ import absolute_import
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -22,6 +22,9 @@ if ENV == "local":
|
|||||||
elif ENV == "server":
|
elif ENV == "server":
|
||||||
from .server_settings import *
|
from .server_settings import *
|
||||||
|
|
||||||
|
import djcelery
|
||||||
|
djcelery.setup_loader()
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
|
||||||
@@ -53,7 +56,8 @@ INSTALLED_APPS = (
|
|||||||
'judge_dispatcher',
|
'judge_dispatcher',
|
||||||
|
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'huey.djhuey',
|
'djcelery',
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
@@ -186,14 +190,6 @@ WEBSITE_INFO = {"website_name": "qduoj",
|
|||||||
"website_footer": u"青岛大学信息工程学院 创新实验室 <a href=\"http://www.miibeian.gov.cn/\">京ICP备15062075号-1</a>",
|
"website_footer": u"青岛大学信息工程学院 创新实验室 <a href=\"http://www.miibeian.gov.cn/\">京ICP备15062075号-1</a>",
|
||||||
"url": "https://qduoj.com"}
|
"url": "https://qduoj.com"}
|
||||||
|
|
||||||
HUEY = {
|
|
||||||
'backend': 'huey.backends.redis_backend',
|
|
||||||
'name': 'task_queue',
|
|
||||||
'connection': {'host': REDIS_QUEUE["host"], 'port': REDIS_QUEUE["port"], 'db': REDIS_QUEUE["db"]},
|
|
||||||
'always_eager': False, # Defaults to False when running via manage.py run_huey
|
|
||||||
# Options to pass into the consumer when running ``manage.py run_huey``
|
|
||||||
'consumer_options': {'workers': 50},
|
|
||||||
}
|
|
||||||
|
|
||||||
SMTP_CONFIG = {"smtp_server": "smtp.mxhichina.com",
|
SMTP_CONFIG = {"smtp_server": "smtp.mxhichina.com",
|
||||||
"email": "noreply@qduoj.com",
|
"email": "noreply@qduoj.com",
|
||||||
|
|||||||
29
oj/urls.py
29
oj/urls.py
@@ -6,7 +6,8 @@ from django.views.generic import TemplateView
|
|||||||
from account.views import (UserLoginAPIView, UsernameCheckAPIView, UserRegisterAPIView,
|
from account.views import (UserLoginAPIView, UsernameCheckAPIView, UserRegisterAPIView,
|
||||||
UserChangePasswordAPIView, EmailCheckAPIView,
|
UserChangePasswordAPIView, EmailCheckAPIView,
|
||||||
UserAdminAPIView, UserInfoAPIView, ResetPasswordAPIView,
|
UserAdminAPIView, UserInfoAPIView, ResetPasswordAPIView,
|
||||||
ApplyResetPasswordAPIView, SSOAPIView, UserProfileAPIView)
|
ApplyResetPasswordAPIView, SSOAPIView, UserProfileAPIView,
|
||||||
|
TwoFactorAuthAPIView)
|
||||||
|
|
||||||
from announcement.views import AnnouncementAdminAPIView
|
from announcement.views import AnnouncementAdminAPIView
|
||||||
|
|
||||||
@@ -22,11 +23,9 @@ from admin.views import AdminTemplateView
|
|||||||
from problem.views import TestCaseUploadAPIView, ProblemTagAdminAPIView, ProblemAdminAPIView
|
from problem.views import TestCaseUploadAPIView, ProblemTagAdminAPIView, ProblemAdminAPIView
|
||||||
from submission.views import (SubmissionAPIView, SubmissionAdminAPIView, ContestSubmissionAPIView,
|
from submission.views import (SubmissionAPIView, SubmissionAdminAPIView, ContestSubmissionAPIView,
|
||||||
SubmissionShareAPIView, SubmissionRejudgeAdminAPIView)
|
SubmissionShareAPIView, SubmissionRejudgeAdminAPIView)
|
||||||
from monitor.views import QueueLengthMonitorAPIView
|
from judge_dispatcher.views import AdminJudgeServerAPIView
|
||||||
from utils.views import SimditorImageUploadAPIView
|
from utils.views import SimditorImageUploadAPIView
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url("^$", "account.views.index_page", name="index_page"),
|
url("^$", "account.views.index_page", name="index_page"),
|
||||||
|
|
||||||
@@ -55,7 +54,6 @@ urlpatterns = [
|
|||||||
url(r'^api/contest/submission/$', ContestSubmissionAPIView.as_view(), name="contest_submission_api"),
|
url(r'^api/contest/submission/$', ContestSubmissionAPIView.as_view(), name="contest_submission_api"),
|
||||||
url(r'^api/submission/$', SubmissionAPIView.as_view(), name="submission_api"),
|
url(r'^api/submission/$', SubmissionAPIView.as_view(), name="submission_api"),
|
||||||
url(r'^api/group_join/$', JoinGroupAPIView.as_view(), name="group_join_api"),
|
url(r'^api/group_join/$', JoinGroupAPIView.as_view(), name="group_join_api"),
|
||||||
|
|
||||||
|
|
||||||
url(r'^api/admin/upload_image/$', SimditorImageUploadAPIView.as_view(), name="simditor_upload_image"),
|
url(r'^api/admin/upload_image/$', SimditorImageUploadAPIView.as_view(), name="simditor_upload_image"),
|
||||||
url(r'^api/admin/announcement/$', AnnouncementAdminAPIView.as_view(), name="announcement_admin_api"),
|
url(r'^api/admin/announcement/$', AnnouncementAdminAPIView.as_view(), name="announcement_admin_api"),
|
||||||
@@ -64,17 +62,18 @@ urlpatterns = [
|
|||||||
url(r'^api/admin/group/$', GroupAdminAPIView.as_view(), name="group_admin_api"),
|
url(r'^api/admin/group/$', GroupAdminAPIView.as_view(), name="group_admin_api"),
|
||||||
url(r'^api/admin/group_member/$', GroupMemberAdminAPIView.as_view(), name="group_member_admin_api"),
|
url(r'^api/admin/group_member/$', GroupMemberAdminAPIView.as_view(), name="group_member_admin_api"),
|
||||||
url(r'^api/admin/group/promot_as_admin/$', GroupPrometAdminAPIView.as_view(), name="group_promote_admin_api"),
|
url(r'^api/admin/group/promot_as_admin/$', GroupPrometAdminAPIView.as_view(), name="group_promote_admin_api"),
|
||||||
|
|
||||||
|
|
||||||
url(r'^api/admin/problem/$', ProblemAdminAPIView.as_view(), name="problem_admin_api"),
|
url(r'^api/admin/problem/$', ProblemAdminAPIView.as_view(), name="problem_admin_api"),
|
||||||
url(r'^api/admin/contest_problem/$', ContestProblemAdminAPIView.as_view(), name="contest_problem_admin_api"),
|
url(r'^api/admin/contest_problem/$', ContestProblemAdminAPIView.as_view(), name="contest_problem_admin_api"),
|
||||||
url(r'^api/admin/contest_problem/public/', MakeContestProblemPublicAPIView.as_view(), name="make_contest_problem_public"),
|
url(r'^api/admin/contest_problem/public/', MakeContestProblemPublicAPIView.as_view(),
|
||||||
|
name="make_contest_problem_public"),
|
||||||
url(r'^api/admin/test_case_upload/$', TestCaseUploadAPIView.as_view(), name="test_case_upload_api"),
|
url(r'^api/admin/test_case_upload/$', TestCaseUploadAPIView.as_view(), name="test_case_upload_api"),
|
||||||
url(r'^api/admin/tag/$', ProblemTagAdminAPIView.as_view(), name="problem_tag_admin_api"),
|
url(r'^api/admin/tag/$', ProblemTagAdminAPIView.as_view(), name="problem_tag_admin_api"),
|
||||||
url(r'^api/admin/join_group_request/$', JoinGroupRequestAdminAPIView.as_view(),
|
url(r'^api/admin/join_group_request/$', JoinGroupRequestAdminAPIView.as_view(),
|
||||||
name="join_group_request_admin_api"),
|
name="join_group_request_admin_api"),
|
||||||
url(r'^api/admin/submission/$', SubmissionAdminAPIView.as_view(), name="submission_admin_api_view"),
|
url(r'^api/admin/submission/$', SubmissionAdminAPIView.as_view(), name="submission_admin_api_view"),
|
||||||
url(r'^api/admin/monitor/$', QueueLengthMonitorAPIView.as_view(), name="queue_length_monitor_api"),
|
|
||||||
|
url(r'^api/admin/judges/$', AdminJudgeServerAPIView.as_view(), name="judges_admin_api"),
|
||||||
|
|
||||||
url(r'^contest/(?P<contest_id>\d+)/problem/(?P<contest_problem_id>\d+)/$', "contest.views.contest_problem_page",
|
url(r'^contest/(?P<contest_id>\d+)/problem/(?P<contest_problem_id>\d+)/$', "contest.views.contest_problem_page",
|
||||||
name="contest_problem_page"),
|
name="contest_problem_page"),
|
||||||
@@ -93,14 +92,12 @@ urlpatterns = [
|
|||||||
url(r'^contests/$', "contest.views.contest_list_page", name="contest_list_page"),
|
url(r'^contests/$', "contest.views.contest_list_page", name="contest_list_page"),
|
||||||
url(r'^contests/(?P<page>\d+)/$', "contest.views.contest_list_page", name="contest_list_page"),
|
url(r'^contests/(?P<page>\d+)/$', "contest.views.contest_list_page", name="contest_list_page"),
|
||||||
|
|
||||||
|
|
||||||
url(r'^problem/(?P<problem_id>\d+)/$', "problem.views.problem_page", name="problem_page"),
|
url(r'^problem/(?P<problem_id>\d+)/$', "problem.views.problem_page", name="problem_page"),
|
||||||
url(r'^problems/$', "problem.views.problem_list_page", name="problem_list_page"),
|
url(r'^problems/$', "problem.views.problem_list_page", name="problem_list_page"),
|
||||||
url(r'^problems/(?P<page>\d+)/$', "problem.views.problem_list_page", name="problem_list_page"),
|
url(r'^problems/(?P<page>\d+)/$', "problem.views.problem_list_page", name="problem_list_page"),
|
||||||
url(r'^problem/(?P<problem_id>\d+)/submissions/$', "submission.views.problem_my_submissions_list_page",
|
url(r'^problem/(?P<problem_id>\d+)/submissions/$', "submission.views.problem_my_submissions_list_page",
|
||||||
name="problem_my_submissions_page"),
|
name="problem_my_submissions_page"),
|
||||||
|
|
||||||
|
|
||||||
url(r'^submission/(?P<submission_id>\w+)/$', "submission.views.my_submission", name="my_submission_page"),
|
url(r'^submission/(?P<submission_id>\w+)/$', "submission.views.my_submission", name="my_submission_page"),
|
||||||
url(r'^submissions/$', "submission.views.my_submission_list_page", name="my_submission_list_page"),
|
url(r'^submissions/$', "submission.views.my_submission_list_page", name="my_submission_list_page"),
|
||||||
url(r'^submissions/(?P<page>\d+)/$', "submission.views.my_submission_list_page", name="my_submission_list_page"),
|
url(r'^submissions/(?P<page>\d+)/$', "submission.views.my_submission_list_page", name="my_submission_list_page"),
|
||||||
@@ -127,14 +124,18 @@ urlpatterns = [
|
|||||||
|
|
||||||
url(r'^api/apply_reset_password/$', ApplyResetPasswordAPIView.as_view(), name="apply_reset_password_api"),
|
url(r'^api/apply_reset_password/$', ApplyResetPasswordAPIView.as_view(), name="apply_reset_password_api"),
|
||||||
url(r'^api/reset_password/$', ResetPasswordAPIView.as_view(), name="apply_reset_password_api"),
|
url(r'^api/reset_password/$', ResetPasswordAPIView.as_view(), name="apply_reset_password_api"),
|
||||||
url(r'^account/settings/$', TemplateView.as_view(template_name="oj/account/settings.html"), name="account_setting_page"),
|
url(r'^account/settings/$', TemplateView.as_view(template_name="oj/account/settings.html"),
|
||||||
url(r'^account/settings/avatar/$', TemplateView.as_view(template_name="oj/account/avatar.html"), name="avatar_settings_page"),
|
name="account_setting_page"),
|
||||||
|
url(r'^account/settings/avatar/$', TemplateView.as_view(template_name="oj/account/avatar.html"),
|
||||||
|
name="avatar_settings_page"),
|
||||||
url(r'^account/sso/$', SSOAPIView.as_view(), name="sso_api"),
|
url(r'^account/sso/$', SSOAPIView.as_view(), name="sso_api"),
|
||||||
url(r'^api/account/userprofile/$', UserProfileAPIView.as_view(), name="userprofile_api"),
|
url(r'^api/account/userprofile/$', UserProfileAPIView.as_view(), name="userprofile_api"),
|
||||||
url(r'^reset_password/$', TemplateView.as_view(template_name="oj/account/apply_reset_password.html"), name="apply_reset_password_page"),
|
url(r'^reset_password/$', TemplateView.as_view(template_name="oj/account/apply_reset_password.html"), name="apply_reset_password_page"),
|
||||||
url(r'^reset_password/t/(?P<token>\w+)/$', "account.views.reset_password_page", name="reset_password_page")
|
url(r'^reset_password/t/(?P<token>\w+)/$', "account.views.reset_password_page", name="reset_password_page"),
|
||||||
|
url(r'^api/two_factor_auth/$', TwoFactorAuthAPIView.as_view(), name="two_factor_auth_api"),
|
||||||
|
url(r'^two_factor_auth/$', TemplateView.as_view(template_name="oj/account/two_factor_auth.html"), name="two_factor_auth_page"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns.append(url(r'^docs/', include('rest_framework_swagger.urls')))
|
urlpatterns.append(url(r'^docs/', include('rest_framework_swagger.urls')))
|
||||||
|
|||||||
@@ -118,3 +118,13 @@ li.problem-tag {
|
|||||||
padding-top: 7.5px;
|
padding-top: 7.5px;
|
||||||
padding-bottom: 7.5px;
|
padding-bottom: 7.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#tfa-qrcode{
|
||||||
|
height: 40%;
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tfa-area{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -22,46 +22,48 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "bootstrap"], function ($,
|
|||||||
}
|
}
|
||||||
|
|
||||||
var superAdminNav = [
|
var superAdminNav = [
|
||||||
{ name: "首页",
|
{
|
||||||
children: [{name: "主页", hash: "#index/index"},
|
name: "首页",
|
||||||
{name: "监控", hash: "#monitor/monitor"}]
|
children: [{name: "主页", hash: "#index/index"},
|
||||||
},
|
{name: "判题服务器", hash: "#judges/judges"}]
|
||||||
{
|
},
|
||||||
name: "通用",
|
{
|
||||||
children: [{name: "公告管理", hash: "#announcement/announcement"},
|
name: "通用",
|
||||||
{name: "用户管理", hash: "#user/user_list"}]
|
children: [{name: "公告管理", hash: "#announcement/announcement"},
|
||||||
},
|
{name: "用户管理", hash: "#user/user_list"}]
|
||||||
{
|
},
|
||||||
name: "题目管理",
|
{
|
||||||
children: [{name: "题目列表", hash: "#problem/problem_list"},
|
name: "题目管理",
|
||||||
{name: "创建题目", hash: "#problem/add_problem"}]
|
children: [{name: "题目列表", hash: "#problem/problem_list"},
|
||||||
},
|
{name: "创建题目", hash: "#problem/add_problem"}]
|
||||||
{
|
},
|
||||||
name: "比赛管理",
|
{
|
||||||
children: [{name: "比赛列表", hash: "#contest/contest_list"},
|
name: "比赛管理",
|
||||||
{name: "创建比赛", hash: "#contest/add_contest"}]
|
children: [{name: "比赛列表", hash: "#contest/contest_list"},
|
||||||
},
|
{name: "创建比赛", hash: "#contest/add_contest"}]
|
||||||
{
|
},
|
||||||
name: "小组管理",
|
{
|
||||||
children: [{name: "小组列表", hash: "#group/group"},
|
name: "小组管理",
|
||||||
{name: "加入小组请求", hash: "#group/join_group_request_list"}]
|
children: [{name: "小组列表", hash: "#group/group"},
|
||||||
}
|
{name: "加入小组请求", hash: "#group/join_group_request_list"}]
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
var adminNav = [
|
var adminNav = [
|
||||||
{ name: "首页",
|
{
|
||||||
children: [{name: "主页", hash: "#index/index"}]
|
name: "首页",
|
||||||
},
|
children: [{name: "主页", hash: "#index/index"}]
|
||||||
{
|
},
|
||||||
name: "比赛管理",
|
{
|
||||||
children: [{name: "比赛列表", hash: "#contest/contest_list"},
|
name: "比赛管理",
|
||||||
{name: "创建比赛", hash: "#contest/add_contest"}]
|
children: [{name: "比赛列表", hash: "#contest/contest_list"},
|
||||||
},
|
{name: "创建比赛", hash: "#contest/add_contest"}]
|
||||||
{
|
},
|
||||||
name: "小组管理",
|
{
|
||||||
children: [{name: "小组列表", hash: "#group/group"},
|
name: "小组管理",
|
||||||
{name: "加入小组请求", hash: "#group/join_group_request_list"}]
|
children: [{name: "小组列表", hash: "#group/group"},
|
||||||
}
|
{name: "加入小组请求", hash: "#group/join_group_request_list"}]
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
var vm = avalon.define({
|
var vm = avalon.define({
|
||||||
@@ -79,7 +81,7 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "bootstrap"], function ($,
|
|||||||
hide_loading: function () {
|
hide_loading: function () {
|
||||||
$("#loading-gif").hide();
|
$("#loading-gif").hide();
|
||||||
},
|
},
|
||||||
getLiId: function(hash){
|
getLiId: function (hash) {
|
||||||
return hash.replace("#", "li-").replace("/", "-");
|
return hash.replace("#", "li-").replace("/", "-");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -89,21 +91,20 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "bootstrap"], function ($,
|
|||||||
url: "/api/user/",
|
url: "/api/user/",
|
||||||
method: "get",
|
method: "get",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
success: function(data){
|
success: function (data) {
|
||||||
if(!data.code){
|
if (!data.code) {
|
||||||
vm.username = data.data.username;
|
vm.username = data.data.username;
|
||||||
vm.adminType = data.data.admin_type;
|
vm.adminType = data.data.admin_type;
|
||||||
if (data.data.admin_type == 2){
|
if (data.data.admin_type == 2) {
|
||||||
vm.adminNavList = superAdminNav;
|
vm.adminNavList = superAdminNav;
|
||||||
}
|
}
|
||||||
else{
|
else {
|
||||||
vm.adminNavList = adminNav;
|
vm.adminNavList = adminNav;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
avalon.scan();
|
avalon.scan();
|
||||||
|
|
||||||
@@ -115,12 +116,14 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "bootstrap"], function ($,
|
|||||||
show_template("template/" + hash + ".html");
|
show_template("template/" + hash + ".html");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
setTimeout(function(){li_active("#li-" + hash.replace("/", "-"));}, 500);
|
setTimeout(function () {
|
||||||
|
li_active("#li-" + hash.replace("/", "-"));
|
||||||
|
}, 500);
|
||||||
|
|
||||||
$.ajaxSetup({
|
$.ajaxSetup({
|
||||||
beforeSend: csrfTokenHeader,
|
beforeSend: csrfTokenHeader,
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
error: function(){
|
error: function () {
|
||||||
bsAlert("请求失败");
|
bsAlert("请求失败");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
143
static/src/js/app/admin/judges/judges.js
Normal file
143
static/src/js/app/admin/judges/judges.js
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
require(["jquery", "avalon", "csrfToken", "bsAlert", "validator", "pager"],
|
||||||
|
function ($, avalon, csrfTokenHeader, bsAlert, editor) {
|
||||||
|
avalon.ready(function () {
|
||||||
|
|
||||||
|
if (avalon.vmodels.judges) {
|
||||||
|
var vm = avalon.vmodels.judges;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var vm = avalon.define({
|
||||||
|
$id: "judges",
|
||||||
|
judgesList: [],
|
||||||
|
isEditing: false,
|
||||||
|
showEnableOnly: false,
|
||||||
|
|
||||||
|
//编辑器同步变量
|
||||||
|
max_instance_number: 0,
|
||||||
|
ipAddress: "",
|
||||||
|
port: 0,
|
||||||
|
status: true,
|
||||||
|
judgesId: -1,
|
||||||
|
name: "",
|
||||||
|
token: "",
|
||||||
|
id: 0,
|
||||||
|
pager: {
|
||||||
|
getPage: function (page) {
|
||||||
|
getPage(page);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
editJudges: function (judges) {
|
||||||
|
vm.id = judges.id;
|
||||||
|
vm.name = judges.name;
|
||||||
|
vm.judgesId = judges.id;
|
||||||
|
vm.status = judges.status;
|
||||||
|
vm.port = judges.port;
|
||||||
|
vm.ipAddress = judges.ip;
|
||||||
|
vm.max_instance_number = judges.max_instance_number;
|
||||||
|
vm.token = judges.token;
|
||||||
|
vm.isEditing = true;
|
||||||
|
},
|
||||||
|
cancelEdit: function () {
|
||||||
|
vm.isEditing = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
vm.$watch("showEnableOnly", function () {
|
||||||
|
getPage(1);
|
||||||
|
avalon.vmodels.judgesPager.currentPage = 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPage(page) {
|
||||||
|
var url = "/api/admin/judges/?paging=true&page=" + page + "&page_size=20";
|
||||||
|
if (vm.showEnableNnly)
|
||||||
|
url += "&status=true";
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
method: "get",
|
||||||
|
success: function (data) {
|
||||||
|
if (!data.code) {
|
||||||
|
vm.judgesList = data.data.results;
|
||||||
|
avalon.vmodels.judgesPager.totalPage = data.data.total_page;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
bsAlert(data.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#judges-form").validator().on('submit', function (e) {
|
||||||
|
if (!e.isDefaultPrevented()) {
|
||||||
|
var name = $("#name").val();
|
||||||
|
var max_instance_number = $("#max_instance_number").val();
|
||||||
|
var ip = $("#ipAddress").val();
|
||||||
|
var port = $("#port").val();
|
||||||
|
var token = $("#token").val();
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/admin/judges/",
|
||||||
|
contentType: "application/json",
|
||||||
|
data: JSON.stringify({
|
||||||
|
name: name,
|
||||||
|
ip: ip,
|
||||||
|
port: port,
|
||||||
|
token: token,
|
||||||
|
max_instance_number: max_instance_number
|
||||||
|
}),
|
||||||
|
dataType: "json",
|
||||||
|
method: "post",
|
||||||
|
success: function (data) {
|
||||||
|
if (!data.code) {
|
||||||
|
bsAlert("提交成功!");
|
||||||
|
$("#name").val("");
|
||||||
|
$("#max_instance_number").val("");
|
||||||
|
$("#ipAddress").val("");
|
||||||
|
$("#port").val("");
|
||||||
|
$("#token").val("");
|
||||||
|
getPage(1);
|
||||||
|
} else {
|
||||||
|
bsAlert(data.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#edit-judges-form").validator().on('submit', function (e) {
|
||||||
|
if (!e.isDefaultPrevented()) {
|
||||||
|
var name = vm.name;
|
||||||
|
var max_instance_number = vm.max_instance_number;
|
||||||
|
var ip = vm.ipAddress;
|
||||||
|
var port = vm.port;
|
||||||
|
var token = vm.token;
|
||||||
|
var status = vm.status;
|
||||||
|
var id = vm.id;
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/admin/judges/",
|
||||||
|
contentType: "application/json",
|
||||||
|
data: JSON.stringify({
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
ip: ip,
|
||||||
|
port: port,
|
||||||
|
token: token,
|
||||||
|
max_instance_number: max_instance_number,
|
||||||
|
status: status
|
||||||
|
}),
|
||||||
|
dataType: "json",
|
||||||
|
method: "put",
|
||||||
|
success: function (data) {
|
||||||
|
if (!data.code) {
|
||||||
|
bsAlert("提交成功!");
|
||||||
|
getPage(1);
|
||||||
|
} else {
|
||||||
|
bsAlert(data.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
avalon.scan();
|
||||||
|
});
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
require(["jquery", "chart"], function ($, Chart) {
|
|
||||||
var data = {
|
|
||||||
labels: ["初始化"],
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: "队列长度",
|
|
||||||
fillColor: "rgba(255,255,255,0.2)",
|
|
||||||
strokeColor: "rgba(151,187,205,1)",
|
|
||||||
pointColor: "rgba(151,187,205,1)",
|
|
||||||
pointStrokeColor: "#fff",
|
|
||||||
pointHighlightFill: "#fff",
|
|
||||||
pointHighlightStroke: "rgba(151,187,205,1)",
|
|
||||||
data: [0]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
var chart = new Chart($("#waiting-queue-chart").get(0).getContext("2d")).Line(data);
|
|
||||||
|
|
||||||
var dataCounter = 0;
|
|
||||||
|
|
||||||
function getMonitorData(){
|
|
||||||
var hash = location.hash;
|
|
||||||
if (hash != "#monitor/monitor"){
|
|
||||||
clearInterval(intervalId);
|
|
||||||
}
|
|
||||||
$.ajax({
|
|
||||||
url: "/api/admin/monitor/",
|
|
||||||
method: "get",
|
|
||||||
dataType: "json",
|
|
||||||
success: function(data){
|
|
||||||
if(!data.code){
|
|
||||||
chart.addData([data.data["count"]], data.data["time"])
|
|
||||||
dataCounter ++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#clear-chart-data").click(function(){
|
|
||||||
for(var i = 0;i < dataCounter;i++) {
|
|
||||||
chart.removeData();
|
|
||||||
}
|
|
||||||
dataCounter = 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
var intervalId = setInterval(getMonitorData, 3000);
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -4,23 +4,31 @@ require(["jquery", "bsAlert", "csrfToken", "validator"], function ($, bsAlert, c
|
|||||||
if (!e.isDefaultPrevented()) {
|
if (!e.isDefaultPrevented()) {
|
||||||
var username = $("#username").val();
|
var username = $("#username").val();
|
||||||
var password = $("#password").val();
|
var password = $("#password").val();
|
||||||
var captcha = $("#captcha").val();
|
var tfaCode = $("#tfa-code").val();
|
||||||
|
console.log(tfaCode);
|
||||||
|
if(tfaCode.length && tfaCode.length != 6){
|
||||||
|
bsAlert("验证码为六位数字");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
beforeSend: csrfTokenHeader,
|
beforeSend: csrfTokenHeader,
|
||||||
url: "/api/login/",
|
url: "/api/login/",
|
||||||
data: {username: username, password: password, captcha: captcha},
|
data: {username: username, password: password, tfa_code: tfaCode},
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
method: "post",
|
method: "post",
|
||||||
success: function (data) {
|
success: function (data) {
|
||||||
if (!data.code) {
|
if (!data.code) {
|
||||||
|
if(data.data == "tfa_required"){
|
||||||
|
$("#tfa-area").show();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
function getLocationVal(id){
|
function getLocationVal(id){
|
||||||
var temp = unescape(location.search).split(id+"=")[1] || "";
|
var temp = unescape(location.search).split(id+"=")[1] || "";
|
||||||
return temp.indexOf("&")>=0 ? temp.split("&")[0] : temp;
|
return temp.indexOf("&")>=0 ? temp.split("&")[0] : temp;
|
||||||
}
|
}
|
||||||
var from = getLocationVal("__from");
|
var from = getLocationVal("__from");
|
||||||
if(from != ""){
|
if(from != ""){
|
||||||
console.log(from);
|
|
||||||
window.location.href = from;
|
window.location.href = from;
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
@@ -28,7 +36,6 @@ require(["jquery", "bsAlert", "csrfToken", "validator"], function ($, bsAlert, c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
refresh_captcha();
|
|
||||||
bsAlert(data.data);
|
bsAlert(data.data);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -40,11 +47,5 @@ require(["jquery", "bsAlert", "csrfToken", "validator"], function ($, bsAlert, c
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
function refresh_captcha(){
|
|
||||||
$("#captcha-img")[0].src = "/captcha/?" + Math.random();
|
|
||||||
$("#captcha")[0].value = "";
|
|
||||||
}
|
|
||||||
$("#captcha-img").click(function(){
|
|
||||||
refresh_captcha();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
27
static/src/js/app/oj/account/twoFactorAuth.js
Normal file
27
static/src/js/app/oj/account/twoFactorAuth.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
require(["jquery", "bsAlert", "csrfToken"], function ($, bsAlert, csrfTokenHeader) {
|
||||||
|
$("#tfa_submit").click(function(){
|
||||||
|
var code = $("#tfa_code").val();
|
||||||
|
if (code.length != 6){
|
||||||
|
bsAlert("验证码是6位数字");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
beforeSend: csrfTokenHeader,
|
||||||
|
url: "/api/two_factor_auth/",
|
||||||
|
data: {code: code},
|
||||||
|
dataType: "json",
|
||||||
|
method: "post",
|
||||||
|
success: function(data){
|
||||||
|
if(data.code){
|
||||||
|
bsAlert(data.data);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
bsAlert("两步验证开启成功");
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
@@ -54,31 +54,31 @@
|
|||||||
//以下都是页面 script 标签引用的js
|
//以下都是页面 script 标签引用的js
|
||||||
announcement_0_pack: "app/admin/announcement/announcement",
|
announcement_0_pack: "app/admin/announcement/announcement",
|
||||||
userList_1_pack: "app/admin/user/userList",
|
userList_1_pack: "app/admin/user/userList",
|
||||||
problem_2_pack: "app/oj/problem/problem",
|
twoFactorAuth_2_pack: "app/oj/account/twoFactorAuth",
|
||||||
submissionList_3_pack: "app/admin/problem/submissionList",
|
problem_3_pack: "app/oj/problem/problem",
|
||||||
contestCountdown_4_pack: "app/oj/contest/contestCountdown",
|
submissionList_4_pack: "app/admin/problem/submissionList",
|
||||||
avatar_5_pack: "app/oj/account/avatar",
|
contestCountdown_5_pack: "app/oj/contest/contestCountdown",
|
||||||
addProblem_6_pack: "app/admin/problem/addProblem",
|
avatar_6_pack: "app/oj/account/avatar",
|
||||||
problem_7_pack: "app/admin/problem/problem",
|
addProblem_7_pack: "app/admin/problem/addProblem",
|
||||||
contestList_8_pack: "app/admin/contest/contestList",
|
problem_8_pack: "app/admin/problem/problem",
|
||||||
admin_9_pack: "app/admin/admin",
|
contestList_9_pack: "app/admin/contest/contestList",
|
||||||
login_10_pack: "app/oj/account/login",
|
admin_10_pack: "app/admin/admin",
|
||||||
applyResetPassword_11_pack: "app/oj/account/applyResetPassword",
|
login_11_pack: "app/oj/account/login",
|
||||||
addContest_12_pack: "app/admin/contest/addContest",
|
applyResetPassword_12_pack: "app/oj/account/applyResetPassword",
|
||||||
contestPassword_13_pack: "app/oj/contest/contestPassword",
|
addContest_13_pack: "app/admin/contest/addContest",
|
||||||
changePassword_14_pack: "app/oj/account/changePassword",
|
contestPassword_14_pack: "app/oj/contest/contestPassword",
|
||||||
monitor_15_pack: "app/admin/monitor/monitor",
|
changePassword_15_pack: "app/oj/account/changePassword",
|
||||||
editProblem_16_pack: "app/admin/contest/editProblem",
|
editProblem_17_pack: "app/admin/contest/editProblem",
|
||||||
joinGroupRequestList_17_pack: "app/admin/group/joinGroupRequestList",
|
joinGroupRequestList_18_pack: "app/admin/group/joinGroupRequestList",
|
||||||
group_18_pack: "app/oj/group/group",
|
group_19_pack: "app/oj/group/group",
|
||||||
contestProblemList_19_pack: "app/admin/contest/contestProblemList",
|
contestProblemList_20_pack: "app/admin/contest/contestProblemList",
|
||||||
editProblem_20_pack: "app/admin/problem/editProblem",
|
editProblem_21_pack: "app/admin/problem/editProblem",
|
||||||
register_21_pack: "app/oj/account/register",
|
register_22_pack: "app/oj/account/register",
|
||||||
groupDetail_22_pack: "app/admin/group/groupDetail",
|
groupDetail_23_pack: "app/admin/group/groupDetail",
|
||||||
editContest_23_pack: "app/admin/contest/editContest",
|
editContest_24_pack: "app/admin/contest/editContest",
|
||||||
resetPassword_24_pack: "app/oj/account/resetPassword",
|
resetPassword_25_pack: "app/oj/account/resetPassword",
|
||||||
group_25_pack: "app/admin/group/group",
|
group_26_pack: "app/admin/group/group",
|
||||||
settings_26_pack: "app/oj/account/settings"
|
settings_27_pack: "app/oj/account/settings"
|
||||||
},
|
},
|
||||||
shim: {
|
shim: {
|
||||||
avalon: {
|
avalon: {
|
||||||
@@ -96,79 +96,79 @@
|
|||||||
name: "userList_1_pack"
|
name: "userList_1_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "problem_2_pack"
|
name: "twoFactorAuth_2_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "submissionList_3_pack"
|
name: "problem_3_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "contestCountdown_4_pack"
|
name: "submissionList_4_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "avatar_5_pack"
|
name: "contestCountdown_5_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "addProblem_6_pack"
|
name: "avatar_6_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "problem_7_pack"
|
name: "addProblem_7_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "contestList_8_pack"
|
name: "problem_8_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "admin_9_pack"
|
name: "contestList_9_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "login_10_pack"
|
name: "admin_10_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "applyResetPassword_11_pack"
|
name: "login_11_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "addContest_12_pack"
|
name: "applyResetPassword_12_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "contestPassword_13_pack"
|
name: "addContest_13_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "changePassword_14_pack"
|
name: "contestPassword_14_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "monitor_15_pack"
|
name: "changePassword_15_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "editProblem_16_pack"
|
name: "editProblem_17_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "joinGroupRequestList_17_pack"
|
name: "joinGroupRequestList_18_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "group_18_pack"
|
name: "group_19_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "contestProblemList_19_pack"
|
name: "contestProblemList_20_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "editProblem_20_pack"
|
name: "editProblem_21_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "register_21_pack"
|
name: "register_22_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "groupDetail_22_pack"
|
name: "groupDetail_23_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "editContest_23_pack"
|
name: "editContest_24_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "resetPassword_24_pack"
|
name: "resetPassword_25_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "group_25_pack"
|
name: "group_26_pack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "settings_26_pack"
|
name: "settings_27_pack"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
optimizeCss: "standard",
|
optimizeCss: "standard",
|
||||||
|
|||||||
@@ -56,31 +56,31 @@ var require = {
|
|||||||
//以下都是页面 script 标签引用的js
|
//以下都是页面 script 标签引用的js
|
||||||
announcement_0_pack: "app/admin/announcement/announcement",
|
announcement_0_pack: "app/admin/announcement/announcement",
|
||||||
userList_1_pack: "app/admin/user/userList",
|
userList_1_pack: "app/admin/user/userList",
|
||||||
problem_2_pack: "app/oj/problem/problem",
|
twoFactorAuth_2_pack: "app/oj/account/twoFactorAuth",
|
||||||
submissionList_3_pack: "app/admin/problem/submissionList",
|
problem_3_pack: "app/oj/problem/problem",
|
||||||
contestCountdown_4_pack: "app/oj/contest/contestCountdown",
|
submissionList_4_pack: "app/admin/problem/submissionList",
|
||||||
avatar_5_pack: "app/oj/account/avatar",
|
contestCountdown_5_pack: "app/oj/contest/contestCountdown",
|
||||||
addProblem_6_pack: "app/admin/problem/addProblem",
|
avatar_6_pack: "app/oj/account/avatar",
|
||||||
problem_7_pack: "app/admin/problem/problem",
|
addProblem_7_pack: "app/admin/problem/addProblem",
|
||||||
contestList_8_pack: "app/admin/contest/contestList",
|
problem_8_pack: "app/admin/problem/problem",
|
||||||
admin_9_pack: "app/admin/admin",
|
contestList_9_pack: "app/admin/contest/contestList",
|
||||||
login_10_pack: "app/oj/account/login",
|
admin_10_pack: "app/admin/admin",
|
||||||
applyResetPassword_11_pack: "app/oj/account/applyResetPassword",
|
login_11_pack: "app/oj/account/login",
|
||||||
addContest_12_pack: "app/admin/contest/addContest",
|
applyResetPassword_12_pack: "app/oj/account/applyResetPassword",
|
||||||
contestPassword_13_pack: "app/oj/contest/contestPassword",
|
addContest_13_pack: "app/admin/contest/addContest",
|
||||||
changePassword_14_pack: "app/oj/account/changePassword",
|
contestPassword_14_pack: "app/oj/contest/contestPassword",
|
||||||
monitor_15_pack: "app/admin/monitor/monitor",
|
changePassword_15_pack: "app/oj/account/changePassword",
|
||||||
editProblem_16_pack: "app/admin/contest/editProblem",
|
editProblem_17_pack: "app/admin/contest/editProblem",
|
||||||
joinGroupRequestList_17_pack: "app/admin/group/joinGroupRequestList",
|
joinGroupRequestList_18_pack: "app/admin/group/joinGroupRequestList",
|
||||||
group_18_pack: "app/oj/group/group",
|
group_19_pack: "app/oj/group/group",
|
||||||
contestProblemList_19_pack: "app/admin/contest/contestProblemList",
|
contestProblemList_20_pack: "app/admin/contest/contestProblemList",
|
||||||
editProblem_20_pack: "app/admin/problem/editProblem",
|
editProblem_21_pack: "app/admin/problem/editProblem",
|
||||||
register_21_pack: "app/oj/account/register",
|
register_22_pack: "app/oj/account/register",
|
||||||
groupDetail_22_pack: "app/admin/group/groupDetail",
|
groupDetail_23_pack: "app/admin/group/groupDetail",
|
||||||
editContest_23_pack: "app/admin/contest/editContest",
|
editContest_24_pack: "app/admin/contest/editContest",
|
||||||
resetPassword_24_pack: "app/oj/account/resetPassword",
|
resetPassword_25_pack: "app/oj/account/resetPassword",
|
||||||
group_25_pack: "app/admin/group/group",
|
group_26_pack: "app/admin/group/group",
|
||||||
settings_26_pack: "app/oj/account/settings",
|
settings_27_pack: "app/oj/account/settings"
|
||||||
},
|
},
|
||||||
shim: {
|
shim: {
|
||||||
avalon: {
|
avalon: {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
from huey.djhuey import task
|
from __future__ import absolute_import
|
||||||
|
from celery import shared_task
|
||||||
from judge_dispatcher.tasks import JudgeDispatcher
|
from judge_dispatcher.tasks import JudgeDispatcher
|
||||||
|
|
||||||
|
|
||||||
@task()
|
@shared_task
|
||||||
def _judge(submission, time_limit, memory_limit, test_case_id, is_waiting_task=False):
|
def _judge(submission, time_limit, memory_limit, test_case_id):
|
||||||
JudgeDispatcher(submission, time_limit, memory_limit, test_case_id).judge(is_waiting_task)
|
JudgeDispatcher(submission, time_limit, memory_limit, test_case_id).judge()
|
||||||
@@ -43,7 +43,7 @@ class SubmissionAPIView(APIView):
|
|||||||
problem_id=problem.id)
|
problem_id=problem.id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_judge(submission, problem.time_limit, problem.memory_limit, problem.test_case_id)
|
_judge.delay(submission, problem.time_limit, problem.memory_limit, problem.test_case_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
return error_response(u"提交判题任务失败")
|
return error_response(u"提交判题任务失败")
|
||||||
@@ -88,7 +88,7 @@ class ContestSubmissionAPIView(APIView):
|
|||||||
code=data["code"],
|
code=data["code"],
|
||||||
problem_id=problem.id)
|
problem_id=problem.id)
|
||||||
try:
|
try:
|
||||||
_judge(submission, problem.time_limit, problem.memory_limit, problem.test_case_id)
|
_judge.delay(submission, problem.time_limit, problem.memory_limit, problem.test_case_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
return error_response(u"提交判题任务失败")
|
return error_response(u"提交判题任务失败")
|
||||||
@@ -273,7 +273,7 @@ class SubmissionRejudgeAdminAPIView(APIView):
|
|||||||
except Problem.DoesNotExist:
|
except Problem.DoesNotExist:
|
||||||
return error_response(u"题目不存在")
|
return error_response(u"题目不存在")
|
||||||
try:
|
try:
|
||||||
_judge(submission, problem.time_limit, problem.memory_limit, problem.test_case_id)
|
_judge.delay(submission, problem.time_limit, problem.memory_limit, problem.test_case_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
return error_response(u"提交判题任务失败")
|
return error_response(u"提交判题任务失败")
|
||||||
|
|||||||
131
template/src/admin/judges/judges.html
Normal file
131
template/src/admin/judges/judges.html
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<div ms-controller="judges" class="col-md-9">
|
||||||
|
<h1>判题服务器管理</h1>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<tr>
|
||||||
|
<th>编号</th>
|
||||||
|
<th>名字</th>
|
||||||
|
<th>最大实例数量</th>
|
||||||
|
<th>负载</th>
|
||||||
|
<th>创建时间</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
<tr ms-repeat="judgesList">
|
||||||
|
<td>{{ el.id }}</td>
|
||||||
|
<td>{{ el.name }}</td>
|
||||||
|
<td>{{ el.max_instance_number }}</td>
|
||||||
|
<td>{{ el.workload }}</td>
|
||||||
|
<td>{{ el.create_time|date("yyyy-MM-dd HH:mm:ss")}}</td>
|
||||||
|
<td ms-text="el.status?'启用':'停用'"></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn-sm btn-info" ms-click="editJudges(el)">编辑</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>仅显示启用 <input ms-duplex-checked="showEnableOnly" type="checkbox"/></label>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<ms:pager $id="judgesPager" config="pager"></ms:pager>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ms-visible="isEditing">
|
||||||
|
<h3>编辑判题服务器</h3>
|
||||||
|
|
||||||
|
<form id="edit-judges-form">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>名字</label>
|
||||||
|
<input name="title" type="text" class="form-control" placeholder="名字" maxlength="30" ms-duplex="name" required data-error="请填写合法的服务器名称">
|
||||||
|
<div class="help-block with-errors"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>口令</label>
|
||||||
|
<input name="title" type="text" class="form-control" ms-duplex="token" placeholder="口令" maxlength="30" required data-error="请填写合法的口令">
|
||||||
|
<div class="help-block with-errors"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>IP</label>
|
||||||
|
<input name="ip" type="text" class="form-control" ms-duplex="ipAddress" placeholder="IP" required data-error="请填写合法的IP地址">
|
||||||
|
<div class="help-block with-errors"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>端口</label>
|
||||||
|
<input name="port" type="number" class="form-control" ms-duplex="port" placeholder="端口" required data-error="请填写合法的端口号">
|
||||||
|
<div class="help-block with-errors"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>最大实例数量</label>
|
||||||
|
<input type="number" class="form-control" placeholder="最大实例数量" ms-duplex="max_instance_number" required data-error="请填写合法的最大实例数量">
|
||||||
|
<div class="help-block with-errors"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>启用 <input ms-duplex-checked="status" type="checkbox"/></label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<button class="btn btn-success">保存修改</button>
|
||||||
|
|
||||||
|
<a ms-click="cancelEdit()" class="btn btn-danger">取消</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>添加判题服务器</h3>
|
||||||
|
|
||||||
|
<form id="judges-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>名字</label>
|
||||||
|
<input name="title" type="text" class="form-control" id="name" placeholder="名字" maxlength="30" required data-error="请填写合法的服务器名称">
|
||||||
|
<div class="help-block with-errors"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>口令</label>
|
||||||
|
<input name="title" type="text" class="form-control" id="token" placeholder="口令" maxlength="30" required data-error="请填写合法的口令">
|
||||||
|
<div class="help-block with-errors"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>IP</label>
|
||||||
|
<input name="ip" type="text" class="form-control" id="ipAddress" placeholder="IP" required data-error="请填写合法的IP地址">
|
||||||
|
<div class="help-block with-errors"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>端口</label>
|
||||||
|
<input name="port" type="number" class="form-control" id="port" placeholder="端口" required data-error="请填写合法的端口号">
|
||||||
|
<div class="help-block with-errors"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>最大实例数量</label>
|
||||||
|
<input type="number" class="form-control" placeholder="最大实例数量" id="max_instance_number" required data-error="请填写合法的最大实例数量">
|
||||||
|
<div class="help-block with-errors"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-success">添加</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<script src="/static/js/app/admin/judges/judges.js"></script>
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
<li class="list-group-header">通用设置</li>
|
<li class="list-group-header">通用设置</li>
|
||||||
<li class="list-group-item"><a href="/account/settings/">个人信息</a></li>
|
<li class="list-group-item"><a href="/account/settings/">个人信息</a></li>
|
||||||
<li class="list-group-item active"><a href="/account/settings/avatar/">更换头像</a></li>
|
<li class="list-group-item active"><a href="/account/settings/avatar/">更换头像</a></li>
|
||||||
|
<li class="list-group-item"><a href="/two_factor_auth/">两步验证</a></li>
|
||||||
<li class="list-group-item"><a href="/change_password/">修改密码</a></li>
|
<li class="list-group-item"><a href="/change_password/">修改密码</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<li class="list-group-header">通用设置</li>
|
<li class="list-group-header">通用设置</li>
|
||||||
<li class="list-group-item"><a href="/account/settings/">个人信息</a></li>
|
<li class="list-group-item"><a href="/account/settings/">个人信息</a></li>
|
||||||
<li class="list-group-item"><a href="/account/settings/avatar/">更换头像</a></li>
|
<li class="list-group-item"><a href="/account/settings/avatar/">更换头像</a></li>
|
||||||
|
<li class="list-group-item"><a href="/two_factor_auth/">两步验证</a></li>
|
||||||
<li class="list-group-item active"><a href="/change_password/">修改密码</a></li>
|
<li class="list-group-item active"><a href="/change_password/">修改密码</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,22 +11,21 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="username">用户名</label>
|
<label for="username">用户名</label>
|
||||||
<input type="text" class="form-control input-lg" id="username" name="username" maxlength="30"
|
<input type="text" class="form-control input-lg" id="username" name="username" maxlength="30"
|
||||||
data-error="请填写用户名" placeholder="用户名" autofocus required>
|
data-error="请填写用户名" placeholder="用户名" autofocus required autocomplete="off">
|
||||||
|
|
||||||
<div class="help-block with-errors"></div>
|
<div class="help-block with-errors"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="password">密码</label>
|
<label for="password">密码</label>
|
||||||
<input type="password" class="form-control input-lg" id="password" name="password" maxlength="30"
|
<input type="password" class="form-control input-lg" id="password" name="password" maxlength="30"
|
||||||
data-error="请填写密码" placeholder="密码" required>
|
data-error="请填写密码" placeholder="密码" required autocomplete="off">
|
||||||
|
|
||||||
<div class="help-block with-errors"></div>
|
<div class="help-block with-errors"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" id="captcha-area">
|
<div class="form-group" id="tfa-area">
|
||||||
<label for="captcha">验证码</label> <img src="/captcha/" id="captcha-img"><small>
|
<label for="captcha">两步验证</label>
|
||||||
<p></p></small>
|
<input type="text" class="form-control input-lg" id="tfa-code" name="tfa-code"
|
||||||
<input type="text" class="form-control input-lg" id="captcha" name="captcha"
|
placeholder="两步验证验证码" maxlength="6" data-error="请填写两步验证验证码" required autocomplete="off">
|
||||||
placeholder="验证码" maxlength="4" data-error="请填写验证码" required>
|
|
||||||
<div class="help-block with-errors"></div>
|
<div class="help-block with-errors"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<li class="list-group-header">通用设置</li>
|
<li class="list-group-header">通用设置</li>
|
||||||
<li class="list-group-item active"><a href="/account/settings/">个人信息</a></li>
|
<li class="list-group-item active"><a href="/account/settings/">个人信息</a></li>
|
||||||
<li class="list-group-item"><a href="/account/settings/avatar/">更换头像</a></li>
|
<li class="list-group-item"><a href="/account/settings/avatar/">更换头像</a></li>
|
||||||
|
<li class="list-group-item"><a href="/two_factor_auth/">两步验证</a></li>
|
||||||
<li class="list-group-item"><a href="/change_password/">修改密码</a></li>
|
<li class="list-group-item"><a href="/change_password/">修改密码</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
41
template/src/oj/account/two_factor_auth.html
Normal file
41
template/src/oj/account/two_factor_auth.html
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{% extends "oj_base.html" %}
|
||||||
|
{% block title %}
|
||||||
|
两步验证
|
||||||
|
{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
<div class="container main">
|
||||||
|
|
||||||
|
<div class="col-lg-2">
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-header">通用设置</li>
|
||||||
|
<li class="list-group-item"><a href="/account/settings/">个人信息</a></li>
|
||||||
|
<li class="list-group-item"><a href="/account/settings/avatar/">更换头像</a></li>
|
||||||
|
<li class="list-group-item active"><a href="/two_factor_auth/">两步验证</a></li>
|
||||||
|
<li class="list-group-item"><a href="/change_password/">修改密码</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-8">
|
||||||
|
{% if not request.user.two_factor_auth %}
|
||||||
|
<h3>扫描二维码开启两步验证</h3>
|
||||||
|
<img src="/api/two_factor_auth/" id="tfa-qrcode">
|
||||||
|
|
||||||
|
<div class="form-inline">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tfa_code">验证码</label>
|
||||||
|
<input type="text" maxlength="6" class="form-control" id="tfa_code"
|
||||||
|
placeholder="输入 app 上显示的验证码">
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-primary" id="tfa_submit">确定</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-success" role="alert">两步验证已经开启</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js_block %}
|
||||||
|
<script src="/static/js/app/oj/account/twoFactorAuth.js"></script>
|
||||||
|
{% endblock %}
|
||||||
208
utils/otp_auth.py
Normal file
208
utils/otp_auth.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
otpauth
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
Implements two-step verification of HOTP/TOTP.
|
||||||
|
|
||||||
|
:copyright: (c) 2013 - 2015 by Hsiaoming Yang.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import hmac
|
||||||
|
import base64
|
||||||
|
import struct
|
||||||
|
import hashlib
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info[0] == 3:
|
||||||
|
PY2 = False
|
||||||
|
string_type = str
|
||||||
|
else:
|
||||||
|
PY2 = True
|
||||||
|
string_type = unicode
|
||||||
|
range = xrange
|
||||||
|
|
||||||
|
|
||||||
|
__author__ = 'Hsiaoming Yang <me@lepture.com>'
|
||||||
|
__homepage__ = 'https://github.com/lepture/otpauth'
|
||||||
|
__version__ = '1.0.1'
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['OtpAuth', 'HOTP', 'TOTP', 'generate_hotp', 'generate_totp']
|
||||||
|
|
||||||
|
|
||||||
|
HOTP = 'hotp'
|
||||||
|
TOTP = 'totp'
|
||||||
|
|
||||||
|
|
||||||
|
class OtpAuth(object):
|
||||||
|
"""One Time Password Authentication.
|
||||||
|
|
||||||
|
:param secret: A secret token for the authentication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, secret):
|
||||||
|
self.secret = secret
|
||||||
|
|
||||||
|
def hotp(self, counter=4):
|
||||||
|
"""Generate a HOTP code.
|
||||||
|
|
||||||
|
:param counter: HOTP is a counter based algorithm.
|
||||||
|
"""
|
||||||
|
return generate_hotp(self.secret, counter)
|
||||||
|
|
||||||
|
def totp(self, period=30, timestamp=None):
|
||||||
|
"""Generate a TOTP code.
|
||||||
|
|
||||||
|
A TOTP code is an extension of HOTP algorithm.
|
||||||
|
|
||||||
|
:param period: A period that a TOTP code is valid in seconds
|
||||||
|
:param timestamp: Create TOTP at this given timestamp
|
||||||
|
"""
|
||||||
|
return generate_totp(self.secret, period, timestamp)
|
||||||
|
|
||||||
|
def valid_hotp(self, code, last=0, trials=100):
|
||||||
|
"""Valid a HOTP code.
|
||||||
|
|
||||||
|
:param code: A number that is less than 6 characters.
|
||||||
|
:param last: Guess HOTP code from last + 1 range.
|
||||||
|
:param trials: Guest HOTP code end at last + trials + 1.
|
||||||
|
"""
|
||||||
|
if not valid_code(code):
|
||||||
|
return False
|
||||||
|
|
||||||
|
code = bytes(int(code))
|
||||||
|
for i in range(last + 1, last + trials + 1):
|
||||||
|
if compare_digest(bytes(self.hotp(counter=i)), code):
|
||||||
|
return i
|
||||||
|
return False
|
||||||
|
|
||||||
|
def valid_totp(self, code, period=30, timestamp=None):
|
||||||
|
"""Valid a TOTP code.
|
||||||
|
|
||||||
|
:param code: A number that is less than 6 characters.
|
||||||
|
:param period: A period that a TOTP code is valid in seconds
|
||||||
|
:param timestamp: Validate TOTP at this given timestamp
|
||||||
|
"""
|
||||||
|
if not valid_code(code):
|
||||||
|
return False
|
||||||
|
return compare_digest(
|
||||||
|
bytes(self.totp(period, timestamp)),
|
||||||
|
bytes(int(code))
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def encoded_secret(self):
|
||||||
|
secret = base64.b32encode(to_bytes(self.secret))
|
||||||
|
# bytes to string
|
||||||
|
secret = secret.decode('utf-8')
|
||||||
|
# remove pad string
|
||||||
|
return secret.strip('=')
|
||||||
|
|
||||||
|
def to_uri(self, type, label, issuer, counter=None):
|
||||||
|
"""Generate the otpauth protocal string.
|
||||||
|
|
||||||
|
:param type: Algorithm type, hotp or totp.
|
||||||
|
:param label: Label of the identifier.
|
||||||
|
:param issuer: The company, the organization or something else.
|
||||||
|
:param counter: Counter of the HOTP algorithm.
|
||||||
|
"""
|
||||||
|
type = type.lower()
|
||||||
|
|
||||||
|
if type not in ('hotp', 'totp'):
|
||||||
|
raise ValueError('type must be hotp or totp')
|
||||||
|
|
||||||
|
if type == 'hotp' and not counter:
|
||||||
|
raise ValueError('HOTP type authentication need counter')
|
||||||
|
|
||||||
|
# https://code.google.com/p/google-authenticator/wiki/KeyUriFormat
|
||||||
|
url = ('otpauth://%(type)s/%(label)s?secret=%(secret)s'
|
||||||
|
'&issuer=%(issuer)s')
|
||||||
|
dct = dict(
|
||||||
|
type=type, label=label, issuer=issuer,
|
||||||
|
secret=self.encoded_secret, counter=counter
|
||||||
|
)
|
||||||
|
ret = url % dct
|
||||||
|
if type == 'hotp':
|
||||||
|
ret = '%s&counter=%s' % (ret, counter)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def to_google(self, type, label, issuer, counter=None):
|
||||||
|
"""Generate the otpauth protocal string for Google Authenticator.
|
||||||
|
|
||||||
|
.. deprecated:: 0.2.0
|
||||||
|
Use :func:`to_uri` instead.
|
||||||
|
"""
|
||||||
|
warnings.warn('deprecated, use to_uri instead', DeprecationWarning)
|
||||||
|
return self.to_uri(type, label, issuer, counter)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_hotp(secret, counter=4):
|
||||||
|
"""Generate a HOTP code.
|
||||||
|
|
||||||
|
:param secret: A secret token for the authentication.
|
||||||
|
:param counter: HOTP is a counter based algorithm.
|
||||||
|
"""
|
||||||
|
# https://tools.ietf.org/html/rfc4226
|
||||||
|
msg = struct.pack('>Q', counter)
|
||||||
|
digest = hmac.new(to_bytes(secret), msg, hashlib.sha1).digest()
|
||||||
|
|
||||||
|
ob = digest[19]
|
||||||
|
if PY2:
|
||||||
|
ob = ord(ob)
|
||||||
|
|
||||||
|
pos = ob & 15
|
||||||
|
base = struct.unpack('>I', digest[pos:pos + 4])[0] & 0x7fffffff
|
||||||
|
token = base % 1000000
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def generate_totp(secret, period=30, timestamp=None):
|
||||||
|
"""Generate a TOTP code.
|
||||||
|
|
||||||
|
A TOTP code is an extension of HOTP algorithm.
|
||||||
|
|
||||||
|
:param secret: A secret token for the authentication.
|
||||||
|
:param period: A period that a TOTP code is valid in seconds
|
||||||
|
:param timestamp: Current time stamp.
|
||||||
|
"""
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = time.time()
|
||||||
|
counter = int(timestamp) // period
|
||||||
|
return generate_hotp(secret, counter)
|
||||||
|
|
||||||
|
|
||||||
|
def to_bytes(text):
|
||||||
|
if isinstance(text, string_type):
|
||||||
|
# Python3 str -> bytes
|
||||||
|
# Python2 unicode -> str
|
||||||
|
text = text.encode('utf-8')
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def valid_code(code):
|
||||||
|
code = string_type(code)
|
||||||
|
return code.isdigit() and len(code) <= 6
|
||||||
|
|
||||||
|
|
||||||
|
def compare_digest(a, b):
|
||||||
|
func = getattr(hmac, 'compare_digest', None)
|
||||||
|
if func:
|
||||||
|
return func(a, b)
|
||||||
|
|
||||||
|
# fallback
|
||||||
|
if len(a) != len(b):
|
||||||
|
return False
|
||||||
|
|
||||||
|
rv = 0
|
||||||
|
if PY2:
|
||||||
|
from itertools import izip
|
||||||
|
for x, y in izip(a, b):
|
||||||
|
rv |= ord(x) ^ ord(y)
|
||||||
|
else:
|
||||||
|
for x, y in zip(a, b):
|
||||||
|
rv |= x ^ y
|
||||||
|
return rv == 0
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import time
|
import time
|
||||||
import random
|
import random
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
@@ -9,6 +10,9 @@ from django.core.paginator import Paginator
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger("app_info")
|
||||||
|
|
||||||
|
|
||||||
def error_page(request, error_reason):
|
def error_page(request, error_reason):
|
||||||
return render(request, "utils/error.html", {"error": error_reason})
|
return render(request, "utils/error.html", {"error": error_reason})
|
||||||
|
|
||||||
@@ -96,7 +100,8 @@ def paginate_data(request, query_set, object_serializer):
|
|||||||
def paginate(request, query_set, object_serializer=None):
|
def paginate(request, query_set, object_serializer=None):
|
||||||
try:
|
try:
|
||||||
data= paginate_data(request, query_set, object_serializer)
|
data= paginate_data(request, query_set, object_serializer)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
logger.error(str(e))
|
||||||
return error_response(u"参数错误")
|
return error_response(u"参数错误")
|
||||||
return success_response(data)
|
return success_response(data)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user