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 @@
[](https://www.python.org/downloads/release/python-362/)
[](https://www.djangoproject.com/)
[](http://www.django-rest-framework.org/)
-[](https://travis-ci.org/QingdaoU/OnlineJudge)
+[](https://travis-ci.org/Harry-zklcdc/OnlineJudge)
-> #### 基于 Python 和 Vue 的在线评测系统。 [Demo](https://qduoj.com)
+> #### 基于 Python 和 Vue 的在线评测系统。 [Demo](https://oj.yangzheng.com.cn/)
[English Document](README.md)
+
+
## 概览
-+ 基于 Docker,真正一键部署
-+ 前后端分离,模块化编程,微服务
-+ ACM/OI 两种比赛模式、实时/非实时评判 任意选择
-+ 丰富的可视化图表,一图胜千言
-+ 支持 Template Problem,可以添加函数题甚至填空题
-+ 更细致的权限划分,超级管理员和普通管理员各司其职
-+ 多语言支持:`C`, `C++`, `Java`, `Python2`, `Python3`,题目可以选择使用的语言
-+ Markdown & MathJax 支持
-+ 比赛用户IP限制 (CIDR ranges)
+- 基于 Docker,真正一键部署
+- 前后端分离,模块化编程,微服务
+- ACM/OI 两种比赛模式、实时/非实时评判 任意选择
+- 丰富的可视化图表,一图胜千言
+- 支持 Template Problem,可以添加函数题甚至填空题
+- 更细致的权限划分,超级管理员和普通管理员各司其职
+- 多语言支持:`C`, `C++`, `Java`, `Python2`, `Python3`,题目可以选择使用的语言
+- Markdown & MathJax 支持
+- 比赛用户IP限制 (CIDR ranges)
+- 在线 IDE
+- 自定义关于我们介绍页面
+- 换肤功能
+- 签到系统
+- 经验系统
+- 称号系统
+- 标准输入输出 / 文件输入输出
主要模块均已开源:
-+ 后端(Django): [https://github.com/QingdaoU/OnlineJudge](https://github.com/QingdaoU/OnlineJudge)
-+ 前端(Vue): [https://github.com/QingdaoU/OnlineJudgeFE](https://github.com/QingdaoU/OnlineJudgeFE)
-+ 判题沙箱(Seccomp): [https://github.com/QingdaoU/Judger](https://github.com/QingdaoU/Judger)
-+ 判题服务器(对Judger的封装): [https://github.com/QingdaoU/JudgeServer](https://github.com/QingdaoU/JudgeServer)
+- 后端(Django): [https://github.com/Harry-zklcdc/OnlineJudge](https://github.com/Harry-zklcdc/OnlineJudge)
+- 前端(Vue): [https://github.com/Harry-zklcdc/OJ](https://github.com/Harry-zklcdc/OJ)
+- 判题沙箱(Seccomp): [https://github.com/Harry-zklcdc/Judger](https://github.com/Harry-zklcdc/Judger)
+- 判题服务器(对Judger的封装): [https://github.com/Harry-zklcdc/JudgeServer](https://github.com/Harry-zklcdc/JudgeServe)
## 安装
-请根据此进行安装: [https://github.com/QingdaoU/OnlineJudgeDeploy/tree/2.0](https://github.com/QingdaoU/OnlineJudgeDeploy/tree/2.0)
+请根据此进行安装: [https://github.com/Harry-zklcdc/OnlineJudgeDeploy/tree/2.0](https://github.com/Harry-zklcdc/OnlineJudgeDeploy/tree/2.0)
## 文档
@@ -40,23 +49,33 @@
### OJ前台
-
+
+
+
+
+


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

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

-
## 浏览器支持
Modern browsers(chrome, firefox) 和 Internet Explorer 10+.
## 特别感谢
-+ 所有为本项目做出贡献的人
-+ [heb1c](https://github.com/hebicheng) 同学为我们提供了很多意见和建议
+- 所有为本项目做出贡献的人
+- [heb1c](https://github.com/hebicheng) 同学为我们提供了很多意见和建议
如果您觉得这个项目还不错,就star一下吧 :)
## 许可
The [MIT](http://opensource.org/licenses/MIT) License
+
diff --git a/README.md b/README.md
index 87b07e76..72a8ba79 100644
--- a/README.md
+++ b/README.md
@@ -3,34 +3,43 @@
[](https://www.python.org/downloads/release/python-362/)
[](https://www.djangoproject.com/)
[](http://www.django-rest-framework.org/)
-[](https://travis-ci.org/QingdaoU/OnlineJudge)
+[](https://travis-ci.org/Harry-zklcdc/OnlineJudge)
-> #### An onlinejudge system based on Python and Vue. [Demo](https://qduoj.com)
+>
+>
+> #### An onlinejudge system based on Python and Vue. [Demo](https://oj.yangzheng.com.cn/)
[中文文档](README-CN.md)
## Overview
-+ Based on Docker; One-click deployment
-+ Separated backend and frontend; Modular programming; Micro service
-+ ACM/OI rule support; realtime/non-realtime rank support
-+ Amazing charting and visualization
-+ Template-problem support
-+ More reasonable permission control
-+ Multi-language support: `C`, `C++`, `Java`, `Python2`, `Python3`
-+ Markdown & MathJax support
-+ Contest participants IP limit(CIDR)
+- Based on Docker; One-click deployment
+- Separated backend and frontend; Modular programming; Micro service
+- ACM/OI rule support; realtime/non-realtime rank support
+- Amazing charting and visualization
+- Template-problem support
+- More reasonable permission control
+- Multi-language support: `C`, `C++`, `Java`, `Python2`, `Python3`
+- Markdown & MathJax support
+- Contest participants IP limit(CIDR)
+- Online IDE
+- DIY About Us Page
+- Optional Theme
+- Sighin System
+- Experience system
+- Title System
+- Standard IO/File IO
Main modules are available below:
-+ Backend(Django): [https://github.com/QingdaoU/OnlineJudge](https://github.com/QingdaoU/OnlineJudge)
-+ Frontend(Vue): [https://github.com/QingdaoU/OnlineJudgeFE](https://github.com/QingdaoU/OnlineJudgeFE)
-+ Judger Sandbox(Seccomp): [https://github.com/QingdaoU/Judger](https://github.com/QingdaoU/Judger)
-+ JudgeServer(A wrapper for Judger): [https://github.com/QingdaoU/JudgeServer](https://github.com/QingdaoU/JudgeServer)
+- Backend(Django): [https://github.com/Harry-zklcdc/OnlineJudge](https://github.com/Harry-zklcdc/OnlineJudge)
+- Frontend(Vue): [https://github.com/Harry-zklcdc/OJ](https://github.com/Harry-zklcdc/OJ)
+- Judger Sandbox(Seccomp): [https://github.com/Harry-zklcdc/Judger](https://github.com/Harry-zklcdc/Judger)
+- JudgeServer(A wrapper for Judger): [https://github.com/Harry-zklcdc/JudgeServer](https://github.com/Harry-zklcdc/JudgeServer)
## Installation
-Follow me: [https://github.com/QingdaoU/OnlineJudgeDeploy/tree/2.0](https://github.com/QingdaoU/OnlineJudgeDeploy/tree/2.0)
+Follow me: [https://github.com/Harry-zklcdc/OnlineJudgeDeploy/tree/2.0](https://github.com/Harry-zklcdc/OnlineJudgeDeploy/tree/2.0)
## Documents
@@ -40,27 +49,35 @@ Follow me: [https://github.com/QingdaoU/OnlineJudgeDeploy/tree/2.0](https://git
### Frontend:
-
+
+
+
+
+


-
+
+
+
+
+
You can control the menu and chart status in rankings.

-
+
-
+

-
+
-### Admin:
+### Admin:

@@ -76,10 +93,11 @@ Modern browsers(chrome, firefox) and Internet Explorer 10+.
## Thanks
-+ I'd appreciate a star if you find this helpful.
-+ Thanks to everyone that contributes to this project.
-+ Special thanks to [heb1c](https://github.com/hebicheng), who has given us a lot of suggestions.
+- I'd appreciate a star if you find this helpful.
+- Thanks to everyone that contributes to this project.
+- Special thanks to [heb1c](https://github.com/hebicheng), who has given us a lot of suggestions.
## License
-[MIT](http://opensource.org/licenses/MIT)
+[MIT](http://opensource.org/licenses/MIT)**
+
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 @@
+