This commit is contained in:
Harry-zklcdc 2021-08-06 23:16:30 +08:00 committed by GitHub
commit df0269d31f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 1741 additions and 116 deletions

1
.gitignore vendored
View File

@ -72,6 +72,7 @@ data/public/avatar/*
!data/public/avatar/default.png
data/config/*
!data/config/.gitkeep
plugin/live2d/model/*
http_locations.conf
https_locations.conf

View File

@ -11,7 +11,7 @@ RUN apk add --update --no-cache build-base nginx openssl curl unzip supervisor j
pip install --no-cache-dir -r /app/deploy/requirements.txt && \
apk del build-base --purge
RUN curl -L $(curl -s https://api.github.com/repos/QingdaoU/OnlineJudgeFE/releases/latest | grep /dist.zip | cut -d '"' -f 4) -o dist.zip && \
RUN curl -L $(curl -s https://api.github.com/repos/Harry-zklcdc/OJ/releases/latest | grep /dist.zip | cut -d '"' -f 4) -o dist.zip && \
unzip dist.zip && \
rm dist.zip

View File

@ -3,34 +3,43 @@
[![Python](https://img.shields.io/badge/python-3.6.2-blue.svg?style=flat-square)](https://www.python.org/downloads/release/python-362/)
[![Django](https://img.shields.io/badge/django-1.11.4-blue.svg?style=flat-square)](https://www.djangoproject.com/)
[![Django Rest Framework](https://img.shields.io/badge/django_rest_framework-3.4.0-blue.svg?style=flat-square)](http://www.django-rest-framework.org/)
[![Build Status](https://travis-ci.org/QingdaoU/OnlineJudge.svg?branch=master)](https://travis-ci.org/QingdaoU/OnlineJudge)
[![Build Status](https://travis-ci.org/Harry-zklcdc/OnlineJudge.svg?branch=master)](https://travis-ci.org/Harry-zklcdc/OnlineJudge)
> #### 基于 Python 和 Vue 的在线评测系统。 [Demo](https://qduoj.com)
> #### 基于 Python 和 Vue 的在线评测系统。 [Demo](https://oj.yangzheng.com.cn/)
[English Document](README.md)
## 概览
+ 基于 Docker真正一键部署
+ 前后端分离,模块化编程,微服务
+ ACM/OI 两种比赛模式、实时/非实时评判 任意选择
+ 丰富的可视化图表,一图胜千言
+ 支持 Template Problem可以添加函数题甚至填空题
+ 更细致的权限划分,超级管理员和普通管理员各司其职
+ 多语言支持:`C`, `C++`, `Java`, `Python2`, `Python3`,题目可以选择使用的语言
+ Markdown & MathJax 支持
+ 比赛用户IP限制 (CIDR ranges)
- 基于 Docker真正一键部署
- 前后端分离,模块化编程,微服务
- ACM/OI 两种比赛模式、实时/非实时评判 任意选择
- 丰富的可视化图表,一图胜千言
- 支持 Template Problem可以添加函数题甚至填空题
- 更细致的权限划分,超级管理员和普通管理员各司其职
- 多语言支持:`C`, `C++`, `Java`, `Python2`, `Python3`,题目可以选择使用的语言
- Markdown & MathJax 支持
- 比赛用户IP限制 (CIDR ranges)
- 在线 IDE
- 自定义关于我们介绍页面
- 换肤功能
- 签到系统
- 经验系统
- 称号系统
- 标准输入输出 / 文件输入输出
主要模块均已开源:
+ 后端(Django): [https://github.com/QingdaoU/OnlineJudge](https://github.com/QingdaoU/OnlineJudge)
+ 前端(Vue): [https://github.com/QingdaoU/OnlineJudgeFE](https://github.com/QingdaoU/OnlineJudgeFE)
+ 判题沙箱(Seccomp): [https://github.com/QingdaoU/Judger](https://github.com/QingdaoU/Judger)
+ 判题服务器(对Judger的封装): [https://github.com/QingdaoU/JudgeServer](https://github.com/QingdaoU/JudgeServer)
- 后端(Django): [https://github.com/Harry-zklcdc/OnlineJudge](https://github.com/Harry-zklcdc/OnlineJudge)
- 前端(Vue): [https://github.com/Harry-zklcdc/OJ](https://github.com/Harry-zklcdc/OJ)
- 判题沙箱(Seccomp): [https://github.com/Harry-zklcdc/Judger](https://github.com/Harry-zklcdc/Judger)
- 判题服务器(对Judger的封装): [https://github.com/Harry-zklcdc/JudgeServer](https://github.com/Harry-zklcdc/JudgeServe)
## 安装
请根据此进行安装: [https://github.com/QingdaoU/OnlineJudgeDeploy/tree/2.0](https://github.com/QingdaoU/OnlineJudgeDeploy/tree/2.0)
请根据此进行安装: [https://github.com/Harry-zklcdc/OnlineJudgeDeploy/tree/2.0](https://github.com/Harry-zklcdc/OnlineJudgeDeploy/tree/2.0)
## 文档
@ -40,23 +49,33 @@
### OJ前台
![problem-list](https://user-images.githubusercontent.com/20637881/33372506-402022e4-d539-11e7-8e64-6656f8ceb75a.png)
![home page](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E9%A6%96%E9%A1%B5.jpg)
![announcement](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E5%85%AC%E5%91%8A.jpg)
![problem-list](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E9%A2%98%E7%9B%AE-%E5%8E%9F%E8%B0%85%E7%BB%BF.jpg)
![problem-details](https://user-images.githubusercontent.com/20637881/33372507-4061a782-d539-11e7-8835-076ddae6b529.png)
![statistic-info](https://user-images.githubusercontent.com/20637881/33372508-40a0c6ce-d539-11e7-8d5e-024541b76750.png)
![contest-list](https://user-images.githubusercontent.com/20637881/33372509-40d880dc-d539-11e7-9eba-1f08dcb6b9a0.png)
![online-IDE](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E5%9C%A8%E7%BA%BFIDE.jpg)
![FAQ](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98-%E5%B0%91%E5%A5%B3%E7%B2%89.jpg)
![contest-list](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E6%AF%94%E8%B5%9B-%E5%9F%BA%E4%BD%AC%E7%B4%AB.jpg)
Rankings 中可以控制图表和菜单的显隐。
![acm-rankings](https://user-images.githubusercontent.com/20637881/33372510-41117f68-d539-11e7-9947-70e60bad3cf2.png)
![oi-rankings](https://user-images.githubusercontent.com/20637881/33372511-41d406fa-d539-11e7-9947-7a2a088785b0.png)
![status](https://user-images.githubusercontent.com/20637881/33372512-420ba240-d539-11e7-8645-594cac4a0b78.png)
![acm-rankings](https://user-images.githubusercontent.com/20637881/33372510-41117f68-d539-11e7-9947-70e60bad3cf2.png)
![oi-rankings](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E6%8E%92%E5%90%8D.jpg)
![status](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/状态.jpg)
![status-details](https://user-images.githubusercontent.com/20637881/33365523-787bd0ea-d523-11e7-953f-dacbf7a506df.png)
![user-home](https://user-images.githubusercontent.com/20637881/33365521-7842d808-d523-11e7-84c1-2e2aa0079f32.png)
![user-home](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/个人中心.jpg)
### 后台管理
@ -68,18 +87,18 @@ Rankings 中可以控制图表和菜单的显隐。
![create-contest](https://user-images.githubusercontent.com/20637881/33372514-428ab922-d539-11e7-8f68-da55dedf3ad3.png)
## 浏览器支持
Modern browsers(chrome, firefox) 和 Internet Explorer 10+.
## 特别感谢
+ 所有为本项目做出贡献的人
+ [heb1c](https://github.com/hebicheng) 同学为我们提供了很多意见和建议
- 所有为本项目做出贡献的人
- [heb1c](https://github.com/hebicheng) 同学为我们提供了很多意见和建议
如果您觉得这个项目还不错就star一下吧 )
## 许可
The [MIT](http://opensource.org/licenses/MIT) License

View File

@ -3,34 +3,43 @@
[![Python](https://img.shields.io/badge/python-3.6.2-blue.svg?style=flat-square)](https://www.python.org/downloads/release/python-362/)
[![Django](https://img.shields.io/badge/django-1.11.4-blue.svg?style=flat-square)](https://www.djangoproject.com/)
[![Django Rest Framework](https://img.shields.io/badge/django_rest_framework-3.4.0-blue.svg?style=flat-square)](http://www.django-rest-framework.org/)
[![Build Status](https://travis-ci.org/QingdaoU/OnlineJudge.svg?branch=master)](https://travis-ci.org/QingdaoU/OnlineJudge)
[![Build Status](https://travis-ci.org/Harry-zklcdc/OnlineJudge.svg?branch=master)](https://travis-ci.org/Harry-zklcdc/OnlineJudge)
> #### An onlinejudge system based on Python and Vue. [Demo](https://qduoj.com)
>
>
> #### An onlinejudge system based on Python and Vue. [Demo](https://oj.yangzheng.com.cn/)
[中文文档](README-CN.md)
## Overview
+ Based on Docker; One-click deployment
+ Separated backend and frontend; Modular programming; Micro service
+ ACM/OI rule support; realtime/non-realtime rank support
+ Amazing charting and visualization
+ Template-problem support
+ More reasonable permission control
+ Multi-language support: `C`, `C++`, `Java`, `Python2`, `Python3`
+ Markdown & MathJax support
+ Contest participants IP limit(CIDR)
- Based on Docker; One-click deployment
- Separated backend and frontend; Modular programming; Micro service
- ACM/OI rule support; realtime/non-realtime rank support
- Amazing charting and visualization
- Template-problem support
- More reasonable permission control
- Multi-language support: `C`, `C++`, `Java`, `Python2`, `Python3`
- Markdown & MathJax support
- Contest participants IP limit(CIDR)
- Online IDE
- DIY About Us Page
- Optional Theme
- Sighin System
- Experience system
- Title System
- Standard IO/File IO
Main modules are available below:
+ Backend(Django): [https://github.com/QingdaoU/OnlineJudge](https://github.com/QingdaoU/OnlineJudge)
+ Frontend(Vue): [https://github.com/QingdaoU/OnlineJudgeFE](https://github.com/QingdaoU/OnlineJudgeFE)
+ Judger Sandbox(Seccomp): [https://github.com/QingdaoU/Judger](https://github.com/QingdaoU/Judger)
+ JudgeServer(A wrapper for Judger): [https://github.com/QingdaoU/JudgeServer](https://github.com/QingdaoU/JudgeServer)
- Backend(Django): [https://github.com/Harry-zklcdc/OnlineJudge](https://github.com/Harry-zklcdc/OnlineJudge)
- Frontend(Vue): [https://github.com/Harry-zklcdc/OJ](https://github.com/Harry-zklcdc/OJ)
- Judger Sandbox(Seccomp): [https://github.com/Harry-zklcdc/Judger](https://github.com/Harry-zklcdc/Judger)
- JudgeServer(A wrapper for Judger): [https://github.com/Harry-zklcdc/JudgeServer](https://github.com/Harry-zklcdc/JudgeServer)
## Installation
Follow me: [https://github.com/QingdaoU/OnlineJudgeDeploy/tree/2.0](https://github.com/QingdaoU/OnlineJudgeDeploy/tree/2.0)
Follow me: [https://github.com/Harry-zklcdc/OnlineJudgeDeploy/tree/2.0](https://github.com/Harry-zklcdc/OnlineJudgeDeploy/tree/2.0)
## Documents
@ -40,25 +49,33 @@ Follow me: [https://github.com/QingdaoU/OnlineJudgeDeploy/tree/2.0](https://git
### Frontend:
![problem-list](https://user-images.githubusercontent.com/20637881/33372506-402022e4-d539-11e7-8e64-6656f8ceb75a.png)
![home page](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E9%A6%96%E9%A1%B5.jpg)
![announcement](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E5%85%AC%E5%91%8A.jpg)
![problem-list](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E9%A2%98%E7%9B%AE-%E5%8E%9F%E8%B0%85%E7%BB%BF.jpg)
![problem-details](https://user-images.githubusercontent.com/20637881/33372507-4061a782-d539-11e7-8835-076ddae6b529.png)
![statistic-info](https://user-images.githubusercontent.com/20637881/33372508-40a0c6ce-d539-11e7-8d5e-024541b76750.png)
![contest-list](https://user-images.githubusercontent.com/20637881/33372509-40d880dc-d539-11e7-9eba-1f08dcb6b9a0.png)
![online-IDE](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E5%9C%A8%E7%BA%BFIDE.jpg)
![FAQ](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98-%E5%B0%91%E5%A5%B3%E7%B2%89.jpg)
![contest-list](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E6%AF%94%E8%B5%9B-%E5%9F%BA%E4%BD%AC%E7%B4%AB.jpg)
You can control the menu and chart status in rankings.
![acm-rankings](https://user-images.githubusercontent.com/20637881/33372510-41117f68-d539-11e7-9947-70e60bad3cf2.png)
![oi-rankings](https://user-images.githubusercontent.com/20637881/33372511-41d406fa-d539-11e7-9947-7a2a088785b0.png)
![oi-rankings](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E6%8E%92%E5%90%8D.jpg)
![status](https://user-images.githubusercontent.com/20637881/33372512-420ba240-d539-11e7-8645-594cac4a0b78.png)
![status](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/状态.jpg)
![status-details](https://user-images.githubusercontent.com/20637881/33365523-787bd0ea-d523-11e7-953f-dacbf7a506df.png)
![user-home](https://user-images.githubusercontent.com/20637881/33365521-7842d808-d523-11e7-84c1-2e2aa0079f32.png)
![user-home](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/个人中心.jpg)
### Admin:
@ -76,10 +93,11 @@ Modern browsers(chrome, firefox) and Internet Explorer 10+.
## Thanks
+ I'd appreciate a star if you find this helpful.
+ Thanks to everyone that contributes to this project.
+ Special thanks to [heb1c](https://github.com/hebicheng), who has given us a lot of suggestions.
- I'd appreciate a star if you find this helpful.
- Thanks to everyone that contributes to this project.
- Special thanks to [heb1c](https://github.com/hebicheng), who has given us a lot of suggestions.
## License
[MIT](http://opensource.org/licenses/MIT)
[MIT](http://opensource.org/licenses/MIT)**

View File

@ -0,0 +1,23 @@
# Generated by Django 2.1.7 on 2020-04-19 14:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0012_userprofile_language'),
]
operations = [
migrations.AddField(
model_name='user',
name='continue_sighin_days',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='user',
name='last_sighin_time',
field=models.DateField(null=True),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 2.1.7 on 2020-04-20 02:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0013_auto_20200419_1414'),
]
operations = [
migrations.AddField(
model_name='userprofile',
name='experience',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='userprofile',
name='grade',
field=models.IntegerField(default=0),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.1.7 on 2020-04-21 09:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0014_auto_20200420_1020'),
]
operations = [
migrations.AlterField(
model_name='user',
name='last_sighin_time',
field=models.DateField(default='1970-01-01'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 2.1.7 on 2020-08-02 02:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0015_auto_20200421_1715'),
]
operations = [
migrations.AddField(
model_name='user',
name='title',
field=models.TextField(null=True),
),
migrations.AddField(
model_name='user',
name='title_color',
field=models.TextField(null=True),
),
]

View File

@ -41,6 +41,12 @@ class User(AbstractBaseUser):
open_api = models.BooleanField(default=False)
open_api_appkey = models.TextField(null=True)
is_disabled = models.BooleanField(default=False)
# Sigh in
last_sighin_time = models.DateField(default="1970-01-01")
continue_sighin_days = models.IntegerField(default=0)
# Title Call On
title = models.TextField(null=True)
title_color = models.TextField(null=True)
USERNAME_FIELD = "username"
REQUIRED_FIELDS = []
@ -62,6 +68,12 @@ class User(AbstractBaseUser):
def is_contest_admin(self, contest):
return self.is_authenticated and (contest.created_by == self or self.admin_type == AdminType.SUPER_ADMIN)
def sighin_time(self):
return self.last_sighin_time
def continue_days(self):
return self.continue_sighin_days
class Meta:
db_table = "user"
@ -95,6 +107,8 @@ class UserProfile(models.Model):
school = models.TextField(null=True)
major = models.TextField(null=True)
language = models.TextField(null=True)
grade = models.IntegerField(default=0)
experience = models.IntegerField(default=0)
# for ACM
accepted_number = models.IntegerField(default=0)
# for OI
@ -115,5 +129,16 @@ class UserProfile(models.Model):
self.total_score = models.F("total_score") - last_time_score + this_time_score
self.save()
def add_experience(self, this_time_experience):
self.experience = models.F("experience") + this_time_experience
self.save()
grade = [10000, 5000, 1000, 500, 200, 100, 0]
self.refresh_from_db()
for i in range(0, 7):
if int(self.experience) >= grade[i]:
self.grade = 6 - i
break
self.save()
class Meta:
db_table = "user_profile"

View File

@ -54,7 +54,8 @@ class UserAdminSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "email", "admin_type", "problem_permission", "real_name",
"create_time", "last_login", "two_factor_auth", "open_api", "is_disabled"]
"create_time", "last_login", "two_factor_auth", "open_api", "is_disabled",
"title", "title_color"]
def get_real_name(self, obj):
return obj.userprofile.real_name
@ -64,7 +65,14 @@ class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "email", "admin_type", "problem_permission",
"create_time", "last_login", "two_factor_auth", "open_api", "is_disabled"]
"create_time", "last_login", "two_factor_auth", "open_api", "is_disabled",
"title", "title_color"]
class UserSighinSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["continue_sighin_days", "last_sighin_time"]
class UserProfileSerializer(serializers.ModelSerializer):
@ -95,6 +103,8 @@ class EditUserSerializer(serializers.Serializer):
open_api = serializers.BooleanField()
two_factor_auth = serializers.BooleanField()
is_disabled = serializers.BooleanField()
title = serializers.CharField(max_length=6, allow_blank=True, allow_null=True, required=False)
title_color = serializers.CharField(max_length=24, allow_blank=True, allow_null=True, required=False)
class EditUserProfileSerializer(serializers.Serializer):

View File

@ -574,8 +574,8 @@ class AdminUserTest(APITestCase):
self.assertEqual(User.objects.get(id=self.regular_user.id).open_api_appkey, key)
def test_import_users(self):
data = {"users": [["user1", "pass1", "eami1@e.com"],
["user2", "pass3", "eamil3@e.com"]]
data = {"users": [["user1", "pass1", "eami1@e.com", "user1"],
["user2", "pass3", "eamil3@e.com", "user2"]]
}
resp = self.client.post(self.url, data)
self.assertSuccess(resp)
@ -583,8 +583,8 @@ class AdminUserTest(APITestCase):
self.assertEqual(User.objects.all().count(), 4)
def test_import_duplicate_user(self):
data = {"users": [["user1", "pass1", "eami1@e.com"],
["user1", "pass1", "eami1@e.com"]]
data = {"users": [["user1", "pass1", "eami1@e.com", "user1"],
["user1", "pass1", "eami1@e.com", "user1"]]
}
resp = self.client.post(self.url, data)
self.assertFailed(resp, "DETAIL: Key (username)=(user1) already exists.")

View File

@ -5,7 +5,7 @@ from ..views.oj import (ApplyResetPasswordAPI, ResetPasswordAPI,
UserLoginAPI, UserLogoutAPI, UsernameOrEmailCheck,
AvatarUploadAPI, TwoFactorAuthAPI, UserProfileAPI,
UserRankAPI, CheckTFARequiredAPI, SessionManagementAPI,
ProfileProblemDisplayIDRefreshAPI, OpenAPIAppkeyAPI, SSOAPI)
ProfileProblemDisplayIDRefreshAPI, OpenAPIAppkeyAPI, SSOAPI, UserSighinAPI)
from utils.captcha.views import CaptchaAPIView
@ -27,5 +27,6 @@ urlpatterns = [
url(r"^user_rank/?$", UserRankAPI.as_view(), name="user_rank_api"),
url(r"^sessions/?$", SessionManagementAPI.as_view(), name="session_management_api"),
url(r"^open_api_appkey/?$", OpenAPIAppkeyAPI.as_view(), name="open_api_appkey_api"),
url(r"^sso?$", SSOAPI.as_view(), name="sso_api")
url(r"^sso?$", SSOAPI.as_view(), name="sso_api"),
url(r"^sighin?$", UserSighinAPI.as_view(), name="user_sighin_api")
]

View File

@ -28,14 +28,14 @@ class UserAdminAPI(APIView):
user_list = []
for user_data in data:
if len(user_data) != 3 or len(user_data[0]) > 32:
if len(user_data) != 4 or len(user_data[0]) > 32:
return self.error(f"Error occurred while processing data '{user_data}'")
user_list.append(User(username=user_data[0], password=make_password(user_data[1]), email=user_data[2]))
try:
with transaction.atomic():
ret = User.objects.bulk_create(user_list)
UserProfile.objects.bulk_create([UserProfile(user=user) for user in ret])
UserProfile.objects.bulk_create([UserProfile(user=ret[i], real_name=data[i][3]) for i in range(len(ret))])
return self.success()
except IntegrityError as e:
# Extract detail from exception message
@ -92,6 +92,16 @@ class UserAdminAPI(APIView):
user.two_factor_auth = data["two_factor_auth"]
if data["title"]:
user.title = data["title"]
if data["title_color"]:
user.title_color = data["title_color"]
else:
user.title_color = "#222f3e"
else:
user.title = None
user.title_color = None
user.save()
if pre_username != user.username:
Submission.objects.filter(username=pre_username).update(username=user.username)
@ -119,6 +129,11 @@ class UserAdminAPI(APIView):
user = user.filter(Q(username__icontains=keyword) |
Q(userprofile__real_name__icontains=keyword) |
Q(email__icontains=keyword))
only_admin = request.GET.get("onlyadmin", None)
if only_admin:
user = user.filter(Q(admin_type__icontains="Admin"))
return self.success(self.paginate_data(request, user, UserAdminSerializer))
@super_admin_required

View File

@ -1,4 +1,6 @@
import os
import time
import datetime
from datetime import timedelta
from importlib import import_module
@ -22,7 +24,8 @@ from ..models import User, UserProfile, AdminType
from ..serializers import (ApplyResetPasswordSerializer, ResetPasswordSerializer,
UserChangePasswordSerializer, UserLoginSerializer,
UserRegisterSerializer, UsernameOrEmailCheckSerializer,
RankInfoSerializer, UserChangeEmailSerializer, SSOSerializer)
RankInfoSerializer, UserChangeEmailSerializer, SSOSerializer,
UserSighinSerializer)
from ..serializers import (TwoFactorAuthCodeSerializer, UserProfileSerializer,
EditUserProfileSerializer, ImageUploadForm)
from ..tasks import send_email_async
@ -382,9 +385,19 @@ class UserRankAPI(APIView):
.select_related("user")
if rule_type == ContestRuleType.ACM:
profiles = profiles.filter(submission_number__gt=0).order_by("-accepted_number", "submission_number")
else:
elif rule_type == ContestRuleType.OI:
profiles = profiles.filter(total_score__gt=0).order_by("-total_score")
return self.success(self.paginate_data(request, profiles, RankInfoSerializer))
else:
profiles = profiles.filter(experience__gt=0).order_by("-experience")
data = self.paginate_data(request, profiles, RankInfoSerializer)
for i in range(0, len(data["results"])):
try:
user = User.objects.get(id=data["results"][i]["user"]["id"], is_disabled=False)
userprofile = UserProfileSerializer(user.userprofile, show_real_name=False).data
data["results"][i].update({"grade": userprofile["grade"], "title": userprofile["user"]["title"], "title_color": userprofile["user"]["title_color"]})
except Exception:
data["results"][i].update({"grade": 0, "title": None, "title_color": None})
return self.success(data)
class ProfileProblemDisplayIDRefreshAPI(APIView):
@ -434,3 +447,59 @@ class SSOAPI(CSRFExemptAPIView):
except User.DoesNotExist:
return self.error("User does not exist")
return self.success({"username": user.username, "avatar": user.userprofile.avatar, "admin_type": user.admin_type})
class UserSighinAPI(APIView):
def get(self, request):
if not request.user.is_authenticated:
return self.success()
username = request.user.username
user = User.objects.get(username=username, is_disabled=False)
data = UserSighinSerializer(user).data
day = datetime.datetime.now()
if not data["last_sighin_time"]:
return self.success()
last_sighin_time = datetime.datetime.strptime(str(data["last_sighin_time"]), "%Y-%m-%d")
interval = int((day - last_sighin_time).days)
if interval == 0:
data.update({"sighinstatus": "true"})
elif interval == 1:
data.update({"sighinstatus": "false"})
else:
data.update({"sighinstatus": "false"})
data.update({"continue_sighin_days": 0})
return self.success(data)
@login_required
def post(self, request):
username = request.user.username
user = User.objects.get(username=username, is_disabled=False)
data = UserSighinSerializer(user).data
day = datetime.datetime.strptime(str(time.strftime("%Y-%m-%d", time.localtime())), "%Y-%m-%d")
if not data["last_sighin_time"]:
last_sighin_time = datetime.datetime.strptime("1970-01-01", "%Y-%m-%d")
else:
last_sighin_time = datetime.datetime.strptime(str(data["last_sighin_time"]), "%Y-%m-%d")
interval = int((day - last_sighin_time).days)
if interval == 0:
return self.success("Singined")
elif interval == 1:
user.continue_sighin_days += 1
user.last_sighin_time = day
days = [365, 240, 120, 90, 60, 30, 15, 7, 3]
experience = [88, 54, 33, 20, 12, 7, 4, 2, 1]
score = 1
for i in range(0, 9):
if user.continue_sighin_days == days[i]:
UserProfile.objects.get(user_id=user.id).add_experience(this_time_experience=experience[i])
score += experience[i]
break
UserProfile.objects.get(user_id=user.id).add_experience(this_time_experience=1)
user.save()
return self.success({"info": "Success", "experience": score})
else:
user.continue_sighin_days = 1
user.last_sighin_time = day
UserProfile.objects.get(user_id=user.id).add_experience(this_time_experience=1)
user.save()
return self.success({"info": "Success", "experience": 1})

View File

@ -34,3 +34,4 @@ class Migration(migrations.Migration):
},
),
]

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2019-04-30 21:18
from __future__ import unicode_literals
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import utils.models
class Migration(migrations.Migration):
dependencies = [
('announcement', '0003_auto_20180501_0436'),
]
operations = [
migrations.CreateModel(
name='AboutUs',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', utils.models.RichTextField()),
('last_update_time', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'aboutus',
},
),
]

View File

@ -16,3 +16,12 @@ class Announcement(models.Model):
class Meta:
db_table = "announcement"
ordering = ("-create_time",)
class AboutUs(models.Model):
# HTML
content = RichTextField()
last_update_time = models.DateTimeField(auto_now=True)
class Meta:
db_table = "aboutus"

View File

@ -1,7 +1,7 @@
from utils.api import serializers
from utils.api._serializers import UsernameSerializer
from .models import Announcement
from .models import Announcement, AboutUs
class CreateAnnouncementSerializer(serializers.Serializer):
@ -23,3 +23,19 @@ class EditAnnouncementSerializer(serializers.Serializer):
title = serializers.CharField(max_length=64)
content = serializers.CharField(max_length=1024 * 1024 * 8)
visible = serializers.BooleanField()
class CreateAboutUsSerializer(serializers.Serializer):
content = serializers.CharField(max_length=1024 * 1024 * 8)
class AboutUsSerializer(serializers.ModelSerializer):
class Meta:
model = AboutUs
fields = "__all__"
class EditAboutUsSerializer(serializers.Serializer):
id = 0
content = serializers.CharField(max_length=1024 * 1024 * 8)

View File

@ -1,7 +1,8 @@
from django.conf.urls import url
from ..views.admin import AnnouncementAdminAPI
from ..views.admin import AnnouncementAdminAPI, AboutUsAdminAPI
urlpatterns = [
url(r"^announcement/?$", AnnouncementAdminAPI.as_view(), name="announcement_admin_api"),
url(r"^aboutus/?$", AboutUsAdminAPI.as_view(), name="aboutus_admin_api"),
]

View File

@ -1,7 +1,8 @@
from django.conf.urls import url
from ..views.oj import AnnouncementAPI
from ..views.oj import AnnouncementAPI, AboutUsAPI
urlpatterns = [
url(r"^announcement/?$", AnnouncementAPI.as_view(), name="announcement_api"),
url(r"^aboutus/?$", AboutUsAPI.as_view(), name="aboutus_api"),
]

View File

@ -1,9 +1,9 @@
from account.decorators import super_admin_required
from utils.api import APIView, validate_serializer
from announcement.models import Announcement
from announcement.serializers import (AnnouncementSerializer, CreateAnnouncementSerializer,
EditAnnouncementSerializer)
from announcement.models import Announcement, AboutUs
from announcement.serializers import (AnnouncementSerializer, CreateAnnouncementSerializer, EditAnnouncementSerializer,
AboutUsSerializer, CreateAboutUsSerializer, EditAboutUsSerializer)
class AnnouncementAdminAPI(APIView):
@ -60,3 +60,45 @@ class AnnouncementAdminAPI(APIView):
if request.GET.get("id"):
Announcement.objects.filter(id=request.GET["id"]).delete()
return self.success()
class AboutUsAdminAPI(APIView):
@validate_serializer(CreateAboutUsSerializer)
@super_admin_required
def post(self, request):
"""
publish aboutus
"""
data = request.data
aboutus = AboutUs.objects.create(content=data["content"])
return self.success(AboutUsSerializer(aboutus).data)
@validate_serializer(EditAboutUsSerializer)
@super_admin_required
def put(self, request):
"""
edit aboutus
"""
data = request.data
try:
aboutus = AboutUs.objects.get(id=1)
except AboutUs.DoesNotExist:
return self.error("About Us does not exist")
for k, v in data.items():
setattr(aboutus, k, v)
aboutus.save()
return self.success(AboutUsSerializer(aboutus).data)
@super_admin_required
def get(self, request):
"""
get about_us list / get about_us
"""
aboutus_id = 1
try:
aboutus = AboutUs.objects.get(id=aboutus_id)
return self.success(AboutUsSerializer(aboutus).data)
except AboutUs.DoesNotExist:
return self.success(None)

View File

@ -1,10 +1,19 @@
from utils.api import APIView
from announcement.models import Announcement
from announcement.serializers import AnnouncementSerializer
from announcement.models import Announcement, AboutUs
from announcement.serializers import AnnouncementSerializer, AboutUsSerializer
class AnnouncementAPI(APIView):
def get(self, request):
announcements = Announcement.objects.filter(visible=True)
return self.success(self.paginate_data(request, announcements, AnnouncementSerializer))
class AboutUsAPI(APIView):
def get(self, request):
try:
aboutus = AboutUs.objects.get(id=1)
return self.success(AboutUsSerializer(aboutus).data)
except AboutUs.DoesNotExist:
return self.error("About Us does not exist")

View File

@ -26,6 +26,9 @@ class CreateEditWebsiteConfigSerializer(serializers.Serializer):
website_footer = serializers.CharField(max_length=1024 * 1024)
allow_register = serializers.BooleanField()
submission_list_show_all = serializers.BooleanField()
forum_sort = serializers.ListField(allow_empty=False)
allow_forum_post = serializers.BooleanField()
allow_forum_reply = serializers.BooleanField()
class JudgeServerSerializer(serializers.ModelSerializer):

View File

@ -59,7 +59,12 @@ class WebsiteConfigAPITest(APITestCase):
url = self.reverse("website_config_api")
data = {"website_base_url": "http://test.com", "website_name": "test name",
"website_name_shortcut": "test oj", "website_footer": "<a>test</a>",
"allow_register": True, "submission_list_show_all": False}
"allow_register": True, "submission_list_show_all": False,
"allow_forum_post": True, "allow_forum_reply": True,
"forum_sort": [{"name": "提问版", "permission": "All"},
{"name": "题解版", "permission": "All"},
{"name": "灌水版", "permission": "All"},
{"name": "站务版", "permission": "Super Admin"}]}
resp = self.client.post(url, data=data)
self.assertSuccess(resp)
@ -68,7 +73,12 @@ class WebsiteConfigAPITest(APITestCase):
url = self.reverse("website_config_api")
data = {"website_base_url": "http://test.com", "website_name": "test name",
"website_name_shortcut": "test oj", "website_footer": "<img onerror=alert(1) src=#>",
"allow_register": True, "submission_list_show_all": False}
"allow_register": True, "submission_list_show_all": False,
"allow_forum_post": True, "allow_forum_reply": True,
"forum_sort": [{"name": "提问版", "permission": "All"},
{"name": "题解版", "permission": "All"},
{"name": "灌水版", "permission": "All"},
{"name": "站务版", "permission": "Super Admin"}]}
resp = self.client.post(url, data=data)
self.assertSuccess(resp)
self.assertEqual(SysOptions.website_footer, "<img src=\"#\" />")

View File

@ -10,5 +10,5 @@ urlpatterns = [
url(r"^judge_server/?$", JudgeServerAPI.as_view(), name="judge_server_api"),
url(r"^prune_test_case/?$", TestCasePruneAPI.as_view(), name="prune_test_case_api"),
url(r"^versions/?$", ReleaseNotesAPI.as_view(), name="get_release_notes_api"),
url(r"^dashboard_info", DashboardInfoAPI.as_view(), name="dashboard_info_api"),
url(r"^dashboard_info", DashboardInfoAPI.as_view(), name="dashboard_info_api")
]

View File

@ -91,7 +91,8 @@ class WebsiteConfigAPI(APIView):
def get(self, request):
ret = {key: getattr(SysOptions, key) for key in
["website_base_url", "website_name", "website_name_shortcut",
"website_footer", "allow_register", "submission_list_show_all"]}
"website_footer", "allow_register", "submission_list_show_all",
"forum_sort", "allow_forum_post", "allow_forum_reply"]}
return self.success(ret)
@super_admin_required
@ -211,7 +212,7 @@ class TestCasePruneAPI(APIView):
class ReleaseNotesAPI(APIView):
def get(self, request):
try:
resp = requests.get("https://raw.githubusercontent.com/QingdaoU/OnlineJudge/master/docs/data.json?_=" + str(time.time()),
resp = requests.get("https://gitee.com/zklcdc/OnlineJudge/raw/master/docs/data.json?_=" + str(time.time()),
timeout=3)
releases = resp.json()
except (RequestException, ValueError):

View File

@ -11,7 +11,7 @@ from account.models import User
from submission.models import Submission, JudgeStatus
from utils.api import APIView, validate_serializer
from utils.cache import cache
from utils.constants import CacheKey
from utils.constants import CacheKey, ContestRuleType
from utils.shortcuts import rand_str
from utils.tasks import delete_files
from ..models import Contest, ContestAnnouncement, ACMContestRank
@ -19,6 +19,7 @@ from ..serializers import (ContestAnnouncementSerializer, ContestAdminSerializer
CreateConetestSeriaizer, CreateContestAnnouncementSerializer,
EditConetestSeriaizer, EditContestAnnouncementSerializer,
ACMContesHelperSerializer, )
from django.utils.encoding import escape_uri_path
class ContestAPI(APIView):
@ -200,10 +201,13 @@ class DownloadContestSubmissions(APIView):
problem_ids = contest.problem_set.all().values_list("id", "_id")
id2display_id = {k[0]: k[1] for k in problem_ids}
ac_map = {k[0]: False for k in problem_ids}
if contest.rule_type == ContestRuleType.OI:
submissions = Submission.objects.filter(contest=contest).order_by("-create_time")
else:
submissions = Submission.objects.filter(contest=contest, result=JudgeStatus.ACCEPTED).order_by("-create_time")
user_ids = submissions.values_list("user_id", flat=True)
users = User.objects.filter(id__in=user_ids)
path = f"/tmp/{rand_str()}.zip"
path = f"/tmp/{contest.title}-{rand_str()}.zip"
with zipfile.ZipFile(path, "w") as zip_file:
for user in users:
if user.is_admin_role() and exclude_admin:
@ -214,7 +218,22 @@ class DownloadContestSubmissions(APIView):
problem_id = submission.problem_id
if user_ac_map[problem_id]:
continue
file_name = f"{user.username}_{id2display_id[submission.problem_id]}.txt"
suffix = "cpp"
if submission.language == "Java":
suffix = "java"
elif submission.language == "C":
suffix = "c"
elif submission.language == "Python2":
suffix = "py"
elif submission.language == "Python3":
suffix = "py"
file_name = f"{user.username}/{user.username}_{id2display_id[submission.problem_id]}"
if contest.rule_type == ContestRuleType.OI:
statistic_info = submission.statistic_info
score = statistic_info["score"]
file_name = file_name + f"_{score}"
file_name = file_name + "." + suffix
compression = zipfile.ZIP_DEFLATED
zip_file.writestr(zinfo_or_arcname=f"{file_name}",
data=submission.code,
@ -237,5 +256,6 @@ class DownloadContestSubmissions(APIView):
delete_files.send_with_options(args=(zip_path,), delay=300_000)
resp = FileResponse(open(zip_path, "rb"))
resp["Content-Type"] = "application/zip"
resp["Content-Disposition"] = f"attachment;filename={os.path.basename(zip_path)}"
file_name = os.path.basename(zip_path)
resp["Content-Disposition"] = f"attachment;filename={escape_uri_path(file_name)}"
return resp

View File

@ -9,7 +9,8 @@ from problem.models import Problem
from utils.api import APIView, validate_serializer
from utils.constants import CacheKey, CONTEST_PASSWORD_SESSION_KEY
from utils.shortcuts import datetime2str, check_is_id
from account.models import AdminType
from account.models import AdminType, User
from account.serializers import UserProfileSerializer
from account.decorators import login_required, check_contest_permission, check_contest_password
from utils.constants import ContestRuleType, ContestStatus
@ -189,4 +190,11 @@ class ContestRankAPI(APIView):
page_qs = self.paginate_data(request, qs)
page_qs["results"] = serializer(page_qs["results"], many=True, is_contest_admin=is_contest_admin).data
for i in range(0, len(page_qs["results"])):
try:
user = User.objects.get(id=page_qs["results"][i]["user"]["id"], is_disabled=False)
userprofile = UserProfileSerializer(user.userprofile, show_real_name=True).data
page_qs["results"][i].update({"grade": userprofile["grade"], "title": userprofile["user"]["title"], "title_color": userprofile["user"]["title_color"]})
except Exception:
page_qs["results"][i].update({"grade": 0, "title": None, "title_color": None})
return self.success(page_qs)

View File

@ -48,9 +48,9 @@ fi
cd $APP/dist
if [ ! -z "$STATIC_CDN_HOST" ]; then
find . -name "*.*" -type f -exec sed -i "s/__STATIC_CDN_HOST__/\/$STATIC_CDN_HOST/g" {} \;
find . -name "*.*" -type f -exec sed -i "s|\/__STATIC_CDN_HOST__|$STATIC_CDN_HOST|g" {} \;
else
find . -name "*.*" -type f -exec sed -i "s/__STATIC_CDN_HOST__\///g" {} \;
find . -name "*.*" -type f -exec sed -i "s|__STATIC_CDN_HOST__\/||g" {} \;
fi
cd $APP

25
deploy/nginx/fastcgi.conf Normal file
View File

@ -0,0 +1,25 @@
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param REQUEST_SCHEME $scheme;
fastcgi_param HTTPS $https if_not_empty;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param REDIRECT_STATUS 200;

View File

@ -6,6 +6,11 @@ location /api {
include api_proxy.conf;
}
location /api/live2d {
root /forum;
index index.html index.htm index.php;
}
location /admin {
root /app/dist/admin;
try_files $uri $uri/ /index.html =404;
@ -19,3 +24,14 @@ location / {
root /app/dist;
try_files $uri $uri/ /index.html =404;
}
location ~ [^/]\.php(/|$) {
root /forum;
try_files $uri =404;
fastcgi_pass oj-forum:9000;
fastcgi_split_path_info ^(.+\.php)(.*)$;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
include fastcgi.conf;
}

View File

@ -46,6 +46,9 @@ http {
server {
listen 1443 ssl http2 default_server;
server_name _;
add_header Strict-Transport-Security "max-age=15552000; includeSubdomains; preload";
ssl_certificate /data/ssl/server.crt;
ssl_certificate_key /data/ssl/server.key;
ssl_protocols TLSv1.2;

View File

@ -1,9 +1,9 @@
certifi==2019.3.9
chardet==3.0.4
coverage==4.5.3
Django==2.1.7
Django==2.2.24
django-redis==4.10.0
djangorestframework==3.8.2
djangorestframework==3.11.2
entrypoints==0.3
Envelopes==0.4
flake8==3.7.7
@ -14,7 +14,7 @@ idna==2.8
jsonfield==2.0.2
mccabe==0.6.1
otpauth==1.0.1
Pillow==5.4.1
Pillow==8.2.0
psycopg2-binary==2.7.7
pycodestyle==2.5.0
pyflakes==2.1.1
@ -23,9 +23,9 @@ pytz==2018.9
qrcode==6.1
raven==6.10.0
redis==3.2.0
requests==2.21.0
requests==2.25.1
six==1.12.0
urllib3==1.24.1
urllib3==1.26.5
XlsxWriter==1.1.5
django-dramatiq==0.5.0
dramatiq==1.3.0

15
docker-compose.yml Normal file
View File

@ -0,0 +1,15 @@
version: "3"
services:
oj-rsync-master-dev:
image: registry.cn-hangzhou.aliyuncs.com/onlinejudge/oj_rsync
container_name: oj-rsync-master-dev
volumes:
- $PWD/data/test_case:/test_case:ro
- $PWD/data/rsync_master:/log
environment:
- RSYNC_MODE=master
- RSYNC_USER=ojrsync
- RSYNC_PASSWORD=CHANGE_THIS_PASSWORD
ports:
- "0.0.0.0:873:873"

View File

@ -1,12 +1,143 @@
{
"update": [
{
"version": "2020-07-09",
"version": "2021-08-06",
"level": "Recommend",
"title": "2020-07-09",
"title": "v1.3.11.0.3",
"details": [
"Add Golang language to judge server",
"Fix some frontend bugs"
"[Beautify] Markdown",
"[Opt] Live2D Support CDN",
"[Opt] Code Highlight"
]
},
{
"version": "2021-07-18",
"level": "Recommend",
"title": "v1.3.11.0.2",
"details": [
"[Fix] UI",
"[Fix] Fix missing icons",
"[Fix] Fix missing Alert"
]
},
{
"version": "2021-07-11",
"level": "Important",
"title": "v1.3.11.0.1",
"details": [
"[Add] Global programming language Cache",
"[Add] JudgeServer Support Pypy, Node.js, PHP",
"[Add] Node.js, PHP Highlight",
"[Fix] Safety Problem",
"[Fix] CDN",
"[Fix] Translation",
"[Fix] UI",
"[Fix] Code Format",
"[Upgrade] Node.js to v12.16.1",
"[Upgrade] iview to 4.6.1",
"[Upgrade] Upgrade form QingdaoU/OnlineJudgeFE",
"[Upgrade] Upgrade form QingdaoU/OnlineJudge"
]
},
{
"version": "2020-08-02",
"level": "Recommend",
"title": "v1.3.10.8.1",
"details": [
"[Add] Forum System",
"[Add] Forum Post / Reply",
"[Add] Top / Light / Star",
"[Add] Forum Sort and Sort's posting permission",
"[Add] Overall situation Post / Reply permission, and can Open / Close Forum",
"[Add] Send Email when recepting a reply ",
"[Add] Import Realname when importing users",
"[Add] Experience Rank",
"[Add] Custom User Title/Color",
"[Add] Sighin WebsiteName auto Get",
"[Fix] Safety Problem",
"[Fix] Translation",
"[Fix] User Grade show problem",
"[Fix] Theme Show Problem",
"[Fix] Golang Code Highlight"
]
},
{
"version": "2020-07-16",
"level": "Recommend",
"title": "v1.3.10.7.1",
"details": [
"[Add] Golang language to judge server",
"[Add] Support Mobile Device",
"[Fix] Chinese Translate",
"[Fix] Contest Rank Table Show Problem",
"[Fix] Solve Safty Problem"
]
},
{
"version": "2020-05-06",
"level": "Recommend",
"title": "v1.3.10.6.5",
"details": [
"[Add] Editor add more Function",
"[Fix] Python3 Chinese Character Support",
"[Fix] Table Support",
"[Fix] Katex Multiline Formula Support"
]
},
{
"version": "2020-05-04",
"level": "Important",
"title": "v1.3.10.6.4.4",
"details": [
"[Fix] Translate",
"[Fix] Remove Contest Announcements Sighin Panel"
]
},
{
"version": "2020-05-03",
"level": "Recommend",
"title": "v1.3.10.6.4.3",
"details": [
"[Add] Online IDE Code Cache",
"[Add] Code Editor Match Bracket",
"[Fix] Format: Add FAQ missing spaces",
"[Fix] Chinese Translate",
"[Fix] Solve Safty Problem",
"[Fix] Reset Bottom Don't Reset",
"[Fix] Remove Contest Announcements Sighin Panel",
"[Opt] Upgrade Redis to support Multithreading",
"[Opt] Tiny Live2d Docker Image Size"
]
},
{
"version": "2020-05-02",
"level": "Recommend",
"title": "v1.3.10.6.4.1",
"details": [
"[Fix] Reduce Liv2d Memory Usage"
]
},
{
"version": "2020-04-26",
"level": "Important",
"title": "v1.3.10.6.3.1",
"details": [
"[Fix] Safety Problem",
"[Upgrade] Upgrade form QingdaoU/OnlineJudgeFE"
]
},
{
"version": "2020-04-20",
"level": "Recommend",
"title": "2020-04-20",
"details": [
"[Add] 稳点经验系统",
"[Add] 签到系统",
"[Add] 每日一言",
"[Add] 等级称号系统",
"[Add] 称号信息显示",
"[Fix] 问题标签排序",
"[Fix] 问题标签“Pick One” 上移"
]
},
{

0
forum/__init__.py Normal file
View File

View File

@ -0,0 +1,54 @@
# Generated by Django 2.1.7 on 2020-07-17 08:55
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import utils.models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ForumPost',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.TextField()),
('sort', models.IntegerField()),
('son_sort', models.IntegerField()),
('content', utils.models.RichTextField()),
('create_time', models.DateTimeField(auto_now_add=True)),
('last_update_time', models.DateTimeField(auto_now=True)),
('is_top', models.BooleanField(default=False)),
('is_nice', models.BooleanField(default=False)),
('is_light', models.BooleanField(default=False)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'forumPost',
'ordering': ('-create_time',),
},
),
migrations.CreateModel(
name='ForumReply',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('fa_id', models.IntegerField()),
('content', utils.models.RichTextField()),
('create_time', models.DateTimeField(auto_now_add=True)),
('floor', models.IntegerField()),
('is_top', models.BooleanField(default=False)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'forumReply',
'ordering': ('-create_time',),
},
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 2.1.7 on 2020-07-18 15:24
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('forum', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='forumreply',
options={'ordering': ('floor',)},
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 2.1.7 on 2020-07-31 08:40
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('forum', '0002_auto_20200718_2324'),
]
operations = [
migrations.RemoveField(
model_name='forumreply',
name='is_top',
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 2.1.7 on 2020-08-01 06:10
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('forum', '0003_remove_forumreply_is_top'),
]
operations = [
migrations.RemoveField(
model_name='forumpost',
name='son_sort',
),
]

View File

34
forum/models.py Normal file
View File

@ -0,0 +1,34 @@
from django.db import models
from account.models import User
from utils.models import RichTextField
class ForumPost(models.Model):
title = models.TextField()
sort = models.IntegerField()
# HTML
content = RichTextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
create_time = models.DateTimeField(auto_now_add=True)
last_update_time = models.DateTimeField(auto_now=True)
is_top = models.BooleanField(default=False)
is_nice = models.BooleanField(default=False)
is_light = models.BooleanField(default=False)
class Meta:
db_table = "forumPost"
ordering = ("-create_time",)
class ForumReply(models.Model):
fa_id = models.IntegerField()
# HTML
content = RichTextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
create_time = models.DateTimeField(auto_now_add=True)
floor = models.IntegerField()
class Meta:
db_table = "forumReply"
ordering = ("floor",)

35
forum/serializers.py Normal file
View File

@ -0,0 +1,35 @@
from utils.api import serializers
from utils.api._serializers import UsernameSerializer
from .models import ForumPost, ForumReply
class CreateEditForumPostSerializer(serializers.Serializer):
id = serializers.IntegerField()
title = serializers.CharField(max_length=64)
sort = serializers.IntegerField(min_value=0, max_value=20)
content = serializers.CharField(max_length=1024 * 1024 * 8)
is_top = serializers.BooleanField()
is_nice = serializers.BooleanField()
is_light = serializers.BooleanField()
class ForumPostSerializer(serializers.ModelSerializer):
author = UsernameSerializer()
class Meta:
model = ForumPost
fields = "__all__"
class CreateEditForumReplySerializer(serializers.Serializer):
fa_id = serializers.IntegerField()
content = serializers.CharField(max_length=1024 * 1024 * 8)
class ForumReplySerializer(serializers.ModelSerializer):
author = UsernameSerializer()
class Meta:
model = ForumReply
fields = "__all__"

View File

@ -0,0 +1,28 @@
<div>
<table cellpadding="0" align="center"
style="overflow:hidden;background:#fff;margin:0 auto;text-align:left;position:relative;font-size:14px; font-family:'lucida Grande',Verdana;line-height:1.5;box-shadow:0 0 3px #ccc;border:1px solid #ccc;border-radius:5px;border-collapse:collapse;">
<tbody>
<tr>
<th valign="middle"
style="height:38px;color:#fff; font-size:14px;line-height:38px; font-weight:bold;text-align:left;padding:10px 24px 6px; border-bottom:1px solid #467ec3;background:#518bcb;border-radius:5px 5px 0 0;">
{{ website_name }}</th>
</tr>
<tr>
<td>
<div style="padding:20px 35px 40px;">
<h2 style="font-weight:bold;margin-bottom:5px;font-size:14px;">Hello, {{ username }}:</h2>
<p style="margin-top:20px">
Your Post 《<a href="{{ link }}">{{ title }}</a>》 recepted a reply:
</p>
<p style="margin-top:20px">
Please click the <a href="{{ link }}">{{ link }}</a> to see more.
</p>
<p style="margin-left:2em;"></p>
<p style="text-indent:0;text-align:right;">{{ website_name }}</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>

0
forum/tests.py Normal file
View File

8
forum/urls/oj.py Normal file
View File

@ -0,0 +1,8 @@
from django.conf.urls import url
from ..views.oj import ForumPostAPI, ForumReplyAPI
urlpatterns = [
url(r"^forumpost/?$", ForumPostAPI.as_view(), name="ForumPost_api"),
url(r"^forumreply/?$", ForumReplyAPI.as_view(), name="ForumReply_api"),
]

249
forum/views/oj.py Normal file
View File

@ -0,0 +1,249 @@
from django.db.models import Q
from django.template.loader import render_to_string
from utils.api import APIView, validate_serializer
from utils.cache import cache
from utils.throttling import TokenBucket
from account.models import User
from account.decorators import login_required
from account.serializers import UserProfileSerializer
from options.options import SysOptions
from forum.models import ForumPost, ForumReply
from forum.serializers import (CreateEditForumPostSerializer, ForumPostSerializer,
CreateEditForumReplySerializer, ForumReplySerializer)
from account.tasks import send_email_async
class ForumPostAPI(APIView):
def throttling(self, request, is_super_admin):
# 超级管理员 的请求暂不做限制
if is_super_admin:
return
user_bucket = TokenBucket(key=str(request.user.username),
redis_conn=cache,
capacity=2,
fill_rate=0.03,
default_capacity=2)
can_consume, wait = user_bucket.consume()
if not can_consume:
return "Please wait %d seconds" % (int(wait))
@validate_serializer(CreateEditForumPostSerializer)
@login_required
def post(self, request):
"""
publish ForumPost
"""
username = request.user.username
is_super_admin = User.objects.get(username=str(username), is_disabled=False).is_super_admin()
error = self.throttling(request, is_super_admin)
if error:
return self.error(error)
allow_post = SysOptions.allow_forum_post
if not allow_post:
return self.error("Don't allow to post")
data = request.data
if data["id"] != -1:
try:
forumpost = ForumPost.objects.get(id=data.pop("id"))
if str(username) != str(forumpost.author):
if not is_super_admin:
return self.error("Username doesn't match")
if not is_super_admin:
if data["is_top"] or data["is_light"] or data["is_nice"]:
return self.error("User doesn't have permission")
permission = SysOptions.forum_sort[data["sort"] - 1]["permission"]
if permission == "Super Admin":
return self.error("User doesn't have permission")
except ForumPost.DoesNotExist:
return self.error("ForumPost does not exist")
for k, v in data.items():
setattr(forumpost, k, v)
forumpost.save()
return self.success(ForumPostSerializer(forumpost).data)
if not is_super_admin:
permission = SysOptions.forum_sort[data["sort"] - 1]["permission"]
if permission == "Super Admin":
return self.error("User doesn't have permission")
forumpost = ForumPost.objects.create(title=data["title"],
content=data["content"],
sort=data["sort"],
is_top=False,
is_nice=False,
is_light=False,
author=request.user)
return self.success(ForumPostSerializer(forumpost).data)
def get(self, request):
"""
get ForumPost list / get one ForumPost
"""
forumpost_id = request.GET.get("forumpost_id")
if forumpost_id:
try:
forumpost = ForumPost.objects.select_related("author").get(id=forumpost_id)
forumpost_data = ForumPostSerializer(forumpost).data
return self.success(forumpost_data)
except ForumPost.DoesNotExist:
return self.error("ForumPost does not exist")
limit = request.GET.get("limit")
if not limit:
return self.error("Limit is needed")
forumposts = ForumPost.objects.select_related("author").filter().order_by("-is_top", "-create_time",)
# 按照分区筛选
sort = request.GET.get("sort")
if sort:
forumposts = forumposts.filter(sort=sort)
# 搜索的情况
keyword = request.GET.get("keyword", "").strip()
if keyword:
forumposts = forumposts.filter(Q(title__icontains=keyword) | Q(id__icontains=keyword))
data = self.paginate_data(request, forumposts, ForumPostSerializer)
return self.success(data)
@login_required
def delete(self, request):
"""
delete ForumPost
"""
if request.GET.get("forumpost_id"):
username = request.user.username
is_super_admin = User.objects.get(username=str(username), is_disabled=False).is_super_admin()
forumpost = ForumPost.objects.get(id=request.GET["forumpost_id"])
if str(username) == str(forumpost.author):
ForumPost.objects.filter(id=request.GET["forumpost_id"]).delete()
ForumReply.objects.filter(fa_id=request.GET["forumpost_id"]).delete()
elif is_super_admin:
ForumPost.objects.filter(id=request.GET["forumpost_id"]).delete()
ForumReply.objects.filter(fa_id=request.GET["forumpost_id"]).delete()
else:
return self.error("Username doesn't match")
return self.success()
class ForumReplyAPI(APIView):
def throttling(self, request, is_super_admin):
# 超级管理员 的请求暂不做限制
if is_super_admin:
return
user_bucket = TokenBucket(key=str(request.user.username),
redis_conn=cache,
capacity=2,
fill_rate=0.03,
default_capacity=2)
can_consume, wait = user_bucket.consume()
if not can_consume:
return "Please wait %d seconds" % (int(wait))
@validate_serializer(CreateEditForumReplySerializer)
@login_required
def post(self, request):
"""
publish/edit ForumReply
"""
username = request.user.username
is_super_admin = User.objects.get(username=str(username), is_disabled=False).is_super_admin()
error = self.throttling(request, is_super_admin)
if error:
return self.error(error)
allow_reply = SysOptions.allow_forum_reply
if not allow_reply:
return self.error("Don't allow to reply")
data = request.data
if not data["content"]:
return self.error("Reply can not be empty")
forumreplys = ForumReply.objects.select_related("author").filter(fa_id=data["fa_id"]).order_by("-create_time",)
# 判断楼层
if forumreplys.exists():
floor = forumreplys.values().first()["floor"] + 1
else:
floor = 0
forumreply = ForumReply.objects.create(fa_id=data["fa_id"],
content=data["content"],
floor=floor,
author=request.user)
data = ForumReplySerializer(forumreply).data
try:
user = User.objects.get(id=data["author"]["id"], is_disabled=False)
userprofile = UserProfileSerializer(user.userprofile, show_real_name=False).data
data["author"].update({"grade": userprofile["grade"], "title": userprofile["user"]["title"], "title_color": userprofile["user"]["title_color"]})
except Exception:
data["author"].update({"grade": 0, "title": None, "title_color": None})
forumpost = ForumPostSerializer(ForumPost.objects.select_related("author").get(id=data["fa_id"])).data
author = User.objects.get(username=forumpost["author"]["username"], is_disabled=False)
render_data = {
"username": author.username,
"website_name": SysOptions.website_name,
"title": forumpost["title"],
"link": f"{SysOptions.website_base_url}/Forum/{data['fa_id']}"
}
email_html = render_to_string("reply_email.html", render_data)
send_email_async.send(from_name=SysOptions.website_name_shortcut,
to_email=author.email,
to_name=author.username,
subject=f"Your Post recepted a reply",
content=email_html)
return self.success(data)
def get(self, request):
"""
get ForumReply list / get one ForumReply
"""
fa_id = request.GET.get("fa_id")
if not fa_id:
return self.error("fa_id is needed")
limit = request.GET.get("limit")
if not limit:
return self.error("Limit is needed")
forumreplys = ForumReply.objects.select_related("author").filter(fa_id=fa_id)
data = self.paginate_data(request, forumreplys, ForumReplySerializer)
for i in range(0, len(data["results"])):
try:
user = User.objects.get(id=data["results"][i]["author"]["id"], is_disabled=False)
userprofile = UserProfileSerializer(user.userprofile, show_real_name=False).data
data["results"][i]["author"].update({"grade": userprofile["grade"], "title": userprofile["user"]["title"], "title_color": userprofile["user"]["title_color"]})
except Exception:
data["results"][i]["author"].update({"grade": 0, "title": None, "title_color": None})
return self.success(data)
@login_required
def delete(self, request):
"""
delete ForumReply
"""
if request.GET.get("id"):
username = request.user.username
is_super_admin = User.objects.get(username=str(username), is_disabled=False).is_super_admin()
forumreply = ForumReply.objects.get(id=request.GET["id"])
forumoost = ForumPost.objects.get(id=forumreply.fa_id)
if str(username) == str(forumreply.author):
ForumReply.objects.filter(id=request.GET["id"]).delete()
elif str(username) == str(forumoost.author.username):
ForumReply.objects.filter(id=request.GET["id"]).delete()
elif is_super_admin:
ForumReply.objects.filter(id=request.GET["id"]).delete()
else:
return self.error("Username doesn't match")
return self.success()

View File

@ -8,12 +8,12 @@ fi
sleep 2
docker rm -f oj-postgres-dev oj-redis-dev
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
docker run -it -d -p 127.0.0.1:6380:6379 --name oj-redis-dev redis:4.0-alpine
docker run -it -d -e POSTGRES_DB=onlinejudge -e POSTGRES_USER=onlinejudge -e POSTGRES_PASSWORD=onlinejudge -p 0.0.0.0:5435:5432 --name oj-postgres-dev postgres:10
docker run -it -d -p 127.0.0.1:6380:6379 --name oj-redis-dev redis:6.0-alpine
if [ "$1" = "--migrate" ]; then
sleep 3
echo `cat /dev/urandom | head -1 | md5sum | head -c 32` > data/config/secret.key
python manage.py migrate
python manage.py inituser --username root --password rootroot --action create_super_admin
python3.7 manage.py migrate
python3.7 manage.py inituser --username root --password rootroot --action create_super_admin
fi

View File

@ -7,7 +7,7 @@ import requests
from django.db import transaction, IntegrityError
from django.db.models import F
from account.models import User
from account.models import User, UserProfile
from conf.models import JudgeServer
from contest.models import ContestRuleType, ACMContestRank, OIContestRank, ContestStatus
from options.options import SysOptions
@ -17,6 +17,7 @@ from submission.models import JudgeStatus, Submission
from utils.cache import cache
from utils.constants import CacheKey
logger = logging.getLogger(__name__)
@ -221,6 +222,12 @@ class JudgeDispatcher(DispatcherBase):
acm_problems_status[problem_id]["status"] = self.submission.result
if self.submission.result == JudgeStatus.ACCEPTED:
profile.accepted_number += 1
if problem.difficulty == "High":
UserProfile.objects.get(user_id=self.submission.user_id).add_experience(this_time_experience=15)
if problem.difficulty == "Mid":
UserProfile.objects.get(user_id=self.submission.user_id).add_experience(this_time_experience=8)
if problem.difficulty == "Low":
UserProfile.objects.get(user_id=self.submission.user_id).add_experience(this_time_experience=3)
profile.acm_problems_status["problems"] = acm_problems_status
profile.save(update_fields=["accepted_number", "acm_problems_status"])
@ -235,6 +242,12 @@ class JudgeDispatcher(DispatcherBase):
oi_problems_status[problem_id]["status"] = self.submission.result
if self.submission.result == JudgeStatus.ACCEPTED:
profile.accepted_number += 1
if problem.difficulty == "High":
UserProfile.objects.get(user_id=self.submission.user_id).add_experience(this_time_experience=15)
if problem.difficulty == "Mid":
UserProfile.objects.get(user_id=self.submission.user_id).add_experience(this_time_experience=8)
if problem.difficulty == "Low":
UserProfile.objects.get(user_id=self.submission.user_id).add_experience(this_time_experience=3)
profile.oi_problems_status["problems"] = oi_problems_status
profile.save(update_fields=["accepted_number", "oi_problems_status"])
@ -261,10 +274,22 @@ class JudgeDispatcher(DispatcherBase):
acm_problems_status[problem_id] = {"status": self.submission.result, "_id": self.problem._id}
if self.submission.result == JudgeStatus.ACCEPTED:
user_profile.accepted_number += 1
if problem.difficulty == "High":
UserProfile.objects.get(user_id=self.submission.user_id).add_experience(this_time_experience=15)
if problem.difficulty == "Mid":
UserProfile.objects.get(user_id=self.submission.user_id).add_experience(this_time_experience=8)
if problem.difficulty == "Low":
UserProfile.objects.get(user_id=self.submission.user_id).add_experience(this_time_experience=3)
elif acm_problems_status[problem_id]["status"] != JudgeStatus.ACCEPTED:
acm_problems_status[problem_id]["status"] = self.submission.result
if self.submission.result == JudgeStatus.ACCEPTED:
user_profile.accepted_number += 1
if problem.difficulty == "High":
UserProfile.objects.get(user_id=self.submission.user_id).add_experience(this_time_experience=15)
if problem.difficulty == "Mid":
UserProfile.objects.get(user_id=self.submission.user_id).add_experience(this_time_experience=8)
if problem.difficulty == "Low":
UserProfile.objects.get(user_id=self.submission.user_id).add_experience(this_time_experience=3)
user_profile.acm_problems_status["problems"] = acm_problems_status
user_profile.save(update_fields=["submission_number", "accepted_number", "acm_problems_status"])
@ -278,6 +303,12 @@ class JudgeDispatcher(DispatcherBase):
"score": score}
if self.submission.result == JudgeStatus.ACCEPTED:
user_profile.accepted_number += 1
if problem.difficulty == "High":
UserProfile.objects.get(user_id=self.submission.user_id).add_experience(this_time_experience=15)
if problem.difficulty == "Mid":
UserProfile.objects.get(user_id=self.submission.user_id).add_experience(this_time_experience=8)
if problem.difficulty == "Low":
UserProfile.objects.get(user_id=self.submission.user_id).add_experience(this_time_experience=3)
elif oi_problems_status[problem_id]["status"] != JudgeStatus.ACCEPTED:
# minus last time score, add this time score
user_profile.add_score(this_time_score=score,
@ -286,6 +317,12 @@ class JudgeDispatcher(DispatcherBase):
oi_problems_status[problem_id]["status"] = self.submission.result
if self.submission.result == JudgeStatus.ACCEPTED:
user_profile.accepted_number += 1
if problem.difficulty == "High":
UserProfile.objects.get(user_id=self.submission.user_id).add_experience(this_time_experience=15)
if problem.difficulty == "Mid":
UserProfile.objects.get(user_id=self.submission.user_id).add_experience(this_time_experience=8)
if problem.difficulty == "Low":
UserProfile.objects.get(user_id=self.submission.user_id).add_experience(this_time_experience=3)
user_profile.oi_problems_status["problems"] = oi_problems_status
user_profile.save(update_fields=["submission_number", "accepted_number", "oi_problems_status"])

82
judge/ide.py Normal file
View File

@ -0,0 +1,82 @@
import hashlib
import json
import logging
from urllib.parse import urljoin
import requests
from django.db import transaction
from django.db.models import F
from conf.models import JudgeServer
from options.options import SysOptions
from utils.cache import cache
from utils.constants import CacheKey
logger = logging.getLogger(__name__)
class ChooseJudgeServer:
def __init__(self):
self.server = None
def __enter__(self) -> [JudgeServer, None]:
with transaction.atomic():
servers = JudgeServer.objects.select_for_update().filter(is_disabled=False).order_by("task_number")
servers = [s for s in servers if s.status == "normal"]
for server in servers:
if server.task_number <= server.cpu_core * 2:
server.task_number = F("task_number") + 1
server.save(update_fields=["task_number"])
self.server = server
return server
return None
def __exit__(self, exc_type, exc_val, exc_tb):
if self.server:
JudgeServer.objects.filter(id=self.server.id).update(task_number=F("task_number") - 1)
class JudgeServerClientError(Exception):
pass
class IDEDispatcher(object):
def __init__(self, src, language, test_case=None):
self.src = src
self.language = language
self.test_case = test_case
self.token = hashlib.sha256(SysOptions.judge_server_token.encode("utf-8")).hexdigest()
def _request(self, url, data=None):
kwargs = {"headers": {"X-Judge-Server-Token": self.token,
"Content-Type": "application/json"}}
if data:
kwargs["data"] = json.dumps(data)
try:
return requests.post(url, **kwargs).json()
except Exception as e:
raise JudgeServerClientError(str(e))
def judge(self):
if not self.test_case:
raise ValueError("invalid parameter")
sub_config = list(filter(lambda item: self.language == item["name"], SysOptions.languages))[0]
max_cpu_time = 2000
max_memory = 1024 * 1024 * 128
output = True
data = {
"language_config": sub_config["config"],
"src": self.src,
"max_cpu_time": max_cpu_time,
"max_memory": max_memory,
"test_case": self.test_case,
"output": output
}
with ChooseJudgeServer() as server:
if not server:
cache.lpush(CacheKey.waiting_queue, json.dumps(data))
return "JudgeServer ERROR"
return self._request(urljoin(server.service_url, "/judge"), data=data)
# return data

View File

@ -15,6 +15,39 @@ int add(int a, int b) {
}
//TEMPLATE END
//APPEND BEGIN
int main() {
printf("%d", add(1, 2));
return 0;
}
//APPEND END""",
"compile": {
"src_name": "main.c",
"exe_name": "main",
"max_cpu_time": 3000,
"max_real_time": 10000,
"max_memory": 256 * 1024 * 1024,
"compile_command": "/usr/bin/gcc -DONLINE_JUDGE -w -fmax-errors=3 -std=c11 {src_path} -lm -o {exe_path}",
},
"run": {
"command": "{exe_path}",
"seccomp_rule": {ProblemIOMode.standard: "c_cpp", ProblemIOMode.file: "c_cpp_file_io"},
"env": default_env
}
}
_c_o2_lang_config = {
"template": """//PREPEND BEGIN
#include <stdio.h>
//PREPEND END
//TEMPLATE BEGIN
int add(int a, int b) {
// Please fill this blank
return ___________;
}
//TEMPLATE END
//APPEND BEGIN
int main() {
printf("%d", add(1, 2));
@ -39,10 +72,10 @@ int main() {
_c_lang_spj_compile = {
"src_name": "spj-{spj_version}.c",
"exe_name": "spj-{spj_version}",
"max_cpu_time": 3000,
"max_cpu_time": 8000,
"max_real_time": 10000,
"max_memory": 1024 * 1024 * 1024,
"compile_command": "/usr/bin/gcc -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c11 {src_path} -lm -o {exe_path}"
"compile_command": "/usr/bin/gcc -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c17 {src_path} -lm -o {exe_path}"
}
_c_lang_spj_config = {
@ -63,6 +96,171 @@ int add(int a, int b) {
}
//TEMPLATE END
//APPEND BEGIN
int main() {
std::cout << add(1, 2);
return 0;
}
//APPEND END""",
"compile": {
"src_name": "main.cpp",
"exe_name": "main",
"max_cpu_time": 10000,
"max_real_time": 20000,
"max_memory": 1024 * 1024 * 1024,
"compile_command": "/usr/bin/g++ -DONLINE_JUDGE -w -fmax-errors=3 {src_path} -lm -o {exe_path}",
},
"run": {
"command": "{exe_path}",
"seccomp_rule": {ProblemIOMode.standard: "c_cpp", ProblemIOMode.file: "c_cpp_file_io"},
"env": default_env
}
}
_cpp_o2_lang_config = {
"template": """//PREPEND BEGIN
#include <iostream>
//PREPEND END
//TEMPLATE BEGIN
int add(int a, int b) {
// Please fill this blank
return ___________;
}
//TEMPLATE END
//APPEND BEGIN
int main() {
std::cout << add(1, 2);
return 0;
}
//APPEND END""",
"compile": {
"src_name": "main.cpp",
"exe_name": "main",
"max_cpu_time": 10000,
"max_real_time": 20000,
"max_memory": 1024 * 1024 * 1024,
"compile_command": "/usr/bin/g++ -DONLINE_JUDGE -O2 -w -fmax-errors=3 {src_path} -lm -o {exe_path}",
},
"run": {
"command": "{exe_path}",
"seccomp_rule": {ProblemIOMode.standard: "c_cpp", ProblemIOMode.file: "c_cpp_file_io"},
"env": default_env
}
}
_cpp_11_lang_config = {
"template": """//PREPEND BEGIN
#include <iostream>
//PREPEND END
//TEMPLATE BEGIN
int add(int a, int b) {
// Please fill this blank
return ___________;
}
//TEMPLATE END
//APPEND BEGIN
int main() {
std::cout << add(1, 2);
return 0;
}
//APPEND END""",
"compile": {
"src_name": "main.cpp",
"exe_name": "main",
"max_cpu_time": 10000,
"max_real_time": 20000,
"max_memory": 1024 * 1024 * 1024,
"compile_command": "/usr/bin/g++ -DONLINE_JUDGE -w -fmax-errors=3 -std=c++11 {src_path} -lm -o {exe_path}",
},
"run": {
"command": "{exe_path}",
"seccomp_rule": {ProblemIOMode.standard: "c_cpp", ProblemIOMode.file: "c_cpp_file_io"},
"env": default_env
}
}
_cpp_11_o2_lang_config = {
"template": """//PREPEND BEGIN
#include <iostream>
//PREPEND END
//TEMPLATE BEGIN
int add(int a, int b) {
// Please fill this blank
return ___________;
}
//TEMPLATE END
//APPEND BEGIN
int main() {
std::cout << add(1, 2);
return 0;
}
//APPEND END""",
"compile": {
"src_name": "main.cpp",
"exe_name": "main",
"max_cpu_time": 10000,
"max_real_time": 20000,
"max_memory": 1024 * 1024 * 1024,
"compile_command": "/usr/bin/g++ -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c++11 {src_path} -lm -o {exe_path}",
},
"run": {
"command": "{exe_path}",
"seccomp_rule": {ProblemIOMode.standard: "c_cpp", ProblemIOMode.file: "c_cpp_file_io"},
"env": default_env
}
}
_cpp_14_lang_config = {
"template": """//PREPEND BEGIN
#include <iostream>
//PREPEND END
//TEMPLATE BEGIN
int add(int a, int b) {
// Please fill this blank
return ___________;
}
//TEMPLATE END
//APPEND BEGIN
int main() {
std::cout << add(1, 2);
return 0;
}
//APPEND END""",
"compile": {
"src_name": "main.cpp",
"exe_name": "main",
"max_cpu_time": 10000,
"max_real_time": 20000,
"max_memory": 1024 * 1024 * 1024,
"compile_command": "/usr/bin/g++ -DONLINE_JUDGE -w -fmax-errors=3 -std=c++14 {src_path} -lm -o {exe_path}",
},
"run": {
"command": "{exe_path}",
"seccomp_rule": {ProblemIOMode.standard: "c_cpp", ProblemIOMode.file: "c_cpp_file_io"},
"env": default_env
}
}
_cpp_14_o2_lang_config = {
"template": """//PREPEND BEGIN
#include <iostream>
//PREPEND END
//TEMPLATE BEGIN
int add(int a, int b) {
// Please fill this blank
return ___________;
}
//TEMPLATE END
//APPEND BEGIN
int main() {
std::cout << add(1, 2);
@ -84,13 +282,79 @@ int main() {
}
}
_cpp_17_lang_config = {
"template": """//PREPEND BEGIN
#include <iostream>
//PREPEND END
//TEMPLATE BEGIN
int add(int a, int b) {
// Please fill this blank
return ___________;
}
//TEMPLATE END
//APPEND BEGIN
int main() {
std::cout << add(1, 2);
return 0;
}
//APPEND END""",
"compile": {
"src_name": "main.cpp",
"exe_name": "main",
"max_cpu_time": 10000,
"max_real_time": 20000,
"max_memory": 1024 * 1024 * 1024,
"compile_command": "/usr/bin/g++ -DONLINE_JUDGE -w -fmax-errors=3 -std=c++17 {src_path} -lm -o {exe_path}",
},
"run": {
"command": "{exe_path}",
"seccomp_rule": {ProblemIOMode.standard: "c_cpp", ProblemIOMode.file: "c_cpp_file_io"},
"env": default_env
}
}
_cpp_17_o2_lang_config = {
"template": """//PREPEND BEGIN
#include <iostream>
//PREPEND END
//TEMPLATE BEGIN
int add(int a, int b) {
// Please fill this blank
return ___________;
}
//TEMPLATE END
//APPEND BEGIN
int main() {
std::cout << add(1, 2);
return 0;
}
//APPEND END""",
"compile": {
"src_name": "main.cpp",
"exe_name": "main",
"max_cpu_time": 10000,
"max_real_time": 20000,
"max_memory": 1024 * 1024 * 1024,
"compile_command": "/usr/bin/g++ -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c++17 {src_path} -lm -o {exe_path}",
},
"run": {
"command": "{exe_path}",
"seccomp_rule": {ProblemIOMode.standard: "c_cpp", ProblemIOMode.file: "c_cpp_file_io"},
"env": default_env
}
}
_cpp_lang_spj_compile = {
"src_name": "spj-{spj_version}.cpp",
"exe_name": "spj-{spj_version}",
"max_cpu_time": 10000,
"max_real_time": 20000,
"max_memory": 1024 * 1024 * 1024,
"compile_command": "/usr/bin/g++ -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c++14 {src_path} -lm -o {exe_path}"
"compile_command": "/usr/bin/g++ -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c++17 {src_path} -lm -o {exe_path}"
}
_cpp_lang_spj_config = {
@ -140,7 +404,7 @@ _py2_lang_config = {
"exe_name": "solution.pyc",
"max_cpu_time": 3000,
"max_real_time": 10000,
"max_memory": 128 * 1024 * 1024,
"max_memory": 256 * 1024 * 1024,
"compile_command": "/usr/bin/python -m py_compile {src_path}",
},
"run": {
@ -163,7 +427,7 @@ _py3_lang_config = {
"exe_name": "__pycache__/solution.cpython-36.pyc",
"max_cpu_time": 3000,
"max_real_time": 10000,
"max_memory": 128 * 1024 * 1024,
"max_memory": 256 * 1024 * 1024,
"compile_command": "/usr/bin/python3 -m py_compile {src_path}",
},
"run": {
@ -173,6 +437,74 @@ _py3_lang_config = {
}
}
_pypy2_lang_config = {
"template": """//PREPEND BEGIN
//PREPEND END
//TEMPLATE BEGIN
//TEMPLATE END
//APPEND BEGIN
//APPEND END""",
"run": {
"exe_name": "solution.py",
"command": "/usr/bin/pypy {exe_path}",
"seccomp_rule": "",
"env": default_env,
"memory_limit_check_only": 1
}
}
_pypy3_lang_config = {
"template": """//PREPEND BEGIN
//PREPEND END
//TEMPLATE BEGIN
//TEMPLATE END
//APPEND BEGIN
//APPEND END""",
"run": {
"exe_name": "solution.py",
"command": "/usr/bin/pypy3 {exe_path}",
"seccomp_rule": "",
"env": default_env + ["PYTHONIOENCODING=utf-8"],
"memory_limit_check_only": 1
}
}
_php_lang_config = {
"template": """//PREPEND BEGIN
//PREPEND END
//TEMPLATE BEGIN
//TEMPLATE END
//APPEND BEGIN
//APPEND END""",
"run": {
"exe_name": "solution.php",
"command": "/usr/bin/php {exe_path}",
"seccomp_rule": "",
"env": default_env,
"memory_limit_check_only": 1
}
}
_js_lang_config = {
"template": """//PREPEND BEGIN
//PREPEND END
//TEMPLATE BEGIN
//TEMPLATE END
//APPEND BEGIN
//APPEND END""",
"run": {
"exe_name": "solution.js",
"command": "/usr/bin/node {exe_path}",
"seccomp_rule": "",
"env": ["NO_COLOR=true"] + default_env,
"memory_limit_check_only": 1
}
}
_go_lang_config = {
"template": """//PREPEND BEGIN
//PREPEND END
@ -189,7 +521,7 @@ _go_lang_config = {
"max_real_time": 5000,
"max_memory": 1024 * 1024 * 1024,
"compile_command": "/usr/bin/go build -o {exe_path} {src_path}",
"env": ["GOCACHE=/tmp"]
"env": ["GOCACHE=/tmp", "GOPATH=/tmp/go"]
},
"run": {
"command": "{exe_path}",
@ -202,11 +534,23 @@ _go_lang_config = {
languages = [
{"config": _c_lang_config, "spj": {"compile": _c_lang_spj_compile, "config": _c_lang_spj_config},
"name": "C", "description": "GCC 5.4", "content_type": "text/x-csrc"},
"name": "C", "description": "GCC 9.3.0", "content_type": "text/x-csrc"},
{"config": _c_o2_lang_config, "name": "C With O2", "description": "GCC 9.3.0", "content_type": "text/x-csrc"},
{"config": _cpp_lang_config, "spj": {"compile": _cpp_lang_spj_compile, "config": _cpp_lang_spj_config},
"name": "C++", "description": "G++ 5.4", "content_type": "text/x-c++src"},
"name": "C++", "description": "G++ 9.3.0", "content_type": "text/x-c++src"},
{"config": _cpp_o2_lang_config, "name": "C++ With O2", "description": "G++ 9.3.0", "content_type": "text/x-c++src"},
{"config": _cpp_11_lang_config, "name": "C++ 11", "description": "G++ 9.3.0", "content_type": "text/x-c++src"},
{"config": _cpp_11_o2_lang_config, "name": "C++ 11 With O2", "description": "G++ 9.3.0", "content_type": "text/x-c++src"},
{"config": _cpp_14_lang_config, "name": "C++ 14", "description": "G++ 9.3.0", "content_type": "text/x-c++src"},
{"config": _cpp_14_o2_lang_config, "name": "C++ 14 With O2", "description": "G++ 9.3.0", "content_type": "text/x-c++src"},
{"config": _cpp_17_lang_config, "name": "C++ 17", "description": "G++ 9.3.0", "content_type": "text/x-c++src"},
{"config": _cpp_17_o2_lang_config, "name": "C++ 17 With O2", "description": "G++ 9.3.0", "content_type": "text/x-c++src"},
{"config": _java_lang_config, "name": "Java", "description": "OpenJDK 1.8", "content_type": "text/x-java"},
{"config": _py2_lang_config, "name": "Python2", "description": "Python 2.7", "content_type": "text/x-python"},
{"config": _py3_lang_config, "name": "Python3", "description": "Python 3.6", "content_type": "text/x-python"},
{"config": _go_lang_config, "name": "Golang", "description": "Golang 1.14", "content_type": "text/x-go"},
{"config": _js_lang_config, "name": "JavaScript", "description": "JavaScript (Node.Js 14.17.2)", "content_type": "text/typescript"},
{"config": _php_lang_config, "name": "PHP", "description": "PHP 8.0.8", "content_type": "text/x-php"},
{"config": _py2_lang_config, "name": "Python2", "description": "Python 2.7.17", "content_type": "text/x-python"},
{"config": _py3_lang_config, "name": "Python3", "description": "Python 3.6.9", "content_type": "text/x-python"},
{"config": _pypy2_lang_config, "name": "Pypy2", "description": "Python 2.7.18 with PyPy 7.3.5", "content_type": "text/x-python"},
{"config": _pypy3_lang_config, "name": "Pypy3", "description": "Python 3.7.10 with PyPy 7.3.5", "content_type": "text/x-python"},
{"config": _go_lang_config, "name": "Golang", "description": "Golang 1.14", "content_type": "text/x-go"}
]

View File

@ -3,6 +3,7 @@ import dramatiq
from account.models import User
from submission.models import Submission
from judge.dispatcher import JudgeDispatcher
from judge.ide import IDEDispatcher
from utils.shortcuts import DRAMATIQ_WORKER_ARGS
@ -12,3 +13,8 @@ def judge_task(submission_id, problem_id):
if User.objects.get(id=uid).is_disabled:
return
JudgeDispatcher(submission_id, problem_id).judge()
@dramatiq.actor(**DRAMATIQ_WORKER_ARGS())
def judge_IDE_task(src, language, test_case):
return IDEDispatcher(src, language, test_case).judge()

View File

@ -51,6 +51,7 @@ LOCAL_APPS = [
'submission',
'options',
'judge',
'forum',
]
INSTALLED_APPS = VENDOR_APPS + LOCAL_APPS
@ -110,7 +111,7 @@ AUTH_PASSWORD_VALIDATORS = [
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True

View File

@ -14,4 +14,5 @@ urlpatterns = [
url(r"^api/", include("submission.urls.oj")),
url(r"^api/admin/", include("submission.urls.admin")),
url(r"^api/admin/", include("utils.urls")),
url(r"^api/", include("forum.urls.oj")),
]

View File

@ -103,6 +103,9 @@ class OptionKeys:
judge_server_token = "judge_server_token"
throttling = "throttling"
languages = "languages"
forum_sort = "forum_sort"
allow_forum_post = "allow_forum_post"
allow_forum_reply = "allow_forum_reply"
class OptionDefaultValue:
@ -117,6 +120,12 @@ class OptionDefaultValue:
throttling = {"ip": {"capacity": 100, "fill_rate": 0.1, "default_capacity": 50},
"user": {"capacity": 20, "fill_rate": 0.03, "default_capacity": 10}}
languages = languages
forum_sort = [{"name": "提问版", "permission": "All"},
{"name": "题解版", "permission": "All"},
{"name": "灌水版", "permission": "All"},
{"name": "站务版", "permission": "Super Admin"}]
allow_forum_post = True
allow_forum_reply = True
class _SysOptionsMeta(type):
@ -276,6 +285,30 @@ class _SysOptionsMeta(type):
def reset_languages(cls):
cls.languages = languages
@my_property
def forum_sort(cls):
return cls._get_option(OptionKeys.forum_sort)
@forum_sort.setter
def forum_sort(cls, value):
cls._set_option(OptionKeys.forum_sort, value)
@my_property
def allow_forum_post(cls):
return cls._get_option(OptionKeys.allow_forum_post)
@allow_forum_post.setter
def allow_forum_post(cls, value):
cls._set_option(OptionKeys.allow_forum_post, value)
@my_property
def allow_forum_reply(cls):
return cls._get_option(OptionKeys.allow_forum_reply)
@allow_forum_reply.setter
def allow_forum_reply(cls, value):
cls._set_option(OptionKeys.allow_forum_reply, value)
class SysOptions(metaclass=_SysOptionsMeta):
pass

View File

@ -0,0 +1,17 @@
# Generated by Django 2.1.7 on 2020-04-19 06:13
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('problem', '0014_problem_share_submission'),
]
operations = [
migrations.AlterModelOptions(
name='problem',
options={'ordering': ('_id',)},
),
]

View File

@ -85,7 +85,7 @@ class Problem(models.Model):
class Meta:
db_table = "problem"
unique_together = (("_id", "contest"),)
ordering = ("create_time",)
ordering = ("_id",)
def add_submission_number(self):
self.submission_number = models.F("submission_number") + 1

0
run_test.py Normal file → Executable file
View File

View File

@ -1,10 +1,11 @@
from django.conf.urls import url
from ..views.oj import SubmissionAPI, SubmissionListAPI, ContestSubmissionListAPI, SubmissionExistsAPI
from ..views.oj import SubmissionAPI, SubmissionListAPI, ContestSubmissionListAPI, SubmissionExistsAPI, IDEAPI
urlpatterns = [
url(r"^submission/?$", SubmissionAPI.as_view(), name="submission_api"),
url(r"^submissions/?$", SubmissionListAPI.as_view(), name="submission_list_api"),
url(r"^submission_exists/?$", SubmissionExistsAPI.as_view(), name="submission_exists"),
url(r"^contest_submissions/?$", ContestSubmissionListAPI.as_view(), name="contest_submission_list_api"),
url(r"^ide/?$", IDEAPI.as_view(), name="IDE_api"),
]

View File

@ -1,10 +1,13 @@
import ipaddress
from account.decorators import login_required, check_contest_permission
from account.serializers import UserProfileSerializer
from account.models import User
from contest.models import ContestStatus, ContestRuleType
from judge.tasks import judge_task
from options.options import SysOptions
# from judge.dispatcher import JudgeDispatcher
from judge.ide import IDEDispatcher
from problem.models import Problem, ProblemRuleType
from utils.api import APIView, validate_serializer
from utils.cache import cache
@ -151,6 +154,13 @@ class SubmissionListAPI(APIView):
submissions = submissions.filter(result=result)
data = self.paginate_data(request, submissions)
data["results"] = SubmissionListSerializer(data["results"], many=True, user=request.user).data
for i in range(0, len(data["results"])):
try:
user = User.objects.get(id=data["results"][i]["user_id"], is_disabled=False)
userprofile = UserProfileSerializer(user.userprofile, show_real_name=False).data
data["results"][i].update({"grade": userprofile["grade"], "title": userprofile["user"]["title"], "title_color": userprofile["user"]["title_color"]})
except Exception:
data["results"][i].update({"grade": 0, "title": None, "title_color": None})
return self.success(data)
@ -191,6 +201,13 @@ class ContestSubmissionListAPI(APIView):
data = self.paginate_data(request, submissions)
data["results"] = SubmissionListSerializer(data["results"], many=True, user=request.user).data
for i in range(0, len(data["results"])):
try:
user = User.objects.get(id=data["results"][i]["user_id"], is_disabled=False)
userprofile = UserProfileSerializer(user.userprofile, show_real_name=True).data
data["results"][i].update({"grade": userprofile["grade"], "title": userprofile["user"]["title"], "title_color": userprofile["user"]["title_color"]})
except Exception:
data["results"][i].update({"grade": 0, "title": None, "title_color": None})
return self.success(data)
@ -201,3 +218,43 @@ class SubmissionExistsAPI(APIView):
return self.success(request.user.is_authenticated and
Submission.objects.filter(problem_id=request.GET["problem_id"],
user_id=request.user.id).exists())
class IDEAPI(APIView):
def throttling(self, request):
# 使用 open_api 的请求暂不做限制
auth_method = getattr(request, "auth_method", "")
if auth_method == "api_key":
return
user_bucket = TokenBucket(key=str(request.user.id),
redis_conn=cache, **SysOptions.throttling["user"])
can_consume, wait = user_bucket.consume()
if not can_consume:
return "Please wait %d seconds" % (int(wait))
@login_required
def post(self, request):
data = request.data
if data.get("captcha"):
if not Captcha(request).check(data["captcha"]):
return self.error("Invalid captcha")
error = self.throttling(request)
if error:
return self.error(error)
language = data["language"]
src = data["code"]
test_case = [{
"input": data["input"],
"output": "0"
}]
# use this to debug
result = IDEDispatcher(src, language, test_case).judge()
# result = judge_IDE_task.send(src, language, test_case)
return self.success(result)
def get(self, request):
return self.success

View File

@ -19,6 +19,7 @@ class ContestStatus:
class ContestRuleType(Choices):
ACM = "ACM"
OI = "OI"
experience = "experience"
class CacheKey: