style: ruff format 全仓库

行宽 180 下把历史遗留的折行表达式合并,无语义改动。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 04:45:26 -06:00
parent db104ac091
commit 3a9ab83ba5
56 changed files with 455 additions and 769 deletions

View File

@@ -151,7 +151,9 @@ def check_contest_permission(check_type="details"):
if error: if error:
return error return error
return await func(*args, **kwargs) return await func(*args, **kwargs)
return _wrapper return _wrapper
return decorator return decorator

View File

@@ -17,14 +17,10 @@ class Command(BaseCommand):
dry_run = options["dry_run"] dry_run = options["dry_run"]
# 所有现存非比赛题目的 PK 集合 # 所有现存非比赛题目的 PK 集合
existing_ids = set( existing_ids = set(Problem.objects.filter(contest__isnull=True).values_list("id", flat=True))
Problem.objects.filter(contest__isnull=True).values_list("id", flat=True)
)
self.stdout.write(f"现存题库题目数: {len(existing_ids)}") self.stdout.write(f"现存题库题目数: {len(existing_ids)}")
profiles = UserProfile.objects.select_related("user").exclude( profiles = UserProfile.objects.select_related("user").exclude(acm_problems_status={})
acm_problems_status={}
)
total = profiles.count() total = profiles.count()
self.stdout.write(f"检查用户数: {total}{'dry-run 模式)' if dry_run else ''}") self.stdout.write(f"检查用户数: {total}{'dry-run 模式)' if dry_run else ''}")
@@ -38,17 +34,11 @@ class Command(BaseCommand):
if not stale_keys: if not stale_keys:
continue continue
removed_accepted = sum( removed_accepted = sum(1 for k in stale_keys if problems[k].get("status") in ACCEPTED_STATUSES)
1
for k in stale_keys
if problems[k].get("status") in ACCEPTED_STATUSES
)
stale_display = [problems[k].get("_id", k) for k in stale_keys] stale_display = [problems[k].get("_id", k) for k in stale_keys]
self.stdout.write( self.stdout.write(
f" 用户 {profile.user.username}" f" 用户 {profile.user.username} | 删除 {len(stale_keys)} 题: {', '.join(stale_display)}{f' | 其中已AC {removed_accepted}' if removed_accepted else ''}"
f" | 删除 {len(stale_keys)} 题: {', '.join(stale_display)}"
f"{f' | 其中已AC {removed_accepted}' if removed_accepted else ''}"
) )
if dry_run: if dry_run:

View File

@@ -13,11 +13,6 @@ def send_email_async(from_name, to_email, to_name, subject, content):
if not SysOptions.smtp_config: if not SysOptions.smtp_config:
return return
try: try:
send_email(smtp_config=SysOptions.smtp_config, send_email(smtp_config=SysOptions.smtp_config, from_name=from_name, to_email=to_email, to_name=to_name, subject=subject, content=content)
from_name=from_name,
to_email=to_email,
to_name=to_name,
subject=subject,
content=content)
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)

View File

@@ -61,12 +61,7 @@ class UserAdminAPI(APIView):
try: try:
with transaction.atomic(): with transaction.atomic():
ret = User.objects.bulk_create(user_list) ret = User.objects.bulk_create(user_list)
UserProfile.objects.bulk_create( UserProfile.objects.bulk_create([UserProfile(user=ret[i], real_name=data[i][3]) for i in range(len(ret))])
[
UserProfile(user=ret[i], real_name=data[i][3])
for i in range(len(ret))
]
)
return self.success() return self.success()
except IntegrityError as e: except IntegrityError as e:
# Extract detail from exception message # Extract detail from exception message
@@ -85,17 +80,9 @@ class UserAdminAPI(APIView):
user = User.objects.get(id=data["id"]) user = User.objects.get(id=data["id"])
except User.DoesNotExist: except User.DoesNotExist:
return self.error("User does not exist") return self.error("User does not exist")
if ( if User.objects.filter(username=data["username"].lower()).exclude(id=user.id).exists():
User.objects.filter(username=data["username"].lower())
.exclude(id=user.id)
.exists()
):
return self.error("Username already exists") return self.error("Username already exists")
if ( if User.objects.filter(email=data["email"].lower()).exclude(id=user.id).exists():
User.objects.filter(email=data["email"].lower())
.exclude(id=user.id)
.exists()
):
return self.error("Email already exists") return self.error("Email already exists")
pre_username = user.username pre_username = user.username
@@ -136,9 +123,7 @@ class UserAdminAPI(APIView):
user.save() user.save()
if pre_username != user.username: if pre_username != user.username:
Submission.objects.filter(username=pre_username).update( Submission.objects.filter(username=pre_username).update(username=user.username)
username=user.username
)
UserProfile.objects.filter(user=user).update(real_name=data["real_name"]) UserProfile.objects.filter(user=user).update(real_name=data["real_name"])
return self.success(UserAdminSerializer(user).data) return self.success(UserAdminSerializer(user).data)
@@ -158,7 +143,7 @@ class UserAdminAPI(APIView):
# 获取排序参数 # 获取排序参数
order_by = request.GET.get("order_by", "") order_by = request.GET.get("order_by", "")
# 根据排序参数设置排序规则 # 根据排序参数设置排序规则
if order_by == "-last_login": if order_by == "-last_login":
# 最近登录,将 None 值放在最后 # 最近登录,将 None 值放在最后
@@ -174,11 +159,7 @@ class UserAdminAPI(APIView):
keyword = request.GET.get("keyword", None) keyword = request.GET.get("keyword", None)
if keyword: if keyword:
user = user.filter( user = user.filter(Q(username__icontains=keyword) | Q(userprofile__real_name__icontains=keyword) | Q(email__icontains=keyword))
Q(username__icontains=keyword)
| Q(userprofile__real_name__icontains=keyword)
| Q(email__icontains=keyword)
)
return self.success(self.paginate_data(request, user, UserAdminSerializer)) return self.success(self.paginate_data(request, user, UserAdminSerializer))
@super_admin_required @super_admin_required
@@ -223,9 +204,7 @@ class GenerateUserAPI(APIView):
Generate User Generate User
""" """
data = request.data data = request.data
number_max_length = max( number_max_length = max(len(str(data["number_from"])), len(str(data["number_to"])))
len(str(data["number_from"])), len(str(data["number_to"]))
)
if number_max_length + len(data["prefix"]) + len(data["suffix"]) > 32: if number_max_length + len(data["prefix"]) + len(data["suffix"]) > 32:
return self.error("Username should not more than 32 characters") return self.error("Username should not more than 32 characters")
if data["number_from"] > data["number_to"]: if data["number_from"] > data["number_to"]:
@@ -253,9 +232,7 @@ class GenerateUserAPI(APIView):
try: try:
with transaction.atomic(): with transaction.atomic():
ret = User.objects.bulk_create(user_list) ret = User.objects.bulk_create(user_list)
UserProfile.objects.bulk_create( UserProfile.objects.bulk_create([UserProfile(user=user) for user in ret])
[UserProfile(user=user) for user in ret]
)
for item in user_list: for item in user_list:
worksheet.write_string(i, 0, item.username) worksheet.write_string(i, 0, item.username)
worksheet.write_string(i, 1, item.raw_password) worksheet.write_string(i, 1, item.raw_password)
@@ -277,17 +254,17 @@ class ResetUserPasswordAPI(APIView):
""" """
data = request.data data = request.data
user_id = data["id"] user_id = data["id"]
try: try:
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
except User.DoesNotExist: except User.DoesNotExist:
return self.error("User does not exist") return self.error("User does not exist")
# 生成6位随机数字密码(不包括0) # 生成6位随机数字密码(不包括0)
new_password = get_random_string(6, allowed_chars="123456789") new_password = get_random_string(6, allowed_chars="123456789")
# 设置新密码 # 设置新密码
user.set_password(new_password) user.set_password(new_password)
user.save() user.save()
return self.success(new_password) return self.success(new_password)

View File

@@ -431,11 +431,16 @@ class UserRankAPI(AsyncAPIView):
except ValueError: except ValueError:
n = 0 n = 0
profiles = UserProfile.objects.filter( profiles = (
user__admin_type__in=[AdminType.REGULAR_USER, AdminType.STUDENT_ADMIN], UserProfile.objects.filter(
user__is_disabled=False, user__admin_type__in=[AdminType.REGULAR_USER, AdminType.STUDENT_ADMIN],
user__username__icontains=username, user__is_disabled=False,
).select_related("user").filter(accepted_number__gte=0).order_by("-accepted_number", "submission_number") user__username__icontains=username,
)
.select_related("user")
.filter(accepted_number__gte=0)
.order_by("-accepted_number", "submission_number")
)
if n > 0: if n > 0:
profiles = profiles[:n] profiles = profiles[:n]
return self.success(await self.async_paginate_data(request, profiles, RankInfoSerializer)) return self.success(await self.async_paginate_data(request, profiles, RankInfoSerializer))
@@ -457,12 +462,7 @@ class UserActivityRankAPI(AsyncAPIView):
create_time__gte=start, create_time__gte=start,
result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED], result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED],
).exclude(username__in=hidden_names) ).exclude(username__in=hidden_names)
data = [ data = [row async for row in submissions.values("username").annotate(count=Count("problem_id", distinct=True)).order_by("-count")[:10]]
row
async for row in submissions.values("username")
.annotate(count=Count("problem_id", distinct=True))
.order_by("-count")[:10]
]
await async_cache_set(cache_key, data, 600) await async_cache_set(cache_key, data, 600)
return self.success(data) return self.success(data)

View File

@@ -114,16 +114,12 @@ def get_class_user_ids(user):
cache_key = get_cache_key("class_users", user.class_name) cache_key = get_cache_key("class_users", user.class_name)
user_ids = cache.get(cache_key) user_ids = cache.get(cache_key)
if user_ids is None: if user_ids is None:
user_ids = list( user_ids = list(User.objects.filter(class_name=user.class_name).values_list("id", flat=True))
User.objects.filter(class_name=user.class_name).values_list("id", flat=True)
)
cache.set(cache_key, user_ids, CACHE_TIMEOUT) cache.set(cache_key, user_ids, CACHE_TIMEOUT)
return user_ids return user_ids
def get_user_first_ac_submissions( def get_user_first_ac_submissions(user_id, start, end, class_user_ids=None, use_class_scope=False, include_all_time=True):
user_id, start, end, class_user_ids=None, use_class_scope=False, include_all_time=True
):
# 用户自己的 AC 记录按时间范围过滤 # 用户自己的 AC 记录按时间范围过滤
user_first_ac = list( user_first_ac = list(
Submission.objects.filter( Submission.objects.filter(
@@ -151,9 +147,7 @@ def get_user_first_ac_submissions(
if use_class_scope and class_user_ids: if use_class_scope and class_user_ids:
rank_qs = rank_qs.filter(user_id__in=class_user_ids) rank_qs = rank_qs.filter(user_id__in=class_user_ids)
ranked_first_ac = list( ranked_first_ac = list(rank_qs.values("user_id", "problem_id").annotate(first_ac_time=Min("create_time")))
rank_qs.values("user_id", "problem_id").annotate(first_ac_time=Min("create_time"))
)
by_problem = defaultdict(list) by_problem = defaultdict(list)
for item in ranked_first_ac: for item in ranked_first_ac:
@@ -241,18 +235,14 @@ class AIDetailDataAPI(APIView):
except User.DoesNotExist: except User.DoesNotExist:
return self.error("User not found") return self.error("User not found")
cache_key = get_cache_key( cache_key = get_cache_key("ai_detail", user.id, user.class_name or "", start, end)
"ai_detail", user.id, user.class_name or "", start, end
)
cached_result = cache.get(cache_key) cached_result = cache.get(cache_key)
if cached_result: if cached_result:
return self.success(cached_result) return self.success(cached_result)
class_user_ids = get_class_user_ids(user) class_user_ids = get_class_user_ids(user)
use_class_scope = bool(user.class_name) and len(class_user_ids) > 1 use_class_scope = bool(user.class_name) and len(class_user_ids) > 1
user_first_ac, by_problem, problem_ids = get_user_first_ac_submissions( user_first_ac, by_problem, problem_ids = get_user_first_ac_submissions(user.id, start, end, class_user_ids, use_class_scope)
user.id, start, end, class_user_ids, use_class_scope
)
# 同期排名:只统计时间窗口内解题的人 # 同期排名:只统计时间窗口内解题的人
by_problem_period = defaultdict(list) by_problem_period = defaultdict(list)
@@ -265,9 +255,7 @@ class AIDetailDataAPI(APIView):
) )
if use_class_scope and class_user_ids: if use_class_scope and class_user_ids:
period_qs = period_qs.filter(user_id__in=class_user_ids) period_qs = period_qs.filter(user_id__in=class_user_ids)
for item in period_qs.values("user_id", "problem_id").annotate( for item in period_qs.values("user_id", "problem_id").annotate(first_ac_time=Min("create_time")):
first_ac_time=Min("create_time")
):
by_problem_period[item["problem_id"]].append(item) by_problem_period[item["problem_id"]].append(item)
for lst in by_problem_period.values(): for lst in by_problem_period.values():
lst.sort(key=lambda x: (x["first_ac_time"], x["user_id"])) lst.sort(key=lambda x: (x["first_ac_time"], x["user_id"]))
@@ -286,15 +274,8 @@ class AIDetailDataAPI(APIView):
} }
if user_first_ac: if user_first_ac:
problems = { problems = {p.id: p for p in Problem.objects.filter(id__in=problem_ids).select_related("contest").prefetch_related("tags")}
p.id: p solved, contest_ids = self._build_solved_records(user_first_ac, by_problem, by_problem_period, problems, user.id)
for p in Problem.objects.filter(id__in=problem_ids)
.select_related("contest")
.prefetch_related("tags")
}
solved, contest_ids = self._build_solved_records(
user_first_ac, by_problem, by_problem_period, problems, user.id
)
# 查找 flowchart submissions # 查找 flowchart submissions
flowcharts_query = FlowchartSubmission.objects.filter( flowcharts_query = FlowchartSubmission.objects.filter(
user_id=user, user_id=user,
@@ -336,9 +317,7 @@ class AIDetailDataAPI(APIView):
# 找到最高分和对应的等级 # 找到最高分和对应的等级
best_score = max(scores) if scores else 0 best_score = max(scores) if scores else 0
best_submission = next( best_submission = next((s for s in submissions if s.ai_score == best_score), submissions[0])
(s for s in submissions if s.ai_score == best_score), submissions[0]
)
best_grade = best_submission.ai_grade or "" best_grade = best_submission.ai_grade or ""
# 计算平均分 # 计算平均分
@@ -360,9 +339,7 @@ class AIDetailDataAPI(APIView):
flowcharts_data.append(merged_item) flowcharts_data.append(merged_item)
# 按最新提交时间排序 # 按最新提交时间排序
flowcharts_data.sort( flowcharts_data.sort(key=lambda x: x["latest_submission_time"] or "", reverse=True)
key=lambda x: x["latest_submission_time"] or "", reverse=True
)
result.update( result.update(
{ {
@@ -370,9 +347,7 @@ class AIDetailDataAPI(APIView):
"flowcharts": flowcharts_data, "flowcharts": flowcharts_data,
"grade": calculate_average_grade([s["grade"] for s in solved]), "grade": calculate_average_grade([s["grade"] for s in solved]),
"tags": self._calculate_top_tags(problems.values()), "tags": self._calculate_top_tags(problems.values()),
"difficulty": self._calculate_difficulty_distribution( "difficulty": self._calculate_difficulty_distribution(problems.values()),
problems.values()
),
"contest_count": len(set(contest_ids)), "contest_count": len(set(contest_ids)),
} }
) )
@@ -428,13 +403,8 @@ class AIDetailDataAPI(APIView):
def _calculate_difficulty_distribution(self, problems): def _calculate_difficulty_distribution(self, problems):
diff_counter = {"Low": 0, "Mid": 0, "High": 0} diff_counter = {"Low": 0, "Mid": 0, "High": 0}
for problem in problems: for problem in problems:
diff_counter[ diff_counter[problem.difficulty if problem.difficulty in diff_counter else "Mid"] += 1
problem.difficulty if problem.difficulty in diff_counter else "Mid" return {get_difficulty(k): v for k, v in sorted(diff_counter.items(), key=lambda x: x[1], reverse=True)}
] += 1
return {
get_difficulty(k): v
for k, v in sorted(diff_counter.items(), key=lambda x: x[1], reverse=True)
}
class AIDurationDataAPI(APIView): class AIDurationDataAPI(APIView):
@@ -451,9 +421,7 @@ class AIDurationDataAPI(APIView):
except User.DoesNotExist: except User.DoesNotExist:
return self.error("User not found") return self.error("User not found")
cache_key = get_cache_key( cache_key = get_cache_key("ai_duration", user.id, user.class_name or "", end_iso, duration)
"ai_duration", user.id, user.class_name or "", end_iso, duration
)
cached_result = cache.get(cache_key) cached_result = cache.get(cache_key)
if cached_result: if cached_result:
return self.success(cached_result) return self.success(cached_result)
@@ -468,9 +436,7 @@ class AIDurationDataAPI(APIView):
start = start + time_config["delta"] start = start + time_config["delta"]
period_end = start + time_config["delta"] period_end = start + time_config["delta"]
submission_count = Submission.objects.filter( submission_count = Submission.objects.filter(user_id=user.id, create_time__gte=start, create_time__lte=period_end).count()
user_id=user.id, create_time__gte=start, create_time__lte=period_end
).count()
period_data = { period_data = {
"unit": time_config["show_unit"], "unit": time_config["show_unit"],
@@ -502,9 +468,7 @@ class AIDurationDataAPI(APIView):
) )
if use_class_scope and class_user_ids: if use_class_scope and class_user_ids:
period_qs = period_qs.filter(user_id__in=class_user_ids) period_qs = period_qs.filter(user_id__in=class_user_ids)
for row in period_qs.values("user_id", "problem_id").annotate( for row in period_qs.values("user_id", "problem_id").annotate(first_ac_time=Min("create_time")):
first_ac_time=Min("create_time")
):
by_problem_period[row["problem_id"]].append(row) by_problem_period[row["problem_id"]].append(row)
for lst in by_problem_period.values(): for lst in by_problem_period.values():
lst.sort(key=lambda x: (x["first_ac_time"], x["user_id"])) lst.sort(key=lambda x: (x["first_ac_time"], x["user_id"]))
@@ -558,7 +522,6 @@ class AIDurationDataAPI(APIView):
) )
class AILoginSummaryAPI(APIView): class AILoginSummaryAPI(APIView):
@login_required @login_required
def get(self, request): def get(self, request):
@@ -574,20 +537,11 @@ class AILoginSummaryAPI(APIView):
) )
new_problem_count = problems_qs.count() new_problem_count = problems_qs.count()
submissions_qs = Submission.objects.filter( submissions_qs = Submission.objects.filter(user_id=user.id, create_time__gte=start_time, create_time__lte=end_time)
user_id=user.id, create_time__gte=start_time, create_time__lte=end_time
)
submission_count = submissions_qs.count() submission_count = submissions_qs.count()
accepted_count = submissions_qs.filter(result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED]).count() accepted_count = submissions_qs.filter(result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED]).count()
solved_count = ( solved_count = submissions_qs.filter(result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED]).values("problem_id").distinct().count()
submissions_qs.filter(result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED]) flowchart_submission_count = FlowchartSubmission.objects.filter(user_id=user.id, create_time__gte=start_time, create_time__lte=end_time).count()
.values("problem_id")
.distinct()
.count()
)
flowchart_submission_count = FlowchartSubmission.objects.filter(
user_id=user.id, create_time__gte=start_time, create_time__lte=end_time
).count()
summary = { summary = {
"start": datetime2str(start_time), "start": datetime2str(start_time),
@@ -614,9 +568,7 @@ class AILoginSummaryAPI(APIView):
start_time = parse_datetime(start_raw) if start_raw else None start_time = parse_datetime(start_raw) if start_raw else None
if start_time and timezone.is_naive(start_time): if start_time and timezone.is_naive(start_time):
start_time = timezone.make_aware( start_time = timezone.make_aware(start_time, timezone.get_current_timezone())
start_time, timezone.get_current_timezone()
)
if not start_time: if not start_time:
if user.last_login and user.last_login < end_time: if user.last_login and user.last_login < end_time:
@@ -637,11 +589,7 @@ class AILoginSummaryAPI(APIView):
except Exception as exc: except Exception as exc:
return "", str(exc) return "", str(exc)
system_prompt = ( system_prompt = "你是 OnlineJudge 的学习助教。请根据统计数据给出简短分析(1-2句),再给出一行结论,结论用“结论:”开头。"
"你是 OnlineJudge 的学习助教。"
"请根据统计数据给出简短分析(1-2句),再给出一行结论,"
"结论用“结论:”开头。"
)
user_prompt = ( user_prompt = (
f"时间范围:{summary['start']}{summary['end']}\n" f"时间范围:{summary['start']}{summary['end']}\n"
f"新题目数:{summary['new_problem_count']}\n" f"新题目数:{summary['new_problem_count']}\n"
@@ -669,6 +617,7 @@ class AILoginSummaryAPI(APIView):
content = completion.choices[0].message.content or "" content = completion.choices[0].message.content or ""
return content.strip(), "" return content.strip(), ""
class AIAnalysisAPI(APIView): class AIAnalysisAPI(APIView):
@login_required @login_required
def post(self, request): def post(self, request):
@@ -697,9 +646,7 @@ class AIAnalysisAPI(APIView):
analysis=full_text, analysis=full_text,
) )
return make_sse_response( return make_sse_response(stream_ai_response(client, system_prompt, user_prompt, on_complete))
stream_ai_response(client, system_prompt, user_prompt, on_complete)
)
class ClassPKAnalysisAPI(APIView): class ClassPKAnalysisAPI(APIView):
@@ -745,24 +692,11 @@ class ClassPKAnalysisAPI(APIView):
class_display = fmt_class(c["class_name"]) class_display = fmt_class(c["class_name"])
lines.append(f"\n### 第{i + 1}名:{class_display}(综合分 {c['composite_score']:.1f}") lines.append(f"\n### 第{i + 1}名:{class_display}(综合分 {c['composite_score']:.1f}")
lines.append(f"- 人数:{c['user_count']}") lines.append(f"- 人数:{c['user_count']}")
lines.append( lines.append(f"- 总AC数{c['total_ac']},总提交数:{c['total_submission']}AC率{c['ac_rate']:.1f}%")
f"- AC{c['total_ac']},总提交数:{c['total_submission']}AC{c['ac_rate']:.1f}%" lines.append(f"- 平均AC{c['avg_ac']:.2f}中位数AC{c['median_ac']:.2f}")
) lines.append(f"- Q1{c['q1_ac']:.2f}Q3{c['q3_ac']:.2f}IQR四分位距{c['iqr']:.2f},标准差:{c['std_dev']:.2f}")
lines.append( lines.append(f"- 前10%均值:{c['top_10_avg']:.2f}中间80%均值:{c['middle_80_avg']:.2f}后10%均值:{c['bottom_10_avg']:.2f}")
f"- 平均AC{c['avg_ac']:.2f}中位数AC{c['median_ac']:.2f}" lines.append(f"- 优秀率:{c['excellent_rate']:.1f}%,及格率{c['pass_rate']:.1f}%,参与度{c['active_rate']:.1f}%")
)
lines.append(
f"- Q1{c['q1_ac']:.2f}Q3{c['q3_ac']:.2f}"
f"IQR四分位距{c['iqr']:.2f},标准差:{c['std_dev']:.2f}"
)
lines.append(
f"- 前10%均值:{c['top_10_avg']:.2f}中间80%均值:{c['middle_80_avg']:.2f}"
f"后10%均值:{c['bottom_10_avg']:.2f}"
)
lines.append(
f"- 优秀率:{c['excellent_rate']:.1f}%,及格率:{c['pass_rate']:.1f}%"
f"参与度:{c['active_rate']:.1f}%"
)
if c.get("recent_total_ac") is not None: if c.get("recent_total_ac") is not None:
lines.append( lines.append(
@@ -781,19 +715,15 @@ class ClassPKAnalysisAPI(APIView):
"", "",
"**2. 参与积极性**:对比参与度和总提交数,谁的班学生更积极主动?", "**2. 参与积极性**:对比参与度和总提交数,谁的班学生更积极主动?",
"", "",
'**3. 典型学生水平**重点用中位数AC数对比而非平均值' '**3. 典型学生水平**重点用中位数AC数对比而非平均值分析谁班的"普通学生"更强。若均值明显高于中位数,说明均值被少数强者拉高,需指出。',
'分析谁班的"普通学生"更强。若均值明显高于中位数,说明均值被少数强者拉高,需指出。',
"", "",
'**4. 班级内部均衡性**结合标准差、IQR、前10%与后10%差距,' '**4. 班级内部均衡性**结合标准差、IQR、前10%与后10%差距,判断哪个班是"均衡型",哪个班是"两极型"',
'判断哪个班是"均衡型",哪个班是"两极型"',
"", "",
"**5. 梯队深度对比**对比各班前10%均值尖子生天花板和后10%均值(薄弱学生水平)," "**5. 梯队深度对比**对比各班前10%均值尖子生天花板和后10%均值(薄弱学生水平),分析各班在培养尖子生和帮扶后进生上的差异。",
"分析各班在培养尖子生和帮扶后进生上的差异。",
"", "",
'**6. 代码提交质量**对比AC率是否有班级存在"凑提交次数但不思考"的问题?', '**6. 代码提交质量**对比AC率是否有班级存在"凑提交次数但不思考"的问题?',
"", "",
"**7. 综合结论与建议**用1句话明确说明胜负" "**7. 综合结论与建议**用1句话明确说明胜负对落后班级给出2~3条具体可操作的改进建议点出领先班级1条值得借鉴的做法。",
"对落后班级给出2~3条具体可操作的改进建议点出领先班级1条值得借鉴的做法。",
"", "",
"分析对象是班级任课教师,语言专业但不过分学术。", "分析对象是班级任课教师,语言专业但不过分学术。",
] ]
@@ -916,9 +846,7 @@ class AIHintAPI(APIView):
f"学生代码:\n```\n{submission.code[:2000]}\n```" f"学生代码:\n```\n{submission.code[:2000]}\n```"
) )
return make_sse_response( return make_sse_response(stream_ai_response(client, system_prompt, user_prompt))
stream_ai_response(client, system_prompt, user_prompt)
)
class AIHeatmapDataAPI(APIView): class AIHeatmapDataAPI(APIView):
@@ -941,9 +869,7 @@ class AIHeatmapDataAPI(APIView):
# 使用单次查询获取所有数据,按日期分组统计 # 使用单次查询获取所有数据,按日期分组统计
submission_counts = ( submission_counts = (
Submission.objects.filter( Submission.objects.filter(user_id=user.id, create_time__gte=start, create_time__lte=end)
user_id=user.id, create_time__gte=start, create_time__lte=end
)
.annotate(date=TruncDate("create_time")) .annotate(date=TruncDate("create_time"))
.values("date") .values("date")
.annotate(count=Count("id")) .annotate(count=Count("id"))
@@ -961,10 +887,7 @@ class AIHeatmapDataAPI(APIView):
submission_count = submission_dict.get(day_date, 0) submission_count = submission_dict.get(day_date, 0)
heatmap_data.append( heatmap_data.append(
{ {
"timestamp": int( "timestamp": int(datetime.combine(day_date, datetime.min.time()).timestamp() * 1000),
datetime.combine(day_date, datetime.min.time()).timestamp()
* 1000
),
"value": submission_count, "value": submission_count,
} }
) )

View File

@@ -17,7 +17,10 @@ class Announcement(models.Model):
class Meta: class Meta:
db_table = "announcement" db_table = "announcement"
ordering = ("-top", "-create_time",) ordering = (
"-top",
"-create_time",
)
indexes = [ indexes = [
models.Index(fields=["visible", "-top", "-create_time"], name="announcement_list_idx"), models.Index(fields=["visible", "-top", "-create_time"], name="announcement_list_idx"),
] ]

View File

@@ -25,7 +25,7 @@ class AnnouncementListSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Announcement model = Announcement
exclude = ['content'] exclude = ["content"]
class EditAnnouncementSerializer(serializers.Serializer): class EditAnnouncementSerializer(serializers.Serializer):

View File

@@ -52,9 +52,7 @@ class AnnouncementAdminAPI(APIView):
announcement = Announcement.objects.all().order_by("-create_time") announcement = Announcement.objects.all().order_by("-create_time")
if request.GET.get("visible") == "true": if request.GET.get("visible") == "true":
announcement = announcement.filter(visible=True) announcement = announcement.filter(visible=True)
return self.success( return self.success(self.paginate_data(request, announcement, AnnouncementSerializer))
self.paginate_data(request, announcement, AnnouncementSerializer)
)
@super_admin_required @super_admin_required
def delete(self, request): def delete(self, request):

View File

@@ -8,11 +8,7 @@ class AnnouncementAPI(AsyncAPIView):
id = request.GET.get("id") id = request.GET.get("id")
if id: if id:
try: try:
announcement = await ( announcement = await Announcement.objects.select_related("created_by").filter(id=id, visible=True).afirst()
Announcement.objects.select_related("created_by")
.filter(id=id, visible=True)
.afirst()
)
if announcement is None: if announcement is None:
raise Announcement.DoesNotExist raise Announcement.DoesNotExist
return self.success(await self.async_serialize_data(AnnouncementSerializer, announcement)) return self.success(await self.async_serialize_data(AnnouncementSerializer, announcement))
@@ -20,6 +16,4 @@ class AnnouncementAPI(AsyncAPIView):
return self.error("Announcement does not exist") return self.error("Announcement does not exist")
announcements = Announcement.objects.select_related("created_by").filter(visible=True) announcements = Announcement.objects.select_related("created_by").filter(visible=True)
return self.success( return self.success(await self.async_paginate_data(request, announcements, AnnouncementListSerializer))
await self.async_paginate_data(request, announcements, AnnouncementListSerializer)
)

View File

@@ -1,2 +1 @@
# Register your models here. # Register your models here.

View File

@@ -1,3 +1,2 @@
# 如果需要存储班级PK历史记录可以在这里定义模型 # 如果需要存储班级PK历史记录可以在这里定义模型
# 目前暂时不需要,因为都是实时计算 # 目前暂时不需要,因为都是实时计算

View File

@@ -7,4 +7,3 @@ urlpatterns = [
path("user_class_rank", UserClassRankAPI.as_view()), path("user_class_rank", UserClassRankAPI.as_view()),
path("class_pk", ClassPKAPI.as_view()), path("class_pk", ClassPKAPI.as_view()),
] ]

View File

@@ -42,9 +42,7 @@ class ClassRankAPI(APIView):
profiles = UserProfile.objects.filter(user_id__in=user_ids) profiles = UserProfile.objects.filter(user_id__in=user_ids)
total_ac = profiles.aggregate(total=Sum("accepted_number"))["total"] or 0 total_ac = profiles.aggregate(total=Sum("accepted_number"))["total"] or 0
total_submission = ( total_submission = profiles.aggregate(total=Sum("submission_number"))["total"] or 0
profiles.aggregate(total=Sum("submission_number"))["total"] or 0
)
avg_ac = profiles.aggregate(avg=Avg("accepted_number"))["avg"] or 0 avg_ac = profiles.aggregate(avg=Avg("accepted_number"))["avg"] or 0
user_count = users.count() user_count = users.count()
@@ -56,9 +54,7 @@ class ClassRankAPI(APIView):
"total_ac": int(total_ac), "total_ac": int(total_ac),
"total_submission": int(total_submission), "total_submission": int(total_submission),
"avg_ac": round(avg_ac, 2), "avg_ac": round(avg_ac, 2),
"ac_rate": round(total_ac / total_submission * 100, 2) "ac_rate": round(total_ac / total_submission * 100, 2) if total_submission > 0 else 0,
if total_submission > 0
else 0,
} }
) )
@@ -213,9 +209,7 @@ class ClassPKAPI(APIView):
# 获取所有学生的AC数列表用于统计计算 # 获取所有学生的AC数列表用于统计计算
profiles = UserProfile.objects.filter(user_id__in=user_ids) profiles = UserProfile.objects.filter(user_id__in=user_ids)
ac_list = sorted([p.accepted_number for p in profiles], reverse=True) ac_list = sorted([p.accepted_number for p in profiles], reverse=True)
submission_list = sorted( submission_list = sorted([p.submission_number for p in profiles], reverse=True)
[p.submission_number for p in profiles], reverse=True
)
user_count = len(ac_list) user_count = len(ac_list)
if user_count == 0: if user_count == 0:
@@ -238,14 +232,8 @@ class ClassPKAPI(APIView):
# 前10%和后10%统计 # 前10%和后10%统计
top_10_count = max(1, math.ceil(user_count * 0.10)) top_10_count = max(1, math.ceil(user_count * 0.10))
bottom_10_count = max(1, math.ceil(user_count * 0.10)) bottom_10_count = max(1, math.ceil(user_count * 0.10))
top_10_avg = ( top_10_avg = statistics.mean(ac_list[:top_10_count]) if top_10_count > 0 else 0
statistics.mean(ac_list[:top_10_count]) if top_10_count > 0 else 0 bottom_10_avg = statistics.mean(ac_list[-bottom_10_count:]) if bottom_10_count > 0 else 0
)
bottom_10_avg = (
statistics.mean(ac_list[-bottom_10_count:])
if bottom_10_count > 0
else 0
)
# 中间80%均值截尾均值去掉前10%和后10% # 中间80%均值截尾均值去掉前10%和后10%
if top_10_count + bottom_10_count < user_count: if top_10_count + bottom_10_count < user_count:
@@ -256,9 +244,7 @@ class ClassPKAPI(APIView):
# 优秀率AC数 >= 全局Q3即超过PK组所有学生的前25% # 优秀率AC数 >= 全局Q3即超过PK组所有学生的前25%
excellent_count = sum(1 for ac in ac_list if ac >= global_q3) excellent_count = sum(1 for ac in ac_list if ac >= global_q3)
excellent_rate = ( excellent_rate = (excellent_count / user_count * 100) if user_count > 0 else 0
(excellent_count / user_count * 100) if user_count > 0 else 0
)
# 及格率AC数 >= 全局Q1即超过PK组所有学生的后25% # 及格率AC数 >= 全局Q1即超过PK组所有学生的后25%
pass_count = sum(1 for ac in ac_list if ac >= global_q1) pass_count = sum(1 for ac in ac_list if ac >= global_q1)
@@ -276,23 +262,13 @@ class ClassPKAPI(APIView):
create_time__gte=start_time, create_time__gte=start_time,
create_time__lte=end_time, create_time__lte=end_time,
) )
recent_ac = ( recent_ac = submissions.filter(result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED]).values("user_id", "problem_id").distinct().count()
submissions.filter(result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED])
.values("user_id", "problem_id")
.distinct()
.count()
)
recent_submission = submissions.count() recent_submission = submissions.count()
# 时间段内的用户AC数列表 # 时间段内的用户AC数列表
recent_user_ac = {} recent_user_ac = {}
for user_id in user_ids: for user_id in user_ids:
user_recent_ac = ( user_recent_ac = submissions.filter(user_id=user_id, result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED]).values("problem_id").distinct().count()
submissions.filter(user_id=user_id, result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED])
.values("problem_id")
.distinct()
.count()
)
recent_user_ac[user_id] = user_recent_ac recent_user_ac[user_id] = user_recent_ac
recent_ac_list = sorted(recent_user_ac.values(), reverse=True) recent_ac_list = sorted(recent_user_ac.values(), reverse=True)
@@ -302,14 +278,8 @@ class ClassPKAPI(APIView):
"recent_total_submission": recent_submission, "recent_total_submission": recent_submission,
"recent_avg_ac": statistics.mean(recent_ac_list), "recent_avg_ac": statistics.mean(recent_ac_list),
"recent_median_ac": statistics.median(recent_ac_list), "recent_median_ac": statistics.median(recent_ac_list),
"recent_top_10_avg": statistics.mean( "recent_top_10_avg": statistics.mean(recent_ac_list[: max(1, math.ceil(len(recent_ac_list) * 0.10))]) if recent_ac_list else 0,
recent_ac_list[: max(1, math.ceil(len(recent_ac_list) * 0.10))] "recent_active_count": sum(1 for ac in recent_ac_list if ac > 0),
)
if recent_ac_list
else 0,
"recent_active_count": sum(
1 for ac in recent_ac_list if ac > 0
),
} }
class_comparisons.append( class_comparisons.append(
@@ -336,9 +306,7 @@ class ClassPKAPI(APIView):
"pass_rate": round(pass_rate, 2), "pass_rate": round(pass_rate, 2),
"active_rate": round(active_rate, 2), "active_rate": round(active_rate, 2),
# 正确率 # 正确率
"ac_rate": round(total_ac / total_submission * 100, 2) "ac_rate": round(total_ac / total_submission * 100, 2) if total_submission > 0 else 0,
if total_submission > 0
else 0,
# 时间段统计(如果有) # 时间段统计(如果有)
**recent_stats, **recent_stats,
} }
@@ -359,9 +327,7 @@ class ClassPKAPI(APIView):
c["composite_score"] = round(score, 1) c["composite_score"] = round(score, 1)
# 按综合分排序(主),中位数(次) # 按综合分排序(主),中位数(次)
class_comparisons.sort( class_comparisons.sort(key=lambda x: (-x["composite_score"], -x["median_ac"]))
key=lambda x: (-x["composite_score"], -x["median_ac"])
)
return self.success( return self.success(
{ {

View File

@@ -43,5 +43,3 @@ class Comment(models.Model):
indexes = [ indexes = [
models.Index(fields=["problem", "create_time"], name="comment_problem_time_idx"), models.Index(fields=["problem", "create_time"], name="comment_problem_time_idx"),
] ]

View File

@@ -28,4 +28,4 @@ class CommentListSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Comment model = Comment
fields = "__all__" fields = "__all__"

View File

@@ -17,9 +17,7 @@ class CommentAPI(APIView):
except Problem.DoesNotExist: except Problem.DoesNotExist:
return self.error("Problem doesn't exist") return self.error("Problem doesn't exist")
comments = comments.filter(problem=problem) comments = comments.filter(problem=problem)
return self.success( return self.success(self.paginate_data(request, comments, CommentListSerializer))
self.paginate_data(request, comments, CommentListSerializer)
)
@super_admin_required @super_admin_required
def delete(self, request): def delete(self, request):

View File

@@ -54,11 +54,7 @@ class CommentAPI(AsyncAPIView):
@login_required @login_required
async def get(self, request): async def get(self, request):
problem_id = request.GET.get("problem_id") problem_id = request.GET.get("problem_id")
comment = await ( comment = await Comment.objects.select_related("problem").filter(user=request.user, problem_id=problem_id).afirst()
Comment.objects.select_related("problem")
.filter(user=request.user, problem_id=problem_id)
.afirst()
)
if comment: if comment:
return self.success(await self.async_serialize_data(CommentSerializer, comment)) return self.success(await self.async_serialize_data(CommentSerializer, comment))
else: else:
@@ -82,10 +78,13 @@ class CommentStatisticsAPI(AsyncAPIView):
if not agg["count"]: if not agg["count"]:
return self.success() return self.success()
data = {"count": agg["count"], "rating": { data = {
"description": agg["description"], "count": agg["count"],
"difficulty": agg["difficulty"], "rating": {
"comprehensive": agg["comprehensive"], "description": agg["description"],
}} "difficulty": agg["difficulty"],
"comprehensive": agg["comprehensive"],
},
}
await async_cache_set(cache_key, data, 3600) await async_cache_set(cache_key, data, 3600)
return self.success(data) return self.success(data)

View File

@@ -1,6 +1,7 @@
""" """
WebSocket consumers for configuration updates WebSocket consumers for configuration updates
""" """
import json import json
import logging import logging
@@ -18,31 +19,25 @@ class ConfigConsumer(AsyncWebsocketConsumer):
async def connect(self): async def connect(self):
"""处理 WebSocket 连接""" """处理 WebSocket 连接"""
self.user = self.scope["user"] self.user = self.scope["user"]
# 只允许认证用户连接 # 只允许认证用户连接
if not self.user.is_authenticated: if not self.user.is_authenticated:
await self.close() await self.close()
return return
# 使用全局配置组名,所有用户都能接收配置更新 # 使用全局配置组名,所有用户都能接收配置更新
self.group_name = "config_updates" self.group_name = "config_updates"
# 加入配置更新组 # 加入配置更新组
await self.channel_layer.group_add( await self.channel_layer.group_add(self.group_name, self.channel_name)
self.group_name,
self.channel_name
)
await self.accept() await self.accept()
logger.info(f"Config WebSocket connected: user_id={self.user.id}, channel={self.channel_name}") logger.info(f"Config WebSocket connected: user_id={self.user.id}, channel={self.channel_name}")
async def disconnect(self, close_code): async def disconnect(self, close_code):
"""处理 WebSocket 断开连接""" """处理 WebSocket 断开连接"""
if hasattr(self, 'group_name'): if hasattr(self, "group_name"):
await self.channel_layer.group_discard( await self.channel_layer.group_discard(self.group_name, self.channel_name)
self.group_name,
self.channel_name
)
logger.info(f"Config WebSocket disconnected: user_id={self.user.id}, close_code={close_code}") logger.info(f"Config WebSocket disconnected: user_id={self.user.id}, close_code={close_code}")
async def receive(self, text_data): async def receive(self, text_data):
@@ -53,13 +48,10 @@ class ConfigConsumer(AsyncWebsocketConsumer):
try: try:
data = json.loads(text_data) data = json.loads(text_data)
message_type = data.get("type") message_type = data.get("type")
if message_type == "ping": if message_type == "ping":
# 响应心跳包 # 响应心跳包
await self.send(text_data=json.dumps({ await self.send(text_data=json.dumps({"type": "pong", "timestamp": data.get("timestamp")}))
"type": "pong",
"timestamp": data.get("timestamp")
}))
elif message_type == "config_update": elif message_type == "config_update":
# 处理配置更新请求 # 处理配置更新请求
key = data.get("key") key = data.get("key")
@@ -69,17 +61,7 @@ class ConfigConsumer(AsyncWebsocketConsumer):
# 这里可以添加权限检查,只有管理员才能发送配置更新 # 这里可以添加权限检查,只有管理员才能发送配置更新
if self.user.is_superuser: if self.user.is_superuser:
# 广播配置更新给所有连接的客户端 # 广播配置更新给所有连接的客户端
await self.channel_layer.group_send( await self.channel_layer.group_send(self.group_name, {"type": "config_update", "data": {"type": "config_update", "key": key, "value": value}})
self.group_name,
{
"type": "config_update",
"data": {
"type": "config_update",
"key": key,
"value": value
}
}
)
except json.JSONDecodeError: except json.JSONDecodeError:
logger.error(f"Invalid JSON received from user {self.user.id}") logger.error(f"Invalid JSON received from user {self.user.id}")
except Exception as e: except Exception as e:

View File

@@ -44,7 +44,7 @@ class JudgeServerHeartbeatSerializer(serializers.Serializer):
cpu_core = serializers.IntegerField(min_value=1) cpu_core = serializers.IntegerField(min_value=1)
memory = serializers.FloatField(min_value=0, max_value=100) memory = serializers.FloatField(min_value=0, max_value=100)
cpu = serializers.FloatField(min_value=0, max_value=100) cpu = serializers.FloatField(min_value=0, max_value=100)
action = serializers.ChoiceField(choices=("heartbeat", )) action = serializers.ChoiceField(choices=("heartbeat",))
service_url = serializers.CharField(max_length=256) service_url = serializers.CharField(max_length=256)

View File

@@ -153,9 +153,7 @@ class JudgeServerAPI(APIView):
@super_admin_required @super_admin_required
def put(self, request): def put(self, request):
is_disabled = request.data.get("is_disabled", False) is_disabled = request.data.get("is_disabled", False)
JudgeServer.objects.filter(id=request.data["id"]).update( JudgeServer.objects.filter(id=request.data["id"]).update(is_disabled=is_disabled)
is_disabled=is_disabled
)
if not is_disabled: if not is_disabled:
process_pending_task() process_pending_task()
return self.success() return self.success()
@@ -166,10 +164,7 @@ class JudgeServerHeartbeatAPI(CSRFExemptAPIView):
def post(self, request): def post(self, request):
data = request.data data = request.data
client_token = request.META.get("HTTP_X_JUDGE_SERVER_TOKEN") client_token = request.META.get("HTTP_X_JUDGE_SERVER_TOKEN")
if ( if hashlib.sha256(SysOptions.judge_server_token.encode("utf-8")).hexdigest() != client_token:
hashlib.sha256(SysOptions.judge_server_token.encode("utf-8")).hexdigest()
!= client_token
):
return self.error("Invalid token") return self.error("Invalid token")
try: try:
@@ -263,8 +258,7 @@ class ReleaseNotesAPI(APIView):
def get(self, request): def get(self, request):
try: try:
resp = requests.get( resp = requests.get(
"https://raw.githubusercontent.com/QingdaoU/OnlineJudge/master/docs/data.json?_=" "https://raw.githubusercontent.com/QingdaoU/OnlineJudge/master/docs/data.json?_=" + str(time.time()),
+ str(time.time()),
timeout=3, timeout=3,
) )
releases = resp.json() releases = resp.json()
@@ -289,9 +283,7 @@ class DashboardInfoAPI(AsyncAPIView):
User.objects.acount(), User.objects.acount(),
Submission.objects.filter(create_time__gte=today_start).acount(), Submission.objects.filter(create_time__gte=today_start).acount(),
Contest.objects.exclude(end_time__lt=timezone.now()).acount(), Contest.objects.exclude(end_time__lt=timezone.now()).acount(),
JudgeServer.objects.filter( JudgeServer.objects.filter(last_heartbeat__gte=timezone.now() - timedelta(seconds=6)).acount(),
last_heartbeat__gte=timezone.now() - timedelta(seconds=6)
).acount(),
) )
return self.success( return self.success(
{ {
@@ -312,20 +304,14 @@ class RandomUsernameAPI(AsyncAPIView):
classroom = request.GET.get("classroom", "") classroom = request.GET.get("classroom", "")
if not classroom: if not classroom:
return self.error("需要班级号") return self.error("需要班级号")
usernames = [ usernames = [u async for u in User.objects.filter(username__istartswith=classroom).values_list("username", flat=True).order_by("?")[:10]]
u async for u in User.objects.filter(username__istartswith=classroom)
.values_list("username", flat=True)
.order_by("?")[:10]
]
return self.success(usernames) return self.success(usernames)
class HitokotoAPI(AsyncAPIView): class HitokotoAPI(AsyncAPIView):
async def get(self, request): async def get(self, request):
try: try:
categories = JsonDataLoader.load_data( categories = JsonDataLoader.load_data(settings.HITOKOTO_DIR, "categories.json")
settings.HITOKOTO_DIR, "categories.json"
)
path = random.choice(categories).get("path") path = random.choice(categories).get("path")
sentences = JsonDataLoader.load_data(settings.HITOKOTO_DIR, path) sentences = JsonDataLoader.load_data(settings.HITOKOTO_DIR, path)
sentence = random.choice(sentences) sentence = random.choice(sentences)
@@ -341,7 +327,6 @@ class ClassUsernamesAPI(AsyncAPIView):
return self.error("需要班级号") return self.error("需要班级号")
prefix = f"ks{classroom}" prefix = f"ks{classroom}"
names = [ names = [
user.username[len(prefix):] if user.username.startswith(prefix) else user.username user.username[len(prefix) :] if user.username.startswith(prefix) else user.username async for user in User.objects.filter(class_name=classroom).order_by("-create_time")
async for user in User.objects.filter(class_name=classroom).order_by("-create_time")
] ]
return self.success(names) return self.success(names)

View File

@@ -75,7 +75,6 @@ class ACMContestRank(AbstractContestRank):
] ]
class ContestAnnouncement(models.Model): class ContestAnnouncement(models.Model):
contest = models.ForeignKey(Contest, on_delete=models.CASCADE) contest = models.ForeignKey(Contest, on_delete=models.CASCADE)
title = models.TextField() title = models.TextField()

View File

@@ -84,7 +84,6 @@ class ACMContestRankSerializer(serializers.ModelSerializer):
return UsernameSerializer(obj.user, need_real_name=self.is_contest_admin).data return UsernameSerializer(obj.user, need_real_name=self.is_contest_admin).data
class ACMContesHelperSerializer(serializers.Serializer): class ACMContesHelperSerializer(serializers.Serializer):
contest_id = serializers.IntegerField() contest_id = serializers.IntegerField()
problem_id = serializers.CharField() problem_id = serializers.CharField()

View File

@@ -27,9 +27,7 @@ class ContestAnnouncementListAPI(AsyncAPIView):
contest_id = request.GET.get("contest_id") contest_id = request.GET.get("contest_id")
if not contest_id: if not contest_id:
return self.error("Invalid parameter, contest_id is required") return self.error("Invalid parameter, contest_id is required")
qs = ContestAnnouncement.objects.select_related("created_by").filter( qs = ContestAnnouncement.objects.select_related("created_by").filter(contest_id=contest_id, visible=True)
contest_id=contest_id, visible=True
)
max_id = request.GET.get("max_id") max_id = request.GET.get("max_id")
if max_id: if max_id:
qs = qs.filter(id__gt=max_id) qs = qs.filter(id__gt=max_id)
@@ -43,11 +41,7 @@ class ContestAPI(AsyncAPIView):
if not id or not check_is_id(id): if not id or not check_is_id(id):
return self.error("Invalid parameter, id is required") return self.error("Invalid parameter, id is required")
try: try:
contest = await ( contest = await Contest.objects.select_related("created_by").filter(id=id, visible=True).afirst()
Contest.objects.select_related("created_by")
.filter(id=id, visible=True)
.afirst()
)
if contest is None: if contest is None:
raise Contest.DoesNotExist raise Contest.DoesNotExist
except Contest.DoesNotExist: except Contest.DoesNotExist:
@@ -84,9 +78,7 @@ class ContestPasswordVerifyAPI(AsyncAPIView):
async def post(self, request): async def post(self, request):
data = request.data data = request.data
try: try:
contest = await Contest.objects.aget( contest = await Contest.objects.aget(id=data["contest_id"], visible=True, password__isnull=False)
id=data["contest_id"], visible=True, password__isnull=False
)
except Contest.DoesNotExist: except Contest.DoesNotExist:
return self.error("Contest does not exist") return self.error("Contest does not exist")
if not check_contest_password(data["password"], contest.password): if not check_contest_password(data["password"], contest.password):
@@ -106,17 +98,11 @@ class ContestAccessAPI(AsyncAPIView):
if not contest_id: if not contest_id:
return self.error() return self.error()
try: try:
contest = await Contest.objects.aget( contest = await Contest.objects.aget(id=contest_id, visible=True, password__isnull=False)
id=contest_id, visible=True, password__isnull=False
)
except Contest.DoesNotExist: except Contest.DoesNotExist:
return self.error("Contest does not exist") return self.error("Contest does not exist")
session_pass = request.session.get(CONTEST_PASSWORD_SESSION_KEY, {}).get( session_pass = request.session.get(CONTEST_PASSWORD_SESSION_KEY, {}).get(str(contest.id))
str(contest.id) return self.success({"access": check_contest_password(session_pass, contest.password)})
)
return self.success(
{"access": check_contest_password(session_pass, contest.password)}
)
class ContestRankAPI(AsyncAPIView): class ContestRankAPI(AsyncAPIView):
@@ -155,16 +141,12 @@ class ContestRankAPI(AsyncAPIView):
for index, item in enumerate(data): for index, item in enumerate(data):
worksheet.write_string(index + 1, 0, str(item["user"]["id"])) worksheet.write_string(index + 1, 0, str(item["user"]["id"]))
worksheet.write_string(index + 1, 1, item["user"]["username"]) worksheet.write_string(index + 1, 1, item["user"]["username"])
worksheet.write_string( worksheet.write_string(index + 1, 2, item["user"]["real_name"] or "")
index + 1, 2, item["user"]["real_name"] or ""
)
worksheet.write_string(index + 1, 3, str(item["accepted_number"])) worksheet.write_string(index + 1, 3, str(item["accepted_number"]))
worksheet.write_string(index + 1, 4, str(item["submission_number"])) worksheet.write_string(index + 1, 4, str(item["submission_number"]))
worksheet.write_string(index + 1, 5, str(item["total_time"])) worksheet.write_string(index + 1, 5, str(item["total_time"]))
for k, v in item["submission_info"].items(): for k, v in item["submission_info"].items():
worksheet.write_string( worksheet.write_string(index + 1, 6 + problem_id_to_col[int(k)], str(v["is_ac"]))
index + 1, 6 + problem_id_to_col[int(k)], str(v["is_ac"])
)
workbook.close() workbook.close()
f.seek(0) f.seek(0)
@@ -173,32 +155,20 @@ class ContestRankAPI(AsyncAPIView):
@check_contest_permission(check_type="ranks") @check_contest_permission(check_type="ranks")
async def get(self, request): async def get(self, request):
download_csv = request.GET.get("download_csv") download_csv = request.GET.get("download_csv")
is_contest_admin = ( is_contest_admin = request.user.is_authenticated and request.user.is_contest_admin(self.contest)
request.user.is_authenticated
and request.user.is_contest_admin(self.contest)
)
qs = self.get_rank() qs = self.get_rank()
if download_csv: if download_csv:
rank_list = [item async for item in qs] rank_list = [item async for item in qs]
data = await self.async_serialize_data( data = await self.async_serialize_data(ACMContestRankSerializer, rank_list, many=True, is_contest_admin=is_contest_admin)
ACMContestRankSerializer, rank_list, many=True, is_contest_admin=is_contest_admin contest_problems = await sync_to_async(lambda: list(Problem.objects.filter(contest=self.contest, visible=True).order_by("_id")))()
)
contest_problems = await sync_to_async(
lambda: list(Problem.objects.filter(contest=self.contest, visible=True).order_by("_id"))
)()
xlsx_bytes = await sync_to_async(self._build_xlsx)(data, contest_problems) xlsx_bytes = await sync_to_async(self._build_xlsx)(data, contest_problems)
response = HttpResponse(xlsx_bytes) response = HttpResponse(xlsx_bytes)
response["Content-Disposition"] = ( response["Content-Disposition"] = f"attachment; filename=content-{self.contest.id}-rank.xlsx"
f"attachment; filename=content-{self.contest.id}-rank.xlsx"
)
response["Content-Type"] = "application/xlsx" response["Content-Type"] = "application/xlsx"
return response return response
page_qs = await self.async_paginate_data(request, qs) page_qs = await self.async_paginate_data(request, qs)
page_qs["results"] = await self.async_serialize_data( page_qs["results"] = await self.async_serialize_data(ACMContestRankSerializer, page_qs["results"], many=True, is_contest_admin=is_contest_admin)
ACMContestRankSerializer,
page_qs["results"], many=True, is_contest_admin=is_contest_admin
)
return self.success(page_qs) return self.success(page_qs)

View File

@@ -1,6 +1,7 @@
""" """
WebSocket consumers for flowchart evaluation updates WebSocket consumers for flowchart evaluation updates
""" """
import json import json
import logging import logging
@@ -18,31 +19,25 @@ class FlowchartConsumer(AsyncWebsocketConsumer):
async def connect(self): async def connect(self):
"""处理 WebSocket 连接""" """处理 WebSocket 连接"""
self.user = self.scope["user"] self.user = self.scope["user"]
# 只允许认证用户连接 # 只允许认证用户连接
if not self.user.is_authenticated: if not self.user.is_authenticated:
await self.close() await self.close()
return return
# 使用用户 ID 作为组名,这样可以向特定用户推送消息 # 使用用户 ID 作为组名,这样可以向特定用户推送消息
self.group_name = f"flowchart_user_{self.user.id}" self.group_name = f"flowchart_user_{self.user.id}"
# 加入用户专属的组 # 加入用户专属的组
await self.channel_layer.group_add( await self.channel_layer.group_add(self.group_name, self.channel_name)
self.group_name,
self.channel_name
)
await self.accept() await self.accept()
logger.info(f"Flowchart WebSocket connected: user_id={self.user.id}, channel={self.channel_name}") logger.info(f"Flowchart WebSocket connected: user_id={self.user.id}, channel={self.channel_name}")
async def disconnect(self, close_code): async def disconnect(self, close_code):
"""处理 WebSocket 断开连接""" """处理 WebSocket 断开连接"""
if hasattr(self, 'group_name'): if hasattr(self, "group_name"):
await self.channel_layer.group_discard( await self.channel_layer.group_discard(self.group_name, self.channel_name)
self.group_name,
self.channel_name
)
logger.info(f"Flowchart WebSocket disconnected: user_id={self.user.id}, close_code={close_code}") logger.info(f"Flowchart WebSocket disconnected: user_id={self.user.id}, close_code={close_code}")
async def receive(self, text_data): async def receive(self, text_data):
@@ -53,13 +48,10 @@ class FlowchartConsumer(AsyncWebsocketConsumer):
try: try:
data = json.loads(text_data) data = json.loads(text_data)
message_type = data.get("type") message_type = data.get("type")
if message_type == "ping": if message_type == "ping":
# 响应心跳包 # 响应心跳包
await self.send(text_data=json.dumps({ await self.send(text_data=json.dumps({"type": "pong", "timestamp": data.get("timestamp")}))
"type": "pong",
"timestamp": data.get("timestamp")
}))
elif message_type == "subscribe": elif message_type == "subscribe":
# 订阅特定流程图提交的更新 # 订阅特定流程图提交的更新
submission_id = data.get("submission_id") submission_id = data.get("submission_id")

View File

@@ -6,61 +6,59 @@ from utils.shortcuts import rand_str
User = get_user_model() User = get_user_model()
class FlowchartSubmissionStatus: class FlowchartSubmissionStatus:
PENDING = 0 # 等待AI评分 PENDING = 0 # 等待AI评分
PROCESSING = 1 # AI评分中 PROCESSING = 1 # AI评分中
COMPLETED = 2 # 评分完成 COMPLETED = 2 # 评分完成
FAILED = 3 # 评分失败 FAILED = 3 # 评分失败
class FlowchartSubmission(models.Model): class FlowchartSubmission(models.Model):
"""流程图提交模型""" """流程图提交模型"""
id = models.TextField(default=rand_str, primary_key=True, db_index=True) id = models.TextField(default=rand_str, primary_key=True, db_index=True)
# 基础信息 # 基础信息
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='flowchart_submissions') user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="flowchart_submissions")
problem = models.ForeignKey(Problem, on_delete=models.CASCADE, related_name='flowchart_submissions') problem = models.ForeignKey(Problem, on_delete=models.CASCADE, related_name="flowchart_submissions")
# 提交内容 # 提交内容
mermaid_code = models.TextField() # Mermaid代码 mermaid_code = models.TextField() # Mermaid代码
flowchart_data = models.JSONField(default=dict) # 流程图元数据 flowchart_data = models.JSONField(default=dict) # 流程图元数据
# 状态信息 # 状态信息
status = models.IntegerField(default=FlowchartSubmissionStatus.PENDING) status = models.IntegerField(default=FlowchartSubmissionStatus.PENDING)
create_time = models.DateTimeField(auto_now_add=True) create_time = models.DateTimeField(auto_now_add=True)
# AI评分结果 # AI评分结果
ai_score = models.FloatField(null=True, blank=True) # AI评分 (0-100) ai_score = models.FloatField(null=True, blank=True) # AI评分 (0-100)
ai_grade = models.CharField(max_length=10, null=True, blank=True) # 等级 (S/A/B/C) ai_grade = models.CharField(max_length=10, null=True, blank=True) # 等级 (S/A/B/C)
ai_feedback = models.TextField(null=True, blank=True) # AI反馈 ai_feedback = models.TextField(null=True, blank=True) # AI反馈
ai_suggestions = models.TextField(null=True, blank=True) # AI建议 ai_suggestions = models.TextField(null=True, blank=True) # AI建议
ai_criteria_details = models.JSONField(default=dict) # 详细评分标准 ai_criteria_details = models.JSONField(default=dict) # 详细评分标准
# 处理信息 # 处理信息
ai_provider = models.CharField(max_length=50, default='deepseek') ai_provider = models.CharField(max_length=50, default="deepseek")
ai_model = models.CharField(max_length=50, default='deepseek-v4-flash') ai_model = models.CharField(max_length=50, default="deepseek-v4-flash")
processing_time = models.FloatField(null=True, blank=True) # AI处理耗时(秒) processing_time = models.FloatField(null=True, blank=True) # AI处理耗时(秒)
evaluation_time = models.DateTimeField(null=True, blank=True) # 评分完成时间 evaluation_time = models.DateTimeField(null=True, blank=True) # 评分完成时间
class Meta: class Meta:
db_table = 'flowchart_submission' db_table = "flowchart_submission"
ordering = ['-create_time'] ordering = ["-create_time"]
indexes = [ indexes = [
models.Index(fields=['user', 'create_time'], name='flowchart_user_time_idx'), models.Index(fields=["user", "create_time"], name="flowchart_user_time_idx"),
models.Index(fields=['problem', 'create_time'], name='flowchart_problem_time_idx'), models.Index(fields=["problem", "create_time"], name="flowchart_problem_time_idx"),
models.Index(fields=['status'], name='flowchart_status_idx'), models.Index(fields=["status"], name="flowchart_status_idx"),
] ]
def __str__(self): def __str__(self):
return f"FlowchartSubmission {self.id}" return f"FlowchartSubmission {self.id}"
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
return False return False

View File

@@ -18,6 +18,7 @@ class CreateFlowchartSubmissionSerializer(serializers.Serializer):
def validate_flowchart_data(self, value): def validate_flowchart_data(self, value):
import json import json
if len(json.dumps(value)) > 500 * 1024: if len(json.dumps(value)) > 500 * 1024:
raise serializers.ValidationError("流程图数据过大") raise serializers.ValidationError("流程图数据过大")
return value return value
@@ -55,6 +56,7 @@ class FlowchartSubmissionListSerializer(serializers.ModelSerializer):
username = serializers.CharField(source="user.username") username = serializers.CharField(source="user.username")
problem = serializers.CharField(source="problem._id") problem = serializers.CharField(source="problem._id")
problem_title = serializers.CharField(source="problem.title") problem_title = serializers.CharField(source="problem.title")
class Meta: class Meta:
model = FlowchartSubmission model = FlowchartSubmission
fields = [ fields = [

View File

@@ -20,16 +20,16 @@ def evaluate_flowchart_task(submission_id):
submission = None submission = None
try: try:
submission = FlowchartSubmission.objects.get(id=submission_id) submission = FlowchartSubmission.objects.get(id=submission_id)
# 更新状态为处理中 # 更新状态为处理中
submission.status = FlowchartSubmissionStatus.PROCESSING submission.status = FlowchartSubmissionStatus.PROCESSING
submission.save() submission.save()
start_time = time.time() start_time = time.time()
# 使用固定评分标准 # 使用固定评分标准
system_prompt = build_evaluation_prompt(submission.problem) system_prompt = build_evaluation_prompt(submission.problem)
# 构建用户提示词,包含标准答案对比 # 构建用户提示词,包含标准答案对比
user_prompt = f""" user_prompt = f"""
请对以下Mermaid流程图进行评分 请对以下Mermaid流程图进行评分
@@ -53,16 +53,13 @@ def evaluate_flowchart_task(submission_id):
user_prompt += f"\n设计提示:{submission.problem.flowchart_hint}\n" user_prompt += f"\n设计提示:{submission.problem.flowchart_hint}\n"
user_prompt += "\n请按照评分标准进行详细评估并给出0-100的分数。\n" user_prompt += "\n请按照评分标准进行详细评估并给出0-100的分数。\n"
# 调用AI进行评分 # 调用AI进行评分
client = get_ai_client() client = get_ai_client()
response = client.chat.completions.create( response = client.chat.completions.create(
model="deepseek-v4-flash", model="deepseek-v4-flash",
messages=[ messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}],
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0, temperature=0,
extra_body={"thinking": {"type": "disabled"}}, extra_body={"thinking": {"type": "disabled"}},
) )
@@ -74,30 +71,31 @@ def evaluate_flowchart_task(submission_id):
# 保存评分结果 # 保存评分结果
with transaction.atomic(): with transaction.atomic():
submission.ai_score = score_data['score'] submission.ai_score = score_data["score"]
submission.ai_grade = score_data['grade'] submission.ai_grade = score_data["grade"]
submission.ai_feedback = score_data['feedback'] submission.ai_feedback = score_data["feedback"]
submission.ai_suggestions = score_data.get('suggestions', '') submission.ai_suggestions = score_data.get("suggestions", "")
submission.ai_criteria_details = score_data.get('criteria_details', {}) submission.ai_criteria_details = score_data.get("criteria_details", {})
submission.ai_provider = 'deepseek' submission.ai_provider = "deepseek"
submission.ai_model = 'deepseek-v4-flash' submission.ai_model = "deepseek-v4-flash"
submission.processing_time = processing_time submission.processing_time = processing_time
submission.status = FlowchartSubmissionStatus.COMPLETED submission.status = FlowchartSubmissionStatus.COMPLETED
submission.evaluation_time = timezone.now() submission.evaluation_time = timezone.now()
submission.save() submission.save()
# 推送评分完成通知 # 推送评分完成通知
from utils.websocket import push_flowchart_evaluation_update from utils.websocket import push_flowchart_evaluation_update
push_flowchart_evaluation_update( push_flowchart_evaluation_update(
submission_id=str(submission.id), submission_id=str(submission.id),
user_id=submission.user_id, user_id=submission.user_id,
data={ data={
"type": "flowchart_evaluation_completed", "type": "flowchart_evaluation_completed",
"score": score_data['score'], "score": score_data["score"],
"grade": score_data['grade'], "grade": score_data["grade"],
} },
) )
except Exception as e: except Exception as e:
logger.exception("evaluate_flowchart_task failed for submission %s", submission_id) logger.exception("evaluate_flowchart_task failed for submission %s", submission_id)
if submission is not None: if submission is not None:
@@ -105,6 +103,7 @@ def evaluate_flowchart_task(submission_id):
submission.save() submission.save()
from utils.websocket import push_flowchart_evaluation_update from utils.websocket import push_flowchart_evaluation_update
push_flowchart_evaluation_update( push_flowchart_evaluation_update(
submission_id=str(submission.id), submission_id=str(submission.id),
user_id=submission.user_id, user_id=submission.user_id,
@@ -116,9 +115,10 @@ def evaluate_flowchart_task(submission_id):
) )
raise e raise e
def build_evaluation_prompt(problem): def build_evaluation_prompt(problem):
"""构建AI评分提示词 - 使用固定标准""" """构建AI评分提示词 - 使用固定标准"""
# 使用固定的评分标准 # 使用固定的评分标准
criteria_text = """ criteria_text = """
- 逻辑正确性 (权重: 1.0, 最高分: 40): 检查流程图的逻辑是否正确,包括条件判断、循环结构等 - 逻辑正确性 (权重: 1.0, 最高分: 40): 检查流程图的逻辑是否正确,包括条件判断、循环结构等
@@ -126,7 +126,7 @@ def build_evaluation_prompt(problem):
- 规范性 (权重: 0.6, 最高分: 20): 检查流程图符号使用是否规范是否符合标准不要评价节点ID - 规范性 (权重: 0.6, 最高分: 20): 检查流程图符号使用是否规范是否符合标准不要评价节点ID
- 清晰度 (权重: 0.4, 最高分: 10): 评估流程图的整体布局和连线情况不要因节点ID扣分 - 清晰度 (权重: 0.4, 最高分: 10): 评估流程图的整体布局和连线情况不要因节点ID扣分
""" """
return f""" return f"""
你是一个专业的编程教学助手负责评估学生提交的Mermaid流程图。 你是一个专业的编程教学助手负责评估学生提交的Mermaid流程图。
@@ -166,15 +166,17 @@ def build_evaluation_prompt(problem):
}} }}
""" """
def parse_ai_evaluation_response(ai_response): def parse_ai_evaluation_response(ai_response):
"""解析AI评分响应解析失败时抛出异常由调用方处理""" """解析AI评分响应解析失败时抛出异常由调用方处理"""
import re import re
# 优先匹配代码块中的 JSON避免贪婪匹配误抓 reasoning 段落 # 优先匹配代码块中的 JSON避免贪婪匹配误抓 reasoning 段落
code_block = re.search(r'```(?:json)?\s*(\{[\s\S]*?\})\s*```', ai_response, re.DOTALL) code_block = re.search(r"```(?:json)?\s*(\{[\s\S]*?\})\s*```", ai_response, re.DOTALL)
if code_block: if code_block:
json_str = code_block.group(1) json_str = code_block.group(1)
else: else:
json_match = re.search(r'\{.*\}', ai_response, re.DOTALL) json_match = re.search(r"\{.*\}", ai_response, re.DOTALL)
if not json_match: if not json_match:
raise ValueError("AI响应中未找到JSON数据") raise ValueError("AI响应中未找到JSON数据")
json_str = json_match.group() json_str = json_match.group()

View File

@@ -9,9 +9,9 @@ from ..views.oj import (
) )
urlpatterns = [ urlpatterns = [
path('flowchart/submission', FlowchartSubmissionAPI.as_view()), path("flowchart/submission", FlowchartSubmissionAPI.as_view()),
path('flowchart/submissions', FlowchartSubmissionListAPI.as_view()), path("flowchart/submissions", FlowchartSubmissionListAPI.as_view()),
path('flowchart/submission/retry', FlowchartSubmissionRetryAPI.as_view()), path("flowchart/submission/retry", FlowchartSubmissionRetryAPI.as_view()),
path('flowchart/submission/detail', FlowchartSubmissionDetailAPI.as_view()), path("flowchart/submission/detail", FlowchartSubmissionDetailAPI.as_view()),
path('flowchart/submission/current', FlowchartSubmissionCurrentAPI.as_view()), path("flowchart/submission/current", FlowchartSubmissionCurrentAPI.as_view()),
] ]

View File

@@ -1,2 +1 @@
# Create your views here. # Create your views here.

View File

@@ -20,16 +20,44 @@ STOPWORDS = frozenset(
) )
CUSTOM_WORDS = [ CUSTOM_WORDS = [
"循环结构", "条件判断", "判断条件", "结束条件", "循环条件", "循环结构",
"异常处理", "边界条件", "输入输出", "输入验证", "条件判断",
"开始结束", "结束节点", "开始节点", "判断节点", "判断条件",
"流程走向", "逻辑错误", "逻辑缺陷", "逻辑不清", "结束条件",
"缺少分支", "缺少步骤", "缺少判断", "缺少循环", "循环条件",
"死循环", "无限循环", "循环出口", "循环体", "异常处理",
"条件分支", "分支结构", "分支不全", "分支缺失", "边界条件",
"符号使用", "符号不规范", "连线混乱", "输入输出",
"变量初始化", "赋值操作", "累加操作", "输入验证",
"终止条件", "退出条件", "返回值", "开始结束",
"结束节点",
"开始节点",
"判断节点",
"流程走向",
"逻辑错误",
"逻辑缺陷",
"逻辑不清",
"缺少分支",
"缺少步骤",
"缺少判断",
"缺少循环",
"死循环",
"无限循环",
"循环出口",
"循环体",
"条件分支",
"分支结构",
"分支不全",
"分支缺失",
"符号使用",
"符号不规范",
"连线混乱",
"变量初始化",
"赋值操作",
"累加操作",
"终止条件",
"退出条件",
"返回值",
] ]
for _w in CUSTOM_WORDS: for _w in CUSTOM_WORDS:
@@ -38,7 +66,7 @@ for _w in CUSTOM_WORDS:
def get_real_name(username, class_name): def get_real_name(username, class_name):
if class_name and username.startswith("ks"): if class_name and username.startswith("ks"):
return username[len(f"ks{class_name}"):] return username[len(f"ks{class_name}") :]
return username return username
@@ -63,9 +91,7 @@ class FlowchartStatisticsAPI(APIView):
problem_id = request.GET.get("problem_id") problem_id = request.GET.get("problem_id")
if problem_id: if problem_id:
try: try:
problem = Problem.objects.get( problem = Problem.objects.get(_id__iexact=problem_id, contest_id__isnull=True, visible=True)
_id__iexact=problem_id, contest_id__isnull=True, visible=True
)
except Problem.DoesNotExist: except Problem.DoesNotExist:
return self.error("Problem doesn't exist") return self.error("Problem doesn't exist")
submissions = submissions.filter(problem=problem) submissions = submissions.filter(problem=problem)
@@ -85,23 +111,21 @@ class FlowchartStatisticsAPI(APIView):
total_count = submissions.count() total_count = submissions.count()
if total_count == 0: if total_count == 0:
return self.success({ return self.success(
"total_count": 0, {
"avg_score": 0, "total_count": 0,
"grade_distribution": {}, "avg_score": 0,
"criteria_averages": {}, "grade_distribution": {},
"person_count": len(all_users_dict), "criteria_averages": {},
"completed_count": 0, "person_count": len(all_users_dict),
"word_frequencies": [], "completed_count": 0,
"data_unaccepted": [], "word_frequencies": [],
}) "data_unaccepted": [],
}
)
# 1. Grade distribution # 1. Grade distribution
grade_counts = dict( grade_counts = dict(submissions.values_list("ai_grade").annotate(count=Count("id")).values_list("ai_grade", "count"))
submissions.values_list("ai_grade")
.annotate(count=Count("id"))
.values_list("ai_grade", "count")
)
# 2. Average score # 2. Average score
avg_score = submissions.aggregate(avg=Avg("ai_score"))["avg"] or 0 avg_score = submissions.aggregate(avg=Avg("ai_score"))["avg"] or 0
@@ -113,9 +137,7 @@ class FlowchartStatisticsAPI(APIView):
wordcloud_texts = [] wordcloud_texts = []
for row in submissions.values_list( for row in submissions.values_list("ai_criteria_details", "ai_feedback", "ai_suggestions").iterator():
"ai_criteria_details", "ai_feedback", "ai_suggestions"
).iterator():
details, feedback, suggestions = row details, feedback, suggestions = row
if details and isinstance(details, dict): if details and isinstance(details, dict):
for key, val in details.items(): for key, val in details.items():
@@ -139,9 +161,7 @@ class FlowchartStatisticsAPI(APIView):
} }
# 4. Completion stats # 4. Completion stats
submitted_users = set( submitted_users = set(submissions.values_list("user__username", flat=True).distinct())
submissions.values_list("user__username", flat=True).distinct()
)
completed_count = len(submitted_users) completed_count = len(submitted_users)
# Unaccepted users # Unaccepted users
@@ -155,16 +175,18 @@ class FlowchartStatisticsAPI(APIView):
# 5. Word cloud from feedback + suggestions + criteria comments # 5. Word cloud from feedback + suggestions + criteria comments
word_freq = self._build_word_frequencies(wordcloud_texts) word_freq = self._build_word_frequencies(wordcloud_texts)
return self.success({ return self.success(
"total_count": total_count, {
"avg_score": round(avg_score, 1), "total_count": total_count,
"grade_distribution": grade_counts, "avg_score": round(avg_score, 1),
"criteria_averages": criteria_averages, "grade_distribution": grade_counts,
"person_count": len(all_users_dict), "criteria_averages": criteria_averages,
"completed_count": completed_count, "person_count": len(all_users_dict),
"word_frequencies": word_freq, "completed_count": completed_count,
"data_unaccepted": unaccepted, "word_frequencies": word_freq,
}) "data_unaccepted": unaccepted,
}
)
@staticmethod @staticmethod
def _build_word_frequencies(texts, top_n=80): def _build_word_frequencies(texts, top_n=80):

View File

@@ -47,11 +47,7 @@ class FlowchartSubmissionAPI(AsyncAPIView):
return self.error("submission_id is required") return self.error("submission_id is required")
try: try:
submission = await ( submission = await FlowchartSubmission.objects.select_related("user", "problem").filter(id=submission_id).afirst()
FlowchartSubmission.objects.select_related("user", "problem")
.filter(id=submission_id)
.afirst()
)
if submission is None: if submission is None:
raise FlowchartSubmission.DoesNotExist raise FlowchartSubmission.DoesNotExist
except FlowchartSubmission.DoesNotExist: except FlowchartSubmission.DoesNotExist:
@@ -74,9 +70,7 @@ class FlowchartSubmissionListAPI(AsyncAPIView):
if problem_id: if problem_id:
try: try:
problem = await Problem.objects.aget( problem = await Problem.objects.aget(_id__iexact=problem_id, contest_id__isnull=True, visible=True)
_id__iexact=problem_id, contest_id__isnull=True, visible=True
)
except Problem.DoesNotExist: except Problem.DoesNotExist:
return self.error("Problem doesn't exist") return self.error("Problem doesn't exist")
queryset = queryset.filter(problem=problem) queryset = queryset.filter(problem=problem)
@@ -90,9 +84,7 @@ class FlowchartSubmissionListAPI(AsyncAPIView):
if request.GET.get("today") == "1": if request.GET.get("today") == "1":
now = timezone.now() now = timezone.now()
queryset = queryset.filter( queryset = queryset.filter(create_time__gte=now.replace(hour=0, minute=0, second=0, microsecond=0))
create_time__gte=now.replace(hour=0, minute=0, second=0, microsecond=0)
)
grade = request.GET.get("grade") grade = request.GET.get("grade")
if grade in ("S", "A", "B", "C"): if grade in ("S", "A", "B", "C"):
@@ -115,11 +107,7 @@ class FlowchartSubmissionRetryAPI(AsyncAPIView):
return self.error("submission_id is required") return self.error("submission_id is required")
try: try:
submission = await ( submission = await FlowchartSubmission.objects.select_related("problem").filter(id=submission_id).afirst()
FlowchartSubmission.objects.select_related("problem")
.filter(id=submission_id)
.afirst()
)
if submission is None: if submission is None:
raise FlowchartSubmission.DoesNotExist raise FlowchartSubmission.DoesNotExist
except FlowchartSubmission.DoesNotExist: except FlowchartSubmission.DoesNotExist:
@@ -187,7 +175,7 @@ class FlowchartSubmissionDetailAPI(AsyncAPIView):
else: else:
if page < 0 or page > count: if page < 0 or page > count:
return self.error("Page out of range") return self.error("Page out of range")
result = [s async for s in submissions[page - 1:page]] result = [s async for s in submissions[page - 1 : page]]
submission = result[0] submission = result[0]
data = await self.async_serialize_data(FlowchartSubmissionSerializer, submission) data = await self.async_serialize_data(FlowchartSubmissionSerializer, submission)
return self.success({"submission": data, "count": count}) return self.success({"submission": data, "count": count})

View File

@@ -35,14 +35,24 @@ class FPSParser(object):
def _parse_one_problem(self, node): def _parse_one_problem(self, node):
sample_start = True sample_start = True
test_case_start = True test_case_start = True
problem = {"title": "No Title", "description": "No Description", problem = {
"input": "No Input Description", "title": "No Title",
"output": "No Output Description", "description": "No Description",
"memory_limit": {"unit": None, "value": None}, "input": "No Input Description",
"time_limit": {"unit": None, "value": None}, "output": "No Output Description",
"samples": [], "images": [], "append": [], "memory_limit": {"unit": None, "value": None},
"template": [], "prepend": [], "test_cases": [], "time_limit": {"unit": None, "value": None},
"hint": None, "source": None, "spj": None, "solution": []} "samples": [],
"images": [],
"append": [],
"template": [],
"prepend": [],
"test_cases": [],
"hint": None,
"source": None,
"spj": None,
"solution": [],
}
for item in node: for item in node:
tag = item.tag tag = item.tag
if tag in ["title", "description", "input", "output", "hint", "source"]: if tag in ["title", "description", "input", "output", "hint", "source"]:
@@ -144,23 +154,17 @@ class FPSHelper(object):
with open(os.path.join(base_dir, str(index + 1) + ".out"), "w", encoding="utf-8") as f: with open(os.path.join(base_dir, str(index + 1) + ".out"), "w", encoding="utf-8") as f:
f.write(output_content) f.write(output_content)
if spj: if spj:
one_info = { one_info = {"input_size": len(input_content), "input_name": f"{index + 1}.in"}
"input_size": len(input_content),
"input_name": f"{index + 1}.in"
}
else: else:
one_info = { one_info = {
"input_size": len(input_content), "input_size": len(input_content),
"input_name": f"{index + 1}.in", "input_name": f"{index + 1}.in",
"output_size": len(output_content), "output_size": len(output_content),
"output_name": f"{index + 1}.out", "output_name": f"{index + 1}.out",
"stripped_output_md5": hashlib.md5(output_content.rstrip().encode("utf-8")).hexdigest() "stripped_output_md5": hashlib.md5(output_content.rstrip().encode("utf-8")).hexdigest(),
} }
test_cases[index] = one_info test_cases[index] = one_info
info = { info = {"spj": True if spj else False, "test_cases": test_cases}
"spj": True if spj else False,
"test_cases": test_cases
}
with open(os.path.join(base_dir, "info"), "w", encoding="utf-8") as f: with open(os.path.join(base_dir, "info"), "w", encoding="utf-8") as f:
f.write(json.dumps(info, indent=4)) f.write(json.dumps(info, indent=4))
return info return info

View File

@@ -7,6 +7,7 @@ if __name__ == "__main__":
import django import django
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
sys.stdout.write("Django VERSION " + str(django.VERSION) + "\n") sys.stdout.write("Django VERSION " + str(django.VERSION) + "\n")
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)

View File

@@ -7,9 +7,7 @@ from utils.models import RichTextField
class Message(models.Model): class Message(models.Model):
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name="sender") sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name="sender")
recipient = models.ForeignKey( recipient = models.ForeignKey(User, on_delete=models.CASCADE, related_name="recipient")
User, on_delete=models.CASCADE, related_name="recipient"
)
submission = models.ForeignKey(Submission, on_delete=models.CASCADE) submission = models.ForeignKey(Submission, on_delete=models.CASCADE)
message = RichTextField() message = RichTextField()
create_time = models.DateTimeField(auto_now_add=True) create_time = models.DateTimeField(auto_now_add=True)

View File

@@ -10,9 +10,7 @@ from utils.api.api import validate_serializer
class MessageAPI(AsyncAPIView): class MessageAPI(AsyncAPIView):
@login_required @login_required
async def get(self, request): async def get(self, request):
messages = Message.objects.select_related( messages = Message.objects.select_related("recipient", "sender", "submission", "submission__problem").filter(recipient=request.user)
"recipient", "sender", "submission", "submission__problem"
).filter(recipient=request.user)
return self.success(await self.async_paginate_data(request, messages, MessageSerializer)) return self.success(await self.async_paginate_data(request, messages, MessageSerializer))
@super_admin_required @super_admin_required

View File

@@ -28,4 +28,3 @@ application = ProtocolTypeRouter(
"websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns)), "websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns)),
} }
) )

View File

@@ -13,4 +13,3 @@ websocket_urlpatterns = [
path("ws/config/", ConfigConsumer.as_asgi()), path("ws/config/", ConfigConsumer.as_asgi()),
path("ws/flowchart/", FlowchartConsumer.as_asgi()), path("ws/flowchart/", FlowchartConsumer.as_asgi()),
] ]

View File

@@ -18,6 +18,7 @@ class my_property:
2. ttl is callable条件缓存 2. ttl is callable条件缓存
3. 缓存 ttl 秒 3. 缓存 ttl 秒
""" """
def __init__(self, func=None, fset=None, ttl=None): def __init__(self, func=None, fset=None, ttl=None):
self.fset = fset self.fset = fset
self.local = threading.local() self.local = threading.local()
@@ -118,8 +119,7 @@ class OptionDefaultValue:
class_list = [] class_list = []
smtp_config = {} smtp_config = {}
judge_server_token = default_token judge_server_token = default_token
throttling = {"ip": {"capacity": 100, "fill_rate": 0.1, "default_capacity": 50}, throttling = {"ip": {"capacity": 100, "fill_rate": 0.1, "default_capacity": 50}, "user": {"capacity": 20, "fill_rate": 0.03, "default_capacity": 10}}
"user": {"capacity": 20, "fill_rate": 0.03, "default_capacity": 10}}
languages = languages languages = languages
enable_maxkb = True enable_maxkb = True
@@ -286,7 +286,6 @@ class _SysOptionsMeta(type):
def enable_maxkb(cls, value): def enable_maxkb(cls, value):
cls._set_option(OptionKeys.enable_maxkb, value) cls._set_option(OptionKeys.enable_maxkb, value)
def reset_languages(cls): def reset_languages(cls):
cls.languages = languages cls.languages = languages
@@ -295,6 +294,7 @@ class SysOptions(metaclass=_SysOptionsMeta):
@classmethod @classmethod
async def aget(cls, key): async def aget(cls, key):
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
return await sync_to_async(getattr)(cls, key) return await sync_to_async(getattr)(cls, key)
@classmethod @classmethod
@@ -303,4 +303,5 @@ class SysOptions(metaclass=_SysOptionsMeta):
def _get_all(): def _get_all():
return {k: getattr(cls, k) for k in keys} return {k: getattr(cls, k) for k in keys}
return await sync_to_async(_get_all)() return await sync_to_async(_get_all)()

View File

@@ -24,9 +24,7 @@ class Command(BaseCommand):
fixed_count = 0 fixed_count = 0
for progress in progresses: for progress in progresses:
problemset_problems = ProblemSetProblem.objects.filter( problemset_problems = ProblemSetProblem.objects.filter(problemset=progress.problemset).select_related("problem")
problemset=progress.problemset
).select_related("problem")
updated = False updated = False
for psp in problemset_problems: for psp in problemset_problems:
@@ -46,10 +44,7 @@ class Command(BaseCommand):
if not accepted: if not accepted:
continue continue
self.stdout.write( self.stdout.write(f" 用户 {progress.user.username} | 题单「{progress.problemset.title}」 | 题目 {psp.problem._id} 已AC但进度未记录")
f" 用户 {progress.user.username} | 题单「{progress.problemset.title}"
f" | 题目 {psp.problem._id} 已AC但进度未记录"
)
if dry_run: if dry_run:
continue continue

View File

@@ -16,21 +16,19 @@ def sync_progress_on_problem_change(sender, instance, created, **kwargs):
try: try:
with transaction.atomic(): with transaction.atomic():
# 获取该题单的所有用户进度 # 获取该题单的所有用户进度
progresses = ProblemSetProgress.objects.filter( progresses = ProblemSetProgress.objects.filter(problemset=instance.problemset)
problemset=instance.problemset
)
# 批量更新所有用户的进度 # 批量更新所有用户的进度
for progress in progresses: for progress in progresses:
progress.update_progress() progress.update_progress()
# 重新计算该题单的所有徽章资格 # 重新计算该题单的所有徽章资格
badges = ProblemSetBadge.objects.filter(problemset=instance.problemset) badges = ProblemSetBadge.objects.filter(problemset=instance.problemset)
for badge in badges: for badge in badges:
badge.recalculate_user_badges() badge.recalculate_user_badges()
logger.info(f"已同步题单 {instance.problemset.id} 的所有用户进度和徽章资格") logger.info(f"已同步题单 {instance.problemset.id} 的所有用户进度和徽章资格")
except Exception as e: except Exception as e:
logger.error(f"同步题单进度时出错: {e}") logger.error(f"同步题单进度时出错: {e}")
@@ -42,27 +40,23 @@ def sync_progress_on_problem_delete(sender, instance, **kwargs):
with transaction.atomic(): with transaction.atomic():
# 清理该题目在题单中的所有提交记录 # 清理该题目在题单中的所有提交记录
from .models import ProblemSetSubmission from .models import ProblemSetSubmission
ProblemSetSubmission.objects.filter(
problemset=instance.problemset, ProblemSetSubmission.objects.filter(problemset=instance.problemset, problem=instance.problem).delete()
problem=instance.problem
).delete()
# 获取该题单的所有用户进度 # 获取该题单的所有用户进度
progresses = ProblemSetProgress.objects.filter( progresses = ProblemSetProgress.objects.filter(problemset=instance.problemset)
problemset=instance.problemset
)
# 批量更新所有用户的进度 # 批量更新所有用户的进度
for progress in progresses: for progress in progresses:
progress.update_progress() progress.update_progress()
# 重新计算该题单的所有徽章资格 # 重新计算该题单的所有徽章资格
badges = ProblemSetBadge.objects.filter(problemset=instance.problemset) badges = ProblemSetBadge.objects.filter(problemset=instance.problemset)
for badge in badges: for badge in badges:
badge.recalculate_user_badges() badge.recalculate_user_badges()
logger.info(f"已同步题单 {instance.problemset.id} 的所有用户进度和徽章资格(删除题目后)") logger.info(f"已同步题单 {instance.problemset.id} 的所有用户进度和徽章资格(删除题目后)")
except Exception as e: except Exception as e:
logger.error(f"同步题单进度时出错: {e}") logger.error(f"同步题单进度时出错: {e}")
@@ -75,7 +69,7 @@ def sync_badges_on_badge_change(sender, instance, created, **kwargs):
# 重新计算该奖章的所有用户资格 # 重新计算该奖章的所有用户资格
instance.recalculate_user_badges() instance.recalculate_user_badges()
logger.info(f"已重新计算题单 {instance.problemset.id} 的奖章 {instance.id} 的用户资格") logger.info(f"已重新计算题单 {instance.problemset.id} 的奖章 {instance.id} 的用户资格")
except Exception as e: except Exception as e:
logger.error(f"重新计算奖章资格时出错: {e}") logger.error(f"重新计算奖章资格时出错: {e}")
@@ -88,6 +82,6 @@ def cleanup_badges_on_badge_delete(sender, instance, **kwargs):
# 删除该奖章的所有用户奖章记录 # 删除该奖章的所有用户奖章记录
UserBadge.objects.filter(badge=instance).delete() UserBadge.objects.filter(badge=instance).delete()
logger.info(f"已清理奖章 {instance.id} 的所有用户奖章记录") logger.info(f"已清理奖章 {instance.id} 的所有用户奖章记录")
except Exception as e: except Exception as e:
logger.error(f"清理用户奖章记录时出错: {e}") logger.error(f"清理用户奖章记录时出错: {e}")

View File

@@ -76,8 +76,7 @@ class ProblemSetAPI(AsyncAPIView):
# 批量查询用户已获得的奖章ID这些题单相关的 # 批量查询用户已获得的奖章ID这些题单相关的
user_earned_badge_ids = { user_earned_badge_ids = {
badge_id badge_id async for badge_id in UserBadge.objects.filter(user=request.user, badge__problemset_id__in=problem_set_ids).values_list("badge_id", flat=True)
async for badge_id in UserBadge.objects.filter(user=request.user, badge__problemset_id__in=problem_set_ids).values_list("badge_id", flat=True)
} }
# 预加载奖章信息在获取ID之后应用避免在获取ID时也预加载 # 预加载奖章信息在获取ID之后应用避免在获取ID时也预加载
@@ -97,12 +96,7 @@ class ProblemSetDetailAPI(AsyncAPIView):
async def get(self, request, problem_set_id): async def get(self, request, problem_set_id):
"""获取题单详情""" """获取题单详情"""
try: try:
problem_set = await ( problem_set = await ProblemSet.objects.select_related("created_by").filter(id=problem_set_id, visible=True).exclude(status=ProblemSetStatus.DRAFT).aget()
ProblemSet.objects.select_related("created_by")
.filter(id=problem_set_id, visible=True)
.exclude(status=ProblemSetStatus.DRAFT)
.aget()
)
except ProblemSet.DoesNotExist: except ProblemSet.DoesNotExist:
return self.error("题单不存在") return self.error("题单不存在")

View File

@@ -19,10 +19,7 @@ def bulk_fetch_problemset_progress(user, problem_ids):
problemset__status=ProblemSetStatus.ACTIVE, problemset__status=ProblemSetStatus.ACTIVE,
problemset__problemsetproblem__problem_id__in=problem_ids, problemset__problemsetproblem__problem_id__in=problem_ids,
) )
.filter( .filter(models.Q(problemset__end_time__isnull=True) | models.Q(problemset__end_time__gt=timezone.now()))
models.Q(problemset__end_time__isnull=True)
| models.Q(problemset__end_time__gt=timezone.now())
)
.annotate(matched_problem_id=F("problemset__problemsetproblem__problem_id")) .annotate(matched_problem_id=F("problemset__problemsetproblem__problem_id"))
.only("join_time", "progress_detail") .only("join_time", "progress_detail")
) )
@@ -51,7 +48,6 @@ class ShareSubmissionSerializer(serializers.Serializer):
class SubmissionModelSerializer(serializers.ModelSerializer): class SubmissionModelSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Submission model = Submission
fields = "__all__" fields = "__all__"
@@ -92,11 +88,7 @@ class SubmissionListSerializer(serializers.ModelSerializer):
# 如果该题目已在题单中做出来了,则恢复显示 # 如果该题目已在题单中做出来了,则恢复显示
if obj.user_id == self.user.id and self.user.is_regular_user(): if obj.user_id == self.user.id and self.user.is_regular_user():
progress = self._get_problemset_progress(obj.problem_id) progress = self._get_problemset_progress(obj.problem_id)
if ( if progress and obj.create_time < progress.join_time and str(obj.problem_id) not in progress.progress_detail:
progress
and obj.create_time < progress.join_time
and str(obj.problem_id) not in progress.progress_detail
):
return False return False
return True return True
@@ -111,10 +103,7 @@ class SubmissionListSerializer(serializers.ModelSerializer):
problemset__status=ProblemSetStatus.ACTIVE, problemset__status=ProblemSetStatus.ACTIVE,
problemset__problemsetproblem__problem_id=problem_id, problemset__problemsetproblem__problem_id=problem_id,
) )
.filter( .filter(models.Q(problemset__end_time__isnull=True) | models.Q(problemset__end_time__gt=timezone.now()))
models.Q(problemset__end_time__isnull=True)
| models.Q(problemset__end_time__gt=timezone.now())
)
.only("join_time", "progress_detail") .only("join_time", "progress_detail")
.first() .first()
) )

View File

@@ -22,9 +22,7 @@ class SubmissionRejudgeAPI(APIView):
if not id: if not id:
return self.error("Parameter error, id is required") return self.error("Parameter error, id is required")
try: try:
submission = Submission.objects.select_related("problem").get( submission = Submission.objects.select_related("problem").get(id=id, contest_id__isnull=True)
id=id, contest_id__isnull=True
)
except Submission.DoesNotExist: except Submission.DoesNotExist:
return self.error("Submission does not exists") return self.error("Submission does not exists")
submission.statistic_info = {} submission.statistic_info = {}
@@ -46,17 +44,13 @@ class SubmissionStatisticsAPI(APIView):
filters = {"contest_id__isnull": True, "create_time__lte": end} filters = {"contest_id__isnull": True, "create_time__lte": end}
if start: if start:
filters["create_time__gte"] = start filters["create_time__gte"] = start
submissions = Submission.objects.filter( submissions = Submission.objects.filter(**filters).select_related("problem__created_by")
**filters
).select_related("problem__created_by")
problem_id = request.GET.get("problem_id") problem_id = request.GET.get("problem_id")
if problem_id: if problem_id:
try: try:
problem = Problem.objects.get( problem = Problem.objects.get(_id__iexact=problem_id, contest_id__isnull=True, visible=True)
_id__iexact=problem_id, contest_id__isnull=True, visible=True
)
except Problem.DoesNotExist: except Problem.DoesNotExist:
return self.error("Problem doesn't exist") return self.error("Problem doesn't exist")
submissions = submissions.filter(problem=problem) submissions = submissions.filter(problem=problem)
@@ -82,9 +76,7 @@ class SubmissionStatisticsAPI(APIView):
) )
submission_count = submission_stats["total_count"] submission_count = submission_stats["total_count"]
accepted_count = submission_stats["accepted_count"] accepted_count = submission_stats["accepted_count"]
correct_rate = ( correct_rate = round(accepted_count / submission_count * 100, 2) if submission_count else 0
round(accepted_count / submission_count * 100, 2) if submission_count else 0
)
# 优化:获取用户提交统计 # 优化:获取用户提交统计
user_submissions = ( user_submissions = (
@@ -99,20 +91,13 @@ class SubmissionStatisticsAPI(APIView):
# 获取所有有提交记录的用户的class_name信息 # 获取所有有提交记录的用户的class_name信息
submitted_usernames = {item["username"] for item in user_submissions} submitted_usernames = {item["username"] for item in user_submissions}
if submitted_usernames: if submitted_usernames:
submitted_users_dict = { submitted_users_dict = {user["username"]: user["class_name"] for user in User.objects.filter(username__in=submitted_usernames).values("username", "class_name")}
user["username"]: user["class_name"]
for user in User.objects.filter(
username__in=submitted_usernames
).values("username", "class_name")
}
else: else:
submitted_users_dict = {} submitted_users_dict = {}
# 预先收集每个用户的提交ID和结果按时间倒序 # 预先收集每个用户的提交ID和结果按时间倒序
submission_items_by_user = {} submission_items_by_user = {}
for submission in submissions.values("username", "id", "result").order_by( for submission in submissions.values("username", "id", "result").order_by("-create_time"):
"-create_time"
):
username_key = submission["username"] username_key = submission["username"]
submission_id = str(submission["id"]) submission_id = str(submission["id"])
submission_items_by_user.setdefault(username_key, []).append( submission_items_by_user.setdefault(username_key, []).append(
@@ -137,9 +122,7 @@ class SubmissionStatisticsAPI(APIView):
"submission_count": item["submission_count"], "submission_count": item["submission_count"],
"accepted_count": item["accepted_count"], "accepted_count": item["accepted_count"],
"correct_rate": f"{rate}%", "correct_rate": f"{rate}%",
"submission_items": submission_items_by_user.get( "submission_items": submission_items_by_user.get(username_key, []),
username_key, []
),
} }
) )

View File

@@ -37,9 +37,7 @@ class SubmissionAPI(AsyncAPIView):
auth_method = getattr(request, "auth_method", "") auth_method = getattr(request, "auth_method", "")
if auth_method == "api_key": if auth_method == "api_key":
return return
user_bucket = TokenBucket( user_bucket = TokenBucket(key=str(request.user.id), redis_conn=cache, **SysOptions.throttling["user"])
key=str(request.user.id), redis_conn=cache, **SysOptions.throttling["user"]
)
can_consume, wait = user_bucket.consume() can_consume, wait = user_bucket.consume()
if not can_consume: if not can_consume:
return "Please wait %d seconds" % (int(wait)) return "Please wait %d seconds" % (int(wait))
@@ -52,10 +50,7 @@ class SubmissionAPI(AsyncAPIView):
if not request.user.is_contest_admin(contest): if not request.user.is_contest_admin(contest):
user_ip = ipaddress.ip_address(request.session.get("ip")) user_ip = ipaddress.ip_address(request.session.get("ip"))
if contest.allowed_ip_ranges: if contest.allowed_ip_ranges:
if not any( if not any(user_ip in ipaddress.ip_network(cidr, strict=False) for cidr in contest.allowed_ip_ranges):
user_ip in ipaddress.ip_network(cidr, strict=False)
for cidr in contest.allowed_ip_ranges
):
return self.error("Your IP is not allowed in this contest") return self.error("Your IP is not allowed in this contest")
@login_required @login_required
@@ -79,9 +74,7 @@ class SubmissionAPI(AsyncAPIView):
return self.error(error) return self.error(error)
try: try:
problem = await Problem.objects.aget( problem = await Problem.objects.aget(id=data["problem_id"], contest_id=data.get("contest_id"), visible=True)
id=data["problem_id"], contest_id=data.get("contest_id"), visible=True
)
except Problem.DoesNotExist: except Problem.DoesNotExist:
return self.error("Problem not exist") return self.error("Problem not exist")
if data["language"] not in problem.languages: if data["language"] not in problem.languages:
@@ -108,9 +101,7 @@ class SubmissionAPI(AsyncAPIView):
if not submission_id: if not submission_id:
return self.error("Parameter id doesn't exist") return self.error("Parameter id doesn't exist")
try: try:
submission = await Submission.objects.select_related("problem", "contest").aget( submission = await Submission.objects.select_related("problem", "contest").aget(id=submission_id)
id=submission_id
)
except Submission.DoesNotExist: except Submission.DoesNotExist:
return self.error("Submission doesn't exist") return self.error("Submission doesn't exist")
if not submission.check_user_permission(request.user): if not submission.check_user_permission(request.user):
@@ -120,26 +111,19 @@ class SubmissionAPI(AsyncAPIView):
submission_data = await self.async_serialize_data(SubmissionModelSerializer, submission) submission_data = await self.async_serialize_data(SubmissionModelSerializer, submission)
else: else:
submission_data = await self.async_serialize_data(SubmissionSafeModelSerializer, submission) submission_data = await self.async_serialize_data(SubmissionSafeModelSerializer, submission)
submission_data["can_unshare"] = submission.check_user_permission( submission_data["can_unshare"] = submission.check_user_permission(request.user, check_share=False)
request.user, check_share=False
)
return self.success(submission_data) return self.success(submission_data)
@login_required @login_required
@validate_serializer(ShareSubmissionSerializer) @validate_serializer(ShareSubmissionSerializer)
async def put(self, request): async def put(self, request):
try: try:
submission = await Submission.objects.select_related("problem", "contest").aget( submission = await Submission.objects.select_related("problem", "contest").aget(id=request.data["id"])
id=request.data["id"]
)
except Submission.DoesNotExist: except Submission.DoesNotExist:
return self.error("Submission doesn't exist") return self.error("Submission doesn't exist")
if not submission.check_user_permission(request.user, check_share=False): if not submission.check_user_permission(request.user, check_share=False):
return self.error("No permission to share the submission") return self.error("No permission to share the submission")
if ( if submission.contest and submission.contest.status == ContestStatus.CONTEST_UNDERWAY:
submission.contest
and submission.contest.status == ContestStatus.CONTEST_UNDERWAY
):
return self.error("Can not share submission now") return self.error("Can not share submission now")
submission.shared = request.data["shared"] submission.shared = request.data["shared"]
await submission.asave(update_fields=["shared"]) await submission.asave(update_fields=["shared"])
@@ -153,9 +137,7 @@ class SubmissionListAPI(AsyncAPIView):
if request.GET.get("contest_id"): if request.GET.get("contest_id"):
return self.error("Parameter error") return self.error("Parameter error")
submissions = Submission.objects.filter(contest_id__isnull=True).select_related( submissions = Submission.objects.filter(contest_id__isnull=True).select_related("problem").order_by("-create_time")
"problem"
).order_by("-create_time")
problem_id = request.GET.get("problem_id") problem_id = request.GET.get("problem_id")
myself = request.GET.get("myself") myself = request.GET.get("myself")
result = request.GET.get("result") result = request.GET.get("result")
@@ -163,9 +145,7 @@ class SubmissionListAPI(AsyncAPIView):
language = request.GET.get("language") language = request.GET.get("language")
if problem_id: if problem_id:
try: try:
problem = await Problem.objects.aget( problem = await Problem.objects.aget(_id__iexact=problem_id, contest_id__isnull=True, visible=True)
_id__iexact=problem_id, contest_id__isnull=True, visible=True
)
except Problem.DoesNotExist: except Problem.DoesNotExist:
return self.error("Problem doesn't exist") return self.error("Problem doesn't exist")
submissions = submissions.filter(problem=problem) submissions = submissions.filter(problem=problem)
@@ -184,9 +164,7 @@ class SubmissionListAPI(AsyncAPIView):
submissions = submissions.filter(language=language) submissions = submissions.filter(language=language)
if request.GET.get("today") == "1": if request.GET.get("today") == "1":
now = timezone.now() now = timezone.now()
submissions = submissions.filter( submissions = submissions.filter(create_time__gte=now.replace(hour=0, minute=0, second=0, microsecond=0))
create_time__gte=now.replace(hour=0, minute=0, second=0, microsecond=0)
)
data = await self.async_paginate_data(request, submissions) data = await self.async_paginate_data(request, submissions)
results = data["results"] results = data["results"]
@@ -212,18 +190,14 @@ class ContestSubmissionListAPI(AsyncAPIView):
return self.error("Limit is needed") return self.error("Limit is needed")
contest = self.contest contest = self.contest
submissions = Submission.objects.filter(contest_id=contest.id).select_related( submissions = Submission.objects.filter(contest_id=contest.id).select_related("problem", "contest").order_by("-create_time")
"problem", "contest"
).order_by("-create_time")
problem_id = request.GET.get("problem_id") problem_id = request.GET.get("problem_id")
myself = request.GET.get("myself") myself = request.GET.get("myself")
result = request.GET.get("result") result = request.GET.get("result")
username = request.GET.get("username") username = request.GET.get("username")
if problem_id: if problem_id:
try: try:
problem = await Problem.objects.aget( problem = await Problem.objects.aget(_id__iexact=problem_id, contest_id=contest.id, visible=True)
_id__iexact=problem_id, contest_id=contest.id, visible=True
)
except Problem.DoesNotExist: except Problem.DoesNotExist:
return self.error("Problem doesn't exist") return self.error("Problem doesn't exist")
submissions = submissions.filter(problem=problem) submissions = submissions.filter(problem=problem)
@@ -245,10 +219,7 @@ class ContestSubmissionListAPI(AsyncAPIView):
progress_cache = await sync_to_async(bulk_fetch_problemset_progress)(request.user, problem_ids) progress_cache = await sync_to_async(bulk_fetch_problemset_progress)(request.user, problem_ids)
else: else:
progress_cache = {} progress_cache = {}
data["results"] = await self.async_serialize_data( data["results"] = await self.async_serialize_data(SubmissionListSerializer, results, many=True, user=request.user, problemset_progress_cache=progress_cache)
SubmissionListSerializer,
results, many=True, user=request.user, problemset_progress_cache=progress_cache
)
return self.success(data) return self.success(data)
@@ -257,12 +228,7 @@ class SubmissionExistsAPI(AsyncAPIView):
async def get(self, request): async def get(self, request):
if not request.GET.get("problem_id"): if not request.GET.get("problem_id"):
return self.error("Parameter error, problem_id is required") return self.error("Parameter error, problem_id is required")
exists = ( exists = request.user.is_authenticated and await Submission.objects.filter(problem_id=request.GET["problem_id"], user_id=request.user.id).aexists()
request.user.is_authenticated
and await Submission.objects.filter(
problem_id=request.GET["problem_id"], user_id=request.user.id
).aexists()
)
return self.success(exists) return self.success(exists)
@@ -271,13 +237,9 @@ class SubmissionsTodayCount(AsyncAPIView):
now = timezone.now() now = timezone.now()
start = now.replace(hour=0, minute=0, second=0, microsecond=0) start = now.replace(hour=0, minute=0, second=0, microsecond=0)
if request.GET.get("language") == "Flowchart": if request.GET.get("language") == "Flowchart":
count = await FlowchartSubmission.objects.filter( count = await FlowchartSubmission.objects.filter(create_time__gte=start).acount()
create_time__gte=start
).acount()
else: else:
count = await Submission.objects.filter( count = await Submission.objects.filter(contest_id__isnull=True, create_time__gte=start).acount()
contest_id__isnull=True, create_time__gte=start
).acount()
return self.success(count) return self.success(count)

View File

@@ -1 +0,0 @@

View File

@@ -1 +0,0 @@

View File

@@ -53,19 +53,10 @@ class TutorialAdminAPI(APIView):
return self.success(TutorialSerializer(tutorial).data) return self.success(TutorialSerializer(tutorial).data)
except Tutorial.DoesNotExist: except Tutorial.DoesNotExist:
return self.error("Tutorial does not exist") return self.error("Tutorial does not exist")
tutorials = Tutorial.objects.all().order_by("order", "-created_at") tutorials = Tutorial.objects.all().order_by("order", "-created_at")
# 按 type 分组返回数据 # 按 type 分组返回数据
result = { result = {"python": TutorialListSerializer(tutorials.filter(type="python"), many=True).data, "c": TutorialListSerializer(tutorials.filter(type="c"), many=True).data}
"python": TutorialListSerializer(
tutorials.filter(type="python"),
many=True
).data,
"c": TutorialListSerializer(
tutorials.filter(type="c"),
many=True
).data
}
return self.success(result) return self.success(result)
@super_admin_required @super_admin_required

View File

@@ -16,9 +16,7 @@ class TutorialAPI(APIView):
class TutorialTitlesAPI(APIView): class TutorialTitlesAPI(APIView):
def get(self, request): def get(self, request):
type = request.GET.get("type") or "python" type = request.GET.get("type") or "python"
tutorials = Tutorial.objects.filter(is_public=True, type=type).values( tutorials = Tutorial.objects.filter(is_public=True, type=type).values("id", "title")
"id", "title"
)
return self.success(list(tutorials)) return self.success(list(tutorials))

View File

@@ -62,6 +62,7 @@ class APIView(View):
- self.response 返回一个django HttpResponse, 具体在self.response_class中实现 - self.response 返回一个django HttpResponse, 具体在self.response_class中实现
- parse请求的类需要定义在request_parser中, 目前只支持json和urlencoded的类型, 用来解析请求的数据 - parse请求的类需要定义在request_parser中, 目前只支持json和urlencoded的类型, 用来解析请求的数据
""" """
request_parsers = (JSONParser, URLEncodedParser) request_parsers = (JSONParser, URLEncodedParser)
response_class = JSONResponse response_class = JSONResponse
@@ -134,11 +135,10 @@ class APIView(View):
offset = 0 offset = 0
# 只调用一次 count(),避免重复查询 # 只调用一次 count(),避免重复查询
count = query_set.count() count = query_set.count()
results = query_set[offset:offset + limit] results = query_set[offset : offset + limit]
if object_serializer: if object_serializer:
results = object_serializer(results, many=True, context={"request": request}).data results = object_serializer(results, many=True, context={"request": request}).data
data = {"results": results, data = {"results": results, "total": count}
"total": count}
return data return data
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
@@ -215,8 +215,9 @@ class AsyncAPIView(APIView):
offset = 0 offset = 0
if offset < 0: if offset < 0:
offset = 0 offset = 0
async def _slice(): async def _slice():
return [item async for item in query_set[offset:offset + limit]] return [item async for item in query_set[offset : offset + limit]]
count, results = await asyncio.gather( count, results = await asyncio.gather(
query_set.acount(), query_set.acount(),
@@ -245,8 +246,10 @@ def validate_serializer(serializer):
def post(self, request): def post(self, request):
return self.success(request.data) return self.success(request.data)
""" """
def validate(view_method): def validate(view_method):
if inspect.iscoroutinefunction(view_method): if inspect.iscoroutinefunction(view_method):
@functools.wraps(view_method) @functools.wraps(view_method)
async def async_handle(*args, **kwargs): async def async_handle(*args, **kwargs):
self = args[0] self = args[0]
@@ -261,6 +264,7 @@ def validate_serializer(serializer):
return response return response
else: else:
return self.invalid_serializer(s) return self.invalid_serializer(s)
return async_handle return async_handle
@functools.wraps(view_method) @functools.wraps(view_method)
@@ -274,6 +278,7 @@ def validate_serializer(serializer):
return view_method(*args, **kwargs) return view_method(*args, **kwargs)
else: else:
return self.invalid_serializer(s) return self.invalid_serializer(s)
return handle return handle
return validate return validate

View File

@@ -15,7 +15,7 @@ class Command(BaseCommand):
password = options["password"] password = options["password"]
action = options["action"] action = options["action"]
if not(username and password and action): if not (username and password and action):
self.stdout.write(self.style.ERROR("Invalid args")) self.stdout.write(self.style.ERROR("Invalid args"))
exit(1) exit(1)
@@ -24,8 +24,7 @@ class Command(BaseCommand):
self.stdout.write(self.style.SUCCESS(f"User {username} exists, operation ignored")) self.stdout.write(self.style.SUCCESS(f"User {username} exists, operation ignored"))
exit() exit()
user = User.objects.create(username=username, admin_type=AdminType.SUPER_ADMIN, user = User.objects.create(username=username, admin_type=AdminType.SUPER_ADMIN, problem_permission=ProblemPermission.ALL)
problem_permission=ProblemPermission.ALL)
user.set_password(password) user.set_password(password)
user.save() user.save()
UserProfile.objects.create(user=user) UserProfile.objects.create(user=user)

View File

@@ -37,7 +37,7 @@ def build_query_string(kv_data, ignore_none=True):
query_string += "&" query_string += "&"
else: else:
query_string = "?" query_string = "?"
query_string += (k + "=" + str(v)) query_string += k + "=" + str(v)
return query_string return query_string
@@ -60,8 +60,7 @@ def datetime2str(value, format="iso-8601"):
def natural_sort_key(s, _nsre=re.compile(r"(\d+)")): def natural_sort_key(s, _nsre=re.compile(r"(\d+)")):
return [int(text) if text.isdigit() else text.lower() return [int(text) if text.isdigit() else text.lower() for text in re.split(_nsre, s)]
for text in re.split(_nsre, s)]
def send_email(smtp_config, from_name, to_email, to_name, subject, content): def send_email(smtp_config, from_name, to_email, to_name, subject, content):

View File

@@ -5,6 +5,7 @@ class TokenBucket:
""" """
注意对于单个key的操作不是线程安全的 注意对于单个key的操作不是线程安全的
""" """
def __init__(self, key, capacity, fill_rate, default_capacity, redis_conn): def __init__(self, key, capacity, fill_rate, default_capacity, redis_conn):
""" """
:param capacity: 最大容量 :param capacity: 最大容量

View File

@@ -18,17 +18,11 @@ class SimditorImageUploadAPIView(CSRFExemptAPIView):
if form.is_valid(): if form.is_valid():
img = form.cleaned_data["image"] img = form.cleaned_data["image"]
else: else:
return self.response({ return self.response({"success": False, "msg": "Upload failed", "file_path": ""})
"success": False,
"msg": "Upload failed",
"file_path": ""})
suffix = os.path.splitext(img.name)[-1].lower() suffix = os.path.splitext(img.name)[-1].lower()
if suffix not in [".gif", ".jpg", ".jpeg", ".bmp", ".png"]: if suffix not in [".gif", ".jpg", ".jpeg", ".bmp", ".png"]:
return self.response({ return self.response({"success": False, "msg": "Unsupported file format", "file_path": ""})
"success": False,
"msg": "Unsupported file format",
"file_path": ""})
img_name = rand_str(10) + suffix img_name = rand_str(10) + suffix
try: try:
with open(os.path.join(settings.UPLOAD_DIR, img_name), "wb") as imgFile: with open(os.path.join(settings.UPLOAD_DIR, img_name), "wb") as imgFile:
@@ -36,14 +30,8 @@ class SimditorImageUploadAPIView(CSRFExemptAPIView):
imgFile.write(chunk) imgFile.write(chunk)
except IOError as e: except IOError as e:
logger.error(e) logger.error(e)
return self.response({ return self.response({"success": False, "msg": "Upload Error", "file_path": ""})
"success": False, return self.response({"success": True, "msg": "Success", "file_path": f"{settings.UPLOAD_PREFIX}/{img_name}"})
"msg": "Upload Error",
"file_path": ""})
return self.response({
"success": True,
"msg": "Success",
"file_path": f"{settings.UPLOAD_PREFIX}/{img_name}"})
# DEPRECATED: 前端未调用 (2026-05-26) # DEPRECATED: 前端未调用 (2026-05-26)
@@ -55,10 +43,7 @@ class SimditorFileUploadAPIView(CSRFExemptAPIView):
if form.is_valid(): if form.is_valid():
file = form.cleaned_data["file"] file = form.cleaned_data["file"]
else: else:
return self.response({ return self.response({"success": False, "msg": "Upload failed"})
"success": False,
"msg": "Upload failed"
})
suffix = os.path.splitext(file.name)[-1].lower() suffix = os.path.splitext(file.name)[-1].lower()
file_name = rand_str(10) + suffix file_name = rand_str(10) + suffix
@@ -68,11 +53,5 @@ class SimditorFileUploadAPIView(CSRFExemptAPIView):
f.write(chunk) f.write(chunk)
except IOError as e: except IOError as e:
logger.error(e) logger.error(e)
return self.response({ return self.response({"success": False, "msg": "Upload Error"})
"success": False, return self.response({"success": True, "msg": "Success", "file_path": f"{settings.UPLOAD_PREFIX}/{file_name}", "file_name": file.name})
"msg": "Upload Error"})
return self.response({
"success": True,
"msg": "Success",
"file_path": f"{settings.UPLOAD_PREFIX}/{file_name}",
"file_name": file.name})

View File

@@ -1,6 +1,7 @@
""" """
WebSocket utility functions for pushing real-time updates WebSocket utility functions for pushing real-time updates
""" """
import logging import logging
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
@@ -12,21 +13,21 @@ logger = logging.getLogger(__name__)
def push_submission_update(submission_id: str, user_id: int, data: dict): def push_submission_update(submission_id: str, user_id: int, data: dict):
""" """
推送提交状态更新到指定用户的 WebSocket 连接 推送提交状态更新到指定用户的 WebSocket 连接
Args: Args:
submission_id: 提交 ID submission_id: 提交 ID
user_id: 用户 ID user_id: 用户 ID
data: 要发送的数据,应该包含 type, submission_id, result 等字段 data: 要发送的数据,应该包含 type, submission_id, result 等字段
""" """
channel_layer = get_channel_layer() channel_layer = get_channel_layer()
if channel_layer is None: if channel_layer is None:
logger.warning("Channel layer is not configured, cannot push submission update") logger.warning("Channel layer is not configured, cannot push submission update")
return return
# 构建组名,与 SubmissionConsumer 中的组名一致 # 构建组名,与 SubmissionConsumer 中的组名一致
group_name = f"submission_user_{user_id}" group_name = f"submission_user_{user_id}"
try: try:
# 向指定用户组发送消息 # 向指定用户组发送消息
# type 字段对应 consumer 中的方法名submission_update # type 字段对应 consumer 中的方法名submission_update
@@ -35,7 +36,7 @@ def push_submission_update(submission_id: str, user_id: int, data: dict):
{ {
"type": "submission_update", # 对应 SubmissionConsumer.submission_update 方法 "type": "submission_update", # 对应 SubmissionConsumer.submission_update 方法
"data": data, "data": data,
} },
) )
logger.info(f"Pushed submission update: submission_id={submission_id}, user_id={user_id}, status={data.get('status')}") logger.info(f"Pushed submission update: submission_id={submission_id}, user_id={user_id}, status={data.get('status')}")
except Exception as e: except Exception as e:
@@ -45,30 +46,27 @@ def push_submission_update(submission_id: str, user_id: int, data: dict):
def push_to_user(user_id: int, message_type: str, data: dict): def push_to_user(user_id: int, message_type: str, data: dict):
""" """
向指定用户推送自定义消息 向指定用户推送自定义消息
Args: Args:
user_id: 用户 ID user_id: 用户 ID
message_type: 消息类型 message_type: 消息类型
data: 消息数据 data: 消息数据
""" """
channel_layer = get_channel_layer() channel_layer = get_channel_layer()
if channel_layer is None: if channel_layer is None:
logger.warning("Channel layer is not configured, cannot push message") logger.warning("Channel layer is not configured, cannot push message")
return return
group_name = f"submission_user_{user_id}" group_name = f"submission_user_{user_id}"
try: try:
async_to_sync(channel_layer.group_send)( async_to_sync(channel_layer.group_send)(
group_name, group_name,
{ {
"type": "submission_update", "type": "submission_update",
"data": { "data": {"type": message_type, **data},
"type": message_type, },
**data
},
}
) )
logger.info(f"Pushed message to user {user_id}: type={message_type}") logger.info(f"Pushed message to user {user_id}: type={message_type}")
except Exception as e: except Exception as e:
@@ -78,21 +76,21 @@ def push_to_user(user_id: int, message_type: str, data: dict):
def push_flowchart_evaluation_update(submission_id: str, user_id: int, data: dict): def push_flowchart_evaluation_update(submission_id: str, user_id: int, data: dict):
""" """
推送流程图评分状态更新到指定用户的 WebSocket 连接 推送流程图评分状态更新到指定用户的 WebSocket 连接
Args: Args:
submission_id: 流程图提交 ID submission_id: 流程图提交 ID
user_id: 用户 ID user_id: 用户 ID
data: 要发送的数据,应该包含 type, submission_id, score, grade, feedback 等字段 data: 要发送的数据,应该包含 type, submission_id, score, grade, feedback 等字段
""" """
channel_layer = get_channel_layer() channel_layer = get_channel_layer()
if channel_layer is None: if channel_layer is None:
logger.warning("Channel layer is not configured, cannot push flowchart evaluation update") logger.warning("Channel layer is not configured, cannot push flowchart evaluation update")
return return
# 构建组名,与 FlowchartConsumer 中的组名一致 # 构建组名,与 FlowchartConsumer 中的组名一致
group_name = f"flowchart_user_{user_id}" group_name = f"flowchart_user_{user_id}"
try: try:
# 向指定用户组发送消息 # 向指定用户组发送消息
# type 字段对应 consumer 中的方法名flowchart_evaluation_update # type 字段对应 consumer 中的方法名flowchart_evaluation_update
@@ -101,7 +99,7 @@ def push_flowchart_evaluation_update(submission_id: str, user_id: int, data: dic
{ {
"type": "flowchart_evaluation_update", # 对应 FlowchartConsumer.flowchart_evaluation_update 方法 "type": "flowchart_evaluation_update", # 对应 FlowchartConsumer.flowchart_evaluation_update 方法
"data": data, "data": data,
} },
) )
logger.info(f"Pushed flowchart evaluation update: submission_id={submission_id}, user_id={user_id}, type={data.get('type')}") logger.info(f"Pushed flowchart evaluation update: submission_id={submission_id}, user_id={user_id}, type={data.get('type')}")
except Exception as e: except Exception as e:
@@ -111,33 +109,23 @@ def push_flowchart_evaluation_update(submission_id: str, user_id: int, data: dic
def push_config_update(key: str, value): def push_config_update(key: str, value):
""" """
推送配置更新到所有连接的客户端 推送配置更新到所有连接的客户端
Args: Args:
key: 配置键名 key: 配置键名
value: 配置值 value: 配置值
""" """
channel_layer = get_channel_layer() channel_layer = get_channel_layer()
if channel_layer is None: if channel_layer is None:
logger.warning("Channel layer is not configured, cannot push config update") logger.warning("Channel layer is not configured, cannot push config update")
return return
# 使用全局配置组名 # 使用全局配置组名
group_name = "config_updates" group_name = "config_updates"
try: try:
# 向所有连接的客户端发送配置更新 # 向所有连接的客户端发送配置更新
async_to_sync(channel_layer.group_send)( async_to_sync(channel_layer.group_send)(group_name, {"type": "config_update", "data": {"type": "config_update", "key": key, "value": value}})
group_name,
{
"type": "config_update",
"data": {
"type": "config_update",
"key": key,
"value": value
}
}
)
logger.info(f"Pushed config update: {key}={value}") logger.info(f"Pushed config update: {key}={value}")
except Exception as e: except Exception as e:
logger.error(f"Failed to push config update: {key}={value}, error={str(e)}") logger.error(f"Failed to push config update: {key}={value}, error={str(e)}")

View File

@@ -25,17 +25,52 @@ Python 2.6+ or 3.2+
Cannot defense xss in browser which is belowed IE7 Cannot defense xss in browser which is belowed IE7
浏览器版本IE7+ 或其他浏览器无法防御IE6及以下版本浏览器中的XSS 浏览器版本IE7+ 或其他浏览器无法防御IE6及以下版本浏览器中的XSS
""" """
import copy import copy
import re import re
from html.parser import HTMLParser from html.parser import HTMLParser
class XSSHtml(HTMLParser): class XSSHtml(HTMLParser):
allow_tags = ['a', 'img', 'br', 'strong', 'b', 'code', 'pre', allow_tags = [
'p', 'div', 'em', 'span', 'h1', 'h2', 'h3', 'h4', "a",
'h5', 'h6', 'blockquote', 'ul', 'ol', 'tr', 'th', 'td', "img",
'hr', 'li', 'u', 'embed', 's', 'table', 'thead', 'tbody', "br",
'caption', 'small', 'q', 'sup', 'sub', 'font'] "strong",
"b",
"code",
"pre",
"p",
"div",
"em",
"span",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"blockquote",
"ul",
"ol",
"tr",
"th",
"td",
"hr",
"li",
"u",
"embed",
"s",
"table",
"thead",
"tbody",
"caption",
"small",
"q",
"sup",
"sub",
"font",
]
common_attrs = ["style", "class", "name"] common_attrs = ["style", "class", "name"]
nonend_tags = ["img", "hr", "br", "embed"] nonend_tags = ["img", "hr", "br", "embed"]
tags_own_attrs = { tags_own_attrs = {
@@ -43,7 +78,7 @@ class XSSHtml(HTMLParser):
"a": ["href", "target", "rel", "title"], "a": ["href", "target", "rel", "title"],
"embed": ["src", "width", "height", "type", "allowfullscreen", "loop", "play", "wmode", "menu"], "embed": ["src", "width", "height", "type", "allowfullscreen", "loop", "play", "wmode", "menu"],
"table": ["border", "cellpadding", "cellspacing"], "table": ["border", "cellpadding", "cellspacing"],
"font": ["color"] "font": ["color"],
} }
def __init__(self, allows=[]): def __init__(self, allows=[]):
@@ -68,9 +103,9 @@ class XSSHtml(HTMLParser):
Get the safe html code Get the safe html code
""" """
for i in range(0, len(self.result)): for i in range(0, len(self.result)):
if self.result[i].strip('\n'): if self.result[i].strip("\n"):
self.data.append(self.result[i]) self.data.append(self.result[i])
return ''.join(self.data) return "".join(self.data)
def handle_startendtag(self, tag, attrs): def handle_startendtag(self, tag, attrs):
self.handle_starttag(tag, attrs) self.handle_starttag(tag, attrs)
@@ -78,7 +113,7 @@ class XSSHtml(HTMLParser):
def handle_starttag(self, tag, attrs): def handle_starttag(self, tag, attrs):
if tag not in self.allow_tags: if tag not in self.allow_tags:
return return
end_diagonal = ' /' if tag in self.nonend_tags else '' end_diagonal = " /" if tag in self.nonend_tags else ""
if not end_diagonal: if not end_diagonal:
self.start.append(tag) self.start.append(tag)
attdict = {} attdict = {}
@@ -92,14 +127,14 @@ class XSSHtml(HTMLParser):
attdict = self.node_default(attdict) attdict = self.node_default(attdict)
attrs = [] attrs = []
for (key, value) in attdict.items(): for key, value in attdict.items():
attrs.append('%s="%s"' % (key, self._htmlspecialchars(value))) attrs.append('%s="%s"' % (key, self._htmlspecialchars(value)))
attrs = (' ' + ' '.join(attrs)) if attrs else '' attrs = (" " + " ".join(attrs)) if attrs else ""
self.result.append('<' + tag + attrs + end_diagonal + '>') self.result.append("<" + tag + attrs + end_diagonal + ">")
def handle_endtag(self, tag): def handle_endtag(self, tag):
if self.start and tag == self.start[len(self.start) - 1]: if self.start and tag == self.start[len(self.start) - 1]:
self.result.append('</' + tag + '>') self.result.append("</" + tag + ">")
self.start.pop() self.start.pop()
def handle_data(self, data): def handle_data(self, data):
@@ -121,22 +156,23 @@ class XSSHtml(HTMLParser):
attrs = self._common_attr(attrs) attrs = self._common_attr(attrs)
attrs = self._get_link(attrs, "href") attrs = self._get_link(attrs, "href")
attrs = self._set_attr_default(attrs, "target", "_blank") attrs = self._set_attr_default(attrs, "target", "_blank")
attrs = self._limit_attr(attrs, { attrs = self._limit_attr(attrs, {"target": ["_blank", "_self"]})
"target": ["_blank", "_self"]
})
return attrs return attrs
def node_embed(self, attrs): def node_embed(self, attrs):
attrs = self._common_attr(attrs) attrs = self._common_attr(attrs)
attrs = self._get_link(attrs, "src") attrs = self._get_link(attrs, "src")
attrs = self._limit_attr(attrs, { attrs = self._limit_attr(
"type": ["application/x-shockwave-flash"], attrs,
"wmode": ["transparent", "window", "opaque"], {
"play": ["true", "false"], "type": ["application/x-shockwave-flash"],
"loop": ["true", "false"], "wmode": ["transparent", "window", "opaque"],
"menu": ["true", "false"], "play": ["true", "false"],
"allowfullscreen": ["true", "false"] "loop": ["true", "false"],
}) "menu": ["true", "false"],
"allowfullscreen": ["true", "false"],
},
)
attrs["allowscriptaccess"] = "never" attrs["allowscriptaccess"] = "never"
attrs["allownetworking"] = "none" attrs["allownetworking"] = "none"
return attrs return attrs
@@ -179,22 +215,19 @@ class XSSHtml(HTMLParser):
attrs = self._get_style(attrs) attrs = self._get_style(attrs)
return attrs return attrs
def _set_attr_default(self, attrs, name, default=''): def _set_attr_default(self, attrs, name, default=""):
if name not in attrs: if name not in attrs:
attrs[name] = default attrs[name] = default
return attrs return attrs
def _limit_attr(self, attrs, limit={}): def _limit_attr(self, attrs, limit={}):
for (key, value) in limit.items(): for key, value in limit.items():
if key in attrs and attrs[key] not in value: if key in attrs and attrs[key] not in value:
del attrs[key] del attrs[key]
return attrs return attrs
def _htmlspecialchars(self, html): def _htmlspecialchars(self, html):
return html.replace("<", "&lt;") \ return html.replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;").replace("'", "&#039;")
.replace(">", "&gt;") \
.replace('"', "&quot;") \
.replace("'", "&#039;")
if "__main__" == __name__: if "__main__" == __name__: