diff --git a/.gitignore b/.gitignore index e9a0324..3a8cb90 100644 --- a/.gitignore +++ b/.gitignore @@ -54,21 +54,18 @@ db.db #*.out *.sqlite3 .DS_Store -log/ -static/release/css -static/release/js -static/release/img -static/src/upload_image/* build.txt tmp/ -test_case/ -release/ -upload/ custom_settings.py -docker-compose.yml *.zip -rsyncd.passwd -node_modules/ -update.sh -ssh.sh +data/log/* +!data/log/.gitkeep +data/test_case/* +!data/test_case/.gitkeep +data/ssl/* +!data/ssl/.gitkeep +data/public/upload/* +!data/public/upload/.gitkeep +data/public/avatar/* +!data/public/avatar/default.png diff --git a/.travis.yml b/.travis.yml index 7782d02..5a01bfa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,11 +9,9 @@ before_install: - docker run -it -d -e POSTGRES_DB=onlinejudge -e POSTGRES_USER=onlinejudge -e POSTGRES_PASSWORD=onlinejudge -p 127.0.0.1:5433:5432 postgres:10 install: - pip install -r deploy/requirements.txt - - mkdir log test_case upload - cp oj/custom_settings.example.py oj/custom_settings.py - echo "SECRET_KEY=\"`cat /dev/urandom | head -1 | md5sum | head -c 32`\"" >> oj/custom_settings.py - python manage.py migrate - - python manage.py initadmin script: - docker ps -a - flake8 . diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9e3f69c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.6-alpine3.6 + +ENV OJ_ENV production + +ADD . /app +WORKDIR /app + +RUN printf "https://mirrors.tuna.tsinghua.edu.cn/alpine/v3.6/community/\nhttps://mirrors.tuna.tsinghua.edu.cn/alpine/v3.6/main/" > /etc/apk/repositories && \ + apk add --update --no-cache build-base nginx openssl curl unzip supervisor jpeg-dev zlib-dev postgresql-dev freetype-dev && \ + pip install --no-cache-dir -r /app/deploy/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple && \ + apk del build-base --purge +RUN curl -L $(curl -s https://api.github.com/repos/QingdaoU/OnlineJudgeFE/releases/latest | grep /dist.zip | cut -d '"' -f 4) -o dist.zip && \ + unzip dist.zip && \ + rm dist.zip +CMD sh /app/deploy/run.sh diff --git a/account/middleware.py b/account/middleware.py index 1ef78cc..245c32a 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -3,6 +3,18 @@ from django.utils.timezone import now from django.utils.deprecation import MiddlewareMixin from utils.api import JSONResponse +from account.models import User + + +class APITokenAuthMiddleware(MiddlewareMixin): + def process_request(self, request): + appkey = request.META.get("HTTP_APPKEY") + if appkey: + try: + request.user = User.objects.get(open_api_appkey=appkey, open_api=True, is_disabled=False) + request.csrf_processing_done = True + except User.DoesNotExist: + pass class SessionRecordMiddleware(MiddlewareMixin): diff --git a/account/serializers.py b/account/serializers.py index 945e096..d346677 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -40,8 +40,7 @@ class GenerateUserSerializer(serializers.Serializer): suffix = serializers.CharField(max_length=16, allow_blank=True) number_from = serializers.IntegerField() number_to = serializers.IntegerField() - default_email = serializers.CharField(max_length=64) - password_length = serializers.IntegerField(required=False, max_value=16) + password_length = serializers.IntegerField(max_value=16, default=8) class ImportUserSeralizer(serializers.Serializer): diff --git a/account/tests.py b/account/tests.py index 4279c96..97880aa 100644 --- a/account/tests.py +++ b/account/tests.py @@ -1,4 +1,5 @@ import time + from unittest import mock from datetime import timedelta from copy import deepcopy @@ -557,20 +558,25 @@ class AdminUserTest(APITestCase): def test_import_users(self): data = {"users": [["user1", "pass1", "eami1@e.com"], - ["user1", "pass1", "eami1@e.com"], - ["user2", "pass2"], ["user3", "pass3", "eamil3@e.com"]] + ["user2", "pass3", "eamil3@e.com"]] } resp = self.client.post(self.url, data) self.assertSuccess(resp) - self.assertDictEqual(resp.data["data"], {"omitted_count": 1, - "created_count": 2, - "get_count": 1}) # successfully created 2 users self.assertEqual(User.objects.all().count(), 4) + def test_import_duplicate_user(self): + data = {"users": [["user1", "pass1", "eami1@e.com"], + ["user1", "pass1", "eami1@e.com"]] + } + resp = self.client.post(self.url, data) + self.assertFailed(resp, "DETAIL: Key (username)=(user1) already exists.") + # no user is created + self.assertEqual(User.objects.all().count(), 2) + def test_delete_users(self): self.test_import_users() - user_ids = User.objects.filter(username__in=["user1", "user3"]).values_list("id", flat=True) + user_ids = User.objects.filter(username__in=["user1", "user2"]).values_list("id", flat=True) user_ids = ",".join([str(id) for id in user_ids]) resp = self.client.delete(self.url + "?id=" + user_ids) self.assertSuccess(resp) @@ -605,6 +611,19 @@ class GenerateUserAPITest(APITestCase): resp = self.client.post(self.url, data=self.data) self.assertSuccess(resp) mock_workbook.assert_called() - data = resp.data["data"] - self.assertEqual(data["created_count"], 6) - self.assertEqual(data["get_count"], 0) + + +class OpenAPIAppkeyAPITest(APITestCase): + def setUp(self): + self.user = self.create_super_admin() + self.url = self.reverse("open_api_appkey_api") + + def test_reset_appkey(self): + resp = self.client.post(self.url, data={}) + self.assertFailed(resp) + + self.user.open_api = True + self.user.save() + resp = self.client.post(self.url, data={}) + self.assertSuccess(resp) + self.assertEqual(resp.data["data"]["appkey"], User.objects.get(username=self.user.username).open_api_appkey) diff --git a/account/urls/oj.py b/account/urls/oj.py index a92dd5b..1b26e14 100644 --- a/account/urls/oj.py +++ b/account/urls/oj.py @@ -5,7 +5,7 @@ from ..views.oj import (ApplyResetPasswordAPI, ResetPasswordAPI, UserLoginAPI, UserLogoutAPI, UsernameOrEmailCheck, AvatarUploadAPI, TwoFactorAuthAPI, UserProfileAPI, UserRankAPI, CheckTFARequiredAPI, SessionManagementAPI, - ProfileProblemDisplayIDRefreshAPI) + ProfileProblemDisplayIDRefreshAPI, OpenAPIAppkeyAPI) from utils.captcha.views import CaptchaAPIView @@ -25,5 +25,6 @@ urlpatterns = [ url(r"^tfa_required/?$", CheckTFARequiredAPI.as_view(), name="tfa_required_check"), url(r"^two_factor_auth/?$", TwoFactorAuthAPI.as_view(), name="two_factor_auth_api"), url(r"^user_rank/?$", UserRankAPI.as_view(), name="user_rank_api"), - url(r"^sessions/?$", SessionManagementAPI.as_view(), name="session_management_api") + url(r"^sessions/?$", SessionManagementAPI.as_view(), name="session_management_api"), + url(r"^open_api_appkey/?$", OpenAPIAppkeyAPI.as_view(), name="open_api_appkey_api"), ] diff --git a/account/views/admin.py b/account/views/admin.py index 062fdcf..b1ef688 100644 --- a/account/views/admin.py +++ b/account/views/admin.py @@ -1,8 +1,11 @@ import os import re import xlsxwriter + +from django.db import transaction, IntegrityError from django.db.models import Q from django.http import HttpResponse +from django.contrib.auth.hashers import make_password from submission.models import Submission from utils.api import APIView, validate_serializer @@ -18,26 +21,27 @@ class UserAdminAPI(APIView): @validate_serializer(ImportUserSeralizer) @super_admin_required def post(self, request): + """ + Generate user + """ data = request.data["users"] - omitted_count = created_count = get_count = 0 + + user_list = [] for user_data in data: if len(user_data) != 3 or len(user_data[0]) > 32: - omitted_count += 1 - continue - user, created = User.objects.get_or_create(username=user_data[0]) - user.set_password(user_data[1]) - user.email = user_data[2] - user.save() - if created: - UserProfile.objects.create(user=user) - created_count += 1 - else: - get_count += 1 - return self.success({ - "omitted_count": omitted_count, - "created_count": created_count, - "get_count": get_count - }) + return self.error(f"Error occurred while processing data '{user_data}'") + user_list.append(User(username=user_data[0], password=make_password(user_data[1]), email=user_data[2])) + + try: + with transaction.atomic(): + ret = User.objects.bulk_create(user_list) + UserProfile.objects.bulk_create([UserProfile(user=user) for user in ret]) + return self.success() + except IntegrityError as e: + # Extract detail from exception message + # duplicate key value violates unique constraint "user_username_key" + # DETAIL: Key (username)=(root11) already exists. + return self.error(str(e).split("\n")[1]) @validate_serializer(EditUserSerializer) @super_admin_required @@ -146,7 +150,7 @@ class GenerateUserAPI(APIView): file_id = request.GET.get("file_id") if not file_id: return self.error("Invalid Parameter, file_id is required") - if not re.match(r"[a-zA-Z0-9]+", file_id): + if not re.match(r"^[a-zA-Z0-9]+$", file_id): return self.error("Illegal file_id") file_path = f"/tmp/{file_id}.xlsx" if not os.path.isfile(file_path): @@ -169,9 +173,6 @@ class GenerateUserAPI(APIView): if data["number_from"] > data["number_to"]: return self.error("Start number must be lower than end number") - password_length = data.get("password_length", 8) - default_email = data.get("default_email") - file_id = rand_str(8) filename = f"/tmp/{file_id}.xlsx" workbook = xlsxwriter.Workbook(filename) @@ -180,26 +181,27 @@ class GenerateUserAPI(APIView): worksheet.write("A1", "Username") worksheet.write("B1", "Password") i = 1 - created_count = 0 - get_count = 0 + + user_list = [] for number in range(data["number_from"], data["number_to"] + 1): - username = f"{data['prefix']}{number}{data['suffix']}" - password = rand_str(password_length) - user, created = User.objects.get_or_create(username=username) - user.email = default_email - user.set_password(password) - user.save() - if created: - UserProfile.objects.create(user=user) - created_count += 1 - else: - get_count += 1 - worksheet.write_string(i, 0, username) - worksheet.write_string(i, 1, password) - i += 1 - workbook.close() - return self.success({ - "file_id": file_id, - "created_count": created_count, - "get_count": get_count - }) + raw_password = rand_str(data["password_length"]) + user = User(username=f"{data['prefix']}{number}{data['suffix']}", password=make_password(raw_password)) + user.raw_password = raw_password + user_list.append(user) + + try: + with transaction.atomic(): + + ret = User.objects.bulk_create(user_list) + UserProfile.objects.bulk_create([UserProfile(user=user) for user in ret]) + for item in user_list: + worksheet.write_string(i, 0, item.username) + worksheet.write_string(i, 1, item.raw_password) + i += 1 + workbook.close() + return self.success({"file_id": file_id}) + except IntegrityError as e: + # Extract detail from exception message + # duplicate key value violates unique constraint "user_username_key" + # DETAIL: Key (username)=(root11) already exists. + return self.error(str(e).split("\n")[1]) diff --git a/account/views/oj.py b/account/views/oj.py index 344a419..5f25817 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -401,3 +401,15 @@ class ProfileProblemDisplayIDRefreshAPI(APIView): v["_id"] = id_map[k] profile.save(update_fields=["acm_problems_status", "oi_problems_status"]) return self.success() + + +class OpenAPIAppkeyAPI(APIView): + @login_required + def post(self, request): + user = request.user + if not user.open_api: + return self.error("Permission denied") + api_appkey = rand_str() + user.open_api_appkey = api_appkey + user.save() + return self.success({"appkey": api_appkey}) diff --git a/data/log/.gitkeep b/data/log/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/public/avatar/default.png b/data/public/avatar/default.png new file mode 100644 index 0000000..97f3495 Binary files /dev/null and b/data/public/avatar/default.png differ diff --git a/data/public/upload/.gitkeep b/data/public/upload/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/ssl/.gitkeep b/data/ssl/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/test_case/.gitkeep b/data/test_case/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/deploy/Dockerfile b/deploy/Dockerfile deleted file mode 100644 index 55af522..0000000 --- a/deploy/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM python:3.6-alpine3.6 - -ENV OJ_ENV production -RUN apk add --no-cache supervisor jpeg-dev zlib-dev postgresql-dev freetype-dev - -ADD requirements.txt /tmp -RUN apk add --no-cache build-base && \ - pip install --no-cache-dir -r /tmp/requirements.txt -i https://pypi.doubanio.com/simple && \ - apk del build-base --purge - -VOLUME [ "/app" ] - -CMD sh /app/deploy/run.sh diff --git a/deploy/nginx/common.conf b/deploy/nginx/common.conf new file mode 100644 index 0000000..ecfd251 --- /dev/null +++ b/deploy/nginx/common.conf @@ -0,0 +1,20 @@ +location /public { + root /data; +} + +location /api { + proxy_pass http://backend; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + client_max_body_size 200M; +} + +location /admin { + root /app/dist/admin; + try_files $uri $uri/ /index.html =404; +} + +location / { + root /app/dist; + try_files $uri $uri/ /index.html =404; +} \ No newline at end of file diff --git a/deploy/nginx/nginx.conf b/deploy/nginx/nginx.conf new file mode 100644 index 0000000..0942890 --- /dev/null +++ b/deploy/nginx/nginx.conf @@ -0,0 +1,57 @@ +user nobody; +daemon off; +pid /tmp/nginx.pid; +worker_processes auto; +pcre_jit on; +error_log /data/log/nginx_error.log warn; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + server_tokens off; + keepalive_timeout 65; + sendfile on; + tcp_nodelay on; + + gzip on; + gzip_vary on; + gzip_types application/javascript text/css; + client_body_temp_path /tmp 1 2; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /data/log/nginx_access.log main; + + upstream backend { + server 127.0.0.1:8080; + keepalive 32; + } + + server { + listen 8000 default_server; + server_name _; + + include common.conf; + } + + server { + listen 1443 ssl http2 default_server; + server_name _; + ssl_certificate /data/ssl/server.crt; + ssl_certificate_key /data/ssl/server.key; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + + include common.conf; + } + +} + diff --git a/deploy/run.sh b/deploy/run.sh index c8ca9d5..b710cba 100644 --- a/deploy/run.sh +++ b/deploy/run.sh @@ -1,39 +1,33 @@ #!/bin/bash -BASE=/app +APP=/app +DATA=/data -if [ ! -f "$BASE/custom_settings.py" ]; then - echo SECRET_KEY=\"$(cat /dev/urandom | head -1 | md5sum | head -c 32)\" >> /app/oj/custom_settings.py +if [ ! -f "$APP/oj/custom_settings.py" ]; then + echo SECRET_KEY=\"$(cat /dev/urandom | head -1 | md5sum | head -c 32)\" >> $APP/oj/custom_settings.py fi -if [ ! -d "$BASE/log" ]; then - mkdir -p $BASE/log +mkdir -p $DATA/log $DATA/ssl $DATA/test_case $DATA/public/upload $DATA/public/avatar + +SSL="$DATA/ssl" +if [ ! -f "$SSL/server.key" ]; then + openssl req -x509 -newkey rsa:2048 -keyout "$SSL/server.key" -out "$SSL/server.crt" -days 1000 \ + -subj "/C=CN/ST=Beijing/L=Beijing/O=Beijing OnlineJudge Technology Co., Ltd./OU=Service Infrastructure Department/CN=`hostname`" -nodes fi -cd $BASE -find . -name "*.pyc" -delete - -# wait for postgresql start -sleep 6 +cd $APP n=0 -while [ $n -lt 3 ] +while [ $n -lt 5 ] do -python manage.py migrate -if [ $? -ne 0 ]; then - echo "Can't start server, try again in 3 seconds.." - sleep 3 - let "n+=1" - continue -fi -python manage.py initinstall -break + python manage.py migrate --no-input && + python manage.py inituser --username=root --password=rootroot --action=create_super_admin && + break + n=$(($n+1)) + echo "Failed to migrate, going to retry..." + sleep 8 done -if [ $n -eq 3 ]; then - echo "Can't start server, please check log file for details." - exit 1 -fi - -chown -R nobody:nogroup /data/log /data/test_case /data/avatar /data/upload -exec supervisord -c /app/deploy/supervisor.conf +cp data/public/avatar/default.png /data/public/avatar +chown -R nobody:nogroup $DATA $APP/dist +exec supervisord -c /app/deploy/supervisord.conf diff --git a/deploy/supervisor.conf b/deploy/supervisord.conf similarity index 50% rename from deploy/supervisor.conf rename to deploy/supervisord.conf index fc248d8..6b23166 100644 --- a/deploy/supervisor.conf +++ b/deploy/supervisord.conf @@ -1,5 +1,5 @@ [supervisord] -logfile=/app/log/supervisord.log +logfile=/data/log/supervisord.log logfile_maxbytes=10MB logfile_backups=10 loglevel=info @@ -7,12 +7,30 @@ pidfile=/tmp/supervisord.pid nodaemon=true childlogdir=/data/log/ +[inet_http_server] +port=127.0.0.1:9005 + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface + [supervisorctl] -serverurl=unix:///tmp/supervisor.sock +serverurl=http://127.0.0.1:9005 + +[program:nginx] +command=nginx -c /app/deploy/nginx/nginx.conf +directory=/app/ +stdout_logfile=/data/log/nginx.log +stderr_logfile=/data/log/nginx.log +autostart=true +autorestart=true +startsecs=5 +stopwaitsecs = 5 +killasgroup=true [program:gunicorn] -command=sh -c "gunicorn oj.wsgi --user nobody -b 0.0.0.0:8080 --reload -w `grep -c ^processor /proc/cpuinfo`" +command=sh -c "gunicorn oj.wsgi --user nobody -b 127.0.0.1:8080 --reload -w `grep -c ^processor /proc/cpuinfo`" directory=/app/ +user=nobody stdout_logfile=/data/log/gunicorn.log stderr_logfile=/data/log/gunicorn.log autostart=true diff --git a/judge/dispatcher.py b/judge/dispatcher.py index 7c04029..0f5e3cb 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -14,6 +14,7 @@ from contest.models import ContestRuleType, ACMContestRank, OIContestRank, Conte from judge.languages import languages, spj_languages from options.options import SysOptions from problem.models import Problem, ProblemRuleType +from problem.utils import parse_problem_template from submission.models import JudgeStatus, Submission from utils.cache import cache from utils.constants import CacheKey @@ -123,16 +124,24 @@ class JudgeDispatcher(DispatcherBase): cache.lpush(CacheKey.waiting_queue, json.dumps(data)) return - sub_config = list(filter(lambda item: self.submission.language == item["name"], languages))[0] + language = self.submission.language + sub_config = list(filter(lambda item: language == item["name"], languages))[0] spj_config = {} if self.problem.spj_code: for lang in spj_languages: if lang["name"] == self.problem.spj_language: spj_config = lang["spj"] break + + if language in self.problem.template: + template = parse_problem_template(self.problem.template[language]) + code = f"{template['prepend']}\n{self.submission.code}\n{template['append']}" + else: + code = self.submission.code + data = { "language_config": sub_config["config"], - "src": self.submission.code, + "src": code, "max_cpu_time": self.problem.time_limit, "max_memory": 1024 * 1024 * self.problem.memory_limit, "test_case_id": self.problem.test_case_id, diff --git a/judge/languages.py b/judge/languages.py index cc5f09b..e7f35ff 100644 --- a/judge/languages.py +++ b/judge/languages.py @@ -1,7 +1,7 @@ _c_lang_config = { - "template": """//PREPEND START + "template": """//PREPEND BEGIN #include //PREPEND END @@ -12,7 +12,7 @@ int add(int a, int b) { } //TEMPLATE END -//APPEND START +//APPEND BEGIN int main() { printf("%d", add(1, 2)); return 0; @@ -48,12 +48,23 @@ _c_lang_spj_config = { } _cpp_lang_config = { - "template": """/*--PREPEND START--*/ -/*--PREPEND END--*/ -/*--TEMPLATE BEGIN--*/ -/*--TEMPLATE END--*/ -/*--APPEND START--*/ -/*--APPEND END--*/""", + "template": """//PREPEND BEGIN +#include +//PREPEND END + +//TEMPLATE BEGIN +int add(int a, int b) { + // Please fill this blank + return ___________; +} +//TEMPLATE END + +//APPEND BEGIN +int main() { + std::cout << add(1, 2); + return 0; +} +//APPEND END""", "compile": { "src_name": "main.cpp", "exe_name": "main", diff --git a/oj/dev_settings.py b/oj/dev_settings.py index 724a5dc..5c75a0b 100644 --- a/oj/dev_settings.py +++ b/oj/dev_settings.py @@ -24,16 +24,4 @@ DEBUG = True ALLOWED_HOSTS = ["*"] -TEST_CASE_DIR = "/tmp" - -LOG_PATH = f"{BASE_DIR}/log/" - -AVATAR_URI_PREFIX = "/static/avatar" -AVATAR_UPLOAD_DIR = f"{BASE_DIR}{AVATAR_URI_PREFIX}" - -UPLOAD_PREFIX = "/static/upload" -UPLOAD_DIR = f"{BASE_DIR}{UPLOAD_PREFIX}" - -STATICFILES_DIRS = [ - os.path.join(BASE_DIR, "static"), -] +DATA_DIR = f"{BASE_DIR}/data" diff --git a/oj/production_settings.py b/oj/production_settings.py index 3f9dee6..026c53a 100644 --- a/oj/production_settings.py +++ b/oj/production_settings.py @@ -8,8 +8,8 @@ def get_env(name, default=""): DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'HOST': get_env("POSTGRES_HOST", "postgres"), - 'PORT': get_env("POSTGRES_PORT", "5433"), + 'HOST': get_env("POSTGRES_HOST", "oj-postgres"), + 'PORT': get_env("POSTGRES_PORT", "5432"), 'NAME': get_env("POSTGRES_DB"), 'USER': get_env("POSTGRES_USER"), 'PASSWORD': get_env("POSTGRES_PASSWORD") @@ -17,7 +17,7 @@ DATABASES = { } REDIS_CONF = { - "host": get_env("REDIS_HOST", "redis"), + "host": get_env("REDIS_HOST", "oj-redis"), "port": get_env("REDIS_PORT", "6379") } @@ -25,12 +25,4 @@ DEBUG = False ALLOWED_HOSTS = ['*'] -AVATAR_URI_PREFIX = "/static/avatar" -AVATAR_UPLOAD_DIR = "/data/avatar" - -UPLOAD_PREFIX = "/static/upload" -UPLOAD_DIR = "/data/upload" - -TEST_CASE_DIR = "/data/test_case" -LOG_PATH = "/data/log" -DEFAULT_JUDGE_SERVER_SERVICE_URL = "http://judge-server:8080/" +DATA_DIR = "/data" diff --git a/oj/settings.py b/oj/settings.py index 0c6e768..40335a1 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -49,6 +49,7 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'account.middleware.APITokenAuthMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', @@ -109,57 +110,55 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.8/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = '/public/' AUTH_USER_MODEL = 'account.User' +TEST_CASE_DIR = os.path.join(DATA_DIR, "test_case") +LOG_PATH = os.path.join(DATA_DIR, "log") + +AVATAR_URI_PREFIX = "/public/avatar" +AVATAR_UPLOAD_DIR = f"{DATA_DIR}{AVATAR_URI_PREFIX}" + +UPLOAD_PREFIX = "/public/upload" +UPLOAD_DIR = f"{DATA_DIR}{UPLOAD_PREFIX}" + +STATICFILES_DIRS = [os.path.join(DATA_DIR, "public")] + LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'standard': { - 'format': '%(asctime)s [%(threadName)s:%(thread)d] [%(name)s:%(lineno)d] [%(module)s:%(funcName)s] [%(levelname)s]- %(message)s', - 'datefmt': '%Y-%m-%d %H:%M:%S' - } - }, - 'handlers': { - 'django_error': { - 'level': 'WARNING', - 'class': 'logging.handlers.RotatingFileHandler', - 'filename': os.path.join(LOG_PATH, 'django.log'), - 'formatter': 'standard' - }, - 'app_info': { - 'level': 'INFO', - 'class': 'logging.handlers.RotatingFileHandler', - 'filename': os.path.join(LOG_PATH, 'app_info.log'), - 'formatter': 'standard' - }, - 'console': { - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'formatter': 'standard' - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['django_error', 'console'], - 'level': 'WARNING', - 'propagate': True, - }, - 'django.db.backends': { - 'handlers': ['django_error', 'console'], - 'level': 'WARNING', - 'propagate': True, - }, - }, + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'standard': { + 'format': '[%(asctime)s] - [%(levelname)s] - [%(name)s:%(lineno)d] - %(message)s', + 'datefmt': '%Y-%m-%d %H:%M:%S' + } + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'standard' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['console'], + 'level': 'ERROR', + 'propagate': True, + }, + 'django.db.backends': { + 'handlers': ['console'], + 'level': 'ERROR', + 'propagate': True, + }, + '': { + 'handlers': ['console'], + 'level': 'WARNING', + 'propagate': True, + } + }, } -app_logger = { - 'handlers': ['app_info', 'console'], - 'level': 'DEBUG', - 'propagate': False -} -LOGGING["loggers"].update({app: deepcopy(app_logger) for app in LOCAL_APPS}) REST_FRAMEWORK = { 'TEST_REQUEST_DEFAULT_FORMAT': 'json', diff --git a/problem/serializers.py b/problem/serializers.py index 57a034c..46b2e22 100644 --- a/problem/serializers.py +++ b/problem/serializers.py @@ -4,6 +4,7 @@ from judge.languages import language_names, spj_language_names from utils.api import DateTimeTZField, UsernameSerializer, serializers from .models import Problem, ProblemRuleType, ProblemTag +from .utils import parse_problem_template class TestCaseUploadForm(forms.Form): @@ -110,9 +111,18 @@ class ContestProblemAdminSerializer(BaseProblemSerializer): class ProblemSerializer(BaseProblemSerializer): + template = serializers.SerializerMethodField() + + def get_template(self, obj): + ret = {} + for lang, code in obj.template.items(): + ret[lang] = parse_problem_template(code)["template"] + return ret + class Meta: model = Problem - exclude = ("contest", "test_case_score", "test_case_id", "visible", "is_public") + exclude = ("contest", "test_case_score", "test_case_id", "visible", "is_public", + "template", "spj_code", "spj_version", "spj_compile_ok") class ContestProblemSerializer(BaseProblemSerializer): @@ -131,3 +141,54 @@ class ContestProblemSafeSerializer(BaseProblemSerializer): class ContestProblemMakePublicSerializer(serializers.Serializer): id = serializers.IntegerField() display_id = serializers.CharField(max_length=32) + + +class ExportProblemSerializer(serializers.ModelSerializer): + description = serializers.SerializerMethodField() + input_description = serializers.SerializerMethodField() + output_description = serializers.SerializerMethodField() + test_case_score = serializers.SerializerMethodField() + hint = serializers.SerializerMethodField() + time_limit = serializers.SerializerMethodField() + memory_limit = serializers.SerializerMethodField() + spj = serializers.SerializerMethodField() + template = serializers.SerializerMethodField() + + def get_description(self, obj): + return {"format": "html", "value": obj.description} + + def get_input_description(self, obj): + return {"format": "html", "value": obj.input_description} + + def get_output_description(self, obj): + return {"format": "html", "value": obj.output_description} + + def get_hint(self, obj): + return {"format": "html", "value": obj.hint} + + def get_test_case_score(self, obj): + return obj.test_case_score if obj.rule_type == ProblemRuleType.OI else [] + + def get_time_limit(self, obj): + return {"unit": "ms", "value": obj.time_limit} + + def get_memory_limit(self, obj): + return {"unit": "MB", "value": obj.memory_limit} + + def get_spj(self, obj): + return {"enabled": obj.spj, + "code": obj.spj_code if obj.spj else None, + "language": obj.spj_language if obj.spj else None} + + def get_template(self, obj): + ret = {} + for k, v in obj.template.items(): + ret[k] = parse_problem_template(v) + return ret + + class Meta: + model = Problem + fields = ("_id", "title", "description", + "input_description", "output_description", + "test_case_score", "hint", "time_limit", "memory_limit", "samples", + "template", "spj", "rule_type", "source", "template") diff --git a/problem/tests.py b/problem/tests.py index dbd01b7..93cd058 100644 --- a/problem/tests.py +++ b/problem/tests.py @@ -11,10 +11,13 @@ from utils.api.tests import APITestCase from .models import ProblemTag from .models import Problem, ProblemRuleType -from .views.admin import TestCaseAPI from contest.models import Contest from contest.tests import DEFAULT_CONTEST_DATA +from .views.admin import TestCaseAPI +from .utils import parse_problem_template + + DEFAULT_PROBLEM_DATA = {"_id": "A-110", "title": "test", "description": "

test

", "input_description": "test", "output_description": "test", "time_limit": 1000, "memory_limit": 256, "difficulty": "Low", "visible": True, "tags": ["test"], "languages": ["C", "C++", "Java", "Python2"], "template": {}, @@ -257,3 +260,44 @@ class ContestProblemTest(ProblemCreateTestBase): contest.save() resp = self.client.get(self.url + "?contest_id=" + str(self.contest["id"])) self.assertSuccess(resp) + + +class ParseProblemTemplateTest(APITestCase): + def test_parse(self): + template_str = """ +//PREPEND BEGIN +aaa +//PREPEND END + +//TEMPLATE BEGIN +bbb +//TEMPLATE END + +//APPEND BEGIN +ccc +//APPEND END +""" + + ret = parse_problem_template(template_str) + self.assertEqual(ret["prepend"], "aaa\n") + self.assertEqual(ret["template"], "bbb\n") + self.assertEqual(ret["append"], "ccc\n") + + def test_parse1(self): + template_str = """ +//PREPEND BEGIN +aaa +//PREPEND END + +//APPEND BEGIN +ccc +//APPEND END +//APPEND BEGIN +ddd +//APPEND END +""" + + ret = parse_problem_template(template_str) + self.assertEqual(ret["prepend"], "aaa\n") + self.assertEqual(ret["template"], "") + self.assertEqual(ret["append"], "ccc\n") diff --git a/problem/utils.py b/problem/utils.py new file mode 100644 index 0000000..f824309 --- /dev/null +++ b/problem/utils.py @@ -0,0 +1,10 @@ +import re + + +def parse_problem_template(template_str): + prepend = re.findall("//PREPEND BEGIN\n([\s\S]+?)//PREPEND END", template_str) + template = re.findall("//TEMPLATE BEGIN\n([\s\S]+?)//TEMPLATE END", template_str) + append = re.findall("//APPEND BEGIN\n([\s\S]+?)//APPEND END", template_str) + return {"prepend": prepend[0] if prepend else "", + "template": template[0] if template else "", + "append": append[0] if append else ""} diff --git a/utils/management/commands/initadmin.py b/utils/management/commands/initadmin.py deleted file mode 100644 index 78e8a45..0000000 --- a/utils/management/commands/initadmin.py +++ /dev/null @@ -1,38 +0,0 @@ -from django.core.management.base import BaseCommand - -from account.models import AdminType, ProblemPermission, User, UserProfile -from utils.shortcuts import rand_str # NOQA - - -class Command(BaseCommand): - def handle(self, *args, **options): - try: - admin = User.objects.get(username="root") - if admin.admin_type == AdminType.SUPER_ADMIN: - self.stdout.write(self.style.WARNING("Super admin user 'root' already exists, " - "would you like to reset it's password?\n" - "Input yes to confirm: ")) - if input() == "yes": - rand_password = "rootroot" - admin.save() - self.stdout.write(self.style.SUCCESS("Successfully created super admin user password.\n" - "Username: root\nPassword: %s\n" - "Remember to change password and turn on two factors auth " - "after installation." % rand_password)) - else: - self.stdout.write(self.style.SUCCESS("Nothing happened")) - else: - self.stdout.write(self.style.ERROR("User 'root' is not super admin.")) - except User.DoesNotExist: - user = User.objects.create(username="root", email="root@oj.com", admin_type=AdminType.SUPER_ADMIN, - problem_permission=ProblemPermission.ALL) - # for dev - # rand_password = rand_str(length=6) - rand_password = "rootroot" - user.set_password(rand_password) - user.save() - UserProfile.objects.create(user=user) - self.stdout.write(self.style.SUCCESS("Successfully created super admin user.\n" - "Username: root\nPassword: %s\n" - "Remember to change password and turn on two factors auth " - "after installation." % rand_password)) diff --git a/utils/management/commands/initinstall.py b/utils/management/commands/initinstall.py deleted file mode 100644 index bdb0f0a..0000000 --- a/utils/management/commands/initinstall.py +++ /dev/null @@ -1,17 +0,0 @@ -import os -from account.models import User -from django.core.management.base import BaseCommand - - -class Command(BaseCommand): - def handle(self, *args, **options): - if User.objects.exists(): - self.stdout.write(self.style.WARNING("Nothing happened\n")) - return - try: - if os.system("python manage.py initadmin") != 0: - self.stdout.write(self.style.ERROR("Failed to execute command 'initadmin'")) - exit(1) - self.stdout.write(self.style.SUCCESS("Done")) - except Exception as e: - self.stdout.write(self.style.ERROR("Failed to initialize, error: " + str(e))) diff --git a/utils/management/commands/inituser.py b/utils/management/commands/inituser.py new file mode 100644 index 0000000..c3f0827 --- /dev/null +++ b/utils/management/commands/inituser.py @@ -0,0 +1,44 @@ +from django.core.management.base import BaseCommand + +from account.models import AdminType, ProblemPermission, User, UserProfile +from utils.shortcuts import rand_str # NOQA + + +class Command(BaseCommand): + def add_arguments(self, parser): + parser.add_argument("--username", type=str) + parser.add_argument("--password", type=str) + parser.add_argument("--action", type=str) + + def handle(self, *args, **options): + username = options["username"] + password = options["password"] + action = options["action"] + + if not(username and password and action): + self.stdout.write(self.style.ERROR("Invalid args")) + exit(1) + + if action == "create_super_admin": + if User.objects.filter(username=username).exists(): + self.stdout.write(self.style.SUCCESS(f"User {username} exists, operation ignored")) + exit() + + user = User.objects.create(username=username, admin_type=AdminType.SUPER_ADMIN, + problem_permission=ProblemPermission.ALL) + user.set_password(password) + user.save() + UserProfile.objects.create(user=user) + + self.stdout.write(self.style.SUCCESS("User created")) + elif action == "reset": + try: + user = User.objects.get(username=username) + user.set_password(password) + user.save() + self.stdout.write(self.style.SUCCESS(f"Password is rested")) + except User.DoesNotExist: + self.stdout.write(self.style.ERROR(f"User {username} doesnot exist, operation ignored")) + exit(1) + else: + raise ValueError("Invalid action")