mirror of
https://github.com/QingdaoU/OnlineJudge.git
synced 2025-11-04 14:49:58 +08:00
Merge 186dad9341 into 856c3e52b6
This commit is contained in:
commit
df0269d31f
1
.gitignore
vendored
1
.gitignore
vendored
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
69
README-CN.md
69
README-CN.md
@ -3,34 +3,43 @@
|
||||
[](https://www.python.org/downloads/release/python-362/)
|
||||
[](https://www.djangoproject.com/)
|
||||
[](http://www.django-rest-framework.org/)
|
||||
[](https://travis-ci.org/QingdaoU/OnlineJudge)
|
||||
[](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前台
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Rankings 中可以控制图表和菜单的显隐。
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
### 后台管理
|
||||
|
||||
@ -68,18 +87,18 @@ Rankings 中可以控制图表和菜单的显隐。
|
||||
|
||||

|
||||
|
||||
|
||||
## 浏览器支持
|
||||
|
||||
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
|
||||
|
||||
|
||||
68
README.md
68
README.md
@ -3,34 +3,43 @@
|
||||
[](https://www.python.org/downloads/release/python-362/)
|
||||
[](https://www.djangoproject.com/)
|
||||
[](http://www.django-rest-framework.org/)
|
||||
[](https://travis-ci.org/QingdaoU/OnlineJudge)
|
||||
[](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:
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
You can control the menu and chart status in rankings.
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
### 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)**
|
||||
|
||||
|
||||
23
account/migrations/0013_auto_20200419_1414.py
Normal file
23
account/migrations/0013_auto_20200419_1414.py
Normal 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),
|
||||
),
|
||||
]
|
||||
23
account/migrations/0014_auto_20200420_1020.py
Normal file
23
account/migrations/0014_auto_20200420_1020.py
Normal 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),
|
||||
),
|
||||
]
|
||||
18
account/migrations/0015_auto_20200421_1715.py
Normal file
18
account/migrations/0015_auto_20200421_1715.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
23
account/migrations/0016_auto_20200802_1040.py
Normal file
23
account/migrations/0016_auto_20200802_1040.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@ -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"
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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.")
|
||||
|
||||
@ -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")
|
||||
]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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})
|
||||
|
||||
@ -34,3 +34,4 @@ class Migration(migrations.Migration):
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
30
announcement/migrations/0004_auto_20190430_2118.py
Normal file
30
announcement/migrations/0004_auto_20190430_2118.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"),
|
||||
]
|
||||
|
||||
@ -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"),
|
||||
]
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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=\"#\" />")
|
||||
|
||||
@ -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")
|
||||
]
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
25
deploy/nginx/fastcgi.conf
Normal 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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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
15
docker-compose.yml
Normal 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"
|
||||
|
||||
139
docs/data.json
139
docs/data.json
@ -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
0
forum/__init__.py
Normal file
54
forum/migrations/0001_initial.py
Normal file
54
forum/migrations/0001_initial.py
Normal 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',),
|
||||
},
|
||||
),
|
||||
]
|
||||
17
forum/migrations/0002_auto_20200718_2324.py
Normal file
17
forum/migrations/0002_auto_20200718_2324.py
Normal 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',)},
|
||||
),
|
||||
]
|
||||
17
forum/migrations/0003_remove_forumreply_is_top.py
Normal file
17
forum/migrations/0003_remove_forumreply_is_top.py
Normal 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',
|
||||
),
|
||||
]
|
||||
17
forum/migrations/0004_remove_forumpost_son_sort.py
Normal file
17
forum/migrations/0004_remove_forumpost_son_sort.py
Normal 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',
|
||||
),
|
||||
]
|
||||
0
forum/migrations/__init__.py
Normal file
0
forum/migrations/__init__.py
Normal file
34
forum/models.py
Normal file
34
forum/models.py
Normal 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
35
forum/serializers.py
Normal 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__"
|
||||
28
forum/templates/reply_email.html
Normal file
28
forum/templates/reply_email.html
Normal 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
0
forum/tests.py
Normal file
8
forum/urls/oj.py
Normal file
8
forum/urls/oj.py
Normal 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
249
forum/views/oj.py
Normal 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()
|
||||
@ -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
|
||||
@ -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
82
judge/ide.py
Normal 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
|
||||
@ -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"}
|
||||
]
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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")),
|
||||
]
|
||||
|
||||
@ -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
|
||||
|
||||
17
problem/migrations/0015_auto_20200419_0613.py
Normal file
17
problem/migrations/0015_auto_20200419_0613.py
Normal 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',)},
|
||||
),
|
||||
]
|
||||
@ -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
0
run_test.py
Normal file → Executable 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"),
|
||||
]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -19,6 +19,7 @@ class ContestStatus:
|
||||
class ContestRuleType(Choices):
|
||||
ACM = "ACM"
|
||||
OI = "OI"
|
||||
experience = "experience"
|
||||
|
||||
|
||||
class CacheKey:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user