Merge pull request #118 from QingdaoU/zemal_dev

This commit is contained in:
zema1 2018-01-06 20:43:44 +08:00 committed by GitHub
commit ef6679acb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 165 additions and 86 deletions

View File

@ -1,10 +1,7 @@
import functools
from utils.api import JSONResponse
from .models import ProblemPermission
from contest.models import Contest, ContestType, ContestStatus, ContestRuleType
from utils.api import JSONResponse, APIError
from .models import ProblemPermission
class BasePermissionDecorator(object):
@ -90,8 +87,7 @@ def check_contest_permission(check_type="details"):
if not user.is_authenticated():
return self.error("Please login first.")
# password error
if ("accessible_contests" not in request.session) or \
(self.contest.id not in request.session["accessible_contests"]):
if self.contest.id not in request.session.get("accessible_contests", []):
return self.error("Password is required.")
# regular user get contest problems, ranks etc. before contest started
@ -104,7 +100,10 @@ def check_contest_permission(check_type="details"):
return self.error(f"No permission to get {check_type}")
return func(*args, **kwargs)
return _check_permission
return decorator
def ensure_created_by(obj, user):
if not user.is_admin_role() or (user.is_admin() and obj.created_by != user):
raise APIError(msg=f"{obj.__class__.__name__} does not exist")

View File

@ -86,7 +86,7 @@ class UserProfileSerializer(serializers.ModelSerializer):
class EditUserSerializer(serializers.Serializer):
id = serializers.IntegerField()
username = serializers.CharField(max_length=32)
real_name = serializers.CharField(max_length=32, allow_blank=True)
real_name = serializers.CharField(max_length=32, allow_blank=True, allow_null=True)
password = serializers.CharField(min_length=6, allow_blank=True, required=False, default=None)
email = serializers.EmailField(max_length=64)
admin_type = serializers.ChoiceField(choices=(AdminType.REGULAR_USER, AdminType.ADMIN, AdminType.SUPER_ADMIN))

View File

@ -153,3 +153,32 @@ class TestCasePruneAPITest(APITestCase):
resp = self.client.delete(self.url)
self.assertSuccess(resp)
mocked_delete_one.assert_called_once_with(valid_id)
class ReleaseNoteAPITest(APITestCase):
def setUp(self):
self.url = self.reverse("get_release_notes_api")
self.create_super_admin()
self.latest_data = {"update": [
{
"version": "2099-12-25",
"level": 1,
"title": "Update at 2099-12-25",
"details": ["test get", ]
}
]}
def test_get_versions(self):
resp = self.client.get(self.url)
self.assertSuccess(resp)
class DashboardInfoAPITest(APITestCase):
def setUp(self):
self.url = self.reverse("dashboard_info_api")
self.create_admin()
def test_get_info(self):
resp = self.client.get(self.url)
self.assertSuccess(resp)
self.assertEqual(resp.data["data"]["user_count"], 1)

View File

@ -1,6 +1,7 @@
from django.conf.urls import url
from ..views import SMTPAPI, JudgeServerAPI, WebsiteConfigAPI, TestCasePruneAPI, SMTPTestAPI
from ..views import ReleaseNotesAPI, DashboardInfoAPI
urlpatterns = [
url(r"^smtp/?$", SMTPAPI.as_view(), name="smtp_admin_api"),
@ -8,4 +9,6 @@ urlpatterns = [
url(r"^website/?$", WebsiteConfigAPI.as_view(), name="website_config_api"),
url(r"^judge_server/?$", JudgeServerAPI.as_view(), name="judge_server_api"),
url(r"^prune_test_case/?$", TestCasePruneAPI.as_view(), name="prune_test_case_api"),
url(r"^versions/?$", ReleaseNotesAPI.as_view(), name="get_release_notes_api"),
url(r"^dashboard_info", DashboardInfoAPI.as_view(), name="dashboard_info_api"),
]

View File

@ -2,17 +2,25 @@ import os
import re
import hashlib
import shutil
import json
import pytz
import requests
from datetime import datetime
from requests.exceptions import RequestException
from django.utils import timezone
from django.conf import settings
from account.decorators import super_admin_required
from problem.models import Problem
from account.models import User
from submission.models import Submission
from contest.models import Contest
from judge.dispatcher import process_pending_task
from judge.languages import languages, spj_languages
from options.options import SysOptions
from utils.api import APIView, CSRFExemptAPIView, validate_serializer
from utils.shortcuts import send_email
from utils.shortcuts import send_email, get_env
from utils.xss_filter import XSSHtml
from .models import JudgeServer
from .serializers import (CreateEditWebsiteConfigSerializer,
@ -30,14 +38,14 @@ class SMTPAPI(APIView):
smtp.pop("password")
return self.success(smtp)
@validate_serializer(CreateSMTPConfigSerializer)
@super_admin_required
@validate_serializer(CreateSMTPConfigSerializer)
def post(self, request):
SysOptions.smtp_config = request.data
return self.success()
@validate_serializer(EditSMTPConfigSerializer)
@super_admin_required
@validate_serializer(EditSMTPConfigSerializer)
def put(self, request):
smtp = SysOptions.smtp_config
data = request.data
@ -81,8 +89,8 @@ class WebsiteConfigAPI(APIView):
"website_footer", "allow_register", "submission_list_show_all"]}
return self.success(ret)
@validate_serializer(CreateEditWebsiteConfigSerializer)
@super_admin_required
@validate_serializer(CreateEditWebsiteConfigSerializer)
def post(self, request):
for k, v in request.data.items():
if k == "website_footer":
@ -156,7 +164,7 @@ class TestCasePruneAPI(APIView):
@super_admin_required
def get(self, request):
"""
return isolated test_case list
return orphan test_case list
"""
ret_data = []
dir_to_be_removed = self.get_orphan_ids()
@ -190,3 +198,36 @@ class TestCasePruneAPI(APIView):
test_case_dir = os.path.join(settings.TEST_CASE_DIR, id)
if os.path.isdir(test_case_dir):
shutil.rmtree(test_case_dir, ignore_errors=True)
class ReleaseNotesAPI(APIView):
def get(self, request):
try:
resp = requests.get("https://raw.githubusercontent.com/QingdaoU/OnlineJudge/master/docs/data.json",
timeout=3)
releases = resp.json()
except (RequestException, ValueError):
return self.success()
with open("docs/data.json", "r") as f:
local_version = json.load(f)["update"][0]["version"]
releases["local_version"] = local_version
return self.success(releases)
class DashboardInfoAPI(APIView):
def get(self, request):
today = datetime.today()
today_submission_count = Submission.objects.filter(
create_time__gte=datetime(today.year, today.month, today.day, 0, 0, tzinfo=pytz.UTC)).count()
recent_contest_count = Contest.objects.exclude(end_time__lt=timezone.now()).count()
judge_server_count = len(list(filter(lambda x: x.status == "normal", JudgeServer.objects.all())))
return self.success({
"user_count": User.objects.count(),
"recent_contest_count": recent_contest_count,
"today_submission_count": today_submission_count,
"judge_server_count": judge_server_count,
"env": {
"FORCE_HTTPS": get_env("FORCE_HTTPS", default=False),
"STATIC_CDN_HOST": get_env("STATIC_CDN_HOST", default="")
}
})

View File

@ -5,7 +5,7 @@ from utils.api import APIView, validate_serializer
from utils.cache import cache
from utils.constants import CacheKey
from account.decorators import check_contest_permission
from account.decorators import check_contest_permission, ensure_created_by
from ..models import Contest, ContestAnnouncement, ACMContestRank
from ..serializers import (ContestAnnouncementSerializer, ContestAdminSerializer,
CreateConetestSeriaizer, CreateContestAnnouncementSerializer,
@ -37,8 +37,7 @@ class ContestAPI(APIView):
data = request.data
try:
contest = Contest.objects.get(id=data.pop("id"))
if request.user.is_admin() and contest.created_by != request.user:
return self.error("Contest does not exist")
ensure_created_by(contest, request.user)
except Contest.DoesNotExist:
return self.error("Contest does not exist")
data["start_time"] = dateutil.parser.parse(data["start_time"])
@ -66,20 +65,18 @@ class ContestAPI(APIView):
if contest_id:
try:
contest = Contest.objects.get(id=contest_id)
if request.user.is_admin() and contest.created_by != request.user:
return self.error("Contest does not exist")
ensure_created_by(contest, request.user)
return self.success(ContestAdminSerializer(contest).data)
except Contest.DoesNotExist:
return self.error("Contest does not exist")
contests = Contest.objects.all().order_by("-create_time")
if request.user.is_admin():
contests = contests.filter(created_by=request.user)
keyword = request.GET.get("keyword")
if keyword:
contests = contests.filter(title__contains=keyword)
if request.user.is_admin():
contests = contests.filter(created_by=request.user)
return self.success(self.paginate_data(request, contests, ContestAdminSerializer))
@ -92,8 +89,7 @@ class ContestAnnouncementAPI(APIView):
data = request.data
try:
contest = Contest.objects.get(id=data.pop("contest_id"))
if request.user.is_admin() and contest.created_by != request.user:
return self.error("Contest does not exist")
ensure_created_by(contest, request.user)
data["contest"] = contest
data["created_by"] = request.user
except Contest.DoesNotExist:
@ -109,8 +105,7 @@ class ContestAnnouncementAPI(APIView):
data = request.data
try:
contest_announcement = ContestAnnouncement.objects.get(id=data.pop("id"))
if request.user.is_admin() and contest_announcement.created_by != request.user:
return self.error("Contest announcement does not exist")
ensure_created_by(contest_announcement, request.user)
except ContestAnnouncement.DoesNotExist:
return self.error("Contest announcement does not exist")
for k, v in data.items():
@ -139,15 +134,14 @@ class ContestAnnouncementAPI(APIView):
if contest_announcement_id:
try:
contest_announcement = ContestAnnouncement.objects.get(id=contest_announcement_id)
if request.user.is_admin() and contest_announcement.created_by != request.user:
return self.error("Contest announcement does not exist")
ensure_created_by(contest_announcement, request.user)
return self.success(ContestAnnouncementSerializer(contest_announcement).data)
except ContestAnnouncement.DoesNotExist:
return self.error("Contest announcement does not exist")
contest_id = request.GET.get("contest_id")
if not contest_id:
return self.error("Paramater error")
return self.error("Parameter error")
contest_announcements = ContestAnnouncement.objects.filter(contest_id=contest_id)
if request.user.is_admin():
contest_announcements = contest_announcements.filter(created_by=request.user)
@ -177,12 +171,10 @@ class ACMContestHelper(APIView):
results.sort(key=lambda x: -x["ac_info"]["ac_time"])
return self.success(results)
@validate_serializer(ACMContesHelperSerializer)
@check_contest_permission(check_type="ranks")
@validate_serializer(ACMContesHelperSerializer)
def put(self, request):
data = request.data
if not request.user.is_contest_admin(self.contest):
return self.error("You are not contest admin")
try:
rank = ACMContestRank.objects.get(pk=data["rank_id"])
except ACMContestRank.DoesNotExist:

View File

@ -31,10 +31,17 @@ else
ln -sf https_redirect.conf http_locations.conf
fi
if [ ! -z "$LOWER_IP_HEADER" ]; then
sed -i "s/__IP_HEADER__/\$http_$LOWER_IP_HEADER/g" api_proxy.conf;
else
sed -i "s/__IP_HEADER__/\$remote_addr/g" api_proxy.conf;
fi
cd $APP/dist
if [ ! -z "$STATIC_CDN_HOST" ]; then
find . -name index.html -exec sed -i "s/link href=\/static/link href=\/\/$STATIC_CDN_HOST\/static/g" {} \;
find . -name index.html -exec sed -i "s/script type=text\/javascript src=\/static/script type=text\/javascript src=\/\/$STATIC_CDN_HOST\/static/g" {} \;
find . -name index.html -exec sed -i "s/__STATIC_CDN_HOST__/\/\/$STATIC_CDN_HOST/g" {} \;
else
find . -name index.html -exec sed -i "s/__STATIC_CDN_HOST__//g" {} \;
fi
cd $APP
@ -44,13 +51,13 @@ while [ $n -lt 5 ]
do
python manage.py migrate --no-input &&
python manage.py inituser --username=root --password=rootroot --action=create_super_admin &&
echo "from options.options import SysOptions; SysOptions.judge_server_token='$JUDGE_SERVER_TOKEN'" | python manage.py shell &&
break
n=$(($n+1))
echo "Failed to migrate, going to retry..."
sleep 8
done
echo "from options.options import SysOptions; SysOptions.judge_server_token='$JUDGE_SERVER_TOKEN'" | python manage.py shell || exit 1
chown -R nobody:nogroup $DATA $APP/dist
exec supervisord -c /app/deploy/supervisord.conf

View File

@ -0,0 +1,5 @@
proxy_pass http://backend;
proxy_set_header X-Real-IP __IP_HEADER__;
proxy_set_header Host $http_host;client_max_body_size 200M;
proxy_http_version 1.1;
proxy_set_header Connection '';

View File

@ -1,3 +1,7 @@
location /api/judge_server_heartbeat {
include api_proxy.conf;
}
location / {
return 301 https://$host$request_uri;
}

View File

@ -3,10 +3,7 @@ location /public {
}
location /api {
proxy_pass http://backend;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
client_max_body_size 200M;
include api_proxy.conf;
}
location /data/ {

View File

@ -1,14 +1,15 @@
{
"update": [
{
"version": "2017-12-25",
"level": 1,
"title": "Update at 2017-12-25",
"version": "2017-01-04",
"level": "Recommend",
"title": "Update at 2018-01-04",
"details": [
"Fix some issues under IE/Edge",
"Add backend error reporter",
"New email template",
"A more flexible throttling function",
"Add admin dashboard (this page)",
"Other bugs and enhancements"
]
}

View File

@ -1,9 +1,4 @@
import os
def get_env(name, default=""):
return os.environ.get(name, default)
from utils.shortcuts import get_env
DATABASES = {
'default': {

View File

@ -12,14 +12,15 @@ https://docs.djangoproject.com/en/1.8/ref/settings/
import os
import raven
from copy import deepcopy
from utils.shortcuts import get_env
from .custom_settings import *
if os.environ.get("OJ_ENV") == "production":
production_env = get_env("OJ_ENV", "dev") == "production"
if production_env:
from .production_settings import *
else:
from .dev_settings import *
from .custom_settings import *
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Applications
@ -128,7 +129,7 @@ UPLOAD_DIR = f"{DATA_DIR}{UPLOAD_PREFIX}"
STATICFILES_DIRS = [os.path.join(DATA_DIR, "public")]
LOGGING_HANDLERS = ['console'] if DEBUG else ['console', 'sentry']
LOGGING_HANDLERS = ['console', 'sentry'] if production_env else ['console']
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
@ -204,13 +205,6 @@ BROKER_URL = f"{REDIS_URL}/3"
CELERY_TASK_SOFT_TIME_LIMIT = CELERY_TASK_TIME_LIMIT = 180
CELERY_ACCEPT_CONTENT = ["json"]
CELERY_TASK_SERIALIZER = "json"
# 用于限制用户恶意提交大量代码
TOKEN_BUCKET_DEFAULT_CAPACITY = 10
# 单位:每分钟
TOKEN_BUCKET_FILL_RATE = 2
RAVEN_CONFIG = {
'dsn': 'https://b200023b8aed4d708fb593c5e0a6ad3d:1fddaba168f84fcf97e0d549faaeaff0@sentry.io/263057'
}

View File

@ -8,7 +8,7 @@ from wsgiref.util import FileWrapper
from django.conf import settings
from django.http import StreamingHttpResponse, HttpResponse
from account.decorators import problem_permission_required
from account.decorators import problem_permission_required, ensure_created_by
from judge.dispatcher import SPJCompiler
from contest.models import Contest, ContestStatus
from submission.models import Submission
@ -49,7 +49,6 @@ class TestCaseAPI(CSRFExemptAPIView):
else:
return sorted(ret, key=natural_sort_key)
@problem_permission_required
def get(self, request):
problem_id = request.GET.get("problem_id")
if not problem_id:
@ -59,6 +58,11 @@ class TestCaseAPI(CSRFExemptAPIView):
except Problem.DoesNotExist:
return self.error("Problem does not exists")
if problem.contest:
ensure_created_by(problem.contest, request.user)
else:
ensure_created_by(problem, request.user)
test_case_dir = os.path.join(settings.TEST_CASE_DIR, problem.test_case_id)
if not os.path.isdir(test_case_dir):
return self.error("Test case does not exists")
@ -79,7 +83,6 @@ class TestCaseAPI(CSRFExemptAPIView):
response["Content-Length"] = os.path.getsize(file_name)
return response
@problem_permission_required
def post(self, request):
form = TestCaseUploadForm(request.POST, request.FILES)
if form.is_valid():
@ -147,7 +150,6 @@ class TestCaseAPI(CSRFExemptAPIView):
class CompileSPJAPI(APIView):
@validate_serializer(CompileSPJSerializer)
@problem_permission_required
def post(self, request):
data = request.data
spj_version = rand_str(8)
@ -186,11 +188,12 @@ class ProblemBase(APIView):
def delete(self, request):
id = request.GET.get("id")
if not id:
return self.error("Invalid parameter, id is requred")
return self.error("Invalid parameter, id is required")
try:
problem = Problem.objects.get(id=id)
problem = Problem.objects.get(id=id, contest_id__isnull=True)
except Problem.DoesNotExist:
return self.error("Problem does not exists")
ensure_created_by(problem, request.user)
if Submission.objects.filter(problem=problem).exists():
return self.error("Can't delete the problem as it has submissions")
d = os.path.join(settings.TEST_CASE_DIR, problem.test_case_id)
@ -201,11 +204,10 @@ class ProblemBase(APIView):
class ProblemAPI(ProblemBase):
@validate_serializer(CreateProblemSerializer)
@problem_permission_required
@validate_serializer(CreateProblemSerializer)
def post(self, request):
data = request.data
_id = data["_id"]
if not _id:
return self.error("Display ID is required")
@ -236,8 +238,7 @@ class ProblemAPI(ProblemBase):
if problem_id:
try:
problem = Problem.objects.get(id=problem_id)
if not user.can_mgmt_all_problem() and problem.created_by != user:
return self.error("Problem does not exist")
ensure_created_by(problem, request.user)
return self.success(ProblemAdminSerializer(problem).data)
except Problem.DoesNotExist:
return self.error("Problem does not exist")
@ -256,17 +257,15 @@ class ProblemAPI(ProblemBase):
problems = problems.filter(title__contains=keyword)
return self.success(self.paginate_data(request, problems, ProblemAdminSerializer))
@validate_serializer(EditProblemSerializer)
@problem_permission_required
@validate_serializer(EditProblemSerializer)
def put(self, request):
data = request.data
problem_id = data.pop("id")
user = request.user
try:
problem = Problem.objects.get(id=problem_id)
if not user.can_mgmt_all_problem() and problem.created_by != user:
return self.error("Problem does not exist")
ensure_created_by(problem, request.user)
except Problem.DoesNotExist:
return self.error("Problem does not exist")
@ -300,13 +299,11 @@ class ProblemAPI(ProblemBase):
class ContestProblemAPI(ProblemBase):
@validate_serializer(CreateContestProblemSerializer)
@problem_permission_required
def post(self, request):
data = request.data
try:
contest = Contest.objects.get(id=data.pop("contest_id"))
if request.user.is_admin() and contest.created_by != request.user:
return self.error("Contest does not exist")
ensure_created_by(contest, request.user)
except Contest.DoesNotExist:
return self.error("Contest does not exist")
@ -345,8 +342,7 @@ class ContestProblemAPI(ProblemBase):
if problem_id:
try:
problem = Problem.objects.get(id=problem_id)
if user.is_admin() and problem.contest.created_by != user:
return self.error("Problem does not exist")
ensure_created_by(problem, user)
except Problem.DoesNotExist:
return self.error("Problem does not exist")
return self.success(ProblemAdminSerializer(problem).data)
@ -366,10 +362,11 @@ class ContestProblemAPI(ProblemBase):
@problem_permission_required
def put(self, request):
data = request.data
user = request.user
try:
contest = Contest.objects.get(id=data.pop("contest_id"))
if request.user.is_admin() and contest.created_by != request.user:
return self.error("Contest does not exist")
ensure_created_by(contest, user)
except Contest.DoesNotExist:
return self.error("Contest does not exist")
@ -377,12 +374,10 @@ class ContestProblemAPI(ProblemBase):
return self.error("Invalid rule type")
problem_id = data.pop("id")
user = request.user
try:
problem = Problem.objects.get(id=problem_id)
if not user.can_mgmt_all_problem() and problem.created_by != user:
return self.error("Problem does not exist")
ensure_created_by(problem, user)
except Problem.DoesNotExist:
return self.error("Problem does not exist")
@ -428,7 +423,7 @@ class MakeContestProblemPublicAPIView(APIView):
return self.error("Problem does not exist")
if not problem.contest or problem.is_public:
return self.error("Alreay be a public problem")
return self.error("Already be a public problem")
problem.is_public = True
problem.save()
# https://docs.djangoproject.com/en/1.11/topics/db/queries/#copying-model-instances

View File

@ -11,6 +11,13 @@ from django.views.generic import View
logger = logging.getLogger("")
class APIError(Exception):
def __init__(self, msg, err=None):
self.err = err
self.msg = msg
super().__init__(err, msg)
class ContentType(object):
json_request = "application/json"
json_response = "application/json;charset=UTF-8"
@ -137,6 +144,11 @@ class APIView(View):
return self.error(err="invalid-request", msg=str(e))
try:
return super(APIView, self).dispatch(request, *args, **kwargs)
except APIError as e:
ret = {"msg": e.msg}
if e.err:
ret["err"] = e.err
return self.error(**ret)
except Exception as e:
logger.exception(e)
return self.server_error()

View File

@ -1,3 +1,4 @@
import os
import re
import datetime
import random
@ -76,3 +77,7 @@ def send_email(smtp_config, from_name, to_email, to_name, subject, content):
password=smtp_config["password"],
port=smtp_config["port"],
tls=smtp_config["tls"])
def get_env(name, default=""):
return os.environ.get(name, default)