diff --git a/.gitignore b/.gitignore index 8b3b73ff..609637bb 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Dockerfile b/Dockerfile index f66305b3..ac1dddbd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README-CN.md b/README-CN.md index 5e523be1..85f62b6b 100644 --- a/README-CN.md +++ b/README-CN.md @@ -3,34 +3,43 @@ [![Python](https://img.shields.io/badge/python-3.6.2-blue.svg?style=flat-square)](https://www.python.org/downloads/release/python-362/) [![Django](https://img.shields.io/badge/django-1.11.4-blue.svg?style=flat-square)](https://www.djangoproject.com/) [![Django Rest Framework](https://img.shields.io/badge/django_rest_framework-3.4.0-blue.svg?style=flat-square)](http://www.django-rest-framework.org/) -[![Build Status](https://travis-ci.org/QingdaoU/OnlineJudge.svg?branch=master)](https://travis-ci.org/QingdaoU/OnlineJudge) +[![Build Status](https://travis-ci.org/Harry-zklcdc/OnlineJudge.svg?branch=master)](https://travis-ci.org/Harry-zklcdc/OnlineJudge) -> #### 基于 Python 和 Vue 的在线评测系统。 [Demo](https://qduoj.com) +> #### 基于 Python 和 Vue 的在线评测系统。 [Demo](https://oj.yangzheng.com.cn/) [English Document](README.md) + + ## 概览 -+ 基于 Docker,真正一键部署 -+ 前后端分离,模块化编程,微服务 -+ ACM/OI 两种比赛模式、实时/非实时评判 任意选择 -+ 丰富的可视化图表,一图胜千言 -+ 支持 Template Problem,可以添加函数题甚至填空题 -+ 更细致的权限划分,超级管理员和普通管理员各司其职 -+ 多语言支持:`C`, `C++`, `Java`, `Python2`, `Python3`,题目可以选择使用的语言 -+ Markdown & MathJax 支持 -+ 比赛用户IP限制 (CIDR ranges) +- 基于 Docker,真正一键部署 +- 前后端分离,模块化编程,微服务 +- ACM/OI 两种比赛模式、实时/非实时评判 任意选择 +- 丰富的可视化图表,一图胜千言 +- 支持 Template Problem,可以添加函数题甚至填空题 +- 更细致的权限划分,超级管理员和普通管理员各司其职 +- 多语言支持:`C`, `C++`, `Java`, `Python2`, `Python3`,题目可以选择使用的语言 +- Markdown & MathJax 支持 +- 比赛用户IP限制 (CIDR ranges) +- 在线 IDE +- 自定义关于我们介绍页面 +- 换肤功能 +- 签到系统 +- 经验系统 +- 称号系统 +- 标准输入输出 / 文件输入输出 主要模块均已开源: -+ 后端(Django): [https://github.com/QingdaoU/OnlineJudge](https://github.com/QingdaoU/OnlineJudge) -+ 前端(Vue): [https://github.com/QingdaoU/OnlineJudgeFE](https://github.com/QingdaoU/OnlineJudgeFE) -+ 判题沙箱(Seccomp): [https://github.com/QingdaoU/Judger](https://github.com/QingdaoU/Judger) -+ 判题服务器(对Judger的封装): [https://github.com/QingdaoU/JudgeServer](https://github.com/QingdaoU/JudgeServer) +- 后端(Django): [https://github.com/Harry-zklcdc/OnlineJudge](https://github.com/Harry-zklcdc/OnlineJudge) +- 前端(Vue): [https://github.com/Harry-zklcdc/OJ](https://github.com/Harry-zklcdc/OJ) +- 判题沙箱(Seccomp): [https://github.com/Harry-zklcdc/Judger](https://github.com/Harry-zklcdc/Judger) +- 判题服务器(对Judger的封装): [https://github.com/Harry-zklcdc/JudgeServer](https://github.com/Harry-zklcdc/JudgeServe) ## 安装 -请根据此进行安装: [https://github.com/QingdaoU/OnlineJudgeDeploy/tree/2.0](https://github.com/QingdaoU/OnlineJudgeDeploy/tree/2.0) +请根据此进行安装: [https://github.com/Harry-zklcdc/OnlineJudgeDeploy/tree/2.0](https://github.com/Harry-zklcdc/OnlineJudgeDeploy/tree/2.0) ## 文档 @@ -40,23 +49,33 @@ ### OJ前台 -![problem-list](https://user-images.githubusercontent.com/20637881/33372506-402022e4-d539-11e7-8e64-6656f8ceb75a.png) +![home page](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E9%A6%96%E9%A1%B5.jpg) + +![announcement](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E5%85%AC%E5%91%8A.jpg) + +![problem-list](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E9%A2%98%E7%9B%AE-%E5%8E%9F%E8%B0%85%E7%BB%BF.jpg) ![problem-details](https://user-images.githubusercontent.com/20637881/33372507-4061a782-d539-11e7-8835-076ddae6b529.png) ![statistic-info](https://user-images.githubusercontent.com/20637881/33372508-40a0c6ce-d539-11e7-8d5e-024541b76750.png) -![contest-list](https://user-images.githubusercontent.com/20637881/33372509-40d880dc-d539-11e7-9eba-1f08dcb6b9a0.png) +![online-IDE](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E5%9C%A8%E7%BA%BFIDE.jpg) + +![FAQ](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98-%E5%B0%91%E5%A5%B3%E7%B2%89.jpg) + +![contest-list](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E6%AF%94%E8%B5%9B-%E5%9F%BA%E4%BD%AC%E7%B4%AB.jpg) Rankings 中可以控制图表和菜单的显隐。 -![acm-rankings](https://user-images.githubusercontent.com/20637881/33372510-41117f68-d539-11e7-9947-70e60bad3cf2.png) -![oi-rankings](https://user-images.githubusercontent.com/20637881/33372511-41d406fa-d539-11e7-9947-7a2a088785b0.png) -![status](https://user-images.githubusercontent.com/20637881/33372512-420ba240-d539-11e7-8645-594cac4a0b78.png) +![acm-rankings](https://user-images.githubusercontent.com/20637881/33372510-41117f68-d539-11e7-9947-70e60bad3cf2.png) + +![oi-rankings](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E6%8E%92%E5%90%8D.jpg) + +![status](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/状态.jpg) ![status-details](https://user-images.githubusercontent.com/20637881/33365523-787bd0ea-d523-11e7-953f-dacbf7a506df.png) -![user-home](https://user-images.githubusercontent.com/20637881/33365521-7842d808-d523-11e7-84c1-2e2aa0079f32.png) +![user-home](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/个人中心.jpg) ### 后台管理 @@ -68,18 +87,18 @@ Rankings 中可以控制图表和菜单的显隐。 ![create-contest](https://user-images.githubusercontent.com/20637881/33372514-428ab922-d539-11e7-8f68-da55dedf3ad3.png) - ## 浏览器支持 Modern browsers(chrome, firefox) 和 Internet Explorer 10+. ## 特别感谢 -+ 所有为本项目做出贡献的人 -+ [heb1c](https://github.com/hebicheng) 同学为我们提供了很多意见和建议 +- 所有为本项目做出贡献的人 +- [heb1c](https://github.com/hebicheng) 同学为我们提供了很多意见和建议 如果您觉得这个项目还不错,就star一下吧 :) ## 许可 The [MIT](http://opensource.org/licenses/MIT) License + diff --git a/README.md b/README.md index 87b07e76..72a8ba79 100644 --- a/README.md +++ b/README.md @@ -3,34 +3,43 @@ [![Python](https://img.shields.io/badge/python-3.6.2-blue.svg?style=flat-square)](https://www.python.org/downloads/release/python-362/) [![Django](https://img.shields.io/badge/django-1.11.4-blue.svg?style=flat-square)](https://www.djangoproject.com/) [![Django Rest Framework](https://img.shields.io/badge/django_rest_framework-3.4.0-blue.svg?style=flat-square)](http://www.django-rest-framework.org/) -[![Build Status](https://travis-ci.org/QingdaoU/OnlineJudge.svg?branch=master)](https://travis-ci.org/QingdaoU/OnlineJudge) +[![Build Status](https://travis-ci.org/Harry-zklcdc/OnlineJudge.svg?branch=master)](https://travis-ci.org/Harry-zklcdc/OnlineJudge) -> #### An onlinejudge system based on Python and Vue. [Demo](https://qduoj.com) +> +> +> #### An onlinejudge system based on Python and Vue. [Demo](https://oj.yangzheng.com.cn/) [中文文档](README-CN.md) ## Overview -+ Based on Docker; One-click deployment -+ Separated backend and frontend; Modular programming; Micro service -+ ACM/OI rule support; realtime/non-realtime rank support -+ Amazing charting and visualization -+ Template-problem support -+ More reasonable permission control -+ Multi-language support: `C`, `C++`, `Java`, `Python2`, `Python3` -+ Markdown & MathJax support -+ Contest participants IP limit(CIDR) +- Based on Docker; One-click deployment +- Separated backend and frontend; Modular programming; Micro service +- ACM/OI rule support; realtime/non-realtime rank support +- Amazing charting and visualization +- Template-problem support +- More reasonable permission control +- Multi-language support: `C`, `C++`, `Java`, `Python2`, `Python3` +- Markdown & MathJax support +- Contest participants IP limit(CIDR) +- Online IDE +- DIY About Us Page +- Optional Theme +- Sighin System +- Experience system +- Title System +- Standard IO/File IO Main modules are available below: -+ Backend(Django): [https://github.com/QingdaoU/OnlineJudge](https://github.com/QingdaoU/OnlineJudge) -+ Frontend(Vue): [https://github.com/QingdaoU/OnlineJudgeFE](https://github.com/QingdaoU/OnlineJudgeFE) -+ Judger Sandbox(Seccomp): [https://github.com/QingdaoU/Judger](https://github.com/QingdaoU/Judger) -+ JudgeServer(A wrapper for Judger): [https://github.com/QingdaoU/JudgeServer](https://github.com/QingdaoU/JudgeServer) +- Backend(Django): [https://github.com/Harry-zklcdc/OnlineJudge](https://github.com/Harry-zklcdc/OnlineJudge) +- Frontend(Vue): [https://github.com/Harry-zklcdc/OJ](https://github.com/Harry-zklcdc/OJ) +- Judger Sandbox(Seccomp): [https://github.com/Harry-zklcdc/Judger](https://github.com/Harry-zklcdc/Judger) +- JudgeServer(A wrapper for Judger): [https://github.com/Harry-zklcdc/JudgeServer](https://github.com/Harry-zklcdc/JudgeServer) ## Installation -Follow me: [https://github.com/QingdaoU/OnlineJudgeDeploy/tree/2.0](https://github.com/QingdaoU/OnlineJudgeDeploy/tree/2.0) +Follow me: [https://github.com/Harry-zklcdc/OnlineJudgeDeploy/tree/2.0](https://github.com/Harry-zklcdc/OnlineJudgeDeploy/tree/2.0) ## Documents @@ -40,27 +49,35 @@ Follow me: [https://github.com/QingdaoU/OnlineJudgeDeploy/tree/2.0](https://git ### Frontend: -![problem-list](https://user-images.githubusercontent.com/20637881/33372506-402022e4-d539-11e7-8e64-6656f8ceb75a.png) +![home page](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E9%A6%96%E9%A1%B5.jpg) + +![announcement](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E5%85%AC%E5%91%8A.jpg) + +![problem-list](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E9%A2%98%E7%9B%AE-%E5%8E%9F%E8%B0%85%E7%BB%BF.jpg) ![problem-details](https://user-images.githubusercontent.com/20637881/33372507-4061a782-d539-11e7-8835-076ddae6b529.png) ![statistic-info](https://user-images.githubusercontent.com/20637881/33372508-40a0c6ce-d539-11e7-8d5e-024541b76750.png) -![contest-list](https://user-images.githubusercontent.com/20637881/33372509-40d880dc-d539-11e7-9eba-1f08dcb6b9a0.png) +![online-IDE](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E5%9C%A8%E7%BA%BFIDE.jpg) + +![FAQ](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98-%E5%B0%91%E5%A5%B3%E7%B2%89.jpg) + +![contest-list](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E6%AF%94%E8%B5%9B-%E5%9F%BA%E4%BD%AC%E7%B4%AB.jpg) You can control the menu and chart status in rankings. ![acm-rankings](https://user-images.githubusercontent.com/20637881/33372510-41117f68-d539-11e7-9947-70e60bad3cf2.png) -![oi-rankings](https://user-images.githubusercontent.com/20637881/33372511-41d406fa-d539-11e7-9947-7a2a088785b0.png) +![oi-rankings](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/%E6%8E%92%E5%90%8D.jpg) -![status](https://user-images.githubusercontent.com/20637881/33372512-420ba240-d539-11e7-8645-594cac4a0b78.png) +![status](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/状态.jpg) ![status-details](https://user-images.githubusercontent.com/20637881/33365523-787bd0ea-d523-11e7-953f-dacbf7a506df.png) -![user-home](https://user-images.githubusercontent.com/20637881/33365521-7842d808-d523-11e7-84c1-2e2aa0079f32.png) +![user-home](https://raw.githubusercontent.com/Harry-zklcdc/docs/master/image/个人中心.jpg) -### Admin: +### Admin: ![admin-users](https://user-images.githubusercontent.com/20637881/33372516-42c34fda-d539-11e7-9f4e-5109477f83be.png) @@ -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)** + diff --git a/account/migrations/0013_auto_20200419_1414.py b/account/migrations/0013_auto_20200419_1414.py new file mode 100644 index 00000000..4040f5cb --- /dev/null +++ b/account/migrations/0013_auto_20200419_1414.py @@ -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), + ), + ] diff --git a/account/migrations/0014_auto_20200420_1020.py b/account/migrations/0014_auto_20200420_1020.py new file mode 100644 index 00000000..d5e6ce32 --- /dev/null +++ b/account/migrations/0014_auto_20200420_1020.py @@ -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), + ), + ] diff --git a/account/migrations/0015_auto_20200421_1715.py b/account/migrations/0015_auto_20200421_1715.py new file mode 100644 index 00000000..2f75ce85 --- /dev/null +++ b/account/migrations/0015_auto_20200421_1715.py @@ -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'), + ), + ] diff --git a/account/migrations/0016_auto_20200802_1040.py b/account/migrations/0016_auto_20200802_1040.py new file mode 100644 index 00000000..9a44c898 --- /dev/null +++ b/account/migrations/0016_auto_20200802_1040.py @@ -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), + ), + ] diff --git a/account/models.py b/account/models.py index afd644ed..aacbe407 100644 --- a/account/models.py +++ b/account/models.py @@ -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" diff --git a/account/serializers.py b/account/serializers.py index faec66e2..7328dbd4 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -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): diff --git a/account/tests.py b/account/tests.py index 4941eb27..c727f03a 100644 --- a/account/tests.py +++ b/account/tests.py @@ -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.") diff --git a/account/urls/oj.py b/account/urls/oj.py index a2cebe02..af116741 100644 --- a/account/urls/oj.py +++ b/account/urls/oj.py @@ -5,7 +5,7 @@ from ..views.oj import (ApplyResetPasswordAPI, ResetPasswordAPI, UserLoginAPI, UserLogoutAPI, UsernameOrEmailCheck, AvatarUploadAPI, TwoFactorAuthAPI, UserProfileAPI, UserRankAPI, CheckTFARequiredAPI, SessionManagementAPI, - ProfileProblemDisplayIDRefreshAPI, 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") ] diff --git a/account/views/admin.py b/account/views/admin.py index 581a607e..099f0446 100644 --- a/account/views/admin.py +++ b/account/views/admin.py @@ -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 diff --git a/account/views/oj.py b/account/views/oj.py index 2d26e484..977e2270 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -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}) diff --git a/announcement/migrations/0001_initial.py b/announcement/migrations/0001_initial.py index cd92e223..8879e8b2 100644 --- a/announcement/migrations/0001_initial.py +++ b/announcement/migrations/0001_initial.py @@ -34,3 +34,4 @@ class Migration(migrations.Migration): }, ), ] + diff --git a/announcement/migrations/0004_auto_20190430_2118.py b/announcement/migrations/0004_auto_20190430_2118.py new file mode 100644 index 00000000..1f8a1b50 --- /dev/null +++ b/announcement/migrations/0004_auto_20190430_2118.py @@ -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', + }, + ), + ] diff --git a/announcement/models.py b/announcement/models.py index 441c4c18..c93e93f4 100644 --- a/announcement/models.py +++ b/announcement/models.py @@ -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" diff --git a/announcement/serializers.py b/announcement/serializers.py index 1edd1d9c..84e103b5 100644 --- a/announcement/serializers.py +++ b/announcement/serializers.py @@ -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) diff --git a/announcement/urls/admin.py b/announcement/urls/admin.py index 09673e63..26d80b6c 100644 --- a/announcement/urls/admin.py +++ b/announcement/urls/admin.py @@ -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"), ] diff --git a/announcement/urls/oj.py b/announcement/urls/oj.py index 67178b01..d04b39d2 100644 --- a/announcement/urls/oj.py +++ b/announcement/urls/oj.py @@ -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"), ] diff --git a/announcement/views/admin.py b/announcement/views/admin.py index 58d35780..fad8b68d 100644 --- a/announcement/views/admin.py +++ b/announcement/views/admin.py @@ -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) diff --git a/announcement/views/oj.py b/announcement/views/oj.py index 1176c368..0470a9e4 100644 --- a/announcement/views/oj.py +++ b/announcement/views/oj.py @@ -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") diff --git a/conf/serializers.py b/conf/serializers.py index e50dfa21..dbf6da9a 100644 --- a/conf/serializers.py +++ b/conf/serializers.py @@ -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): diff --git a/conf/tests.py b/conf/tests.py index dce80c68..bff32e98 100644 --- a/conf/tests.py +++ b/conf/tests.py @@ -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": "test", - "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": "", - "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, "") diff --git a/conf/urls/admin.py b/conf/urls/admin.py index 8e6d293f..9355bedb 100644 --- a/conf/urls/admin.py +++ b/conf/urls/admin.py @@ -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") ] diff --git a/conf/views.py b/conf/views.py index be737442..26e2f2b6 100644 --- a/conf/views.py +++ b/conf/views.py @@ -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): diff --git a/contest/views/admin.py b/contest/views/admin.py index 66addb15..329c5b1b 100644 --- a/contest/views/admin.py +++ b/contest/views/admin.py @@ -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} - submissions = Submission.objects.filter(contest=contest, result=JudgeStatus.ACCEPTED).order_by("-create_time") + 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 diff --git a/contest/views/oj.py b/contest/views/oj.py index 4164ec33..e8acb6bc 100644 --- a/contest/views/oj.py +++ b/contest/views/oj.py @@ -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) diff --git a/deploy/entrypoint.sh b/deploy/entrypoint.sh index 627c75a5..af4c90f0 100755 --- a/deploy/entrypoint.sh +++ b/deploy/entrypoint.sh @@ -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 diff --git a/deploy/nginx/fastcgi.conf b/deploy/nginx/fastcgi.conf new file mode 100644 index 00000000..7c8a3645 --- /dev/null +++ b/deploy/nginx/fastcgi.conf @@ -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; \ No newline at end of file diff --git a/deploy/nginx/locations.conf b/deploy/nginx/locations.conf index da7fa38b..6b428552 100644 --- a/deploy/nginx/locations.conf +++ b/deploy/nginx/locations.conf @@ -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; +} \ No newline at end of file diff --git a/deploy/nginx/nginx.conf b/deploy/nginx/nginx.conf index 81ff345b..c4d57ed0 100644 --- a/deploy/nginx/nginx.conf +++ b/deploy/nginx/nginx.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; diff --git a/deploy/requirements.txt b/deploy/requirements.txt index b0c6fe9b..2ee85d50 100644 --- a/deploy/requirements.txt +++ b/deploy/requirements.txt @@ -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,10 +23,10 @@ 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 -django-dbconn-retry==0.1.5 +django-dbconn-retry==0.1.5 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..9f22af9c --- /dev/null +++ b/docker-compose.yml @@ -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" + diff --git a/docs/data.json b/docs/data.json index ab1d8e3b..19283acb 100644 --- a/docs/data.json +++ b/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” 上移" ] }, { diff --git a/forum/__init__.py b/forum/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/forum/migrations/0001_initial.py b/forum/migrations/0001_initial.py new file mode 100644 index 00000000..f19555ce --- /dev/null +++ b/forum/migrations/0001_initial.py @@ -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',), + }, + ), + ] diff --git a/forum/migrations/0002_auto_20200718_2324.py b/forum/migrations/0002_auto_20200718_2324.py new file mode 100644 index 00000000..13f350ff --- /dev/null +++ b/forum/migrations/0002_auto_20200718_2324.py @@ -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',)}, + ), + ] diff --git a/forum/migrations/0003_remove_forumreply_is_top.py b/forum/migrations/0003_remove_forumreply_is_top.py new file mode 100644 index 00000000..7584ca45 --- /dev/null +++ b/forum/migrations/0003_remove_forumreply_is_top.py @@ -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', + ), + ] diff --git a/forum/migrations/0004_remove_forumpost_son_sort.py b/forum/migrations/0004_remove_forumpost_son_sort.py new file mode 100644 index 00000000..6a1b1cb5 --- /dev/null +++ b/forum/migrations/0004_remove_forumpost_son_sort.py @@ -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', + ), + ] diff --git a/forum/migrations/__init__.py b/forum/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/forum/models.py b/forum/models.py new file mode 100644 index 00000000..926bb89a --- /dev/null +++ b/forum/models.py @@ -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",) diff --git a/forum/serializers.py b/forum/serializers.py new file mode 100644 index 00000000..cd852e9d --- /dev/null +++ b/forum/serializers.py @@ -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__" diff --git a/forum/templates/reply_email.html b/forum/templates/reply_email.html new file mode 100644 index 00000000..de342b6f --- /dev/null +++ b/forum/templates/reply_email.html @@ -0,0 +1,28 @@ +
+ + + + + + + + + + +
+ {{ website_name }}
+
+

Hello, {{ username }}:

+

+ Your Post 《{{ title }}》 recepted a reply: +

+

+ Please click the {{ link }} to see more. +

+

+

{{ website_name }}

+
+
+
\ No newline at end of file diff --git a/forum/tests.py b/forum/tests.py new file mode 100644 index 00000000..e69de29b diff --git a/forum/urls/oj.py b/forum/urls/oj.py new file mode 100644 index 00000000..eab01cd2 --- /dev/null +++ b/forum/urls/oj.py @@ -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"), +] diff --git a/forum/views/oj.py b/forum/views/oj.py new file mode 100644 index 00000000..dee3d4c1 --- /dev/null +++ b/forum/views/oj.py @@ -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() diff --git a/init_db.sh b/init_db.sh index 6fc29b06..704e6ec9 100755 --- a/init_db.sh +++ b/init_db.sh @@ -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 -fi \ No newline at end of file + python3.7 manage.py migrate + python3.7 manage.py inituser --username root --password rootroot --action create_super_admin +fi diff --git a/judge/dispatcher.py b/judge/dispatcher.py index bd099ac2..24b8af00 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -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"]) diff --git a/judge/ide.py b/judge/ide.py new file mode 100644 index 00000000..b0ad1567 --- /dev/null +++ b/judge/ide.py @@ -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 diff --git a/judge/languages.py b/judge/languages.py index 8c590e51..bd2698e7 100644 --- a/judge/languages.py +++ b/judge/languages.py @@ -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 +//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 +//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 +//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 +//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 +//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 +//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 +//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 +//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"} ] diff --git a/judge/tasks.py b/judge/tasks.py index 8a1794a1..ad9aa298 100644 --- a/judge/tasks.py +++ b/judge/tasks.py @@ -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() diff --git a/oj/settings.py b/oj/settings.py index c1141a02..46de6aae 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -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 diff --git a/oj/urls.py b/oj/urls.py index c626656b..ae38fe8e 100644 --- a/oj/urls.py +++ b/oj/urls.py @@ -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")), ] diff --git a/options/options.py b/options/options.py index f1843fd3..0654b91a 100644 --- a/options/options.py +++ b/options/options.py @@ -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 diff --git a/problem/migrations/0015_auto_20200419_0613.py b/problem/migrations/0015_auto_20200419_0613.py new file mode 100644 index 00000000..5d60a224 --- /dev/null +++ b/problem/migrations/0015_auto_20200419_0613.py @@ -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',)}, + ), + ] diff --git a/problem/models.py b/problem/models.py index c16aeada..e396fd3d 100644 --- a/problem/models.py +++ b/problem/models.py @@ -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 diff --git a/run_test.py b/run_test.py old mode 100644 new mode 100755 diff --git a/submission/urls/oj.py b/submission/urls/oj.py index 49116b9f..4005a1d4 100644 --- a/submission/urls/oj.py +++ b/submission/urls/oj.py @@ -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"), ] diff --git a/submission/views/oj.py b/submission/views/oj.py index 0f7c4a58..2674cf91 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -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 diff --git a/utils/constants.py b/utils/constants.py index 749b20da..68295af4 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -19,6 +19,7 @@ class ContestStatus: class ContestRuleType(Choices): ACM = "ACM" OI = "OI" + experience = "experience" class CacheKey: