diff --git a/class_pk/views/oj.py b/class_pk/views/oj.py index 1b221e4..de12117 100644 --- a/class_pk/views/oj.py +++ b/class_pk/views/oj.py @@ -1,3 +1,4 @@ +import re import statistics from datetime import datetime from django.db.models import Sum, Avg @@ -10,302 +11,334 @@ from submission.models import Submission, JudgeStatus class ClassRankAPI(APIView): """获取班级排名列表""" + def get(self, request): # 获取年级参数 - grade = request.GET.get("grade") - + grade = int(request.GET.get("grade")) # 获取所有有用户的班级 - classes_query = User.objects.filter( - class_name__isnull=False, - is_disabled=False, - admin_type__in=[AdminType.REGULAR_USER, AdminType.ADMIN] + classes = ( + User.objects.filter( + class_name__isnull=False, + is_disabled=False, + admin_type__in=[AdminType.REGULAR_USER, AdminType.ADMIN], + class_name__startswith=str(grade), + ) + .values("class_name") + .distinct() ) - - # 如果指定了年级,过滤班级名称以该年级开头的班级 - if grade: - try: - grade = int(grade) - # 匹配以年级开头的班级名称,如 "22级"、"22"、"2022级" 等 - classes_query = classes_query.filter( - class_name__startswith=str(grade) - ) - except ValueError: - pass - - classes = classes_query.values('class_name').distinct() - + class_stats = [] for class_info in classes: - class_name = class_info['class_name'] + class_name = class_info["class_name"] users = User.objects.filter( class_name=class_name, is_disabled=False, - admin_type__in=[AdminType.REGULAR_USER, AdminType.ADMIN] + admin_type__in=[AdminType.REGULAR_USER, AdminType.ADMIN], ) - user_ids = list(users.values_list('id', flat=True)) - + user_ids = list(users.values_list("id", flat=True)) + 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 - avg_ac = profiles.aggregate( - avg=Avg('accepted_number') - )['avg'] or 0 - + + total_ac = profiles.aggregate(total=Sum("accepted_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() - - class_stats.append({ - 'class_name': class_name, - 'user_count': user_count, - '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 - }) - + + class_stats.append( + { + "class_name": class_name, + "user_count": user_count, + "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数排序 - class_stats.sort(key=lambda x: (-x['total_ac'], x['total_submission'])) - + class_stats.sort(key=lambda x: (-x["total_ac"], x["total_submission"])) + # 添加排名 for i, stat in enumerate(class_stats): - stat['rank'] = i + 1 - - # 手动实现分页(因为class_stats是list,不是QuerySet) + stat["rank"] = i + 1 + + return self.success(class_stats) + + +class UserClassRankAPI(APIView): + """获取用户在班级中的排名""" + + @login_required + def get(self, request): + user = request.user + if not user.class_name: + return self.error("用户没有班级信息") + scope = request.GET.get("scope", "").lower() + show_all = scope == "all" try: - limit = int(request.GET.get("limit", "20")) + limit = int(request.GET.get("limit", "10")) except ValueError: - limit = 20 - if limit < 0 or limit > 250: - limit = 20 + limit = 10 + if limit <= 0 or limit > 250: + limit = 10 try: offset = int(request.GET.get("offset", "0")) except ValueError: offset = 0 if offset < 0: offset = 0 - - total = len(class_stats) - results = class_stats[offset:offset + limit] - - return self.success({ - "results": results, - "total": total - }) - -class UserClassRankAPI(APIView): - """获取用户在班级中的排名""" - @login_required - def get(self, request): - user = request.user - if not user.class_name: - return self.error("用户没有班级信息") - # 获取同班所有用户 class_users = User.objects.filter( class_name=user.class_name, is_disabled=False, - admin_type__in=[AdminType.REGULAR_USER, AdminType.ADMIN] - ).select_related('userprofile') - + admin_type__in=[AdminType.REGULAR_USER, AdminType.ADMIN], + ).select_related("userprofile") + user_ranks = [] for class_user in class_users: profile = class_user.userprofile - user_ranks.append({ - 'user_id': class_user.id, - 'username': class_user.username, - 'accepted_number': profile.accepted_number, - 'submission_number': profile.submission_number, - }) - + user_ranks.append( + { + "user_id": class_user.id, + "username": class_user.username, + "accepted_number": profile.accepted_number, + "submission_number": profile.submission_number, + } + ) + # 按AC数排序 - user_ranks.sort(key=lambda x: (-x['accepted_number'], x['submission_number'])) - + user_ranks.sort(key=lambda x: (-x["accepted_number"], x["submission_number"])) + # 添加排名 my_rank = -1 for i, rank_info in enumerate(user_ranks): - rank_info['rank'] = i + 1 - if rank_info['user_id'] == user.id: + rank_info["rank"] = i + 1 + if rank_info["user_id"] == user.id: my_rank = i + 1 - - return self.success({ - 'class_name': user.class_name, - 'my_rank': my_rank, - 'ranks': user_ranks, - }) + + trimmed_ranks = user_ranks + if not show_all and my_rank > 0 and len(user_ranks) > 10: + center_index = my_rank - 1 + start = max(0, center_index - 5) + end = start + 10 + if end > len(user_ranks): + end = len(user_ranks) + start = max(0, end - 10) + trimmed_ranks = user_ranks[start:end] + elif show_all: + trimmed_ranks = user_ranks[offset : offset + limit] + + return self.success( + { + "class_name": user.class_name, + "my_rank": my_rank, + "total": len(user_ranks), + "ranks": trimmed_ranks, + } + ) class ClassPKAPI(APIView): """班级PK比较 - 多维度教育评价""" + def post(self, request): - class_names = request.data.get('class_name', []) + class_names = request.data.get("class_name", []) if not class_names or len(class_names) < 2: return self.error("至少需要选择2个班级进行比较") - + # 获取时间段参数 start_time = request.data.get("start_time") end_time = request.data.get("end_time") - + # 将时间字符串转换为datetime对象 # 处理空字符串、None 或 undefined 的情况 if start_time and isinstance(start_time, str) and start_time.strip(): try: - start_time = datetime.fromisoformat(start_time.replace('Z', '+00:00')) + start_time = datetime.fromisoformat(start_time.replace("Z", "+00:00")) if timezone.is_naive(start_time): start_time = timezone.make_aware(start_time) except (ValueError, AttributeError): start_time = None else: start_time = None - + if end_time and isinstance(end_time, str) and end_time.strip(): try: - end_time = datetime.fromisoformat(end_time.replace('Z', '+00:00')) + end_time = datetime.fromisoformat(end_time.replace("Z", "+00:00")) if timezone.is_naive(end_time): end_time = timezone.make_aware(end_time) except (ValueError, AttributeError): end_time = None else: end_time = None - + class_comparisons = [] - + for class_name in class_names: users = User.objects.filter( class_name=class_name, is_disabled=False, - admin_type__in=[AdminType.REGULAR_USER, AdminType.ADMIN] + admin_type__in=[AdminType.REGULAR_USER, AdminType.ADMIN], ) - user_ids = list(users.values_list('id', flat=True)) - + user_ids = list(users.values_list("id", flat=True)) + # 获取所有学生的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: continue - + # 基础统计 total_ac = sum(ac_list) total_submission = sum(submission_list) avg_ac = statistics.mean(ac_list) if ac_list else 0 - + # 中位数和分位数 median_ac = statistics.median(ac_list) if ac_list else 0 q1_ac = statistics.quantiles(ac_list, n=4)[0] if len(ac_list) > 1 else 0 q3_ac = statistics.quantiles(ac_list, n=4)[2] if len(ac_list) > 1 else 0 iqr = q3_ac - q1_ac - + # 标准差 std_dev = statistics.stdev(ac_list) if len(ac_list) > 1 else 0 - + # 前10名和后10名统计 top_10_count = min(10, user_count) bottom_10_count = min(10, user_count) - 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 + ) + # 前25%和后25%统计 top_25_count = max(1, user_count // 4) bottom_25_count = max(1, user_count // 4) - top_25_avg = statistics.mean(ac_list[:top_25_count]) if top_25_count > 0 else 0 - bottom_25_avg = statistics.mean(ac_list[-bottom_25_count:]) if bottom_25_count > 0 else 0 - + top_25_avg = ( + statistics.mean(ac_list[:top_25_count]) if top_25_count > 0 else 0 + ) + bottom_25_avg = ( + statistics.mean(ac_list[-bottom_25_count:]) + if bottom_25_count > 0 + else 0 + ) + # 优秀率(AC数 >= 中位数 + 标准差) # 使用中位数+标准差方法,既不受极端值影响,又能反映班级差异 - excellent_threshold = median_ac + std_dev if std_dev > 0 else median_ac * 1.5 + excellent_threshold = ( + median_ac + std_dev if std_dev > 0 else median_ac * 1.5 + ) excellent_count = sum(1 for ac in ac_list if ac >= excellent_threshold) - 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数 >= 平均值的0.5倍) pass_threshold = avg_ac * 0.5 pass_count = sum(1 for ac in ac_list if ac >= pass_threshold) pass_rate = (pass_count / user_count * 100) if user_count > 0 else 0 - + # 参与度(有提交记录的学生比例) active_count = sum(1 for sub in submission_list if sub > 0) active_rate = (active_count / user_count * 100) if user_count > 0 else 0 - + # 时间段内的统计(如果提供了时间段) recent_stats = {} if start_time and end_time: submissions = Submission.objects.filter( user_id__in=user_ids, create_time__gte=start_time, - create_time__lte=end_time + create_time__lte=end_time, + ) + recent_ac = ( + submissions.filter(result=JudgeStatus.ACCEPTED) + .values("user_id", "problem_id") + .distinct() + .count() ) - recent_ac = submissions.filter( - result=JudgeStatus.ACCEPTED - ).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=JudgeStatus.ACCEPTED - ).values('problem_id').distinct().count() + user_recent_ac = ( + submissions.filter(user_id=user_id, result=JudgeStatus.ACCEPTED) + .values("problem_id") + .distinct() + .count() + ) recent_user_ac[user_id] = user_recent_ac - + recent_ac_list = sorted(recent_user_ac.values(), reverse=True) if recent_ac_list: recent_stats = { - 'recent_total_ac': recent_ac, - '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[:min(10, len(recent_ac_list))]) if recent_ac_list else 0, - 'recent_active_count': sum(1 for ac in recent_ac_list if ac > 0), + "recent_total_ac": recent_ac, + "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[: min(10, len(recent_ac_list))] + ) + if recent_ac_list + else 0, + "recent_active_count": sum( + 1 for ac in recent_ac_list if ac > 0 + ), } - - class_comparisons.append({ - 'class_name': class_name, - 'user_count': user_count, - - # 基础统计 - 'total_ac': int(total_ac), - 'total_submission': int(total_submission), - 'avg_ac': round(avg_ac, 2), - - # 中位数和分位数 - 'median_ac': round(median_ac, 2), - 'q1_ac': round(q1_ac, 2), - 'q3_ac': round(q3_ac, 2), - 'iqr': round(iqr, 2), - - # 标准差 - 'std_dev': round(std_dev, 2), - - # 分层统计 - 'top_10_avg': round(top_10_avg, 2), - 'bottom_10_avg': round(bottom_10_avg, 2), - 'top_25_avg': round(top_25_avg, 2), - 'bottom_25_avg': round(bottom_25_avg, 2), - - # 比率统计 - 'excellent_rate': round(excellent_rate, 2), - '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, - - # 时间段统计(如果有) - **recent_stats, - }) - - # 按总AC数排序 - class_comparisons.sort(key=lambda x: (-x['total_ac'], x['total_submission'])) - - return self.success({ - 'comparisons': class_comparisons, - 'has_time_range': bool(start_time and end_time) - }) + class_comparisons.append( + { + "class_name": class_name, + "user_count": user_count, + # 基础统计 + "total_ac": int(total_ac), + "total_submission": int(total_submission), + "avg_ac": round(avg_ac, 2), + # 中位数和分位数 + "median_ac": round(median_ac, 2), + "q1_ac": round(q1_ac, 2), + "q3_ac": round(q3_ac, 2), + "iqr": round(iqr, 2), + # 标准差 + "std_dev": round(std_dev, 2), + # 分层统计 + "top_10_avg": round(top_10_avg, 2), + "bottom_10_avg": round(bottom_10_avg, 2), + "top_25_avg": round(top_25_avg, 2), + "bottom_25_avg": round(bottom_25_avg, 2), + # 比率统计 + "excellent_rate": round(excellent_rate, 2), + "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, + # 时间段统计(如果有) + **recent_stats, + } + ) + + # 按总AC数排序 + class_comparisons.sort(key=lambda x: (-x["total_ac"], x["total_submission"])) + + return self.success( + { + "comparisons": class_comparisons, + "has_time_range": bool(start_time and end_time), + } + )