style: ruff format 全仓库

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

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

View File

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

View File

@@ -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:

View File

@@ -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)

View File

@@ -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)
@@ -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)

View File

@@ -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)

View File

@@ -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,
}
)

View File

@@ -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"),
]

View File

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

View File

@@ -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):

View File

@@ -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))

View File

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

View File

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

View File

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

View File

@@ -42,9 +42,7 @@ class ClassRankAPI(APIView):
profiles = UserProfile.objects.filter(user_id__in=user_ids)
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(
{

View File

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

View File

@@ -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):

View File

@@ -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)

View File

@@ -1,6 +1,7 @@
"""
WebSocket consumers for configuration updates
"""
import json
import logging
@@ -28,21 +29,15 @@ class ConfigConsumer(AsyncWebsocketConsumer):
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):
@@ -56,10 +51,7 @@ class ConfigConsumer(AsyncWebsocketConsumer):
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:

View File

@@ -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)

View File

@@ -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)

View File

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

View File

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

View File

@@ -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)

View File

@@ -1,6 +1,7 @@
"""
WebSocket consumers for flowchart evaluation updates
"""
import json
import logging
@@ -28,21 +29,15 @@ class FlowchartConsumer(AsyncWebsocketConsumer):
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):
@@ -56,10 +51,7 @@ class FlowchartConsumer(AsyncWebsocketConsumer):
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")

View File

@@ -6,19 +6,22 @@ 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代码
@@ -36,19 +39,18 @@ class FlowchartSubmission(models.Model):
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):
@@ -56,11 +58,7 @@ class FlowchartSubmission(models.Model):
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

View File

@@ -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 = [

View File

@@ -59,10 +59,7 @@ def evaluate_flowchart_task(submission_id):
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,13 +71,13 @@ 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()
@@ -88,14 +85,15 @@ def evaluate_flowchart_task(submission_id):
# 推送评分完成通知
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:
@@ -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,6 +115,7 @@ def evaluate_flowchart_task(submission_id):
)
raise e
def build_evaluation_prompt(problem):
"""构建AI评分提示词 - 使用固定标准"""
@@ -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()

View File

@@ -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()),
]

View File

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

View File

@@ -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):

View File

@@ -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})

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

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

View File

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

View File

@@ -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)()

View File

@@ -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

View File

@@ -16,9 +16,7 @@ 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:
@@ -42,15 +40,11 @@ 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:

View File

@@ -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("题单不存在")

View File

@@ -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()
)

View File

@@ -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, []),
}
)

View File

@@ -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)

View File

@@ -1 +0,0 @@

View File

@@ -1 +0,0 @@

View File

@@ -56,16 +56,7 @@ class TutorialAdminAPI(APIView):
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

View File

@@ -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))

View File

@@ -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

View File

@@ -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)

View File

@@ -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):

View File

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

View File

@@ -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})

View File

@@ -1,6 +1,7 @@
"""
WebSocket utility functions for pushing real-time updates
"""
import logging
from asgiref.sync import async_to_sync
@@ -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:
@@ -64,11 +65,8 @@ def push_to_user(user_id: int, message_type: str, data: dict):
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:
@@ -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:
@@ -127,17 +125,7 @@ def push_config_update(key: str, value):
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)}")

View File

@@ -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("<", "&lt;") \
.replace(">", "&gt;") \
.replace('"', "&quot;") \
.replace("'", "&#039;")
return html.replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;").replace("'", "&#039;")
if "__main__" == __name__: