From 3a9ab83ba56f9c0c2a860d3e6d4d5d296bbe35b7 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Wed, 5 Aug 2026 04:45:26 -0600 Subject: [PATCH] =?UTF-8?q?style:=20ruff=20format=20=E5=85=A8=E4=BB=93?= =?UTF-8?q?=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 行宽 180 下把历史遗留的折行表达式合并,无语义改动。 Co-Authored-By: Claude Sonnet 5 --- account/decorators.py | 2 + .../commands/clean_deleted_problems.py | 18 +-- account/tasks.py | 7 +- account/views/admin.py | 49 ++---- account/views/oj.py | 22 +-- ai/views/oj.py | 147 +++++------------- announcement/models.py | 5 +- announcement/serializers.py | 2 +- announcement/views/admin.py | 4 +- announcement/views/oj.py | 10 +- class_pk/admin.py | 1 - class_pk/models.py | 1 - class_pk/urls/oj.py | 1 - class_pk/views/oj.py | 58 ++----- comment/models.py | 2 - comment/serializers.py | 2 +- comment/views/admin.py | 4 +- comment/views/oj.py | 19 ++- conf/consumers.py | 40 ++--- conf/serializers.py | 2 +- conf/views.py | 29 +--- contest/models.py | 1 - contest/serializers.py | 1 - contest/views/oj.py | 56 ++----- flowchart/consumers.py | 28 ++-- flowchart/models.py | 56 ++++--- flowchart/serializers.py | 2 + flowchart/tasks.py | 54 +++---- flowchart/urls/oj.py | 10 +- flowchart/views.py | 1 - flowchart/views/admin.py | 112 +++++++------ flowchart/views/oj.py | 22 +-- fps/parser.py | 38 +++-- manage.py | 1 + message/models.py | 4 +- message/views/oj.py | 4 +- oj/asgi.py | 1 - oj/routing.py | 1 - options/options.py | 7 +- .../commands/fix_problemset_progress.py | 9 +- problemset/signals.py | 36 ++--- problemset/views/oj.py | 10 +- submission/serializers.py | 17 +- submission/views/admin.py | 31 +--- submission/views/oj.py | 70 ++------- tutorial/__init__.py | 1 - tutorial/urls/__init__.py | 1 - tutorial/views/admin.py | 13 +- tutorial/views/oj.py | 4 +- utils/api/api.py | 13 +- utils/management/commands/inituser.py | 5 +- utils/shortcuts.py | 5 +- utils/throttling.py | 1 + utils/views.py | 35 +---- utils/websocket.py | 56 +++---- utils/xss_filter.py | 93 +++++++---- 56 files changed, 455 insertions(+), 769 deletions(-) diff --git a/account/decorators.py b/account/decorators.py index 6f3e274..89580cf 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -151,7 +151,9 @@ def check_contest_permission(check_type="details"): if error: return error return await func(*args, **kwargs) + return _wrapper + return decorator diff --git a/account/management/commands/clean_deleted_problems.py b/account/management/commands/clean_deleted_problems.py index 81a3f08..ff6469f 100644 --- a/account/management/commands/clean_deleted_problems.py +++ b/account/management/commands/clean_deleted_problems.py @@ -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: diff --git a/account/tasks.py b/account/tasks.py index 9f8f25c..9e2042b 100644 --- a/account/tasks.py +++ b/account/tasks.py @@ -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) diff --git a/account/views/admin.py b/account/views/admin.py index c6134ac..95dd756 100644 --- a/account/views/admin.py +++ b/account/views/admin.py @@ -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) \ No newline at end of file + + return self.success(new_password) diff --git a/account/views/oj.py b/account/views/oj.py index 51ff3b9..4f4616b 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -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) diff --git a/ai/views/oj.py b/ai/views/oj.py index ed95aef..6152b82 100644 --- a/ai/views/oj.py +++ b/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, } ) diff --git a/announcement/models.py b/announcement/models.py index c338b2b..6d7f5c9 100644 --- a/announcement/models.py +++ b/announcement/models.py @@ -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"), ] diff --git a/announcement/serializers.py b/announcement/serializers.py index cc7b0fe..a1f26ed 100644 --- a/announcement/serializers.py +++ b/announcement/serializers.py @@ -25,7 +25,7 @@ class AnnouncementListSerializer(serializers.ModelSerializer): class Meta: model = Announcement - exclude = ['content'] + exclude = ["content"] class EditAnnouncementSerializer(serializers.Serializer): diff --git a/announcement/views/admin.py b/announcement/views/admin.py index 32bbee6..0b38031 100644 --- a/announcement/views/admin.py +++ b/announcement/views/admin.py @@ -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): diff --git a/announcement/views/oj.py b/announcement/views/oj.py index ea9e81e..47e3f70 100644 --- a/announcement/views/oj.py +++ b/announcement/views/oj.py @@ -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)) diff --git a/class_pk/admin.py b/class_pk/admin.py index c3779ed..3d744bc 100644 --- a/class_pk/admin.py +++ b/class_pk/admin.py @@ -1,2 +1 @@ - # Register your models here. diff --git a/class_pk/models.py b/class_pk/models.py index 5be8fa6..2ac5f4f 100644 --- a/class_pk/models.py +++ b/class_pk/models.py @@ -1,3 +1,2 @@ - # 如果需要存储班级PK历史记录,可以在这里定义模型 # 目前暂时不需要,因为都是实时计算 diff --git a/class_pk/urls/oj.py b/class_pk/urls/oj.py index e0de529..8423ee0 100644 --- a/class_pk/urls/oj.py +++ b/class_pk/urls/oj.py @@ -7,4 +7,3 @@ urlpatterns = [ path("user_class_rank", UserClassRankAPI.as_view()), path("class_pk", ClassPKAPI.as_view()), ] - diff --git a/class_pk/views/oj.py b/class_pk/views/oj.py index 4c939a8..c3a1d49 100644 --- a/class_pk/views/oj.py +++ b/class_pk/views/oj.py @@ -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( { diff --git a/comment/models.py b/comment/models.py index f400d2d..b1061ab 100644 --- a/comment/models.py +++ b/comment/models.py @@ -43,5 +43,3 @@ class Comment(models.Model): indexes = [ models.Index(fields=["problem", "create_time"], name="comment_problem_time_idx"), ] - - diff --git a/comment/serializers.py b/comment/serializers.py index c2170ef..240f4f6 100644 --- a/comment/serializers.py +++ b/comment/serializers.py @@ -28,4 +28,4 @@ class CommentListSerializer(serializers.ModelSerializer): class Meta: model = Comment - fields = "__all__" \ No newline at end of file + fields = "__all__" diff --git a/comment/views/admin.py b/comment/views/admin.py index 2dcda9a..d795d9a 100644 --- a/comment/views/admin.py +++ b/comment/views/admin.py @@ -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): diff --git a/comment/views/oj.py b/comment/views/oj.py index cf3b3ce..e24a06c 100644 --- a/comment/views/oj.py +++ b/comment/views/oj.py @@ -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) diff --git a/conf/consumers.py b/conf/consumers.py index 602ab16..27522e2 100644 --- a/conf/consumers.py +++ b/conf/consumers.py @@ -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: diff --git a/conf/serializers.py b/conf/serializers.py index 34a96a3..c63149a 100644 --- a/conf/serializers.py +++ b/conf/serializers.py @@ -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) diff --git a/conf/views.py b/conf/views.py index 08b5b24..2429ca2 100644 --- a/conf/views.py +++ b/conf/views.py @@ -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) diff --git a/contest/models.py b/contest/models.py index 5854174..3b38efb 100644 --- a/contest/models.py +++ b/contest/models.py @@ -75,7 +75,6 @@ class ACMContestRank(AbstractContestRank): ] - class ContestAnnouncement(models.Model): contest = models.ForeignKey(Contest, on_delete=models.CASCADE) title = models.TextField() diff --git a/contest/serializers.py b/contest/serializers.py index dc4c255..5f7c08a 100644 --- a/contest/serializers.py +++ b/contest/serializers.py @@ -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() diff --git a/contest/views/oj.py b/contest/views/oj.py index 62a1b88..ed98f8f 100644 --- a/contest/views/oj.py +++ b/contest/views/oj.py @@ -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) diff --git a/flowchart/consumers.py b/flowchart/consumers.py index 315197f..6a32023 100644 --- a/flowchart/consumers.py +++ b/flowchart/consumers.py @@ -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") diff --git a/flowchart/models.py b/flowchart/models.py index 18ff263..d497b28 100644 --- a/flowchart/models.py +++ b/flowchart/models.py @@ -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 diff --git a/flowchart/serializers.py b/flowchart/serializers.py index f95a25e..336e4d3 100644 --- a/flowchart/serializers.py +++ b/flowchart/serializers.py @@ -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 = [ diff --git a/flowchart/tasks.py b/flowchart/tasks.py index 4758e63..c8c1bd3 100644 --- a/flowchart/tasks.py +++ b/flowchart/tasks.py @@ -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() diff --git a/flowchart/urls/oj.py b/flowchart/urls/oj.py index c77b9e5..6eb7e84 100644 --- a/flowchart/urls/oj.py +++ b/flowchart/urls/oj.py @@ -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()), ] diff --git a/flowchart/views.py b/flowchart/views.py index b8e4ee0..60f00ef 100644 --- a/flowchart/views.py +++ b/flowchart/views.py @@ -1,2 +1 @@ - # Create your views here. diff --git a/flowchart/views/admin.py b/flowchart/views/admin.py index cfa5e27..d0d1880 100644 --- a/flowchart/views/admin.py +++ b/flowchart/views/admin.py @@ -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): diff --git a/flowchart/views/oj.py b/flowchart/views/oj.py index cd3dff2..607403b 100644 --- a/flowchart/views/oj.py +++ b/flowchart/views/oj.py @@ -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}) diff --git a/fps/parser.py b/fps/parser.py index 02cb129..2d86532 100644 --- a/fps/parser.py +++ b/fps/parser.py @@ -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 diff --git a/manage.py b/manage.py index 46a2483..1cd7644 100755 --- a/manage.py +++ b/manage.py @@ -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) diff --git a/message/models.py b/message/models.py index d46f7ca..67ee25a 100644 --- a/message/models.py +++ b/message/models.py @@ -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) diff --git a/message/views/oj.py b/message/views/oj.py index 004c1a1..2bd4779 100644 --- a/message/views/oj.py +++ b/message/views/oj.py @@ -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 diff --git a/oj/asgi.py b/oj/asgi.py index 2c221fc..1a85738 100644 --- a/oj/asgi.py +++ b/oj/asgi.py @@ -28,4 +28,3 @@ application = ProtocolTypeRouter( "websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns)), } ) - diff --git a/oj/routing.py b/oj/routing.py index 466ac0c..09e6c24 100644 --- a/oj/routing.py +++ b/oj/routing.py @@ -13,4 +13,3 @@ websocket_urlpatterns = [ path("ws/config/", ConfigConsumer.as_asgi()), path("ws/flowchart/", FlowchartConsumer.as_asgi()), ] - diff --git a/options/options.py b/options/options.py index fa673a4..27e5691 100644 --- a/options/options.py +++ b/options/options.py @@ -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)() diff --git a/problemset/management/commands/fix_problemset_progress.py b/problemset/management/commands/fix_problemset_progress.py index d6961ab..2378110 100644 --- a/problemset/management/commands/fix_problemset_progress.py +++ b/problemset/management/commands/fix_problemset_progress.py @@ -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 diff --git a/problemset/signals.py b/problemset/signals.py index fe46746..6f09495 100644 --- a/problemset/signals.py +++ b/problemset/signals.py @@ -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}") diff --git a/problemset/views/oj.py b/problemset/views/oj.py index 70b45d6..b79b6b0 100644 --- a/problemset/views/oj.py +++ b/problemset/views/oj.py @@ -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("题单不存在") diff --git a/submission/serializers.py b/submission/serializers.py index 7c0dd3e..386ba95 100644 --- a/submission/serializers.py +++ b/submission/serializers.py @@ -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() ) diff --git a/submission/views/admin.py b/submission/views/admin.py index 65ba85e..928ed93 100644 --- a/submission/views/admin.py +++ b/submission/views/admin.py @@ -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, []), } ) diff --git a/submission/views/oj.py b/submission/views/oj.py index 4c8bb11..e9b92d2 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -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) diff --git a/tutorial/__init__.py b/tutorial/__init__.py index 0519ecb..e69de29 100644 --- a/tutorial/__init__.py +++ b/tutorial/__init__.py @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/tutorial/urls/__init__.py b/tutorial/urls/__init__.py index 0519ecb..e69de29 100644 --- a/tutorial/urls/__init__.py +++ b/tutorial/urls/__init__.py @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/tutorial/views/admin.py b/tutorial/views/admin.py index 72b135c..5078927 100644 --- a/tutorial/views/admin.py +++ b/tutorial/views/admin.py @@ -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 diff --git a/tutorial/views/oj.py b/tutorial/views/oj.py index b02d4cb..df36522 100644 --- a/tutorial/views/oj.py +++ b/tutorial/views/oj.py @@ -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)) diff --git a/utils/api/api.py b/utils/api/api.py index a8a99f1..ba0fc15 100644 --- a/utils/api/api.py +++ b/utils/api/api.py @@ -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 diff --git a/utils/management/commands/inituser.py b/utils/management/commands/inituser.py index e776936..0518f8f 100644 --- a/utils/management/commands/inituser.py +++ b/utils/management/commands/inituser.py @@ -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) diff --git a/utils/shortcuts.py b/utils/shortcuts.py index acb4710..aad9f2a 100644 --- a/utils/shortcuts.py +++ b/utils/shortcuts.py @@ -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): diff --git a/utils/throttling.py b/utils/throttling.py index 9857790..6571b2f 100644 --- a/utils/throttling.py +++ b/utils/throttling.py @@ -5,6 +5,7 @@ class TokenBucket: """ 注意:对于单个key的操作不是线程安全的 """ + def __init__(self, key, capacity, fill_rate, default_capacity, redis_conn): """ :param capacity: 最大容量 diff --git a/utils/views.py b/utils/views.py index 5351f78..51e2c04 100644 --- a/utils/views.py +++ b/utils/views.py @@ -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}) diff --git a/utils/websocket.py b/utils/websocket.py index b049be2..e767b96 100644 --- a/utils/websocket.py +++ b/utils/websocket.py @@ -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)}") diff --git a/utils/xss_filter.py b/utils/xss_filter.py index 7e4a4b0..98de4b7 100644 --- a/utils/xss_filter.py +++ b/utils/xss_filter.py @@ -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('') + self.result.append("") 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__: