style: ruff format 全仓库
行宽 180 下把历史遗留的折行表达式合并,无语义改动。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -151,7 +151,9 @@ def check_contest_permission(check_type="details"):
|
||||
if error:
|
||||
return error
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
return _wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
|
||||
@@ -17,14 +17,10 @@ class Command(BaseCommand):
|
||||
dry_run = options["dry_run"]
|
||||
|
||||
# 所有现存非比赛题目的 PK 集合
|
||||
existing_ids = set(
|
||||
Problem.objects.filter(contest__isnull=True).values_list("id", flat=True)
|
||||
)
|
||||
existing_ids = set(Problem.objects.filter(contest__isnull=True).values_list("id", flat=True))
|
||||
self.stdout.write(f"现存题库题目数: {len(existing_ids)}")
|
||||
|
||||
profiles = UserProfile.objects.select_related("user").exclude(
|
||||
acm_problems_status={}
|
||||
)
|
||||
profiles = UserProfile.objects.select_related("user").exclude(acm_problems_status={})
|
||||
total = profiles.count()
|
||||
self.stdout.write(f"检查用户数: {total}{'(dry-run 模式)' if dry_run else ''}")
|
||||
|
||||
@@ -38,17 +34,11 @@ class Command(BaseCommand):
|
||||
if not stale_keys:
|
||||
continue
|
||||
|
||||
removed_accepted = sum(
|
||||
1
|
||||
for k in stale_keys
|
||||
if problems[k].get("status") in ACCEPTED_STATUSES
|
||||
)
|
||||
removed_accepted = sum(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]
|
||||
self.stdout.write(
|
||||
f" 用户 {profile.user.username}"
|
||||
f" | 删除 {len(stale_keys)} 题: {', '.join(stale_display)}"
|
||||
f"{f' | 其中已AC {removed_accepted} 题' if removed_accepted else ''}"
|
||||
f" 用户 {profile.user.username} | 删除 {len(stale_keys)} 题: {', '.join(stale_display)}{f' | 其中已AC {removed_accepted} 题' if removed_accepted else ''}"
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
|
||||
@@ -13,11 +13,6 @@ def send_email_async(from_name, to_email, to_name, subject, content):
|
||||
if not SysOptions.smtp_config:
|
||||
return
|
||||
try:
|
||||
send_email(smtp_config=SysOptions.smtp_config,
|
||||
from_name=from_name,
|
||||
to_email=to_email,
|
||||
to_name=to_name,
|
||||
subject=subject,
|
||||
content=content)
|
||||
send_email(smtp_config=SysOptions.smtp_config, from_name=from_name, to_email=to_email, to_name=to_name, subject=subject, content=content)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
@@ -61,12 +61,7 @@ class UserAdminAPI(APIView):
|
||||
try:
|
||||
with transaction.atomic():
|
||||
ret = User.objects.bulk_create(user_list)
|
||||
UserProfile.objects.bulk_create(
|
||||
[
|
||||
UserProfile(user=ret[i], real_name=data[i][3])
|
||||
for i in range(len(ret))
|
||||
]
|
||||
)
|
||||
UserProfile.objects.bulk_create([UserProfile(user=ret[i], real_name=data[i][3]) for i in range(len(ret))])
|
||||
return self.success()
|
||||
except IntegrityError as e:
|
||||
# Extract detail from exception message
|
||||
@@ -85,17 +80,9 @@ class UserAdminAPI(APIView):
|
||||
user = User.objects.get(id=data["id"])
|
||||
except User.DoesNotExist:
|
||||
return self.error("User does not exist")
|
||||
if (
|
||||
User.objects.filter(username=data["username"].lower())
|
||||
.exclude(id=user.id)
|
||||
.exists()
|
||||
):
|
||||
if User.objects.filter(username=data["username"].lower()).exclude(id=user.id).exists():
|
||||
return self.error("Username already exists")
|
||||
if (
|
||||
User.objects.filter(email=data["email"].lower())
|
||||
.exclude(id=user.id)
|
||||
.exists()
|
||||
):
|
||||
if User.objects.filter(email=data["email"].lower()).exclude(id=user.id).exists():
|
||||
return self.error("Email already exists")
|
||||
|
||||
pre_username = user.username
|
||||
@@ -136,9 +123,7 @@ class UserAdminAPI(APIView):
|
||||
|
||||
user.save()
|
||||
if pre_username != user.username:
|
||||
Submission.objects.filter(username=pre_username).update(
|
||||
username=user.username
|
||||
)
|
||||
Submission.objects.filter(username=pre_username).update(username=user.username)
|
||||
|
||||
UserProfile.objects.filter(user=user).update(real_name=data["real_name"])
|
||||
return self.success(UserAdminSerializer(user).data)
|
||||
@@ -158,7 +143,7 @@ class UserAdminAPI(APIView):
|
||||
|
||||
# 获取排序参数
|
||||
order_by = request.GET.get("order_by", "")
|
||||
|
||||
|
||||
# 根据排序参数设置排序规则
|
||||
if order_by == "-last_login":
|
||||
# 最近登录,将 None 值放在最后
|
||||
@@ -174,11 +159,7 @@ class UserAdminAPI(APIView):
|
||||
|
||||
keyword = request.GET.get("keyword", None)
|
||||
if keyword:
|
||||
user = user.filter(
|
||||
Q(username__icontains=keyword)
|
||||
| Q(userprofile__real_name__icontains=keyword)
|
||||
| Q(email__icontains=keyword)
|
||||
)
|
||||
user = user.filter(Q(username__icontains=keyword) | Q(userprofile__real_name__icontains=keyword) | Q(email__icontains=keyword))
|
||||
return self.success(self.paginate_data(request, user, UserAdminSerializer))
|
||||
|
||||
@super_admin_required
|
||||
@@ -223,9 +204,7 @@ class GenerateUserAPI(APIView):
|
||||
Generate User
|
||||
"""
|
||||
data = request.data
|
||||
number_max_length = max(
|
||||
len(str(data["number_from"])), len(str(data["number_to"]))
|
||||
)
|
||||
number_max_length = max(len(str(data["number_from"])), len(str(data["number_to"])))
|
||||
if number_max_length + len(data["prefix"]) + len(data["suffix"]) > 32:
|
||||
return self.error("Username should not more than 32 characters")
|
||||
if data["number_from"] > data["number_to"]:
|
||||
@@ -253,9 +232,7 @@ class GenerateUserAPI(APIView):
|
||||
try:
|
||||
with transaction.atomic():
|
||||
ret = User.objects.bulk_create(user_list)
|
||||
UserProfile.objects.bulk_create(
|
||||
[UserProfile(user=user) for user in ret]
|
||||
)
|
||||
UserProfile.objects.bulk_create([UserProfile(user=user) for user in ret])
|
||||
for item in user_list:
|
||||
worksheet.write_string(i, 0, item.username)
|
||||
worksheet.write_string(i, 1, item.raw_password)
|
||||
@@ -277,17 +254,17 @@ class ResetUserPasswordAPI(APIView):
|
||||
"""
|
||||
data = request.data
|
||||
user_id = data["id"]
|
||||
|
||||
|
||||
try:
|
||||
user = User.objects.get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return self.error("User does not exist")
|
||||
|
||||
|
||||
# 生成6位随机数字密码(不包括0)
|
||||
new_password = get_random_string(6, allowed_chars="123456789")
|
||||
|
||||
|
||||
# 设置新密码
|
||||
user.set_password(new_password)
|
||||
user.save()
|
||||
|
||||
return self.success(new_password)
|
||||
|
||||
return self.success(new_password)
|
||||
|
||||
@@ -431,11 +431,16 @@ class UserRankAPI(AsyncAPIView):
|
||||
except ValueError:
|
||||
n = 0
|
||||
|
||||
profiles = UserProfile.objects.filter(
|
||||
user__admin_type__in=[AdminType.REGULAR_USER, AdminType.STUDENT_ADMIN],
|
||||
user__is_disabled=False,
|
||||
user__username__icontains=username,
|
||||
).select_related("user").filter(accepted_number__gte=0).order_by("-accepted_number", "submission_number")
|
||||
profiles = (
|
||||
UserProfile.objects.filter(
|
||||
user__admin_type__in=[AdminType.REGULAR_USER, AdminType.STUDENT_ADMIN],
|
||||
user__is_disabled=False,
|
||||
user__username__icontains=username,
|
||||
)
|
||||
.select_related("user")
|
||||
.filter(accepted_number__gte=0)
|
||||
.order_by("-accepted_number", "submission_number")
|
||||
)
|
||||
if n > 0:
|
||||
profiles = profiles[:n]
|
||||
return self.success(await self.async_paginate_data(request, profiles, RankInfoSerializer))
|
||||
@@ -457,12 +462,7 @@ class UserActivityRankAPI(AsyncAPIView):
|
||||
create_time__gte=start,
|
||||
result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED],
|
||||
).exclude(username__in=hidden_names)
|
||||
data = [
|
||||
row
|
||||
async for row in submissions.values("username")
|
||||
.annotate(count=Count("problem_id", distinct=True))
|
||||
.order_by("-count")[:10]
|
||||
]
|
||||
data = [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)
|
||||
return self.success(data)
|
||||
|
||||
|
||||
147
ai/views/oj.py
147
ai/views/oj.py
@@ -114,16 +114,12 @@ def get_class_user_ids(user):
|
||||
cache_key = get_cache_key("class_users", user.class_name)
|
||||
user_ids = cache.get(cache_key)
|
||||
if user_ids is None:
|
||||
user_ids = list(
|
||||
User.objects.filter(class_name=user.class_name).values_list("id", flat=True)
|
||||
)
|
||||
user_ids = list(User.objects.filter(class_name=user.class_name).values_list("id", flat=True))
|
||||
cache.set(cache_key, user_ids, CACHE_TIMEOUT)
|
||||
return user_ids
|
||||
|
||||
|
||||
def get_user_first_ac_submissions(
|
||||
user_id, start, end, class_user_ids=None, use_class_scope=False, include_all_time=True
|
||||
):
|
||||
def get_user_first_ac_submissions(user_id, start, end, class_user_ids=None, use_class_scope=False, include_all_time=True):
|
||||
# 用户自己的 AC 记录按时间范围过滤
|
||||
user_first_ac = list(
|
||||
Submission.objects.filter(
|
||||
@@ -151,9 +147,7 @@ def get_user_first_ac_submissions(
|
||||
if use_class_scope and class_user_ids:
|
||||
rank_qs = rank_qs.filter(user_id__in=class_user_ids)
|
||||
|
||||
ranked_first_ac = list(
|
||||
rank_qs.values("user_id", "problem_id").annotate(first_ac_time=Min("create_time"))
|
||||
)
|
||||
ranked_first_ac = list(rank_qs.values("user_id", "problem_id").annotate(first_ac_time=Min("create_time")))
|
||||
|
||||
by_problem = defaultdict(list)
|
||||
for item in ranked_first_ac:
|
||||
@@ -241,18 +235,14 @@ class AIDetailDataAPI(APIView):
|
||||
except User.DoesNotExist:
|
||||
return self.error("User not found")
|
||||
|
||||
cache_key = get_cache_key(
|
||||
"ai_detail", user.id, user.class_name or "", start, end
|
||||
)
|
||||
cache_key = get_cache_key("ai_detail", user.id, user.class_name or "", start, end)
|
||||
cached_result = cache.get(cache_key)
|
||||
if cached_result:
|
||||
return self.success(cached_result)
|
||||
|
||||
class_user_ids = get_class_user_ids(user)
|
||||
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.id, start, end, class_user_ids, use_class_scope
|
||||
)
|
||||
user_first_ac, by_problem, problem_ids = get_user_first_ac_submissions(user.id, start, end, class_user_ids, use_class_scope)
|
||||
|
||||
# 同期排名:只统计时间窗口内解题的人
|
||||
by_problem_period = defaultdict(list)
|
||||
@@ -265,9 +255,7 @@ class AIDetailDataAPI(APIView):
|
||||
)
|
||||
if use_class_scope and 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(
|
||||
first_ac_time=Min("create_time")
|
||||
):
|
||||
for item in period_qs.values("user_id", "problem_id").annotate(first_ac_time=Min("create_time")):
|
||||
by_problem_period[item["problem_id"]].append(item)
|
||||
for lst in by_problem_period.values():
|
||||
lst.sort(key=lambda x: (x["first_ac_time"], x["user_id"]))
|
||||
@@ -286,15 +274,8 @@ class AIDetailDataAPI(APIView):
|
||||
}
|
||||
|
||||
if user_first_ac:
|
||||
problems = {
|
||||
p.id: p
|
||||
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
|
||||
)
|
||||
problems = {p.id: p 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
|
||||
flowcharts_query = FlowchartSubmission.objects.filter(
|
||||
user_id=user,
|
||||
@@ -336,9 +317,7 @@ class AIDetailDataAPI(APIView):
|
||||
|
||||
# 找到最高分和对应的等级
|
||||
best_score = max(scores) if scores else 0
|
||||
best_submission = next(
|
||||
(s for s in submissions if s.ai_score == best_score), submissions[0]
|
||||
)
|
||||
best_submission = next((s for s in submissions if s.ai_score == best_score), submissions[0])
|
||||
best_grade = best_submission.ai_grade or ""
|
||||
|
||||
# 计算平均分
|
||||
@@ -360,9 +339,7 @@ class AIDetailDataAPI(APIView):
|
||||
flowcharts_data.append(merged_item)
|
||||
|
||||
# 按最新提交时间排序
|
||||
flowcharts_data.sort(
|
||||
key=lambda x: x["latest_submission_time"] or "", reverse=True
|
||||
)
|
||||
flowcharts_data.sort(key=lambda x: x["latest_submission_time"] or "", reverse=True)
|
||||
|
||||
result.update(
|
||||
{
|
||||
@@ -370,9 +347,7 @@ class AIDetailDataAPI(APIView):
|
||||
"flowcharts": flowcharts_data,
|
||||
"grade": calculate_average_grade([s["grade"] for s in solved]),
|
||||
"tags": self._calculate_top_tags(problems.values()),
|
||||
"difficulty": self._calculate_difficulty_distribution(
|
||||
problems.values()
|
||||
),
|
||||
"difficulty": self._calculate_difficulty_distribution(problems.values()),
|
||||
"contest_count": len(set(contest_ids)),
|
||||
}
|
||||
)
|
||||
@@ -428,13 +403,8 @@ class AIDetailDataAPI(APIView):
|
||||
def _calculate_difficulty_distribution(self, problems):
|
||||
diff_counter = {"Low": 0, "Mid": 0, "High": 0}
|
||||
for problem in problems:
|
||||
diff_counter[
|
||||
problem.difficulty if problem.difficulty in diff_counter else "Mid"
|
||||
] += 1
|
||||
return {
|
||||
get_difficulty(k): v
|
||||
for k, v in sorted(diff_counter.items(), key=lambda x: x[1], reverse=True)
|
||||
}
|
||||
diff_counter[problem.difficulty if problem.difficulty in diff_counter else "Mid"] += 1
|
||||
return {get_difficulty(k): v for k, v in sorted(diff_counter.items(), key=lambda x: x[1], reverse=True)}
|
||||
|
||||
|
||||
class AIDurationDataAPI(APIView):
|
||||
@@ -451,9 +421,7 @@ class AIDurationDataAPI(APIView):
|
||||
except User.DoesNotExist:
|
||||
return self.error("User not found")
|
||||
|
||||
cache_key = get_cache_key(
|
||||
"ai_duration", user.id, user.class_name or "", end_iso, duration
|
||||
)
|
||||
cache_key = get_cache_key("ai_duration", user.id, user.class_name or "", end_iso, duration)
|
||||
cached_result = cache.get(cache_key)
|
||||
if cached_result:
|
||||
return self.success(cached_result)
|
||||
@@ -468,9 +436,7 @@ class AIDurationDataAPI(APIView):
|
||||
start = start + time_config["delta"]
|
||||
period_end = start + time_config["delta"]
|
||||
|
||||
submission_count = Submission.objects.filter(
|
||||
user_id=user.id, create_time__gte=start, create_time__lte=period_end
|
||||
).count()
|
||||
submission_count = Submission.objects.filter(user_id=user.id, create_time__gte=start, create_time__lte=period_end).count()
|
||||
|
||||
period_data = {
|
||||
"unit": time_config["show_unit"],
|
||||
@@ -502,9 +468,7 @@ class AIDurationDataAPI(APIView):
|
||||
)
|
||||
if use_class_scope and 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(
|
||||
first_ac_time=Min("create_time")
|
||||
):
|
||||
for row in period_qs.values("user_id", "problem_id").annotate(first_ac_time=Min("create_time")):
|
||||
by_problem_period[row["problem_id"]].append(row)
|
||||
for lst in by_problem_period.values():
|
||||
lst.sort(key=lambda x: (x["first_ac_time"], x["user_id"]))
|
||||
@@ -558,7 +522,6 @@ class AIDurationDataAPI(APIView):
|
||||
)
|
||||
|
||||
|
||||
|
||||
class AILoginSummaryAPI(APIView):
|
||||
@login_required
|
||||
def get(self, request):
|
||||
@@ -574,20 +537,11 @@ class AILoginSummaryAPI(APIView):
|
||||
)
|
||||
new_problem_count = problems_qs.count()
|
||||
|
||||
submissions_qs = Submission.objects.filter(
|
||||
user_id=user.id, create_time__gte=start_time, create_time__lte=end_time
|
||||
)
|
||||
submissions_qs = Submission.objects.filter(user_id=user.id, create_time__gte=start_time, create_time__lte=end_time)
|
||||
submission_count = submissions_qs.count()
|
||||
accepted_count = submissions_qs.filter(result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED]).count()
|
||||
solved_count = (
|
||||
submissions_qs.filter(result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED])
|
||||
.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()
|
||||
solved_count = submissions_qs.filter(result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED]).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 = {
|
||||
"start": datetime2str(start_time),
|
||||
@@ -614,9 +568,7 @@ class AILoginSummaryAPI(APIView):
|
||||
start_time = parse_datetime(start_raw) if start_raw else None
|
||||
|
||||
if start_time and timezone.is_naive(start_time):
|
||||
start_time = timezone.make_aware(
|
||||
start_time, timezone.get_current_timezone()
|
||||
)
|
||||
start_time = timezone.make_aware(start_time, timezone.get_current_timezone())
|
||||
|
||||
if not start_time:
|
||||
if user.last_login and user.last_login < end_time:
|
||||
@@ -637,11 +589,7 @@ class AILoginSummaryAPI(APIView):
|
||||
except Exception as exc:
|
||||
return "", str(exc)
|
||||
|
||||
system_prompt = (
|
||||
"你是 OnlineJudge 的学习助教。"
|
||||
"请根据统计数据给出简短分析(1-2句),再给出一行结论,"
|
||||
"结论用“结论:”开头。"
|
||||
)
|
||||
system_prompt = "你是 OnlineJudge 的学习助教。请根据统计数据给出简短分析(1-2句),再给出一行结论,结论用“结论:”开头。"
|
||||
user_prompt = (
|
||||
f"时间范围:{summary['start']} 到 {summary['end']}\n"
|
||||
f"新题目数:{summary['new_problem_count']}\n"
|
||||
@@ -669,6 +617,7 @@ class AILoginSummaryAPI(APIView):
|
||||
content = completion.choices[0].message.content or ""
|
||||
return content.strip(), ""
|
||||
|
||||
|
||||
class AIAnalysisAPI(APIView):
|
||||
@login_required
|
||||
def post(self, request):
|
||||
@@ -697,9 +646,7 @@ class AIAnalysisAPI(APIView):
|
||||
analysis=full_text,
|
||||
)
|
||||
|
||||
return make_sse_response(
|
||||
stream_ai_response(client, system_prompt, user_prompt, on_complete)
|
||||
)
|
||||
return make_sse_response(stream_ai_response(client, system_prompt, user_prompt, on_complete))
|
||||
|
||||
|
||||
class ClassPKAnalysisAPI(APIView):
|
||||
@@ -745,24 +692,11 @@ class ClassPKAnalysisAPI(APIView):
|
||||
class_display = fmt_class(c["class_name"])
|
||||
lines.append(f"\n### 第{i + 1}名:{class_display}(综合分 {c['composite_score']:.1f})")
|
||||
lines.append(f"- 人数:{c['user_count']}")
|
||||
lines.append(
|
||||
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},"
|
||||
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}%"
|
||||
)
|
||||
lines.append(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(f"- 前10%均值:{c['top_10_avg']:.2f},中间80%均值:{c['middle_80_avg']:.2f},后10%均值:{c['bottom_10_avg']:.2f}")
|
||||
lines.append(f"- 优秀率:{c['excellent_rate']:.1f}%,及格率:{c['pass_rate']:.1f}%,参与度:{c['active_rate']:.1f}%")
|
||||
|
||||
if c.get("recent_total_ac") is not None:
|
||||
lines.append(
|
||||
@@ -781,19 +715,15 @@ class ClassPKAnalysisAPI(APIView):
|
||||
"",
|
||||
"**2. 参与积极性**:对比参与度和总提交数,谁的班学生更积极主动?",
|
||||
"",
|
||||
'**3. 典型学生水平**:重点用中位数AC数对比(而非平均值),'
|
||||
'分析谁班的"普通学生"更强。若均值明显高于中位数,说明均值被少数强者拉高,需指出。',
|
||||
'**3. 典型学生水平**:重点用中位数AC数对比(而非平均值),分析谁班的"普通学生"更强。若均值明显高于中位数,说明均值被少数强者拉高,需指出。',
|
||||
"",
|
||||
'**4. 班级内部均衡性**:结合标准差、IQR、前10%与后10%差距,'
|
||||
'判断哪个班是"均衡型",哪个班是"两极型"。',
|
||||
'**4. 班级内部均衡性**:结合标准差、IQR、前10%与后10%差距,判断哪个班是"均衡型",哪个班是"两极型"。',
|
||||
"",
|
||||
"**5. 梯队深度对比**:对比各班前10%均值(尖子生天花板)和后10%均值(薄弱学生水平),"
|
||||
"分析各班在培养尖子生和帮扶后进生上的差异。",
|
||||
"**5. 梯队深度对比**:对比各班前10%均值(尖子生天花板)和后10%均值(薄弱学生水平),分析各班在培养尖子生和帮扶后进生上的差异。",
|
||||
"",
|
||||
'**6. 代码提交质量**:对比AC率,是否有班级存在"凑提交次数但不思考"的问题?',
|
||||
"",
|
||||
"**7. 综合结论与建议**:用1句话明确说明胜负;"
|
||||
"对落后班级给出2~3条具体可操作的改进建议;点出领先班级1条值得借鉴的做法。",
|
||||
"**7. 综合结论与建议**:用1句话明确说明胜负;对落后班级给出2~3条具体可操作的改进建议;点出领先班级1条值得借鉴的做法。",
|
||||
"",
|
||||
"分析对象是班级任课教师,语言专业但不过分学术。",
|
||||
]
|
||||
@@ -916,9 +846,7 @@ class AIHintAPI(APIView):
|
||||
f"学生代码:\n```\n{submission.code[:2000]}\n```"
|
||||
)
|
||||
|
||||
return make_sse_response(
|
||||
stream_ai_response(client, system_prompt, user_prompt)
|
||||
)
|
||||
return make_sse_response(stream_ai_response(client, system_prompt, user_prompt))
|
||||
|
||||
|
||||
class AIHeatmapDataAPI(APIView):
|
||||
@@ -941,9 +869,7 @@ class AIHeatmapDataAPI(APIView):
|
||||
|
||||
# 使用单次查询获取所有数据,按日期分组统计
|
||||
submission_counts = (
|
||||
Submission.objects.filter(
|
||||
user_id=user.id, create_time__gte=start, create_time__lte=end
|
||||
)
|
||||
Submission.objects.filter(user_id=user.id, create_time__gte=start, create_time__lte=end)
|
||||
.annotate(date=TruncDate("create_time"))
|
||||
.values("date")
|
||||
.annotate(count=Count("id"))
|
||||
@@ -961,10 +887,7 @@ class AIHeatmapDataAPI(APIView):
|
||||
submission_count = submission_dict.get(day_date, 0)
|
||||
heatmap_data.append(
|
||||
{
|
||||
"timestamp": int(
|
||||
datetime.combine(day_date, datetime.min.time()).timestamp()
|
||||
* 1000
|
||||
),
|
||||
"timestamp": int(datetime.combine(day_date, datetime.min.time()).timestamp() * 1000),
|
||||
"value": submission_count,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -17,7 +17,10 @@ class Announcement(models.Model):
|
||||
|
||||
class Meta:
|
||||
db_table = "announcement"
|
||||
ordering = ("-top", "-create_time",)
|
||||
ordering = (
|
||||
"-top",
|
||||
"-create_time",
|
||||
)
|
||||
indexes = [
|
||||
models.Index(fields=["visible", "-top", "-create_time"], name="announcement_list_idx"),
|
||||
]
|
||||
|
||||
@@ -25,7 +25,7 @@ class AnnouncementListSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Announcement
|
||||
exclude = ['content']
|
||||
exclude = ["content"]
|
||||
|
||||
|
||||
class EditAnnouncementSerializer(serializers.Serializer):
|
||||
|
||||
@@ -52,9 +52,7 @@ class AnnouncementAdminAPI(APIView):
|
||||
announcement = Announcement.objects.all().order_by("-create_time")
|
||||
if request.GET.get("visible") == "true":
|
||||
announcement = announcement.filter(visible=True)
|
||||
return self.success(
|
||||
self.paginate_data(request, announcement, AnnouncementSerializer)
|
||||
)
|
||||
return self.success(self.paginate_data(request, announcement, AnnouncementSerializer))
|
||||
|
||||
@super_admin_required
|
||||
def delete(self, request):
|
||||
|
||||
@@ -8,11 +8,7 @@ class AnnouncementAPI(AsyncAPIView):
|
||||
id = request.GET.get("id")
|
||||
if id:
|
||||
try:
|
||||
announcement = await (
|
||||
Announcement.objects.select_related("created_by")
|
||||
.filter(id=id, visible=True)
|
||||
.afirst()
|
||||
)
|
||||
announcement = await Announcement.objects.select_related("created_by").filter(id=id, visible=True).afirst()
|
||||
if announcement is None:
|
||||
raise Announcement.DoesNotExist
|
||||
return self.success(await self.async_serialize_data(AnnouncementSerializer, announcement))
|
||||
@@ -20,6 +16,4 @@ class AnnouncementAPI(AsyncAPIView):
|
||||
return self.error("Announcement does not exist")
|
||||
|
||||
announcements = Announcement.objects.select_related("created_by").filter(visible=True)
|
||||
return self.success(
|
||||
await self.async_paginate_data(request, announcements, AnnouncementListSerializer)
|
||||
)
|
||||
return self.success(await self.async_paginate_data(request, announcements, AnnouncementListSerializer))
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
|
||||
# Register your models here.
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
|
||||
# 如果需要存储班级PK历史记录,可以在这里定义模型
|
||||
# 目前暂时不需要,因为都是实时计算
|
||||
|
||||
@@ -7,4 +7,3 @@ urlpatterns = [
|
||||
path("user_class_rank", UserClassRankAPI.as_view()),
|
||||
path("class_pk", ClassPKAPI.as_view()),
|
||||
]
|
||||
|
||||
|
||||
@@ -42,9 +42,7 @@ class ClassRankAPI(APIView):
|
||||
profiles = UserProfile.objects.filter(user_id__in=user_ids)
|
||||
|
||||
total_ac = profiles.aggregate(total=Sum("accepted_number"))["total"] or 0
|
||||
total_submission = (
|
||||
profiles.aggregate(total=Sum("submission_number"))["total"] or 0
|
||||
)
|
||||
total_submission = profiles.aggregate(total=Sum("submission_number"))["total"] or 0
|
||||
avg_ac = profiles.aggregate(avg=Avg("accepted_number"))["avg"] or 0
|
||||
|
||||
user_count = users.count()
|
||||
@@ -56,9 +54,7 @@ class ClassRankAPI(APIView):
|
||||
"total_ac": int(total_ac),
|
||||
"total_submission": int(total_submission),
|
||||
"avg_ac": round(avg_ac, 2),
|
||||
"ac_rate": round(total_ac / total_submission * 100, 2)
|
||||
if total_submission > 0
|
||||
else 0,
|
||||
"ac_rate": round(total_ac / total_submission * 100, 2) if total_submission > 0 else 0,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -213,9 +209,7 @@ class ClassPKAPI(APIView):
|
||||
# 获取所有学生的AC数列表(用于统计计算)
|
||||
profiles = UserProfile.objects.filter(user_id__in=user_ids)
|
||||
ac_list = sorted([p.accepted_number for p in profiles], reverse=True)
|
||||
submission_list = sorted(
|
||||
[p.submission_number for p in profiles], reverse=True
|
||||
)
|
||||
submission_list = sorted([p.submission_number for p in profiles], reverse=True)
|
||||
|
||||
user_count = len(ac_list)
|
||||
if user_count == 0:
|
||||
@@ -238,14 +232,8 @@ class ClassPKAPI(APIView):
|
||||
# 前10%和后10%统计
|
||||
top_10_count = max(1, math.ceil(user_count * 0.10))
|
||||
bottom_10_count = max(1, math.ceil(user_count * 0.10))
|
||||
top_10_avg = (
|
||||
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
|
||||
)
|
||||
top_10_avg = 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
|
||||
|
||||
# 中间80%均值(截尾均值,去掉前10%和后10%)
|
||||
if top_10_count + bottom_10_count < user_count:
|
||||
@@ -256,9 +244,7 @@ class ClassPKAPI(APIView):
|
||||
|
||||
# 优秀率(AC数 >= 全局Q3,即超过PK组所有学生的前25%)
|
||||
excellent_count = sum(1 for ac in ac_list if ac >= global_q3)
|
||||
excellent_rate = (
|
||||
(excellent_count / user_count * 100) if user_count > 0 else 0
|
||||
)
|
||||
excellent_rate = (excellent_count / user_count * 100) if user_count > 0 else 0
|
||||
|
||||
# 及格率(AC数 >= 全局Q1,即超过PK组所有学生的后25%)
|
||||
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__lte=end_time,
|
||||
)
|
||||
recent_ac = (
|
||||
submissions.filter(result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED])
|
||||
.values("user_id", "problem_id")
|
||||
.distinct()
|
||||
.count()
|
||||
)
|
||||
recent_ac = submissions.filter(result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED]).values("user_id", "problem_id").distinct().count()
|
||||
recent_submission = submissions.count()
|
||||
|
||||
# 时间段内的用户AC数列表
|
||||
recent_user_ac = {}
|
||||
for user_id in user_ids:
|
||||
user_recent_ac = (
|
||||
submissions.filter(user_id=user_id, result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED])
|
||||
.values("problem_id")
|
||||
.distinct()
|
||||
.count()
|
||||
)
|
||||
user_recent_ac = 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_ac_list = sorted(recent_user_ac.values(), reverse=True)
|
||||
@@ -302,14 +278,8 @@ class ClassPKAPI(APIView):
|
||||
"recent_total_submission": recent_submission,
|
||||
"recent_avg_ac": statistics.mean(recent_ac_list),
|
||||
"recent_median_ac": statistics.median(recent_ac_list),
|
||||
"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_active_count": sum(
|
||||
1 for ac in recent_ac_list if ac > 0
|
||||
),
|
||||
"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_active_count": sum(1 for ac in recent_ac_list if ac > 0),
|
||||
}
|
||||
|
||||
class_comparisons.append(
|
||||
@@ -336,9 +306,7 @@ class ClassPKAPI(APIView):
|
||||
"pass_rate": round(pass_rate, 2),
|
||||
"active_rate": round(active_rate, 2),
|
||||
# 正确率
|
||||
"ac_rate": round(total_ac / total_submission * 100, 2)
|
||||
if total_submission > 0
|
||||
else 0,
|
||||
"ac_rate": round(total_ac / total_submission * 100, 2) if total_submission > 0 else 0,
|
||||
# 时间段统计(如果有)
|
||||
**recent_stats,
|
||||
}
|
||||
@@ -359,9 +327,7 @@ class ClassPKAPI(APIView):
|
||||
c["composite_score"] = round(score, 1)
|
||||
|
||||
# 按综合分排序(主),中位数(次)
|
||||
class_comparisons.sort(
|
||||
key=lambda x: (-x["composite_score"], -x["median_ac"])
|
||||
)
|
||||
class_comparisons.sort(key=lambda x: (-x["composite_score"], -x["median_ac"]))
|
||||
|
||||
return self.success(
|
||||
{
|
||||
|
||||
@@ -43,5 +43,3 @@ class Comment(models.Model):
|
||||
indexes = [
|
||||
models.Index(fields=["problem", "create_time"], name="comment_problem_time_idx"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -28,4 +28,4 @@ class CommentListSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Comment
|
||||
fields = "__all__"
|
||||
fields = "__all__"
|
||||
|
||||
@@ -17,9 +17,7 @@ class CommentAPI(APIView):
|
||||
except Problem.DoesNotExist:
|
||||
return self.error("Problem doesn't exist")
|
||||
comments = comments.filter(problem=problem)
|
||||
return self.success(
|
||||
self.paginate_data(request, comments, CommentListSerializer)
|
||||
)
|
||||
return self.success(self.paginate_data(request, comments, CommentListSerializer))
|
||||
|
||||
@super_admin_required
|
||||
def delete(self, request):
|
||||
|
||||
@@ -54,11 +54,7 @@ class CommentAPI(AsyncAPIView):
|
||||
@login_required
|
||||
async def get(self, request):
|
||||
problem_id = request.GET.get("problem_id")
|
||||
comment = await (
|
||||
Comment.objects.select_related("problem")
|
||||
.filter(user=request.user, problem_id=problem_id)
|
||||
.afirst()
|
||||
)
|
||||
comment = await Comment.objects.select_related("problem").filter(user=request.user, problem_id=problem_id).afirst()
|
||||
if comment:
|
||||
return self.success(await self.async_serialize_data(CommentSerializer, comment))
|
||||
else:
|
||||
@@ -82,10 +78,13 @@ class CommentStatisticsAPI(AsyncAPIView):
|
||||
if not agg["count"]:
|
||||
return self.success()
|
||||
|
||||
data = {"count": agg["count"], "rating": {
|
||||
"description": agg["description"],
|
||||
"difficulty": agg["difficulty"],
|
||||
"comprehensive": agg["comprehensive"],
|
||||
}}
|
||||
data = {
|
||||
"count": agg["count"],
|
||||
"rating": {
|
||||
"description": agg["description"],
|
||||
"difficulty": agg["difficulty"],
|
||||
"comprehensive": agg["comprehensive"],
|
||||
},
|
||||
}
|
||||
await async_cache_set(cache_key, data, 3600)
|
||||
return self.success(data)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
WebSocket consumers for configuration updates
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
@@ -18,31 +19,25 @@ class ConfigConsumer(AsyncWebsocketConsumer):
|
||||
async def connect(self):
|
||||
"""处理 WebSocket 连接"""
|
||||
self.user = self.scope["user"]
|
||||
|
||||
|
||||
# 只允许认证用户连接
|
||||
if not self.user.is_authenticated:
|
||||
await self.close()
|
||||
return
|
||||
|
||||
|
||||
# 使用全局配置组名,所有用户都能接收配置更新
|
||||
self.group_name = "config_updates"
|
||||
|
||||
|
||||
# 加入配置更新组
|
||||
await self.channel_layer.group_add(
|
||||
self.group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
await self.channel_layer.group_add(self.group_name, self.channel_name)
|
||||
|
||||
await self.accept()
|
||||
logger.info(f"Config WebSocket connected: user_id={self.user.id}, channel={self.channel_name}")
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
"""处理 WebSocket 断开连接"""
|
||||
if hasattr(self, 'group_name'):
|
||||
await self.channel_layer.group_discard(
|
||||
self.group_name,
|
||||
self.channel_name
|
||||
)
|
||||
if hasattr(self, "group_name"):
|
||||
await self.channel_layer.group_discard(self.group_name, self.channel_name)
|
||||
logger.info(f"Config WebSocket disconnected: user_id={self.user.id}, close_code={close_code}")
|
||||
|
||||
async def receive(self, text_data):
|
||||
@@ -53,13 +48,10 @@ class ConfigConsumer(AsyncWebsocketConsumer):
|
||||
try:
|
||||
data = json.loads(text_data)
|
||||
message_type = data.get("type")
|
||||
|
||||
|
||||
if message_type == "ping":
|
||||
# 响应心跳包
|
||||
await self.send(text_data=json.dumps({
|
||||
"type": "pong",
|
||||
"timestamp": data.get("timestamp")
|
||||
}))
|
||||
await self.send(text_data=json.dumps({"type": "pong", "timestamp": data.get("timestamp")}))
|
||||
elif message_type == "config_update":
|
||||
# 处理配置更新请求
|
||||
key = data.get("key")
|
||||
@@ -69,17 +61,7 @@ class ConfigConsumer(AsyncWebsocketConsumer):
|
||||
# 这里可以添加权限检查,只有管理员才能发送配置更新
|
||||
if self.user.is_superuser:
|
||||
# 广播配置更新给所有连接的客户端
|
||||
await self.channel_layer.group_send(
|
||||
self.group_name,
|
||||
{
|
||||
"type": "config_update",
|
||||
"data": {
|
||||
"type": "config_update",
|
||||
"key": key,
|
||||
"value": value
|
||||
}
|
||||
}
|
||||
)
|
||||
await self.channel_layer.group_send(self.group_name, {"type": "config_update", "data": {"type": "config_update", "key": key, "value": value}})
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"Invalid JSON received from user {self.user.id}")
|
||||
except Exception as e:
|
||||
|
||||
@@ -44,7 +44,7 @@ class JudgeServerHeartbeatSerializer(serializers.Serializer):
|
||||
cpu_core = serializers.IntegerField(min_value=1)
|
||||
memory = 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)
|
||||
|
||||
|
||||
|
||||
@@ -153,9 +153,7 @@ class JudgeServerAPI(APIView):
|
||||
@super_admin_required
|
||||
def put(self, request):
|
||||
is_disabled = request.data.get("is_disabled", False)
|
||||
JudgeServer.objects.filter(id=request.data["id"]).update(
|
||||
is_disabled=is_disabled
|
||||
)
|
||||
JudgeServer.objects.filter(id=request.data["id"]).update(is_disabled=is_disabled)
|
||||
if not is_disabled:
|
||||
process_pending_task()
|
||||
return self.success()
|
||||
@@ -166,10 +164,7 @@ class JudgeServerHeartbeatAPI(CSRFExemptAPIView):
|
||||
def post(self, request):
|
||||
data = request.data
|
||||
client_token = request.META.get("HTTP_X_JUDGE_SERVER_TOKEN")
|
||||
if (
|
||||
hashlib.sha256(SysOptions.judge_server_token.encode("utf-8")).hexdigest()
|
||||
!= client_token
|
||||
):
|
||||
if hashlib.sha256(SysOptions.judge_server_token.encode("utf-8")).hexdigest() != client_token:
|
||||
return self.error("Invalid token")
|
||||
|
||||
try:
|
||||
@@ -263,8 +258,7 @@ class ReleaseNotesAPI(APIView):
|
||||
def get(self, request):
|
||||
try:
|
||||
resp = requests.get(
|
||||
"https://raw.githubusercontent.com/QingdaoU/OnlineJudge/master/docs/data.json?_="
|
||||
+ str(time.time()),
|
||||
"https://raw.githubusercontent.com/QingdaoU/OnlineJudge/master/docs/data.json?_=" + str(time.time()),
|
||||
timeout=3,
|
||||
)
|
||||
releases = resp.json()
|
||||
@@ -289,9 +283,7 @@ class DashboardInfoAPI(AsyncAPIView):
|
||||
User.objects.acount(),
|
||||
Submission.objects.filter(create_time__gte=today_start).acount(),
|
||||
Contest.objects.exclude(end_time__lt=timezone.now()).acount(),
|
||||
JudgeServer.objects.filter(
|
||||
last_heartbeat__gte=timezone.now() - timedelta(seconds=6)
|
||||
).acount(),
|
||||
JudgeServer.objects.filter(last_heartbeat__gte=timezone.now() - timedelta(seconds=6)).acount(),
|
||||
)
|
||||
return self.success(
|
||||
{
|
||||
@@ -312,20 +304,14 @@ class RandomUsernameAPI(AsyncAPIView):
|
||||
classroom = request.GET.get("classroom", "")
|
||||
if not classroom:
|
||||
return self.error("需要班级号")
|
||||
usernames = [
|
||||
u async for u in User.objects.filter(username__istartswith=classroom)
|
||||
.values_list("username", flat=True)
|
||||
.order_by("?")[:10]
|
||||
]
|
||||
usernames = [u async for u in User.objects.filter(username__istartswith=classroom).values_list("username", flat=True).order_by("?")[:10]]
|
||||
return self.success(usernames)
|
||||
|
||||
|
||||
class HitokotoAPI(AsyncAPIView):
|
||||
async def get(self, request):
|
||||
try:
|
||||
categories = JsonDataLoader.load_data(
|
||||
settings.HITOKOTO_DIR, "categories.json"
|
||||
)
|
||||
categories = JsonDataLoader.load_data(settings.HITOKOTO_DIR, "categories.json")
|
||||
path = random.choice(categories).get("path")
|
||||
sentences = JsonDataLoader.load_data(settings.HITOKOTO_DIR, path)
|
||||
sentence = random.choice(sentences)
|
||||
@@ -341,7 +327,6 @@ class ClassUsernamesAPI(AsyncAPIView):
|
||||
return self.error("需要班级号")
|
||||
prefix = f"ks{classroom}"
|
||||
names = [
|
||||
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")
|
||||
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")
|
||||
]
|
||||
return self.success(names)
|
||||
|
||||
@@ -75,7 +75,6 @@ class ACMContestRank(AbstractContestRank):
|
||||
]
|
||||
|
||||
|
||||
|
||||
class ContestAnnouncement(models.Model):
|
||||
contest = models.ForeignKey(Contest, on_delete=models.CASCADE)
|
||||
title = models.TextField()
|
||||
|
||||
@@ -84,7 +84,6 @@ class ACMContestRankSerializer(serializers.ModelSerializer):
|
||||
return UsernameSerializer(obj.user, need_real_name=self.is_contest_admin).data
|
||||
|
||||
|
||||
|
||||
class ACMContesHelperSerializer(serializers.Serializer):
|
||||
contest_id = serializers.IntegerField()
|
||||
problem_id = serializers.CharField()
|
||||
|
||||
@@ -27,9 +27,7 @@ class ContestAnnouncementListAPI(AsyncAPIView):
|
||||
contest_id = request.GET.get("contest_id")
|
||||
if not contest_id:
|
||||
return self.error("Invalid parameter, contest_id is required")
|
||||
qs = ContestAnnouncement.objects.select_related("created_by").filter(
|
||||
contest_id=contest_id, visible=True
|
||||
)
|
||||
qs = ContestAnnouncement.objects.select_related("created_by").filter(contest_id=contest_id, visible=True)
|
||||
max_id = request.GET.get("max_id")
|
||||
if 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):
|
||||
return self.error("Invalid parameter, id is required")
|
||||
try:
|
||||
contest = await (
|
||||
Contest.objects.select_related("created_by")
|
||||
.filter(id=id, visible=True)
|
||||
.afirst()
|
||||
)
|
||||
contest = await Contest.objects.select_related("created_by").filter(id=id, visible=True).afirst()
|
||||
if contest is None:
|
||||
raise Contest.DoesNotExist
|
||||
except Contest.DoesNotExist:
|
||||
@@ -84,9 +78,7 @@ class ContestPasswordVerifyAPI(AsyncAPIView):
|
||||
async def post(self, request):
|
||||
data = request.data
|
||||
try:
|
||||
contest = await Contest.objects.aget(
|
||||
id=data["contest_id"], visible=True, password__isnull=False
|
||||
)
|
||||
contest = await Contest.objects.aget(id=data["contest_id"], visible=True, password__isnull=False)
|
||||
except Contest.DoesNotExist:
|
||||
return self.error("Contest does not exist")
|
||||
if not check_contest_password(data["password"], contest.password):
|
||||
@@ -106,17 +98,11 @@ class ContestAccessAPI(AsyncAPIView):
|
||||
if not contest_id:
|
||||
return self.error()
|
||||
try:
|
||||
contest = await Contest.objects.aget(
|
||||
id=contest_id, visible=True, password__isnull=False
|
||||
)
|
||||
contest = await Contest.objects.aget(id=contest_id, visible=True, password__isnull=False)
|
||||
except Contest.DoesNotExist:
|
||||
return self.error("Contest does not exist")
|
||||
session_pass = request.session.get(CONTEST_PASSWORD_SESSION_KEY, {}).get(
|
||||
str(contest.id)
|
||||
)
|
||||
return self.success(
|
||||
{"access": check_contest_password(session_pass, contest.password)}
|
||||
)
|
||||
session_pass = request.session.get(CONTEST_PASSWORD_SESSION_KEY, {}).get(str(contest.id))
|
||||
return self.success({"access": check_contest_password(session_pass, contest.password)})
|
||||
|
||||
|
||||
class ContestRankAPI(AsyncAPIView):
|
||||
@@ -155,16 +141,12 @@ class ContestRankAPI(AsyncAPIView):
|
||||
for index, item in enumerate(data):
|
||||
worksheet.write_string(index + 1, 0, str(item["user"]["id"]))
|
||||
worksheet.write_string(index + 1, 1, item["user"]["username"])
|
||||
worksheet.write_string(
|
||||
index + 1, 2, item["user"]["real_name"] or ""
|
||||
)
|
||||
worksheet.write_string(index + 1, 2, item["user"]["real_name"] or "")
|
||||
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, 5, str(item["total_time"]))
|
||||
for k, v in item["submission_info"].items():
|
||||
worksheet.write_string(
|
||||
index + 1, 6 + problem_id_to_col[int(k)], str(v["is_ac"])
|
||||
)
|
||||
worksheet.write_string(index + 1, 6 + problem_id_to_col[int(k)], str(v["is_ac"]))
|
||||
|
||||
workbook.close()
|
||||
f.seek(0)
|
||||
@@ -173,32 +155,20 @@ class ContestRankAPI(AsyncAPIView):
|
||||
@check_contest_permission(check_type="ranks")
|
||||
async def get(self, request):
|
||||
download_csv = request.GET.get("download_csv")
|
||||
is_contest_admin = (
|
||||
request.user.is_authenticated
|
||||
and request.user.is_contest_admin(self.contest)
|
||||
)
|
||||
is_contest_admin = request.user.is_authenticated and request.user.is_contest_admin(self.contest)
|
||||
|
||||
qs = self.get_rank()
|
||||
|
||||
if download_csv:
|
||||
rank_list = [item async for item in qs]
|
||||
data = await self.async_serialize_data(
|
||||
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"))
|
||||
)()
|
||||
data = await self.async_serialize_data(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")))()
|
||||
xlsx_bytes = await sync_to_async(self._build_xlsx)(data, contest_problems)
|
||||
response = HttpResponse(xlsx_bytes)
|
||||
response["Content-Disposition"] = (
|
||||
f"attachment; filename=content-{self.contest.id}-rank.xlsx"
|
||||
)
|
||||
response["Content-Disposition"] = f"attachment; filename=content-{self.contest.id}-rank.xlsx"
|
||||
response["Content-Type"] = "application/xlsx"
|
||||
return response
|
||||
|
||||
page_qs = await self.async_paginate_data(request, qs)
|
||||
page_qs["results"] = await self.async_serialize_data(
|
||||
ACMContestRankSerializer,
|
||||
page_qs["results"], many=True, is_contest_admin=is_contest_admin
|
||||
)
|
||||
page_qs["results"] = await self.async_serialize_data(ACMContestRankSerializer, page_qs["results"], many=True, is_contest_admin=is_contest_admin)
|
||||
return self.success(page_qs)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
WebSocket consumers for flowchart evaluation updates
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
@@ -18,31 +19,25 @@ class FlowchartConsumer(AsyncWebsocketConsumer):
|
||||
async def connect(self):
|
||||
"""处理 WebSocket 连接"""
|
||||
self.user = self.scope["user"]
|
||||
|
||||
|
||||
# 只允许认证用户连接
|
||||
if not self.user.is_authenticated:
|
||||
await self.close()
|
||||
return
|
||||
|
||||
|
||||
# 使用用户 ID 作为组名,这样可以向特定用户推送消息
|
||||
self.group_name = f"flowchart_user_{self.user.id}"
|
||||
|
||||
|
||||
# 加入用户专属的组
|
||||
await self.channel_layer.group_add(
|
||||
self.group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
await self.channel_layer.group_add(self.group_name, self.channel_name)
|
||||
|
||||
await self.accept()
|
||||
logger.info(f"Flowchart WebSocket connected: user_id={self.user.id}, channel={self.channel_name}")
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
"""处理 WebSocket 断开连接"""
|
||||
if hasattr(self, 'group_name'):
|
||||
await self.channel_layer.group_discard(
|
||||
self.group_name,
|
||||
self.channel_name
|
||||
)
|
||||
if hasattr(self, "group_name"):
|
||||
await self.channel_layer.group_discard(self.group_name, self.channel_name)
|
||||
logger.info(f"Flowchart WebSocket disconnected: user_id={self.user.id}, close_code={close_code}")
|
||||
|
||||
async def receive(self, text_data):
|
||||
@@ -53,13 +48,10 @@ class FlowchartConsumer(AsyncWebsocketConsumer):
|
||||
try:
|
||||
data = json.loads(text_data)
|
||||
message_type = data.get("type")
|
||||
|
||||
|
||||
if message_type == "ping":
|
||||
# 响应心跳包
|
||||
await self.send(text_data=json.dumps({
|
||||
"type": "pong",
|
||||
"timestamp": data.get("timestamp")
|
||||
}))
|
||||
await self.send(text_data=json.dumps({"type": "pong", "timestamp": data.get("timestamp")}))
|
||||
elif message_type == "subscribe":
|
||||
# 订阅特定流程图提交的更新
|
||||
submission_id = data.get("submission_id")
|
||||
|
||||
@@ -6,61 +6,59 @@ from utils.shortcuts import rand_str
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class FlowchartSubmissionStatus:
|
||||
PENDING = 0 # 等待AI评分
|
||||
PROCESSING = 1 # AI评分中
|
||||
COMPLETED = 2 # 评分完成
|
||||
FAILED = 3 # 评分失败
|
||||
PENDING = 0 # 等待AI评分
|
||||
PROCESSING = 1 # AI评分中
|
||||
COMPLETED = 2 # 评分完成
|
||||
FAILED = 3 # 评分失败
|
||||
|
||||
|
||||
class FlowchartSubmission(models.Model):
|
||||
"""流程图提交模型"""
|
||||
|
||||
id = models.TextField(default=rand_str, primary_key=True, db_index=True)
|
||||
|
||||
|
||||
# 基础信息
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='flowchart_submissions')
|
||||
problem = models.ForeignKey(Problem, 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")
|
||||
|
||||
# 提交内容
|
||||
mermaid_code = models.TextField() # Mermaid代码
|
||||
flowchart_data = models.JSONField(default=dict) # 流程图元数据
|
||||
|
||||
|
||||
# 状态信息
|
||||
status = models.IntegerField(default=FlowchartSubmissionStatus.PENDING)
|
||||
create_time = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
# AI评分结果
|
||||
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_feedback = models.TextField(null=True, blank=True) # AI反馈
|
||||
ai_suggestions = models.TextField(null=True, blank=True) # AI建议
|
||||
ai_criteria_details = models.JSONField(default=dict) # 详细评分标准
|
||||
|
||||
|
||||
# 处理信息
|
||||
ai_provider = models.CharField(max_length=50, default='deepseek')
|
||||
ai_model = models.CharField(max_length=50, default='deepseek-v4-flash')
|
||||
ai_provider = models.CharField(max_length=50, default="deepseek")
|
||||
ai_model = models.CharField(max_length=50, default="deepseek-v4-flash")
|
||||
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:
|
||||
db_table = 'flowchart_submission'
|
||||
ordering = ['-create_time']
|
||||
db_table = "flowchart_submission"
|
||||
ordering = ["-create_time"]
|
||||
indexes = [
|
||||
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=['status'], name='flowchart_status_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=["status"], name="flowchart_status_idx"),
|
||||
]
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"FlowchartSubmission {self.id}"
|
||||
|
||||
|
||||
def check_user_permission(self, user, check_share=True):
|
||||
"""检查用户权限"""
|
||||
if (
|
||||
self.user_id == user.id
|
||||
or not user.is_regular_user()
|
||||
or self.problem.created_by_id == user.id
|
||||
):
|
||||
if self.user_id == user.id or not user.is_regular_user() or self.problem.created_by_id == user.id:
|
||||
return True
|
||||
|
||||
|
||||
return False
|
||||
|
||||
@@ -18,6 +18,7 @@ class CreateFlowchartSubmissionSerializer(serializers.Serializer):
|
||||
|
||||
def validate_flowchart_data(self, value):
|
||||
import json
|
||||
|
||||
if len(json.dumps(value)) > 500 * 1024:
|
||||
raise serializers.ValidationError("流程图数据过大")
|
||||
return value
|
||||
@@ -55,6 +56,7 @@ class FlowchartSubmissionListSerializer(serializers.ModelSerializer):
|
||||
username = serializers.CharField(source="user.username")
|
||||
problem = serializers.CharField(source="problem._id")
|
||||
problem_title = serializers.CharField(source="problem.title")
|
||||
|
||||
class Meta:
|
||||
model = FlowchartSubmission
|
||||
fields = [
|
||||
|
||||
@@ -20,16 +20,16 @@ def evaluate_flowchart_task(submission_id):
|
||||
submission = None
|
||||
try:
|
||||
submission = FlowchartSubmission.objects.get(id=submission_id)
|
||||
|
||||
|
||||
# 更新状态为处理中
|
||||
submission.status = FlowchartSubmissionStatus.PROCESSING
|
||||
submission.save()
|
||||
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
|
||||
# 使用固定评分标准
|
||||
system_prompt = build_evaluation_prompt(submission.problem)
|
||||
|
||||
|
||||
# 构建用户提示词,包含标准答案对比
|
||||
user_prompt = f"""
|
||||
请对以下Mermaid流程图进行评分:
|
||||
@@ -53,16 +53,13 @@ def evaluate_flowchart_task(submission_id):
|
||||
user_prompt += f"\n设计提示:{submission.problem.flowchart_hint}\n"
|
||||
|
||||
user_prompt += "\n请按照评分标准进行详细评估,并给出0-100的分数。\n"
|
||||
|
||||
|
||||
# 调用AI进行评分
|
||||
client = get_ai_client()
|
||||
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="deepseek-v4-flash",
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_prompt}
|
||||
],
|
||||
messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}],
|
||||
temperature=0,
|
||||
extra_body={"thinking": {"type": "disabled"}},
|
||||
)
|
||||
@@ -74,30 +71,31 @@ def evaluate_flowchart_task(submission_id):
|
||||
|
||||
# 保存评分结果
|
||||
with transaction.atomic():
|
||||
submission.ai_score = score_data['score']
|
||||
submission.ai_grade = score_data['grade']
|
||||
submission.ai_feedback = score_data['feedback']
|
||||
submission.ai_suggestions = score_data.get('suggestions', '')
|
||||
submission.ai_criteria_details = score_data.get('criteria_details', {})
|
||||
submission.ai_provider = 'deepseek'
|
||||
submission.ai_model = 'deepseek-v4-flash'
|
||||
submission.ai_score = score_data["score"]
|
||||
submission.ai_grade = score_data["grade"]
|
||||
submission.ai_feedback = score_data["feedback"]
|
||||
submission.ai_suggestions = score_data.get("suggestions", "")
|
||||
submission.ai_criteria_details = score_data.get("criteria_details", {})
|
||||
submission.ai_provider = "deepseek"
|
||||
submission.ai_model = "deepseek-v4-flash"
|
||||
submission.processing_time = processing_time
|
||||
submission.status = FlowchartSubmissionStatus.COMPLETED
|
||||
submission.evaluation_time = timezone.now()
|
||||
submission.save()
|
||||
|
||||
|
||||
# 推送评分完成通知
|
||||
from utils.websocket import push_flowchart_evaluation_update
|
||||
|
||||
push_flowchart_evaluation_update(
|
||||
submission_id=str(submission.id),
|
||||
user_id=submission.user_id,
|
||||
data={
|
||||
"type": "flowchart_evaluation_completed",
|
||||
"score": score_data['score'],
|
||||
"grade": score_data['grade'],
|
||||
}
|
||||
"score": score_data["score"],
|
||||
"grade": score_data["grade"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("evaluate_flowchart_task failed for submission %s", submission_id)
|
||||
if submission is not None:
|
||||
@@ -105,6 +103,7 @@ def evaluate_flowchart_task(submission_id):
|
||||
submission.save()
|
||||
|
||||
from utils.websocket import push_flowchart_evaluation_update
|
||||
|
||||
push_flowchart_evaluation_update(
|
||||
submission_id=str(submission.id),
|
||||
user_id=submission.user_id,
|
||||
@@ -116,9 +115,10 @@ def evaluate_flowchart_task(submission_id):
|
||||
)
|
||||
raise e
|
||||
|
||||
|
||||
def build_evaluation_prompt(problem):
|
||||
"""构建AI评分提示词 - 使用固定标准"""
|
||||
|
||||
|
||||
# 使用固定的评分标准
|
||||
criteria_text = """
|
||||
- 逻辑正确性 (权重: 1.0, 最高分: 40): 检查流程图的逻辑是否正确,包括条件判断、循环结构等
|
||||
@@ -126,7 +126,7 @@ def build_evaluation_prompt(problem):
|
||||
- 规范性 (权重: 0.6, 最高分: 20): 检查流程图符号使用是否规范,是否符合标准;不要评价节点ID
|
||||
- 清晰度 (权重: 0.4, 最高分: 10): 评估流程图的整体布局和连线情况;不要因节点ID扣分
|
||||
"""
|
||||
|
||||
|
||||
return f"""
|
||||
你是一个专业的编程教学助手,负责评估学生提交的Mermaid流程图。
|
||||
|
||||
@@ -166,15 +166,17 @@ def build_evaluation_prompt(problem):
|
||||
}}
|
||||
"""
|
||||
|
||||
|
||||
def parse_ai_evaluation_response(ai_response):
|
||||
"""解析AI评分响应,解析失败时抛出异常由调用方处理"""
|
||||
import re
|
||||
|
||||
# 优先匹配代码块中的 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:
|
||||
json_str = code_block.group(1)
|
||||
else:
|
||||
json_match = re.search(r'\{.*\}', ai_response, re.DOTALL)
|
||||
json_match = re.search(r"\{.*\}", ai_response, re.DOTALL)
|
||||
if not json_match:
|
||||
raise ValueError("AI响应中未找到JSON数据")
|
||||
json_str = json_match.group()
|
||||
|
||||
@@ -9,9 +9,9 @@ from ..views.oj import (
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path('flowchart/submission', FlowchartSubmissionAPI.as_view()),
|
||||
path('flowchart/submissions', FlowchartSubmissionListAPI.as_view()),
|
||||
path('flowchart/submission/retry', FlowchartSubmissionRetryAPI.as_view()),
|
||||
path('flowchart/submission/detail', FlowchartSubmissionDetailAPI.as_view()),
|
||||
path('flowchart/submission/current', FlowchartSubmissionCurrentAPI.as_view()),
|
||||
path("flowchart/submission", FlowchartSubmissionAPI.as_view()),
|
||||
path("flowchart/submissions", FlowchartSubmissionListAPI.as_view()),
|
||||
path("flowchart/submission/retry", FlowchartSubmissionRetryAPI.as_view()),
|
||||
path("flowchart/submission/detail", FlowchartSubmissionDetailAPI.as_view()),
|
||||
path("flowchart/submission/current", FlowchartSubmissionCurrentAPI.as_view()),
|
||||
]
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
|
||||
# Create your views here.
|
||||
|
||||
@@ -20,16 +20,44 @@ STOPWORDS = frozenset(
|
||||
)
|
||||
|
||||
CUSTOM_WORDS = [
|
||||
"循环结构", "条件判断", "判断条件", "结束条件", "循环条件",
|
||||
"异常处理", "边界条件", "输入输出", "输入验证",
|
||||
"开始结束", "结束节点", "开始节点", "判断节点",
|
||||
"流程走向", "逻辑错误", "逻辑缺陷", "逻辑不清",
|
||||
"缺少分支", "缺少步骤", "缺少判断", "缺少循环",
|
||||
"死循环", "无限循环", "循环出口", "循环体",
|
||||
"条件分支", "分支结构", "分支不全", "分支缺失",
|
||||
"符号使用", "符号不规范", "连线混乱",
|
||||
"变量初始化", "赋值操作", "累加操作",
|
||||
"终止条件", "退出条件", "返回值",
|
||||
"循环结构",
|
||||
"条件判断",
|
||||
"判断条件",
|
||||
"结束条件",
|
||||
"循环条件",
|
||||
"异常处理",
|
||||
"边界条件",
|
||||
"输入输出",
|
||||
"输入验证",
|
||||
"开始结束",
|
||||
"结束节点",
|
||||
"开始节点",
|
||||
"判断节点",
|
||||
"流程走向",
|
||||
"逻辑错误",
|
||||
"逻辑缺陷",
|
||||
"逻辑不清",
|
||||
"缺少分支",
|
||||
"缺少步骤",
|
||||
"缺少判断",
|
||||
"缺少循环",
|
||||
"死循环",
|
||||
"无限循环",
|
||||
"循环出口",
|
||||
"循环体",
|
||||
"条件分支",
|
||||
"分支结构",
|
||||
"分支不全",
|
||||
"分支缺失",
|
||||
"符号使用",
|
||||
"符号不规范",
|
||||
"连线混乱",
|
||||
"变量初始化",
|
||||
"赋值操作",
|
||||
"累加操作",
|
||||
"终止条件",
|
||||
"退出条件",
|
||||
"返回值",
|
||||
]
|
||||
|
||||
for _w in CUSTOM_WORDS:
|
||||
@@ -38,7 +66,7 @@ for _w in CUSTOM_WORDS:
|
||||
|
||||
def get_real_name(username, class_name):
|
||||
if class_name and username.startswith("ks"):
|
||||
return username[len(f"ks{class_name}"):]
|
||||
return username[len(f"ks{class_name}") :]
|
||||
return username
|
||||
|
||||
|
||||
@@ -63,9 +91,7 @@ class FlowchartStatisticsAPI(APIView):
|
||||
problem_id = request.GET.get("problem_id")
|
||||
if problem_id:
|
||||
try:
|
||||
problem = Problem.objects.get(
|
||||
_id__iexact=problem_id, contest_id__isnull=True, visible=True
|
||||
)
|
||||
problem = Problem.objects.get(_id__iexact=problem_id, contest_id__isnull=True, visible=True)
|
||||
except Problem.DoesNotExist:
|
||||
return self.error("Problem doesn't exist")
|
||||
submissions = submissions.filter(problem=problem)
|
||||
@@ -85,23 +111,21 @@ class FlowchartStatisticsAPI(APIView):
|
||||
|
||||
total_count = submissions.count()
|
||||
if total_count == 0:
|
||||
return self.success({
|
||||
"total_count": 0,
|
||||
"avg_score": 0,
|
||||
"grade_distribution": {},
|
||||
"criteria_averages": {},
|
||||
"person_count": len(all_users_dict),
|
||||
"completed_count": 0,
|
||||
"word_frequencies": [],
|
||||
"data_unaccepted": [],
|
||||
})
|
||||
return self.success(
|
||||
{
|
||||
"total_count": 0,
|
||||
"avg_score": 0,
|
||||
"grade_distribution": {},
|
||||
"criteria_averages": {},
|
||||
"person_count": len(all_users_dict),
|
||||
"completed_count": 0,
|
||||
"word_frequencies": [],
|
||||
"data_unaccepted": [],
|
||||
}
|
||||
)
|
||||
|
||||
# 1. Grade distribution
|
||||
grade_counts = dict(
|
||||
submissions.values_list("ai_grade")
|
||||
.annotate(count=Count("id"))
|
||||
.values_list("ai_grade", "count")
|
||||
)
|
||||
grade_counts = dict(submissions.values_list("ai_grade").annotate(count=Count("id")).values_list("ai_grade", "count"))
|
||||
|
||||
# 2. Average score
|
||||
avg_score = submissions.aggregate(avg=Avg("ai_score"))["avg"] or 0
|
||||
@@ -113,9 +137,7 @@ class FlowchartStatisticsAPI(APIView):
|
||||
|
||||
wordcloud_texts = []
|
||||
|
||||
for row in submissions.values_list(
|
||||
"ai_criteria_details", "ai_feedback", "ai_suggestions"
|
||||
).iterator():
|
||||
for row in submissions.values_list("ai_criteria_details", "ai_feedback", "ai_suggestions").iterator():
|
||||
details, feedback, suggestions = row
|
||||
if details and isinstance(details, dict):
|
||||
for key, val in details.items():
|
||||
@@ -139,9 +161,7 @@ class FlowchartStatisticsAPI(APIView):
|
||||
}
|
||||
|
||||
# 4. Completion stats
|
||||
submitted_users = set(
|
||||
submissions.values_list("user__username", flat=True).distinct()
|
||||
)
|
||||
submitted_users = set(submissions.values_list("user__username", flat=True).distinct())
|
||||
completed_count = len(submitted_users)
|
||||
|
||||
# Unaccepted users
|
||||
@@ -155,16 +175,18 @@ class FlowchartStatisticsAPI(APIView):
|
||||
# 5. Word cloud from feedback + suggestions + criteria comments
|
||||
word_freq = self._build_word_frequencies(wordcloud_texts)
|
||||
|
||||
return self.success({
|
||||
"total_count": total_count,
|
||||
"avg_score": round(avg_score, 1),
|
||||
"grade_distribution": grade_counts,
|
||||
"criteria_averages": criteria_averages,
|
||||
"person_count": len(all_users_dict),
|
||||
"completed_count": completed_count,
|
||||
"word_frequencies": word_freq,
|
||||
"data_unaccepted": unaccepted,
|
||||
})
|
||||
return self.success(
|
||||
{
|
||||
"total_count": total_count,
|
||||
"avg_score": round(avg_score, 1),
|
||||
"grade_distribution": grade_counts,
|
||||
"criteria_averages": criteria_averages,
|
||||
"person_count": len(all_users_dict),
|
||||
"completed_count": completed_count,
|
||||
"word_frequencies": word_freq,
|
||||
"data_unaccepted": unaccepted,
|
||||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _build_word_frequencies(texts, top_n=80):
|
||||
|
||||
@@ -47,11 +47,7 @@ class FlowchartSubmissionAPI(AsyncAPIView):
|
||||
return self.error("submission_id is required")
|
||||
|
||||
try:
|
||||
submission = await (
|
||||
FlowchartSubmission.objects.select_related("user", "problem")
|
||||
.filter(id=submission_id)
|
||||
.afirst()
|
||||
)
|
||||
submission = await FlowchartSubmission.objects.select_related("user", "problem").filter(id=submission_id).afirst()
|
||||
if submission is None:
|
||||
raise FlowchartSubmission.DoesNotExist
|
||||
except FlowchartSubmission.DoesNotExist:
|
||||
@@ -74,9 +70,7 @@ class FlowchartSubmissionListAPI(AsyncAPIView):
|
||||
|
||||
if problem_id:
|
||||
try:
|
||||
problem = await Problem.objects.aget(
|
||||
_id__iexact=problem_id, contest_id__isnull=True, visible=True
|
||||
)
|
||||
problem = await Problem.objects.aget(_id__iexact=problem_id, contest_id__isnull=True, visible=True)
|
||||
except Problem.DoesNotExist:
|
||||
return self.error("Problem doesn't exist")
|
||||
queryset = queryset.filter(problem=problem)
|
||||
@@ -90,9 +84,7 @@ class FlowchartSubmissionListAPI(AsyncAPIView):
|
||||
|
||||
if request.GET.get("today") == "1":
|
||||
now = timezone.now()
|
||||
queryset = queryset.filter(
|
||||
create_time__gte=now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
)
|
||||
queryset = queryset.filter(create_time__gte=now.replace(hour=0, minute=0, second=0, microsecond=0))
|
||||
|
||||
grade = request.GET.get("grade")
|
||||
if grade in ("S", "A", "B", "C"):
|
||||
@@ -115,11 +107,7 @@ class FlowchartSubmissionRetryAPI(AsyncAPIView):
|
||||
return self.error("submission_id is required")
|
||||
|
||||
try:
|
||||
submission = await (
|
||||
FlowchartSubmission.objects.select_related("problem")
|
||||
.filter(id=submission_id)
|
||||
.afirst()
|
||||
)
|
||||
submission = await FlowchartSubmission.objects.select_related("problem").filter(id=submission_id).afirst()
|
||||
if submission is None:
|
||||
raise FlowchartSubmission.DoesNotExist
|
||||
except FlowchartSubmission.DoesNotExist:
|
||||
@@ -187,7 +175,7 @@ class FlowchartSubmissionDetailAPI(AsyncAPIView):
|
||||
else:
|
||||
if page < 0 or page > count:
|
||||
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]
|
||||
data = await self.async_serialize_data(FlowchartSubmissionSerializer, submission)
|
||||
return self.success({"submission": data, "count": count})
|
||||
|
||||
@@ -35,14 +35,24 @@ class FPSParser(object):
|
||||
def _parse_one_problem(self, node):
|
||||
sample_start = True
|
||||
test_case_start = True
|
||||
problem = {"title": "No Title", "description": "No Description",
|
||||
"input": "No Input Description",
|
||||
"output": "No Output Description",
|
||||
"memory_limit": {"unit": None, "value": None},
|
||||
"time_limit": {"unit": None, "value": None},
|
||||
"samples": [], "images": [], "append": [],
|
||||
"template": [], "prepend": [], "test_cases": [],
|
||||
"hint": None, "source": None, "spj": None, "solution": []}
|
||||
problem = {
|
||||
"title": "No Title",
|
||||
"description": "No Description",
|
||||
"input": "No Input Description",
|
||||
"output": "No Output Description",
|
||||
"memory_limit": {"unit": None, "value": None},
|
||||
"time_limit": {"unit": None, "value": None},
|
||||
"samples": [],
|
||||
"images": [],
|
||||
"append": [],
|
||||
"template": [],
|
||||
"prepend": [],
|
||||
"test_cases": [],
|
||||
"hint": None,
|
||||
"source": None,
|
||||
"spj": None,
|
||||
"solution": [],
|
||||
}
|
||||
for item in node:
|
||||
tag = item.tag
|
||||
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:
|
||||
f.write(output_content)
|
||||
if spj:
|
||||
one_info = {
|
||||
"input_size": len(input_content),
|
||||
"input_name": f"{index + 1}.in"
|
||||
}
|
||||
one_info = {"input_size": len(input_content), "input_name": f"{index + 1}.in"}
|
||||
else:
|
||||
one_info = {
|
||||
"input_size": len(input_content),
|
||||
"input_name": f"{index + 1}.in",
|
||||
"output_size": len(output_content),
|
||||
"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
|
||||
info = {
|
||||
"spj": True if spj else False,
|
||||
"test_cases": test_cases
|
||||
}
|
||||
info = {"spj": True if spj else False, "test_cases": test_cases}
|
||||
with open(os.path.join(base_dir, "info"), "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps(info, indent=4))
|
||||
return info
|
||||
|
||||
@@ -7,6 +7,7 @@ if __name__ == "__main__":
|
||||
|
||||
import django
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
sys.stdout.write("Django VERSION " + str(django.VERSION) + "\n")
|
||||
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
@@ -7,9 +7,7 @@ from utils.models import RichTextField
|
||||
|
||||
class Message(models.Model):
|
||||
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name="sender")
|
||||
recipient = models.ForeignKey(
|
||||
User, on_delete=models.CASCADE, related_name="recipient"
|
||||
)
|
||||
recipient = models.ForeignKey(User, on_delete=models.CASCADE, related_name="recipient")
|
||||
submission = models.ForeignKey(Submission, on_delete=models.CASCADE)
|
||||
message = RichTextField()
|
||||
create_time = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@@ -10,9 +10,7 @@ from utils.api.api import validate_serializer
|
||||
class MessageAPI(AsyncAPIView):
|
||||
@login_required
|
||||
async def get(self, request):
|
||||
messages = Message.objects.select_related(
|
||||
"recipient", "sender", "submission", "submission__problem"
|
||||
).filter(recipient=request.user)
|
||||
messages = Message.objects.select_related("recipient", "sender", "submission", "submission__problem").filter(recipient=request.user)
|
||||
return self.success(await self.async_paginate_data(request, messages, MessageSerializer))
|
||||
|
||||
@super_admin_required
|
||||
|
||||
@@ -28,4 +28,3 @@ application = ProtocolTypeRouter(
|
||||
"websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -13,4 +13,3 @@ websocket_urlpatterns = [
|
||||
path("ws/config/", ConfigConsumer.as_asgi()),
|
||||
path("ws/flowchart/", FlowchartConsumer.as_asgi()),
|
||||
]
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ class my_property:
|
||||
2. ttl is callable,条件缓存
|
||||
3. 缓存 ttl 秒
|
||||
"""
|
||||
|
||||
def __init__(self, func=None, fset=None, ttl=None):
|
||||
self.fset = fset
|
||||
self.local = threading.local()
|
||||
@@ -118,8 +119,7 @@ class OptionDefaultValue:
|
||||
class_list = []
|
||||
smtp_config = {}
|
||||
judge_server_token = default_token
|
||||
throttling = {"ip": {"capacity": 100, "fill_rate": 0.1, "default_capacity": 50},
|
||||
"user": {"capacity": 20, "fill_rate": 0.03, "default_capacity": 10}}
|
||||
throttling = {"ip": {"capacity": 100, "fill_rate": 0.1, "default_capacity": 50}, "user": {"capacity": 20, "fill_rate": 0.03, "default_capacity": 10}}
|
||||
languages = languages
|
||||
enable_maxkb = True
|
||||
|
||||
@@ -286,7 +286,6 @@ class _SysOptionsMeta(type):
|
||||
def enable_maxkb(cls, value):
|
||||
cls._set_option(OptionKeys.enable_maxkb, value)
|
||||
|
||||
|
||||
def reset_languages(cls):
|
||||
cls.languages = languages
|
||||
|
||||
@@ -295,6 +294,7 @@ class SysOptions(metaclass=_SysOptionsMeta):
|
||||
@classmethod
|
||||
async def aget(cls, key):
|
||||
from asgiref.sync import sync_to_async
|
||||
|
||||
return await sync_to_async(getattr)(cls, key)
|
||||
|
||||
@classmethod
|
||||
@@ -303,4 +303,5 @@ class SysOptions(metaclass=_SysOptionsMeta):
|
||||
|
||||
def _get_all():
|
||||
return {k: getattr(cls, k) for k in keys}
|
||||
|
||||
return await sync_to_async(_get_all)()
|
||||
|
||||
@@ -24,9 +24,7 @@ class Command(BaseCommand):
|
||||
|
||||
fixed_count = 0
|
||||
for progress in progresses:
|
||||
problemset_problems = ProblemSetProblem.objects.filter(
|
||||
problemset=progress.problemset
|
||||
).select_related("problem")
|
||||
problemset_problems = ProblemSetProblem.objects.filter(problemset=progress.problemset).select_related("problem")
|
||||
|
||||
updated = False
|
||||
for psp in problemset_problems:
|
||||
@@ -46,10 +44,7 @@ class Command(BaseCommand):
|
||||
if not accepted:
|
||||
continue
|
||||
|
||||
self.stdout.write(
|
||||
f" 用户 {progress.user.username} | 题单「{progress.problemset.title}」"
|
||||
f" | 题目 {psp.problem._id} 已AC但进度未记录"
|
||||
)
|
||||
self.stdout.write(f" 用户 {progress.user.username} | 题单「{progress.problemset.title}」 | 题目 {psp.problem._id} 已AC但进度未记录")
|
||||
if dry_run:
|
||||
continue
|
||||
|
||||
|
||||
@@ -16,21 +16,19 @@ def sync_progress_on_problem_change(sender, instance, created, **kwargs):
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# 获取该题单的所有用户进度
|
||||
progresses = ProblemSetProgress.objects.filter(
|
||||
problemset=instance.problemset
|
||||
)
|
||||
|
||||
progresses = ProblemSetProgress.objects.filter(problemset=instance.problemset)
|
||||
|
||||
# 批量更新所有用户的进度
|
||||
for progress in progresses:
|
||||
progress.update_progress()
|
||||
|
||||
|
||||
# 重新计算该题单的所有徽章资格
|
||||
badges = ProblemSetBadge.objects.filter(problemset=instance.problemset)
|
||||
for badge in badges:
|
||||
badge.recalculate_user_badges()
|
||||
|
||||
|
||||
logger.info(f"已同步题单 {instance.problemset.id} 的所有用户进度和徽章资格")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"同步题单进度时出错: {e}")
|
||||
|
||||
@@ -42,27 +40,23 @@ def sync_progress_on_problem_delete(sender, instance, **kwargs):
|
||||
with transaction.atomic():
|
||||
# 清理该题目在题单中的所有提交记录
|
||||
from .models import ProblemSetSubmission
|
||||
ProblemSetSubmission.objects.filter(
|
||||
problemset=instance.problemset,
|
||||
problem=instance.problem
|
||||
).delete()
|
||||
|
||||
|
||||
ProblemSetSubmission.objects.filter(problemset=instance.problemset, problem=instance.problem).delete()
|
||||
|
||||
# 获取该题单的所有用户进度
|
||||
progresses = ProblemSetProgress.objects.filter(
|
||||
problemset=instance.problemset
|
||||
)
|
||||
|
||||
progresses = ProblemSetProgress.objects.filter(problemset=instance.problemset)
|
||||
|
||||
# 批量更新所有用户的进度
|
||||
for progress in progresses:
|
||||
progress.update_progress()
|
||||
|
||||
|
||||
# 重新计算该题单的所有徽章资格
|
||||
badges = ProblemSetBadge.objects.filter(problemset=instance.problemset)
|
||||
for badge in badges:
|
||||
badge.recalculate_user_badges()
|
||||
|
||||
|
||||
logger.info(f"已同步题单 {instance.problemset.id} 的所有用户进度和徽章资格(删除题目后)")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"同步题单进度时出错: {e}")
|
||||
|
||||
@@ -75,7 +69,7 @@ def sync_badges_on_badge_change(sender, instance, created, **kwargs):
|
||||
# 重新计算该奖章的所有用户资格
|
||||
instance.recalculate_user_badges()
|
||||
logger.info(f"已重新计算题单 {instance.problemset.id} 的奖章 {instance.id} 的用户资格")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"重新计算奖章资格时出错: {e}")
|
||||
|
||||
@@ -88,6 +82,6 @@ def cleanup_badges_on_badge_delete(sender, instance, **kwargs):
|
||||
# 删除该奖章的所有用户奖章记录
|
||||
UserBadge.objects.filter(badge=instance).delete()
|
||||
logger.info(f"已清理奖章 {instance.id} 的所有用户奖章记录")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"清理用户奖章记录时出错: {e}")
|
||||
|
||||
@@ -76,8 +76,7 @@ class ProblemSetAPI(AsyncAPIView):
|
||||
|
||||
# 批量查询用户已获得的奖章ID(这些题单相关的)
|
||||
user_earned_badge_ids = {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
# 预加载奖章信息(在获取ID之后应用,避免在获取ID时也预加载)
|
||||
@@ -97,12 +96,7 @@ class ProblemSetDetailAPI(AsyncAPIView):
|
||||
async def get(self, request, problem_set_id):
|
||||
"""获取题单详情"""
|
||||
try:
|
||||
problem_set = await (
|
||||
ProblemSet.objects.select_related("created_by")
|
||||
.filter(id=problem_set_id, visible=True)
|
||||
.exclude(status=ProblemSetStatus.DRAFT)
|
||||
.aget()
|
||||
)
|
||||
problem_set = await ProblemSet.objects.select_related("created_by").filter(id=problem_set_id, visible=True).exclude(status=ProblemSetStatus.DRAFT).aget()
|
||||
except ProblemSet.DoesNotExist:
|
||||
return self.error("题单不存在")
|
||||
|
||||
|
||||
@@ -19,10 +19,7 @@ def bulk_fetch_problemset_progress(user, problem_ids):
|
||||
problemset__status=ProblemSetStatus.ACTIVE,
|
||||
problemset__problemsetproblem__problem_id__in=problem_ids,
|
||||
)
|
||||
.filter(
|
||||
models.Q(problemset__end_time__isnull=True)
|
||||
| models.Q(problemset__end_time__gt=timezone.now())
|
||||
)
|
||||
.filter(models.Q(problemset__end_time__isnull=True) | models.Q(problemset__end_time__gt=timezone.now()))
|
||||
.annotate(matched_problem_id=F("problemset__problemsetproblem__problem_id"))
|
||||
.only("join_time", "progress_detail")
|
||||
)
|
||||
@@ -51,7 +48,6 @@ class ShareSubmissionSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class SubmissionModelSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Submission
|
||||
fields = "__all__"
|
||||
@@ -92,11 +88,7 @@ class SubmissionListSerializer(serializers.ModelSerializer):
|
||||
# 如果该题目已在题单中做出来了,则恢复显示
|
||||
if obj.user_id == self.user.id and self.user.is_regular_user():
|
||||
progress = self._get_problemset_progress(obj.problem_id)
|
||||
if (
|
||||
progress
|
||||
and obj.create_time < progress.join_time
|
||||
and str(obj.problem_id) not in progress.progress_detail
|
||||
):
|
||||
if progress and obj.create_time < progress.join_time and str(obj.problem_id) not in progress.progress_detail:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -111,10 +103,7 @@ class SubmissionListSerializer(serializers.ModelSerializer):
|
||||
problemset__status=ProblemSetStatus.ACTIVE,
|
||||
problemset__problemsetproblem__problem_id=problem_id,
|
||||
)
|
||||
.filter(
|
||||
models.Q(problemset__end_time__isnull=True)
|
||||
| models.Q(problemset__end_time__gt=timezone.now())
|
||||
)
|
||||
.filter(models.Q(problemset__end_time__isnull=True) | models.Q(problemset__end_time__gt=timezone.now()))
|
||||
.only("join_time", "progress_detail")
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -22,9 +22,7 @@ class SubmissionRejudgeAPI(APIView):
|
||||
if not id:
|
||||
return self.error("Parameter error, id is required")
|
||||
try:
|
||||
submission = Submission.objects.select_related("problem").get(
|
||||
id=id, contest_id__isnull=True
|
||||
)
|
||||
submission = Submission.objects.select_related("problem").get(id=id, contest_id__isnull=True)
|
||||
except Submission.DoesNotExist:
|
||||
return self.error("Submission does not exists")
|
||||
submission.statistic_info = {}
|
||||
@@ -46,17 +44,13 @@ class SubmissionStatisticsAPI(APIView):
|
||||
filters = {"contest_id__isnull": True, "create_time__lte": end}
|
||||
if start:
|
||||
filters["create_time__gte"] = start
|
||||
submissions = Submission.objects.filter(
|
||||
**filters
|
||||
).select_related("problem__created_by")
|
||||
submissions = Submission.objects.filter(**filters).select_related("problem__created_by")
|
||||
|
||||
problem_id = request.GET.get("problem_id")
|
||||
|
||||
if problem_id:
|
||||
try:
|
||||
problem = Problem.objects.get(
|
||||
_id__iexact=problem_id, contest_id__isnull=True, visible=True
|
||||
)
|
||||
problem = Problem.objects.get(_id__iexact=problem_id, contest_id__isnull=True, visible=True)
|
||||
except Problem.DoesNotExist:
|
||||
return self.error("Problem doesn't exist")
|
||||
submissions = submissions.filter(problem=problem)
|
||||
@@ -82,9 +76,7 @@ class SubmissionStatisticsAPI(APIView):
|
||||
)
|
||||
submission_count = submission_stats["total_count"]
|
||||
accepted_count = submission_stats["accepted_count"]
|
||||
correct_rate = (
|
||||
round(accepted_count / submission_count * 100, 2) if submission_count else 0
|
||||
)
|
||||
correct_rate = round(accepted_count / submission_count * 100, 2) if submission_count else 0
|
||||
|
||||
# 优化:获取用户提交统计
|
||||
user_submissions = (
|
||||
@@ -99,20 +91,13 @@ class SubmissionStatisticsAPI(APIView):
|
||||
# 获取所有有提交记录的用户的class_name信息
|
||||
submitted_usernames = {item["username"] for item in user_submissions}
|
||||
if submitted_usernames:
|
||||
submitted_users_dict = {
|
||||
user["username"]: user["class_name"]
|
||||
for user in User.objects.filter(
|
||||
username__in=submitted_usernames
|
||||
).values("username", "class_name")
|
||||
}
|
||||
submitted_users_dict = {user["username"]: user["class_name"] for user in User.objects.filter(username__in=submitted_usernames).values("username", "class_name")}
|
||||
else:
|
||||
submitted_users_dict = {}
|
||||
|
||||
# 预先收集每个用户的提交ID和结果,按时间倒序
|
||||
submission_items_by_user = {}
|
||||
for submission in submissions.values("username", "id", "result").order_by(
|
||||
"-create_time"
|
||||
):
|
||||
for submission in submissions.values("username", "id", "result").order_by("-create_time"):
|
||||
username_key = submission["username"]
|
||||
submission_id = str(submission["id"])
|
||||
submission_items_by_user.setdefault(username_key, []).append(
|
||||
@@ -137,9 +122,7 @@ class SubmissionStatisticsAPI(APIView):
|
||||
"submission_count": item["submission_count"],
|
||||
"accepted_count": item["accepted_count"],
|
||||
"correct_rate": f"{rate}%",
|
||||
"submission_items": submission_items_by_user.get(
|
||||
username_key, []
|
||||
),
|
||||
"submission_items": submission_items_by_user.get(username_key, []),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -37,9 +37,7 @@ class SubmissionAPI(AsyncAPIView):
|
||||
auth_method = getattr(request, "auth_method", "")
|
||||
if auth_method == "api_key":
|
||||
return
|
||||
user_bucket = TokenBucket(
|
||||
key=str(request.user.id), redis_conn=cache, **SysOptions.throttling["user"]
|
||||
)
|
||||
user_bucket = TokenBucket(key=str(request.user.id), redis_conn=cache, **SysOptions.throttling["user"])
|
||||
can_consume, wait = user_bucket.consume()
|
||||
if not can_consume:
|
||||
return "Please wait %d seconds" % (int(wait))
|
||||
@@ -52,10 +50,7 @@ class SubmissionAPI(AsyncAPIView):
|
||||
if not request.user.is_contest_admin(contest):
|
||||
user_ip = ipaddress.ip_address(request.session.get("ip"))
|
||||
if contest.allowed_ip_ranges:
|
||||
if not any(
|
||||
user_ip in ipaddress.ip_network(cidr, strict=False)
|
||||
for cidr in contest.allowed_ip_ranges
|
||||
):
|
||||
if not any(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")
|
||||
|
||||
@login_required
|
||||
@@ -79,9 +74,7 @@ class SubmissionAPI(AsyncAPIView):
|
||||
return self.error(error)
|
||||
|
||||
try:
|
||||
problem = await Problem.objects.aget(
|
||||
id=data["problem_id"], contest_id=data.get("contest_id"), visible=True
|
||||
)
|
||||
problem = await Problem.objects.aget(id=data["problem_id"], contest_id=data.get("contest_id"), visible=True)
|
||||
except Problem.DoesNotExist:
|
||||
return self.error("Problem not exist")
|
||||
if data["language"] not in problem.languages:
|
||||
@@ -108,9 +101,7 @@ class SubmissionAPI(AsyncAPIView):
|
||||
if not submission_id:
|
||||
return self.error("Parameter id doesn't exist")
|
||||
try:
|
||||
submission = await Submission.objects.select_related("problem", "contest").aget(
|
||||
id=submission_id
|
||||
)
|
||||
submission = await Submission.objects.select_related("problem", "contest").aget(id=submission_id)
|
||||
except Submission.DoesNotExist:
|
||||
return self.error("Submission doesn't exist")
|
||||
if not submission.check_user_permission(request.user):
|
||||
@@ -120,26 +111,19 @@ class SubmissionAPI(AsyncAPIView):
|
||||
submission_data = await self.async_serialize_data(SubmissionModelSerializer, submission)
|
||||
else:
|
||||
submission_data = await self.async_serialize_data(SubmissionSafeModelSerializer, submission)
|
||||
submission_data["can_unshare"] = submission.check_user_permission(
|
||||
request.user, check_share=False
|
||||
)
|
||||
submission_data["can_unshare"] = submission.check_user_permission(request.user, check_share=False)
|
||||
return self.success(submission_data)
|
||||
|
||||
@login_required
|
||||
@validate_serializer(ShareSubmissionSerializer)
|
||||
async def put(self, request):
|
||||
try:
|
||||
submission = await Submission.objects.select_related("problem", "contest").aget(
|
||||
id=request.data["id"]
|
||||
)
|
||||
submission = await Submission.objects.select_related("problem", "contest").aget(id=request.data["id"])
|
||||
except Submission.DoesNotExist:
|
||||
return self.error("Submission doesn't exist")
|
||||
if not submission.check_user_permission(request.user, check_share=False):
|
||||
return self.error("No permission to share the submission")
|
||||
if (
|
||||
submission.contest
|
||||
and submission.contest.status == ContestStatus.CONTEST_UNDERWAY
|
||||
):
|
||||
if submission.contest and submission.contest.status == ContestStatus.CONTEST_UNDERWAY:
|
||||
return self.error("Can not share submission now")
|
||||
submission.shared = request.data["shared"]
|
||||
await submission.asave(update_fields=["shared"])
|
||||
@@ -153,9 +137,7 @@ class SubmissionListAPI(AsyncAPIView):
|
||||
if request.GET.get("contest_id"):
|
||||
return self.error("Parameter error")
|
||||
|
||||
submissions = Submission.objects.filter(contest_id__isnull=True).select_related(
|
||||
"problem"
|
||||
).order_by("-create_time")
|
||||
submissions = Submission.objects.filter(contest_id__isnull=True).select_related("problem").order_by("-create_time")
|
||||
problem_id = request.GET.get("problem_id")
|
||||
myself = request.GET.get("myself")
|
||||
result = request.GET.get("result")
|
||||
@@ -163,9 +145,7 @@ class SubmissionListAPI(AsyncAPIView):
|
||||
language = request.GET.get("language")
|
||||
if problem_id:
|
||||
try:
|
||||
problem = await Problem.objects.aget(
|
||||
_id__iexact=problem_id, contest_id__isnull=True, visible=True
|
||||
)
|
||||
problem = await Problem.objects.aget(_id__iexact=problem_id, contest_id__isnull=True, visible=True)
|
||||
except Problem.DoesNotExist:
|
||||
return self.error("Problem doesn't exist")
|
||||
submissions = submissions.filter(problem=problem)
|
||||
@@ -184,9 +164,7 @@ class SubmissionListAPI(AsyncAPIView):
|
||||
submissions = submissions.filter(language=language)
|
||||
if request.GET.get("today") == "1":
|
||||
now = timezone.now()
|
||||
submissions = submissions.filter(
|
||||
create_time__gte=now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
)
|
||||
submissions = submissions.filter(create_time__gte=now.replace(hour=0, minute=0, second=0, microsecond=0))
|
||||
|
||||
data = await self.async_paginate_data(request, submissions)
|
||||
results = data["results"]
|
||||
@@ -212,18 +190,14 @@ class ContestSubmissionListAPI(AsyncAPIView):
|
||||
return self.error("Limit is needed")
|
||||
|
||||
contest = self.contest
|
||||
submissions = Submission.objects.filter(contest_id=contest.id).select_related(
|
||||
"problem", "contest"
|
||||
).order_by("-create_time")
|
||||
submissions = Submission.objects.filter(contest_id=contest.id).select_related("problem", "contest").order_by("-create_time")
|
||||
problem_id = request.GET.get("problem_id")
|
||||
myself = request.GET.get("myself")
|
||||
result = request.GET.get("result")
|
||||
username = request.GET.get("username")
|
||||
if problem_id:
|
||||
try:
|
||||
problem = await Problem.objects.aget(
|
||||
_id__iexact=problem_id, contest_id=contest.id, visible=True
|
||||
)
|
||||
problem = await Problem.objects.aget(_id__iexact=problem_id, contest_id=contest.id, visible=True)
|
||||
except Problem.DoesNotExist:
|
||||
return self.error("Problem doesn't exist")
|
||||
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)
|
||||
else:
|
||||
progress_cache = {}
|
||||
data["results"] = await self.async_serialize_data(
|
||||
SubmissionListSerializer,
|
||||
results, many=True, user=request.user, problemset_progress_cache=progress_cache
|
||||
)
|
||||
data["results"] = await self.async_serialize_data(SubmissionListSerializer, results, many=True, user=request.user, problemset_progress_cache=progress_cache)
|
||||
return self.success(data)
|
||||
|
||||
|
||||
@@ -257,12 +228,7 @@ class SubmissionExistsAPI(AsyncAPIView):
|
||||
async def get(self, request):
|
||||
if not request.GET.get("problem_id"):
|
||||
return self.error("Parameter error, problem_id is required")
|
||||
exists = (
|
||||
request.user.is_authenticated
|
||||
and await Submission.objects.filter(
|
||||
problem_id=request.GET["problem_id"], user_id=request.user.id
|
||||
).aexists()
|
||||
)
|
||||
exists = 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)
|
||||
|
||||
|
||||
@@ -271,13 +237,9 @@ class SubmissionsTodayCount(AsyncAPIView):
|
||||
now = timezone.now()
|
||||
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
if request.GET.get("language") == "Flowchart":
|
||||
count = await FlowchartSubmission.objects.filter(
|
||||
create_time__gte=start
|
||||
).acount()
|
||||
count = await FlowchartSubmission.objects.filter(create_time__gte=start).acount()
|
||||
else:
|
||||
count = await Submission.objects.filter(
|
||||
contest_id__isnull=True, create_time__gte=start
|
||||
).acount()
|
||||
count = await Submission.objects.filter(contest_id__isnull=True, create_time__gte=start).acount()
|
||||
return self.success(count)
|
||||
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -53,19 +53,10 @@ class TutorialAdminAPI(APIView):
|
||||
return self.success(TutorialSerializer(tutorial).data)
|
||||
except Tutorial.DoesNotExist:
|
||||
return self.error("Tutorial does not exist")
|
||||
|
||||
|
||||
tutorials = Tutorial.objects.all().order_by("order", "-created_at")
|
||||
# 按 type 分组返回数据
|
||||
result = {
|
||||
"python": TutorialListSerializer(
|
||||
tutorials.filter(type="python"),
|
||||
many=True
|
||||
).data,
|
||||
"c": TutorialListSerializer(
|
||||
tutorials.filter(type="c"),
|
||||
many=True
|
||||
).data
|
||||
}
|
||||
result = {"python": TutorialListSerializer(tutorials.filter(type="python"), many=True).data, "c": TutorialListSerializer(tutorials.filter(type="c"), many=True).data}
|
||||
return self.success(result)
|
||||
|
||||
@super_admin_required
|
||||
|
||||
@@ -16,9 +16,7 @@ class TutorialAPI(APIView):
|
||||
class TutorialTitlesAPI(APIView):
|
||||
def get(self, request):
|
||||
type = request.GET.get("type") or "python"
|
||||
tutorials = Tutorial.objects.filter(is_public=True, type=type).values(
|
||||
"id", "title"
|
||||
)
|
||||
tutorials = Tutorial.objects.filter(is_public=True, type=type).values("id", "title")
|
||||
return self.success(list(tutorials))
|
||||
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ class APIView(View):
|
||||
- self.response 返回一个django HttpResponse, 具体在self.response_class中实现
|
||||
- parse请求的类需要定义在request_parser中, 目前只支持json和urlencoded的类型, 用来解析请求的数据
|
||||
"""
|
||||
|
||||
request_parsers = (JSONParser, URLEncodedParser)
|
||||
response_class = JSONResponse
|
||||
|
||||
@@ -134,11 +135,10 @@ class APIView(View):
|
||||
offset = 0
|
||||
# 只调用一次 count(),避免重复查询
|
||||
count = query_set.count()
|
||||
results = query_set[offset:offset + limit]
|
||||
results = query_set[offset : offset + limit]
|
||||
if object_serializer:
|
||||
results = object_serializer(results, many=True, context={"request": request}).data
|
||||
data = {"results": results,
|
||||
"total": count}
|
||||
data = {"results": results, "total": count}
|
||||
return data
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
@@ -215,8 +215,9 @@ class AsyncAPIView(APIView):
|
||||
offset = 0
|
||||
if offset < 0:
|
||||
offset = 0
|
||||
|
||||
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(
|
||||
query_set.acount(),
|
||||
@@ -245,8 +246,10 @@ def validate_serializer(serializer):
|
||||
def post(self, request):
|
||||
return self.success(request.data)
|
||||
"""
|
||||
|
||||
def validate(view_method):
|
||||
if inspect.iscoroutinefunction(view_method):
|
||||
|
||||
@functools.wraps(view_method)
|
||||
async def async_handle(*args, **kwargs):
|
||||
self = args[0]
|
||||
@@ -261,6 +264,7 @@ def validate_serializer(serializer):
|
||||
return response
|
||||
else:
|
||||
return self.invalid_serializer(s)
|
||||
|
||||
return async_handle
|
||||
|
||||
@functools.wraps(view_method)
|
||||
@@ -274,6 +278,7 @@ def validate_serializer(serializer):
|
||||
return view_method(*args, **kwargs)
|
||||
else:
|
||||
return self.invalid_serializer(s)
|
||||
|
||||
return handle
|
||||
|
||||
return validate
|
||||
|
||||
@@ -15,7 +15,7 @@ class Command(BaseCommand):
|
||||
password = options["password"]
|
||||
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"))
|
||||
exit(1)
|
||||
|
||||
@@ -24,8 +24,7 @@ class Command(BaseCommand):
|
||||
self.stdout.write(self.style.SUCCESS(f"User {username} exists, operation ignored"))
|
||||
exit()
|
||||
|
||||
user = User.objects.create(username=username, admin_type=AdminType.SUPER_ADMIN,
|
||||
problem_permission=ProblemPermission.ALL)
|
||||
user = User.objects.create(username=username, admin_type=AdminType.SUPER_ADMIN, problem_permission=ProblemPermission.ALL)
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
UserProfile.objects.create(user=user)
|
||||
|
||||
@@ -37,7 +37,7 @@ def build_query_string(kv_data, ignore_none=True):
|
||||
query_string += "&"
|
||||
else:
|
||||
query_string = "?"
|
||||
query_string += (k + "=" + str(v))
|
||||
query_string += k + "=" + str(v)
|
||||
return query_string
|
||||
|
||||
|
||||
@@ -60,8 +60,7 @@ def datetime2str(value, format="iso-8601"):
|
||||
|
||||
|
||||
def natural_sort_key(s, _nsre=re.compile(r"(\d+)")):
|
||||
return [int(text) if text.isdigit() else text.lower()
|
||||
for text in re.split(_nsre, s)]
|
||||
return [int(text) if text.isdigit() else text.lower() for text in re.split(_nsre, s)]
|
||||
|
||||
|
||||
def send_email(smtp_config, from_name, to_email, to_name, subject, content):
|
||||
|
||||
@@ -5,6 +5,7 @@ class TokenBucket:
|
||||
"""
|
||||
注意:对于单个key的操作不是线程安全的
|
||||
"""
|
||||
|
||||
def __init__(self, key, capacity, fill_rate, default_capacity, redis_conn):
|
||||
"""
|
||||
:param capacity: 最大容量
|
||||
|
||||
@@ -18,17 +18,11 @@ class SimditorImageUploadAPIView(CSRFExemptAPIView):
|
||||
if form.is_valid():
|
||||
img = form.cleaned_data["image"]
|
||||
else:
|
||||
return self.response({
|
||||
"success": False,
|
||||
"msg": "Upload failed",
|
||||
"file_path": ""})
|
||||
return self.response({"success": False, "msg": "Upload failed", "file_path": ""})
|
||||
|
||||
suffix = os.path.splitext(img.name)[-1].lower()
|
||||
if suffix not in [".gif", ".jpg", ".jpeg", ".bmp", ".png"]:
|
||||
return self.response({
|
||||
"success": False,
|
||||
"msg": "Unsupported file format",
|
||||
"file_path": ""})
|
||||
return self.response({"success": False, "msg": "Unsupported file format", "file_path": ""})
|
||||
img_name = rand_str(10) + suffix
|
||||
try:
|
||||
with open(os.path.join(settings.UPLOAD_DIR, img_name), "wb") as imgFile:
|
||||
@@ -36,14 +30,8 @@ class SimditorImageUploadAPIView(CSRFExemptAPIView):
|
||||
imgFile.write(chunk)
|
||||
except IOError as e:
|
||||
logger.error(e)
|
||||
return self.response({
|
||||
"success": False,
|
||||
"msg": "Upload Error",
|
||||
"file_path": ""})
|
||||
return self.response({
|
||||
"success": True,
|
||||
"msg": "Success",
|
||||
"file_path": f"{settings.UPLOAD_PREFIX}/{img_name}"})
|
||||
return self.response({"success": False, "msg": "Upload Error", "file_path": ""})
|
||||
return self.response({"success": True, "msg": "Success", "file_path": f"{settings.UPLOAD_PREFIX}/{img_name}"})
|
||||
|
||||
|
||||
# DEPRECATED: 前端未调用 (2026-05-26)
|
||||
@@ -55,10 +43,7 @@ class SimditorFileUploadAPIView(CSRFExemptAPIView):
|
||||
if form.is_valid():
|
||||
file = form.cleaned_data["file"]
|
||||
else:
|
||||
return self.response({
|
||||
"success": False,
|
||||
"msg": "Upload failed"
|
||||
})
|
||||
return self.response({"success": False, "msg": "Upload failed"})
|
||||
|
||||
suffix = os.path.splitext(file.name)[-1].lower()
|
||||
file_name = rand_str(10) + suffix
|
||||
@@ -68,11 +53,5 @@ class SimditorFileUploadAPIView(CSRFExemptAPIView):
|
||||
f.write(chunk)
|
||||
except IOError as e:
|
||||
logger.error(e)
|
||||
return self.response({
|
||||
"success": False,
|
||||
"msg": "Upload Error"})
|
||||
return self.response({
|
||||
"success": True,
|
||||
"msg": "Success",
|
||||
"file_path": f"{settings.UPLOAD_PREFIX}/{file_name}",
|
||||
"file_name": file.name})
|
||||
return self.response({"success": False, "msg": "Upload Error"})
|
||||
return self.response({"success": True, "msg": "Success", "file_path": f"{settings.UPLOAD_PREFIX}/{file_name}", "file_name": file.name})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
WebSocket utility functions for pushing real-time updates
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
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):
|
||||
"""
|
||||
推送提交状态更新到指定用户的 WebSocket 连接
|
||||
|
||||
|
||||
Args:
|
||||
submission_id: 提交 ID
|
||||
user_id: 用户 ID
|
||||
data: 要发送的数据,应该包含 type, submission_id, result 等字段
|
||||
"""
|
||||
channel_layer = get_channel_layer()
|
||||
|
||||
|
||||
if channel_layer is None:
|
||||
logger.warning("Channel layer is not configured, cannot push submission update")
|
||||
return
|
||||
|
||||
|
||||
# 构建组名,与 SubmissionConsumer 中的组名一致
|
||||
group_name = f"submission_user_{user_id}"
|
||||
|
||||
|
||||
try:
|
||||
# 向指定用户组发送消息
|
||||
# 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 方法
|
||||
"data": data,
|
||||
}
|
||||
},
|
||||
)
|
||||
logger.info(f"Pushed submission update: submission_id={submission_id}, user_id={user_id}, status={data.get('status')}")
|
||||
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):
|
||||
"""
|
||||
向指定用户推送自定义消息
|
||||
|
||||
|
||||
Args:
|
||||
user_id: 用户 ID
|
||||
message_type: 消息类型
|
||||
data: 消息数据
|
||||
"""
|
||||
channel_layer = get_channel_layer()
|
||||
|
||||
|
||||
if channel_layer is None:
|
||||
logger.warning("Channel layer is not configured, cannot push message")
|
||||
return
|
||||
|
||||
|
||||
group_name = f"submission_user_{user_id}"
|
||||
|
||||
|
||||
try:
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
group_name,
|
||||
{
|
||||
"type": "submission_update",
|
||||
"data": {
|
||||
"type": message_type,
|
||||
**data
|
||||
},
|
||||
}
|
||||
"data": {"type": message_type, **data},
|
||||
},
|
||||
)
|
||||
logger.info(f"Pushed message to user {user_id}: type={message_type}")
|
||||
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):
|
||||
"""
|
||||
推送流程图评分状态更新到指定用户的 WebSocket 连接
|
||||
|
||||
|
||||
Args:
|
||||
submission_id: 流程图提交 ID
|
||||
user_id: 用户 ID
|
||||
data: 要发送的数据,应该包含 type, submission_id, score, grade, feedback 等字段
|
||||
"""
|
||||
channel_layer = get_channel_layer()
|
||||
|
||||
|
||||
if channel_layer is None:
|
||||
logger.warning("Channel layer is not configured, cannot push flowchart evaluation update")
|
||||
return
|
||||
|
||||
|
||||
# 构建组名,与 FlowchartConsumer 中的组名一致
|
||||
group_name = f"flowchart_user_{user_id}"
|
||||
|
||||
|
||||
try:
|
||||
# 向指定用户组发送消息
|
||||
# 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 方法
|
||||
"data": data,
|
||||
}
|
||||
},
|
||||
)
|
||||
logger.info(f"Pushed flowchart evaluation update: submission_id={submission_id}, user_id={user_id}, type={data.get('type')}")
|
||||
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):
|
||||
"""
|
||||
推送配置更新到所有连接的客户端
|
||||
|
||||
|
||||
Args:
|
||||
key: 配置键名
|
||||
value: 配置值
|
||||
"""
|
||||
channel_layer = get_channel_layer()
|
||||
|
||||
|
||||
if channel_layer is None:
|
||||
logger.warning("Channel layer is not configured, cannot push config update")
|
||||
return
|
||||
|
||||
|
||||
# 使用全局配置组名
|
||||
group_name = "config_updates"
|
||||
|
||||
|
||||
try:
|
||||
# 向所有连接的客户端发送配置更新
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
group_name,
|
||||
{
|
||||
"type": "config_update",
|
||||
"data": {
|
||||
"type": "config_update",
|
||||
"key": key,
|
||||
"value": value
|
||||
}
|
||||
}
|
||||
)
|
||||
async_to_sync(channel_layer.group_send)(group_name, {"type": "config_update", "data": {"type": "config_update", "key": key, "value": value}})
|
||||
logger.info(f"Pushed config update: {key}={value}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to push config update: {key}={value}, error={str(e)}")
|
||||
|
||||
@@ -25,17 +25,52 @@ Python 2.6+ or 3.2+
|
||||
Cannot defense xss in browser which is belowed IE7
|
||||
浏览器版本:IE7+ 或其他浏览器,无法防御IE6及以下版本浏览器中的XSS
|
||||
"""
|
||||
|
||||
import copy
|
||||
import re
|
||||
from html.parser import HTMLParser
|
||||
|
||||
|
||||
class XSSHtml(HTMLParser):
|
||||
allow_tags = ['a', 'img', 'br', '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']
|
||||
allow_tags = [
|
||||
"a",
|
||||
"img",
|
||||
"br",
|
||||
"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"]
|
||||
nonend_tags = ["img", "hr", "br", "embed"]
|
||||
tags_own_attrs = {
|
||||
@@ -43,7 +78,7 @@ class XSSHtml(HTMLParser):
|
||||
"a": ["href", "target", "rel", "title"],
|
||||
"embed": ["src", "width", "height", "type", "allowfullscreen", "loop", "play", "wmode", "menu"],
|
||||
"table": ["border", "cellpadding", "cellspacing"],
|
||||
"font": ["color"]
|
||||
"font": ["color"],
|
||||
}
|
||||
|
||||
def __init__(self, allows=[]):
|
||||
@@ -68,9 +103,9 @@ class XSSHtml(HTMLParser):
|
||||
Get the safe html code
|
||||
"""
|
||||
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])
|
||||
return ''.join(self.data)
|
||||
return "".join(self.data)
|
||||
|
||||
def handle_startendtag(self, tag, attrs):
|
||||
self.handle_starttag(tag, attrs)
|
||||
@@ -78,7 +113,7 @@ class XSSHtml(HTMLParser):
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag not in self.allow_tags:
|
||||
return
|
||||
end_diagonal = ' /' if tag in self.nonend_tags else ''
|
||||
end_diagonal = " /" if tag in self.nonend_tags else ""
|
||||
if not end_diagonal:
|
||||
self.start.append(tag)
|
||||
attdict = {}
|
||||
@@ -92,14 +127,14 @@ class XSSHtml(HTMLParser):
|
||||
attdict = self.node_default(attdict)
|
||||
|
||||
attrs = []
|
||||
for (key, value) in attdict.items():
|
||||
for key, value in attdict.items():
|
||||
attrs.append('%s="%s"' % (key, self._htmlspecialchars(value)))
|
||||
attrs = (' ' + ' '.join(attrs)) if attrs else ''
|
||||
self.result.append('<' + tag + attrs + end_diagonal + '>')
|
||||
attrs = (" " + " ".join(attrs)) if attrs else ""
|
||||
self.result.append("<" + tag + attrs + end_diagonal + ">")
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if self.start and tag == self.start[len(self.start) - 1]:
|
||||
self.result.append('</' + tag + '>')
|
||||
self.result.append("</" + tag + ">")
|
||||
self.start.pop()
|
||||
|
||||
def handle_data(self, data):
|
||||
@@ -121,22 +156,23 @@ class XSSHtml(HTMLParser):
|
||||
attrs = self._common_attr(attrs)
|
||||
attrs = self._get_link(attrs, "href")
|
||||
attrs = self._set_attr_default(attrs, "target", "_blank")
|
||||
attrs = self._limit_attr(attrs, {
|
||||
"target": ["_blank", "_self"]
|
||||
})
|
||||
attrs = self._limit_attr(attrs, {"target": ["_blank", "_self"]})
|
||||
return attrs
|
||||
|
||||
def node_embed(self, attrs):
|
||||
attrs = self._common_attr(attrs)
|
||||
attrs = self._get_link(attrs, "src")
|
||||
attrs = self._limit_attr(attrs, {
|
||||
"type": ["application/x-shockwave-flash"],
|
||||
"wmode": ["transparent", "window", "opaque"],
|
||||
"play": ["true", "false"],
|
||||
"loop": ["true", "false"],
|
||||
"menu": ["true", "false"],
|
||||
"allowfullscreen": ["true", "false"]
|
||||
})
|
||||
attrs = self._limit_attr(
|
||||
attrs,
|
||||
{
|
||||
"type": ["application/x-shockwave-flash"],
|
||||
"wmode": ["transparent", "window", "opaque"],
|
||||
"play": ["true", "false"],
|
||||
"loop": ["true", "false"],
|
||||
"menu": ["true", "false"],
|
||||
"allowfullscreen": ["true", "false"],
|
||||
},
|
||||
)
|
||||
attrs["allowscriptaccess"] = "never"
|
||||
attrs["allownetworking"] = "none"
|
||||
return attrs
|
||||
@@ -179,22 +215,19 @@ class XSSHtml(HTMLParser):
|
||||
attrs = self._get_style(attrs)
|
||||
return attrs
|
||||
|
||||
def _set_attr_default(self, attrs, name, default=''):
|
||||
def _set_attr_default(self, attrs, name, default=""):
|
||||
if name not in attrs:
|
||||
attrs[name] = default
|
||||
return attrs
|
||||
|
||||
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:
|
||||
del attrs[key]
|
||||
return attrs
|
||||
|
||||
def _htmlspecialchars(self, html):
|
||||
return html.replace("<", "<") \
|
||||
.replace(">", ">") \
|
||||
.replace('"', """) \
|
||||
.replace("'", "'")
|
||||
return html.replace("<", "<").replace(">", ">").replace('"', """).replace("'", "'")
|
||||
|
||||
|
||||
if "__main__" == __name__:
|
||||
|
||||
Reference in New Issue
Block a user