Files
OnlineJudge/class_pk/views/oj.py
2026-01-03 21:52:02 +08:00

312 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import statistics
from datetime import datetime
from django.db.models import Sum, Avg
from django.utils import timezone
from utils.api import APIView
from account.decorators import login_required
from account.models import User, UserProfile, AdminType
from submission.models import Submission, JudgeStatus
class ClassRankAPI(APIView):
"""获取班级排名列表"""
def get(self, request):
# 获取年级参数
grade = request.GET.get("grade")
# 获取所有有用户的班级
classes_query = User.objects.filter(
class_name__isnull=False,
is_disabled=False,
admin_type__in=[AdminType.REGULAR_USER, AdminType.ADMIN]
)
# 如果指定了年级,过滤班级名称以该年级开头的班级
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']
users = User.objects.filter(
class_name=class_name,
is_disabled=False,
admin_type__in=[AdminType.REGULAR_USER, AdminType.ADMIN]
)
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
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
})
# 按总AC数排序
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
try:
limit = int(request.GET.get("limit", "20"))
except ValueError:
limit = 20
if limit < 0 or limit > 250:
limit = 20
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')
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,
})
# 按AC数排序
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:
my_rank = i + 1
return self.success({
'class_name': user.class_name,
'my_rank': my_rank,
'total_users': len(user_ranks),
'ranks': self.paginate_data(request, user_ranks, None)
})
class ClassPKAPI(APIView):
"""班级PK比较 - 多维度教育评价"""
def post(self, request):
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'))
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'))
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]
)
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)
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
# 前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
# 优秀率AC数 >= 平均值的1.5倍)
excellent_threshold = avg_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
# 及格率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
)
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()
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),
}
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)
})