change enum

This commit is contained in:
2026-05-09 02:30:47 -06:00
parent 78158471b2
commit c466dfd3c6
23 changed files with 451 additions and 503 deletions

View File

@@ -0,0 +1,22 @@
# Generated by Django 6.0.4 on 2026-05-09 08:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("account", "0003_remove_userprofile_class_name_user_class_name"),
]
operations = [
migrations.AlterField(
model_name="user",
name="admin_type",
field=models.TextField(choices=[("Regular User", "Regular User"), ("Admin", "Admin"), ("Super Admin", "Super Admin")], default="Regular User"),
),
migrations.AlterField(
model_name="user",
name="problem_permission",
field=models.TextField(choices=[("None", "None"), ("Own", "Own"), ("All", "All")], default="None"),
),
]

View File

@@ -5,16 +5,16 @@ from django.db import models
from utils.models import JSONField from utils.models import JSONField
class AdminType(object): class AdminType(models.TextChoices):
REGULAR_USER = "Regular User" REGULAR_USER = "Regular User", "Regular User"
ADMIN = "Admin" ADMIN = "Admin", "Admin"
SUPER_ADMIN = "Super Admin" SUPER_ADMIN = "Super Admin", "Super Admin"
class ProblemPermission(object): class ProblemPermission(models.TextChoices):
NONE = "None" NONE = "None", "None"
OWN = "Own" OWN = "Own", "Own"
ALL = "All" ALL = "All", "All"
class UserManager(models.Manager): class UserManager(models.Manager):
@@ -30,8 +30,8 @@ class User(AbstractBaseUser):
email = models.TextField(null=True) email = models.TextField(null=True)
create_time = models.DateTimeField(auto_now_add=True, null=True) create_time = models.DateTimeField(auto_now_add=True, null=True)
# One of UserType # One of UserType
admin_type = models.TextField(default=AdminType.REGULAR_USER) admin_type = models.TextField(default=AdminType.REGULAR_USER, choices=AdminType.choices)
problem_permission = models.TextField(default=ProblemPermission.NONE) problem_permission = models.TextField(default=ProblemPermission.NONE, choices=ProblemPermission.choices)
reset_password_token = models.TextField(null=True) reset_password_token = models.TextField(null=True)
reset_password_token_expire_time = models.DateTimeField(null=True) reset_password_token_expire_time = models.DateTimeField(null=True)
# SSO auth token # SSO auth token
@@ -43,9 +43,7 @@ class User(AbstractBaseUser):
open_api = models.BooleanField(default=False) open_api = models.BooleanField(default=False)
open_api_appkey = models.TextField(null=True) open_api_appkey = models.TextField(null=True)
is_disabled = models.BooleanField(default=False) is_disabled = models.BooleanField(default=False)
raw_password = models.CharField( raw_password = models.CharField(max_length=20, null=True, blank=True, verbose_name="明文密码")
max_length=20, null=True, blank=True, verbose_name="明文密码"
)
USERNAME_FIELD = "username" USERNAME_FIELD = "username"
REQUIRED_FIELDS = [] REQUIRED_FIELDS = []
@@ -68,9 +66,7 @@ class User(AbstractBaseUser):
return self.problem_permission == ProblemPermission.ALL return self.problem_permission == ProblemPermission.ALL
def is_contest_admin(self, contest): def is_contest_admin(self, contest):
return self.is_authenticated and ( return self.is_authenticated and (contest.created_by == self or self.admin_type == AdminType.SUPER_ADMIN)
contest.created_by == self or self.admin_type == AdminType.SUPER_ADMIN
)
def set_password(self, raw_password): def set_password(self, raw_password):
super().set_password(raw_password) super().set_password(raw_password)

View File

@@ -44,9 +44,7 @@ class GenerateUserSerializer(serializers.Serializer):
class ImportUserSerializer(serializers.Serializer): class ImportUserSerializer(serializers.Serializer):
users = serializers.ListField( users = serializers.ListField(child=serializers.ListField(child=serializers.CharField(max_length=64)))
child=serializers.ListField(child=serializers.CharField(max_length=64))
)
class UserAdminSerializer(serializers.ModelSerializer): class UserAdminSerializer(serializers.ModelSerializer):
@@ -118,21 +116,16 @@ class EditUserSerializer(serializers.Serializer):
id = serializers.IntegerField() id = serializers.IntegerField()
username = serializers.CharField(max_length=32) username = serializers.CharField(max_length=32)
real_name = serializers.CharField(max_length=32, allow_blank=True, allow_null=True) real_name = serializers.CharField(max_length=32, allow_blank=True, allow_null=True)
password = serializers.CharField( password = serializers.CharField(min_length=6, allow_blank=True, required=False, default=None)
min_length=6, allow_blank=True, required=False, default=None
)
email = serializers.EmailField(max_length=64) email = serializers.EmailField(max_length=64)
admin_type = serializers.ChoiceField( admin_type = serializers.ChoiceField(choices=AdminType.choices)
choices=(AdminType.REGULAR_USER, AdminType.ADMIN, AdminType.SUPER_ADMIN) problem_permission = serializers.ChoiceField(choices=ProblemPermission.choices)
)
problem_permission = serializers.ChoiceField(
choices=(ProblemPermission.NONE, ProblemPermission.OWN, ProblemPermission.ALL)
)
open_api = serializers.BooleanField() open_api = serializers.BooleanField()
two_factor_auth = serializers.BooleanField() two_factor_auth = serializers.BooleanField()
is_disabled = serializers.BooleanField() is_disabled = serializers.BooleanField()
class_name = serializers.CharField(required=False, allow_null=True, allow_blank=True) class_name = serializers.CharField(required=False, allow_null=True, allow_blank=True)
class EditUserProfileSerializer(serializers.Serializer): class EditUserProfileSerializer(serializers.Serializer):
real_name = serializers.CharField(max_length=32, allow_null=True, required=False) real_name = serializers.CharField(max_length=32, allow_null=True, required=False)
avatar = serializers.CharField(max_length=256, allow_blank=True, required=False) avatar = serializers.CharField(max_length=256, allow_blank=True, required=False)
@@ -143,6 +136,7 @@ class EditUserProfileSerializer(serializers.Serializer):
major = serializers.CharField(max_length=64, allow_blank=True, required=False) major = serializers.CharField(max_length=64, allow_blank=True, required=False)
language = serializers.CharField(max_length=32, allow_blank=True, required=False) language = serializers.CharField(max_length=32, allow_blank=True, required=False)
class ApplyResetPasswordSerializer(serializers.Serializer): class ApplyResetPasswordSerializer(serializers.Serializer):
email = serializers.EmailField() email = serializers.EmailField()
captcha = serializers.CharField() captcha = serializers.CharField()

View File

@@ -78,9 +78,7 @@ class UserProfileAPI(APIView):
show_real_name = True show_real_name = True
except User.DoesNotExist: except User.DoesNotExist:
return self.error("User does not exist") return self.error("User does not exist")
return self.success( return self.success(UserProfileSerializer(user.userprofile, show_real_name=show_real_name).data)
UserProfileSerializer(user.userprofile, show_real_name=show_real_name).data
)
@validate_serializer(EditUserProfileSerializer) @validate_serializer(EditUserProfileSerializer)
@login_required @login_required
@@ -90,9 +88,7 @@ class UserProfileAPI(APIView):
for k, v in data.items(): for k, v in data.items():
setattr(user_profile, k, v) setattr(user_profile, k, v)
user_profile.save() user_profile.save()
return self.success( return self.success(UserProfileSerializer(user_profile, show_real_name=True).data)
UserProfileSerializer(user_profile, show_real_name=True).data
)
class Metrics(APIView): class Metrics(APIView):
@@ -157,9 +153,7 @@ class TwoFactorAuthAPI(APIView):
user.save() user.save()
label = f"{SysOptions.website_name_shortcut}:{user.username}" label = f"{SysOptions.website_name_shortcut}:{user.username}"
image = qrcode.make( image = qrcode.make(_totp_uri(token, label, SysOptions.website_name.replace(" ", "")))
_totp_uri(token, label, SysOptions.website_name.replace(" ", ""))
)
return self.success(img2base64(image)) return self.success(img2base64(image))
@login_required @login_required
@@ -224,9 +218,7 @@ class UserLoginAPI(APIView):
if not user.two_factor_auth: if not user.two_factor_auth:
prev_login = user.last_login prev_login = user.last_login
auth.login(request, user) auth.login(request, user)
request.session["prev_login"] = ( request.session["prev_login"] = datetime2str(prev_login) if prev_login else ""
datetime2str(prev_login) if prev_login else ""
)
return self.success("Succeeded") return self.success("Succeeded")
# `tfa_code` not in post data # `tfa_code` not in post data
@@ -236,9 +228,7 @@ class UserLoginAPI(APIView):
if _valid_totp(user.tfa_token, data["tfa_code"]): if _valid_totp(user.tfa_token, data["tfa_code"]):
prev_login = user.last_login prev_login = user.last_login
auth.login(request, user) auth.login(request, user)
request.session["prev_login"] = ( request.session["prev_login"] = datetime2str(prev_login) if prev_login else ""
datetime2str(prev_login) if prev_login else ""
)
return self.success("Succeeded") return self.success("Succeeded")
else: else:
return self.error("Invalid two factor verification code") return self.error("Invalid two factor verification code")
@@ -262,9 +252,7 @@ class UsernameOrEmailCheck(APIView):
# True means already exist. # True means already exist.
result = {"username": False, "email": False} result = {"username": False, "email": False}
if data.get("username"): if data.get("username"):
result["username"] = User.objects.filter( result["username"] = User.objects.filter(username=data["username"].lower()).exists()
username=data["username"].lower()
).exists()
if data.get("email"): if data.get("email"):
result["email"] = User.objects.filter(email=data["email"].lower()).exists() result["email"] = User.objects.filter(email=data["email"].lower()).exists()
return self.success(result) return self.success(result)
@@ -301,9 +289,7 @@ class UserChangeEmailAPI(APIView):
@login_required @login_required
def post(self, request): def post(self, request):
data = request.data data = request.data
user = auth.authenticate( user = auth.authenticate(username=request.user.username, password=data["password"])
username=request.user.username, password=data["password"]
)
if user: if user:
if user.two_factor_auth: if user.two_factor_auth:
if "tfa_code" not in data: if "tfa_code" not in data:
@@ -356,12 +342,7 @@ class ApplyResetPasswordAPI(APIView):
user = User.objects.get(email__iexact=data["email"]) user = User.objects.get(email__iexact=data["email"])
except User.DoesNotExist: except User.DoesNotExist:
return self.error("User does not exist") return self.error("User does not exist")
if ( if user.reset_password_token_expire_time and 0 < int((user.reset_password_token_expire_time - now()).total_seconds()) < 20 * 60:
user.reset_password_token_expire_time
and 0
< int((user.reset_password_token_expire_time - now()).total_seconds())
< 20 * 60
):
return self.error("You can only reset password once per 20 minutes") return self.error("You can only reset password once per 20 minutes")
user.reset_password_token = rand_str() user.reset_password_token = rand_str()
user.reset_password_token_expire_time = now() + timedelta(minutes=20) user.reset_password_token_expire_time = now() + timedelta(minutes=20)
@@ -453,7 +434,7 @@ class UserRankAPI(APIView):
n = int(request.GET.get("n", "0")) n = int(request.GET.get("n", "0"))
except ValueError: except ValueError:
n = 0 n = 0
if rule_type not in ContestRuleType.choices(): if rule_type not in ContestRuleType.values:
rule_type = ContestRuleType.ACM rule_type = ContestRuleType.ACM
profiles = UserProfile.objects.filter( profiles = UserProfile.objects.filter(
@@ -462,9 +443,7 @@ class UserRankAPI(APIView):
user__username__icontains=username, user__username__icontains=username,
).select_related("user") ).select_related("user")
if rule_type == ContestRuleType.ACM: if rule_type == ContestRuleType.ACM:
profiles = profiles.filter(accepted_number__gte=0).order_by( profiles = profiles.filter(accepted_number__gte=0).order_by("-accepted_number", "submission_number")
"-accepted_number", "submission_number"
)
else: else:
profiles = profiles.filter(total_score__gt=0).order_by("-total_score") profiles = profiles.filter(total_score__gt=0).order_by("-total_score")
if n > 0: if n > 0:
@@ -482,19 +461,13 @@ class UserActivityRankAPI(APIView):
if cached is not None: if cached is not None:
return self.success(cached) return self.success(cached)
hidden_names = User.objects.filter( hidden_names = User.objects.filter(Q(admin_type=AdminType.SUPER_ADMIN) | Q(is_disabled=True)).values_list("username", flat=True)
Q(admin_type=AdminType.SUPER_ADMIN) | Q(is_disabled=True)
).values_list("username", flat=True)
submissions = Submission.objects.filter( submissions = Submission.objects.filter(
contest_id__isnull=True, contest_id__isnull=True,
create_time__gte=start, create_time__gte=start,
result=JudgeStatus.ACCEPTED, result=JudgeStatus.ACCEPTED,
).exclude(username__in=hidden_names) ).exclude(username__in=hidden_names)
data = list( data = list(submissions.values("username").annotate(count=Count("problem_id", distinct=True)).order_by("-count")[:10])
submissions.values("username")
.annotate(count=Count("problem_id", distinct=True))
.order_by("-count")[:10]
)
cache.set(cache_key, data, 600) cache.set(cache_key, data, 600)
return self.success(data) return self.success(data)
@@ -506,12 +479,8 @@ class UserProblemRankAPI(APIView):
if not user.is_authenticated: if not user.is_authenticated:
return self.error("User is not authenticated") return self.error("User is not authenticated")
problem = Problem.objects.get( problem = Problem.objects.get(_id=problem_id, contest_id__isnull=True, visible=True)
_id=problem_id, contest_id__isnull=True, visible=True submissions = Submission.objects.filter(problem=problem, result=JudgeStatus.ACCEPTED)
)
submissions = Submission.objects.filter(
problem=problem, result=JudgeStatus.ACCEPTED
)
all_ac_count = submissions.values("user_id").distinct().count() all_ac_count = submissions.values("user_id").distinct().count()
@@ -519,9 +488,7 @@ class UserProblemRankAPI(APIView):
class_ac_count = 0 class_ac_count = 0
if class_name: if class_name:
users = User.objects.filter( users = User.objects.filter(class_name=user.class_name, is_disabled=False).values_list("id", flat=True)
class_name=user.class_name, is_disabled=False
).values_list("id", flat=True)
user_ids = list(users) user_ids = list(users)
submissions = submissions.filter(user_id__in=user_ids) submissions = submissions.filter(user_id__in=user_ids)
class_ac_count = submissions.values("user_id").distinct().count() class_ac_count = submissions.values("user_id").distinct().count()
@@ -539,9 +506,7 @@ class UserProblemRankAPI(APIView):
) )
my_first_submission = my_submissions.order_by("create_time").first() my_first_submission = my_submissions.order_by("create_time").first()
rank = submissions.filter( rank = submissions.filter(create_time__lte=my_first_submission.create_time).count()
create_time__lte=my_first_submission.create_time
).count()
return self.success( return self.success(
{ {
"class_name": class_name, "class_name": class_name,
@@ -561,9 +526,7 @@ class ProfileProblemDisplayIDRefreshAPI(APIView):
ids = list(acm_problems.keys()) + list(oi_problems.keys()) ids = list(acm_problems.keys()) + list(oi_problems.keys())
if not ids: if not ids:
return self.success() return self.success()
display_ids = Problem.objects.filter(id__in=ids, visible=True).values_list( display_ids = Problem.objects.filter(id__in=ids, visible=True).values_list("_id", flat=True)
"_id", flat=True
)
id_map = dict(zip(ids, display_ids)) id_map = dict(zip(ids, display_ids))
for k, v in acm_problems.items(): for k, v in acm_problems.items():
v["_id"] = id_map[k] v["_id"] = id_map[k]

View File

@@ -0,0 +1,35 @@
# Generated by Django 6.0.4 on 2026-05-09 08:18
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("contest", "0003_acmcontestrank_acm_rank_contest_user_idx_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterUniqueTogether(
name="acmcontestrank",
unique_together=set(),
),
migrations.AlterUniqueTogether(
name="oicontestrank",
unique_together=set(),
),
migrations.AlterField(
model_name="contest",
name="rule_type",
field=models.TextField(choices=[("ACM", "ACM"), ("OI", "OI")]),
),
migrations.AddConstraint(
model_name="acmcontestrank",
constraint=models.UniqueConstraint(fields=("user", "contest"), name="unique_acm_rank_user_contest"),
),
migrations.AddConstraint(
model_name="oicontestrank",
constraint=models.UniqueConstraint(fields=("user", "contest"), name="unique_oi_rank_user_contest"),
),
]

View File

@@ -15,8 +15,7 @@ class Contest(models.Model):
# show real time rank or cached rank # show real time rank or cached rank
real_time_rank = models.BooleanField() real_time_rank = models.BooleanField()
password = models.TextField(null=True) password = models.TextField(null=True)
# enum of ContestRuleType rule_type = models.TextField(choices=ContestRuleType.choices)
rule_type = models.TextField()
start_time = models.DateTimeField() start_time = models.DateTimeField()
end_time = models.DateTimeField() end_time = models.DateTimeField()
create_time = models.DateTimeField(auto_now_add=True) create_time = models.DateTimeField(auto_now_add=True)
@@ -46,13 +45,7 @@ class Contest(models.Model):
# 是否有权查看problem 的一些统计信息 诸如submission_number, accepted_number 等 # 是否有权查看problem 的一些统计信息 诸如submission_number, accepted_number 等
def problem_details_permission(self, user): def problem_details_permission(self, user):
return ( return self.rule_type == ContestRuleType.ACM or self.status == ContestStatus.CONTEST_ENDED or user.is_authenticated and user.is_contest_admin(self) or self.real_time_rank
self.rule_type == ContestRuleType.ACM
or self.status == ContestStatus.CONTEST_ENDED
or user.is_authenticated
and user.is_contest_admin(self)
or self.real_time_rank
)
class Meta: class Meta:
db_table = "contest" db_table = "contest"
@@ -78,10 +71,11 @@ class ACMContestRank(AbstractContestRank):
class Meta: class Meta:
db_table = "acm_contest_rank" db_table = "acm_contest_rank"
unique_together = (("user", "contest"),) constraints = [
models.UniqueConstraint(fields=["user", "contest"], name="unique_acm_rank_user_contest"),
]
indexes = [ indexes = [
models.Index(fields=["contest", "accepted_number", "total_time"], models.Index(fields=["contest", "accepted_number", "total_time"], name="acm_rank_order_idx"),
name="acm_rank_order_idx"),
models.Index(fields=["contest", "user"], name="acm_rank_contest_user_idx"), models.Index(fields=["contest", "user"], name="acm_rank_contest_user_idx"),
] ]
@@ -94,7 +88,9 @@ class OIContestRank(AbstractContestRank):
class Meta: class Meta:
db_table = "oi_contest_rank" db_table = "oi_contest_rank"
unique_together = (("user", "contest"),) constraints = [
models.UniqueConstraint(fields=["user", "contest"], name="unique_oi_rank_user_contest"),
]
indexes = [ indexes = [
models.Index(fields=["contest", "total_score"], name="oi_rank_order_idx"), models.Index(fields=["contest", "total_score"], name="oi_rank_order_idx"),
models.Index(fields=["contest", "user"], name="oi_rank_contest_user_idx"), models.Index(fields=["contest", "user"], name="oi_rank_contest_user_idx"),

View File

@@ -9,7 +9,7 @@ class CreateConetestSeriaizer(serializers.Serializer):
tag = serializers.CharField() tag = serializers.CharField()
start_time = serializers.DateTimeField() start_time = serializers.DateTimeField()
end_time = serializers.DateTimeField() end_time = serializers.DateTimeField()
rule_type = serializers.ChoiceField(choices=[ContestRuleType.ACM, ContestRuleType.OI]) rule_type = serializers.ChoiceField(choices=ContestRuleType.choices)
password = serializers.CharField(allow_blank=True, max_length=32) password = serializers.CharField(allow_blank=True, max_length=32)
visible = serializers.BooleanField() visible = serializers.BooleanField()
real_time_rank = serializers.BooleanField() real_time_rank = serializers.BooleanField()

View File

@@ -3,8 +3,6 @@ import logging
import time import time
import dramatiq import dramatiq
logger = logging.getLogger(__name__)
from django.db import transaction from django.db import transaction
from django.utils import timezone from django.utils import timezone
@@ -13,6 +11,8 @@ from utils.shortcuts import DRAMATIQ_WORKER_ARGS
from .models import FlowchartSubmission, FlowchartSubmissionStatus from .models import FlowchartSubmission, FlowchartSubmissionStatus
logger = logging.getLogger(__name__)
@dramatiq.actor(**DRAMATIQ_WORKER_ARGS(max_retries=3)) @dramatiq.actor(**DRAMATIQ_WORKER_ARGS(max_retries=3))
def evaluate_flowchart_task(submission_id): def evaluate_flowchart_task(submission_id):

View File

@@ -27,11 +27,7 @@ int main() {
"max_memory": 256 * 1024 * 1024, "max_memory": 256 * 1024 * 1024,
"compile_command": "/usr/bin/gcc -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c17 {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}",
}, },
"run": { "run": {"command": "{exe_path}", "seccomp_rule": {ProblemIOMode.STANDARD: "c_cpp", ProblemIOMode.FILE: "c_cpp_file_io"}, "env": default_env},
"command": "{exe_path}",
"seccomp_rule": {ProblemIOMode.standard: "c_cpp", ProblemIOMode.file: "c_cpp_file_io"},
"env": default_env
}
} }
@@ -60,11 +56,7 @@ int main() {
"max_memory": 1024 * 1024 * 1024, "max_memory": 1024 * 1024 * 1024,
"compile_command": "/usr/bin/g++ -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c++20 {src_path} -lm -o {exe_path}", "compile_command": "/usr/bin/g++ -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c++20 {src_path} -lm -o {exe_path}",
}, },
"run": { "run": {"command": "{exe_path}", "seccomp_rule": {ProblemIOMode.STANDARD: "c_cpp", ProblemIOMode.FILE: "c_cpp_file_io"}, "env": default_env},
"command": "{exe_path}",
"seccomp_rule": {ProblemIOMode.standard: "c_cpp", ProblemIOMode.file: "c_cpp_file_io"},
"env": default_env
}
} }
@@ -91,14 +83,9 @@ class Main {
"max_cpu_time": 5000, "max_cpu_time": 5000,
"max_real_time": 10000, "max_real_time": 10000,
"max_memory": -1, "max_memory": -1,
"compile_command": "/usr/bin/javac {src_path} -d {exe_dir}" "compile_command": "/usr/bin/javac {src_path} -d {exe_dir}",
}, },
"run": { "run": {"command": "/usr/bin/java -cp {exe_dir} -XX:MaxRAM={max_memory}k Main", "seccomp_rule": None, "env": default_env, "memory_limit_check_only": 1},
"command": "/usr/bin/java -cp {exe_dir} -XX:MaxRAM={max_memory}k Main",
"seccomp_rule": None,
"env": default_env,
"memory_limit_check_only": 1
}
} }
_py3_lang_config = { _py3_lang_config = {
@@ -122,11 +109,7 @@ print(add(1, 2))
"max_memory": 128 * 1024 * 1024, "max_memory": 128 * 1024 * 1024,
"compile_command": "/usr/bin/python3 -m py_compile {src_path}", "compile_command": "/usr/bin/python3 -m py_compile {src_path}",
}, },
"run": { "run": {"command": "/usr/bin/python3 -BS {exe_path}", "seccomp_rule": "general", "env": default_env},
"command": "/usr/bin/python3 -BS {exe_path}",
"seccomp_rule": "general",
"env": default_env
}
} }
_go_lang_config = { _go_lang_config = {
@@ -154,14 +137,9 @@ func main() {
"max_real_time": 5000, "max_real_time": 5000,
"max_memory": 1024 * 1024 * 1024, "max_memory": 1024 * 1024 * 1024,
"compile_command": "/usr/bin/go build -o {exe_path} {src_path}", "compile_command": "/usr/bin/go build -o {exe_path} {src_path}",
"env": ["GOCACHE=/tmp", "GOPATH=/tmp", "GOMAXPROCS=1"] + default_env "env": ["GOCACHE=/tmp", "GOPATH=/tmp", "GOMAXPROCS=1"] + default_env,
}, },
"run": { "run": {"command": "{exe_path}", "seccomp_rule": "golang", "env": ["GOMAXPROCS=1"] + default_env, "memory_limit_check_only": 1},
"command": "{exe_path}",
"seccomp_rule": "golang",
"env": ["GOMAXPROCS=1"] + default_env,
"memory_limit_check_only": 1
}
} }
_node_lang_config = { _node_lang_config = {
@@ -184,14 +162,9 @@ console.log(add(1, 2))
"max_real_time": 5000, "max_real_time": 5000,
"max_memory": 1024 * 1024 * 1024, "max_memory": 1024 * 1024 * 1024,
"compile_command": "/usr/bin/node --check {src_path}", "compile_command": "/usr/bin/node --check {src_path}",
"env": default_env
},
"run": {
"command": "/usr/bin/node {exe_path}",
"seccomp_rule": "node",
"env": default_env, "env": default_env,
"memory_limit_check_only": 1 },
} "run": {"command": "/usr/bin/node {exe_path}", "seccomp_rule": "node", "env": default_env, "memory_limit_check_only": 1},
} }
languages = [ languages = [

View File

@@ -0,0 +1,33 @@
# Generated by Django 6.0.4 on 2026-05-09 08:18
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("contest", "0004_alter_acmcontestrank_unique_together_and_more"),
("problem", "0007_problem_problem_visible_idx"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterUniqueTogether(
name="problem",
unique_together=set(),
),
migrations.AlterField(
model_name="problem",
name="difficulty",
field=models.TextField(choices=[("Low", "Low"), ("Mid", "Mid"), ("High", "High")]),
),
migrations.AlterField(
model_name="problem",
name="rule_type",
field=models.TextField(choices=[("ACM", "ACM"), ("OI", "OI")]),
),
migrations.AddConstraint(
model_name="problem",
constraint=models.UniqueConstraint(fields=("_id", "contest"), name="unique_problem_id_contest"),
),
]

View File

@@ -2,7 +2,7 @@ from django.db import models
from account.models import User from account.models import User
from contest.models import Contest from contest.models import Contest
from utils.constants import Choices from utils.constants import Difficulty
from utils.models import RichTextField from utils.models import RichTextField
@@ -13,25 +13,19 @@ class ProblemTag(models.Model):
db_table = "problem_tag" db_table = "problem_tag"
class ProblemRuleType(Choices): class ProblemRuleType(models.TextChoices):
ACM = "ACM" ACM = "ACM", "ACM"
OI = "OI" OI = "OI", "OI"
class ProblemDifficulty(object): class ProblemIOMode(models.TextChoices):
High = "High" STANDARD = "Standard IO", "Standard IO"
Mid = "Mid" FILE = "File IO", "File IO"
Low = "Low"
class ProblemIOMode(Choices):
standard = "Standard IO"
file = "File IO"
def _default_io_mode(): def _default_io_mode():
return { return {
"io_mode": ProblemIOMode.standard, "io_mode": ProblemIOMode.STANDARD,
"input": "input.txt", "input": "input.txt",
"output": "output.txt", "output": "output.txt",
} }
@@ -66,9 +60,9 @@ class Problem(models.Model):
memory_limit = models.IntegerField() memory_limit = models.IntegerField()
# io mode # io mode
io_mode = models.JSONField(default=_default_io_mode) io_mode = models.JSONField(default=_default_io_mode)
rule_type = models.TextField() rule_type = models.TextField(choices=ProblemRuleType.choices)
visible = models.BooleanField(default=True) visible = models.BooleanField(default=True)
difficulty = models.TextField() difficulty = models.TextField(choices=Difficulty.choices)
tags = models.ManyToManyField(ProblemTag) tags = models.ManyToManyField(ProblemTag)
source = models.TextField(null=True) source = models.TextField(null=True)
prompt = models.TextField(null=True) prompt = models.TextField(null=True)
@@ -81,7 +75,7 @@ class Problem(models.Model):
# {JudgeStatus.ACCEPTED: 3, JudgeStatus.WRONG_ANSWER: 11}, the number means count # {JudgeStatus.ACCEPTED: 3, JudgeStatus.WRONG_ANSWER: 11}, the number means count
statistic_info = models.JSONField(default=dict) statistic_info = models.JSONField(default=dict)
share_submission = models.BooleanField(default=False) share_submission = models.BooleanField(default=False)
# 流程图相关字段 # 流程图相关字段
allow_flowchart = models.BooleanField(default=False) # 是否允许/需要提交流程图 allow_flowchart = models.BooleanField(default=False) # 是否允许/需要提交流程图
mermaid_code = models.TextField(null=True, blank=True) # 流程图答案(Mermaid代码) mermaid_code = models.TextField(null=True, blank=True) # 流程图答案(Mermaid代码)
@@ -91,7 +85,9 @@ class Problem(models.Model):
class Meta: class Meta:
db_table = "problem" db_table = "problem"
unique_together = (("_id", "contest"),) constraints = [
models.UniqueConstraint(fields=["_id", "contest"], name="unique_problem_id_contest"),
]
ordering = ("create_time",) ordering = ("create_time",)
indexes = [ indexes = [
models.Index(fields=["contest", "visible"], name="problem_contest_visible_idx"), models.Index(fields=["contest", "visible"], name="problem_contest_visible_idx"),

View File

@@ -38,7 +38,7 @@ class CreateProblemCodeTemplateSerializer(serializers.Serializer):
class ProblemIOModeSerializer(serializers.Serializer): class ProblemIOModeSerializer(serializers.Serializer):
io_mode = serializers.ChoiceField(choices=ProblemIOMode.choices()) io_mode = serializers.ChoiceField(choices=ProblemIOMode.choices)
input = serializers.CharField() input = serializers.CharField()
output = serializers.CharField() output = serializers.CharField()
@@ -59,22 +59,16 @@ class CreateOrEditProblemSerializer(serializers.Serializer):
output_description = serializers.CharField() output_description = serializers.CharField()
samples = serializers.ListField(child=CreateSampleSerializer(), allow_empty=False) samples = serializers.ListField(child=CreateSampleSerializer(), allow_empty=False)
test_case_id = serializers.CharField(max_length=32) test_case_id = serializers.CharField(max_length=32)
test_case_score = serializers.ListField( test_case_score = serializers.ListField(child=CreateTestCaseScoreSerializer(), allow_empty=True)
child=CreateTestCaseScoreSerializer(), allow_empty=True
)
time_limit = serializers.IntegerField(min_value=1, max_value=1000 * 60) time_limit = serializers.IntegerField(min_value=1, max_value=1000 * 60)
memory_limit = serializers.IntegerField(min_value=1, max_value=1024) memory_limit = serializers.IntegerField(min_value=1, max_value=1024)
languages = LanguageNameMultiChoiceField() languages = LanguageNameMultiChoiceField()
template = serializers.DictField(child=serializers.CharField(min_length=1)) template = serializers.DictField(child=serializers.CharField(min_length=1))
rule_type = serializers.ChoiceField( rule_type = serializers.ChoiceField(choices=ProblemRuleType.choices)
choices=[ProblemRuleType.ACM, ProblemRuleType.OI]
)
io_mode = ProblemIOModeSerializer() io_mode = ProblemIOModeSerializer()
visible = serializers.BooleanField() visible = serializers.BooleanField()
difficulty = serializers.ChoiceField(choices=Difficulty.choices()) difficulty = serializers.ChoiceField(choices=Difficulty.choices)
tags = serializers.ListField( tags = serializers.ListField(child=serializers.CharField(max_length=32), allow_empty=False)
child=serializers.CharField(max_length=32), allow_empty=False
)
hint = serializers.CharField(allow_blank=True, allow_null=True) hint = serializers.CharField(allow_blank=True, allow_null=True)
source = serializers.CharField(max_length=256, allow_blank=True, allow_null=True) source = serializers.CharField(max_length=256, allow_blank=True, allow_null=True)
prompt = serializers.CharField(allow_blank=True, allow_null=True) prompt = serializers.CharField(allow_blank=True, allow_null=True)
@@ -88,13 +82,9 @@ class CreateOrEditProblemSerializer(serializers.Serializer):
# 流程图相关字段 # 流程图相关字段
allow_flowchart = serializers.BooleanField(required=False, default=False) allow_flowchart = serializers.BooleanField(required=False, default=False)
show_flowchart = serializers.BooleanField(required=False, default=False) show_flowchart = serializers.BooleanField(required=False, default=False)
mermaid_code = serializers.CharField( mermaid_code = serializers.CharField(allow_blank=True, allow_null=True, required=False)
allow_blank=True, allow_null=True, required=False
)
flowchart_hint = serializers.CharField( flowchart_hint = serializers.CharField(allow_blank=True, allow_null=True, required=False)
allow_blank=True, allow_null=True, required=False
)
class CreateProblemSerializer(CreateOrEditProblemSerializer): class CreateProblemSerializer(CreateOrEditProblemSerializer):
@@ -220,6 +210,7 @@ class ProblemSafeSerializer(BaseProblemSerializer):
return None return None
return obj.flowchart_data return obj.flowchart_data
class ContestProblemMakePublicSerializer(serializers.Serializer): class ContestProblemMakePublicSerializer(serializers.Serializer):
id = serializers.IntegerField() id = serializers.IntegerField()
display_id = serializers.CharField(max_length=32) display_id = serializers.CharField(max_length=32)

View File

@@ -0,0 +1,54 @@
# Generated by Django 6.0.4 on 2026-05-09 08:18
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("problem", "0008_alter_problem_unique_together_and_more"),
("problemset", "0007_problemset_end_time"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterUniqueTogether(
name="problemsetproblem",
unique_together=set(),
),
migrations.AlterUniqueTogether(
name="problemsetprogress",
unique_together=set(),
),
migrations.AlterUniqueTogether(
name="userbadge",
unique_together=set(),
),
migrations.AlterField(
model_name="problemset",
name="status",
field=models.TextField(choices=[("draft", "Draft"), ("active", "Active"), ("archived", "Archived")], default="draft", verbose_name="状态"),
),
migrations.AlterField(
model_name="problemset",
name="difficulty",
field=models.TextField(choices=[("Easy", "Easy"), ("Medium", "Medium"), ("Hard", "Hard")], default="Easy", verbose_name="难度等级"),
),
migrations.AlterField(
model_name="problemsetbadge",
name="condition_type",
field=models.TextField(choices=[("all_problems", "All Problems"), ("problem_count", "Problem Count"), ("score", "Score")], verbose_name="获得条件类型"),
),
migrations.AddConstraint(
model_name="problemsetproblem",
constraint=models.UniqueConstraint(fields=("problemset", "problem"), name="unique_problemset_problem"),
),
migrations.AddConstraint(
model_name="problemsetprogress",
constraint=models.UniqueConstraint(fields=("problemset", "user"), name="unique_problemset_progress_user"),
),
migrations.AddConstraint(
model_name="userbadge",
constraint=models.UniqueConstraint(fields=("user", "badge"), name="unique_user_badge"),
),
]

View File

@@ -6,15 +6,31 @@ from problem.models import Problem
from utils.models import JSONField, RichTextField from utils.models import JSONField, RichTextField
class ProblemSetStatus(models.TextChoices):
DRAFT = "draft", "Draft"
ACTIVE = "active", "Active"
ARCHIVED = "archived", "Archived"
class ProblemSetDifficulty(models.TextChoices):
EASY = "Easy", "Easy"
MEDIUM = "Medium", "Medium"
HARD = "Hard", "Hard"
class BadgeConditionType(models.TextChoices):
ALL_PROBLEMS = "all_problems", "All Problems"
PROBLEM_COUNT = "problem_count", "Problem Count"
SCORE = "score", "Score"
class ProblemSet(models.Model): class ProblemSet(models.Model):
"""题单模型""" """题单模型"""
title = models.TextField(verbose_name="题单标题") title = models.TextField(verbose_name="题单标题")
description = RichTextField(verbose_name="题单描述") description = RichTextField(verbose_name="题单描述")
# 创建者 # 创建者
created_by = models.ForeignKey( created_by = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="创建者")
User, on_delete=models.CASCADE, verbose_name="创建者"
)
# 创建时间 # 创建时间
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
# 更新时间 # 更新时间
@@ -22,11 +38,13 @@ class ProblemSet(models.Model):
# 是否可见 # 是否可见
visible = models.BooleanField(default=True, verbose_name="是否可见") visible = models.BooleanField(default=True, verbose_name="是否可见")
# 题单难度等级 # 题单难度等级
difficulty = models.TextField(default="Easy", verbose_name="难度等级") difficulty = models.TextField(
default=ProblemSetDifficulty.EASY,
choices=ProblemSetDifficulty.choices,
verbose_name="难度等级",
)
# 题单状态 # 题单状态
status = models.TextField( status = models.TextField(default=ProblemSetStatus.DRAFT, choices=ProblemSetStatus.choices, verbose_name="状态")
default="draft", verbose_name="状态"
) # active, archived, draft
# 截止时间(到期后自动解除防作弊隐藏) # 截止时间(到期后自动解除防作弊隐藏)
end_time = models.DateTimeField(null=True, blank=True, verbose_name="截止时间") end_time = models.DateTimeField(null=True, blank=True, verbose_name="截止时间")
@@ -43,9 +61,7 @@ class ProblemSet(models.Model):
class ProblemSetProblem(models.Model): class ProblemSetProblem(models.Model):
"""题单题目关联模型""" """题单题目关联模型"""
problemset = models.ForeignKey( problemset = models.ForeignKey(ProblemSet, on_delete=models.CASCADE, verbose_name="题单")
ProblemSet, on_delete=models.CASCADE, verbose_name="题单"
)
problem = models.ForeignKey(Problem, on_delete=models.CASCADE, verbose_name="题目") problem = models.ForeignKey(Problem, on_delete=models.CASCADE, verbose_name="题目")
# 在题单中的顺序 # 在题单中的顺序
order = models.IntegerField(default=0, verbose_name="顺序") order = models.IntegerField(default=0, verbose_name="顺序")
@@ -58,7 +74,9 @@ class ProblemSetProblem(models.Model):
class Meta: class Meta:
db_table = "problemset_problem" db_table = "problemset_problem"
unique_together = (("problemset", "problem"),) constraints = [
models.UniqueConstraint(fields=["problemset", "problem"], name="unique_problemset_problem"),
]
ordering = ("order",) ordering = ("order",)
verbose_name = "题单题目" verbose_name = "题单题目"
verbose_name_plural = "题单题目" verbose_name_plural = "题单题目"
@@ -70,17 +88,13 @@ class ProblemSetProblem(models.Model):
class ProblemSetBadge(models.Model): class ProblemSetBadge(models.Model):
"""题单奖章模型""" """题单奖章模型"""
problemset = models.ForeignKey( problemset = models.ForeignKey(ProblemSet, on_delete=models.CASCADE, verbose_name="题单")
ProblemSet, on_delete=models.CASCADE, verbose_name="题单"
)
name = models.TextField(verbose_name="奖章名称") name = models.TextField(verbose_name="奖章名称")
description = models.TextField(verbose_name="奖章描述") description = models.TextField(verbose_name="奖章描述")
# 奖章图标路径 # 奖章图标路径
icon = models.TextField(verbose_name="奖章图标") icon = models.TextField(verbose_name="奖章图标")
# 获得条件:完成所有题目、完成指定数量题目、达到指定分数等 # 获得条件:完成所有题目、完成指定数量题目、达到指定分数等
condition_type = models.TextField( condition_type = models.TextField(choices=BadgeConditionType.choices, verbose_name="获得条件类型")
verbose_name="获得条件类型"
) # all_problems, problem_count, score
condition_value = models.IntegerField(default=0, verbose_name="条件值") condition_value = models.IntegerField(default=0, verbose_name="条件值")
class Meta: class Meta:
@@ -90,17 +104,13 @@ class ProblemSetBadge(models.Model):
def __str__(self): def __str__(self):
return f"{self.problemset.title} - {self.name}" return f"{self.problemset.title} - {self.name}"
def recalculate_user_badges(self): def recalculate_user_badges(self):
"""重新计算所有用户的徽章资格""" """重新计算所有用户的徽章资格"""
from django.db import transaction from django.db import transaction
user_progresses = ProblemSetProgress.objects.filter(problemset=self.problemset) user_progresses = ProblemSetProgress.objects.filter(problemset=self.problemset)
new_badges = [ new_badges = [UserBadge(user=progress.user, badge=self) for progress in user_progresses if self._is_eligible(progress)]
UserBadge(user=progress.user, badge=self)
for progress in user_progresses
if self._is_eligible(progress)
]
with transaction.atomic(): with transaction.atomic():
UserBadge.objects.filter(badge=self).delete() UserBadge.objects.filter(badge=self).delete()
if new_badges: if new_badges:
@@ -118,9 +128,7 @@ class ProblemSetBadge(models.Model):
def _check_user_badge_eligibility(self, progress): def _check_user_badge_eligibility(self, progress):
"""检查并授予单个用户的徽章(供外部单次调用)""" """检查并授予单个用户的徽章(供外部单次调用)"""
if self._is_eligible(progress) and not UserBadge.objects.filter( if self._is_eligible(progress) and not UserBadge.objects.filter(user=progress.user, badge=self).exists():
user=progress.user, badge=self
).exists():
UserBadge.objects.create(user=progress.user, badge=self) UserBadge.objects.create(user=progress.user, badge=self)
return True return True
return False return False
@@ -129,9 +137,7 @@ class ProblemSetBadge(models.Model):
class ProblemSetProgress(models.Model): class ProblemSetProgress(models.Model):
"""题单进度模型""" """题单进度模型"""
problemset = models.ForeignKey( problemset = models.ForeignKey(ProblemSet, on_delete=models.CASCADE, verbose_name="题单")
ProblemSet, on_delete=models.CASCADE, verbose_name="题单"
)
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户") user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户")
# 加入时间 # 加入时间
join_time = models.DateTimeField(auto_now_add=True, verbose_name="加入时间") join_time = models.DateTimeField(auto_now_add=True, verbose_name="加入时间")
@@ -142,9 +148,7 @@ class ProblemSetProgress(models.Model):
# 完成进度百分比 # 完成进度百分比
progress_percentage = models.FloatField(default=0.0, verbose_name="完成进度") progress_percentage = models.FloatField(default=0.0, verbose_name="完成进度")
# 已完成的题目数量 # 已完成的题目数量
completed_problems_count = models.IntegerField( completed_problems_count = models.IntegerField(default=0, verbose_name="已完成题目数")
default=0, verbose_name="已完成题目数"
)
# 总题目数量 # 总题目数量
total_problems_count = models.IntegerField(default=0, verbose_name="总题目数") total_problems_count = models.IntegerField(default=0, verbose_name="总题目数")
# 获得的总分 # 获得的总分
@@ -155,7 +159,9 @@ class ProblemSetProgress(models.Model):
class Meta: class Meta:
db_table = "problemset_progress" db_table = "problemset_progress"
unique_together = (("problemset", "user"),) constraints = [
models.UniqueConstraint(fields=["problemset", "user"], name="unique_problemset_progress_user"),
]
verbose_name = "题单进度" verbose_name = "题单进度"
verbose_name_plural = "题单进度" verbose_name_plural = "题单进度"
@@ -165,9 +171,7 @@ class ProblemSetProgress(models.Model):
def update_progress(self): def update_progress(self):
"""更新进度信息""" """更新进度信息"""
# 获取题单中的所有题目 # 获取题单中的所有题目
problemset_problems = ProblemSetProblem.objects.filter( problemset_problems = ProblemSetProblem.objects.filter(problemset=self.problemset)
problemset=self.problemset
)
self.total_problems_count = problemset_problems.count() self.total_problems_count = problemset_problems.count()
# 获取当前题单中所有题目的ID集合直接用 problem_id FK 字段,无需额外查询) # 获取当前题单中所有题目的ID集合直接用 problem_id FK 字段,无需额外查询)
@@ -199,9 +203,7 @@ class ProblemSetProgress(models.Model):
# 计算完成百分比 # 计算完成百分比
if self.total_problems_count > 0: if self.total_problems_count > 0:
self.progress_percentage = ( self.progress_percentage = (completed_count / self.total_problems_count) * 100
completed_count / self.total_problems_count
) * 100
else: else:
self.progress_percentage = 0 self.progress_percentage = 0
@@ -223,17 +225,11 @@ class ProblemSetProgress(models.Model):
class ProblemSetSubmission(models.Model): class ProblemSetSubmission(models.Model):
"""题单提交记录模型""" """题单提交记录模型"""
problemset = models.ForeignKey( problemset = models.ForeignKey(ProblemSet, on_delete=models.CASCADE, verbose_name="题单")
ProblemSet, on_delete=models.CASCADE, verbose_name="题单"
)
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户") user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户")
submission = models.ForeignKey( submission = models.ForeignKey("submission.Submission", on_delete=models.CASCADE, verbose_name="提交记录")
"submission.Submission", on_delete=models.CASCADE, verbose_name="提交记录" problem = models.ForeignKey("problem.Problem", on_delete=models.CASCADE, verbose_name="题目")
)
problem = models.ForeignKey(
"problem.Problem", on_delete=models.CASCADE, verbose_name="题目"
)
class Meta: class Meta:
db_table = "problemset_submission" db_table = "problemset_submission"
@@ -253,34 +249,33 @@ class ProblemSetSubmission(models.Model):
def submit_time(self): def submit_time(self):
"""提交时间""" """提交时间"""
return self.submission.create_time return self.submission.create_time
@property @property
def result(self): def result(self):
"""提交结果""" """提交结果"""
return self.submission.result return self.submission.result
@property @property
def language(self): def language(self):
"""编程语言""" """编程语言"""
return self.submission.language return self.submission.language
class UserBadge(models.Model): class UserBadge(models.Model):
"""用户奖章模型""" """用户奖章模型"""
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户") user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户")
badge = models.ForeignKey( badge = models.ForeignKey(ProblemSetBadge, on_delete=models.CASCADE, verbose_name="奖章")
ProblemSetBadge, on_delete=models.CASCADE, verbose_name="奖章"
)
# 获得时间 # 获得时间
earned_time = models.DateTimeField(auto_now_add=True, verbose_name="获得时间") earned_time = models.DateTimeField(auto_now_add=True, verbose_name="获得时间")
class Meta: class Meta:
db_table = "user_badge" db_table = "user_badge"
unique_together = (("user", "badge"),) constraints = [
models.UniqueConstraint(fields=["user", "badge"], name="unique_user_badge"),
]
verbose_name = "用户奖章" verbose_name = "用户奖章"
verbose_name_plural = "用户奖章" verbose_name_plural = "用户奖章"
def __str__(self): def __str__(self):
return f"{self.user.username} - {self.badge.name}" return f"{self.user.username} - {self.badge.name}"

View File

@@ -1,10 +1,13 @@
from utils.api import UsernameSerializer, serializers from utils.api import UsernameSerializer, serializers
from .models import ( from .models import (
BadgeConditionType,
ProblemSet, ProblemSet,
ProblemSetBadge, ProblemSetBadge,
ProblemSetDifficulty,
ProblemSetProblem, ProblemSetProblem,
ProblemSetProgress, ProblemSetProgress,
ProblemSetStatus,
UserBadge, UserBadge,
) )
@@ -13,9 +16,7 @@ def get_user_progress_data(problemset, request):
"""获取当前用户在该题单中的进度 - 公共方法""" """获取当前用户在该题单中的进度 - 公共方法"""
if request and request.user.is_authenticated: if request and request.user.is_authenticated:
try: try:
progress = ProblemSetProgress.objects.get( progress = ProblemSetProgress.objects.get(problemset=problemset, user=request.user)
problemset=problemset, user=request.user
)
return { return {
"is_joined": True, "is_joined": True,
"progress_percentage": progress.progress_percentage, "progress_percentage": progress.progress_percentage,
@@ -61,9 +62,7 @@ class ProblemSetSerializer(serializers.ModelSerializer):
request = self.context.get("request") request = self.context.get("request")
if request and request.user.is_authenticated: if request and request.user.is_authenticated:
try: try:
progress = ProblemSetProgress.objects.get( progress = ProblemSetProgress.objects.get(problemset=obj, user=request.user)
problemset=obj, user=request.user
)
return progress.completed_problems_count return progress.completed_problems_count
except ProblemSetProgress.DoesNotExist: except ProblemSetProgress.DoesNotExist:
return 0 return 0
@@ -124,22 +123,22 @@ class ProblemSetListSerializer(serializers.ModelSerializer):
def get_badges(self, obj): def get_badges(self, obj):
"""获取题单的奖章列表,并标记用户已获得的徽章""" """获取题单的奖章列表,并标记用户已获得的徽章"""
request = self.context.get("request") request = self.context.get("request")
# 使用预加载的奖章数据 # 使用预加载的奖章数据
badges = getattr(obj, "badges", []) badges = getattr(obj, "badges", [])
badge_data = ProblemSetBadgeSerializer(badges, many=True).data badge_data = ProblemSetBadgeSerializer(badges, many=True).data
# 如果用户已登录,检查哪些徽章已被获得 # 如果用户已登录,检查哪些徽章已被获得
if request and request.user.is_authenticated and hasattr(request, "_user_earned_badge_ids"): if request and request.user.is_authenticated and hasattr(request, "_user_earned_badge_ids"):
earned_badge_ids = request._user_earned_badge_ids earned_badge_ids = request._user_earned_badge_ids
# 为每个徽章添加是否已获得的标记 # 为每个徽章添加是否已获得的标记
for badge in badge_data: for badge in badge_data:
badge['is_earned'] = badge['id'] in earned_badge_ids badge["is_earned"] = badge["id"] in earned_badge_ids
else: else:
# 未登录用户或未预加载,所有徽章都标记为未获得 # 未登录用户或未预加载,所有徽章都标记为未获得
for badge in badge_data: for badge in badge_data:
badge['is_earned'] = False badge["is_earned"] = False
return badge_data return badge_data
@@ -148,8 +147,8 @@ class CreateProblemSetSerializer(serializers.Serializer):
title = serializers.CharField(max_length=200) title = serializers.CharField(max_length=200)
description = serializers.CharField() description = serializers.CharField()
difficulty = serializers.CharField(default="Easy") difficulty = serializers.ChoiceField(choices=ProblemSetDifficulty.choices, default=ProblemSetDifficulty.EASY)
status = serializers.CharField(default="active") status = serializers.ChoiceField(choices=ProblemSetStatus.choices, default=ProblemSetStatus.ACTIVE)
end_time = serializers.DateTimeField(required=False) end_time = serializers.DateTimeField(required=False)
@@ -159,8 +158,8 @@ class EditProblemSetSerializer(serializers.Serializer):
id = serializers.IntegerField() id = serializers.IntegerField()
title = serializers.CharField(max_length=200, required=False) title = serializers.CharField(max_length=200, required=False)
description = serializers.CharField(required=False) description = serializers.CharField(required=False)
difficulty = serializers.CharField(required=False) difficulty = serializers.ChoiceField(choices=ProblemSetDifficulty.choices, required=False)
status = serializers.CharField(required=False) status = serializers.ChoiceField(choices=ProblemSetStatus.choices, required=False)
visible = serializers.BooleanField(required=False) visible = serializers.BooleanField(required=False)
end_time = serializers.DateTimeField(required=False, allow_null=True) end_time = serializers.DateTimeField(required=False, allow_null=True)
@@ -190,9 +189,7 @@ class ProblemSetProblemSerializer(serializers.ModelSerializer):
progress = self.context.get("user_progress") progress = self.context.get("user_progress")
if progress is None: if progress is None:
try: try:
progress = ProblemSetProgress.objects.get( progress = ProblemSetProgress.objects.get(problemset=obj.problemset, user=request.user)
problemset=obj.problemset, user=request.user
)
except ProblemSetProgress.DoesNotExist: except ProblemSetProgress.DoesNotExist:
return False return False
return str(obj.problem.id) in progress.progress_detail return str(obj.problem.id) in progress.progress_detail
@@ -227,19 +224,21 @@ class ProblemSetBadgeSerializer(serializers.ModelSerializer):
class CreateProblemSetBadgeSerializer(serializers.Serializer): class CreateProblemSetBadgeSerializer(serializers.Serializer):
"""创建题单奖章序列化器""" """创建题单奖章序列化器"""
name = serializers.CharField(max_length=100) name = serializers.CharField(max_length=100)
description = serializers.CharField() description = serializers.CharField()
icon = serializers.CharField() icon = serializers.CharField()
condition_type = serializers.CharField() # all_problems, problem_count, score condition_type = serializers.ChoiceField(choices=BadgeConditionType.choices)
condition_value = serializers.IntegerField(required=False) condition_value = serializers.IntegerField(required=False)
class EditProblemSetBadgeSerializer(serializers.Serializer): class EditProblemSetBadgeSerializer(serializers.Serializer):
"""编辑题单奖章序列化器""" """编辑题单奖章序列化器"""
name = serializers.CharField(max_length=100, required=False) name = serializers.CharField(max_length=100, required=False)
description = serializers.CharField(required=False) description = serializers.CharField(required=False)
icon = serializers.CharField(required=False) icon = serializers.CharField(required=False)
condition_type = serializers.CharField(required=False) # all_problems, problem_count, score condition_type = serializers.ChoiceField(choices=BadgeConditionType.choices, required=False)
condition_value = serializers.IntegerField(required=False) condition_value = serializers.IntegerField(required=False)
@@ -252,42 +251,35 @@ class ProblemSetProgressSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = ProblemSetProgress model = ProblemSetProgress
fields = "__all__" fields = "__all__"
def get_completed_problems(self, obj): def get_completed_problems(self, obj):
"""获取已完成的题目列表""" """获取已完成的题目列表"""
completed_problems = [] completed_problems = []
# 尝试从 request 中获取预加载的问题字典(用于批量查询优化) # 尝试从 request 中获取预加载的问题字典(用于批量查询优化)
problems_dict = {} problems_dict = {}
request = self.context.get('request') request = self.context.get("request")
if request and hasattr(request, '_problems_dict_cache'): if request and hasattr(request, "_problems_dict_cache"):
problems_dict = request._problems_dict_cache problems_dict = request._problems_dict_cache
if obj.progress_detail: if obj.progress_detail:
for problem_id in obj.progress_detail.keys(): for problem_id in obj.progress_detail.keys():
# 优先使用预加载的问题字典 # 优先使用预加载的问题字典
if problems_dict: if problems_dict:
problem = problems_dict.get(problem_id) problem = problems_dict.get(problem_id)
if problem: if problem:
completed_problems.append({ completed_problems.append({"id": problem.id, "_id": problem._id, "title": problem.title})
'id': problem.id,
'_id': problem._id,
'title': problem.title
})
continue continue
# 如果没有预加载字典,则回退到单独查询(向后兼容) # 如果没有预加载字典,则回退到单独查询(向后兼容)
from problem.models import Problem from problem.models import Problem
try: try:
problem = Problem.objects.get(id=problem_id) problem = Problem.objects.get(id=problem_id)
completed_problems.append({ completed_problems.append({"id": problem.id, "_id": problem._id, "title": problem.title})
'id': problem.id,
'_id': problem._id,
'title': problem.title
})
except Problem.DoesNotExist: except Problem.DoesNotExist:
continue continue
return completed_problems return completed_problems
@@ -313,5 +305,3 @@ class UpdateProgressSerializer(serializers.Serializer):
problemset_id = serializers.IntegerField() problemset_id = serializers.IntegerField()
problem_id = serializers.IntegerField() problem_id = serializers.IntegerField()
submission_id = serializers.CharField(required=False) submission_id = serializers.CharField(required=False)

View File

@@ -7,6 +7,7 @@ from problemset.models import (
ProblemSetBadge, ProblemSetBadge,
ProblemSetProblem, ProblemSetProblem,
ProblemSetProgress, ProblemSetProgress,
ProblemSetStatus,
) )
from problemset.serializers import ( from problemset.serializers import (
AddProblemToSetSerializer, AddProblemToSetSerializer,
@@ -35,9 +36,7 @@ class ProblemSetAdminAPI(APIView):
# 过滤条件 # 过滤条件
keyword = request.GET.get("keyword", "").strip() keyword = request.GET.get("keyword", "").strip()
if keyword: if keyword:
problem_sets = problem_sets.filter( problem_sets = problem_sets.filter(Q(title__icontains=keyword) | Q(description__icontains=keyword))
Q(title__icontains=keyword) | Q(description__icontains=keyword)
)
difficulty = request.GET.get("difficulty") difficulty = request.GET.get("difficulty")
if difficulty: if difficulty:
@@ -129,12 +128,8 @@ class ProblemSetProblemAdminAPI(APIView):
except ProblemSet.DoesNotExist: except ProblemSet.DoesNotExist:
return self.error("题单不存在") return self.error("题单不存在")
problems = ProblemSetProblem.objects.filter(problemset=problem_set).order_by( problems = ProblemSetProblem.objects.filter(problemset=problem_set).order_by("order")
"order" serializer = ProblemSetProblemSerializer(problems, many=True, context={"request": request})
)
serializer = ProblemSetProblemSerializer(
problems, many=True, context={"request": request}
)
return self.success(serializer.data) return self.success(serializer.data)
@super_admin_required @super_admin_required
@@ -158,9 +153,7 @@ class ProblemSetProblemAdminAPI(APIView):
return self.error("题目不存在或不可见") return self.error("题目不存在或不可见")
# 检查题目是否已经在题单中 # 检查题目是否已经在题单中
if ProblemSetProblem.objects.filter( if ProblemSetProblem.objects.filter(problemset=problem_set, problem=problem).exists():
problemset=problem_set, problem=problem
).exists():
return self.error("题目已在该题单中") return self.error("题目已在该题单中")
ProblemSetProblem.objects.create( ProblemSetProblem.objects.create(
@@ -188,9 +181,7 @@ class ProblemSetProblemAdminAPI(APIView):
return self.error("题单不存在") return self.error("题单不存在")
try: try:
problem_set_problem = ProblemSetProblem.objects.get( problem_set_problem = ProblemSetProblem.objects.get(id=problem_set_problem_id, problemset=problem_set)
id=problem_set_problem_id, problemset=problem_set
)
except ProblemSetProblem.DoesNotExist: except ProblemSetProblem.DoesNotExist:
return self.error("题目不在该题单中") return self.error("题目不在该题单中")
@@ -206,10 +197,10 @@ class ProblemSetProblemAdminAPI(APIView):
problem_set_problem.hint = data["hint"] problem_set_problem.hint = data["hint"]
problem_set_problem.save() problem_set_problem.save()
# 同步所有用户的进度 # 同步所有用户的进度
ProblemSetProgress.sync_all_progress_for_problemset(problem_set) ProblemSetProgress.sync_all_progress_for_problemset(problem_set)
return self.success("题目已更新") return self.success("题目已更新")
@super_admin_required @super_admin_required
@@ -222,14 +213,12 @@ class ProblemSetProblemAdminAPI(APIView):
return self.error("题单不存在") return self.error("题单不存在")
try: try:
problem_set_problem = ProblemSetProblem.objects.get( problem_set_problem = ProblemSetProblem.objects.get(id=problem_set_problem_id, problemset=problem_set)
id=problem_set_problem_id, problemset=problem_set
)
problem_set_problem.delete() problem_set_problem.delete()
# 同步所有用户的进度 # 同步所有用户的进度
ProblemSetProgress.sync_all_progress_for_problemset(problem_set) ProblemSetProgress.sync_all_progress_for_problemset(problem_set)
return self.success("题目已从题单中移除") return self.success("题目已从题单中移除")
except ProblemSetProblem.DoesNotExist: except ProblemSetProblem.DoesNotExist:
return self.error("题目不在该题单中") return self.error("题目不在该题单中")
@@ -283,10 +272,10 @@ class ProblemSetBadgeAdminAPI(APIView):
return self.error("奖章不存在") return self.error("奖章不存在")
data = request.data data = request.data
# 记录是否修改了条件相关的字段 # 记录是否修改了条件相关的字段
condition_changed = False condition_changed = False
# 更新奖章属性 # 更新奖章属性
if "name" in data: if "name" in data:
badge.name = data["name"] badge.name = data["name"]
@@ -304,7 +293,7 @@ class ProblemSetBadgeAdminAPI(APIView):
badge.level = data["level"] badge.level = data["level"]
badge.save() badge.save()
# 如果修改了条件,重新计算所有用户的徽章资格 # 如果修改了条件,重新计算所有用户的徽章资格
if condition_changed: if condition_changed:
try: try:
@@ -312,7 +301,7 @@ class ProblemSetBadgeAdminAPI(APIView):
return self.success("奖章已更新,并重新计算了所有用户的徽章资格") return self.success("奖章已更新,并重新计算了所有用户的徽章资格")
except Exception as e: except Exception as e:
return self.error(f"奖章已更新,但重新计算徽章资格时出错: {str(e)}") return self.error(f"奖章已更新,但重新计算徽章资格时出错: {str(e)}")
return self.success("奖章已更新") return self.success("奖章已更新")
@super_admin_required @super_admin_required
@@ -344,9 +333,7 @@ class ProblemSetProgressAdminAPI(APIView):
except ProblemSet.DoesNotExist: except ProblemSet.DoesNotExist:
return self.error("题单不存在") return self.error("题单不存在")
progress_list = ProblemSetProgress.objects.filter( progress_list = ProblemSetProgress.objects.filter(problemset=problem_set).order_by("-join_time")
problemset=problem_set
).order_by("-join_time")
serializer = ProblemSetProgressSerializer(progress_list, many=True) serializer = ProblemSetProgressSerializer(progress_list, many=True)
return self.success(serializer.data) return self.success(serializer.data)
@@ -360,9 +347,7 @@ class ProblemSetProgressAdminAPI(APIView):
return self.error("题单不存在") return self.error("题单不存在")
try: try:
progress = ProblemSetProgress.objects.get( progress = ProblemSetProgress.objects.get(problemset=problem_set, user_id=user_id)
problemset=problem_set, user_id=user_id
)
progress.delete() progress.delete()
return self.success("用户已从题单中移除") return self.success("用户已从题单中移除")
except ProblemSetProgress.DoesNotExist: except ProblemSetProgress.DoesNotExist:
@@ -371,7 +356,7 @@ class ProblemSetProgressAdminAPI(APIView):
class ProblemSetSyncAPI(APIView): class ProblemSetSyncAPI(APIView):
"""题单同步管理API""" """题单同步管理API"""
@super_admin_required @super_admin_required
def post(self, request, problem_set_id): def post(self, request, problem_set_id):
"""手动同步题单的所有用户进度(管理员)""" """手动同步题单的所有用户进度(管理员)"""
@@ -380,10 +365,10 @@ class ProblemSetSyncAPI(APIView):
ensure_created_by(problem_set, request.user) ensure_created_by(problem_set, request.user)
except ProblemSet.DoesNotExist: except ProblemSet.DoesNotExist:
return self.error("题单不存在") return self.error("题单不存在")
# 同步所有用户的进度 # 同步所有用户的进度
synced_count = ProblemSetProgress.sync_all_progress_for_problemset(problem_set) synced_count = ProblemSetProgress.sync_all_progress_for_problemset(problem_set)
return self.success(f"已同步 {synced_count} 个用户的进度") return self.success(f"已同步 {synced_count} 个用户的进度")
@@ -419,7 +404,7 @@ class ProblemSetStatusAPI(APIView):
return self.error("题单不存在") return self.error("题单不存在")
status = data.get("status") status = data.get("status")
if status not in ["active", "archived", "draft"]: if status not in ProblemSetStatus.values:
return self.error("无效的状态") return self.error("无效的状态")
problem_set.status = status problem_set.status = status

View File

@@ -32,18 +32,14 @@ class ProblemSetAPI(APIView):
"""获取题单列表""" """获取题单列表"""
# 预加载创建者信息 # 预加载创建者信息
problem_sets = ProblemSet.objects.filter(visible=True).exclude(status="draft").select_related("created_by") problem_sets = ProblemSet.objects.filter(visible=True).exclude(status="draft").select_related("created_by")
# 使用annotate在查询时计算题目数量避免N+1查询 # 使用annotate在查询时计算题目数量避免N+1查询
problem_sets = problem_sets.annotate( problem_sets = problem_sets.annotate(problems_count=Count("problemsetproblem", distinct=True))
problems_count=Count("problemsetproblem", distinct=True)
)
# 过滤条件 # 过滤条件
keyword = request.GET.get("keyword", "").strip() keyword = request.GET.get("keyword", "").strip()
if keyword: if keyword:
problem_sets = problem_sets.filter( problem_sets = problem_sets.filter(Q(title__icontains=keyword) | Q(description__icontains=keyword))
Q(title__icontains=keyword) | Q(description__icontains=keyword)
)
difficulty = request.GET.get("difficulty") difficulty = request.GET.get("difficulty")
if difficulty: if difficulty:
@@ -67,33 +63,19 @@ class ProblemSetAPI(APIView):
if request.user.is_authenticated: if request.user.is_authenticated:
# 先获取所有题单ID不应用prefetch_related只获取ID # 先获取所有题单ID不应用prefetch_related只获取ID
problem_set_ids = list(problem_sets.values_list("id", flat=True)) problem_set_ids = list(problem_sets.values_list("id", flat=True))
if problem_set_ids: if problem_set_ids:
# 批量查询用户在这些题单中的进度 # 批量查询用户在这些题单中的进度
user_progresses = ProblemSetProgress.objects.filter( user_progresses = ProblemSetProgress.objects.filter(problemset_id__in=problem_set_ids, user=request.user).select_related("problemset")
problemset_id__in=problem_set_ids,
user=request.user
).select_related("problemset")
# 构建映射题单ID -> 进度对象 # 构建映射题单ID -> 进度对象
user_progress_map = {progress.problemset_id: progress for progress in user_progresses} user_progress_map = {progress.problemset_id: progress for progress in user_progresses}
# 批量查询用户已获得的奖章ID这些题单相关的 # 批量查询用户已获得的奖章ID这些题单相关的
user_earned_badge_ids = set( user_earned_badge_ids = set(UserBadge.objects.filter(user=request.user, badge__problemset_id__in=problem_set_ids).values_list("badge_id", flat=True))
UserBadge.objects.filter(
user=request.user,
badge__problemset_id__in=problem_set_ids
).values_list('badge_id', flat=True)
)
# 预加载奖章信息在获取ID之后应用避免在获取ID时也预加载 # 预加载奖章信息在获取ID之后应用避免在获取ID时也预加载
problem_sets = problem_sets.prefetch_related( problem_sets = problem_sets.prefetch_related(Prefetch("problemsetbadge_set", queryset=ProblemSetBadge.objects.all(), to_attr="badges"))
Prefetch(
"problemsetbadge_set",
queryset=ProblemSetBadge.objects.all(),
to_attr="badges"
)
)
# 将用户进度映射和已获得的奖章ID集合存储到request中供序列化器使用 # 将用户进度映射和已获得的奖章ID集合存储到request中供序列化器使用
request._user_progress_map = user_progress_map request._user_progress_map = user_progress_map
request._user_earned_badge_ids = user_earned_badge_ids request._user_earned_badge_ids = user_earned_badge_ids
@@ -108,11 +90,7 @@ class ProblemSetDetailAPI(APIView):
def get(self, request, problem_set_id): def get(self, request, problem_set_id):
"""获取题单详情""" """获取题单详情"""
try: try:
problem_set = ( problem_set = ProblemSet.objects.filter(id=problem_set_id, visible=True).exclude(status="draft").get()
ProblemSet.objects.filter(id=problem_set_id, visible=True)
.exclude(status="draft")
.get()
)
except ProblemSet.DoesNotExist: except ProblemSet.DoesNotExist:
return self.error("题单不存在") return self.error("题单不存在")
@@ -126,32 +104,19 @@ class ProblemSetProblemAPI(APIView):
def get(self, request, problem_set_id): def get(self, request, problem_set_id):
"""获取题单中的题目列表""" """获取题单中的题目列表"""
try: try:
problem_set = ( problem_set = ProblemSet.objects.filter(id=problem_set_id, visible=True).exclude(status="draft").get()
ProblemSet.objects.filter(id=problem_set_id, visible=True)
.exclude(status="draft")
.get()
)
except ProblemSet.DoesNotExist: except ProblemSet.DoesNotExist:
return self.error("题单不存在") return self.error("题单不存在")
problems = ( problems = ProblemSetProblem.objects.filter(problemset=problem_set).select_related("problem__created_by").prefetch_related("problem__tags").order_by("order")
ProblemSetProblem.objects.filter(problemset=problem_set)
.select_related("problem__created_by")
.prefetch_related("problem__tags")
.order_by("order")
)
# 预取当前用户的题单进度,供 get_is_completed 使用,避免 N+1 # 预取当前用户的题单进度,供 get_is_completed 使用,避免 N+1
user_progress = None user_progress = None
if request.user.is_authenticated: if request.user.is_authenticated:
try: try:
user_progress = ProblemSetProgress.objects.get( user_progress = ProblemSetProgress.objects.get(problemset=problem_set, user=request.user)
problemset=problem_set, user=request.user
)
except ProblemSetProgress.DoesNotExist: except ProblemSetProgress.DoesNotExist:
pass pass
serializer = ProblemSetProblemSerializer( serializer = ProblemSetProblemSerializer(problems, many=True, context={"request": request, "user_progress": user_progress})
problems, many=True, context={"request": request, "user_progress": user_progress}
)
return self.success(serializer.data) return self.success(serializer.data)
@@ -163,23 +128,15 @@ class ProblemSetProgressAPI(APIView):
"""加入题单""" """加入题单"""
data = request.data data = request.data
try: try:
problem_set = ( problem_set = ProblemSet.objects.filter(id=data["problemset_id"], visible=True).exclude(status="draft").get()
ProblemSet.objects.filter(id=data["problemset_id"], visible=True)
.exclude(status="draft")
.get()
)
except ProblemSet.DoesNotExist: except ProblemSet.DoesNotExist:
return self.error("题单不存在") return self.error("题单不存在")
if ProblemSetProgress.objects.filter( if ProblemSetProgress.objects.filter(problemset=problem_set, user=request.user).exists():
problemset=problem_set, user=request.user
).exists():
return self.error("已经加入该题单") return self.error("已经加入该题单")
# 创建进度记录 # 创建进度记录
progress = ProblemSetProgress.objects.create( progress = ProblemSetProgress.objects.create(problemset=problem_set, user=request.user)
problemset=problem_set, user=request.user
)
progress.update_progress() progress.update_progress()
return self.success("成功加入题单") return self.success("成功加入题单")
@@ -187,18 +144,12 @@ class ProblemSetProgressAPI(APIView):
def get(self, request, problem_set_id): def get(self, request, problem_set_id):
"""获取题单进度""" """获取题单进度"""
try: try:
problem_set = ( problem_set = ProblemSet.objects.filter(id=problem_set_id, visible=True).exclude(status="draft").get()
ProblemSet.objects.filter(id=problem_set_id, visible=True)
.exclude(status="draft")
.get()
)
except ProblemSet.DoesNotExist: except ProblemSet.DoesNotExist:
return self.error("题单不存在") return self.error("题单不存在")
try: try:
progress = ProblemSetProgress.objects.get( progress = ProblemSetProgress.objects.get(problemset=problem_set, user=request.user)
problemset=problem_set, user=request.user
)
except ProblemSetProgress.DoesNotExist: except ProblemSetProgress.DoesNotExist:
return self.error("未加入该题单") return self.error("未加入该题单")
@@ -210,18 +161,12 @@ class ProblemSetProgressAPI(APIView):
"""更新进度""" """更新进度"""
data = request.data data = request.data
try: try:
problem_set = ( problem_set = ProblemSet.objects.filter(id=data["problemset_id"], visible=True).exclude(status="draft").get()
ProblemSet.objects.filter(id=data["problemset_id"], visible=True)
.exclude(status="draft")
.get()
)
except ProblemSet.DoesNotExist: except ProblemSet.DoesNotExist:
return self.error("题单不存在") return self.error("题单不存在")
try: try:
progress = ProblemSetProgress.objects.get( progress = ProblemSetProgress.objects.get(problemset=problem_set, user=request.user)
problemset=problem_set, user=request.user
)
except ProblemSetProgress.DoesNotExist: except ProblemSetProgress.DoesNotExist:
return self.error("未加入该题单") return self.error("未加入该题单")
@@ -230,9 +175,7 @@ class ProblemSetProgressAPI(APIView):
# 获取该题目在题单中的分值 # 获取该题目在题单中的分值
try: try:
problemset_problem = ProblemSetProblem.objects.get( problemset_problem = ProblemSetProblem.objects.get(problemset=problem_set, problem_id=problem_id)
problemset=problem_set, problem_id=problem_id
)
problem_score = problemset_problem.score problem_score = problemset_problem.score
except ProblemSetProblem.DoesNotExist: except ProblemSetProblem.DoesNotExist:
problem_score = 0 problem_score = 0
@@ -296,9 +239,7 @@ class UserProgressAPI(APIView):
def get(self, request): def get(self, request):
"""获取用户的题单进度列表""" """获取用户的题单进度列表"""
progress_list = ProblemSetProgress.objects.filter(user=request.user).order_by( progress_list = ProblemSetProgress.objects.filter(user=request.user).order_by("-join_time")
"-join_time"
)
serializer = ProblemSetProgressSerializer(progress_list, many=True) serializer = ProblemSetProgressSerializer(progress_list, many=True)
return self.success(serializer.data) return self.success(serializer.data)
@@ -315,16 +256,12 @@ class UserBadgeAPI(APIView):
# 获取指定用户的徽章 # 获取指定用户的徽章
try: try:
target_user = User.objects.get(username=username, is_disabled=False) target_user = User.objects.get(username=username, is_disabled=False)
badges = UserBadge.objects.filter(user=target_user).order_by( badges = UserBadge.objects.filter(user=target_user).order_by("-earned_time")
"-earned_time"
)
except User.DoesNotExist: except User.DoesNotExist:
return self.error("用户不存在") return self.error("用户不存在")
else: else:
# 获取当前用户的徽章 # 获取当前用户的徽章
badges = UserBadge.objects.filter(user=request.user).order_by( badges = UserBadge.objects.filter(user=request.user).order_by("-earned_time")
"-earned_time"
)
serializer = UserBadgeSerializer(badges, many=True) serializer = UserBadgeSerializer(badges, many=True)
return self.success(serializer.data) return self.success(serializer.data)
@@ -336,11 +273,7 @@ class ProblemSetBadgeAPI(APIView):
def get(self, request, problem_set_id): def get(self, request, problem_set_id):
"""获取题单的奖章列表""" """获取题单的奖章列表"""
try: try:
problem_set = ( problem_set = ProblemSet.objects.filter(id=problem_set_id, visible=True).exclude(status="draft").get()
ProblemSet.objects.filter(id=problem_set_id, visible=True)
.exclude(status="draft")
.get()
)
except ProblemSet.DoesNotExist: except ProblemSet.DoesNotExist:
return self.error("题单不存在") return self.error("题单不存在")
@@ -355,18 +288,12 @@ class ProblemSetUserProgressAPI(APIView):
def get(self, request, problem_set_id: int): def get(self, request, problem_set_id: int):
"""获取题单的用户进度列表""" """获取题单的用户进度列表"""
try: try:
problem_set = ( problem_set = ProblemSet.objects.filter(id=problem_set_id, visible=True).exclude(status="draft").get()
ProblemSet.objects.filter(id=problem_set_id, visible=True)
.exclude(status="draft")
.get()
)
except ProblemSet.DoesNotExist: except ProblemSet.DoesNotExist:
return self.error("题单不存在") return self.error("题单不存在")
# 获取所有参与该题单的用户进度,使用 select_related 预加载用户信息 # 获取所有参与该题单的用户进度,使用 select_related 预加载用户信息
progresses = ProblemSetProgress.objects.filter( progresses = ProblemSetProgress.objects.filter(problemset=problem_set).select_related("user")
problemset=problem_set
).select_related("user")
# 班级过滤 # 班级过滤
class_name = request.GET.get("class_name", "").strip() class_name = request.GET.get("class_name", "").strip()
@@ -386,9 +313,7 @@ class ProblemSetUserProgressAPI(APIView):
progresses = progresses.filter(completed_problems_count=0) progresses = progresses.filter(completed_problems_count=0)
# 排序 # 排序
progresses = progresses.order_by( progresses = progresses.order_by("-is_completed", "-progress_percentage", "join_time")
"-is_completed", "-progress_percentage", "join_time"
)
# 计算统计数据(基于所有数据,而非分页数据) # 计算统计数据(基于所有数据,而非分页数据)
# 使用一次查询获取所有统计数据 # 使用一次查询获取所有统计数据
@@ -416,12 +341,9 @@ class ProblemSetUserProgressAPI(APIView):
# 提前获取题单的所有题目(用于前端显示未完成题目和序列化器) # 提前获取题单的所有题目(用于前端显示未完成题目和序列化器)
# 使用 select_related 和 only 优化查询,只选择需要的字段 # 使用 select_related 和 only 优化查询,只选择需要的字段
all_problemset_problems = ( all_problemset_problems = (
ProblemSetProblem.objects.filter(problemset=problem_set) ProblemSetProblem.objects.filter(problemset=problem_set).select_related("problem").only("problem__id", "problem___id", "problem__title", "order").order_by("order")
.select_related("problem")
.only("problem__id", "problem___id", "problem__title", "order")
.order_by("order")
) )
# 构建题单所有题目的数据结构和映射 # 构建题单所有题目的数据结构和映射
all_problems_list = [] all_problems_list = []
all_problems_map = {} all_problems_map = {}
@@ -444,11 +366,7 @@ class ProblemSetUserProgressAPI(APIView):
completed_problem_ids.update(progress.progress_detail.keys()) completed_problem_ids.update(progress.progress_detail.keys())
# 从已加载的题单题目中构建 problems_dict避免重复查询 # 从已加载的题单题目中构建 problems_dict避免重复查询
problems_dict = { problems_dict = {pid: all_problems_map[pid] for pid in completed_problem_ids if pid in all_problems_map}
pid: all_problems_map[pid]
for pid in completed_problem_ids
if pid in all_problems_map
}
# 将预加载的问题字典存储到 request 中,供序列化器使用 # 将预加载的问题字典存储到 request 中,供序列化器使用
request._problems_dict_cache = problems_dict request._problems_dict_cache = problems_dict

View File

@@ -0,0 +1,33 @@
# Generated by Django 6.0.4 on 2026-05-09 08:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("submission", "0004_submission_problem_user_idx"),
]
operations = [
migrations.AlterField(
model_name="submission",
name="result",
field=models.IntegerField(
choices=[
(-2, "Compile Error"),
(-1, "Wrong Answer"),
(0, "Accepted"),
(1, "CPU Time Limit Exceeded"),
(2, "Real Time Limit Exceeded"),
(3, "Memory Limit Exceeded"),
(4, "Runtime Error"),
(5, "System Error"),
(6, "Pending"),
(7, "Judging"),
(8, "Partially Accepted"),
],
db_index=True,
default=6,
),
),
]

View File

@@ -7,18 +7,18 @@ from utils.models import JSONField
from utils.shortcuts import rand_str from utils.shortcuts import rand_str
class JudgeStatus: class JudgeStatus(models.IntegerChoices):
COMPILE_ERROR = -2 COMPILE_ERROR = -2, "Compile Error"
WRONG_ANSWER = -1 WRONG_ANSWER = -1, "Wrong Answer"
ACCEPTED = 0 ACCEPTED = 0, "Accepted"
CPU_TIME_LIMIT_EXCEEDED = 1 CPU_TIME_LIMIT_EXCEEDED = 1, "CPU Time Limit Exceeded"
REAL_TIME_LIMIT_EXCEEDED = 2 REAL_TIME_LIMIT_EXCEEDED = 2, "Real Time Limit Exceeded"
MEMORY_LIMIT_EXCEEDED = 3 MEMORY_LIMIT_EXCEEDED = 3, "Memory Limit Exceeded"
RUNTIME_ERROR = 4 RUNTIME_ERROR = 4, "Runtime Error"
SYSTEM_ERROR = 5 SYSTEM_ERROR = 5, "System Error"
PENDING = 6 PENDING = 6, "Pending"
JUDGING = 7 JUDGING = 7, "Judging"
PARTIALLY_ACCEPTED = 8 PARTIALLY_ACCEPTED = 8, "Partially Accepted"
class Submission(models.Model): class Submission(models.Model):
@@ -29,7 +29,7 @@ class Submission(models.Model):
user_id = models.IntegerField(db_index=True) user_id = models.IntegerField(db_index=True)
username = models.TextField() username = models.TextField()
code = models.TextField() code = models.TextField()
result = models.IntegerField(db_index=True, default=JudgeStatus.PENDING) result = models.IntegerField(choices=JudgeStatus.choices, db_index=True, default=JudgeStatus.PENDING)
# 从JudgeServer返回的判题详情 # 从JudgeServer返回的判题详情
info = JSONField(default=dict) info = JSONField(default=dict)
language = models.TextField() language = models.TextField()
@@ -40,11 +40,7 @@ class Submission(models.Model):
ip = models.TextField(null=True) ip = models.TextField(null=True)
def check_user_permission(self, user, check_share=True): def check_user_permission(self, user, check_share=True):
if ( if self.user_id == user.id or not user.is_regular_user() or self.problem.created_by_id == user.id:
self.user_id == user.id
or not user.is_regular_user()
or self.problem.created_by_id == user.id
):
return True return True
if check_share: if check_share:
@@ -58,15 +54,9 @@ class Submission(models.Model):
db_table = "submission" db_table = "submission"
ordering = ("-create_time",) ordering = ("-create_time",)
indexes = [ indexes = [
models.Index( models.Index(fields=["user_id", "create_time"], name="user_create_time_idx"),
fields=["user_id", "create_time"], name="user_create_time_idx" models.Index(fields=["contest_id", "-create_time"], name="contest_create_time_idx"),
), models.Index(fields=["problem_id", "user_id"], name="problem_user_idx"),
models.Index(
fields=["contest_id", "-create_time"], name="contest_create_time_idx"
),
models.Index(
fields=["problem_id", "user_id"], name="problem_user_idx"
),
] ]
def __str__(self): def __str__(self):

View File

@@ -3,16 +3,22 @@ from django.db import models
from account.models import User from account.models import User
class TutorialType(models.TextChoices):
PYTHON = "python", "Python"
C = "c", "C"
class ExerciseType(models.TextChoices):
MCQ = "mcq", "选择题"
SORT = "sort", "代码排序"
FILL = "fill", "代码填空"
class Tutorial(models.Model): class Tutorial(models.Model):
TYPE_CHOICES = [
('python', 'Python'),
('c', 'C'),
]
title = models.CharField(max_length=128) title = models.CharField(max_length=128)
content = models.TextField() content = models.TextField()
code = models.TextField(null=True, blank=True) code = models.TextField(null=True, blank=True)
type = models.CharField(max_length=10, choices=TYPE_CHOICES, default='python') type = models.CharField(max_length=10, choices=TutorialType.choices, default=TutorialType.PYTHON)
created_by = models.ForeignKey(User, on_delete=models.CASCADE) created_by = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
@@ -21,21 +27,15 @@ class Tutorial(models.Model):
class Meta: class Meta:
db_table = "tutorial" db_table = "tutorial"
ordering = ['order', '-created_at'] ordering = ["order", "-created_at"]
def __str__(self): def __str__(self):
return self.title return self.title
class Exercise(models.Model): class Exercise(models.Model):
TYPE_CHOICES = [
("mcq", "选择题"),
("sort", "代码排序"),
("fill", "代码填空"),
]
tutorial = models.ForeignKey(Tutorial, on_delete=models.CASCADE, related_name="exercises") tutorial = models.ForeignKey(Tutorial, on_delete=models.CASCADE, related_name="exercises")
type = models.CharField(max_length=16, choices=TYPE_CHOICES) type = models.CharField(max_length=16, choices=ExerciseType.choices)
data = models.JSONField() data = models.JSONField()
order = models.IntegerField(default=0) order = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
@@ -45,4 +45,4 @@ class Exercise(models.Model):
ordering = ["order", "created_at"] ordering = ["order", "created_at"]
def __str__(self): def __str__(self):
return f"{self.get_type_display()} (Order {self.order})" return f"{self.get_type_display()} (Order {self.order})"

View File

@@ -2,7 +2,7 @@ from rest_framework import serializers
from account.serializers import UserSerializer from account.serializers import UserSerializer
from .models import Exercise, Tutorial from .models import Exercise, ExerciseType, Tutorial
class TutorialListSerializer(serializers.ModelSerializer): class TutorialListSerializer(serializers.ModelSerializer):
@@ -65,13 +65,13 @@ class ExerciseSerializer(serializers.ModelSerializer):
class CreateExerciseSerializer(serializers.Serializer): class CreateExerciseSerializer(serializers.Serializer):
tutorial_id = serializers.IntegerField() tutorial_id = serializers.IntegerField()
type = serializers.ChoiceField(choices=["mcq", "sort", "fill"]) type = serializers.ChoiceField(choices=ExerciseType.choices)
data = serializers.JSONField() data = serializers.JSONField()
order = serializers.IntegerField(default=0) order = serializers.IntegerField(default=0)
class EditExerciseSerializer(serializers.Serializer): class EditExerciseSerializer(serializers.Serializer):
id = serializers.IntegerField() id = serializers.IntegerField()
type = serializers.ChoiceField(choices=["mcq", "sort", "fill"]) type = serializers.ChoiceField(choices=ExerciseType.choices)
data = serializers.JSONField() data = serializers.JSONField()
order = serializers.IntegerField(default=0) order = serializers.IntegerField(default=0)

View File

@@ -1,24 +1,20 @@
class Choices: from django.db import models
@classmethod
def choices(cls):
d = cls.__dict__
return [d[item] for item in d.keys() if not item.startswith("__")]
class ContestType: class ContestType(models.TextChoices):
PUBLIC_CONTEST = "Public" PUBLIC_CONTEST = "Public", "Public"
PASSWORD_PROTECTED_CONTEST = "Password Protected" PASSWORD_PROTECTED_CONTEST = "Password Protected", "Password Protected"
class ContestStatus: class ContestStatus(models.TextChoices):
CONTEST_NOT_START = "1" CONTEST_NOT_START = "1", "Not Started"
CONTEST_ENDED = "-1" CONTEST_ENDED = "-1", "Ended"
CONTEST_UNDERWAY = "0" CONTEST_UNDERWAY = "0", "Underway"
class ContestRuleType(Choices): class ContestRuleType(models.TextChoices):
ACM = "ACM" ACM = "ACM", "ACM"
OI = "OI" OI = "OI", "OI"
class CacheKey: class CacheKey:
@@ -31,10 +27,10 @@ class CacheKey:
user_activity_rank = "user_activity_rank" user_activity_rank = "user_activity_rank"
class Difficulty(Choices): class Difficulty(models.TextChoices):
LOW = "Low" LOW = "Low", "Low"
MID = "Mid" MID = "Mid", "Mid"
HIGH = "High" HIGH = "High", "High"
CONTEST_PASSWORD_SESSION_KEY = "contest_password" CONTEST_PASSWORD_SESSION_KEY = "contest_password"

View File

@@ -12,18 +12,11 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "oj.settings")
django.setup() django.setup()
from django.conf import settings from django.conf import settings
from account.models import User, UserProfile, AdminType, ProblemPermission from account.models import User, UserProfile, AdminType, ProblemPermission
from problem.models import Problem, ProblemTag, ProblemDifficulty, ProblemRuleType from problem.models import Problem, ProblemTag, ProblemRuleType
from utils.constants import Difficulty
admin_type_map = { admin_type_map = {0: AdminType.REGULAR_USER, 1: AdminType.ADMIN, 2: AdminType.SUPER_ADMIN}
0: AdminType.REGULAR_USER, languages_map = {1: "C", 2: "C++", 3: "Java"}
1: AdminType.ADMIN,
2: AdminType.SUPER_ADMIN
}
languages_map = {
1: "C",
2: "C++",
3: "Java"
}
email_regex = re.compile(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)") email_regex = re.compile(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)")
# pk -> name # pk -> name
@@ -46,9 +39,7 @@ def get_input_result():
def set_problem_display_id_prefix(): def set_problem_display_id_prefix():
while True: while True:
print("Please input a prefix which will be used in all the imported problem's displayID") print("Please input a prefix which will be used in all the imported problem's displayID")
print( print("For example, if your input is 'old'(no quote), the problems' display id will be old1, old2, old3..\ninput:", end="")
"For example, if your input is 'old'(no quote), the problems' display id will be old1, old2, old3..\ninput:",
end="")
resp = input() resp = input()
if resp.strip(): if resp.strip():
return resp.strip() return resp.strip()
@@ -60,8 +51,8 @@ def set_problem_display_id_prefix():
def get_stripped_output_md5(test_case_id, output_name): def get_stripped_output_md5(test_case_id, output_name):
output_path = os.path.join(settings.TEST_CASE_DIR, test_case_id, output_name) output_path = os.path.join(settings.TEST_CASE_DIR, test_case_id, output_name)
with open(output_path, 'r') as f: with open(output_path, "r") as f:
return hashlib.md5(f.read().rstrip().encode('utf-8')).hexdigest() return hashlib.md5(f.read().rstrip().encode("utf-8")).hexdigest()
def get_test_case_score(test_case_id): def get_test_case_score(test_case_id):
@@ -79,9 +70,7 @@ def get_test_case_score(test_case_id):
test_case["stripped_output_md5"] = test_case.pop("striped_output_md5") test_case["stripped_output_md5"] = test_case.pop("striped_output_md5")
else: else:
test_case["stripped_output_md5"] = get_stripped_output_md5(test_case_id, test_case["output_name"]) test_case["stripped_output_md5"] = get_stripped_output_md5(test_case_id, test_case["output_name"])
test_case_score.append({"input_name": test_case["input_name"], test_case_score.append({"input_name": test_case["input_name"], "output_name": test_case.get("output_name", "-"), "score": 0})
"output_name": test_case.get("output_name", "-"),
"score": 0})
if need_rewrite: if need_rewrite:
with open(info_path, "w") as f: with open(info_path, "w") as f:
f.write(json.dumps(info)) f.write(json.dumps(info))
@@ -120,7 +109,7 @@ def import_users():
def import_tags(): def import_tags():
i = 0 i = 0
print("\nFind these tags in old data:") print("\nFind these tags in old data:")
print(", ".join(tags.values()), '\n') print(", ".join(tags.values()), "\n")
print("import tags now? (yes/no)") print("import tags now? (yes/no)")
if get_input_result(): if get_input_result():
for tagname in tags.values(): for tagname in tags.values():
@@ -149,14 +138,13 @@ def import_problems():
print("%s has the same display_id with the db problem" % data["title"]) print("%s has the same display_id with the db problem" % data["title"])
continue continue
try: try:
creator_id = \ creator_id = User.objects.filter(username=users[data["created_by"]]["username"]).values_list("id", flat=True)[0]
User.objects.filter(username=users[data["created_by"]]["username"]).values_list("id", flat=True)[0]
except (User.DoesNotExist, IndexError): except (User.DoesNotExist, IndexError):
print("The origin creator does not exist, set it to default_creator") print("The origin creator does not exist, set it to default_creator")
creator_id = default_creator.id creator_id = default_creator.id
data["created_by_id"] = creator_id data["created_by_id"] = creator_id
data.pop("created_by") data.pop("created_by")
data["difficulty"] = ProblemDifficulty.Mid data["difficulty"] = Difficulty.MID
if data["spj_language"]: if data["spj_language"]:
data["spj_language"] = languages_map[data["spj_language"]] data["spj_language"] = languages_map[data["spj_language"]]
data["samples"] = json.loads(data["samples"]) data["samples"] = json.loads(data["samples"])