345 lines
13 KiB
Python
345 lines
13 KiB
Python
import re
|
||
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 = int(request.GET.get("grade"))
|
||
# 获取所有有用户的班级
|
||
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()
|
||
)
|
||
|
||
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
|
||
|
||
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", "10"))
|
||
except ValueError:
|
||
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
|
||
|
||
# 获取同班所有用户
|
||
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
|
||
|
||
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", [])
|
||
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数 >= 中位数 + 标准差)
|
||
# 使用中位数+标准差方法,既不受极端值影响,又能反映班级差异
|
||
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
|
||
)
|
||
|
||
# 及格率(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),
|
||
}
|
||
)
|