├── 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 | |
8 | {{ website_name }} |
9 |
10 |
11 |
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 | |
27 |
28 |
29 |
30 |
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 | [](https://www.python.org/downloads/release/python-362/)
4 | [](https://www.djangoproject.com/)
5 | [](http://www.django-rest-framework.org/)
6 | [](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 | 
44 |
45 | 
46 |
47 | 
48 |
49 | 
50 |
51 | Rankings 中可以控制图表和菜单的显隐。
52 | 
53 | 
54 |
55 | 
56 |
57 | 
58 |
59 | 
60 |
61 | ### 后台管理
62 |
63 | 
64 |
65 | 
66 |
67 | 
68 |
69 | 
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 | [](https://www.python.org/downloads/release/python-362/)
4 | [](https://www.djangoproject.com/)
5 | [](http://www.django-rest-framework.org/)
6 | [](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 | 
44 |
45 | 
46 |
47 | 
48 |
49 | 
50 |
51 | You can control the menu and chart status in rankings.
52 |
53 | 
54 |
55 | 
56 |
57 | 
58 |
59 | 
60 |
61 | 
62 |
63 | ### Admin:
64 |
65 | 
66 |
67 | 
68 |
69 | 
70 |
71 | 
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 |
6 |
7 |
8 |
9 |
]]>
10 |
11 | Two integer a,b (0<=a,b<=10)
12 |
]]>
13 |
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 |
39 |
40 | int main()
41 | {
42 | int a,b;
43 | scanf("%d %d",&a,&b);
44 | printf("%d",a+b);
45 | return 0;
46 | }
47 | ]]>
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 |
64 | using namespace std;
65 | int main(){
66 | int a,b;
67 | while(cin >> a >> b)
68 | cout << a+b << endl;
69 | return 0;
70 | }
71 | ]]>
72 |
79 |
95 |
103 |
117 |
124 |
125 |
126 | int main()
127 | {
128 | int a,b;
129 | scanf("%d %d",&a,&b);
130 | printf("%d",a+b);
131 | return 0;
132 | }
133 | ]]>
134 |
135 | using namespace std;
136 | int main(){
137 | int a,b;
138 | while(cin >> a >> b)
139 | cout << a+b << endl;
140 | return 0;
141 | }
142 | ]]>
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 |
--------------------------------------------------------------------------------