├── conf ├── __init__.py ├── urls │ ├── __init__.py │ ├── oj.py │ └── admin.py ├── migrations │ ├── __init__.py │ ├── 0003_judgeserver_is_disabled.py │ ├── 0004_auto_20180501_0436.py │ ├── 0002_auto_20171011_1214.py │ └── 0001_initial.py ├── models.py └── serializers.py ├── data ├── log │ └── .gitkeep ├── ssl │ └── .gitkeep ├── config │ └── .gitkeep ├── test_case │ └── .gitkeep └── public │ ├── upload │ └── .gitkeep │ ├── avatar │ └── default.png │ └── website │ └── favicon.ico ├── fps ├── __init__.py └── fps.xml ├── judge ├── __init__.py └── tasks.py ├── oj ├── __init__.py ├── wsgi.py ├── production_settings.py ├── urls.py └── dev_settings.py ├── utils ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── inituser.py ├── api │ ├── __init__.py │ ├── _serializers.py │ ├── tests.py │ └── api.py ├── captcha │ ├── Menlo.ttc │ ├── timesbi.ttf │ ├── views.py │ └── __init__.py ├── tasks.py ├── models.py ├── urls.py ├── constants.py ├── cache.py ├── serializers.py ├── throttling.py ├── views.py └── shortcuts.py ├── account ├── __init__.py ├── urls │ ├── __init__.py │ ├── admin.py │ └── oj.py ├── views │ └── __init__.py ├── migrations │ ├── __init__.py │ ├── 0011_auto_20180501_0456.py │ ├── 0012_userprofile_language.py │ ├── 0009_auto_20171125_1514.py │ ├── 0002_auto_20170209_1028.py │ ├── 0003_userprofile_total_score.py │ ├── 0006_user_session_keys.py │ ├── 0005_auto_20170830_1154.py │ ├── 0010_auto_20180501_0436.py │ ├── 0001_initial.py │ └── 0008_auto_20171011_1214.py ├── tasks.py ├── templates │ └── reset_password_email.html ├── middleware.py ├── models.py ├── serializers.py └── decorators.py ├── contest ├── __init__.py ├── urls │ ├── __init__.py │ ├── admin.py │ └── oj.py ├── views │ └── __init__.py ├── migrations │ ├── __init__.py │ ├── 0007_contestannouncement_visible.py │ ├── 0008_contest_allowed_ip_ranges.py │ ├── 0004_auto_20170717_1324.py │ ├── 0010_auto_20190326_0201.py │ ├── 0002_auto_20170209_0845.py │ ├── 0005_auto_20170823_0918.py │ ├── 0003_auto_20170217_0820.py │ ├── 0006_auto_20171011_1214.py │ ├── 0009_auto_20180501_0436.py │ └── 0001_initial.py ├── models.py └── serializers.py ├── options ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0003_migrate_languages_options.py │ ├── 0002_auto_20180501_0436.py │ └── 0001_initial.py ├── tests.py ├── views.py └── models.py ├── problem ├── __init__.py ├── urls │ ├── __init__.py │ ├── oj.py │ └── admin.py ├── views │ ├── __init__.py │ └── oj.py ├── migrations │ ├── __init__.py │ ├── 0014_problem_share_submission.py │ ├── 0010_problem_spj_compile_ok.py │ ├── 0013_problem_io_mode.py │ ├── 0002_problem__id.py │ ├── 0005_auto_20170815_1258.py │ ├── 0006_auto_20170823_0918.py │ ├── 0004_auto_20170501_0637.py │ ├── 0011_fix_problem_ac_count.py │ ├── 0009_auto_20171011_1214.py │ ├── 0008_auto_20170923_1318.py │ ├── 0012_auto_20180501_0436.py │ ├── 0003_auto_20170217_0820.py │ └── 0001_initial.py ├── utils.py └── models.py ├── submission ├── __init__.py ├── urls │ ├── __init__.py │ ├── admin.py │ └── oj.py ├── views │ ├── __init__.py │ └── admin.py ├── migrations │ ├── __init__.py │ ├── 0006_auto_20170830_1154.py │ ├── 0008_submission_ip.py │ ├── 0005_submission_username.py │ ├── 0009_delete_user_output.py │ ├── 0011_fix_submission_number.py │ ├── 0012_auto_20180501_0436.py │ ├── 0002_auto_20170509_1203.py │ ├── 0001_initial.py │ └── 0007_auto_20170923_1318.py ├── serializers.py ├── models.py └── tests.py ├── announcement ├── __init__.py ├── urls │ ├── __init__.py │ ├── oj.py │ └── admin.py ├── views │ ├── __init__.py │ ├── oj.py │ └── admin.py ├── migrations │ ├── __init__.py │ ├── 0003_auto_20180501_0436.py │ ├── 0002_auto_20171011_1214.py │ └── 0001_initial.py ├── models.py ├── serializers.py └── tests.py ├── .dockerignore ├── .coveragerc ├── deploy ├── nginx │ ├── https_redirect.conf │ ├── api_proxy.conf │ ├── locations.conf │ └── nginx.conf ├── test_case_rsync │ ├── Dockerfile │ ├── rsyncd.conf │ ├── docker-compose.yml │ └── run.sh ├── health_check.py ├── requirements.txt ├── supervisord.conf └── entrypoint.sh ├── .flake8 ├── .github ├── issue_template.md └── workflows │ └── release.yml ├── manage.py ├── .travis.yml ├── init_db.sh ├── run_test.py ├── LICENSE ├── Dockerfile ├── .gitignore ├── README-CN.md ├── README.md └── docs └── data.json /conf/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/log/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/ssl/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /judge/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /oj/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /conf/urls/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /contest/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/config/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/test_case/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /options/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /problem/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /submission/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account/urls/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /announcement/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /conf/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /contest/urls/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /contest/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/public/upload/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /problem/urls/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /problem/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /submission/urls/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /submission/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /account/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /announcement/urls/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /announcement/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /contest/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /options/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /problem/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /submission/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /announcement/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /options/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /options/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | venv 2 | .idea 3 | .git 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /utils/api/__init__.py: -------------------------------------------------------------------------------- 1 | from ._serializers import * # NOQA 2 | from .api import * # NOQA 3 | -------------------------------------------------------------------------------- /utils/captcha/Menlo.ttc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QingdaoU/OnlineJudge/HEAD/utils/captcha/Menlo.ttc -------------------------------------------------------------------------------- /utils/captcha/timesbi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QingdaoU/OnlineJudge/HEAD/utils/captcha/timesbi.ttf -------------------------------------------------------------------------------- /data/public/avatar/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QingdaoU/OnlineJudge/HEAD/data/public/avatar/default.png -------------------------------------------------------------------------------- /data/public/website/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QingdaoU/OnlineJudge/HEAD/data/public/website/favicon.ico -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = */urls/* 3 | */__init__.py 4 | */tests.py 5 | */migrations/* 6 | *urls.py 7 | utils/xss_filter.py 8 | -------------------------------------------------------------------------------- /deploy/nginx/https_redirect.conf: -------------------------------------------------------------------------------- 1 | location /api/judge_server_heartbeat { 2 | include api_proxy.conf; 3 | } 4 | 5 | location / { 6 | return 301 https://$host$request_uri; 7 | } -------------------------------------------------------------------------------- /deploy/test_case_rsync/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.6 2 | 3 | RUN apk add --update --no-cache rsync 4 | 5 | ADD ./run.sh /tmp/run.sh 6 | ADD ./rsyncd.conf /etc/rsyncd.conf 7 | 8 | CMD /bin/sh /tmp/run.sh -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | xss_filter.py, 4 | */migrations/, 5 | *settings.py 6 | */apps.py 7 | venv/ 8 | max-line-length = 180 9 | inline-quotes = " 10 | no-accept-encodings = True 11 | -------------------------------------------------------------------------------- /options/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from utils.models import JSONField 3 | 4 | 5 | class SysOptions(models.Model): 6 | key = models.TextField(unique=True, db_index=True) 7 | value = JSONField() 8 | -------------------------------------------------------------------------------- /announcement/urls/oj.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from ..views.oj import AnnouncementAPI 4 | 5 | urlpatterns = [ 6 | url(r"^announcement/?$", AnnouncementAPI.as_view(), name="announcement_api"), 7 | ] 8 | -------------------------------------------------------------------------------- /deploy/nginx/api_proxy.conf: -------------------------------------------------------------------------------- 1 | proxy_pass http://backend; 2 | proxy_set_header X-Real-IP __IP_HEADER__; 3 | proxy_set_header Host $http_host; 4 | client_max_body_size 200M; 5 | proxy_http_version 1.1; 6 | proxy_set_header Connection ''; -------------------------------------------------------------------------------- /announcement/urls/admin.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from ..views.admin import AnnouncementAdminAPI 4 | 5 | urlpatterns = [ 6 | url(r"^announcement/?$", AnnouncementAdminAPI.as_view(), name="announcement_admin_api"), 7 | ] 8 | -------------------------------------------------------------------------------- /submission/urls/admin.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from ..views.admin import SubmissionRejudgeAPI 4 | 5 | urlpatterns = [ 6 | url(r"^submission/rejudge?$", SubmissionRejudgeAPI.as_view(), name="submission_rejudge_api"), 7 | ] 8 | -------------------------------------------------------------------------------- /utils/captcha/views.py: -------------------------------------------------------------------------------- 1 | from . import Captcha 2 | from ..api import APIView 3 | from ..shortcuts import img2base64 4 | 5 | 6 | class CaptchaAPIView(APIView): 7 | def get(self, request): 8 | return self.success(img2base64(Captcha(request).get())) 9 | -------------------------------------------------------------------------------- /deploy/test_case_rsync/rsyncd.conf: -------------------------------------------------------------------------------- 1 | port = 873 2 | uid = root 3 | gid = root 4 | use chroot = yes 5 | read only = yes 6 | log file = /log/rsyncd.log 7 | 8 | [testcase] 9 | path = /test_case/ 10 | list = yes 11 | auth users = ojrsync 12 | secrets file = /etc/rsyncd.passwd 13 | -------------------------------------------------------------------------------- /account/urls/admin.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from ..views.admin import UserAdminAPI, GenerateUserAPI 4 | 5 | urlpatterns = [ 6 | url(r"^user/?$", UserAdminAPI.as_view(), name="user_admin_api"), 7 | url(r"^generate_user/?$", GenerateUserAPI.as_view(), name="generate_user_api"), 8 | ] 9 | -------------------------------------------------------------------------------- /utils/tasks.py: -------------------------------------------------------------------------------- 1 | import os 2 | import dramatiq 3 | 4 | from utils.shortcuts import DRAMATIQ_WORKER_ARGS 5 | 6 | 7 | @dramatiq.actor(**DRAMATIQ_WORKER_ARGS()) 8 | def delete_files(*args): 9 | for item in args: 10 | try: 11 | os.remove(item) 12 | except Exception: 13 | pass 14 | -------------------------------------------------------------------------------- /utils/models.py: -------------------------------------------------------------------------------- 1 | from django.db.models import JSONField # NOQA 2 | from django.db import models 3 | 4 | from utils.xss_filter import XSSHtml 5 | 6 | 7 | class RichTextField(models.TextField): 8 | def get_prep_value(self, value): 9 | with XSSHtml() as parser: 10 | return parser.clean(value or "") 11 | -------------------------------------------------------------------------------- /utils/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from .views import SimditorImageUploadAPIView, SimditorFileUploadAPIView 4 | 5 | urlpatterns = [ 6 | url(r"^upload_image/?$", SimditorImageUploadAPIView.as_view(), name="upload_image"), 7 | url(r"^upload_file/?$", SimditorFileUploadAPIView.as_view(), name="upload_file") 8 | ] 9 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 在提交issue之前请 2 | 3 | - 认真阅读文档 http://docs.onlinejudge.me/#/ 4 | - 搜索和查看历史issues 5 | - 安全类问题请不要在 GitHub 上公布,请发送邮件到 `admin@qduoj.com`,根据漏洞危害程度发送红包感谢。 6 | 7 | 然后提交issue请写清楚下列事项 8 | 9 |  - 进行什么操作的时候遇到了什么问题,最好能有复现步骤 10 |  - 错误提示是什么,如果看不到错误提示,请去data文件夹查看相应log文件。大段的错误提示请包在代码块标记里面。 11 | - 你尝试修复问题的操作 12 | - 页面问题请写清浏览器版本,尽量有截图 13 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "oj.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | import django 10 | sys.stdout.write("Django VERSION " + str(django.VERSION) + "\n") 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /announcement/views/oj.py: -------------------------------------------------------------------------------- 1 | from utils.api import APIView 2 | 3 | from announcement.models import Announcement 4 | from announcement.serializers import AnnouncementSerializer 5 | 6 | 7 | class AnnouncementAPI(APIView): 8 | def get(self, request): 9 | announcements = Announcement.objects.filter(visible=True) 10 | return self.success(self.paginate_data(request, announcements, AnnouncementSerializer)) 11 | -------------------------------------------------------------------------------- /deploy/nginx/locations.conf: -------------------------------------------------------------------------------- 1 | location /public { 2 | root /data; 3 | } 4 | 5 | location /api { 6 | include api_proxy.conf; 7 | } 8 | 9 | location /admin { 10 | root /app/dist/admin; 11 | try_files $uri $uri/ /index.html =404; 12 | } 13 | 14 | location /.well-known { 15 | alias /data/ssl/.well-known; 16 | } 17 | 18 | location / { 19 | root /app/dist; 20 | try_files $uri $uri/ /index.html =404; 21 | } 22 | -------------------------------------------------------------------------------- /conf/urls/oj.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from ..views import JudgeServerHeartbeatAPI, LanguagesAPI, WebsiteConfigAPI 4 | 5 | urlpatterns = [ 6 | url(r"^website/?$", WebsiteConfigAPI.as_view(), name="website_info_api"), 7 | url(r"^judge_server_heartbeat/?$", JudgeServerHeartbeatAPI.as_view(), name="judge_server_heartbeat_api"), 8 | url(r"^languages/?$", LanguagesAPI.as_view(), name="language_list_api") 9 | ] 10 | -------------------------------------------------------------------------------- /deploy/health_check.py: -------------------------------------------------------------------------------- 1 | import xmlrpc.client 2 | 3 | if __name__ == "__main__": 4 | try: 5 | with xmlrpc.client.ServerProxy("http://localhost:9005/RPC2") as server: 6 | info = server.supervisor.getAllProcessInfo() 7 | error_states = list(filter(lambda x: x["state"] != 20, info)) 8 | exit(len(error_states)) 9 | except Exception as e: 10 | print(e.with_traceback()) 11 | exit(1) 12 | -------------------------------------------------------------------------------- /oj/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for qduoj project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "oj.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /problem/urls/oj.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from ..views.oj import ProblemTagAPI, ProblemAPI, ContestProblemAPI, PickOneAPI 4 | 5 | urlpatterns = [ 6 | url(r"^problem/tags/?$", ProblemTagAPI.as_view(), name="problem_tag_list_api"), 7 | url(r"^problem/?$", ProblemAPI.as_view(), name="problem_api"), 8 | url(r"^pickone/?$", PickOneAPI.as_view(), name="pick_one_api"), 9 | url(r"^contest/problem/?$", ContestProblemAPI.as_view(), name="contest_problem_api"), 10 | ] 11 | -------------------------------------------------------------------------------- /judge/tasks.py: -------------------------------------------------------------------------------- 1 | import dramatiq 2 | 3 | from account.models import User 4 | from submission.models import Submission 5 | from judge.dispatcher import JudgeDispatcher 6 | from utils.shortcuts import DRAMATIQ_WORKER_ARGS 7 | 8 | 9 | @dramatiq.actor(**DRAMATIQ_WORKER_ARGS()) 10 | def judge_task(submission_id, problem_id): 11 | uid = Submission.objects.get(id=submission_id).user_id 12 | if User.objects.get(id=uid).is_disabled: 13 | return 14 | JudgeDispatcher(submission_id, problem_id).judge() 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.8" 4 | services: 5 | - docker 6 | - postgresql 7 | install: 8 | - pip install -r deploy/requirements.txt 9 | - echo `cat /dev/urandom | head -1 | md5sum | head -c 32` > data/config/secret.key 10 | - ./init_db.sh 11 | script: 12 | - docker ps -a 13 | - flake8 --config=./.flake8 . 14 | - coverage run --include="$PWD/*" manage.py test 15 | - coverage report 16 | notifications: 17 | slack: onlinejudgeteam:BzBz8UFgmS5crpiblof17K2W 18 | -------------------------------------------------------------------------------- /problem/migrations/0014_problem_share_submission.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-03-13 09:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('problem', '0013_problem_io_mode'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='problem', 15 | name='share_submission', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /deploy/requirements.txt: -------------------------------------------------------------------------------- 1 | coverage==6.5.0 2 | django-cas-ng==5.0.1 3 | django-dbconn-retry==0.1.7 4 | django-dramatiq==0.11.6 5 | django-redis==5.4.0 6 | Django==3.2.25 7 | djangorestframework==3.14.0 8 | dramatiq==1.16.0 9 | entrypoints==0.4 10 | Envelopes==0.4 11 | flake8-coding==1.3.2 12 | flake8-quotes==3.3.2 13 | flake8==7.0.0 14 | gunicorn==21.2.0 15 | jsonfield==3.1.0 16 | otpauth==1.0.1 17 | pillow==10.2.0 18 | psycopg2==2.9.9 19 | python-dateutil==2.8.2 20 | qrcode==7.4.2 21 | raven==6.10.0 22 | XlsxWriter==3.1.9 23 | -------------------------------------------------------------------------------- /options/migrations/0003_migrate_languages_options.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.3 on 2018-05-01 04:36 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('options', '0002_auto_20180501_0436'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RunSQL(""" 16 | DELETE FROM options_sysoptions WHERE key = 'languages'; 17 | """) 18 | ] 19 | -------------------------------------------------------------------------------- /utils/api/_serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | 4 | class UsernameSerializer(serializers.Serializer): 5 | id = serializers.IntegerField() 6 | username = serializers.CharField() 7 | real_name = serializers.SerializerMethodField() 8 | 9 | def __init__(self, *args, **kwargs): 10 | self.need_real_name = kwargs.pop("need_real_name", False) 11 | super().__init__(*args, **kwargs) 12 | 13 | def get_real_name(self, obj): 14 | return obj.userprofile.real_name if self.need_real_name else None 15 | -------------------------------------------------------------------------------- /account/migrations/0011_auto_20180501_0456.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.3 on 2018-05-01 04:56 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('account', '0010_auto_20180501_0436'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='user', 17 | name='email', 18 | field=models.TextField(null=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /submission/urls/oj.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from ..views.oj import SubmissionAPI, SubmissionListAPI, ContestSubmissionListAPI, SubmissionExistsAPI 4 | 5 | urlpatterns = [ 6 | url(r"^submission/?$", SubmissionAPI.as_view(), name="submission_api"), 7 | url(r"^submissions/?$", SubmissionListAPI.as_view(), name="submission_list_api"), 8 | url(r"^submission_exists/?$", SubmissionExistsAPI.as_view(), name="submission_exists"), 9 | url(r"^contest_submissions/?$", ContestSubmissionListAPI.as_view(), name="contest_submission_list_api"), 10 | ] 11 | -------------------------------------------------------------------------------- /account/migrations/0012_userprofile_language.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2018-07-15 02:06 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('account', '0011_auto_20180501_0456'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='userprofile', 17 | name='language', 18 | field=models.TextField(null=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /announcement/migrations/0003_auto_20180501_0436.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.3 on 2018-05-01 04:36 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('announcement', '0002_auto_20171011_1214'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='announcement', 17 | name='title', 18 | field=models.TextField(), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /options/migrations/0002_auto_20180501_0436.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.3 on 2018-05-01 04:36 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('options', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='sysoptions', 17 | name='key', 18 | field=models.TextField(db_index=True, unique=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /conf/migrations/0003_judgeserver_is_disabled.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-12-24 03:44 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('conf', '0002_auto_20171011_1214'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='judgeserver', 17 | name='is_disabled', 18 | field=models.BooleanField(default=False), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /contest/urls/admin.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from ..views.admin import ContestAnnouncementAPI, ContestAPI, ACMContestHelper, DownloadContestSubmissions 4 | 5 | urlpatterns = [ 6 | url(r"^contest/?$", ContestAPI.as_view(), name="contest_admin_api"), 7 | url(r"^contest/announcement/?$", ContestAnnouncementAPI.as_view(), name="contest_announcement_admin_api"), 8 | url(r"^contest/acm_helper/?$", ACMContestHelper.as_view(), name="acm_contest_helper"), 9 | url(r"^download_submissions/?$", DownloadContestSubmissions.as_view(), name="acm_contest_helper"), 10 | ] 11 | -------------------------------------------------------------------------------- /problem/migrations/0010_problem_spj_compile_ok.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-11-16 12:42 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('problem', '0009_auto_20171011_1214'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='problem', 17 | name='spj_compile_ok', 18 | field=models.BooleanField(default=False), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /contest/migrations/0007_contestannouncement_visible.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-11-06 09:02 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('contest', '0006_auto_20171011_1214'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='contestannouncement', 17 | name='visible', 18 | field=models.BooleanField(default=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /submission/migrations/0006_auto_20170830_1154.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-08-30 11:54 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('submission', '0005_submission_username'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='submission', 17 | name='result', 18 | field=models.IntegerField(db_index=True, default=6), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /submission/migrations/0008_submission_ip.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-11-10 06:57 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('submission', '0007_auto_20170923_1318'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='submission', 17 | name='ip', 18 | field=models.CharField(blank=True, max_length=32, null=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /account/migrations/0009_auto_20171125_1514.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-11-25 15:14 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('account', '0008_auto_20171011_1214'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='userprofile', 17 | name='avatar', 18 | field=models.CharField(default='/public/avatar/default.png', max_length=256), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /announcement/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from account.models import User 4 | from utils.models import RichTextField 5 | 6 | 7 | class Announcement(models.Model): 8 | title = models.TextField() 9 | # HTML 10 | content = RichTextField() 11 | create_time = models.DateTimeField(auto_now_add=True) 12 | created_by = models.ForeignKey(User, on_delete=models.CASCADE) 13 | last_update_time = models.DateTimeField(auto_now=True) 14 | visible = models.BooleanField(default=True) 15 | 16 | class Meta: 17 | db_table = "announcement" 18 | ordering = ("-create_time",) 19 | -------------------------------------------------------------------------------- /problem/migrations/0013_problem_io_mode.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-03-12 07:13 2 | 3 | import django.contrib.postgres.fields.jsonb 4 | from django.db import migrations 5 | import problem.models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('problem', '0012_auto_20180501_0436'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='problem', 17 | name='io_mode', 18 | field=django.contrib.postgres.fields.jsonb.JSONField(default=problem.models._default_io_mode), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /problem/migrations/0002_problem__id.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.12 on 2017-02-09 08:45 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('problem', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='problem', 17 | name='_id', 18 | field=models.CharField(db_index=True, default='1', max_length=24, unique=True), 19 | preserve_default=False, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /submission/migrations/0005_submission_username.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-08-26 03:47 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('submission', '0002_auto_20170509_1203'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='submission', 17 | name='username', 18 | field=models.CharField(default="", max_length=30), 19 | preserve_default=False, 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /oj/production_settings.py: -------------------------------------------------------------------------------- 1 | from utils.shortcuts import get_env 2 | 3 | DATABASES = { 4 | 'default': { 5 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 6 | 'HOST': get_env("POSTGRES_HOST", "oj-postgres"), 7 | 'PORT': get_env("POSTGRES_PORT", "5432"), 8 | 'NAME': get_env("POSTGRES_DB"), 9 | 'USER': get_env("POSTGRES_USER"), 10 | 'PASSWORD': get_env("POSTGRES_PASSWORD") 11 | } 12 | } 13 | 14 | REDIS_CONF = { 15 | "host": get_env("REDIS_HOST", "oj-redis"), 16 | "port": get_env("REDIS_PORT", "6379") 17 | } 18 | 19 | DEBUG = False 20 | 21 | ALLOWED_HOSTS = ['*'] 22 | 23 | DATA_DIR = "/data" 24 | -------------------------------------------------------------------------------- /contest/migrations/0008_contest_allowed_ip_ranges.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-11-10 06:57 3 | from __future__ import unicode_literals 4 | 5 | import django.contrib.postgres.fields.jsonb 6 | from django.db import migrations 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('contest', '0007_contestannouncement_visible'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='contest', 18 | name='allowed_ip_ranges', 19 | field=django.contrib.postgres.fields.jsonb.JSONField(default=list), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /contest/migrations/0004_auto_20170717_1324.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.6 on 2017-07-17 13:24 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('contest', '0003_auto_20170217_0820'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='contest', 17 | options={'ordering': ('-create_time',)}, 18 | ), 19 | migrations.AlterModelOptions( 20 | name='contestannouncement', 21 | options={'ordering': ('-create_time',)}, 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /deploy/test_case_rsync/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | oj-rsync-master: 4 | image: oj_rsync 5 | container_name: oj-rsync 6 | volumes: 7 | - $PWD/data/backend/test_case:/test_case:ro 8 | - $PWD/data/rsync_master:/log 9 | environment: 10 | - RSYNC_MODE=master 11 | - RSYNC_USER=ojrsync 12 | - RSYNC_PASSWORD=CHANGE_THIS_PASSWORD 13 | ports: 14 | - "0.0.0.0:873:873" 15 | 16 | oj-rsync-slave: 17 | image: oj-rsync 18 | volumes: 19 | - $PWD/test_case:/test_case 20 | - $PWD/rsync_slave:/log 21 | environment: 22 | - RSYNC_MODE=slave 23 | - RSYNC_USER=ojrsync 24 | - RSYNC_PASSWORD=CHANGE_THIS_PASSWORD 25 | -------------------------------------------------------------------------------- /announcement/migrations/0002_auto_20171011_1214.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-10-11 12:14 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('announcement', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='announcement', 17 | name='title', 18 | field=models.CharField(max_length=64), 19 | ), 20 | migrations.AlterModelOptions( 21 | name='announcement', 22 | options={'ordering': ('-create_time',)}, 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /init_db.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -x 3 | 4 | if [[ ! -f manage.py ]]; then 5 | echo "No manage.py, wrong location" 6 | exit 1 7 | fi 8 | 9 | sleep 2 10 | docker rm -f oj-postgres-dev oj-redis-dev 11 | docker run -it -d -e POSTGRES_DB=onlinejudge -e POSTGRES_USER=onlinejudge -e POSTGRES_PASSWORD=onlinejudge -p 127.0.0.1:5435:5432 --name oj-postgres-dev postgres:10 12 | docker run -it -d -p 127.0.0.1:6380:6379 --name oj-redis-dev redis:4.0-alpine 13 | 14 | if [ "$1" = "--migrate" ]; then 15 | sleep 3 16 | echo `cat /dev/urandom | head -1 | md5sum | head -c 32` > data/config/secret.key 17 | python manage.py migrate 18 | python manage.py inituser --username root --password rootroot --action create_super_admin 19 | fi -------------------------------------------------------------------------------- /contest/migrations/0010_auto_20190326_0201.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-03-26 02:01 2 | 3 | from django.conf import settings 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 11 | ('contest', '0009_auto_20180501_0436'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterUniqueTogether( 16 | name='acmcontestrank', 17 | unique_together={('user', 'contest')}, 18 | ), 19 | migrations.AlterUniqueTogether( 20 | name='oicontestrank', 21 | unique_together={('user', 'contest')}, 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /account/tasks.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import dramatiq 3 | 4 | from options.options import SysOptions 5 | from utils.shortcuts import send_email, DRAMATIQ_WORKER_ARGS 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | @dramatiq.actor(**DRAMATIQ_WORKER_ARGS(max_retries=3)) 11 | def send_email_async(from_name, to_email, to_name, subject, content): 12 | if not SysOptions.smtp_config: 13 | return 14 | try: 15 | send_email(smtp_config=SysOptions.smtp_config, 16 | from_name=from_name, 17 | to_email=to_email, 18 | to_name=to_name, 19 | subject=subject, 20 | content=content) 21 | except Exception as e: 22 | logger.exception(e) 23 | -------------------------------------------------------------------------------- /account/migrations/0002_auto_20170209_1028.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.12 on 2017-02-09 10:28 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('account', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='user', 17 | name='problem_permission', 18 | field=models.CharField(default='None', max_length=24), 19 | ), 20 | migrations.AlterField( 21 | model_name='user', 22 | name='admin_type', 23 | field=models.CharField(default='Regular User', max_length=24), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /oj/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | 3 | urlpatterns = [ 4 | url(r"^api/", include("account.urls.oj")), 5 | url(r"^api/admin/", include("account.urls.admin")), 6 | url(r"^api/", include("announcement.urls.oj")), 7 | url(r"^api/admin/", include("announcement.urls.admin")), 8 | url(r"^api/", include("conf.urls.oj")), 9 | url(r"^api/admin/", include("conf.urls.admin")), 10 | url(r"^api/", include("problem.urls.oj")), 11 | url(r"^api/admin/", include("problem.urls.admin")), 12 | url(r"^api/", include("contest.urls.oj")), 13 | url(r"^api/admin/", include("contest.urls.admin")), 14 | url(r"^api/", include("submission.urls.oj")), 15 | url(r"^api/admin/", include("submission.urls.admin")), 16 | url(r"^api/admin/", include("utils.urls")), 17 | ] 18 | -------------------------------------------------------------------------------- /problem/migrations/0005_auto_20170815_1258.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.6 on 2017-08-15 12:58 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | import jsonfield.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('problem', '0004_auto_20170501_0637'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='contestproblem', 18 | name='statistic_info', 19 | field=jsonfield.fields.JSONField(default={}), 20 | ), 21 | migrations.AddField( 22 | model_name='problem', 23 | name='statistic_info', 24 | field=jsonfield.fields.JSONField(default={}), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /conf/urls/admin.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from ..views import SMTPAPI, JudgeServerAPI, WebsiteConfigAPI, TestCasePruneAPI, SMTPTestAPI 4 | from ..views import ReleaseNotesAPI, DashboardInfoAPI 5 | 6 | urlpatterns = [ 7 | url(r"^smtp/?$", SMTPAPI.as_view(), name="smtp_admin_api"), 8 | url(r"^smtp_test/?$", SMTPTestAPI.as_view(), name="smtp_test_api"), 9 | url(r"^website/?$", WebsiteConfigAPI.as_view(), name="website_config_api"), 10 | url(r"^judge_server/?$", JudgeServerAPI.as_view(), name="judge_server_api"), 11 | url(r"^prune_test_case/?$", TestCasePruneAPI.as_view(), name="prune_test_case_api"), 12 | url(r"^versions/?$", ReleaseNotesAPI.as_view(), name="get_release_notes_api"), 13 | url(r"^dashboard_info", DashboardInfoAPI.as_view(), name="dashboard_info_api"), 14 | ] 15 | -------------------------------------------------------------------------------- /oj/dev_settings.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import os 3 | from utils.shortcuts import get_env 4 | 5 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 | 7 | DATABASES = { 8 | 'default': { 9 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 10 | 'HOST': get_env('POSTGRES_HOST', '127.0.0.1'), 11 | 'PORT': get_env('POSTGRES_PORT', '5435'), 12 | 'NAME': get_env('POSTGRES_DB', 'onlinejudge'), 13 | 'USER': get_env('POSTGRES_USER', 'onlinejudge'), 14 | 'PASSWORD': get_env('POSTGRES_PASSWORD', 'onlinejudge') 15 | } 16 | } 17 | 18 | REDIS_CONF = { 19 | 'host': get_env('REDIS_HOST', '127.0.0.1'), 20 | 'port': get_env('REDIS_PORT', '6380') 21 | } 22 | 23 | 24 | DEBUG = True 25 | 26 | ALLOWED_HOSTS = ["*"] 27 | 28 | DATA_DIR = f"{BASE_DIR}/data" 29 | -------------------------------------------------------------------------------- /submission/migrations/0009_delete_user_output.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations 5 | 6 | 7 | def delete_user_output(apps, schema_editor): 8 | Submission = apps.get_model("submission", "Submission") 9 | for item in Submission.objects.all(): 10 | if "data" in item.info and isinstance(item.info["data"], list): 11 | for index in range(len(item.info["data"])): 12 | item.info["data"][index]["output"] = "" 13 | item.save() 14 | 15 | 16 | class Migration(migrations.Migration): 17 | 18 | dependencies = [ 19 | ('submission', '0008_submission_ip'), 20 | ] 21 | 22 | operations = [ 23 | migrations.RunPython(delete_user_output, reverse_code=migrations.RunPython.noop) 24 | ] 25 | -------------------------------------------------------------------------------- /options/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-10-23 08:11 3 | from __future__ import unicode_literals 4 | 5 | import django.contrib.postgres.fields.jsonb 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='SysOptions', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('key', models.CharField(db_index=True, max_length=128, unique=True)), 22 | ('value', django.contrib.postgres.fields.jsonb.JSONField()), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /contest/urls/oj.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from ..views.oj import ContestAnnouncementListAPI 4 | from ..views.oj import ContestPasswordVerifyAPI, ContestAccessAPI 5 | from ..views.oj import ContestListAPI, ContestAPI 6 | from ..views.oj import ContestRankAPI 7 | 8 | urlpatterns = [ 9 | url(r"^contests/?$", ContestListAPI.as_view(), name="contest_list_api"), 10 | url(r"^contest/?$", ContestAPI.as_view(), name="contest_api"), 11 | url(r"^contest/password/?$", ContestPasswordVerifyAPI.as_view(), name="contest_password_api"), 12 | url(r"^contest/announcement/?$", ContestAnnouncementListAPI.as_view(), name="contest_announcement_api"), 13 | url(r"^contest/access/?$", ContestAccessAPI.as_view(), name="contest_access_api"), 14 | url(r"^contest_rank/?$", ContestRankAPI.as_view(), name="contest_rank_api"), 15 | ] 16 | -------------------------------------------------------------------------------- /run_test.py: -------------------------------------------------------------------------------- 1 | import getopt 2 | import os 3 | import sys 4 | 5 | opts, args = getopt.getopt(sys.argv[1:], "cm:", ["coverage=", "module="]) 6 | 7 | is_coverage = False 8 | test_module = "" 9 | setting = "oj.settings" 10 | 11 | for opt, arg in opts: 12 | if opt in ["-c", "--coverage"]: 13 | is_coverage = True 14 | if opt in ["-m", "--module"]: 15 | test_module = arg 16 | 17 | print("Coverage: {cov}".format(cov=is_coverage)) 18 | print("Module: {mod}".format(mod=(test_module if test_module else "All"))) 19 | 20 | print("running flake8...") 21 | if os.system("flake8 --statistics ."): 22 | exit() 23 | 24 | ret = os.system('coverage run --include="$PWD/*" manage.py test {module} --settings={setting}'.format(module=test_module, setting=setting)) 25 | 26 | if not ret and is_coverage: 27 | os.system("coverage html && open htmlcov/index.html") 28 | -------------------------------------------------------------------------------- /utils/constants.py: -------------------------------------------------------------------------------- 1 | class Choices: 2 | @classmethod 3 | def choices(cls): 4 | d = cls.__dict__ 5 | return [d[item] for item in d.keys() if not item.startswith("__")] 6 | 7 | 8 | class ContestType: 9 | PUBLIC_CONTEST = "Public" 10 | PASSWORD_PROTECTED_CONTEST = "Password Protected" 11 | 12 | 13 | class ContestStatus: 14 | CONTEST_NOT_START = "1" 15 | CONTEST_ENDED = "-1" 16 | CONTEST_UNDERWAY = "0" 17 | 18 | 19 | class ContestRuleType(Choices): 20 | ACM = "ACM" 21 | OI = "OI" 22 | 23 | 24 | class CacheKey: 25 | waiting_queue = "waiting_queue" 26 | contest_rank_cache = "contest_rank_cache" 27 | website_config = "website_config" 28 | 29 | 30 | class Difficulty(Choices): 31 | LOW = "Low" 32 | MID = "Mid" 33 | HIGH = "High" 34 | 35 | 36 | CONTEST_PASSWORD_SESSION_KEY = "contest_password" 37 | -------------------------------------------------------------------------------- /announcement/serializers.py: -------------------------------------------------------------------------------- 1 | from utils.api import serializers 2 | from utils.api._serializers import UsernameSerializer 3 | 4 | from .models import Announcement 5 | 6 | 7 | class CreateAnnouncementSerializer(serializers.Serializer): 8 | title = serializers.CharField(max_length=64) 9 | content = serializers.CharField(max_length=1024 * 1024 * 8) 10 | visible = serializers.BooleanField() 11 | 12 | 13 | class AnnouncementSerializer(serializers.ModelSerializer): 14 | created_by = UsernameSerializer() 15 | 16 | class Meta: 17 | model = Announcement 18 | fields = "__all__" 19 | 20 | 21 | class EditAnnouncementSerializer(serializers.Serializer): 22 | id = serializers.IntegerField() 23 | title = serializers.CharField(max_length=64) 24 | content = serializers.CharField(max_length=1024 * 1024 * 8) 25 | visible = serializers.BooleanField() 26 | -------------------------------------------------------------------------------- /utils/cache.py: -------------------------------------------------------------------------------- 1 | from django.core.cache import cache, caches # noqa 2 | from django.conf import settings # noqa 3 | 4 | from django_redis.cache import RedisCache 5 | from django_redis.client.default import DefaultClient 6 | 7 | 8 | class MyRedisClient(DefaultClient): 9 | def __getattr__(self, item): 10 | client = self.get_client(write=True) 11 | return getattr(client, item) 12 | 13 | def redis_incr(self, key, count=1): 14 | """ 15 | django 默认的 incr 在 key 不存在时候会抛异常 16 | """ 17 | client = self.get_client(write=True) 18 | return client.incr(key, count) 19 | 20 | 21 | class MyRedisCache(RedisCache): 22 | def __init__(self, server, params): 23 | super().__init__(server, params) 24 | self._client_cls = MyRedisClient 25 | 26 | def __getattr__(self, item): 27 | return getattr(self.client, item) 28 | -------------------------------------------------------------------------------- /submission/views/admin.py: -------------------------------------------------------------------------------- 1 | from account.decorators import super_admin_required 2 | from judge.tasks import judge_task 3 | # from judge.dispatcher import JudgeDispatcher 4 | from utils.api import APIView 5 | from ..models import Submission 6 | 7 | 8 | class SubmissionRejudgeAPI(APIView): 9 | @super_admin_required 10 | def get(self, request): 11 | id = request.GET.get("id") 12 | if not id: 13 | return self.error("Parameter error, id is required") 14 | try: 15 | submission = Submission.objects.select_related("problem").get(id=id, contest_id__isnull=True) 16 | except Submission.DoesNotExist: 17 | return self.error("Submission does not exists") 18 | submission.statistic_info = {} 19 | submission.save() 20 | 21 | judge_task.send(submission.id, submission.problem.id) 22 | return self.success() 23 | -------------------------------------------------------------------------------- /deploy/test_case_rsync/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | slave_runner() 4 | { 5 | while true 6 | do 7 | rsync -avzP --delete --progress --password-file=/etc/rsync_slave.passwd $RSYNC_USER@$RSYNC_MASTER_ADDR::testcase /test_case >> /log/rsync_slave.log 8 | sleep 5 9 | done 10 | } 11 | 12 | master_runner() 13 | { 14 | rsync --daemon --config=/etc/rsyncd.conf 15 | while true 16 | do 17 | sleep 60 18 | done 19 | } 20 | 21 | if [ "$RSYNC_MODE" = "master" ]; then 22 | if [ ! -f "/etc/rsyncd.passwd" ]; then 23 | echo "$RSYNC_USER:$RSYNC_PASSWORD" > /etc/rsyncd.passwd 24 | fi 25 | chmod 600 /etc/rsyncd.passwd 26 | master_runner 27 | else 28 | if [ ! -f "/etc/rsync_slave.passwd" ]; then 29 | echo "$RSYNC_PASSWORD" > /etc/rsync_slave.passwd 30 | fi 31 | chmod 600 /etc/rsync_slave.passwd 32 | slave_runner 33 | fi 34 | -------------------------------------------------------------------------------- /account/migrations/0003_userprofile_total_score.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.6 on 2017-08-20 02:03 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('account', '0002_auto_20170209_1028'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='userprofile', 17 | name='total_score', 18 | field=models.BigIntegerField(default=0), 19 | ), 20 | migrations.RenameField( 21 | model_name='userprofile', 22 | old_name='accepted_problem_number', 23 | new_name='accepted_number', 24 | ), 25 | migrations.RemoveField( 26 | model_name='userprofile', 27 | name='time_zone', 28 | ) 29 | ] 30 | -------------------------------------------------------------------------------- /conf/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils import timezone 3 | 4 | 5 | class JudgeServer(models.Model): 6 | hostname = models.TextField() 7 | ip = models.TextField(null=True) 8 | judger_version = models.TextField() 9 | cpu_core = models.IntegerField() 10 | memory_usage = models.FloatField() 11 | cpu_usage = models.FloatField() 12 | last_heartbeat = models.DateTimeField() 13 | create_time = models.DateTimeField(auto_now_add=True) 14 | task_number = models.IntegerField(default=0) 15 | service_url = models.TextField(null=True) 16 | is_disabled = models.BooleanField(default=False) 17 | 18 | @property 19 | def status(self): 20 | # 增加一秒延时,提高对网络环境的适应性 21 | if (timezone.now() - self.last_heartbeat).total_seconds() > 6: 22 | return "abnormal" 23 | return "normal" 24 | 25 | class Meta: 26 | db_table = "judge_server" 27 | -------------------------------------------------------------------------------- /contest/migrations/0002_auto_20170209_0845.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.12 on 2017-02-09 08:45 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('contest', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='contestproblem', 17 | name='_id', 18 | field=models.CharField(db_index=True, default='1', max_length=24), 19 | preserve_default=False, 20 | ), 21 | migrations.RemoveField( 22 | model_name='contestproblem', 23 | name='sort_index', 24 | ), 25 | migrations.AlterUniqueTogether( 26 | name='contestproblem', 27 | unique_together=set([('_id', 'contest')]), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /problem/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | from functools import lru_cache 3 | 4 | 5 | TEMPLATE_BASE = """//PREPEND BEGIN 6 | {} 7 | //PREPEND END 8 | 9 | //TEMPLATE BEGIN 10 | {} 11 | //TEMPLATE END 12 | 13 | //APPEND BEGIN 14 | {} 15 | //APPEND END""" 16 | 17 | 18 | @lru_cache(maxsize=100) 19 | def parse_problem_template(template_str): 20 | prepend = re.findall(r"//PREPEND BEGIN\n([\s\S]+?)//PREPEND END", template_str) 21 | template = re.findall(r"//TEMPLATE BEGIN\n([\s\S]+?)//TEMPLATE END", template_str) 22 | append = re.findall(r"//APPEND BEGIN\n([\s\S]+?)//APPEND END", template_str) 23 | return {"prepend": prepend[0] if prepend else "", 24 | "template": template[0] if template else "", 25 | "append": append[0] if append else ""} 26 | 27 | 28 | @lru_cache(maxsize=100) 29 | def build_problem_template(prepend, template, append): 30 | return TEMPLATE_BASE.format(prepend, template, append) 31 | -------------------------------------------------------------------------------- /contest/migrations/0005_auto_20170823_0918.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.6 on 2017-08-23 09:18 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('contest', '0004_auto_20170717_1324'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameField( 16 | model_name='acmcontestrank', 17 | old_name='total_ac_number', 18 | new_name='accepted_number', 19 | ), 20 | migrations.RenameField( 21 | model_name='acmcontestrank', 22 | old_name='total_submission_number', 23 | new_name='submission_number', 24 | ), 25 | migrations.RenameField( 26 | model_name='oicontestrank', 27 | old_name='total_submission_number', 28 | new_name='submission_number', 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /contest/migrations/0003_auto_20170217_0820.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.12 on 2017-02-17 08:20 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('contest', '0002_auto_20170209_0845'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterUniqueTogether( 16 | name='contestproblem', 17 | unique_together=set([]), 18 | ), 19 | migrations.RemoveField( 20 | model_name='contestproblem', 21 | name='contest', 22 | ), 23 | migrations.RemoveField( 24 | model_name='contestproblem', 25 | name='created_by', 26 | ), 27 | migrations.RemoveField( 28 | model_name='contestproblem', 29 | name='tags', 30 | ), 31 | migrations.DeleteModel( 32 | name='ContestProblem', 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /contest/migrations/0006_auto_20171011_1214.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-10-11 12:14 3 | from __future__ import unicode_literals 4 | 5 | import django.contrib.postgres.fields.jsonb 6 | from django.db import migrations 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('contest', '0005_auto_20170823_0918'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='acmcontestrank', 18 | name='submission_info', 19 | field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), 20 | ), 21 | migrations.AlterField( 22 | model_name='oicontestrank', 23 | name='submission_info', 24 | field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), 25 | ), 26 | migrations.AlterModelOptions( 27 | name='contest', 28 | options={'ordering': ('-start_time',)}, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /submission/migrations/0011_fix_submission_number.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations 5 | 6 | 7 | def fix_rejudge_bugs(apps, schema_editor): 8 | Submission = apps.get_model("submission", "Submission") 9 | User = apps.get_model("account", "User") 10 | 11 | for user in User.objects.all(): 12 | submissions = Submission.objects.filter(user_id=user.id, contest__isnull=True) 13 | profile = user.userprofile 14 | profile.submission_number = submissions.count() 15 | profile.accepted_number = submissions.filter(result=0).count() 16 | profile.save(update_fields=["submission_number", "accepted_number"]) 17 | 18 | 19 | class Migration(migrations.Migration): 20 | dependencies = [ 21 | ('submission', '0009_delete_user_output'), 22 | ('problem', '0010_problem_spj_compile_ok'), 23 | ] 24 | 25 | operations = [ 26 | migrations.RunPython(fix_rejudge_bugs, reverse_code=migrations.RunPython.noop) 27 | ] 28 | -------------------------------------------------------------------------------- /contest/migrations/0009_auto_20180501_0436.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.3 on 2018-05-01 04:36 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('contest', '0008_contest_allowed_ip_ranges'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='contest', 17 | name='password', 18 | field=models.TextField(null=True), 19 | ), 20 | migrations.AlterField( 21 | model_name='contest', 22 | name='rule_type', 23 | field=models.TextField(), 24 | ), 25 | migrations.AlterField( 26 | model_name='contest', 27 | name='title', 28 | field=models.TextField(), 29 | ), 30 | migrations.AlterField( 31 | model_name='contestannouncement', 32 | name='title', 33 | field=models.TextField(), 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /conf/migrations/0004_auto_20180501_0436.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.3 on 2018-05-01 04:36 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('conf', '0003_judgeserver_is_disabled'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='judgeserver', 17 | name='hostname', 18 | field=models.TextField(), 19 | ), 20 | migrations.AlterField( 21 | model_name='judgeserver', 22 | name='ip', 23 | field=models.TextField(null=True), 24 | ), 25 | migrations.AlterField( 26 | model_name='judgeserver', 27 | name='judger_version', 28 | field=models.TextField(), 29 | ), 30 | migrations.AlterField( 31 | model_name='judgeserver', 32 | name='service_url', 33 | field=models.TextField(null=True), 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /problem/migrations/0006_auto_20170823_0918.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.6 on 2017-08-23 09:18 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('problem', '0005_auto_20170815_1258'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameField( 16 | model_name='contestproblem', 17 | old_name='total_accepted_number', 18 | new_name='accepted_number', 19 | ), 20 | migrations.RenameField( 21 | model_name='contestproblem', 22 | old_name='total_submit_number', 23 | new_name='submission_number', 24 | ), 25 | migrations.RenameField( 26 | model_name='problem', 27 | old_name='total_accepted_number', 28 | new_name='accepted_number', 29 | ), 30 | migrations.RenameField( 31 | model_name='problem', 32 | old_name='total_submit_number', 33 | new_name='submission_number', 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present OnineJudge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /problem/migrations/0004_auto_20170501_0637.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.6 on 2017-05-01 06:37 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('problem', '0003_auto_20170217_0820'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='contestproblem', 17 | name='total_accepted_number', 18 | field=models.BigIntegerField(default=0), 19 | ), 20 | migrations.AlterField( 21 | model_name='contestproblem', 22 | name='total_submit_number', 23 | field=models.BigIntegerField(default=0), 24 | ), 25 | migrations.AlterField( 26 | model_name='problem', 27 | name='total_accepted_number', 28 | field=models.BigIntegerField(default=0), 29 | ), 30 | migrations.AlterField( 31 | model_name='problem', 32 | name='total_submit_number', 33 | field=models.BigIntegerField(default=0), 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /problem/urls/admin.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from ..views.admin import (ContestProblemAPI, ProblemAPI, TestCaseAPI, MakeContestProblemPublicAPIView, 4 | CompileSPJAPI, AddContestProblemAPI, ExportProblemAPI, ImportProblemAPI, 5 | FPSProblemImport) 6 | 7 | urlpatterns = [ 8 | url(r"^test_case/?$", TestCaseAPI.as_view(), name="test_case_api"), 9 | url(r"^compile_spj/?$", CompileSPJAPI.as_view(), name="compile_spj"), 10 | url(r"^problem/?$", ProblemAPI.as_view(), name="problem_admin_api"), 11 | url(r"^contest/problem/?$", ContestProblemAPI.as_view(), name="contest_problem_admin_api"), 12 | url(r"^contest_problem/make_public/?$", MakeContestProblemPublicAPIView.as_view(), name="make_public_api"), 13 | url(r"^contest/add_problem_from_public/?$", AddContestProblemAPI.as_view(), name="add_contest_problem_from_public_api"), 14 | url(r"^export_problem/?$", ExportProblemAPI.as_view(), name="export_problem_api"), 15 | url(r"^import_problem/?$", ImportProblemAPI.as_view(), name="import_problem_api"), 16 | url(r"^import_fps/?$", FPSProblemImport.as_view(), name="fps_problem_api"), 17 | ] 18 | -------------------------------------------------------------------------------- /submission/migrations/0012_auto_20180501_0436.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.3 on 2018-05-01 04:36 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import utils.shortcuts 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('submission', '0011_fix_submission_number'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='submission', 18 | name='id', 19 | field=models.TextField(db_index=True, default=utils.shortcuts.rand_str, primary_key=True, serialize=False), 20 | ), 21 | migrations.AlterField( 22 | model_name='submission', 23 | name='ip', 24 | field=models.TextField(null=True), 25 | ), 26 | migrations.AlterField( 27 | model_name='submission', 28 | name='language', 29 | field=models.TextField(), 30 | ), 31 | migrations.AlterField( 32 | model_name='submission', 33 | name='username', 34 | field=models.TextField(), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /submission/migrations/0002_auto_20170509_1203.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.6 on 2017-05-09 12:03 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('submission', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='submission', 17 | name='code', 18 | field=models.TextField(), 19 | ), 20 | migrations.RenameField( 21 | model_name='submission', 22 | old_name='accepted_info', 23 | new_name='statistic_info', 24 | ), 25 | migrations.RemoveField( 26 | model_name='submission', 27 | name='accepted_time', 28 | ), 29 | migrations.RenameField( 30 | model_name='submission', 31 | old_name='created_time', 32 | new_name='create_time', 33 | ), 34 | migrations.AlterModelOptions( 35 | name='submission', 36 | options={'ordering': ('-create_time',)}, 37 | ) 38 | ] 39 | -------------------------------------------------------------------------------- /account/migrations/0006_user_session_keys.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-09-16 06:22 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import jsonfield.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('account', '0005_auto_20170830_1154'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='user', 18 | name='session_keys', 19 | field=jsonfield.fields.JSONField(default=[]), 20 | ), 21 | migrations.RenameField( 22 | model_name='userprofile', 23 | old_name='phone_number', 24 | new_name='github', 25 | ), 26 | migrations.AlterField( 27 | model_name='userprofile', 28 | name='avatar', 29 | field=models.CharField(default='/static/avatar/default.png', max_length=50), 30 | ), 31 | migrations.AlterField( 32 | model_name='userprofile', 33 | name='github', 34 | field=models.CharField(blank=True, max_length=50, null=True), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /conf/migrations/0002_auto_20171011_1214.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-10-11 12:14 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('conf', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.DeleteModel( 16 | name='JudgeServerToken', 17 | ), 18 | migrations.DeleteModel( 19 | name='SMTPConfig', 20 | ), 21 | migrations.DeleteModel( 22 | name='WebsiteConfig', 23 | ), 24 | migrations.AlterField( 25 | model_name='judgeserver', 26 | name='hostname', 27 | field=models.CharField(max_length=128), 28 | ), 29 | migrations.AlterField( 30 | model_name='judgeserver', 31 | name='judger_version', 32 | field=models.CharField(max_length=32), 33 | ), 34 | migrations.AlterField( 35 | model_name='judgeserver', 36 | name='service_url', 37 | field=models.CharField(blank=True, max_length=256, null=True), 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /account/migrations/0005_auto_20170830_1154.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-08-30 11:54 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import jsonfield.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('account', '0003_userprofile_total_score'), 13 | ] 14 | 15 | operations = [ 16 | migrations.RenameField( 17 | model_name='userprofile', 18 | old_name='problems_status', 19 | new_name='acm_problems_status', 20 | ), 21 | migrations.AddField( 22 | model_name='userprofile', 23 | name='oi_problems_status', 24 | field=jsonfield.fields.JSONField(default={}), 25 | ), 26 | migrations.RemoveField( 27 | model_name='user', 28 | name='real_name', 29 | ), 30 | migrations.RemoveField( 31 | model_name='userprofile', 32 | name='student_id', 33 | ), 34 | migrations.AddField( 35 | model_name='userprofile', 36 | name='real_name', 37 | field=models.CharField(max_length=30, blank=True, null=True), 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.19 AS downloader 2 | 3 | WORKDIR /app 4 | 5 | RUN < 2 | 4 | 5 | 6 | 9 | 10 | 11 | 27 | 28 | 29 | 30 |
8 | {{ website_name }}
12 |
13 |

Hello, {{ username }}:

14 |

15 | Please click {{ link }} to reset your password in 20 minutes. 16 |

17 |

18 | To protect your account, please do not use simple passwords. 19 |

20 |

21 | If you still have any questions, please contract system administrator. 22 |

23 |

24 |

{{ website_name }}

25 |
26 |
31 | -------------------------------------------------------------------------------- /submission/migrations/0007_auto_20170923_1318.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-09-23 13:18 3 | from __future__ import unicode_literals 4 | 5 | import django.contrib.postgres.fields.jsonb 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('submission', '0006_auto_20170830_1154'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterField( 18 | model_name='submission', 19 | name='contest_id', 20 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='contest.Contest'), 21 | ), 22 | migrations.AlterField( 23 | model_name='submission', 24 | name='problem_id', 25 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='problem.Problem'), 26 | ), 27 | migrations.RenameField( 28 | model_name='submission', 29 | old_name='contest_id', 30 | new_name='contest', 31 | ), 32 | migrations.RenameField( 33 | model_name='submission', 34 | old_name='problem_id', 35 | new_name='problem', 36 | ), 37 | migrations.AlterField( 38 | model_name='submission', 39 | name='info', 40 | field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), 41 | ), 42 | migrations.AlterField( 43 | model_name='submission', 44 | name='statistic_info', 45 | field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), 46 | ), 47 | ] 48 | -------------------------------------------------------------------------------- /submission/serializers.py: -------------------------------------------------------------------------------- 1 | from .models import Submission 2 | from utils.api import serializers 3 | from utils.serializers import LanguageNameChoiceField 4 | 5 | 6 | class CreateSubmissionSerializer(serializers.Serializer): 7 | problem_id = serializers.IntegerField() 8 | language = LanguageNameChoiceField() 9 | code = serializers.CharField(max_length=1024 * 1024) 10 | contest_id = serializers.IntegerField(required=False) 11 | captcha = serializers.CharField(required=False) 12 | 13 | 14 | class ShareSubmissionSerializer(serializers.Serializer): 15 | id = serializers.CharField() 16 | shared = serializers.BooleanField() 17 | 18 | 19 | class SubmissionModelSerializer(serializers.ModelSerializer): 20 | 21 | class Meta: 22 | model = Submission 23 | fields = "__all__" 24 | 25 | 26 | # 不显示submission info的serializer, 用于ACM rule_type 27 | class SubmissionSafeModelSerializer(serializers.ModelSerializer): 28 | problem = serializers.SlugRelatedField(read_only=True, slug_field="_id") 29 | 30 | class Meta: 31 | model = Submission 32 | exclude = ("info", "contest", "ip") 33 | 34 | 35 | class SubmissionListSerializer(serializers.ModelSerializer): 36 | problem = serializers.SlugRelatedField(read_only=True, slug_field="_id") 37 | show_link = serializers.SerializerMethodField() 38 | 39 | def __init__(self, *args, **kwargs): 40 | self.user = kwargs.pop("user", None) 41 | super().__init__(*args, **kwargs) 42 | 43 | class Meta: 44 | model = Submission 45 | exclude = ("info", "contest", "code", "ip") 46 | 47 | def get_show_link(self, obj): 48 | # 没传user或为匿名user 49 | if self.user is None or not self.user.is_authenticated: 50 | return False 51 | return obj.check_user_permission(self.user) 52 | -------------------------------------------------------------------------------- /deploy/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | daemon off; 3 | pid /tmp/nginx.pid; 4 | worker_processes auto; 5 | pcre_jit on; 6 | error_log /data/log/nginx_error.log warn; 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | http { 13 | include /etc/nginx/mime.types; 14 | default_type application/octet-stream; 15 | server_tokens off; 16 | keepalive_timeout 65; 17 | sendfile on; 18 | tcp_nodelay on; 19 | 20 | gzip on; 21 | gzip_vary on; 22 | gzip_types application/javascript text/css; 23 | 24 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 25 | '$status $body_bytes_sent "$http_referer" ' 26 | '"$http_user_agent" "$http_x_forwarded_for"'; 27 | 28 | access_log /data/log/nginx_access.log main; 29 | 30 | upstream backend { 31 | server 127.0.0.1:8080; 32 | keepalive 32; 33 | } 34 | 35 | add_header X-XSS-Protection "1; mode=block" always; 36 | add_header X-Frame-Options SAMEORIGIN always; 37 | add_header X-Content-Type-Options nosniff always; 38 | 39 | server { 40 | listen 8000 default_server; 41 | server_name _; 42 | 43 | include http_locations.conf; 44 | } 45 | 46 | server { 47 | listen 1443 ssl http2 default_server; 48 | server_name _; 49 | ssl_certificate /data/ssl/server.crt; 50 | ssl_certificate_key /data/ssl/server.key; 51 | ssl_protocols TLSv1.2; 52 | ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; 53 | ssl_prefer_server_ciphers on; 54 | ssl_session_cache shared:SSL:10m; 55 | 56 | include https_locations.conf; 57 | } 58 | 59 | } 60 | 61 | -------------------------------------------------------------------------------- /utils/api/tests.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from django.test.testcases import TestCase 3 | from rest_framework.test import APIClient 4 | 5 | from account.models import AdminType, ProblemPermission, User, UserProfile 6 | 7 | 8 | class APITestCase(TestCase): 9 | client_class = APIClient 10 | 11 | def create_user(self, username, password, admin_type=AdminType.REGULAR_USER, login=True, 12 | problem_permission=ProblemPermission.NONE): 13 | user = User.objects.create(username=username, admin_type=admin_type, problem_permission=problem_permission) 14 | user.set_password(password) 15 | UserProfile.objects.create(user=user) 16 | user.save() 17 | if login: 18 | self.client.login(username=username, password=password) 19 | return user 20 | 21 | def create_admin(self, username="admin", password="admin", login=True): 22 | return self.create_user(username=username, password=password, admin_type=AdminType.ADMIN, 23 | problem_permission=ProblemPermission.OWN, 24 | login=login) 25 | 26 | def create_super_admin(self, username="root", password="root", login=True): 27 | return self.create_user(username=username, password=password, admin_type=AdminType.SUPER_ADMIN, 28 | problem_permission=ProblemPermission.ALL, login=login) 29 | 30 | def reverse(self, url_name, *args, **kwargs): 31 | return reverse(url_name, *args, **kwargs) 32 | 33 | def assertSuccess(self, response): 34 | if not response.data["error"] is None: 35 | raise AssertionError("response with errors, response: " + str(response.data)) 36 | 37 | def assertFailed(self, response, msg=None): 38 | self.assertTrue(response.data["error"] is not None) 39 | if msg: 40 | self.assertEqual(response.data["data"], msg) 41 | -------------------------------------------------------------------------------- /utils/management/commands/inituser.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from account.models import AdminType, ProblemPermission, User, UserProfile 4 | from utils.shortcuts import rand_str # NOQA 5 | 6 | 7 | class Command(BaseCommand): 8 | def add_arguments(self, parser): 9 | parser.add_argument("--username", type=str) 10 | parser.add_argument("--password", type=str) 11 | parser.add_argument("--action", type=str) 12 | 13 | def handle(self, *args, **options): 14 | username = options["username"] 15 | password = options["password"] 16 | action = options["action"] 17 | 18 | if not(username and password and action): 19 | self.stdout.write(self.style.ERROR("Invalid args")) 20 | exit(1) 21 | 22 | if action == "create_super_admin": 23 | if User.objects.filter(id=1).exists(): 24 | self.stdout.write(self.style.SUCCESS(f"User {username} exists, operation ignored")) 25 | exit() 26 | 27 | user = User.objects.create(username=username, admin_type=AdminType.SUPER_ADMIN, 28 | problem_permission=ProblemPermission.ALL) 29 | user.set_password(password) 30 | user.save() 31 | UserProfile.objects.create(user=user) 32 | 33 | self.stdout.write(self.style.SUCCESS("User created")) 34 | elif action == "reset": 35 | try: 36 | user = User.objects.get(username=username) 37 | user.set_password(password) 38 | user.save() 39 | self.stdout.write(self.style.SUCCESS("Password is rested")) 40 | except User.DoesNotExist: 41 | self.stdout.write(self.style.ERROR(f"User {username} doesnot exist, operation ignored")) 42 | exit(1) 43 | else: 44 | raise ValueError("Invalid action") 45 | -------------------------------------------------------------------------------- /announcement/tests.py: -------------------------------------------------------------------------------- 1 | from utils.api.tests import APITestCase 2 | 3 | from .models import Announcement 4 | 5 | 6 | class AnnouncementAdminTest(APITestCase): 7 | def setUp(self): 8 | self.user = self.create_super_admin() 9 | self.url = self.reverse("announcement_admin_api") 10 | 11 | def test_announcement_list(self): 12 | response = self.client.get(self.url) 13 | self.assertSuccess(response) 14 | 15 | def create_announcement(self): 16 | return self.client.post(self.url, data={"title": "test", "content": "test", "visible": True}) 17 | 18 | def test_create_announcement(self): 19 | resp = self.create_announcement() 20 | self.assertSuccess(resp) 21 | return resp 22 | 23 | def test_edit_announcement(self): 24 | data = {"id": self.create_announcement().data["data"]["id"], "title": "ahaha", "content": "test content", 25 | "visible": False} 26 | resp = self.client.put(self.url, data=data) 27 | self.assertSuccess(resp) 28 | resp_data = resp.data["data"] 29 | self.assertEqual(resp_data["title"], "ahaha") 30 | self.assertEqual(resp_data["content"], "test content") 31 | self.assertEqual(resp_data["visible"], False) 32 | 33 | def test_delete_announcement(self): 34 | id = self.test_create_announcement().data["data"]["id"] 35 | resp = self.client.delete(self.url + "?id=" + str(id)) 36 | self.assertSuccess(resp) 37 | self.assertFalse(Announcement.objects.filter(id=id).exists()) 38 | 39 | 40 | class AnnouncementAPITest(APITestCase): 41 | def setUp(self): 42 | self.user = self.create_super_admin() 43 | Announcement.objects.create(title="title", content="content", visible=True, created_by=self.user) 44 | self.url = self.reverse("announcement_api") 45 | 46 | def test_get_announcement_list(self): 47 | resp = self.client.get(self.url) 48 | self.assertSuccess(resp) 49 | -------------------------------------------------------------------------------- /conf/serializers.py: -------------------------------------------------------------------------------- 1 | from utils.api import serializers 2 | 3 | from .models import JudgeServer 4 | 5 | 6 | class EditSMTPConfigSerializer(serializers.Serializer): 7 | server = serializers.CharField(max_length=128) 8 | port = serializers.IntegerField(default=25) 9 | email = serializers.CharField(max_length=256) 10 | password = serializers.CharField(max_length=128, required=False, allow_null=True, allow_blank=True) 11 | tls = serializers.BooleanField() 12 | 13 | 14 | class CreateSMTPConfigSerializer(EditSMTPConfigSerializer): 15 | password = serializers.CharField(max_length=128) 16 | 17 | 18 | class TestSMTPConfigSerializer(serializers.Serializer): 19 | email = serializers.EmailField() 20 | 21 | 22 | class CreateEditWebsiteConfigSerializer(serializers.Serializer): 23 | website_base_url = serializers.CharField(max_length=128) 24 | website_name = serializers.CharField(max_length=64) 25 | website_name_shortcut = serializers.CharField(max_length=64) 26 | website_footer = serializers.CharField(max_length=1024 * 1024) 27 | allow_register = serializers.BooleanField() 28 | submission_list_show_all = serializers.BooleanField() 29 | 30 | 31 | class JudgeServerSerializer(serializers.ModelSerializer): 32 | status = serializers.CharField() 33 | 34 | class Meta: 35 | model = JudgeServer 36 | fields = "__all__" 37 | 38 | 39 | class JudgeServerHeartbeatSerializer(serializers.Serializer): 40 | hostname = serializers.CharField(max_length=128) 41 | judger_version = serializers.CharField(max_length=32) 42 | cpu_core = serializers.IntegerField(min_value=1) 43 | memory = serializers.FloatField(min_value=0, max_value=100) 44 | cpu = serializers.FloatField(min_value=0, max_value=100) 45 | action = serializers.ChoiceField(choices=("heartbeat", )) 46 | service_url = serializers.CharField(max_length=256) 47 | 48 | 49 | class EditJudgeServerSerializer(serializers.Serializer): 50 | id = serializers.IntegerField() 51 | is_disabled = serializers.BooleanField() 52 | -------------------------------------------------------------------------------- /submission/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from utils.constants import ContestStatus 4 | from utils.models import JSONField 5 | from problem.models import Problem 6 | from contest.models import Contest 7 | 8 | from utils.shortcuts import rand_str 9 | 10 | 11 | class JudgeStatus: 12 | COMPILE_ERROR = -2 13 | WRONG_ANSWER = -1 14 | ACCEPTED = 0 15 | CPU_TIME_LIMIT_EXCEEDED = 1 16 | REAL_TIME_LIMIT_EXCEEDED = 2 17 | MEMORY_LIMIT_EXCEEDED = 3 18 | RUNTIME_ERROR = 4 19 | SYSTEM_ERROR = 5 20 | PENDING = 6 21 | JUDGING = 7 22 | PARTIALLY_ACCEPTED = 8 23 | 24 | 25 | class Submission(models.Model): 26 | id = models.TextField(default=rand_str, primary_key=True, db_index=True) 27 | contest = models.ForeignKey(Contest, null=True, on_delete=models.CASCADE) 28 | problem = models.ForeignKey(Problem, on_delete=models.CASCADE) 29 | create_time = models.DateTimeField(auto_now_add=True) 30 | user_id = models.IntegerField(db_index=True) 31 | username = models.TextField() 32 | code = models.TextField() 33 | result = models.IntegerField(db_index=True, default=JudgeStatus.PENDING) 34 | # 从JudgeServer返回的判题详情 35 | info = JSONField(default=dict) 36 | language = models.TextField() 37 | shared = models.BooleanField(default=False) 38 | # 存储该提交所用时间和内存值,方便提交列表显示 39 | # {time_cost: "", memory_cost: "", err_info: "", score: 0} 40 | statistic_info = JSONField(default=dict) 41 | ip = models.TextField(null=True) 42 | 43 | def check_user_permission(self, user, check_share=True): 44 | if self.user_id == user.id or user.is_super_admin() or user.can_mgmt_all_problem() or self.problem.created_by_id == user.id: 45 | return True 46 | 47 | if check_share: 48 | if self.contest and self.contest.status != ContestStatus.CONTEST_ENDED: 49 | return False 50 | if self.problem.share_submission or self.shared: 51 | return True 52 | return False 53 | 54 | class Meta: 55 | db_table = "submission" 56 | ordering = ("-create_time",) 57 | 58 | def __str__(self): 59 | return self.id 60 | -------------------------------------------------------------------------------- /account/urls/oj.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from ..views.oj import (ApplyResetPasswordAPI, ResetPasswordAPI, 4 | UserChangePasswordAPI, UserRegisterAPI, UserChangeEmailAPI, 5 | UserLoginAPI, UserLogoutAPI, UsernameOrEmailCheck, 6 | AvatarUploadAPI, TwoFactorAuthAPI, UserProfileAPI, 7 | UserRankAPI, CheckTFARequiredAPI, SessionManagementAPI, 8 | ProfileProblemDisplayIDRefreshAPI, OpenAPIAppkeyAPI, SSOAPI) 9 | 10 | from utils.captcha.views import CaptchaAPIView 11 | 12 | urlpatterns = [ 13 | url(r"^login/?$", UserLoginAPI.as_view(), name="user_login_api"), 14 | url(r"^logout/?$", UserLogoutAPI.as_view(), name="user_logout_api"), 15 | url(r"^register/?$", UserRegisterAPI.as_view(), name="user_register_api"), 16 | url(r"^change_password/?$", UserChangePasswordAPI.as_view(), name="user_change_password_api"), 17 | url(r"^change_email/?$", UserChangeEmailAPI.as_view(), name="user_change_email_api"), 18 | url(r"^apply_reset_password/?$", ApplyResetPasswordAPI.as_view(), name="apply_reset_password_api"), 19 | url(r"^reset_password/?$", ResetPasswordAPI.as_view(), name="reset_password_api"), 20 | url(r"^captcha/?$", CaptchaAPIView.as_view(), name="show_captcha"), 21 | url(r"^check_username_or_email", UsernameOrEmailCheck.as_view(), name="check_username_or_email"), 22 | url(r"^profile/?$", UserProfileAPI.as_view(), name="user_profile_api"), 23 | url(r"^profile/fresh_display_id", ProfileProblemDisplayIDRefreshAPI.as_view(), name="display_id_fresh"), 24 | url(r"^upload_avatar/?$", AvatarUploadAPI.as_view(), name="avatar_upload_api"), 25 | url(r"^tfa_required/?$", CheckTFARequiredAPI.as_view(), name="tfa_required_check"), 26 | url(r"^two_factor_auth/?$", TwoFactorAuthAPI.as_view(), name="two_factor_auth_api"), 27 | url(r"^user_rank/?$", UserRankAPI.as_view(), name="user_rank_api"), 28 | url(r"^sessions/?$", SessionManagementAPI.as_view(), name="session_management_api"), 29 | url(r"^open_api_appkey/?$", OpenAPIAppkeyAPI.as_view(), name="open_api_appkey_api"), 30 | url(r"^sso?$", SSOAPI.as_view(), name="sso_api") 31 | ] 32 | -------------------------------------------------------------------------------- /problem/migrations/0008_auto_20170923_1318.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-09-23 13:18 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('contest', '0005_auto_20170823_0918'), 13 | ('problem', '0006_auto_20170823_0918'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AddField( 18 | model_name='contestproblem', 19 | name='total_score', 20 | field=models.IntegerField(blank=True, default=0), 21 | ), 22 | migrations.AddField( 23 | model_name='problem', 24 | name='total_score', 25 | field=models.IntegerField(blank=True, default=0), 26 | ), 27 | migrations.AlterUniqueTogether( 28 | name='contestproblem', 29 | unique_together=set([]), 30 | ), 31 | migrations.RemoveField( 32 | model_name='contestproblem', 33 | name='contest', 34 | ), 35 | migrations.RemoveField( 36 | model_name='contestproblem', 37 | name='created_by', 38 | ), 39 | migrations.RemoveField( 40 | model_name='contestproblem', 41 | name='tags', 42 | ), 43 | migrations.AddField( 44 | model_name='problem', 45 | name='contest', 46 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contest.Contest'), 47 | preserve_default=False, 48 | ), 49 | migrations.AddField( 50 | model_name='problem', 51 | name='is_public', 52 | field=models.BooleanField(default=False), 53 | ), 54 | migrations.AlterField( 55 | model_name='problem', 56 | name='_id', 57 | field=models.CharField(db_index=True, max_length=24), 58 | ), 59 | migrations.AlterUniqueTogether( 60 | name='problem', 61 | unique_together=set([('_id', 'contest')]), 62 | ), 63 | migrations.DeleteModel( 64 | name='ContestProblem', 65 | ), 66 | ] 67 | -------------------------------------------------------------------------------- /account/middleware.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import connection 3 | from django.utils.timezone import now 4 | from django.utils.deprecation import MiddlewareMixin 5 | 6 | from utils.api import JSONResponse 7 | from account.models import User 8 | 9 | 10 | class APITokenAuthMiddleware(MiddlewareMixin): 11 | def process_request(self, request): 12 | appkey = request.META.get("HTTP_APPKEY") 13 | if appkey: 14 | try: 15 | request.user = User.objects.get(open_api_appkey=appkey, open_api=True, is_disabled=False) 16 | request.csrf_processing_done = True 17 | request.auth_method = "api_key" 18 | except User.DoesNotExist: 19 | pass 20 | 21 | 22 | class SessionRecordMiddleware(MiddlewareMixin): 23 | def process_request(self, request): 24 | request.ip = request.META.get(settings.IP_HEADER, request.META.get("REMOTE_ADDR")) 25 | if request.user.is_authenticated: 26 | session = request.session 27 | session["user_agent"] = request.META.get("HTTP_USER_AGENT", "") 28 | session["ip"] = request.ip 29 | session["last_activity"] = now() 30 | user_sessions = request.user.session_keys 31 | if session.session_key not in user_sessions: 32 | user_sessions.append(session.session_key) 33 | request.user.save() 34 | 35 | 36 | class AdminRoleRequiredMiddleware(MiddlewareMixin): 37 | def process_request(self, request): 38 | path = request.path_info 39 | if path.startswith("/admin/") or path.startswith("/api/admin/"): 40 | if not (request.user.is_authenticated and request.user.is_admin_role()): 41 | return JSONResponse.response({"error": "login-required", "data": "Please login in first"}) 42 | 43 | 44 | class LogSqlMiddleware(MiddlewareMixin): 45 | def process_response(self, request, response): 46 | print("\033[94m", "#" * 30, "\033[0m") 47 | time_threshold = 0.03 48 | for query in connection.queries: 49 | if float(query["time"]) > time_threshold: 50 | print("\033[93m", query, "\n", "-" * 30, "\033[0m") 51 | else: 52 | print(query, "\n", "-" * 30) 53 | return response 54 | -------------------------------------------------------------------------------- /utils/throttling.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | class TokenBucket: 5 | """ 6 | 注意:对于单个key的操作不是线程安全的 7 | """ 8 | def __init__(self, key, capacity, fill_rate, default_capacity, redis_conn): 9 | """ 10 | :param capacity: 最大容量 11 | :param fill_rate: 填充速度/每秒 12 | :param default_capacity: 初始容量 13 | :param redis_conn: redis connection 14 | """ 15 | self._key = key 16 | self._capacity = capacity 17 | self._fill_rate = fill_rate 18 | self._default_capacity = default_capacity 19 | self._redis_conn = redis_conn 20 | 21 | self._last_capacity_key = "last_capacity" 22 | self._last_timestamp_key = "last_timestamp" 23 | 24 | def _init_key(self): 25 | self._last_capacity = self._default_capacity 26 | now = time.time() 27 | self._last_timestamp = now 28 | return self._default_capacity, now 29 | 30 | @property 31 | def _last_capacity(self): 32 | last_capacity = self._redis_conn.hget(self._key, self._last_capacity_key) 33 | if last_capacity is None: 34 | return self._init_key()[0] 35 | else: 36 | return float(last_capacity) 37 | 38 | @_last_capacity.setter 39 | def _last_capacity(self, value): 40 | self._redis_conn.hset(self._key, self._last_capacity_key, value) 41 | 42 | @property 43 | def _last_timestamp(self): 44 | return float(self._redis_conn.hget(self._key, self._last_timestamp_key)) 45 | 46 | @_last_timestamp.setter 47 | def _last_timestamp(self, value): 48 | self._redis_conn.hset(self._key, self._last_timestamp_key, value) 49 | 50 | def _try_to_fill(self, now): 51 | delta = self._fill_rate * (now - self._last_timestamp) 52 | return min(self._last_capacity + delta, self._capacity) 53 | 54 | def consume(self, num=1): 55 | """ 56 | 消耗 num 个 token,返回是否成功 57 | :param num: 58 | :return: result: bool, wait_time: float 59 | """ 60 | # print("capacity ", self.fill(time.time())) 61 | if self._last_capacity >= num: 62 | self._last_capacity -= num 63 | return True, 0 64 | else: 65 | now = time.time() 66 | cur_num = self._try_to_fill(now) 67 | if cur_num >= num: 68 | self._last_capacity = cur_num - num 69 | self._last_timestamp = now 70 | return True, 0 71 | else: 72 | return False, (num - cur_num) / self._fill_rate 73 | -------------------------------------------------------------------------------- /announcement/views/admin.py: -------------------------------------------------------------------------------- 1 | from account.decorators import super_admin_required 2 | from utils.api import APIView, validate_serializer 3 | 4 | from announcement.models import Announcement 5 | from announcement.serializers import (AnnouncementSerializer, CreateAnnouncementSerializer, 6 | EditAnnouncementSerializer) 7 | 8 | 9 | class AnnouncementAdminAPI(APIView): 10 | @validate_serializer(CreateAnnouncementSerializer) 11 | @super_admin_required 12 | def post(self, request): 13 | """ 14 | publish announcement 15 | """ 16 | data = request.data 17 | announcement = Announcement.objects.create(title=data["title"], 18 | content=data["content"], 19 | created_by=request.user, 20 | visible=data["visible"]) 21 | return self.success(AnnouncementSerializer(announcement).data) 22 | 23 | @validate_serializer(EditAnnouncementSerializer) 24 | @super_admin_required 25 | def put(self, request): 26 | """ 27 | edit announcement 28 | """ 29 | data = request.data 30 | try: 31 | announcement = Announcement.objects.get(id=data.pop("id")) 32 | except Announcement.DoesNotExist: 33 | return self.error("Announcement does not exist") 34 | 35 | for k, v in data.items(): 36 | setattr(announcement, k, v) 37 | announcement.save() 38 | 39 | return self.success(AnnouncementSerializer(announcement).data) 40 | 41 | @super_admin_required 42 | def get(self, request): 43 | """ 44 | get announcement list / get one announcement 45 | """ 46 | announcement_id = request.GET.get("id") 47 | if announcement_id: 48 | try: 49 | announcement = Announcement.objects.get(id=announcement_id) 50 | return self.success(AnnouncementSerializer(announcement).data) 51 | except Announcement.DoesNotExist: 52 | return self.error("Announcement does not exist") 53 | announcement = Announcement.objects.all().order_by("-create_time") 54 | if request.GET.get("visible") == "true": 55 | announcement = announcement.filter(visible=True) 56 | return self.success(self.paginate_data(request, announcement, AnnouncementSerializer)) 57 | 58 | @super_admin_required 59 | def delete(self, request): 60 | if request.GET.get("id"): 61 | Announcement.objects.filter(id=request.GET["id"]).delete() 62 | return self.success() 63 | -------------------------------------------------------------------------------- /deploy/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | APP=/app 4 | DATA=/data 5 | 6 | mkdir -p $DATA/log $DATA/config $DATA/ssl $DATA/test_case $DATA/public/upload $DATA/public/avatar $DATA/public/website 7 | 8 | if [ ! -f "$DATA/config/secret.key" ]; then 9 | echo $(cat /dev/urandom | head -1 | md5sum | head -c 32) > "$DATA/config/secret.key" 10 | fi 11 | 12 | if [ ! -f "$DATA/public/avatar/default.png" ]; then 13 | cp data/public/avatar/default.png $DATA/public/avatar 14 | fi 15 | 16 | if [ ! -f "$DATA/public/website/favicon.ico" ]; then 17 | cp data/public/website/favicon.ico $DATA/public/website 18 | fi 19 | 20 | SSL="$DATA/ssl" 21 | if [ ! -f "$SSL/server.key" ]; then 22 | openssl req -x509 -newkey rsa:2048 -keyout "$SSL/server.key" -out "$SSL/server.crt" -days 1000 \ 23 | -subj "/C=CN/ST=Beijing/L=Beijing/O=Beijing OnlineJudge Technology Co., Ltd./OU=Service Infrastructure Department/CN=`hostname`" -nodes 24 | fi 25 | 26 | cd $APP/deploy/nginx 27 | ln -sf locations.conf https_locations.conf 28 | if [ -z "$FORCE_HTTPS" ]; then 29 | ln -sf locations.conf http_locations.conf 30 | else 31 | ln -sf https_redirect.conf http_locations.conf 32 | fi 33 | 34 | if [ ! -z "$LOWER_IP_HEADER" ]; then 35 | sed -i "s/__IP_HEADER__/\$http_$LOWER_IP_HEADER/g" api_proxy.conf; 36 | else 37 | sed -i "s/__IP_HEADER__/\$remote_addr/g" api_proxy.conf; 38 | fi 39 | 40 | if [ -z "$MAX_WORKER_NUM" ]; then 41 | export CPU_CORE_NUM=$(grep -c ^processor /proc/cpuinfo) 42 | if [[ $CPU_CORE_NUM -lt 2 ]]; then 43 | export MAX_WORKER_NUM=2 44 | else 45 | export MAX_WORKER_NUM=$(($CPU_CORE_NUM)) 46 | fi 47 | fi 48 | 49 | cd $APP/dist 50 | if [ ! -z "$STATIC_CDN_HOST" ]; then 51 | find . -name "*.*" -type f -exec sed -i "s/__STATIC_CDN_HOST__/\/$STATIC_CDN_HOST/g" {} \; 52 | else 53 | find . -name "*.*" -type f -exec sed -i "s/__STATIC_CDN_HOST__\///g" {} \; 54 | fi 55 | 56 | cd $APP 57 | 58 | n=0 59 | while [ $n -lt 5 ] 60 | do 61 | python manage.py migrate --no-input && 62 | python manage.py inituser --username=root --password=rootroot --action=create_super_admin && 63 | echo "from options.options import SysOptions; SysOptions.judge_server_token='$JUDGE_SERVER_TOKEN'" | python manage.py shell && 64 | echo "from conf.models import JudgeServer; JudgeServer.objects.update(task_number=0)" | python manage.py shell && 65 | break 66 | n=$(($n+1)) 67 | echo "Failed to migrate, going to retry..." 68 | sleep 8 69 | done 70 | 71 | addgroup -g 903 spj 72 | adduser -u 900 -S -G spj server 73 | 74 | chown -R server:spj $DATA $APP/dist 75 | find $DATA/test_case -type d -exec chmod 710 {} \; 76 | find $DATA/test_case -type f -exec chmod 640 {} \; 77 | exec supervisord -c /app/deploy/supervisord.conf 78 | -------------------------------------------------------------------------------- /utils/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.conf import settings 3 | from account.serializers import ImageUploadForm, FileUploadForm 4 | from utils.shortcuts import rand_str 5 | from utils.api import CSRFExemptAPIView 6 | import logging 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class SimditorImageUploadAPIView(CSRFExemptAPIView): 12 | request_parsers = () 13 | 14 | def post(self, request): 15 | form = ImageUploadForm(request.POST, request.FILES) 16 | if form.is_valid(): 17 | img = form.cleaned_data["image"] 18 | else: 19 | return self.response({ 20 | "success": False, 21 | "msg": "Upload failed", 22 | "file_path": ""}) 23 | 24 | suffix = os.path.splitext(img.name)[-1].lower() 25 | if suffix not in [".gif", ".jpg", ".jpeg", ".bmp", ".png"]: 26 | return self.response({ 27 | "success": False, 28 | "msg": "Unsupported file format", 29 | "file_path": ""}) 30 | img_name = rand_str(10) + suffix 31 | try: 32 | with open(os.path.join(settings.UPLOAD_DIR, img_name), "wb") as imgFile: 33 | for chunk in img: 34 | imgFile.write(chunk) 35 | except IOError as e: 36 | logger.error(e) 37 | return self.response({ 38 | "success": False, 39 | "msg": "Upload Error", 40 | "file_path": ""}) 41 | return self.response({ 42 | "success": True, 43 | "msg": "Success", 44 | "file_path": f"{settings.UPLOAD_PREFIX}/{img_name}"}) 45 | 46 | 47 | class SimditorFileUploadAPIView(CSRFExemptAPIView): 48 | request_parsers = () 49 | 50 | def post(self, request): 51 | form = FileUploadForm(request.POST, request.FILES) 52 | if form.is_valid(): 53 | file = form.cleaned_data["file"] 54 | else: 55 | return self.response({ 56 | "success": False, 57 | "msg": "Upload failed" 58 | }) 59 | 60 | suffix = os.path.splitext(file.name)[-1].lower() 61 | file_name = rand_str(10) + suffix 62 | try: 63 | with open(os.path.join(settings.UPLOAD_DIR, file_name), "wb") as f: 64 | for chunk in file: 65 | f.write(chunk) 66 | except IOError as e: 67 | logger.error(e) 68 | return self.response({ 69 | "success": False, 70 | "msg": "Upload Error"}) 71 | return self.response({ 72 | "success": True, 73 | "msg": "Success", 74 | "file_path": f"{settings.UPLOAD_PREFIX}/{file_name}", 75 | "file_name": file.name}) 76 | -------------------------------------------------------------------------------- /account/migrations/0010_auto_20180501_0436.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.3 on 2018-05-01 04:36 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('account', '0009_auto_20171125_1514'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='user', 17 | name='admin_type', 18 | field=models.TextField(default='Regular User'), 19 | ), 20 | migrations.AlterField( 21 | model_name='user', 22 | name='auth_token', 23 | field=models.TextField(null=True), 24 | ), 25 | migrations.AlterField( 26 | model_name='user', 27 | name='open_api_appkey', 28 | field=models.TextField(null=True), 29 | ), 30 | migrations.AlterField( 31 | model_name='user', 32 | name='problem_permission', 33 | field=models.TextField(default='None'), 34 | ), 35 | migrations.AlterField( 36 | model_name='user', 37 | name='reset_password_token', 38 | field=models.TextField(null=True), 39 | ), 40 | migrations.AlterField( 41 | model_name='user', 42 | name='tfa_token', 43 | field=models.TextField(null=True), 44 | ), 45 | migrations.AlterField( 46 | model_name='user', 47 | name='username', 48 | field=models.TextField(unique=True), 49 | ), 50 | migrations.AlterField( 51 | model_name='userprofile', 52 | name='avatar', 53 | field=models.TextField(default='/public/avatar/default.png'), 54 | ), 55 | migrations.AlterField( 56 | model_name='userprofile', 57 | name='blog', 58 | field=models.URLField(null=True), 59 | ), 60 | migrations.AlterField( 61 | model_name='userprofile', 62 | name='github', 63 | field=models.TextField(null=True), 64 | ), 65 | migrations.AlterField( 66 | model_name='userprofile', 67 | name='major', 68 | field=models.TextField(null=True), 69 | ), 70 | migrations.AlterField( 71 | model_name='userprofile', 72 | name='mood', 73 | field=models.TextField(null=True), 74 | ), 75 | migrations.AlterField( 76 | model_name='userprofile', 77 | name='real_name', 78 | field=models.TextField(null=True), 79 | ), 80 | migrations.AlterField( 81 | model_name='userprofile', 82 | name='school', 83 | field=models.TextField(null=True), 84 | ), 85 | ] 86 | -------------------------------------------------------------------------------- /problem/migrations/0012_auto_20180501_0436.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.3 on 2018-05-01 04:36 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | import utils.models 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('problem', '0011_fix_problem_ac_count'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterField( 18 | model_name='problem', 19 | name='_id', 20 | field=models.TextField(db_index=True), 21 | ), 22 | migrations.AlterField( 23 | model_name='problem', 24 | name='contest', 25 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='contest.Contest'), 26 | ), 27 | migrations.AlterField( 28 | model_name='problem', 29 | name='difficulty', 30 | field=models.TextField(), 31 | ), 32 | migrations.AlterField( 33 | model_name='problem', 34 | name='hint', 35 | field=utils.models.RichTextField(null=True), 36 | ), 37 | migrations.AlterField( 38 | model_name='problem', 39 | name='last_update_time', 40 | field=models.DateTimeField(null=True), 41 | ), 42 | migrations.AlterField( 43 | model_name='problem', 44 | name='rule_type', 45 | field=models.TextField(), 46 | ), 47 | migrations.AlterField( 48 | model_name='problem', 49 | name='source', 50 | field=models.TextField(null=True), 51 | ), 52 | migrations.AlterField( 53 | model_name='problem', 54 | name='spj_code', 55 | field=models.TextField(null=True), 56 | ), 57 | migrations.AlterField( 58 | model_name='problem', 59 | name='spj_language', 60 | field=models.TextField(null=True), 61 | ), 62 | migrations.AlterField( 63 | model_name='problem', 64 | name='spj_version', 65 | field=models.TextField(null=True), 66 | ), 67 | migrations.AlterField( 68 | model_name='problem', 69 | name='test_case_id', 70 | field=models.TextField(), 71 | ), 72 | migrations.AlterField( 73 | model_name='problem', 74 | name='title', 75 | field=models.TextField(), 76 | ), 77 | migrations.AlterField( 78 | model_name='problem', 79 | name='total_score', 80 | field=models.IntegerField(default=0), 81 | ), 82 | migrations.AlterField( 83 | model_name='problemtag', 84 | name='name', 85 | field=models.TextField(), 86 | ), 87 | ] 88 | -------------------------------------------------------------------------------- /utils/shortcuts.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import datetime 4 | import random 5 | from base64 import b64encode 6 | from io import BytesIO 7 | 8 | from django.utils.crypto import get_random_string 9 | from envelopes import Envelope 10 | 11 | 12 | def rand_str(length=32, type="lower_hex"): 13 | """ 14 | 生成指定长度的随机字符串或者数字, 可以用于密钥等安全场景 15 | :param length: 字符串或者数字的长度 16 | :param type: str 代表随机字符串,num 代表随机数字 17 | :return: 字符串 18 | """ 19 | if type == "str": 20 | return get_random_string(length, allowed_chars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") 21 | elif type == "lower_str": 22 | return get_random_string(length, allowed_chars="abcdefghijklmnopqrstuvwxyz0123456789") 23 | elif type == "lower_hex": 24 | return random.choice("123456789abcdef") + get_random_string(length - 1, allowed_chars="0123456789abcdef") 25 | else: 26 | return random.choice("123456789") + get_random_string(length - 1, allowed_chars="0123456789") 27 | 28 | 29 | def build_query_string(kv_data, ignore_none=True): 30 | # {"a": 1, "b": "test"} -> "?a=1&b=test" 31 | query_string = "" 32 | for k, v in kv_data.items(): 33 | if ignore_none is True and kv_data[k] is None: 34 | continue 35 | if query_string != "": 36 | query_string += "&" 37 | else: 38 | query_string = "?" 39 | query_string += (k + "=" + str(v)) 40 | return query_string 41 | 42 | 43 | def img2base64(img): 44 | with BytesIO() as buf: 45 | img.save(buf, "gif") 46 | buf_str = buf.getvalue() 47 | img_prefix = "data:image/png;base64," 48 | b64_str = img_prefix + b64encode(buf_str).decode("utf-8") 49 | return b64_str 50 | 51 | 52 | def datetime2str(value, format="iso-8601"): 53 | if format.lower() == "iso-8601": 54 | value = value.isoformat() 55 | if value.endswith("+00:00"): 56 | value = value[:-6] + "Z" 57 | return value 58 | return value.strftime(format) 59 | 60 | 61 | def timestamp2utcstr(value): 62 | return datetime.datetime.utcfromtimestamp(value).isoformat() 63 | 64 | 65 | def natural_sort_key(s, _nsre=re.compile(r"(\d+)")): 66 | return [int(text) if text.isdigit() else text.lower() 67 | for text in re.split(_nsre, s)] 68 | 69 | 70 | def send_email(smtp_config, from_name, to_email, to_name, subject, content): 71 | envelope = Envelope(from_addr=(smtp_config["email"], from_name), 72 | to_addr=(to_email, to_name), 73 | subject=subject, 74 | html_body=content) 75 | return envelope.send(smtp_config["server"], 76 | login=smtp_config["email"], 77 | password=smtp_config["password"], 78 | port=smtp_config["port"], 79 | tls=smtp_config["tls"]) 80 | 81 | 82 | def get_env(name, default=""): 83 | return os.environ.get(name, default) 84 | 85 | 86 | def DRAMATIQ_WORKER_ARGS(time_limit=3600_000, max_retries=0, max_age=7200_000): 87 | return {"max_retries": max_retries, "time_limit": time_limit, "max_age": max_age} 88 | 89 | 90 | def check_is_id(value): 91 | try: 92 | return int(value) > 0 93 | except Exception: 94 | return False 95 | -------------------------------------------------------------------------------- /conf/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.12 on 2017-01-23 07:59 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='JudgeServer', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('hostname', models.CharField(max_length=64)), 21 | ('ip', models.CharField(blank=True, max_length=32, null=True)), 22 | ('judger_version', models.CharField(max_length=24)), 23 | ('cpu_core', models.IntegerField()), 24 | ('memory_usage', models.FloatField()), 25 | ('cpu_usage', models.FloatField()), 26 | ('last_heartbeat', models.DateTimeField()), 27 | ('create_time', models.DateTimeField(auto_now_add=True)), 28 | ('task_number', models.IntegerField(default=0)), 29 | ('service_url', models.CharField(blank=True, max_length=128, null=True)), 30 | ], 31 | options={ 32 | 'db_table': 'judge_server', 33 | }, 34 | ), 35 | migrations.CreateModel( 36 | name='JudgeServerToken', 37 | fields=[ 38 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 39 | ('token', models.CharField(max_length=32)), 40 | ], 41 | options={ 42 | 'db_table': 'judge_server_token', 43 | }, 44 | ), 45 | migrations.CreateModel( 46 | name='SMTPConfig', 47 | fields=[ 48 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 49 | ('server', models.CharField(max_length=128)), 50 | ('port', models.IntegerField(default=25)), 51 | ('email', models.CharField(max_length=128)), 52 | ('password', models.CharField(max_length=128)), 53 | ('tls', models.BooleanField()), 54 | ], 55 | options={ 56 | 'db_table': 'smtp_config', 57 | }, 58 | ), 59 | migrations.CreateModel( 60 | name='WebsiteConfig', 61 | fields=[ 62 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 63 | ('base_url', models.CharField(default='http://127.0.0.1', max_length=128)), 64 | ('name', models.CharField(default='Online Judge', max_length=32)), 65 | ('name_shortcut', models.CharField(default='oj', max_length=32)), 66 | ('footer', models.TextField(default='Online Judge Footer')), 67 | ('allow_register', models.BooleanField(default=True)), 68 | ('submission_list_show_all', models.BooleanField(default=True)), 69 | ], 70 | options={ 71 | 'db_table': 'website_config', 72 | }, 73 | ), 74 | ] 75 | -------------------------------------------------------------------------------- /problem/migrations/0003_auto_20170217_0820.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.12 on 2017-02-17 08:20 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | import jsonfield.fields 9 | import utils.models 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | dependencies = [ 15 | ('contest', '0003_auto_20170217_0820'), 16 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 17 | ('problem', '0002_problem__id'), 18 | ] 19 | 20 | operations = [ 21 | migrations.CreateModel( 22 | name='ContestProblem', 23 | fields=[ 24 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 25 | ('title', models.CharField(max_length=128)), 26 | ('description', utils.models.RichTextField()), 27 | ('input_description', utils.models.RichTextField()), 28 | ('output_description', utils.models.RichTextField()), 29 | ('samples', jsonfield.fields.JSONField()), 30 | ('test_case_id', models.CharField(max_length=32)), 31 | ('test_case_score', jsonfield.fields.JSONField()), 32 | ('hint', utils.models.RichTextField(blank=True, null=True)), 33 | ('languages', jsonfield.fields.JSONField()), 34 | ('template', jsonfield.fields.JSONField()), 35 | ('create_time', models.DateTimeField(auto_now_add=True)), 36 | ('last_update_time', models.DateTimeField(blank=True, null=True)), 37 | ('time_limit', models.IntegerField()), 38 | ('memory_limit', models.IntegerField()), 39 | ('spj', models.BooleanField(default=False)), 40 | ('spj_language', models.CharField(blank=True, max_length=32, null=True)), 41 | ('spj_code', models.TextField(blank=True, null=True)), 42 | ('spj_version', models.CharField(blank=True, max_length=32, null=True)), 43 | ('rule_type', models.CharField(max_length=32)), 44 | ('visible', models.BooleanField(default=True)), 45 | ('difficulty', models.CharField(max_length=32)), 46 | ('source', models.CharField(blank=True, max_length=200, null=True)), 47 | ('total_submit_number', models.IntegerField(default=0)), 48 | ('total_accepted_number', models.IntegerField(default=0)), 49 | ('_id', models.CharField(db_index=True, max_length=24)), 50 | ('is_public', models.BooleanField(default=False)), 51 | ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contest.Contest')), 52 | ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 53 | ('tags', models.ManyToManyField(to='problem.ProblemTag')), 54 | ], 55 | options={ 56 | 'db_table': 'contest_problem', 57 | }, 58 | ), 59 | migrations.AlterUniqueTogether( 60 | name='contestproblem', 61 | unique_together=set([('_id', 'contest')]), 62 | ), 63 | ] 64 | -------------------------------------------------------------------------------- /problem/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.12 on 2017-02-06 09:19 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | import jsonfield.fields 9 | import utils.models 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | initial = True 15 | 16 | dependencies = [ 17 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 18 | ] 19 | 20 | operations = [ 21 | migrations.CreateModel( 22 | name='Problem', 23 | fields=[ 24 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 25 | ('title', models.CharField(max_length=128)), 26 | ('description', utils.models.RichTextField()), 27 | ('input_description', utils.models.RichTextField()), 28 | ('output_description', utils.models.RichTextField()), 29 | ('samples', jsonfield.fields.JSONField()), 30 | ('test_case_id', models.CharField(max_length=32)), 31 | ('test_case_score', jsonfield.fields.JSONField()), 32 | ('hint', utils.models.RichTextField(blank=True, null=True)), 33 | ('languages', jsonfield.fields.JSONField()), 34 | ('template', jsonfield.fields.JSONField()), 35 | ('create_time', models.DateTimeField(auto_now_add=True)), 36 | ('last_update_time', models.DateTimeField(blank=True, null=True)), 37 | ('time_limit', models.IntegerField()), 38 | ('memory_limit', models.IntegerField()), 39 | ('spj', models.BooleanField(default=False)), 40 | ('spj_language', models.CharField(blank=True, max_length=32, null=True)), 41 | ('spj_code', models.TextField(blank=True, null=True)), 42 | ('spj_version', models.CharField(blank=True, max_length=32, null=True)), 43 | ('rule_type', models.CharField(max_length=32)), 44 | ('visible', models.BooleanField(default=True)), 45 | ('difficulty', models.CharField(max_length=32)), 46 | ('source', models.CharField(blank=True, max_length=200, null=True)), 47 | ('total_submit_number', models.IntegerField(default=0)), 48 | ('total_accepted_number', models.IntegerField(default=0)), 49 | ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 50 | ], 51 | options={ 52 | 'db_table': 'problem', 53 | 'abstract': False, 54 | }, 55 | ), 56 | migrations.CreateModel( 57 | name='ProblemTag', 58 | fields=[ 59 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 60 | ('name', models.CharField(max_length=30)), 61 | ], 62 | options={ 63 | 'db_table': 'problem_tag', 64 | }, 65 | ), 66 | migrations.AddField( 67 | model_name='problem', 68 | name='tags', 69 | field=models.ManyToManyField(to='problem.ProblemTag'), 70 | ), 71 | ] 72 | -------------------------------------------------------------------------------- /problem/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from utils.models import JSONField 3 | 4 | from account.models import User 5 | from contest.models import Contest 6 | from utils.models import RichTextField 7 | from utils.constants import Choices 8 | 9 | 10 | class ProblemTag(models.Model): 11 | name = models.TextField() 12 | 13 | class Meta: 14 | db_table = "problem_tag" 15 | 16 | 17 | class ProblemRuleType(Choices): 18 | ACM = "ACM" 19 | OI = "OI" 20 | 21 | 22 | class ProblemDifficulty(object): 23 | High = "High" 24 | Mid = "Mid" 25 | Low = "Low" 26 | 27 | 28 | class ProblemIOMode(Choices): 29 | standard = "Standard IO" 30 | file = "File IO" 31 | 32 | 33 | def _default_io_mode(): 34 | return {"io_mode": ProblemIOMode.standard, "input": "input.txt", "output": "output.txt"} 35 | 36 | 37 | class Problem(models.Model): 38 | # display ID 39 | _id = models.TextField(db_index=True) 40 | contest = models.ForeignKey(Contest, null=True, on_delete=models.CASCADE) 41 | # for contest problem 42 | is_public = models.BooleanField(default=False) 43 | title = models.TextField() 44 | # HTML 45 | description = RichTextField() 46 | input_description = RichTextField() 47 | output_description = RichTextField() 48 | # [{input: "test", output: "123"}, {input: "test123", output: "456"}] 49 | samples = JSONField() 50 | test_case_id = models.TextField() 51 | # [{"input_name": "1.in", "output_name": "1.out", "score": 0}] 52 | test_case_score = JSONField() 53 | hint = RichTextField(null=True) 54 | languages = JSONField() 55 | template = JSONField() 56 | create_time = models.DateTimeField(auto_now_add=True) 57 | # we can not use auto_now here 58 | last_update_time = models.DateTimeField(null=True) 59 | created_by = models.ForeignKey(User, on_delete=models.CASCADE) 60 | # ms 61 | time_limit = models.IntegerField() 62 | # MB 63 | memory_limit = models.IntegerField() 64 | # io mode 65 | io_mode = JSONField(default=_default_io_mode) 66 | # special judge related 67 | spj = models.BooleanField(default=False) 68 | spj_language = models.TextField(null=True) 69 | spj_code = models.TextField(null=True) 70 | spj_version = models.TextField(null=True) 71 | spj_compile_ok = models.BooleanField(default=False) 72 | rule_type = models.TextField() 73 | visible = models.BooleanField(default=True) 74 | difficulty = models.TextField() 75 | tags = models.ManyToManyField(ProblemTag) 76 | source = models.TextField(null=True) 77 | # for OI mode 78 | total_score = models.IntegerField(default=0) 79 | submission_number = models.BigIntegerField(default=0) 80 | accepted_number = models.BigIntegerField(default=0) 81 | # {JudgeStatus.ACCEPTED: 3, JudgeStaus.WRONG_ANSWER: 11}, the number means count 82 | statistic_info = JSONField(default=dict) 83 | share_submission = models.BooleanField(default=False) 84 | 85 | class Meta: 86 | db_table = "problem" 87 | unique_together = (("_id", "contest"),) 88 | ordering = ("create_time",) 89 | 90 | def add_submission_number(self): 91 | self.submission_number = models.F("submission_number") + 1 92 | self.save(update_fields=["submission_number"]) 93 | 94 | def add_ac_number(self): 95 | self.accepted_number = models.F("accepted_number") + 1 96 | self.save(update_fields=["accepted_number"]) 97 | -------------------------------------------------------------------------------- /submission/tests.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from unittest import mock 3 | 4 | from problem.models import Problem, ProblemTag 5 | from utils.api.tests import APITestCase 6 | from .models import Submission 7 | 8 | DEFAULT_PROBLEM_DATA = {"_id": "A-110", "title": "test", "description": "

test

", "input_description": "test", 9 | "output_description": "test", "time_limit": 1000, "memory_limit": 256, "difficulty": "Low", 10 | "visible": True, "tags": ["test"], "languages": ["C", "C++", "Java", "Python2"], "template": {}, 11 | "samples": [{"input": "test", "output": "test"}], "spj": False, "spj_language": "C", 12 | "spj_code": "", "test_case_id": "499b26290cc7994e0b497212e842ea85", 13 | "test_case_score": [{"output_name": "1.out", "input_name": "1.in", "output_size": 0, 14 | "stripped_output_md5": "d41d8cd98f00b204e9800998ecf8427e", 15 | "input_size": 0, "score": 0}], 16 | "rule_type": "ACM", "hint": "

test

", "source": "test"} 17 | 18 | DEFAULT_SUBMISSION_DATA = { 19 | "problem_id": "1", 20 | "user_id": 1, 21 | "username": "test", 22 | "code": "xxxxxxxxxxxxxx", 23 | "result": -2, 24 | "info": {}, 25 | "language": "C", 26 | "statistic_info": {} 27 | } 28 | 29 | 30 | # todo contest submission 31 | 32 | 33 | class SubmissionPrepare(APITestCase): 34 | def _create_problem_and_submission(self): 35 | user = self.create_admin("test", "test123", login=False) 36 | problem_data = deepcopy(DEFAULT_PROBLEM_DATA) 37 | tags = problem_data.pop("tags") 38 | problem_data["created_by"] = user 39 | self.problem = Problem.objects.create(**problem_data) 40 | for tag in tags: 41 | tag = ProblemTag.objects.create(name=tag) 42 | self.problem.tags.add(tag) 43 | self.problem.save() 44 | self.submission_data = deepcopy(DEFAULT_SUBMISSION_DATA) 45 | self.submission_data["problem_id"] = self.problem.id 46 | self.submission = Submission.objects.create(**self.submission_data) 47 | 48 | 49 | class SubmissionListTest(SubmissionPrepare): 50 | def setUp(self): 51 | self._create_problem_and_submission() 52 | self.create_user("123", "345") 53 | self.url = self.reverse("submission_list_api") 54 | 55 | def test_get_submission_list(self): 56 | resp = self.client.get(self.url, data={"limit": "10"}) 57 | self.assertSuccess(resp) 58 | 59 | 60 | @mock.patch("submission.views.oj.judge_task.send") 61 | class SubmissionAPITest(SubmissionPrepare): 62 | def setUp(self): 63 | self._create_problem_and_submission() 64 | self.user = self.create_user("123", "test123") 65 | self.url = self.reverse("submission_api") 66 | 67 | def test_create_submission(self, judge_task): 68 | resp = self.client.post(self.url, self.submission_data) 69 | self.assertSuccess(resp) 70 | judge_task.assert_called() 71 | 72 | def test_create_submission_with_wrong_language(self, judge_task): 73 | self.submission_data.update({"language": "Python3"}) 74 | resp = self.client.post(self.url, self.submission_data) 75 | self.assertFailed(resp) 76 | self.assertDictEqual(resp.data, {"error": "error", 77 | "data": "Python3 is now allowed in the problem"}) 78 | judge_task.assert_not_called() 79 | -------------------------------------------------------------------------------- /account/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.12 on 2017-01-23 07:59 3 | from __future__ import unicode_literals 4 | 5 | import django.db.models.deletion 6 | import jsonfield.fields 7 | from django.conf import settings 8 | from django.db import migrations, models 9 | 10 | import account.models 11 | 12 | 13 | class Migration(migrations.Migration): 14 | 15 | initial = True 16 | 17 | dependencies = [ 18 | ] 19 | 20 | operations = [ 21 | migrations.CreateModel( 22 | name='User', 23 | fields=[ 24 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 25 | ('password', models.CharField(max_length=128, verbose_name='password')), 26 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 27 | ('username', models.CharField(max_length=30, unique=True)), 28 | ('real_name', models.CharField(max_length=30, null=True)), 29 | ('email', models.EmailField(max_length=254, null=True)), 30 | ('create_time', models.DateTimeField(auto_now_add=True, null=True)), 31 | ('admin_type', models.CharField(default='regular_user', max_length=24)), 32 | ('reset_password_token', models.CharField(max_length=40, null=True)), 33 | ('reset_password_token_expire_time', models.DateTimeField(null=True)), 34 | ('auth_token', models.CharField(max_length=40, null=True)), 35 | ('two_factor_auth', models.BooleanField(default=False)), 36 | ('tfa_token', models.CharField(max_length=40, null=True)), 37 | ('open_api', models.BooleanField(default=False)), 38 | ('open_api_appkey', models.CharField(max_length=35, null=True)), 39 | ('is_disabled', models.BooleanField(default=False)), 40 | ], 41 | options={ 42 | 'db_table': 'user', 43 | }, 44 | managers=[ 45 | ('objects', account.models.UserManager()), 46 | ], 47 | ), 48 | migrations.CreateModel( 49 | name='UserProfile', 50 | fields=[ 51 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 52 | ('problems_status', jsonfield.fields.JSONField(default={})), 53 | ('avatar', models.CharField(default="default.png", max_length=50)), 54 | ('blog', models.URLField(blank=True, null=True)), 55 | ('mood', models.CharField(blank=True, max_length=200, null=True)), 56 | ('accepted_problem_number', models.IntegerField(default=0)), 57 | ('submission_number', models.IntegerField(default=0)), 58 | ('phone_number', models.CharField(blank=True, max_length=15, null=True)), 59 | ('school', models.CharField(blank=True, max_length=200, null=True)), 60 | ('major', models.CharField(blank=True, max_length=200, null=True)), 61 | ('student_id', models.CharField(blank=True, max_length=15, null=True)), 62 | ('time_zone', models.CharField(blank=True, max_length=32, null=True)), 63 | ('language', models.CharField(blank=True, max_length=32, null=True)), 64 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 65 | ], 66 | options={ 67 | 'db_table': 'user_profile', 68 | }, 69 | ), 70 | ] 71 | -------------------------------------------------------------------------------- /README-CN.md: -------------------------------------------------------------------------------- 1 | # OnlineJudge 2.0 2 | 3 | [![Python](https://img.shields.io/badge/python-3.8.0-blue.svg?style=flat-square)](https://www.python.org/downloads/release/python-362/) 4 | [![Django](https://img.shields.io/badge/django-3.2.9-blue.svg?style=flat-square)](https://www.djangoproject.com/) 5 | [![Django Rest Framework](https://img.shields.io/badge/django_rest_framework-3.12.0-blue.svg?style=flat-square)](http://www.django-rest-framework.org/) 6 | [![Build Status](https://travis-ci.org/QingdaoU/OnlineJudge.svg?branch=master)](https://travis-ci.org/QingdaoU/OnlineJudge) 7 | 8 | > #### 基于 Python 和 Vue 的在线评测系统。 [Demo](https://qduoj.com) 9 | 10 | [English Document](README.md) 11 | 12 | ## 概览 13 | 14 | + 基于 Docker,真正一键部署 15 | + 前后端分离,模块化编程,微服务 16 | + ACM/OI 两种比赛模式、实时/非实时评判 任意选择 17 | + 丰富的可视化图表,一图胜千言 18 | + 支持 Template Problem,可以添加函数题甚至填空题 19 | + 更细致的权限划分,超级管理员和普通管理员各司其职 20 | + 多语言支持:`C`, `C++`, `Java`, `Python2`, `Python3`,题目可以选择使用的语言 21 | + Markdown & MathJax 支持 22 | + 比赛用户IP限制 (CIDR ranges) 23 | 24 | 主要模块均已开源: 25 | 26 | + 后端(Django): [https://github.com/QingdaoU/OnlineJudge](https://github.com/QingdaoU/OnlineJudge) 27 | + 前端(Vue): [https://github.com/QingdaoU/OnlineJudgeFE](https://github.com/QingdaoU/OnlineJudgeFE) 28 | + 判题沙箱(Seccomp): [https://github.com/QingdaoU/Judger](https://github.com/QingdaoU/Judger) 29 | + 判题服务器(对Judger的封装): [https://github.com/QingdaoU/JudgeServer](https://github.com/QingdaoU/JudgeServer) 30 | 31 | ## 安装 32 | 33 | 请根据此进行安装: [https://github.com/QingdaoU/OnlineJudgeDeploy/tree/2.0](https://github.com/QingdaoU/OnlineJudgeDeploy/tree/2.0) 34 | 35 | ## 文档 36 | 37 | [http://opensource.qduoj.com/](http://opensource.qduoj.com/) 38 | 39 | ## 截图 40 | 41 | ### OJ前台 42 | 43 | ![problem-list](https://user-images.githubusercontent.com/20637881/33372506-402022e4-d539-11e7-8e64-6656f8ceb75a.png) 44 | 45 | ![problem-details](https://user-images.githubusercontent.com/20637881/33372507-4061a782-d539-11e7-8835-076ddae6b529.png) 46 | 47 | ![statistic-info](https://user-images.githubusercontent.com/20637881/33372508-40a0c6ce-d539-11e7-8d5e-024541b76750.png) 48 | 49 | ![contest-list](https://user-images.githubusercontent.com/20637881/33372509-40d880dc-d539-11e7-9eba-1f08dcb6b9a0.png) 50 | 51 | Rankings 中可以控制图表和菜单的显隐。 52 | ![acm-rankings](https://user-images.githubusercontent.com/20637881/33372510-41117f68-d539-11e7-9947-70e60bad3cf2.png) 53 | ![oi-rankings](https://user-images.githubusercontent.com/20637881/33372511-41d406fa-d539-11e7-9947-7a2a088785b0.png) 54 | 55 | ![status](https://user-images.githubusercontent.com/20637881/33372512-420ba240-d539-11e7-8645-594cac4a0b78.png) 56 | 57 | ![status-details](https://user-images.githubusercontent.com/20637881/33365523-787bd0ea-d523-11e7-953f-dacbf7a506df.png) 58 | 59 | ![user-home](https://user-images.githubusercontent.com/20637881/33365521-7842d808-d523-11e7-84c1-2e2aa0079f32.png) 60 | 61 | ### 后台管理 62 | 63 | ![admin-users](https://user-images.githubusercontent.com/20637881/33372516-42c34fda-d539-11e7-9f4e-5109477f83be.png) 64 | 65 | ![judge-server](https://user-images.githubusercontent.com/20637881/33372517-42faef9e-d539-11e7-9f17-df9be3583900.png) 66 | 67 | ![create-problem](https://user-images.githubusercontent.com/20637881/33372513-42472162-d539-11e7-8659-5497bf52dbea.png) 68 | 69 | ![create-contest](https://user-images.githubusercontent.com/20637881/33372514-428ab922-d539-11e7-8f68-da55dedf3ad3.png) 70 | 71 | 72 | ## 浏览器支持 73 | 74 | Modern browsers(chrome, firefox) 和 Internet Explorer 10+. 75 | 76 | ## 特别感谢 77 | 78 | + 所有为本项目做出贡献的人 79 | + [heb1c](https://github.com/hebicheng) 同学为我们提供了很多意见和建议 80 | 81 | 如果您觉得这个项目还不错,就star一下吧 :) 82 | 83 | ## 许可 84 | 85 | The [MIT](http://opensource.org/licenses/MIT) License 86 | -------------------------------------------------------------------------------- /utils/captcha/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2013 TY 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | """ 13 | 14 | import os 15 | import time 16 | import random 17 | 18 | from PIL import Image, ImageDraw, ImageFont 19 | 20 | 21 | class Captcha(object): 22 | def __init__(self, request): 23 | """ 24 | 初始化,设置各种属性 25 | """ 26 | self.django_request = request 27 | self.session_key = "_django_captcha_key" 28 | self.captcha_expires_time = "_django_captcha_expires_time" 29 | 30 | # 验证码图片尺寸 31 | self.img_width = 90 32 | self.img_height = 30 33 | 34 | def _get_font_size(self, code): 35 | """ 36 | 将图片高度的80%作为字体大小 37 | """ 38 | s1 = int(self.img_height * 0.8) 39 | s2 = int(self.img_width / len(code)) 40 | return int(min((s1, s2)) + max((s1, s2)) * 0.05) 41 | 42 | def _set_answer(self, answer): 43 | """ 44 | 设置答案和过期时间 45 | """ 46 | self.django_request.session[self.session_key] = str(answer) 47 | self.django_request.session[self.captcha_expires_time] = time.time() + 60 48 | 49 | def _make_code(self): 50 | """ 51 | 生成随机数或随机字符串 52 | """ 53 | string = random.sample("abcdefghkmnpqrstuvwxyzABCDEFGHGKMNOPQRSTUVWXYZ23456789", 4) 54 | self._set_answer("".join(string)) 55 | return string 56 | 57 | def get(self): 58 | """ 59 | 生成验证码图片,返回值为图片的bytes 60 | """ 61 | background = (random.randrange(200, 255), random.randrange(200, 255), random.randrange(200, 255)) 62 | code_color = (random.randrange(0, 50), random.randrange(0, 50), random.randrange(0, 50), 255) 63 | 64 | font_path = os.path.join(os.path.normpath(os.path.dirname(__file__)), "timesbi.ttf") 65 | 66 | image = Image.new("RGB", (self.img_width, self.img_height), background) 67 | code = self._make_code() 68 | font_size = self._get_font_size(code) 69 | draw = ImageDraw.Draw(image) 70 | 71 | # x是第一个字母的x坐标 72 | x = random.randrange(int(font_size * 0.3), int(font_size * 0.5)) 73 | 74 | for i in code: 75 | # 字符y坐标 76 | y = random.randrange(1, 7) 77 | # 随机字符大小 78 | font = ImageFont.truetype(font_path.replace("\\", "/"), font_size + random.randrange(-3, 7)) 79 | draw.text((x, y), i, font=font, fill=code_color) 80 | # 随机化字符之间的距离 字符粘连可以降低识别率 81 | x += font_size * random.randrange(6, 8) / 10 82 | 83 | self.django_request.session[self.session_key] = "".join(code) 84 | return image 85 | 86 | def check(self, code): 87 | """ 88 | 检查用户输入的验证码是否正确 89 | """ 90 | _code = self.django_request.session.get(self.session_key) or "" 91 | if not _code: 92 | return False 93 | expires_time = self.django_request.session.get(self.captcha_expires_time) or 0 94 | # 注意 如果验证之后不清除之前的验证码的话 可能会造成重复验证的现象 95 | del self.django_request.session[self.session_key] 96 | del self.django_request.session[self.captcha_expires_time] 97 | if _code.lower() == str(code).lower() and time.time() < expires_time: 98 | return True 99 | else: 100 | return False 101 | -------------------------------------------------------------------------------- /contest/models.py: -------------------------------------------------------------------------------- 1 | from utils.constants import ContestRuleType # noqa 2 | from django.db import models 3 | from django.utils.timezone import now 4 | from utils.models import JSONField 5 | 6 | from utils.constants import ContestStatus, ContestType 7 | from account.models import User 8 | from utils.models import RichTextField 9 | 10 | 11 | class Contest(models.Model): 12 | title = models.TextField() 13 | description = RichTextField() 14 | # show real time rank or cached rank 15 | real_time_rank = models.BooleanField() 16 | password = models.TextField(null=True) 17 | # enum of ContestRuleType 18 | rule_type = models.TextField() 19 | start_time = models.DateTimeField() 20 | end_time = models.DateTimeField() 21 | create_time = models.DateTimeField(auto_now_add=True) 22 | last_update_time = models.DateTimeField(auto_now=True) 23 | created_by = models.ForeignKey(User, on_delete=models.CASCADE) 24 | # 是否可见 false的话相当于删除 25 | visible = models.BooleanField(default=True) 26 | allowed_ip_ranges = JSONField(default=list) 27 | 28 | @property 29 | def status(self): 30 | if self.start_time > now(): 31 | # 没有开始 返回1 32 | return ContestStatus.CONTEST_NOT_START 33 | elif self.end_time < now(): 34 | # 已经结束 返回-1 35 | return ContestStatus.CONTEST_ENDED 36 | else: 37 | # 正在进行 返回0 38 | return ContestStatus.CONTEST_UNDERWAY 39 | 40 | @property 41 | def contest_type(self): 42 | if self.password: 43 | return ContestType.PASSWORD_PROTECTED_CONTEST 44 | return ContestType.PUBLIC_CONTEST 45 | 46 | # 是否有权查看problem 的一些统计信息 诸如submission_number, accepted_number 等 47 | def problem_details_permission(self, user): 48 | return self.rule_type == ContestRuleType.ACM or \ 49 | self.status == ContestStatus.CONTEST_ENDED or \ 50 | user.is_authenticated and user.is_contest_admin(self) or \ 51 | self.real_time_rank 52 | 53 | class Meta: 54 | db_table = "contest" 55 | ordering = ("-start_time",) 56 | 57 | 58 | class AbstractContestRank(models.Model): 59 | user = models.ForeignKey(User, on_delete=models.CASCADE) 60 | contest = models.ForeignKey(Contest, on_delete=models.CASCADE) 61 | submission_number = models.IntegerField(default=0) 62 | 63 | class Meta: 64 | abstract = True 65 | 66 | 67 | class ACMContestRank(AbstractContestRank): 68 | accepted_number = models.IntegerField(default=0) 69 | # total_time is only for ACM contest, total_time = ac time + none-ac times * 20 * 60 70 | total_time = models.IntegerField(default=0) 71 | # {"23": {"is_ac": True, "ac_time": 8999, "error_number": 2, "is_first_ac": True}} 72 | # key is problem id 73 | submission_info = JSONField(default=dict) 74 | 75 | class Meta: 76 | db_table = "acm_contest_rank" 77 | unique_together = (("user", "contest"),) 78 | 79 | 80 | class OIContestRank(AbstractContestRank): 81 | total_score = models.IntegerField(default=0) 82 | # {"23": 333} 83 | # key is problem id, value is current score 84 | submission_info = JSONField(default=dict) 85 | 86 | class Meta: 87 | db_table = "oi_contest_rank" 88 | unique_together = (("user", "contest"),) 89 | 90 | 91 | class ContestAnnouncement(models.Model): 92 | contest = models.ForeignKey(Contest, on_delete=models.CASCADE) 93 | title = models.TextField() 94 | content = RichTextField() 95 | created_by = models.ForeignKey(User, on_delete=models.CASCADE) 96 | visible = models.BooleanField(default=True) 97 | create_time = models.DateTimeField(auto_now_add=True) 98 | 99 | class Meta: 100 | db_table = "contest_announcement" 101 | ordering = ("-create_time",) 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OnlineJudge 2.0 2 | 3 | [![Python](https://img.shields.io/badge/python-3.8.0-blue.svg?style=flat-square)](https://www.python.org/downloads/release/python-362/) 4 | [![Django](https://img.shields.io/badge/django-3.2.9-blue.svg?style=flat-square)](https://www.djangoproject.com/) 5 | [![Django Rest Framework](https://img.shields.io/badge/django_rest_framework-3.12.0-blue.svg?style=flat-square)](http://www.django-rest-framework.org/) 6 | [![Build Status](https://travis-ci.org/QingdaoU/OnlineJudge.svg?branch=master)](https://travis-ci.org/QingdaoU/OnlineJudge) 7 | 8 | > #### An onlinejudge system based on Python and Vue. [Demo](https://qduoj.com) 9 | 10 | [中文文档](README-CN.md) 11 | 12 | ## Overview 13 | 14 | + Based on Docker; One-click deployment 15 | + Separated backend and frontend; Modular programming; Micro service 16 | + ACM/OI rule support; realtime/non-realtime rank support 17 | + Amazing charting and visualization 18 | + Template-problem support 19 | + More reasonable permission control 20 | + Multi-language support: `C`, `C++`, `Java`, `Python2`, `Python3` 21 | + Markdown & MathJax support 22 | + Contest participants IP limit(CIDR) 23 | 24 | Main modules are available below: 25 | 26 | + Backend(Django): [https://github.com/QingdaoU/OnlineJudge](https://github.com/QingdaoU/OnlineJudge) 27 | + Frontend(Vue): [https://github.com/QingdaoU/OnlineJudgeFE](https://github.com/QingdaoU/OnlineJudgeFE) 28 | + Judger Sandbox(Seccomp): [https://github.com/QingdaoU/Judger](https://github.com/QingdaoU/Judger) 29 | + JudgeServer(A wrapper for Judger): [https://github.com/QingdaoU/JudgeServer](https://github.com/QingdaoU/JudgeServer) 30 | 31 | ## Installation 32 | 33 | Follow me: [https://github.com/QingdaoU/OnlineJudgeDeploy/tree/2.0](https://github.com/QingdaoU/OnlineJudgeDeploy/tree/2.0) 34 | 35 | ## Documents 36 | 37 | [http://opensource.qduoj.com/](http://opensource.qduoj.com/) 38 | 39 | ## Screenshots 40 | 41 | ### Frontend: 42 | 43 | ![problem-list](https://user-images.githubusercontent.com/20637881/33372506-402022e4-d539-11e7-8e64-6656f8ceb75a.png) 44 | 45 | ![problem-details](https://user-images.githubusercontent.com/20637881/33372507-4061a782-d539-11e7-8835-076ddae6b529.png) 46 | 47 | ![statistic-info](https://user-images.githubusercontent.com/20637881/33372508-40a0c6ce-d539-11e7-8d5e-024541b76750.png) 48 | 49 | ![contest-list](https://user-images.githubusercontent.com/20637881/33372509-40d880dc-d539-11e7-9eba-1f08dcb6b9a0.png) 50 | 51 | You can control the menu and chart status in rankings. 52 | 53 | ![acm-rankings](https://user-images.githubusercontent.com/20637881/33372510-41117f68-d539-11e7-9947-70e60bad3cf2.png) 54 | 55 | ![oi-rankings](https://user-images.githubusercontent.com/20637881/33372511-41d406fa-d539-11e7-9947-7a2a088785b0.png) 56 | 57 | ![status](https://user-images.githubusercontent.com/20637881/33372512-420ba240-d539-11e7-8645-594cac4a0b78.png) 58 | 59 | ![status-details](https://user-images.githubusercontent.com/20637881/33365523-787bd0ea-d523-11e7-953f-dacbf7a506df.png) 60 | 61 | ![user-home](https://user-images.githubusercontent.com/20637881/33365521-7842d808-d523-11e7-84c1-2e2aa0079f32.png) 62 | 63 | ### Admin: 64 | 65 | ![admin-users](https://user-images.githubusercontent.com/20637881/33372516-42c34fda-d539-11e7-9f4e-5109477f83be.png) 66 | 67 | ![judge-server](https://user-images.githubusercontent.com/20637881/33372517-42faef9e-d539-11e7-9f17-df9be3583900.png) 68 | 69 | ![create-problem](https://user-images.githubusercontent.com/20637881/33372513-42472162-d539-11e7-8659-5497bf52dbea.png) 70 | 71 | ![create-contest](https://user-images.githubusercontent.com/20637881/33372514-428ab922-d539-11e7-8f68-da55dedf3ad3.png) 72 | 73 | ## Browser Support 74 | 75 | Modern browsers(chrome, firefox) and Internet Explorer 10+. 76 | 77 | ## Thanks 78 | 79 | + I'd appreciate a star if you find this helpful. 80 | + Thanks to everyone that contributes to this project. 81 | + Special thanks to [heb1c](https://github.com/hebicheng), who has given us a lot of suggestions. 82 | 83 | ## License 84 | 85 | [MIT](http://opensource.org/licenses/MIT) 86 | -------------------------------------------------------------------------------- /account/migrations/0008_auto_20171011_1214.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.4 on 2017-10-11 12:14 3 | from __future__ import unicode_literals 4 | 5 | import django.contrib.postgres.fields.jsonb 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('account', '0006_user_session_keys'), 13 | ] 14 | 15 | operations = [ 16 | migrations.RemoveField( 17 | model_name='userprofile', 18 | name='language', 19 | ), 20 | migrations.AlterField( 21 | model_name='user', 22 | name='admin_type', 23 | field=models.CharField(default='Regular User', max_length=32), 24 | ), 25 | migrations.AlterField( 26 | model_name='user', 27 | name='auth_token', 28 | field=models.CharField(max_length=32, null=True), 29 | ), 30 | migrations.AlterField( 31 | model_name='user', 32 | name='email', 33 | field=models.EmailField(max_length=64, null=True), 34 | ), 35 | migrations.AlterField( 36 | model_name='user', 37 | name='open_api_appkey', 38 | field=models.CharField(max_length=32, null=True), 39 | ), 40 | migrations.AlterField( 41 | model_name='user', 42 | name='problem_permission', 43 | field=models.CharField(default='None', max_length=32), 44 | ), 45 | migrations.AlterField( 46 | model_name='user', 47 | name='reset_password_token', 48 | field=models.CharField(max_length=32, null=True), 49 | ), 50 | migrations.AlterField( 51 | model_name='user', 52 | name='session_keys', 53 | field=django.contrib.postgres.fields.jsonb.JSONField(default=list), 54 | ), 55 | migrations.AlterField( 56 | model_name='user', 57 | name='tfa_token', 58 | field=models.CharField(max_length=32, null=True), 59 | ), 60 | migrations.AlterField( 61 | model_name='user', 62 | name='username', 63 | field=models.CharField(max_length=32, unique=True), 64 | ), 65 | migrations.AlterField( 66 | model_name='userprofile', 67 | name='acm_problems_status', 68 | field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), 69 | ), 70 | migrations.AlterField( 71 | model_name='userprofile', 72 | name='avatar', 73 | field=models.CharField(default='/static/avatar/default.png', max_length=256), 74 | ), 75 | migrations.AlterField( 76 | model_name='userprofile', 77 | name='github', 78 | field=models.CharField(blank=True, max_length=64, null=True), 79 | ), 80 | migrations.AlterField( 81 | model_name='userprofile', 82 | name='major', 83 | field=models.CharField(blank=True, max_length=64, null=True), 84 | ), 85 | migrations.AlterField( 86 | model_name='userprofile', 87 | name='mood', 88 | field=models.CharField(blank=True, max_length=256, null=True), 89 | ), 90 | migrations.AlterField( 91 | model_name='userprofile', 92 | name='oi_problems_status', 93 | field=django.contrib.postgres.fields.jsonb.JSONField(default=dict), 94 | ), 95 | migrations.AlterField( 96 | model_name='userprofile', 97 | name='real_name', 98 | field=models.CharField(blank=True, max_length=32, null=True), 99 | ), 100 | migrations.AlterField( 101 | model_name='userprofile', 102 | name='school', 103 | field=models.CharField(blank=True, max_length=64, null=True), 104 | ), 105 | ] 106 | -------------------------------------------------------------------------------- /contest/serializers.py: -------------------------------------------------------------------------------- 1 | from utils.api import UsernameSerializer, serializers 2 | 3 | from .models import Contest, ContestAnnouncement, ContestRuleType 4 | from .models import ACMContestRank, OIContestRank 5 | 6 | 7 | class CreateConetestSeriaizer(serializers.Serializer): 8 | title = serializers.CharField(max_length=128) 9 | description = serializers.CharField() 10 | start_time = serializers.DateTimeField() 11 | end_time = serializers.DateTimeField() 12 | rule_type = serializers.ChoiceField(choices=[ContestRuleType.ACM, ContestRuleType.OI]) 13 | password = serializers.CharField(allow_blank=True, max_length=32) 14 | visible = serializers.BooleanField() 15 | real_time_rank = serializers.BooleanField() 16 | allowed_ip_ranges = serializers.ListField(child=serializers.CharField(max_length=32), allow_empty=True) 17 | 18 | 19 | class EditConetestSeriaizer(serializers.Serializer): 20 | id = serializers.IntegerField() 21 | title = serializers.CharField(max_length=128) 22 | description = serializers.CharField() 23 | start_time = serializers.DateTimeField() 24 | end_time = serializers.DateTimeField() 25 | password = serializers.CharField(allow_blank=True, allow_null=True, max_length=32) 26 | visible = serializers.BooleanField() 27 | real_time_rank = serializers.BooleanField() 28 | allowed_ip_ranges = serializers.ListField(child=serializers.CharField(max_length=32)) 29 | 30 | 31 | class ContestAdminSerializer(serializers.ModelSerializer): 32 | created_by = UsernameSerializer() 33 | status = serializers.CharField() 34 | contest_type = serializers.CharField() 35 | 36 | class Meta: 37 | model = Contest 38 | fields = "__all__" 39 | 40 | 41 | class ContestSerializer(ContestAdminSerializer): 42 | class Meta: 43 | model = Contest 44 | exclude = ("password", "visible", "allowed_ip_ranges") 45 | 46 | 47 | class ContestAnnouncementSerializer(serializers.ModelSerializer): 48 | created_by = UsernameSerializer() 49 | 50 | class Meta: 51 | model = ContestAnnouncement 52 | fields = "__all__" 53 | 54 | 55 | class CreateContestAnnouncementSerializer(serializers.Serializer): 56 | contest_id = serializers.IntegerField() 57 | title = serializers.CharField(max_length=128) 58 | content = serializers.CharField() 59 | visible = serializers.BooleanField() 60 | 61 | 62 | class EditContestAnnouncementSerializer(serializers.Serializer): 63 | id = serializers.IntegerField() 64 | title = serializers.CharField(max_length=128, required=False) 65 | content = serializers.CharField(required=False, allow_blank=True) 66 | visible = serializers.BooleanField(required=False) 67 | 68 | 69 | class ContestPasswordVerifySerializer(serializers.Serializer): 70 | contest_id = serializers.IntegerField() 71 | password = serializers.CharField(max_length=30, required=True) 72 | 73 | 74 | class ACMContestRankSerializer(serializers.ModelSerializer): 75 | user = serializers.SerializerMethodField() 76 | 77 | class Meta: 78 | model = ACMContestRank 79 | fields = "__all__" 80 | 81 | def __init__(self, *args, **kwargs): 82 | self.is_contest_admin = kwargs.pop("is_contest_admin", False) 83 | super().__init__(*args, **kwargs) 84 | 85 | def get_user(self, obj): 86 | return UsernameSerializer(obj.user, need_real_name=self.is_contest_admin).data 87 | 88 | 89 | class OIContestRankSerializer(serializers.ModelSerializer): 90 | user = serializers.SerializerMethodField() 91 | 92 | class Meta: 93 | model = OIContestRank 94 | fields = "__all__" 95 | 96 | def __init__(self, *args, **kwargs): 97 | self.is_contest_admin = kwargs.pop("is_contest_admin", False) 98 | super().__init__(*args, **kwargs) 99 | 100 | def get_user(self, obj): 101 | return UsernameSerializer(obj.user, need_real_name=self.is_contest_admin).data 102 | 103 | 104 | class ACMContesHelperSerializer(serializers.Serializer): 105 | contest_id = serializers.IntegerField() 106 | problem_id = serializers.CharField() 107 | rank_id = serializers.IntegerField() 108 | checked = serializers.BooleanField() 109 | -------------------------------------------------------------------------------- /account/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractBaseUser 2 | from django.conf import settings 3 | from django.db import models 4 | from utils.models import JSONField 5 | 6 | 7 | class AdminType(object): 8 | REGULAR_USER = "Regular User" 9 | ADMIN = "Admin" 10 | SUPER_ADMIN = "Super Admin" 11 | 12 | 13 | class ProblemPermission(object): 14 | NONE = "None" 15 | OWN = "Own" 16 | ALL = "All" 17 | 18 | 19 | class UserManager(models.Manager): 20 | use_in_migrations = True 21 | 22 | def get_by_natural_key(self, username): 23 | return self.get(**{f"{self.model.USERNAME_FIELD}__iexact": username}) 24 | 25 | 26 | class User(AbstractBaseUser): 27 | username = models.TextField(unique=True) 28 | email = models.TextField(null=True) 29 | create_time = models.DateTimeField(auto_now_add=True, null=True) 30 | # One of UserType 31 | admin_type = models.TextField(default=AdminType.REGULAR_USER) 32 | problem_permission = models.TextField(default=ProblemPermission.NONE) 33 | reset_password_token = models.TextField(null=True) 34 | reset_password_token_expire_time = models.DateTimeField(null=True) 35 | # SSO auth token 36 | auth_token = models.TextField(null=True) 37 | two_factor_auth = models.BooleanField(default=False) 38 | tfa_token = models.TextField(null=True) 39 | session_keys = JSONField(default=list) 40 | # open api key 41 | open_api = models.BooleanField(default=False) 42 | open_api_appkey = models.TextField(null=True) 43 | is_disabled = models.BooleanField(default=False) 44 | 45 | USERNAME_FIELD = "username" 46 | REQUIRED_FIELDS = [] 47 | 48 | objects = UserManager() 49 | 50 | def is_admin(self): 51 | return self.admin_type == AdminType.ADMIN 52 | 53 | def is_super_admin(self): 54 | return self.admin_type == AdminType.SUPER_ADMIN 55 | 56 | def is_admin_role(self): 57 | return self.admin_type in [AdminType.ADMIN, AdminType.SUPER_ADMIN] 58 | 59 | def can_mgmt_all_problem(self): 60 | return self.problem_permission == ProblemPermission.ALL 61 | 62 | def is_contest_admin(self, contest): 63 | return self.is_authenticated and (contest.created_by == self or self.admin_type == AdminType.SUPER_ADMIN) 64 | 65 | class Meta: 66 | db_table = "user" 67 | 68 | 69 | class UserProfile(models.Model): 70 | user = models.OneToOneField(User, on_delete=models.CASCADE) 71 | # acm_problems_status examples: 72 | # { 73 | # "problems": { 74 | # "1": { 75 | # "status": JudgeStatus.ACCEPTED, 76 | # "_id": "1000" 77 | # } 78 | # }, 79 | # "contest_problems": { 80 | # "1": { 81 | # "status": JudgeStatus.ACCEPTED, 82 | # "_id": "1000" 83 | # } 84 | # } 85 | # } 86 | acm_problems_status = JSONField(default=dict) 87 | # like acm_problems_status, merely add "score" field 88 | oi_problems_status = JSONField(default=dict) 89 | 90 | real_name = models.TextField(null=True) 91 | avatar = models.TextField(default=f"{settings.AVATAR_URI_PREFIX}/default.png") 92 | blog = models.URLField(null=True) 93 | mood = models.TextField(null=True) 94 | github = models.TextField(null=True) 95 | school = models.TextField(null=True) 96 | major = models.TextField(null=True) 97 | language = models.TextField(null=True) 98 | # for ACM 99 | accepted_number = models.IntegerField(default=0) 100 | # for OI 101 | total_score = models.BigIntegerField(default=0) 102 | submission_number = models.IntegerField(default=0) 103 | 104 | def add_accepted_problem_number(self): 105 | self.accepted_number = models.F("accepted_number") + 1 106 | self.save() 107 | 108 | def add_submission_number(self): 109 | self.submission_number = models.F("submission_number") + 1 110 | self.save() 111 | 112 | # 计算总分时, 应先减掉上次该题所得分数, 然后再加上本次所得分数 113 | def add_score(self, this_time_score, last_time_score=None): 114 | last_time_score = last_time_score or 0 115 | self.total_score = models.F("total_score") - last_time_score + this_time_score 116 | self.save() 117 | 118 | class Meta: 119 | db_table = "user_profile" 120 | -------------------------------------------------------------------------------- /fps/fps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <![CDATA[A+B Problem]]> 6 | 7 | 8 | 9 | ]]> 10 | 11 | Two integer a,b (0<=a,b<=10) 12 |

]]> 13 | 14 | Output a+b 15 |

]]>
16 | 17 | 18 | 20 | 21 | 24 | 26 | 27 | 28 | 29 | int main() 30 | { 31 | int a,b; 32 | while(scanf("%d%d",&a,&b)!=EOF) 33 | { 34 | printf("%d\n",a+b); 35 | } 36 | return 0; 37 | }]]> 38 | 48 | 49 | #include 50 | using namespace std; 51 | int main() 52 | { 53 | #ifndef ONLINE_JUDGE 54 | freopen("in.txt","r",stdin); 55 | #endif 56 | int a,b; 57 | while(cin >>a >>b) 58 | { 59 | cout < 63 | 72 | 79 | 95 | 103 | 117 | 124 | 134 | 143 |
145 |
146 | -------------------------------------------------------------------------------- /problem/views/oj.py: -------------------------------------------------------------------------------- 1 | import random 2 | from django.db.models import Q, Count 3 | from utils.api import APIView 4 | from account.decorators import check_contest_permission 5 | from ..models import ProblemTag, Problem, ProblemRuleType 6 | from ..serializers import ProblemSerializer, TagSerializer, ProblemSafeSerializer 7 | from contest.models import ContestRuleType 8 | 9 | 10 | class ProblemTagAPI(APIView): 11 | def get(self, request): 12 | qs = ProblemTag.objects 13 | keyword = request.GET.get("keyword") 14 | if keyword: 15 | qs = ProblemTag.objects.filter(name__icontains=keyword) 16 | tags = qs.annotate(problem_count=Count("problem")).filter(problem_count__gt=0) 17 | return self.success(TagSerializer(tags, many=True).data) 18 | 19 | 20 | class PickOneAPI(APIView): 21 | def get(self, request): 22 | problems = Problem.objects.filter(contest_id__isnull=True, visible=True) 23 | count = problems.count() 24 | if count == 0: 25 | return self.error("No problem to pick") 26 | return self.success(problems[random.randint(0, count - 1)]._id) 27 | 28 | 29 | class ProblemAPI(APIView): 30 | @staticmethod 31 | def _add_problem_status(request, queryset_values): 32 | if request.user.is_authenticated: 33 | profile = request.user.userprofile 34 | acm_problems_status = profile.acm_problems_status.get("problems", {}) 35 | oi_problems_status = profile.oi_problems_status.get("problems", {}) 36 | # paginate data 37 | results = queryset_values.get("results") 38 | if results is not None: 39 | problems = results 40 | else: 41 | problems = [queryset_values, ] 42 | for problem in problems: 43 | if problem["rule_type"] == ProblemRuleType.ACM: 44 | problem["my_status"] = acm_problems_status.get(str(problem["id"]), {}).get("status") 45 | else: 46 | problem["my_status"] = oi_problems_status.get(str(problem["id"]), {}).get("status") 47 | 48 | def get(self, request): 49 | # 问题详情页 50 | problem_id = request.GET.get("problem_id") 51 | if problem_id: 52 | try: 53 | problem = Problem.objects.select_related("created_by") \ 54 | .get(_id=problem_id, contest_id__isnull=True, visible=True) 55 | problem_data = ProblemSerializer(problem).data 56 | self._add_problem_status(request, problem_data) 57 | return self.success(problem_data) 58 | except Problem.DoesNotExist: 59 | return self.error("Problem does not exist") 60 | 61 | limit = request.GET.get("limit") 62 | if not limit: 63 | return self.error("Limit is needed") 64 | 65 | problems = Problem.objects.select_related("created_by").filter(contest_id__isnull=True, visible=True) 66 | # 按照标签筛选 67 | tag_text = request.GET.get("tag") 68 | if tag_text: 69 | problems = problems.filter(tags__name=tag_text) 70 | 71 | # 搜索的情况 72 | keyword = request.GET.get("keyword", "").strip() 73 | if keyword: 74 | problems = problems.filter(Q(title__icontains=keyword) | Q(_id__icontains=keyword)) 75 | 76 | # 难度筛选 77 | difficulty = request.GET.get("difficulty") 78 | if difficulty: 79 | problems = problems.filter(difficulty=difficulty) 80 | # 根据profile 为做过的题目添加标记 81 | data = self.paginate_data(request, problems, ProblemSerializer) 82 | self._add_problem_status(request, data) 83 | return self.success(data) 84 | 85 | 86 | class ContestProblemAPI(APIView): 87 | def _add_problem_status(self, request, queryset_values): 88 | if request.user.is_authenticated: 89 | profile = request.user.userprofile 90 | if self.contest.rule_type == ContestRuleType.ACM: 91 | problems_status = profile.acm_problems_status.get("contest_problems", {}) 92 | else: 93 | problems_status = profile.oi_problems_status.get("contest_problems", {}) 94 | for problem in queryset_values: 95 | problem["my_status"] = problems_status.get(str(problem["id"]), {}).get("status") 96 | 97 | @check_contest_permission(check_type="problems") 98 | def get(self, request): 99 | problem_id = request.GET.get("problem_id") 100 | if problem_id: 101 | try: 102 | problem = Problem.objects.select_related("created_by").get(_id=problem_id, 103 | contest=self.contest, 104 | visible=True) 105 | except Problem.DoesNotExist: 106 | return self.error("Problem does not exist.") 107 | if self.contest.problem_details_permission(request.user): 108 | problem_data = ProblemSerializer(problem).data 109 | self._add_problem_status(request, [problem_data, ]) 110 | else: 111 | problem_data = ProblemSafeSerializer(problem).data 112 | return self.success(problem_data) 113 | 114 | contest_problems = Problem.objects.select_related("created_by").filter(contest=self.contest, visible=True) 115 | if self.contest.problem_details_permission(request.user): 116 | data = ProblemSerializer(contest_problems, many=True).data 117 | self._add_problem_status(request, data) 118 | else: 119 | data = ProblemSafeSerializer(contest_problems, many=True).data 120 | return self.success(data) 121 | -------------------------------------------------------------------------------- /account/serializers.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from utils.api import serializers, UsernameSerializer 4 | 5 | from .models import AdminType, ProblemPermission, User, UserProfile 6 | 7 | 8 | class UserLoginSerializer(serializers.Serializer): 9 | username = serializers.CharField() 10 | password = serializers.CharField() 11 | tfa_code = serializers.CharField(required=False, allow_blank=True) 12 | 13 | 14 | class UsernameOrEmailCheckSerializer(serializers.Serializer): 15 | username = serializers.CharField(required=False) 16 | email = serializers.EmailField(required=False) 17 | 18 | 19 | class UserRegisterSerializer(serializers.Serializer): 20 | username = serializers.CharField(max_length=32) 21 | password = serializers.CharField(min_length=6) 22 | email = serializers.EmailField(max_length=64) 23 | captcha = serializers.CharField() 24 | 25 | 26 | class UserChangePasswordSerializer(serializers.Serializer): 27 | old_password = serializers.CharField() 28 | new_password = serializers.CharField(min_length=6) 29 | tfa_code = serializers.CharField(required=False, allow_blank=True) 30 | 31 | 32 | class UserChangeEmailSerializer(serializers.Serializer): 33 | password = serializers.CharField() 34 | new_email = serializers.EmailField(max_length=64) 35 | tfa_code = serializers.CharField(required=False, allow_blank=True) 36 | 37 | 38 | class GenerateUserSerializer(serializers.Serializer): 39 | prefix = serializers.CharField(max_length=16, allow_blank=True) 40 | suffix = serializers.CharField(max_length=16, allow_blank=True) 41 | number_from = serializers.IntegerField() 42 | number_to = serializers.IntegerField() 43 | password_length = serializers.IntegerField(max_value=16, default=8) 44 | 45 | 46 | class ImportUserSeralizer(serializers.Serializer): 47 | users = serializers.ListField( 48 | child=serializers.ListField(child=serializers.CharField(max_length=64))) 49 | 50 | 51 | class UserAdminSerializer(serializers.ModelSerializer): 52 | real_name = serializers.SerializerMethodField() 53 | 54 | class Meta: 55 | model = User 56 | fields = ["id", "username", "email", "admin_type", "problem_permission", "real_name", 57 | "create_time", "last_login", "two_factor_auth", "open_api", "is_disabled"] 58 | 59 | def get_real_name(self, obj): 60 | return obj.userprofile.real_name 61 | 62 | 63 | class UserSerializer(serializers.ModelSerializer): 64 | class Meta: 65 | model = User 66 | fields = ["id", "username", "email", "admin_type", "problem_permission", 67 | "create_time", "last_login", "two_factor_auth", "open_api", "is_disabled"] 68 | 69 | 70 | class UserProfileSerializer(serializers.ModelSerializer): 71 | user = UserSerializer() 72 | real_name = serializers.SerializerMethodField() 73 | 74 | class Meta: 75 | model = UserProfile 76 | fields = "__all__" 77 | 78 | def __init__(self, *args, **kwargs): 79 | self.show_real_name = kwargs.pop("show_real_name", False) 80 | super(UserProfileSerializer, self).__init__(*args, **kwargs) 81 | 82 | def get_real_name(self, obj): 83 | return obj.real_name if self.show_real_name else None 84 | 85 | 86 | class EditUserSerializer(serializers.Serializer): 87 | id = serializers.IntegerField() 88 | username = serializers.CharField(max_length=32) 89 | real_name = serializers.CharField(max_length=32, allow_blank=True, allow_null=True) 90 | password = serializers.CharField(min_length=6, allow_blank=True, required=False, default=None) 91 | email = serializers.EmailField(max_length=64) 92 | admin_type = serializers.ChoiceField(choices=(AdminType.REGULAR_USER, AdminType.ADMIN, AdminType.SUPER_ADMIN)) 93 | problem_permission = serializers.ChoiceField(choices=(ProblemPermission.NONE, ProblemPermission.OWN, 94 | ProblemPermission.ALL)) 95 | open_api = serializers.BooleanField() 96 | two_factor_auth = serializers.BooleanField() 97 | is_disabled = serializers.BooleanField() 98 | 99 | 100 | class EditUserProfileSerializer(serializers.Serializer): 101 | real_name = serializers.CharField(max_length=32, allow_null=True, required=False) 102 | avatar = serializers.CharField(max_length=256, allow_blank=True, required=False) 103 | blog = serializers.URLField(max_length=256, allow_blank=True, required=False) 104 | mood = serializers.CharField(max_length=256, allow_blank=True, required=False) 105 | github = serializers.URLField(max_length=256, allow_blank=True, required=False) 106 | school = serializers.CharField(max_length=64, allow_blank=True, required=False) 107 | major = serializers.CharField(max_length=64, allow_blank=True, required=False) 108 | language = serializers.CharField(max_length=32, allow_blank=True, required=False) 109 | 110 | 111 | class ApplyResetPasswordSerializer(serializers.Serializer): 112 | email = serializers.EmailField() 113 | captcha = serializers.CharField() 114 | 115 | 116 | class ResetPasswordSerializer(serializers.Serializer): 117 | token = serializers.CharField() 118 | password = serializers.CharField(min_length=6) 119 | captcha = serializers.CharField() 120 | 121 | 122 | class SSOSerializer(serializers.Serializer): 123 | token = serializers.CharField() 124 | 125 | 126 | class TwoFactorAuthCodeSerializer(serializers.Serializer): 127 | code = serializers.IntegerField() 128 | 129 | 130 | class ImageUploadForm(forms.Form): 131 | image = forms.FileField() 132 | 133 | 134 | class FileUploadForm(forms.Form): 135 | file = forms.FileField() 136 | 137 | 138 | class RankInfoSerializer(serializers.ModelSerializer): 139 | user = UsernameSerializer() 140 | 141 | class Meta: 142 | model = UserProfile 143 | fields = "__all__" 144 | -------------------------------------------------------------------------------- /account/decorators.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import hashlib 3 | import time 4 | 5 | from problem.models import Problem 6 | from contest.models import Contest, ContestType, ContestStatus, ContestRuleType 7 | from utils.api import JSONResponse, APIError 8 | from utils.constants import CONTEST_PASSWORD_SESSION_KEY 9 | from .models import ProblemPermission 10 | 11 | 12 | class BasePermissionDecorator(object): 13 | def __init__(self, func): 14 | self.func = func 15 | 16 | def __get__(self, obj, obj_type): 17 | return functools.partial(self.__call__, obj) 18 | 19 | def error(self, data): 20 | return JSONResponse.response({"error": "permission-denied", "data": data}) 21 | 22 | def __call__(self, *args, **kwargs): 23 | self.request = args[1] 24 | 25 | if self.check_permission(): 26 | if self.request.user.is_disabled: 27 | return self.error("Your account is disabled") 28 | return self.func(*args, **kwargs) 29 | else: 30 | return self.error("Please login first") 31 | 32 | def check_permission(self): 33 | raise NotImplementedError() 34 | 35 | 36 | class login_required(BasePermissionDecorator): 37 | def check_permission(self): 38 | return self.request.user.is_authenticated 39 | 40 | 41 | class super_admin_required(BasePermissionDecorator): 42 | def check_permission(self): 43 | user = self.request.user 44 | return user.is_authenticated and user.is_super_admin() 45 | 46 | 47 | class admin_role_required(BasePermissionDecorator): 48 | def check_permission(self): 49 | user = self.request.user 50 | return user.is_authenticated and user.is_admin_role() 51 | 52 | 53 | class problem_permission_required(admin_role_required): 54 | def check_permission(self): 55 | if not super(problem_permission_required, self).check_permission(): 56 | return False 57 | if self.request.user.problem_permission == ProblemPermission.NONE: 58 | return False 59 | return True 60 | 61 | 62 | def check_contest_password(password, contest_password): 63 | if not (password and contest_password): 64 | return False 65 | if password == contest_password: 66 | return True 67 | else: 68 | # sig#timestamp 这种形式的密码也可以,但是在界面上没提供支持 69 | # sig = sha256(contest_password + timestamp)[:8] 70 | if "#" in password: 71 | s = password.split("#") 72 | if len(s) != 2: 73 | return False 74 | sig, ts = s[0], s[1] 75 | 76 | if sig == hashlib.sha256((contest_password + ts).encode("utf-8")).hexdigest()[:8]: 77 | try: 78 | ts = int(ts) 79 | except Exception: 80 | return False 81 | return int(time.time()) < ts 82 | else: 83 | return False 84 | else: 85 | return False 86 | 87 | 88 | def check_contest_permission(check_type="details"): 89 | """ 90 | 只供Class based view 使用,检查用户是否有权进入该contest, check_type 可选 details, problems, ranks, submissions 91 | 若通过验证,在view中可通过self.contest获得该contest 92 | """ 93 | 94 | def decorator(func): 95 | def _check_permission(*args, **kwargs): 96 | self = args[0] 97 | request = args[1] 98 | user = request.user 99 | if request.data.get("contest_id"): 100 | contest_id = request.data["contest_id"] 101 | else: 102 | contest_id = request.GET.get("contest_id") 103 | if not contest_id: 104 | return self.error("Parameter error, contest_id is required") 105 | 106 | try: 107 | # use self.contest to avoid query contest again in view. 108 | self.contest = Contest.objects.select_related("created_by").get(id=contest_id, visible=True) 109 | except Contest.DoesNotExist: 110 | return self.error("Contest %s doesn't exist" % contest_id) 111 | 112 | # Anonymous 113 | if not user.is_authenticated: 114 | return self.error("Please login first.") 115 | 116 | # creator or owner 117 | if user.is_contest_admin(self.contest): 118 | return func(*args, **kwargs) 119 | 120 | if self.contest.contest_type == ContestType.PASSWORD_PROTECTED_CONTEST: 121 | # password error 122 | if not check_contest_password(request.session.get(CONTEST_PASSWORD_SESSION_KEY, {}).get(self.contest.id), self.contest.password): 123 | return self.error("Wrong password or password expired") 124 | 125 | # regular user get contest problems, ranks etc. before contest started 126 | if self.contest.status == ContestStatus.CONTEST_NOT_START and check_type != "details": 127 | return self.error("Contest has not started yet.") 128 | 129 | # check does user have permission to get ranks, submissions in OI Contest 130 | if self.contest.status == ContestStatus.CONTEST_UNDERWAY and self.contest.rule_type == ContestRuleType.OI: 131 | if not self.contest.real_time_rank and (check_type == "ranks" or check_type == "submissions"): 132 | return self.error(f"No permission to get {check_type}") 133 | 134 | return func(*args, **kwargs) 135 | return _check_permission 136 | return decorator 137 | 138 | 139 | def ensure_created_by(obj, user): 140 | e = APIError(msg=f"{obj.__class__.__name__} does not exist") 141 | if not user.is_admin_role(): 142 | raise e 143 | if user.is_super_admin(): 144 | return 145 | if isinstance(obj, Problem): 146 | if not user.can_mgmt_all_problem() and obj.created_by != user: 147 | raise e 148 | elif obj.created_by != user: 149 | raise e 150 | -------------------------------------------------------------------------------- /utils/api/api.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import json 3 | import logging 4 | 5 | from django.http import HttpResponse, QueryDict 6 | from django.utils.decorators import method_decorator 7 | from django.views.decorators.csrf import csrf_exempt 8 | from django.views.generic import View 9 | 10 | logger = logging.getLogger("") 11 | 12 | 13 | class APIError(Exception): 14 | def __init__(self, msg, err=None): 15 | self.err = err 16 | self.msg = msg 17 | super().__init__(err, msg) 18 | 19 | 20 | class ContentType(object): 21 | json_request = "application/json" 22 | json_response = "application/json;charset=UTF-8" 23 | url_encoded_request = "application/x-www-form-urlencoded" 24 | binary_response = "application/octet-stream" 25 | 26 | 27 | class JSONParser(object): 28 | content_type = ContentType.json_request 29 | 30 | @staticmethod 31 | def parse(body): 32 | return json.loads(body.decode("utf-8")) 33 | 34 | 35 | class URLEncodedParser(object): 36 | content_type = ContentType.url_encoded_request 37 | 38 | @staticmethod 39 | def parse(body): 40 | return QueryDict(body) 41 | 42 | 43 | class JSONResponse(object): 44 | content_type = ContentType.json_response 45 | 46 | @classmethod 47 | def response(cls, data): 48 | resp = HttpResponse(json.dumps(data, indent=4), content_type=cls.content_type) 49 | resp.data = data 50 | return resp 51 | 52 | 53 | class APIView(View): 54 | """ 55 | Django view的父类, 和django-rest-framework的用法基本一致 56 | - request.data获取解析之后的json或者urlencoded数据, dict类型 57 | - self.success, self.error和self.invalid_serializer可以根据业需求修改, 58 | 写到父类中是为了不同的人开发写法统一,不再使用自己的success/error格式 59 | - self.response 返回一个django HttpResponse, 具体在self.response_class中实现 60 | - parse请求的类需要定义在request_parser中, 目前只支持json和urlencoded的类型, 用来解析请求的数据 61 | """ 62 | request_parsers = (JSONParser, URLEncodedParser) 63 | response_class = JSONResponse 64 | 65 | def _get_request_data(self, request): 66 | if request.method not in ["GET", "DELETE"]: 67 | body = request.body 68 | content_type = request.META.get("CONTENT_TYPE") 69 | if not content_type: 70 | raise ValueError("content_type is required") 71 | for parser in self.request_parsers: 72 | if content_type.startswith(parser.content_type): 73 | break 74 | # else means the for loop is not interrupted by break 75 | else: 76 | raise ValueError("unknown content_type '%s'" % content_type) 77 | if body: 78 | return parser.parse(body) 79 | return {} 80 | return request.GET 81 | 82 | def response(self, data): 83 | return self.response_class.response(data) 84 | 85 | def success(self, data=None): 86 | return self.response({"error": None, "data": data}) 87 | 88 | def error(self, msg="error", err="error"): 89 | return self.response({"error": err, "data": msg}) 90 | 91 | def extract_errors(self, errors, key="field"): 92 | if isinstance(errors, dict): 93 | if not errors: 94 | return key, "Invalid field" 95 | key = list(errors.keys())[0] 96 | return self.extract_errors(errors.pop(key), key) 97 | elif isinstance(errors, list): 98 | return self.extract_errors(errors[0], key) 99 | 100 | return key, errors 101 | 102 | def invalid_serializer(self, serializer): 103 | key, error = self.extract_errors(serializer.errors) 104 | if key == "non_field_errors": 105 | msg = error 106 | else: 107 | msg = f"{key}: {error}" 108 | return self.error(err=f"invalid-{key}", msg=msg) 109 | 110 | def server_error(self): 111 | return self.error(err="server-error", msg="server error") 112 | 113 | def paginate_data(self, request, query_set, object_serializer=None): 114 | """ 115 | :param request: django的request 116 | :param query_set: django model的query set或者其他list like objects 117 | :param object_serializer: 用来序列化query set, 如果为None, 则直接对query set切片 118 | :return: 119 | """ 120 | try: 121 | limit = int(request.GET.get("limit", "10")) 122 | except ValueError: 123 | limit = 10 124 | if limit < 0 or limit > 250: 125 | limit = 10 126 | try: 127 | offset = int(request.GET.get("offset", "0")) 128 | except ValueError: 129 | offset = 0 130 | if offset < 0: 131 | offset = 0 132 | results = query_set[offset:offset + limit] 133 | if object_serializer: 134 | count = query_set.count() 135 | results = object_serializer(results, many=True).data 136 | else: 137 | count = query_set.count() 138 | data = {"results": results, 139 | "total": count} 140 | return data 141 | 142 | def dispatch(self, request, *args, **kwargs): 143 | if self.request_parsers: 144 | try: 145 | request.data = self._get_request_data(self.request) 146 | except ValueError as e: 147 | return self.error(err="invalid-request", msg=str(e)) 148 | try: 149 | return super(APIView, self).dispatch(request, *args, **kwargs) 150 | except APIError as e: 151 | ret = {"msg": e.msg} 152 | if e.err: 153 | ret["err"] = e.err 154 | return self.error(**ret) 155 | except Exception as e: 156 | logger.exception(e) 157 | return self.server_error() 158 | 159 | 160 | class CSRFExemptAPIView(APIView): 161 | @method_decorator(csrf_exempt) 162 | def dispatch(self, request, *args, **kwargs): 163 | return super(CSRFExemptAPIView, self).dispatch(request, *args, **kwargs) 164 | 165 | 166 | def validate_serializer(serializer): 167 | """ 168 | @validate_serializer(TestSerializer) 169 | def post(self, request): 170 | return self.success(request.data) 171 | """ 172 | def validate(view_method): 173 | @functools.wraps(view_method) 174 | def handle(*args, **kwargs): 175 | self = args[0] 176 | request = args[1] 177 | s = serializer(data=request.data) 178 | if s.is_valid(): 179 | request.data = s.data 180 | request.serializer = s 181 | return view_method(*args, **kwargs) 182 | else: 183 | return self.invalid_serializer(s) 184 | 185 | return handle 186 | 187 | return validate 188 | -------------------------------------------------------------------------------- /contest/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.12 on 2017-02-06 09:19 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | import jsonfield.fields 9 | import utils.models 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | initial = True 15 | 16 | dependencies = [ 17 | ('problem', '0001_initial'), 18 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 19 | ] 20 | 21 | operations = [ 22 | migrations.CreateModel( 23 | name='ACMContestRank', 24 | fields=[ 25 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 26 | ('total_submission_number', models.IntegerField(default=0)), 27 | ('total_ac_number', models.IntegerField(default=0)), 28 | ('total_time', models.IntegerField(default=0)), 29 | ('submission_info', jsonfield.fields.JSONField(default={})), 30 | ], 31 | options={ 32 | 'db_table': 'acm_contest_rank', 33 | }, 34 | ), 35 | migrations.CreateModel( 36 | name='Contest', 37 | fields=[ 38 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 39 | ('title', models.CharField(max_length=40)), 40 | ('description', utils.models.RichTextField()), 41 | ('real_time_rank', models.BooleanField()), 42 | ('password', models.CharField(blank=True, max_length=30, null=True)), 43 | ('rule_type', models.CharField(max_length=36)), 44 | ('start_time', models.DateTimeField()), 45 | ('end_time', models.DateTimeField()), 46 | ('create_time', models.DateTimeField(auto_now_add=True)), 47 | ('last_update_time', models.DateTimeField(auto_now=True)), 48 | ('visible', models.BooleanField(default=True)), 49 | ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 50 | ], 51 | options={ 52 | 'db_table': 'contest', 53 | }, 54 | ), 55 | migrations.CreateModel( 56 | name='ContestAnnouncement', 57 | fields=[ 58 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 59 | ('title', models.CharField(max_length=128)), 60 | ('content', utils.models.RichTextField()), 61 | ('create_time', models.DateTimeField(auto_now_add=True)), 62 | ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contest.Contest')), 63 | ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 64 | ], 65 | options={ 66 | 'db_table': 'contest_announcement', 67 | }, 68 | ), 69 | migrations.CreateModel( 70 | name='ContestProblem', 71 | fields=[ 72 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 73 | ('title', models.CharField(max_length=128)), 74 | ('description', utils.models.RichTextField()), 75 | ('input_description', utils.models.RichTextField()), 76 | ('output_description', utils.models.RichTextField()), 77 | ('samples', jsonfield.fields.JSONField()), 78 | ('test_case_id', models.CharField(max_length=32)), 79 | ('test_case_score', jsonfield.fields.JSONField()), 80 | ('hint', utils.models.RichTextField(blank=True, null=True)), 81 | ('languages', jsonfield.fields.JSONField()), 82 | ('template', jsonfield.fields.JSONField()), 83 | ('create_time', models.DateTimeField(auto_now_add=True)), 84 | ('last_update_time', models.DateTimeField(blank=True, null=True)), 85 | ('time_limit', models.IntegerField()), 86 | ('memory_limit', models.IntegerField()), 87 | ('spj', models.BooleanField(default=False)), 88 | ('spj_language', models.CharField(blank=True, max_length=32, null=True)), 89 | ('spj_code', models.TextField(blank=True, null=True)), 90 | ('spj_version', models.CharField(blank=True, max_length=32, null=True)), 91 | ('rule_type', models.CharField(max_length=32)), 92 | ('visible', models.BooleanField(default=True)), 93 | ('difficulty', models.CharField(max_length=32)), 94 | ('source', models.CharField(blank=True, max_length=200, null=True)), 95 | ('total_submit_number', models.IntegerField(default=0)), 96 | ('total_accepted_number', models.IntegerField(default=0)), 97 | ('sort_index', models.CharField(max_length=30)), 98 | ('is_public', models.BooleanField(default=False)), 99 | ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contest.Contest')), 100 | ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 101 | ('tags', models.ManyToManyField(to='problem.ProblemTag')), 102 | ], 103 | options={ 104 | 'db_table': 'contest_problem', 105 | }, 106 | ), 107 | migrations.CreateModel( 108 | name='OIContestRank', 109 | fields=[ 110 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 111 | ('total_submission_number', models.IntegerField(default=0)), 112 | ('total_score', models.IntegerField(default=0)), 113 | ('submission_info', jsonfield.fields.JSONField(default={})), 114 | ('contest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contest.Contest')), 115 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 116 | ], 117 | options={ 118 | 'db_table': 'oi_contest_rank', 119 | }, 120 | ), 121 | migrations.AddField( 122 | model_name='acmcontestrank', 123 | name='contest', 124 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contest.Contest'), 125 | ), 126 | migrations.AddField( 127 | model_name='acmcontestrank', 128 | name='user', 129 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 130 | ), 131 | ] 132 | -------------------------------------------------------------------------------- /docs/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "update": [ 3 | { 4 | "version": "2024-02-04", 5 | "level": "Important", 6 | "title": "OnlineJudge 1.16.0 更新", 7 | "details": [ 8 | "更新基底映像档,从 `python:3.8-alpine3.14` 改为 `python:3.12-alpine`。", 9 | "更新评测的程序语言版本:", 10 | "C/C++: GCC 13", 11 | "Java: Temurin 21", 12 | "Python 2: 移除", 13 | "Python 3: CPython 3.12", 14 | "Golang: Golang 1.22", 15 | "Node.js: Node.js 20", 16 | "PHP: 暫時移除" 17 | ] 18 | }, 19 | { 20 | "version": "2021-11-20", 21 | "level": "Important", 22 | "title": "2021-11-20", 23 | "details": [ 24 | "升级 django-dramatiq 至 0.10.0,解决数据库连接无法释放的问题", 25 | "django2.x即将停止支持,更新django到3.x LTS,相应地更新了全部相关依赖", 26 | "合并底包版本到python3.8-alpine3.14,为移除python2做准备", 27 | "按上游包的生产环境规范,修改依赖psycopg2_binary为psycopg2", 28 | "TODO:本地开发环境搭建文档应做相应修改和提交。" 29 | ] 30 | }, 31 | { 32 | "version": "2021-09-28", 33 | "level": "Recommend", 34 | "title": "2021-09-28", 35 | "details": [ 36 | "修复 Golang 编译运行的一些问题", 37 | "支持 JavaScript 语言,注意,升级后需要按照 文档来 reset_languages,如果有自定义配置将会被重置,需要先备份。", 38 | "增加了一些语言的编辑器语法高亮配置" 39 | ] 40 | }, 41 | { 42 | "version": "2021-08-07", 43 | "level": "Recommend", 44 | "title": "2021-08-07", 45 | "details": [ 46 | "升级 gcc、g++ 编译器到 9.4 版本", 47 | "导入用户的时候支持导入真实姓名", 48 | "修复前端 katex 配置的一些问题", 49 | "修复编辑器无法显示表格的问题", 50 | "增加了一些语言的编辑器语法高亮配置" 51 | ] 52 | }, 53 | { 54 | "version": "2020-07-09", 55 | "level": "Recommend", 56 | "title": "2020-07-09", 57 | "details": [ 58 | "Add Golang language to judge server", 59 | "Fix some frontend bugs" 60 | ] 61 | }, 62 | { 63 | "version": "2019-09-22", 64 | "level": "Recommend", 65 | "title": "2019-09-22", 66 | "details": [ 67 | "Fix bugs in contest password verification", 68 | "Do not delete test case files when contest is deleted", 69 | "Update some compiler runtime config" 70 | ] 71 | }, 72 | { 73 | "version": "2019-04-05", 74 | "level": "Recommend", 75 | "title": "2019-04-05", 76 | "details": [ 77 | "Fix bugs in Free Problem Set parser" 78 | ] 79 | }, 80 | { 81 | "version": "2019-04-03", 82 | "level": "Important", 83 | "title": "2019-04-03", 84 | "details": [ 85 | "Fix bugs in judge server scheduler" 86 | ] 87 | }, 88 | { 89 | "version": "2019-03-25", 90 | "level": "Recommend", 91 | "title": "2019-03-25", 92 | "details": [ 93 | "Update Django to version 2.1 and Python to version 3.7", 94 | "Replace celery with dramatiq", 95 | "Add problem file IO Mode", 96 | "You can add attachments in all editor", 97 | "You can upload source code file in submission editor", 98 | "Frontend and UI improvements", 99 | "Fixed a lot of bugs" 100 | ] 101 | }, 102 | { 103 | "version": "2018-12-15", 104 | "level": "Recommend", 105 | "title": "Update at 2018-12-15", 106 | "details": [ 107 | "Fix bugs when root user is renamed", 108 | "Use more secure ssl ciphers", 109 | "Fix minor bugs in rejudge function and fps related api", 110 | "Frontend and UI improvements" 111 | ] 112 | }, 113 | { 114 | "version": "2018-09-07", 115 | "level": "Recommend", 116 | "title": "Update at 2018-09-07", 117 | "details": [ 118 | "Fix Problem title overflow, by @queensferryme", 119 | "Fix the pagination in problem export page", 120 | "Add a gadget to reset code to problem template", 121 | "Support for contest submissions download (accepted answer only)", 122 | "Progress is preserved when uploading files", 123 | "UI improvements" 124 | ] 125 | }, 126 | { 127 | "version": "2018-08-15", 128 | "level": "Important", 129 | "title": "Update at 2018-08-15", 130 | "details": [ 131 | "Fix some vulnerabilities, by @andyliu24 @plusls", 132 | "Add export and import problem function (beta), if you have any question, please give us feedback" 133 | ] 134 | }, 135 | { 136 | "version": "2018-07-15", 137 | "level": "Recommend", 138 | "title": "Update at 2018-07-15", 139 | "details": [ 140 | "Add Traditional Chinese, by xdavidwu", 141 | "Fix a bug in the navigation bar", 142 | "Move language switch to profile settings page" 143 | ] 144 | }, 145 | { 146 | "version": "2018-06-02", 147 | "level": "Recommend", 148 | "title": "Update at 2018-06-02", 149 | "details": [ 150 | "Add Simple Chinese, by hirCodd", 151 | "Integrate with katex", 152 | "Update libraries to latest version", 153 | "The first page in management is changed to dashboard" 154 | ] 155 | }, 156 | { 157 | "version": "2018-05-25", 158 | "level": "Important", 159 | "title": "Update at 2018-05-05", 160 | "details": [ 161 | "Fix a bug that user is logged out when re-creating container", 162 | "Fix a bug that JudgeServer may use a lot of memory and then getting stuck(Please update docker-compose.yml at first)" 163 | ] 164 | }, 165 | { 166 | "version": "2018-03-27", 167 | "level": "Important", 168 | "title": "Update at 2018-03-27", 169 | "details": [ 170 | "Fix some bugs about problem submission statistics", 171 | "Fix some bugs that may cause 'compile error' when submitted code contains unicode characters" 172 | ] 173 | }, 174 | { 175 | "version": "2018-03-23", 176 | "level": "Recommend", 177 | "title": "Update at 2018-03-23", 178 | "details": [ 179 | "Fix a bug about Java language memory usage", 180 | "Update Java to version 1.8", 181 | "[important] Google analytics is added with developer's account, it is only used for statistics", 182 | "Problem export and import function will be released in next week", 183 | "Other bugs and enhancements" 184 | ] 185 | }, 186 | { 187 | "version": "2018-01-04", 188 | "level": "Recommend", 189 | "title": "Update at 2018-01-04", 190 | "details": [ 191 | "Fix some issues under IE/Edge", 192 | "Add backend error reporter", 193 | "New email template", 194 | "A more flexible throttling function", 195 | "Add admin dashboard (this page)", 196 | "Other bugs and enhancements" 197 | ] 198 | } 199 | ] 200 | } 201 | --------------------------------------------------------------------------------