style: ruff format 全仓库
行宽 180 下把历史遗留的折行表达式合并,无语义改动。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -151,7 +151,9 @@ def check_contest_permission(check_type="details"):
|
|||||||
if error:
|
if error:
|
||||||
return error
|
return error
|
||||||
return await func(*args, **kwargs)
|
return await func(*args, **kwargs)
|
||||||
|
|
||||||
return _wrapper
|
return _wrapper
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,14 +17,10 @@ class Command(BaseCommand):
|
|||||||
dry_run = options["dry_run"]
|
dry_run = options["dry_run"]
|
||||||
|
|
||||||
# 所有现存非比赛题目的 PK 集合
|
# 所有现存非比赛题目的 PK 集合
|
||||||
existing_ids = set(
|
existing_ids = set(Problem.objects.filter(contest__isnull=True).values_list("id", flat=True))
|
||||||
Problem.objects.filter(contest__isnull=True).values_list("id", flat=True)
|
|
||||||
)
|
|
||||||
self.stdout.write(f"现存题库题目数: {len(existing_ids)}")
|
self.stdout.write(f"现存题库题目数: {len(existing_ids)}")
|
||||||
|
|
||||||
profiles = UserProfile.objects.select_related("user").exclude(
|
profiles = UserProfile.objects.select_related("user").exclude(acm_problems_status={})
|
||||||
acm_problems_status={}
|
|
||||||
)
|
|
||||||
total = profiles.count()
|
total = profiles.count()
|
||||||
self.stdout.write(f"检查用户数: {total}{'(dry-run 模式)' if dry_run else ''}")
|
self.stdout.write(f"检查用户数: {total}{'(dry-run 模式)' if dry_run else ''}")
|
||||||
|
|
||||||
@@ -38,17 +34,11 @@ class Command(BaseCommand):
|
|||||||
if not stale_keys:
|
if not stale_keys:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
removed_accepted = sum(
|
removed_accepted = sum(1 for k in stale_keys if problems[k].get("status") in ACCEPTED_STATUSES)
|
||||||
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]
|
stale_display = [problems[k].get("_id", k) for k in stale_keys]
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
f" 用户 {profile.user.username}"
|
f" 用户 {profile.user.username} | 删除 {len(stale_keys)} 题: {', '.join(stale_display)}{f' | 其中已AC {removed_accepted} 题' if removed_accepted else ''}"
|
||||||
f" | 删除 {len(stale_keys)} 题: {', '.join(stale_display)}"
|
|
||||||
f"{f' | 其中已AC {removed_accepted} 题' if removed_accepted else ''}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
|
|||||||
@@ -13,11 +13,6 @@ def send_email_async(from_name, to_email, to_name, subject, content):
|
|||||||
if not SysOptions.smtp_config:
|
if not SysOptions.smtp_config:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
send_email(smtp_config=SysOptions.smtp_config,
|
send_email(smtp_config=SysOptions.smtp_config, from_name=from_name, to_email=to_email, to_name=to_name, subject=subject, content=content)
|
||||||
from_name=from_name,
|
|
||||||
to_email=to_email,
|
|
||||||
to_name=to_name,
|
|
||||||
subject=subject,
|
|
||||||
content=content)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
|
|||||||
@@ -61,12 +61,7 @@ class UserAdminAPI(APIView):
|
|||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
ret = User.objects.bulk_create(user_list)
|
ret = User.objects.bulk_create(user_list)
|
||||||
UserProfile.objects.bulk_create(
|
UserProfile.objects.bulk_create([UserProfile(user=ret[i], real_name=data[i][3]) for i in range(len(ret))])
|
||||||
[
|
|
||||||
UserProfile(user=ret[i], real_name=data[i][3])
|
|
||||||
for i in range(len(ret))
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return self.success()
|
return self.success()
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
# Extract detail from exception message
|
# Extract detail from exception message
|
||||||
@@ -85,17 +80,9 @@ class UserAdminAPI(APIView):
|
|||||||
user = User.objects.get(id=data["id"])
|
user = User.objects.get(id=data["id"])
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
return self.error("User does not exist")
|
return self.error("User does not exist")
|
||||||
if (
|
if User.objects.filter(username=data["username"].lower()).exclude(id=user.id).exists():
|
||||||
User.objects.filter(username=data["username"].lower())
|
|
||||||
.exclude(id=user.id)
|
|
||||||
.exists()
|
|
||||||
):
|
|
||||||
return self.error("Username already exists")
|
return self.error("Username already exists")
|
||||||
if (
|
if User.objects.filter(email=data["email"].lower()).exclude(id=user.id).exists():
|
||||||
User.objects.filter(email=data["email"].lower())
|
|
||||||
.exclude(id=user.id)
|
|
||||||
.exists()
|
|
||||||
):
|
|
||||||
return self.error("Email already exists")
|
return self.error("Email already exists")
|
||||||
|
|
||||||
pre_username = user.username
|
pre_username = user.username
|
||||||
@@ -136,9 +123,7 @@ class UserAdminAPI(APIView):
|
|||||||
|
|
||||||
user.save()
|
user.save()
|
||||||
if pre_username != user.username:
|
if pre_username != user.username:
|
||||||
Submission.objects.filter(username=pre_username).update(
|
Submission.objects.filter(username=pre_username).update(username=user.username)
|
||||||
username=user.username
|
|
||||||
)
|
|
||||||
|
|
||||||
UserProfile.objects.filter(user=user).update(real_name=data["real_name"])
|
UserProfile.objects.filter(user=user).update(real_name=data["real_name"])
|
||||||
return self.success(UserAdminSerializer(user).data)
|
return self.success(UserAdminSerializer(user).data)
|
||||||
@@ -158,7 +143,7 @@ class UserAdminAPI(APIView):
|
|||||||
|
|
||||||
# 获取排序参数
|
# 获取排序参数
|
||||||
order_by = request.GET.get("order_by", "")
|
order_by = request.GET.get("order_by", "")
|
||||||
|
|
||||||
# 根据排序参数设置排序规则
|
# 根据排序参数设置排序规则
|
||||||
if order_by == "-last_login":
|
if order_by == "-last_login":
|
||||||
# 最近登录,将 None 值放在最后
|
# 最近登录,将 None 值放在最后
|
||||||
@@ -174,11 +159,7 @@ class UserAdminAPI(APIView):
|
|||||||
|
|
||||||
keyword = request.GET.get("keyword", None)
|
keyword = request.GET.get("keyword", None)
|
||||||
if keyword:
|
if keyword:
|
||||||
user = user.filter(
|
user = user.filter(Q(username__icontains=keyword) | Q(userprofile__real_name__icontains=keyword) | Q(email__icontains=keyword))
|
||||||
Q(username__icontains=keyword)
|
|
||||||
| Q(userprofile__real_name__icontains=keyword)
|
|
||||||
| Q(email__icontains=keyword)
|
|
||||||
)
|
|
||||||
return self.success(self.paginate_data(request, user, UserAdminSerializer))
|
return self.success(self.paginate_data(request, user, UserAdminSerializer))
|
||||||
|
|
||||||
@super_admin_required
|
@super_admin_required
|
||||||
@@ -223,9 +204,7 @@ class GenerateUserAPI(APIView):
|
|||||||
Generate User
|
Generate User
|
||||||
"""
|
"""
|
||||||
data = request.data
|
data = request.data
|
||||||
number_max_length = max(
|
number_max_length = max(len(str(data["number_from"])), len(str(data["number_to"])))
|
||||||
len(str(data["number_from"])), len(str(data["number_to"]))
|
|
||||||
)
|
|
||||||
if number_max_length + len(data["prefix"]) + len(data["suffix"]) > 32:
|
if number_max_length + len(data["prefix"]) + len(data["suffix"]) > 32:
|
||||||
return self.error("Username should not more than 32 characters")
|
return self.error("Username should not more than 32 characters")
|
||||||
if data["number_from"] > data["number_to"]:
|
if data["number_from"] > data["number_to"]:
|
||||||
@@ -253,9 +232,7 @@ class GenerateUserAPI(APIView):
|
|||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
ret = User.objects.bulk_create(user_list)
|
ret = User.objects.bulk_create(user_list)
|
||||||
UserProfile.objects.bulk_create(
|
UserProfile.objects.bulk_create([UserProfile(user=user) for user in ret])
|
||||||
[UserProfile(user=user) for user in ret]
|
|
||||||
)
|
|
||||||
for item in user_list:
|
for item in user_list:
|
||||||
worksheet.write_string(i, 0, item.username)
|
worksheet.write_string(i, 0, item.username)
|
||||||
worksheet.write_string(i, 1, item.raw_password)
|
worksheet.write_string(i, 1, item.raw_password)
|
||||||
@@ -277,17 +254,17 @@ class ResetUserPasswordAPI(APIView):
|
|||||||
"""
|
"""
|
||||||
data = request.data
|
data = request.data
|
||||||
user_id = data["id"]
|
user_id = data["id"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = User.objects.get(id=user_id)
|
user = User.objects.get(id=user_id)
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
return self.error("User does not exist")
|
return self.error("User does not exist")
|
||||||
|
|
||||||
# 生成6位随机数字密码(不包括0)
|
# 生成6位随机数字密码(不包括0)
|
||||||
new_password = get_random_string(6, allowed_chars="123456789")
|
new_password = get_random_string(6, allowed_chars="123456789")
|
||||||
|
|
||||||
# 设置新密码
|
# 设置新密码
|
||||||
user.set_password(new_password)
|
user.set_password(new_password)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
return self.success(new_password)
|
return self.success(new_password)
|
||||||
|
|||||||
@@ -431,11 +431,16 @@ class UserRankAPI(AsyncAPIView):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
n = 0
|
n = 0
|
||||||
|
|
||||||
profiles = UserProfile.objects.filter(
|
profiles = (
|
||||||
user__admin_type__in=[AdminType.REGULAR_USER, AdminType.STUDENT_ADMIN],
|
UserProfile.objects.filter(
|
||||||
user__is_disabled=False,
|
user__admin_type__in=[AdminType.REGULAR_USER, AdminType.STUDENT_ADMIN],
|
||||||
user__username__icontains=username,
|
user__is_disabled=False,
|
||||||
).select_related("user").filter(accepted_number__gte=0).order_by("-accepted_number", "submission_number")
|
user__username__icontains=username,
|
||||||
|
)
|
||||||
|
.select_related("user")
|
||||||
|
.filter(accepted_number__gte=0)
|
||||||
|
.order_by("-accepted_number", "submission_number")
|
||||||
|
)
|
||||||
if n > 0:
|
if n > 0:
|
||||||
profiles = profiles[:n]
|
profiles = profiles[:n]
|
||||||
return self.success(await self.async_paginate_data(request, profiles, RankInfoSerializer))
|
return self.success(await self.async_paginate_data(request, profiles, RankInfoSerializer))
|
||||||
@@ -457,12 +462,7 @@ class UserActivityRankAPI(AsyncAPIView):
|
|||||||
create_time__gte=start,
|
create_time__gte=start,
|
||||||
result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED],
|
result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED],
|
||||||
).exclude(username__in=hidden_names)
|
).exclude(username__in=hidden_names)
|
||||||
data = [
|
data = [row async for row in submissions.values("username").annotate(count=Count("problem_id", distinct=True)).order_by("-count")[:10]]
|
||||||
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)
|
await async_cache_set(cache_key, data, 600)
|
||||||
return self.success(data)
|
return self.success(data)
|
||||||
|
|
||||||
|
|||||||
147
ai/views/oj.py
147
ai/views/oj.py
@@ -114,16 +114,12 @@ def get_class_user_ids(user):
|
|||||||
cache_key = get_cache_key("class_users", user.class_name)
|
cache_key = get_cache_key("class_users", user.class_name)
|
||||||
user_ids = cache.get(cache_key)
|
user_ids = cache.get(cache_key)
|
||||||
if user_ids is None:
|
if user_ids is None:
|
||||||
user_ids = list(
|
user_ids = list(User.objects.filter(class_name=user.class_name).values_list("id", flat=True))
|
||||||
User.objects.filter(class_name=user.class_name).values_list("id", flat=True)
|
|
||||||
)
|
|
||||||
cache.set(cache_key, user_ids, CACHE_TIMEOUT)
|
cache.set(cache_key, user_ids, CACHE_TIMEOUT)
|
||||||
return user_ids
|
return user_ids
|
||||||
|
|
||||||
|
|
||||||
def get_user_first_ac_submissions(
|
def get_user_first_ac_submissions(user_id, start, end, class_user_ids=None, use_class_scope=False, include_all_time=True):
|
||||||
user_id, start, end, class_user_ids=None, use_class_scope=False, include_all_time=True
|
|
||||||
):
|
|
||||||
# 用户自己的 AC 记录按时间范围过滤
|
# 用户自己的 AC 记录按时间范围过滤
|
||||||
user_first_ac = list(
|
user_first_ac = list(
|
||||||
Submission.objects.filter(
|
Submission.objects.filter(
|
||||||
@@ -151,9 +147,7 @@ def get_user_first_ac_submissions(
|
|||||||
if use_class_scope and class_user_ids:
|
if use_class_scope and class_user_ids:
|
||||||
rank_qs = rank_qs.filter(user_id__in=class_user_ids)
|
rank_qs = rank_qs.filter(user_id__in=class_user_ids)
|
||||||
|
|
||||||
ranked_first_ac = list(
|
ranked_first_ac = list(rank_qs.values("user_id", "problem_id").annotate(first_ac_time=Min("create_time")))
|
||||||
rank_qs.values("user_id", "problem_id").annotate(first_ac_time=Min("create_time"))
|
|
||||||
)
|
|
||||||
|
|
||||||
by_problem = defaultdict(list)
|
by_problem = defaultdict(list)
|
||||||
for item in ranked_first_ac:
|
for item in ranked_first_ac:
|
||||||
@@ -241,18 +235,14 @@ class AIDetailDataAPI(APIView):
|
|||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
return self.error("User not found")
|
return self.error("User not found")
|
||||||
|
|
||||||
cache_key = get_cache_key(
|
cache_key = get_cache_key("ai_detail", user.id, user.class_name or "", start, end)
|
||||||
"ai_detail", user.id, user.class_name or "", start, end
|
|
||||||
)
|
|
||||||
cached_result = cache.get(cache_key)
|
cached_result = cache.get(cache_key)
|
||||||
if cached_result:
|
if cached_result:
|
||||||
return self.success(cached_result)
|
return self.success(cached_result)
|
||||||
|
|
||||||
class_user_ids = get_class_user_ids(user)
|
class_user_ids = get_class_user_ids(user)
|
||||||
use_class_scope = bool(user.class_name) and len(class_user_ids) > 1
|
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_first_ac, by_problem, problem_ids = get_user_first_ac_submissions(user.id, start, end, class_user_ids, use_class_scope)
|
||||||
user.id, start, end, class_user_ids, use_class_scope
|
|
||||||
)
|
|
||||||
|
|
||||||
# 同期排名:只统计时间窗口内解题的人
|
# 同期排名:只统计时间窗口内解题的人
|
||||||
by_problem_period = defaultdict(list)
|
by_problem_period = defaultdict(list)
|
||||||
@@ -265,9 +255,7 @@ class AIDetailDataAPI(APIView):
|
|||||||
)
|
)
|
||||||
if use_class_scope and class_user_ids:
|
if use_class_scope and class_user_ids:
|
||||||
period_qs = period_qs.filter(user_id__in=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(
|
for item in period_qs.values("user_id", "problem_id").annotate(first_ac_time=Min("create_time")):
|
||||||
first_ac_time=Min("create_time")
|
|
||||||
):
|
|
||||||
by_problem_period[item["problem_id"]].append(item)
|
by_problem_period[item["problem_id"]].append(item)
|
||||||
for lst in by_problem_period.values():
|
for lst in by_problem_period.values():
|
||||||
lst.sort(key=lambda x: (x["first_ac_time"], x["user_id"]))
|
lst.sort(key=lambda x: (x["first_ac_time"], x["user_id"]))
|
||||||
@@ -286,15 +274,8 @@ class AIDetailDataAPI(APIView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if user_first_ac:
|
if user_first_ac:
|
||||||
problems = {
|
problems = {p.id: p for p in Problem.objects.filter(id__in=problem_ids).select_related("contest").prefetch_related("tags")}
|
||||||
p.id: p
|
solved, contest_ids = self._build_solved_records(user_first_ac, by_problem, by_problem_period, problems, user.id)
|
||||||
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
|
# 查找 flowchart submissions
|
||||||
flowcharts_query = FlowchartSubmission.objects.filter(
|
flowcharts_query = FlowchartSubmission.objects.filter(
|
||||||
user_id=user,
|
user_id=user,
|
||||||
@@ -336,9 +317,7 @@ class AIDetailDataAPI(APIView):
|
|||||||
|
|
||||||
# 找到最高分和对应的等级
|
# 找到最高分和对应的等级
|
||||||
best_score = max(scores) if scores else 0
|
best_score = max(scores) if scores else 0
|
||||||
best_submission = next(
|
best_submission = next((s for s in submissions if s.ai_score == best_score), submissions[0])
|
||||||
(s for s in submissions if s.ai_score == best_score), submissions[0]
|
|
||||||
)
|
|
||||||
best_grade = best_submission.ai_grade or ""
|
best_grade = best_submission.ai_grade or ""
|
||||||
|
|
||||||
# 计算平均分
|
# 计算平均分
|
||||||
@@ -360,9 +339,7 @@ class AIDetailDataAPI(APIView):
|
|||||||
flowcharts_data.append(merged_item)
|
flowcharts_data.append(merged_item)
|
||||||
|
|
||||||
# 按最新提交时间排序
|
# 按最新提交时间排序
|
||||||
flowcharts_data.sort(
|
flowcharts_data.sort(key=lambda x: x["latest_submission_time"] or "", reverse=True)
|
||||||
key=lambda x: x["latest_submission_time"] or "", reverse=True
|
|
||||||
)
|
|
||||||
|
|
||||||
result.update(
|
result.update(
|
||||||
{
|
{
|
||||||
@@ -370,9 +347,7 @@ class AIDetailDataAPI(APIView):
|
|||||||
"flowcharts": flowcharts_data,
|
"flowcharts": flowcharts_data,
|
||||||
"grade": calculate_average_grade([s["grade"] for s in solved]),
|
"grade": calculate_average_grade([s["grade"] for s in solved]),
|
||||||
"tags": self._calculate_top_tags(problems.values()),
|
"tags": self._calculate_top_tags(problems.values()),
|
||||||
"difficulty": self._calculate_difficulty_distribution(
|
"difficulty": self._calculate_difficulty_distribution(problems.values()),
|
||||||
problems.values()
|
|
||||||
),
|
|
||||||
"contest_count": len(set(contest_ids)),
|
"contest_count": len(set(contest_ids)),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -428,13 +403,8 @@ class AIDetailDataAPI(APIView):
|
|||||||
def _calculate_difficulty_distribution(self, problems):
|
def _calculate_difficulty_distribution(self, problems):
|
||||||
diff_counter = {"Low": 0, "Mid": 0, "High": 0}
|
diff_counter = {"Low": 0, "Mid": 0, "High": 0}
|
||||||
for problem in problems:
|
for problem in problems:
|
||||||
diff_counter[
|
diff_counter[problem.difficulty if problem.difficulty in diff_counter else "Mid"] += 1
|
||||||
problem.difficulty if problem.difficulty in diff_counter else "Mid"
|
return {get_difficulty(k): v for k, v in sorted(diff_counter.items(), key=lambda x: x[1], reverse=True)}
|
||||||
] += 1
|
|
||||||
return {
|
|
||||||
get_difficulty(k): v
|
|
||||||
for k, v in sorted(diff_counter.items(), key=lambda x: x[1], reverse=True)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class AIDurationDataAPI(APIView):
|
class AIDurationDataAPI(APIView):
|
||||||
@@ -451,9 +421,7 @@ class AIDurationDataAPI(APIView):
|
|||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
return self.error("User not found")
|
return self.error("User not found")
|
||||||
|
|
||||||
cache_key = get_cache_key(
|
cache_key = get_cache_key("ai_duration", user.id, user.class_name or "", end_iso, duration)
|
||||||
"ai_duration", user.id, user.class_name or "", end_iso, duration
|
|
||||||
)
|
|
||||||
cached_result = cache.get(cache_key)
|
cached_result = cache.get(cache_key)
|
||||||
if cached_result:
|
if cached_result:
|
||||||
return self.success(cached_result)
|
return self.success(cached_result)
|
||||||
@@ -468,9 +436,7 @@ class AIDurationDataAPI(APIView):
|
|||||||
start = start + time_config["delta"]
|
start = start + time_config["delta"]
|
||||||
period_end = start + time_config["delta"]
|
period_end = start + time_config["delta"]
|
||||||
|
|
||||||
submission_count = Submission.objects.filter(
|
submission_count = Submission.objects.filter(user_id=user.id, create_time__gte=start, create_time__lte=period_end).count()
|
||||||
user_id=user.id, create_time__gte=start, create_time__lte=period_end
|
|
||||||
).count()
|
|
||||||
|
|
||||||
period_data = {
|
period_data = {
|
||||||
"unit": time_config["show_unit"],
|
"unit": time_config["show_unit"],
|
||||||
@@ -502,9 +468,7 @@ class AIDurationDataAPI(APIView):
|
|||||||
)
|
)
|
||||||
if use_class_scope and class_user_ids:
|
if use_class_scope and class_user_ids:
|
||||||
period_qs = period_qs.filter(user_id__in=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(
|
for row in period_qs.values("user_id", "problem_id").annotate(first_ac_time=Min("create_time")):
|
||||||
first_ac_time=Min("create_time")
|
|
||||||
):
|
|
||||||
by_problem_period[row["problem_id"]].append(row)
|
by_problem_period[row["problem_id"]].append(row)
|
||||||
for lst in by_problem_period.values():
|
for lst in by_problem_period.values():
|
||||||
lst.sort(key=lambda x: (x["first_ac_time"], x["user_id"]))
|
lst.sort(key=lambda x: (x["first_ac_time"], x["user_id"]))
|
||||||
@@ -558,7 +522,6 @@ class AIDurationDataAPI(APIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AILoginSummaryAPI(APIView):
|
class AILoginSummaryAPI(APIView):
|
||||||
@login_required
|
@login_required
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
@@ -574,20 +537,11 @@ class AILoginSummaryAPI(APIView):
|
|||||||
)
|
)
|
||||||
new_problem_count = problems_qs.count()
|
new_problem_count = problems_qs.count()
|
||||||
|
|
||||||
submissions_qs = Submission.objects.filter(
|
submissions_qs = Submission.objects.filter(user_id=user.id, create_time__gte=start_time, create_time__lte=end_time)
|
||||||
user_id=user.id, create_time__gte=start_time, create_time__lte=end_time
|
|
||||||
)
|
|
||||||
submission_count = submissions_qs.count()
|
submission_count = submissions_qs.count()
|
||||||
accepted_count = submissions_qs.filter(result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED]).count()
|
accepted_count = submissions_qs.filter(result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED]).count()
|
||||||
solved_count = (
|
solved_count = submissions_qs.filter(result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED]).values("problem_id").distinct().count()
|
||||||
submissions_qs.filter(result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED])
|
flowchart_submission_count = FlowchartSubmission.objects.filter(user_id=user.id, create_time__gte=start_time, create_time__lte=end_time).count()
|
||||||
.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 = {
|
summary = {
|
||||||
"start": datetime2str(start_time),
|
"start": datetime2str(start_time),
|
||||||
@@ -614,9 +568,7 @@ class AILoginSummaryAPI(APIView):
|
|||||||
start_time = parse_datetime(start_raw) if start_raw else None
|
start_time = parse_datetime(start_raw) if start_raw else None
|
||||||
|
|
||||||
if start_time and timezone.is_naive(start_time):
|
if start_time and timezone.is_naive(start_time):
|
||||||
start_time = timezone.make_aware(
|
start_time = timezone.make_aware(start_time, timezone.get_current_timezone())
|
||||||
start_time, timezone.get_current_timezone()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not start_time:
|
if not start_time:
|
||||||
if user.last_login and user.last_login < end_time:
|
if user.last_login and user.last_login < end_time:
|
||||||
@@ -637,11 +589,7 @@ class AILoginSummaryAPI(APIView):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return "", str(exc)
|
return "", str(exc)
|
||||||
|
|
||||||
system_prompt = (
|
system_prompt = "你是 OnlineJudge 的学习助教。请根据统计数据给出简短分析(1-2句),再给出一行结论,结论用“结论:”开头。"
|
||||||
"你是 OnlineJudge 的学习助教。"
|
|
||||||
"请根据统计数据给出简短分析(1-2句),再给出一行结论,"
|
|
||||||
"结论用“结论:”开头。"
|
|
||||||
)
|
|
||||||
user_prompt = (
|
user_prompt = (
|
||||||
f"时间范围:{summary['start']} 到 {summary['end']}\n"
|
f"时间范围:{summary['start']} 到 {summary['end']}\n"
|
||||||
f"新题目数:{summary['new_problem_count']}\n"
|
f"新题目数:{summary['new_problem_count']}\n"
|
||||||
@@ -669,6 +617,7 @@ class AILoginSummaryAPI(APIView):
|
|||||||
content = completion.choices[0].message.content or ""
|
content = completion.choices[0].message.content or ""
|
||||||
return content.strip(), ""
|
return content.strip(), ""
|
||||||
|
|
||||||
|
|
||||||
class AIAnalysisAPI(APIView):
|
class AIAnalysisAPI(APIView):
|
||||||
@login_required
|
@login_required
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
@@ -697,9 +646,7 @@ class AIAnalysisAPI(APIView):
|
|||||||
analysis=full_text,
|
analysis=full_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
return make_sse_response(
|
return make_sse_response(stream_ai_response(client, system_prompt, user_prompt, on_complete))
|
||||||
stream_ai_response(client, system_prompt, user_prompt, on_complete)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ClassPKAnalysisAPI(APIView):
|
class ClassPKAnalysisAPI(APIView):
|
||||||
@@ -745,24 +692,11 @@ class ClassPKAnalysisAPI(APIView):
|
|||||||
class_display = fmt_class(c["class_name"])
|
class_display = fmt_class(c["class_name"])
|
||||||
lines.append(f"\n### 第{i + 1}名:{class_display}(综合分 {c['composite_score']:.1f})")
|
lines.append(f"\n### 第{i + 1}名:{class_display}(综合分 {c['composite_score']:.1f})")
|
||||||
lines.append(f"- 人数:{c['user_count']}")
|
lines.append(f"- 人数:{c['user_count']}")
|
||||||
lines.append(
|
lines.append(f"- 总AC数:{c['total_ac']},总提交数:{c['total_submission']},AC率:{c['ac_rate']:.1f}%")
|
||||||
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(
|
lines.append(f"- 前10%均值:{c['top_10_avg']:.2f},中间80%均值:{c['middle_80_avg']:.2f},后10%均值:{c['bottom_10_avg']:.2f}")
|
||||||
f"- 平均AC:{c['avg_ac']:.2f},中位数AC:{c['median_ac']:.2f}"
|
lines.append(f"- 优秀率:{c['excellent_rate']:.1f}%,及格率:{c['pass_rate']:.1f}%,参与度:{c['active_rate']:.1f}%")
|
||||||
)
|
|
||||||
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}%"
|
|
||||||
)
|
|
||||||
|
|
||||||
if c.get("recent_total_ac") is not None:
|
if c.get("recent_total_ac") is not None:
|
||||||
lines.append(
|
lines.append(
|
||||||
@@ -781,19 +715,15 @@ class ClassPKAnalysisAPI(APIView):
|
|||||||
"",
|
"",
|
||||||
"**2. 参与积极性**:对比参与度和总提交数,谁的班学生更积极主动?",
|
"**2. 参与积极性**:对比参与度和总提交数,谁的班学生更积极主动?",
|
||||||
"",
|
"",
|
||||||
'**3. 典型学生水平**:重点用中位数AC数对比(而非平均值),'
|
'**3. 典型学生水平**:重点用中位数AC数对比(而非平均值),分析谁班的"普通学生"更强。若均值明显高于中位数,说明均值被少数强者拉高,需指出。',
|
||||||
'分析谁班的"普通学生"更强。若均值明显高于中位数,说明均值被少数强者拉高,需指出。',
|
|
||||||
"",
|
"",
|
||||||
'**4. 班级内部均衡性**:结合标准差、IQR、前10%与后10%差距,'
|
'**4. 班级内部均衡性**:结合标准差、IQR、前10%与后10%差距,判断哪个班是"均衡型",哪个班是"两极型"。',
|
||||||
'判断哪个班是"均衡型",哪个班是"两极型"。',
|
|
||||||
"",
|
"",
|
||||||
"**5. 梯队深度对比**:对比各班前10%均值(尖子生天花板)和后10%均值(薄弱学生水平),"
|
"**5. 梯队深度对比**:对比各班前10%均值(尖子生天花板)和后10%均值(薄弱学生水平),分析各班在培养尖子生和帮扶后进生上的差异。",
|
||||||
"分析各班在培养尖子生和帮扶后进生上的差异。",
|
|
||||||
"",
|
"",
|
||||||
'**6. 代码提交质量**:对比AC率,是否有班级存在"凑提交次数但不思考"的问题?',
|
'**6. 代码提交质量**:对比AC率,是否有班级存在"凑提交次数但不思考"的问题?',
|
||||||
"",
|
"",
|
||||||
"**7. 综合结论与建议**:用1句话明确说明胜负;"
|
"**7. 综合结论与建议**:用1句话明确说明胜负;对落后班级给出2~3条具体可操作的改进建议;点出领先班级1条值得借鉴的做法。",
|
||||||
"对落后班级给出2~3条具体可操作的改进建议;点出领先班级1条值得借鉴的做法。",
|
|
||||||
"",
|
"",
|
||||||
"分析对象是班级任课教师,语言专业但不过分学术。",
|
"分析对象是班级任课教师,语言专业但不过分学术。",
|
||||||
]
|
]
|
||||||
@@ -916,9 +846,7 @@ class AIHintAPI(APIView):
|
|||||||
f"学生代码:\n```\n{submission.code[:2000]}\n```"
|
f"学生代码:\n```\n{submission.code[:2000]}\n```"
|
||||||
)
|
)
|
||||||
|
|
||||||
return make_sse_response(
|
return make_sse_response(stream_ai_response(client, system_prompt, user_prompt))
|
||||||
stream_ai_response(client, system_prompt, user_prompt)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AIHeatmapDataAPI(APIView):
|
class AIHeatmapDataAPI(APIView):
|
||||||
@@ -941,9 +869,7 @@ class AIHeatmapDataAPI(APIView):
|
|||||||
|
|
||||||
# 使用单次查询获取所有数据,按日期分组统计
|
# 使用单次查询获取所有数据,按日期分组统计
|
||||||
submission_counts = (
|
submission_counts = (
|
||||||
Submission.objects.filter(
|
Submission.objects.filter(user_id=user.id, create_time__gte=start, create_time__lte=end)
|
||||||
user_id=user.id, create_time__gte=start, create_time__lte=end
|
|
||||||
)
|
|
||||||
.annotate(date=TruncDate("create_time"))
|
.annotate(date=TruncDate("create_time"))
|
||||||
.values("date")
|
.values("date")
|
||||||
.annotate(count=Count("id"))
|
.annotate(count=Count("id"))
|
||||||
@@ -961,10 +887,7 @@ class AIHeatmapDataAPI(APIView):
|
|||||||
submission_count = submission_dict.get(day_date, 0)
|
submission_count = submission_dict.get(day_date, 0)
|
||||||
heatmap_data.append(
|
heatmap_data.append(
|
||||||
{
|
{
|
||||||
"timestamp": int(
|
"timestamp": int(datetime.combine(day_date, datetime.min.time()).timestamp() * 1000),
|
||||||
datetime.combine(day_date, datetime.min.time()).timestamp()
|
|
||||||
* 1000
|
|
||||||
),
|
|
||||||
"value": submission_count,
|
"value": submission_count,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ class Announcement(models.Model):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "announcement"
|
db_table = "announcement"
|
||||||
ordering = ("-top", "-create_time",)
|
ordering = (
|
||||||
|
"-top",
|
||||||
|
"-create_time",
|
||||||
|
)
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["visible", "-top", "-create_time"], name="announcement_list_idx"),
|
models.Index(fields=["visible", "-top", "-create_time"], name="announcement_list_idx"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class AnnouncementListSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Announcement
|
model = Announcement
|
||||||
exclude = ['content']
|
exclude = ["content"]
|
||||||
|
|
||||||
|
|
||||||
class EditAnnouncementSerializer(serializers.Serializer):
|
class EditAnnouncementSerializer(serializers.Serializer):
|
||||||
|
|||||||
@@ -52,9 +52,7 @@ class AnnouncementAdminAPI(APIView):
|
|||||||
announcement = Announcement.objects.all().order_by("-create_time")
|
announcement = Announcement.objects.all().order_by("-create_time")
|
||||||
if request.GET.get("visible") == "true":
|
if request.GET.get("visible") == "true":
|
||||||
announcement = announcement.filter(visible=True)
|
announcement = announcement.filter(visible=True)
|
||||||
return self.success(
|
return self.success(self.paginate_data(request, announcement, AnnouncementSerializer))
|
||||||
self.paginate_data(request, announcement, AnnouncementSerializer)
|
|
||||||
)
|
|
||||||
|
|
||||||
@super_admin_required
|
@super_admin_required
|
||||||
def delete(self, request):
|
def delete(self, request):
|
||||||
|
|||||||
@@ -8,11 +8,7 @@ class AnnouncementAPI(AsyncAPIView):
|
|||||||
id = request.GET.get("id")
|
id = request.GET.get("id")
|
||||||
if id:
|
if id:
|
||||||
try:
|
try:
|
||||||
announcement = await (
|
announcement = await Announcement.objects.select_related("created_by").filter(id=id, visible=True).afirst()
|
||||||
Announcement.objects.select_related("created_by")
|
|
||||||
.filter(id=id, visible=True)
|
|
||||||
.afirst()
|
|
||||||
)
|
|
||||||
if announcement is None:
|
if announcement is None:
|
||||||
raise Announcement.DoesNotExist
|
raise Announcement.DoesNotExist
|
||||||
return self.success(await self.async_serialize_data(AnnouncementSerializer, announcement))
|
return self.success(await self.async_serialize_data(AnnouncementSerializer, announcement))
|
||||||
@@ -20,6 +16,4 @@ class AnnouncementAPI(AsyncAPIView):
|
|||||||
return self.error("Announcement does not exist")
|
return self.error("Announcement does not exist")
|
||||||
|
|
||||||
announcements = Announcement.objects.select_related("created_by").filter(visible=True)
|
announcements = Announcement.objects.select_related("created_by").filter(visible=True)
|
||||||
return self.success(
|
return self.success(await self.async_paginate_data(request, announcements, AnnouncementListSerializer))
|
||||||
await self.async_paginate_data(request, announcements, AnnouncementListSerializer)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
|
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
|
|
||||||
# 如果需要存储班级PK历史记录,可以在这里定义模型
|
# 如果需要存储班级PK历史记录,可以在这里定义模型
|
||||||
# 目前暂时不需要,因为都是实时计算
|
# 目前暂时不需要,因为都是实时计算
|
||||||
|
|||||||
@@ -7,4 +7,3 @@ urlpatterns = [
|
|||||||
path("user_class_rank", UserClassRankAPI.as_view()),
|
path("user_class_rank", UserClassRankAPI.as_view()),
|
||||||
path("class_pk", ClassPKAPI.as_view()),
|
path("class_pk", ClassPKAPI.as_view()),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -42,9 +42,7 @@ class ClassRankAPI(APIView):
|
|||||||
profiles = UserProfile.objects.filter(user_id__in=user_ids)
|
profiles = UserProfile.objects.filter(user_id__in=user_ids)
|
||||||
|
|
||||||
total_ac = profiles.aggregate(total=Sum("accepted_number"))["total"] or 0
|
total_ac = profiles.aggregate(total=Sum("accepted_number"))["total"] or 0
|
||||||
total_submission = (
|
total_submission = profiles.aggregate(total=Sum("submission_number"))["total"] or 0
|
||||||
profiles.aggregate(total=Sum("submission_number"))["total"] or 0
|
|
||||||
)
|
|
||||||
avg_ac = profiles.aggregate(avg=Avg("accepted_number"))["avg"] or 0
|
avg_ac = profiles.aggregate(avg=Avg("accepted_number"))["avg"] or 0
|
||||||
|
|
||||||
user_count = users.count()
|
user_count = users.count()
|
||||||
@@ -56,9 +54,7 @@ class ClassRankAPI(APIView):
|
|||||||
"total_ac": int(total_ac),
|
"total_ac": int(total_ac),
|
||||||
"total_submission": int(total_submission),
|
"total_submission": int(total_submission),
|
||||||
"avg_ac": round(avg_ac, 2),
|
"avg_ac": round(avg_ac, 2),
|
||||||
"ac_rate": round(total_ac / total_submission * 100, 2)
|
"ac_rate": round(total_ac / total_submission * 100, 2) if total_submission > 0 else 0,
|
||||||
if total_submission > 0
|
|
||||||
else 0,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -213,9 +209,7 @@ class ClassPKAPI(APIView):
|
|||||||
# 获取所有学生的AC数列表(用于统计计算)
|
# 获取所有学生的AC数列表(用于统计计算)
|
||||||
profiles = UserProfile.objects.filter(user_id__in=user_ids)
|
profiles = UserProfile.objects.filter(user_id__in=user_ids)
|
||||||
ac_list = sorted([p.accepted_number for p in profiles], reverse=True)
|
ac_list = sorted([p.accepted_number for p in profiles], reverse=True)
|
||||||
submission_list = sorted(
|
submission_list = sorted([p.submission_number for p in profiles], reverse=True)
|
||||||
[p.submission_number for p in profiles], reverse=True
|
|
||||||
)
|
|
||||||
|
|
||||||
user_count = len(ac_list)
|
user_count = len(ac_list)
|
||||||
if user_count == 0:
|
if user_count == 0:
|
||||||
@@ -238,14 +232,8 @@ class ClassPKAPI(APIView):
|
|||||||
# 前10%和后10%统计
|
# 前10%和后10%统计
|
||||||
top_10_count = max(1, math.ceil(user_count * 0.10))
|
top_10_count = max(1, math.ceil(user_count * 0.10))
|
||||||
bottom_10_count = max(1, math.ceil(user_count * 0.10))
|
bottom_10_count = max(1, math.ceil(user_count * 0.10))
|
||||||
top_10_avg = (
|
top_10_avg = statistics.mean(ac_list[:top_10_count]) if top_10_count > 0 else 0
|
||||||
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
|
||||||
)
|
|
||||||
bottom_10_avg = (
|
|
||||||
statistics.mean(ac_list[-bottom_10_count:])
|
|
||||||
if bottom_10_count > 0
|
|
||||||
else 0
|
|
||||||
)
|
|
||||||
|
|
||||||
# 中间80%均值(截尾均值,去掉前10%和后10%)
|
# 中间80%均值(截尾均值,去掉前10%和后10%)
|
||||||
if top_10_count + bottom_10_count < user_count:
|
if top_10_count + bottom_10_count < user_count:
|
||||||
@@ -256,9 +244,7 @@ class ClassPKAPI(APIView):
|
|||||||
|
|
||||||
# 优秀率(AC数 >= 全局Q3,即超过PK组所有学生的前25%)
|
# 优秀率(AC数 >= 全局Q3,即超过PK组所有学生的前25%)
|
||||||
excellent_count = sum(1 for ac in ac_list if ac >= global_q3)
|
excellent_count = sum(1 for ac in ac_list if ac >= global_q3)
|
||||||
excellent_rate = (
|
excellent_rate = (excellent_count / user_count * 100) if user_count > 0 else 0
|
||||||
(excellent_count / user_count * 100) if user_count > 0 else 0
|
|
||||||
)
|
|
||||||
|
|
||||||
# 及格率(AC数 >= 全局Q1,即超过PK组所有学生的后25%)
|
# 及格率(AC数 >= 全局Q1,即超过PK组所有学生的后25%)
|
||||||
pass_count = sum(1 for ac in ac_list if ac >= global_q1)
|
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__gte=start_time,
|
||||||
create_time__lte=end_time,
|
create_time__lte=end_time,
|
||||||
)
|
)
|
||||||
recent_ac = (
|
recent_ac = submissions.filter(result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED]).values("user_id", "problem_id").distinct().count()
|
||||||
submissions.filter(result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED])
|
|
||||||
.values("user_id", "problem_id")
|
|
||||||
.distinct()
|
|
||||||
.count()
|
|
||||||
)
|
|
||||||
recent_submission = submissions.count()
|
recent_submission = submissions.count()
|
||||||
|
|
||||||
# 时间段内的用户AC数列表
|
# 时间段内的用户AC数列表
|
||||||
recent_user_ac = {}
|
recent_user_ac = {}
|
||||||
for user_id in user_ids:
|
for user_id in user_ids:
|
||||||
user_recent_ac = (
|
user_recent_ac = submissions.filter(user_id=user_id, result__in=[JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED]).values("problem_id").distinct().count()
|
||||||
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_user_ac[user_id] = user_recent_ac
|
||||||
|
|
||||||
recent_ac_list = sorted(recent_user_ac.values(), reverse=True)
|
recent_ac_list = sorted(recent_user_ac.values(), reverse=True)
|
||||||
@@ -302,14 +278,8 @@ class ClassPKAPI(APIView):
|
|||||||
"recent_total_submission": recent_submission,
|
"recent_total_submission": recent_submission,
|
||||||
"recent_avg_ac": statistics.mean(recent_ac_list),
|
"recent_avg_ac": statistics.mean(recent_ac_list),
|
||||||
"recent_median_ac": statistics.median(recent_ac_list),
|
"recent_median_ac": statistics.median(recent_ac_list),
|
||||||
"recent_top_10_avg": statistics.mean(
|
"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_ac_list[: max(1, math.ceil(len(recent_ac_list) * 0.10))]
|
"recent_active_count": sum(1 for ac in recent_ac_list if ac > 0),
|
||||||
)
|
|
||||||
if recent_ac_list
|
|
||||||
else 0,
|
|
||||||
"recent_active_count": sum(
|
|
||||||
1 for ac in recent_ac_list if ac > 0
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class_comparisons.append(
|
class_comparisons.append(
|
||||||
@@ -336,9 +306,7 @@ class ClassPKAPI(APIView):
|
|||||||
"pass_rate": round(pass_rate, 2),
|
"pass_rate": round(pass_rate, 2),
|
||||||
"active_rate": round(active_rate, 2),
|
"active_rate": round(active_rate, 2),
|
||||||
# 正确率
|
# 正确率
|
||||||
"ac_rate": round(total_ac / total_submission * 100, 2)
|
"ac_rate": round(total_ac / total_submission * 100, 2) if total_submission > 0 else 0,
|
||||||
if total_submission > 0
|
|
||||||
else 0,
|
|
||||||
# 时间段统计(如果有)
|
# 时间段统计(如果有)
|
||||||
**recent_stats,
|
**recent_stats,
|
||||||
}
|
}
|
||||||
@@ -359,9 +327,7 @@ class ClassPKAPI(APIView):
|
|||||||
c["composite_score"] = round(score, 1)
|
c["composite_score"] = round(score, 1)
|
||||||
|
|
||||||
# 按综合分排序(主),中位数(次)
|
# 按综合分排序(主),中位数(次)
|
||||||
class_comparisons.sort(
|
class_comparisons.sort(key=lambda x: (-x["composite_score"], -x["median_ac"]))
|
||||||
key=lambda x: (-x["composite_score"], -x["median_ac"])
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.success(
|
return self.success(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,5 +43,3 @@ class Comment(models.Model):
|
|||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["problem", "create_time"], name="comment_problem_time_idx"),
|
models.Index(fields=["problem", "create_time"], name="comment_problem_time_idx"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -28,4 +28,4 @@ class CommentListSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Comment
|
model = Comment
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ class CommentAPI(APIView):
|
|||||||
except Problem.DoesNotExist:
|
except Problem.DoesNotExist:
|
||||||
return self.error("Problem doesn't exist")
|
return self.error("Problem doesn't exist")
|
||||||
comments = comments.filter(problem=problem)
|
comments = comments.filter(problem=problem)
|
||||||
return self.success(
|
return self.success(self.paginate_data(request, comments, CommentListSerializer))
|
||||||
self.paginate_data(request, comments, CommentListSerializer)
|
|
||||||
)
|
|
||||||
|
|
||||||
@super_admin_required
|
@super_admin_required
|
||||||
def delete(self, request):
|
def delete(self, request):
|
||||||
|
|||||||
@@ -54,11 +54,7 @@ class CommentAPI(AsyncAPIView):
|
|||||||
@login_required
|
@login_required
|
||||||
async def get(self, request):
|
async def get(self, request):
|
||||||
problem_id = request.GET.get("problem_id")
|
problem_id = request.GET.get("problem_id")
|
||||||
comment = await (
|
comment = await Comment.objects.select_related("problem").filter(user=request.user, problem_id=problem_id).afirst()
|
||||||
Comment.objects.select_related("problem")
|
|
||||||
.filter(user=request.user, problem_id=problem_id)
|
|
||||||
.afirst()
|
|
||||||
)
|
|
||||||
if comment:
|
if comment:
|
||||||
return self.success(await self.async_serialize_data(CommentSerializer, comment))
|
return self.success(await self.async_serialize_data(CommentSerializer, comment))
|
||||||
else:
|
else:
|
||||||
@@ -82,10 +78,13 @@ class CommentStatisticsAPI(AsyncAPIView):
|
|||||||
if not agg["count"]:
|
if not agg["count"]:
|
||||||
return self.success()
|
return self.success()
|
||||||
|
|
||||||
data = {"count": agg["count"], "rating": {
|
data = {
|
||||||
"description": agg["description"],
|
"count": agg["count"],
|
||||||
"difficulty": agg["difficulty"],
|
"rating": {
|
||||||
"comprehensive": agg["comprehensive"],
|
"description": agg["description"],
|
||||||
}}
|
"difficulty": agg["difficulty"],
|
||||||
|
"comprehensive": agg["comprehensive"],
|
||||||
|
},
|
||||||
|
}
|
||||||
await async_cache_set(cache_key, data, 3600)
|
await async_cache_set(cache_key, data, 3600)
|
||||||
return self.success(data)
|
return self.success(data)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
WebSocket consumers for configuration updates
|
WebSocket consumers for configuration updates
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -18,31 +19,25 @@ class ConfigConsumer(AsyncWebsocketConsumer):
|
|||||||
async def connect(self):
|
async def connect(self):
|
||||||
"""处理 WebSocket 连接"""
|
"""处理 WebSocket 连接"""
|
||||||
self.user = self.scope["user"]
|
self.user = self.scope["user"]
|
||||||
|
|
||||||
# 只允许认证用户连接
|
# 只允许认证用户连接
|
||||||
if not self.user.is_authenticated:
|
if not self.user.is_authenticated:
|
||||||
await self.close()
|
await self.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
# 使用全局配置组名,所有用户都能接收配置更新
|
# 使用全局配置组名,所有用户都能接收配置更新
|
||||||
self.group_name = "config_updates"
|
self.group_name = "config_updates"
|
||||||
|
|
||||||
# 加入配置更新组
|
# 加入配置更新组
|
||||||
await self.channel_layer.group_add(
|
await self.channel_layer.group_add(self.group_name, self.channel_name)
|
||||||
self.group_name,
|
|
||||||
self.channel_name
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.accept()
|
await self.accept()
|
||||||
logger.info(f"Config WebSocket connected: user_id={self.user.id}, channel={self.channel_name}")
|
logger.info(f"Config WebSocket connected: user_id={self.user.id}, channel={self.channel_name}")
|
||||||
|
|
||||||
async def disconnect(self, close_code):
|
async def disconnect(self, close_code):
|
||||||
"""处理 WebSocket 断开连接"""
|
"""处理 WebSocket 断开连接"""
|
||||||
if hasattr(self, 'group_name'):
|
if hasattr(self, "group_name"):
|
||||||
await self.channel_layer.group_discard(
|
await self.channel_layer.group_discard(self.group_name, self.channel_name)
|
||||||
self.group_name,
|
|
||||||
self.channel_name
|
|
||||||
)
|
|
||||||
logger.info(f"Config WebSocket disconnected: user_id={self.user.id}, close_code={close_code}")
|
logger.info(f"Config WebSocket disconnected: user_id={self.user.id}, close_code={close_code}")
|
||||||
|
|
||||||
async def receive(self, text_data):
|
async def receive(self, text_data):
|
||||||
@@ -53,13 +48,10 @@ class ConfigConsumer(AsyncWebsocketConsumer):
|
|||||||
try:
|
try:
|
||||||
data = json.loads(text_data)
|
data = json.loads(text_data)
|
||||||
message_type = data.get("type")
|
message_type = data.get("type")
|
||||||
|
|
||||||
if message_type == "ping":
|
if message_type == "ping":
|
||||||
# 响应心跳包
|
# 响应心跳包
|
||||||
await self.send(text_data=json.dumps({
|
await self.send(text_data=json.dumps({"type": "pong", "timestamp": data.get("timestamp")}))
|
||||||
"type": "pong",
|
|
||||||
"timestamp": data.get("timestamp")
|
|
||||||
}))
|
|
||||||
elif message_type == "config_update":
|
elif message_type == "config_update":
|
||||||
# 处理配置更新请求
|
# 处理配置更新请求
|
||||||
key = data.get("key")
|
key = data.get("key")
|
||||||
@@ -69,17 +61,7 @@ class ConfigConsumer(AsyncWebsocketConsumer):
|
|||||||
# 这里可以添加权限检查,只有管理员才能发送配置更新
|
# 这里可以添加权限检查,只有管理员才能发送配置更新
|
||||||
if self.user.is_superuser:
|
if self.user.is_superuser:
|
||||||
# 广播配置更新给所有连接的客户端
|
# 广播配置更新给所有连接的客户端
|
||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(self.group_name, {"type": "config_update", "data": {"type": "config_update", "key": key, "value": value}})
|
||||||
self.group_name,
|
|
||||||
{
|
|
||||||
"type": "config_update",
|
|
||||||
"data": {
|
|
||||||
"type": "config_update",
|
|
||||||
"key": key,
|
|
||||||
"value": value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
logger.error(f"Invalid JSON received from user {self.user.id}")
|
logger.error(f"Invalid JSON received from user {self.user.id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class JudgeServerHeartbeatSerializer(serializers.Serializer):
|
|||||||
cpu_core = serializers.IntegerField(min_value=1)
|
cpu_core = serializers.IntegerField(min_value=1)
|
||||||
memory = serializers.FloatField(min_value=0, max_value=100)
|
memory = serializers.FloatField(min_value=0, max_value=100)
|
||||||
cpu = 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)
|
service_url = serializers.CharField(max_length=256)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -153,9 +153,7 @@ class JudgeServerAPI(APIView):
|
|||||||
@super_admin_required
|
@super_admin_required
|
||||||
def put(self, request):
|
def put(self, request):
|
||||||
is_disabled = request.data.get("is_disabled", False)
|
is_disabled = request.data.get("is_disabled", False)
|
||||||
JudgeServer.objects.filter(id=request.data["id"]).update(
|
JudgeServer.objects.filter(id=request.data["id"]).update(is_disabled=is_disabled)
|
||||||
is_disabled=is_disabled
|
|
||||||
)
|
|
||||||
if not is_disabled:
|
if not is_disabled:
|
||||||
process_pending_task()
|
process_pending_task()
|
||||||
return self.success()
|
return self.success()
|
||||||
@@ -166,10 +164,7 @@ class JudgeServerHeartbeatAPI(CSRFExemptAPIView):
|
|||||||
def post(self, request):
|
def post(self, request):
|
||||||
data = request.data
|
data = request.data
|
||||||
client_token = request.META.get("HTTP_X_JUDGE_SERVER_TOKEN")
|
client_token = request.META.get("HTTP_X_JUDGE_SERVER_TOKEN")
|
||||||
if (
|
if hashlib.sha256(SysOptions.judge_server_token.encode("utf-8")).hexdigest() != client_token:
|
||||||
hashlib.sha256(SysOptions.judge_server_token.encode("utf-8")).hexdigest()
|
|
||||||
!= client_token
|
|
||||||
):
|
|
||||||
return self.error("Invalid token")
|
return self.error("Invalid token")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -263,8 +258,7 @@ class ReleaseNotesAPI(APIView):
|
|||||||
def get(self, request):
|
def get(self, request):
|
||||||
try:
|
try:
|
||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
"https://raw.githubusercontent.com/QingdaoU/OnlineJudge/master/docs/data.json?_="
|
"https://raw.githubusercontent.com/QingdaoU/OnlineJudge/master/docs/data.json?_=" + str(time.time()),
|
||||||
+ str(time.time()),
|
|
||||||
timeout=3,
|
timeout=3,
|
||||||
)
|
)
|
||||||
releases = resp.json()
|
releases = resp.json()
|
||||||
@@ -289,9 +283,7 @@ class DashboardInfoAPI(AsyncAPIView):
|
|||||||
User.objects.acount(),
|
User.objects.acount(),
|
||||||
Submission.objects.filter(create_time__gte=today_start).acount(),
|
Submission.objects.filter(create_time__gte=today_start).acount(),
|
||||||
Contest.objects.exclude(end_time__lt=timezone.now()).acount(),
|
Contest.objects.exclude(end_time__lt=timezone.now()).acount(),
|
||||||
JudgeServer.objects.filter(
|
JudgeServer.objects.filter(last_heartbeat__gte=timezone.now() - timedelta(seconds=6)).acount(),
|
||||||
last_heartbeat__gte=timezone.now() - timedelta(seconds=6)
|
|
||||||
).acount(),
|
|
||||||
)
|
)
|
||||||
return self.success(
|
return self.success(
|
||||||
{
|
{
|
||||||
@@ -312,20 +304,14 @@ class RandomUsernameAPI(AsyncAPIView):
|
|||||||
classroom = request.GET.get("classroom", "")
|
classroom = request.GET.get("classroom", "")
|
||||||
if not classroom:
|
if not classroom:
|
||||||
return self.error("需要班级号")
|
return self.error("需要班级号")
|
||||||
usernames = [
|
usernames = [u async for u in User.objects.filter(username__istartswith=classroom).values_list("username", flat=True).order_by("?")[:10]]
|
||||||
u async for u in User.objects.filter(username__istartswith=classroom)
|
|
||||||
.values_list("username", flat=True)
|
|
||||||
.order_by("?")[:10]
|
|
||||||
]
|
|
||||||
return self.success(usernames)
|
return self.success(usernames)
|
||||||
|
|
||||||
|
|
||||||
class HitokotoAPI(AsyncAPIView):
|
class HitokotoAPI(AsyncAPIView):
|
||||||
async def get(self, request):
|
async def get(self, request):
|
||||||
try:
|
try:
|
||||||
categories = JsonDataLoader.load_data(
|
categories = JsonDataLoader.load_data(settings.HITOKOTO_DIR, "categories.json")
|
||||||
settings.HITOKOTO_DIR, "categories.json"
|
|
||||||
)
|
|
||||||
path = random.choice(categories).get("path")
|
path = random.choice(categories).get("path")
|
||||||
sentences = JsonDataLoader.load_data(settings.HITOKOTO_DIR, path)
|
sentences = JsonDataLoader.load_data(settings.HITOKOTO_DIR, path)
|
||||||
sentence = random.choice(sentences)
|
sentence = random.choice(sentences)
|
||||||
@@ -341,7 +327,6 @@ class ClassUsernamesAPI(AsyncAPIView):
|
|||||||
return self.error("需要班级号")
|
return self.error("需要班级号")
|
||||||
prefix = f"ks{classroom}"
|
prefix = f"ks{classroom}"
|
||||||
names = [
|
names = [
|
||||||
user.username[len(prefix):] if user.username.startswith(prefix) else user.username
|
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")
|
||||||
async for user in User.objects.filter(class_name=classroom).order_by("-create_time")
|
|
||||||
]
|
]
|
||||||
return self.success(names)
|
return self.success(names)
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ class ACMContestRank(AbstractContestRank):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ContestAnnouncement(models.Model):
|
class ContestAnnouncement(models.Model):
|
||||||
contest = models.ForeignKey(Contest, on_delete=models.CASCADE)
|
contest = models.ForeignKey(Contest, on_delete=models.CASCADE)
|
||||||
title = models.TextField()
|
title = models.TextField()
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ class ACMContestRankSerializer(serializers.ModelSerializer):
|
|||||||
return UsernameSerializer(obj.user, need_real_name=self.is_contest_admin).data
|
return UsernameSerializer(obj.user, need_real_name=self.is_contest_admin).data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ACMContesHelperSerializer(serializers.Serializer):
|
class ACMContesHelperSerializer(serializers.Serializer):
|
||||||
contest_id = serializers.IntegerField()
|
contest_id = serializers.IntegerField()
|
||||||
problem_id = serializers.CharField()
|
problem_id = serializers.CharField()
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ class ContestAnnouncementListAPI(AsyncAPIView):
|
|||||||
contest_id = request.GET.get("contest_id")
|
contest_id = request.GET.get("contest_id")
|
||||||
if not contest_id:
|
if not contest_id:
|
||||||
return self.error("Invalid parameter, contest_id is required")
|
return self.error("Invalid parameter, contest_id is required")
|
||||||
qs = ContestAnnouncement.objects.select_related("created_by").filter(
|
qs = ContestAnnouncement.objects.select_related("created_by").filter(contest_id=contest_id, visible=True)
|
||||||
contest_id=contest_id, visible=True
|
|
||||||
)
|
|
||||||
max_id = request.GET.get("max_id")
|
max_id = request.GET.get("max_id")
|
||||||
if max_id:
|
if max_id:
|
||||||
qs = qs.filter(id__gt=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):
|
if not id or not check_is_id(id):
|
||||||
return self.error("Invalid parameter, id is required")
|
return self.error("Invalid parameter, id is required")
|
||||||
try:
|
try:
|
||||||
contest = await (
|
contest = await Contest.objects.select_related("created_by").filter(id=id, visible=True).afirst()
|
||||||
Contest.objects.select_related("created_by")
|
|
||||||
.filter(id=id, visible=True)
|
|
||||||
.afirst()
|
|
||||||
)
|
|
||||||
if contest is None:
|
if contest is None:
|
||||||
raise Contest.DoesNotExist
|
raise Contest.DoesNotExist
|
||||||
except Contest.DoesNotExist:
|
except Contest.DoesNotExist:
|
||||||
@@ -84,9 +78,7 @@ class ContestPasswordVerifyAPI(AsyncAPIView):
|
|||||||
async def post(self, request):
|
async def post(self, request):
|
||||||
data = request.data
|
data = request.data
|
||||||
try:
|
try:
|
||||||
contest = await Contest.objects.aget(
|
contest = await Contest.objects.aget(id=data["contest_id"], visible=True, password__isnull=False)
|
||||||
id=data["contest_id"], visible=True, password__isnull=False
|
|
||||||
)
|
|
||||||
except Contest.DoesNotExist:
|
except Contest.DoesNotExist:
|
||||||
return self.error("Contest does not exist")
|
return self.error("Contest does not exist")
|
||||||
if not check_contest_password(data["password"], contest.password):
|
if not check_contest_password(data["password"], contest.password):
|
||||||
@@ -106,17 +98,11 @@ class ContestAccessAPI(AsyncAPIView):
|
|||||||
if not contest_id:
|
if not contest_id:
|
||||||
return self.error()
|
return self.error()
|
||||||
try:
|
try:
|
||||||
contest = await Contest.objects.aget(
|
contest = await Contest.objects.aget(id=contest_id, visible=True, password__isnull=False)
|
||||||
id=contest_id, visible=True, password__isnull=False
|
|
||||||
)
|
|
||||||
except Contest.DoesNotExist:
|
except Contest.DoesNotExist:
|
||||||
return self.error("Contest does not exist")
|
return self.error("Contest does not exist")
|
||||||
session_pass = request.session.get(CONTEST_PASSWORD_SESSION_KEY, {}).get(
|
session_pass = request.session.get(CONTEST_PASSWORD_SESSION_KEY, {}).get(str(contest.id))
|
||||||
str(contest.id)
|
return self.success({"access": check_contest_password(session_pass, contest.password)})
|
||||||
)
|
|
||||||
return self.success(
|
|
||||||
{"access": check_contest_password(session_pass, contest.password)}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ContestRankAPI(AsyncAPIView):
|
class ContestRankAPI(AsyncAPIView):
|
||||||
@@ -155,16 +141,12 @@ class ContestRankAPI(AsyncAPIView):
|
|||||||
for index, item in enumerate(data):
|
for index, item in enumerate(data):
|
||||||
worksheet.write_string(index + 1, 0, str(item["user"]["id"]))
|
worksheet.write_string(index + 1, 0, str(item["user"]["id"]))
|
||||||
worksheet.write_string(index + 1, 1, item["user"]["username"])
|
worksheet.write_string(index + 1, 1, item["user"]["username"])
|
||||||
worksheet.write_string(
|
worksheet.write_string(index + 1, 2, item["user"]["real_name"] or "")
|
||||||
index + 1, 2, item["user"]["real_name"] or ""
|
|
||||||
)
|
|
||||||
worksheet.write_string(index + 1, 3, str(item["accepted_number"]))
|
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, 4, str(item["submission_number"]))
|
||||||
worksheet.write_string(index + 1, 5, str(item["total_time"]))
|
worksheet.write_string(index + 1, 5, str(item["total_time"]))
|
||||||
for k, v in item["submission_info"].items():
|
for k, v in item["submission_info"].items():
|
||||||
worksheet.write_string(
|
worksheet.write_string(index + 1, 6 + problem_id_to_col[int(k)], str(v["is_ac"]))
|
||||||
index + 1, 6 + problem_id_to_col[int(k)], str(v["is_ac"])
|
|
||||||
)
|
|
||||||
|
|
||||||
workbook.close()
|
workbook.close()
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
@@ -173,32 +155,20 @@ class ContestRankAPI(AsyncAPIView):
|
|||||||
@check_contest_permission(check_type="ranks")
|
@check_contest_permission(check_type="ranks")
|
||||||
async def get(self, request):
|
async def get(self, request):
|
||||||
download_csv = request.GET.get("download_csv")
|
download_csv = request.GET.get("download_csv")
|
||||||
is_contest_admin = (
|
is_contest_admin = request.user.is_authenticated and request.user.is_contest_admin(self.contest)
|
||||||
request.user.is_authenticated
|
|
||||||
and request.user.is_contest_admin(self.contest)
|
|
||||||
)
|
|
||||||
|
|
||||||
qs = self.get_rank()
|
qs = self.get_rank()
|
||||||
|
|
||||||
if download_csv:
|
if download_csv:
|
||||||
rank_list = [item async for item in qs]
|
rank_list = [item async for item in qs]
|
||||||
data = await self.async_serialize_data(
|
data = await self.async_serialize_data(ACMContestRankSerializer, rank_list, many=True, is_contest_admin=is_contest_admin)
|
||||||
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")))()
|
||||||
)
|
|
||||||
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)
|
xlsx_bytes = await sync_to_async(self._build_xlsx)(data, contest_problems)
|
||||||
response = HttpResponse(xlsx_bytes)
|
response = HttpResponse(xlsx_bytes)
|
||||||
response["Content-Disposition"] = (
|
response["Content-Disposition"] = f"attachment; filename=content-{self.contest.id}-rank.xlsx"
|
||||||
f"attachment; filename=content-{self.contest.id}-rank.xlsx"
|
|
||||||
)
|
|
||||||
response["Content-Type"] = "application/xlsx"
|
response["Content-Type"] = "application/xlsx"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
page_qs = await self.async_paginate_data(request, qs)
|
page_qs = await self.async_paginate_data(request, qs)
|
||||||
page_qs["results"] = await self.async_serialize_data(
|
page_qs["results"] = await self.async_serialize_data(ACMContestRankSerializer, page_qs["results"], many=True, is_contest_admin=is_contest_admin)
|
||||||
ACMContestRankSerializer,
|
|
||||||
page_qs["results"], many=True, is_contest_admin=is_contest_admin
|
|
||||||
)
|
|
||||||
return self.success(page_qs)
|
return self.success(page_qs)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
WebSocket consumers for flowchart evaluation updates
|
WebSocket consumers for flowchart evaluation updates
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -18,31 +19,25 @@ class FlowchartConsumer(AsyncWebsocketConsumer):
|
|||||||
async def connect(self):
|
async def connect(self):
|
||||||
"""处理 WebSocket 连接"""
|
"""处理 WebSocket 连接"""
|
||||||
self.user = self.scope["user"]
|
self.user = self.scope["user"]
|
||||||
|
|
||||||
# 只允许认证用户连接
|
# 只允许认证用户连接
|
||||||
if not self.user.is_authenticated:
|
if not self.user.is_authenticated:
|
||||||
await self.close()
|
await self.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
# 使用用户 ID 作为组名,这样可以向特定用户推送消息
|
# 使用用户 ID 作为组名,这样可以向特定用户推送消息
|
||||||
self.group_name = f"flowchart_user_{self.user.id}"
|
self.group_name = f"flowchart_user_{self.user.id}"
|
||||||
|
|
||||||
# 加入用户专属的组
|
# 加入用户专属的组
|
||||||
await self.channel_layer.group_add(
|
await self.channel_layer.group_add(self.group_name, self.channel_name)
|
||||||
self.group_name,
|
|
||||||
self.channel_name
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.accept()
|
await self.accept()
|
||||||
logger.info(f"Flowchart WebSocket connected: user_id={self.user.id}, channel={self.channel_name}")
|
logger.info(f"Flowchart WebSocket connected: user_id={self.user.id}, channel={self.channel_name}")
|
||||||
|
|
||||||
async def disconnect(self, close_code):
|
async def disconnect(self, close_code):
|
||||||
"""处理 WebSocket 断开连接"""
|
"""处理 WebSocket 断开连接"""
|
||||||
if hasattr(self, 'group_name'):
|
if hasattr(self, "group_name"):
|
||||||
await self.channel_layer.group_discard(
|
await self.channel_layer.group_discard(self.group_name, self.channel_name)
|
||||||
self.group_name,
|
|
||||||
self.channel_name
|
|
||||||
)
|
|
||||||
logger.info(f"Flowchart WebSocket disconnected: user_id={self.user.id}, close_code={close_code}")
|
logger.info(f"Flowchart WebSocket disconnected: user_id={self.user.id}, close_code={close_code}")
|
||||||
|
|
||||||
async def receive(self, text_data):
|
async def receive(self, text_data):
|
||||||
@@ -53,13 +48,10 @@ class FlowchartConsumer(AsyncWebsocketConsumer):
|
|||||||
try:
|
try:
|
||||||
data = json.loads(text_data)
|
data = json.loads(text_data)
|
||||||
message_type = data.get("type")
|
message_type = data.get("type")
|
||||||
|
|
||||||
if message_type == "ping":
|
if message_type == "ping":
|
||||||
# 响应心跳包
|
# 响应心跳包
|
||||||
await self.send(text_data=json.dumps({
|
await self.send(text_data=json.dumps({"type": "pong", "timestamp": data.get("timestamp")}))
|
||||||
"type": "pong",
|
|
||||||
"timestamp": data.get("timestamp")
|
|
||||||
}))
|
|
||||||
elif message_type == "subscribe":
|
elif message_type == "subscribe":
|
||||||
# 订阅特定流程图提交的更新
|
# 订阅特定流程图提交的更新
|
||||||
submission_id = data.get("submission_id")
|
submission_id = data.get("submission_id")
|
||||||
|
|||||||
@@ -6,61 +6,59 @@ from utils.shortcuts import rand_str
|
|||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class FlowchartSubmissionStatus:
|
class FlowchartSubmissionStatus:
|
||||||
PENDING = 0 # 等待AI评分
|
PENDING = 0 # 等待AI评分
|
||||||
PROCESSING = 1 # AI评分中
|
PROCESSING = 1 # AI评分中
|
||||||
COMPLETED = 2 # 评分完成
|
COMPLETED = 2 # 评分完成
|
||||||
FAILED = 3 # 评分失败
|
FAILED = 3 # 评分失败
|
||||||
|
|
||||||
|
|
||||||
class FlowchartSubmission(models.Model):
|
class FlowchartSubmission(models.Model):
|
||||||
"""流程图提交模型"""
|
"""流程图提交模型"""
|
||||||
|
|
||||||
id = models.TextField(default=rand_str, primary_key=True, db_index=True)
|
id = models.TextField(default=rand_str, primary_key=True, db_index=True)
|
||||||
|
|
||||||
# 基础信息
|
# 基础信息
|
||||||
user = models.ForeignKey(User, 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')
|
problem = models.ForeignKey(Problem, on_delete=models.CASCADE, related_name="flowchart_submissions")
|
||||||
|
|
||||||
# 提交内容
|
# 提交内容
|
||||||
mermaid_code = models.TextField() # Mermaid代码
|
mermaid_code = models.TextField() # Mermaid代码
|
||||||
flowchart_data = models.JSONField(default=dict) # 流程图元数据
|
flowchart_data = models.JSONField(default=dict) # 流程图元数据
|
||||||
|
|
||||||
# 状态信息
|
# 状态信息
|
||||||
status = models.IntegerField(default=FlowchartSubmissionStatus.PENDING)
|
status = models.IntegerField(default=FlowchartSubmissionStatus.PENDING)
|
||||||
create_time = models.DateTimeField(auto_now_add=True)
|
create_time = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
# AI评分结果
|
# AI评分结果
|
||||||
ai_score = models.FloatField(null=True, blank=True) # AI评分 (0-100)
|
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_grade = models.CharField(max_length=10, null=True, blank=True) # 等级 (S/A/B/C)
|
||||||
ai_feedback = models.TextField(null=True, blank=True) # AI反馈
|
ai_feedback = models.TextField(null=True, blank=True) # AI反馈
|
||||||
ai_suggestions = models.TextField(null=True, blank=True) # AI建议
|
ai_suggestions = models.TextField(null=True, blank=True) # AI建议
|
||||||
ai_criteria_details = models.JSONField(default=dict) # 详细评分标准
|
ai_criteria_details = models.JSONField(default=dict) # 详细评分标准
|
||||||
|
|
||||||
# 处理信息
|
# 处理信息
|
||||||
ai_provider = models.CharField(max_length=50, default='deepseek')
|
ai_provider = models.CharField(max_length=50, default="deepseek")
|
||||||
ai_model = models.CharField(max_length=50, default='deepseek-v4-flash')
|
ai_model = models.CharField(max_length=50, default="deepseek-v4-flash")
|
||||||
processing_time = models.FloatField(null=True, blank=True) # AI处理耗时(秒)
|
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:
|
class Meta:
|
||||||
db_table = 'flowchart_submission'
|
db_table = "flowchart_submission"
|
||||||
ordering = ['-create_time']
|
ordering = ["-create_time"]
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['user', 'create_time'], name='flowchart_user_time_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=["problem", "create_time"], name="flowchart_problem_time_idx"),
|
||||||
models.Index(fields=['status'], name='flowchart_status_idx'),
|
models.Index(fields=["status"], name="flowchart_status_idx"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"FlowchartSubmission {self.id}"
|
return f"FlowchartSubmission {self.id}"
|
||||||
|
|
||||||
def check_user_permission(self, user, check_share=True):
|
def check_user_permission(self, user, check_share=True):
|
||||||
"""检查用户权限"""
|
"""检查用户权限"""
|
||||||
if (
|
if self.user_id == user.id or not user.is_regular_user() or self.problem.created_by_id == user.id:
|
||||||
self.user_id == user.id
|
|
||||||
or not user.is_regular_user()
|
|
||||||
or self.problem.created_by_id == user.id
|
|
||||||
):
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class CreateFlowchartSubmissionSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
def validate_flowchart_data(self, value):
|
def validate_flowchart_data(self, value):
|
||||||
import json
|
import json
|
||||||
|
|
||||||
if len(json.dumps(value)) > 500 * 1024:
|
if len(json.dumps(value)) > 500 * 1024:
|
||||||
raise serializers.ValidationError("流程图数据过大")
|
raise serializers.ValidationError("流程图数据过大")
|
||||||
return value
|
return value
|
||||||
@@ -55,6 +56,7 @@ class FlowchartSubmissionListSerializer(serializers.ModelSerializer):
|
|||||||
username = serializers.CharField(source="user.username")
|
username = serializers.CharField(source="user.username")
|
||||||
problem = serializers.CharField(source="problem._id")
|
problem = serializers.CharField(source="problem._id")
|
||||||
problem_title = serializers.CharField(source="problem.title")
|
problem_title = serializers.CharField(source="problem.title")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FlowchartSubmission
|
model = FlowchartSubmission
|
||||||
fields = [
|
fields = [
|
||||||
|
|||||||
@@ -20,16 +20,16 @@ def evaluate_flowchart_task(submission_id):
|
|||||||
submission = None
|
submission = None
|
||||||
try:
|
try:
|
||||||
submission = FlowchartSubmission.objects.get(id=submission_id)
|
submission = FlowchartSubmission.objects.get(id=submission_id)
|
||||||
|
|
||||||
# 更新状态为处理中
|
# 更新状态为处理中
|
||||||
submission.status = FlowchartSubmissionStatus.PROCESSING
|
submission.status = FlowchartSubmissionStatus.PROCESSING
|
||||||
submission.save()
|
submission.save()
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
# 使用固定评分标准
|
# 使用固定评分标准
|
||||||
system_prompt = build_evaluation_prompt(submission.problem)
|
system_prompt = build_evaluation_prompt(submission.problem)
|
||||||
|
|
||||||
# 构建用户提示词,包含标准答案对比
|
# 构建用户提示词,包含标准答案对比
|
||||||
user_prompt = f"""
|
user_prompt = f"""
|
||||||
请对以下Mermaid流程图进行评分:
|
请对以下Mermaid流程图进行评分:
|
||||||
@@ -53,16 +53,13 @@ def evaluate_flowchart_task(submission_id):
|
|||||||
user_prompt += f"\n设计提示:{submission.problem.flowchart_hint}\n"
|
user_prompt += f"\n设计提示:{submission.problem.flowchart_hint}\n"
|
||||||
|
|
||||||
user_prompt += "\n请按照评分标准进行详细评估,并给出0-100的分数。\n"
|
user_prompt += "\n请按照评分标准进行详细评估,并给出0-100的分数。\n"
|
||||||
|
|
||||||
# 调用AI进行评分
|
# 调用AI进行评分
|
||||||
client = get_ai_client()
|
client = get_ai_client()
|
||||||
|
|
||||||
response = client.chat.completions.create(
|
response = client.chat.completions.create(
|
||||||
model="deepseek-v4-flash",
|
model="deepseek-v4-flash",
|
||||||
messages=[
|
messages=[{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}],
|
||||||
{"role": "system", "content": system_prompt},
|
|
||||||
{"role": "user", "content": user_prompt}
|
|
||||||
],
|
|
||||||
temperature=0,
|
temperature=0,
|
||||||
extra_body={"thinking": {"type": "disabled"}},
|
extra_body={"thinking": {"type": "disabled"}},
|
||||||
)
|
)
|
||||||
@@ -74,30 +71,31 @@ def evaluate_flowchart_task(submission_id):
|
|||||||
|
|
||||||
# 保存评分结果
|
# 保存评分结果
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
submission.ai_score = score_data['score']
|
submission.ai_score = score_data["score"]
|
||||||
submission.ai_grade = score_data['grade']
|
submission.ai_grade = score_data["grade"]
|
||||||
submission.ai_feedback = score_data['feedback']
|
submission.ai_feedback = score_data["feedback"]
|
||||||
submission.ai_suggestions = score_data.get('suggestions', '')
|
submission.ai_suggestions = score_data.get("suggestions", "")
|
||||||
submission.ai_criteria_details = score_data.get('criteria_details', {})
|
submission.ai_criteria_details = score_data.get("criteria_details", {})
|
||||||
submission.ai_provider = 'deepseek'
|
submission.ai_provider = "deepseek"
|
||||||
submission.ai_model = 'deepseek-v4-flash'
|
submission.ai_model = "deepseek-v4-flash"
|
||||||
submission.processing_time = processing_time
|
submission.processing_time = processing_time
|
||||||
submission.status = FlowchartSubmissionStatus.COMPLETED
|
submission.status = FlowchartSubmissionStatus.COMPLETED
|
||||||
submission.evaluation_time = timezone.now()
|
submission.evaluation_time = timezone.now()
|
||||||
submission.save()
|
submission.save()
|
||||||
|
|
||||||
# 推送评分完成通知
|
# 推送评分完成通知
|
||||||
from utils.websocket import push_flowchart_evaluation_update
|
from utils.websocket import push_flowchart_evaluation_update
|
||||||
|
|
||||||
push_flowchart_evaluation_update(
|
push_flowchart_evaluation_update(
|
||||||
submission_id=str(submission.id),
|
submission_id=str(submission.id),
|
||||||
user_id=submission.user_id,
|
user_id=submission.user_id,
|
||||||
data={
|
data={
|
||||||
"type": "flowchart_evaluation_completed",
|
"type": "flowchart_evaluation_completed",
|
||||||
"score": score_data['score'],
|
"score": score_data["score"],
|
||||||
"grade": score_data['grade'],
|
"grade": score_data["grade"],
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("evaluate_flowchart_task failed for submission %s", submission_id)
|
logger.exception("evaluate_flowchart_task failed for submission %s", submission_id)
|
||||||
if submission is not None:
|
if submission is not None:
|
||||||
@@ -105,6 +103,7 @@ def evaluate_flowchart_task(submission_id):
|
|||||||
submission.save()
|
submission.save()
|
||||||
|
|
||||||
from utils.websocket import push_flowchart_evaluation_update
|
from utils.websocket import push_flowchart_evaluation_update
|
||||||
|
|
||||||
push_flowchart_evaluation_update(
|
push_flowchart_evaluation_update(
|
||||||
submission_id=str(submission.id),
|
submission_id=str(submission.id),
|
||||||
user_id=submission.user_id,
|
user_id=submission.user_id,
|
||||||
@@ -116,9 +115,10 @@ def evaluate_flowchart_task(submission_id):
|
|||||||
)
|
)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
def build_evaluation_prompt(problem):
|
def build_evaluation_prompt(problem):
|
||||||
"""构建AI评分提示词 - 使用固定标准"""
|
"""构建AI评分提示词 - 使用固定标准"""
|
||||||
|
|
||||||
# 使用固定的评分标准
|
# 使用固定的评分标准
|
||||||
criteria_text = """
|
criteria_text = """
|
||||||
- 逻辑正确性 (权重: 1.0, 最高分: 40): 检查流程图的逻辑是否正确,包括条件判断、循环结构等
|
- 逻辑正确性 (权重: 1.0, 最高分: 40): 检查流程图的逻辑是否正确,包括条件判断、循环结构等
|
||||||
@@ -126,7 +126,7 @@ def build_evaluation_prompt(problem):
|
|||||||
- 规范性 (权重: 0.6, 最高分: 20): 检查流程图符号使用是否规范,是否符合标准;不要评价节点ID
|
- 规范性 (权重: 0.6, 最高分: 20): 检查流程图符号使用是否规范,是否符合标准;不要评价节点ID
|
||||||
- 清晰度 (权重: 0.4, 最高分: 10): 评估流程图的整体布局和连线情况;不要因节点ID扣分
|
- 清晰度 (权重: 0.4, 最高分: 10): 评估流程图的整体布局和连线情况;不要因节点ID扣分
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return f"""
|
return f"""
|
||||||
你是一个专业的编程教学助手,负责评估学生提交的Mermaid流程图。
|
你是一个专业的编程教学助手,负责评估学生提交的Mermaid流程图。
|
||||||
|
|
||||||
@@ -166,15 +166,17 @@ def build_evaluation_prompt(problem):
|
|||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def parse_ai_evaluation_response(ai_response):
|
def parse_ai_evaluation_response(ai_response):
|
||||||
"""解析AI评分响应,解析失败时抛出异常由调用方处理"""
|
"""解析AI评分响应,解析失败时抛出异常由调用方处理"""
|
||||||
import re
|
import re
|
||||||
|
|
||||||
# 优先匹配代码块中的 JSON,避免贪婪匹配误抓 reasoning 段落
|
# 优先匹配代码块中的 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:
|
if code_block:
|
||||||
json_str = code_block.group(1)
|
json_str = code_block.group(1)
|
||||||
else:
|
else:
|
||||||
json_match = re.search(r'\{.*\}', ai_response, re.DOTALL)
|
json_match = re.search(r"\{.*\}", ai_response, re.DOTALL)
|
||||||
if not json_match:
|
if not json_match:
|
||||||
raise ValueError("AI响应中未找到JSON数据")
|
raise ValueError("AI响应中未找到JSON数据")
|
||||||
json_str = json_match.group()
|
json_str = json_match.group()
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ from ..views.oj import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('flowchart/submission', FlowchartSubmissionAPI.as_view()),
|
path("flowchart/submission", FlowchartSubmissionAPI.as_view()),
|
||||||
path('flowchart/submissions', FlowchartSubmissionListAPI.as_view()),
|
path("flowchart/submissions", FlowchartSubmissionListAPI.as_view()),
|
||||||
path('flowchart/submission/retry', FlowchartSubmissionRetryAPI.as_view()),
|
path("flowchart/submission/retry", FlowchartSubmissionRetryAPI.as_view()),
|
||||||
path('flowchart/submission/detail', FlowchartSubmissionDetailAPI.as_view()),
|
path("flowchart/submission/detail", FlowchartSubmissionDetailAPI.as_view()),
|
||||||
path('flowchart/submission/current', FlowchartSubmissionCurrentAPI.as_view()),
|
path("flowchart/submission/current", FlowchartSubmissionCurrentAPI.as_view()),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|||||||
@@ -20,16 +20,44 @@ STOPWORDS = frozenset(
|
|||||||
)
|
)
|
||||||
|
|
||||||
CUSTOM_WORDS = [
|
CUSTOM_WORDS = [
|
||||||
"循环结构", "条件判断", "判断条件", "结束条件", "循环条件",
|
"循环结构",
|
||||||
"异常处理", "边界条件", "输入输出", "输入验证",
|
"条件判断",
|
||||||
"开始结束", "结束节点", "开始节点", "判断节点",
|
"判断条件",
|
||||||
"流程走向", "逻辑错误", "逻辑缺陷", "逻辑不清",
|
"结束条件",
|
||||||
"缺少分支", "缺少步骤", "缺少判断", "缺少循环",
|
"循环条件",
|
||||||
"死循环", "无限循环", "循环出口", "循环体",
|
"异常处理",
|
||||||
"条件分支", "分支结构", "分支不全", "分支缺失",
|
"边界条件",
|
||||||
"符号使用", "符号不规范", "连线混乱",
|
"输入输出",
|
||||||
"变量初始化", "赋值操作", "累加操作",
|
"输入验证",
|
||||||
"终止条件", "退出条件", "返回值",
|
"开始结束",
|
||||||
|
"结束节点",
|
||||||
|
"开始节点",
|
||||||
|
"判断节点",
|
||||||
|
"流程走向",
|
||||||
|
"逻辑错误",
|
||||||
|
"逻辑缺陷",
|
||||||
|
"逻辑不清",
|
||||||
|
"缺少分支",
|
||||||
|
"缺少步骤",
|
||||||
|
"缺少判断",
|
||||||
|
"缺少循环",
|
||||||
|
"死循环",
|
||||||
|
"无限循环",
|
||||||
|
"循环出口",
|
||||||
|
"循环体",
|
||||||
|
"条件分支",
|
||||||
|
"分支结构",
|
||||||
|
"分支不全",
|
||||||
|
"分支缺失",
|
||||||
|
"符号使用",
|
||||||
|
"符号不规范",
|
||||||
|
"连线混乱",
|
||||||
|
"变量初始化",
|
||||||
|
"赋值操作",
|
||||||
|
"累加操作",
|
||||||
|
"终止条件",
|
||||||
|
"退出条件",
|
||||||
|
"返回值",
|
||||||
]
|
]
|
||||||
|
|
||||||
for _w in CUSTOM_WORDS:
|
for _w in CUSTOM_WORDS:
|
||||||
@@ -38,7 +66,7 @@ for _w in CUSTOM_WORDS:
|
|||||||
|
|
||||||
def get_real_name(username, class_name):
|
def get_real_name(username, class_name):
|
||||||
if class_name and username.startswith("ks"):
|
if class_name and username.startswith("ks"):
|
||||||
return username[len(f"ks{class_name}"):]
|
return username[len(f"ks{class_name}") :]
|
||||||
return username
|
return username
|
||||||
|
|
||||||
|
|
||||||
@@ -63,9 +91,7 @@ class FlowchartStatisticsAPI(APIView):
|
|||||||
problem_id = request.GET.get("problem_id")
|
problem_id = request.GET.get("problem_id")
|
||||||
if problem_id:
|
if problem_id:
|
||||||
try:
|
try:
|
||||||
problem = Problem.objects.get(
|
problem = Problem.objects.get(_id__iexact=problem_id, contest_id__isnull=True, visible=True)
|
||||||
_id__iexact=problem_id, contest_id__isnull=True, visible=True
|
|
||||||
)
|
|
||||||
except Problem.DoesNotExist:
|
except Problem.DoesNotExist:
|
||||||
return self.error("Problem doesn't exist")
|
return self.error("Problem doesn't exist")
|
||||||
submissions = submissions.filter(problem=problem)
|
submissions = submissions.filter(problem=problem)
|
||||||
@@ -85,23 +111,21 @@ class FlowchartStatisticsAPI(APIView):
|
|||||||
|
|
||||||
total_count = submissions.count()
|
total_count = submissions.count()
|
||||||
if total_count == 0:
|
if total_count == 0:
|
||||||
return self.success({
|
return self.success(
|
||||||
"total_count": 0,
|
{
|
||||||
"avg_score": 0,
|
"total_count": 0,
|
||||||
"grade_distribution": {},
|
"avg_score": 0,
|
||||||
"criteria_averages": {},
|
"grade_distribution": {},
|
||||||
"person_count": len(all_users_dict),
|
"criteria_averages": {},
|
||||||
"completed_count": 0,
|
"person_count": len(all_users_dict),
|
||||||
"word_frequencies": [],
|
"completed_count": 0,
|
||||||
"data_unaccepted": [],
|
"word_frequencies": [],
|
||||||
})
|
"data_unaccepted": [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# 1. Grade distribution
|
# 1. Grade distribution
|
||||||
grade_counts = dict(
|
grade_counts = dict(submissions.values_list("ai_grade").annotate(count=Count("id")).values_list("ai_grade", "count"))
|
||||||
submissions.values_list("ai_grade")
|
|
||||||
.annotate(count=Count("id"))
|
|
||||||
.values_list("ai_grade", "count")
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2. Average score
|
# 2. Average score
|
||||||
avg_score = submissions.aggregate(avg=Avg("ai_score"))["avg"] or 0
|
avg_score = submissions.aggregate(avg=Avg("ai_score"))["avg"] or 0
|
||||||
@@ -113,9 +137,7 @@ class FlowchartStatisticsAPI(APIView):
|
|||||||
|
|
||||||
wordcloud_texts = []
|
wordcloud_texts = []
|
||||||
|
|
||||||
for row in submissions.values_list(
|
for row in submissions.values_list("ai_criteria_details", "ai_feedback", "ai_suggestions").iterator():
|
||||||
"ai_criteria_details", "ai_feedback", "ai_suggestions"
|
|
||||||
).iterator():
|
|
||||||
details, feedback, suggestions = row
|
details, feedback, suggestions = row
|
||||||
if details and isinstance(details, dict):
|
if details and isinstance(details, dict):
|
||||||
for key, val in details.items():
|
for key, val in details.items():
|
||||||
@@ -139,9 +161,7 @@ class FlowchartStatisticsAPI(APIView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 4. Completion stats
|
# 4. Completion stats
|
||||||
submitted_users = set(
|
submitted_users = set(submissions.values_list("user__username", flat=True).distinct())
|
||||||
submissions.values_list("user__username", flat=True).distinct()
|
|
||||||
)
|
|
||||||
completed_count = len(submitted_users)
|
completed_count = len(submitted_users)
|
||||||
|
|
||||||
# Unaccepted users
|
# Unaccepted users
|
||||||
@@ -155,16 +175,18 @@ class FlowchartStatisticsAPI(APIView):
|
|||||||
# 5. Word cloud from feedback + suggestions + criteria comments
|
# 5. Word cloud from feedback + suggestions + criteria comments
|
||||||
word_freq = self._build_word_frequencies(wordcloud_texts)
|
word_freq = self._build_word_frequencies(wordcloud_texts)
|
||||||
|
|
||||||
return self.success({
|
return self.success(
|
||||||
"total_count": total_count,
|
{
|
||||||
"avg_score": round(avg_score, 1),
|
"total_count": total_count,
|
||||||
"grade_distribution": grade_counts,
|
"avg_score": round(avg_score, 1),
|
||||||
"criteria_averages": criteria_averages,
|
"grade_distribution": grade_counts,
|
||||||
"person_count": len(all_users_dict),
|
"criteria_averages": criteria_averages,
|
||||||
"completed_count": completed_count,
|
"person_count": len(all_users_dict),
|
||||||
"word_frequencies": word_freq,
|
"completed_count": completed_count,
|
||||||
"data_unaccepted": unaccepted,
|
"word_frequencies": word_freq,
|
||||||
})
|
"data_unaccepted": unaccepted,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_word_frequencies(texts, top_n=80):
|
def _build_word_frequencies(texts, top_n=80):
|
||||||
|
|||||||
@@ -47,11 +47,7 @@ class FlowchartSubmissionAPI(AsyncAPIView):
|
|||||||
return self.error("submission_id is required")
|
return self.error("submission_id is required")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
submission = await (
|
submission = await FlowchartSubmission.objects.select_related("user", "problem").filter(id=submission_id).afirst()
|
||||||
FlowchartSubmission.objects.select_related("user", "problem")
|
|
||||||
.filter(id=submission_id)
|
|
||||||
.afirst()
|
|
||||||
)
|
|
||||||
if submission is None:
|
if submission is None:
|
||||||
raise FlowchartSubmission.DoesNotExist
|
raise FlowchartSubmission.DoesNotExist
|
||||||
except FlowchartSubmission.DoesNotExist:
|
except FlowchartSubmission.DoesNotExist:
|
||||||
@@ -74,9 +70,7 @@ class FlowchartSubmissionListAPI(AsyncAPIView):
|
|||||||
|
|
||||||
if problem_id:
|
if problem_id:
|
||||||
try:
|
try:
|
||||||
problem = await Problem.objects.aget(
|
problem = await Problem.objects.aget(_id__iexact=problem_id, contest_id__isnull=True, visible=True)
|
||||||
_id__iexact=problem_id, contest_id__isnull=True, visible=True
|
|
||||||
)
|
|
||||||
except Problem.DoesNotExist:
|
except Problem.DoesNotExist:
|
||||||
return self.error("Problem doesn't exist")
|
return self.error("Problem doesn't exist")
|
||||||
queryset = queryset.filter(problem=problem)
|
queryset = queryset.filter(problem=problem)
|
||||||
@@ -90,9 +84,7 @@ class FlowchartSubmissionListAPI(AsyncAPIView):
|
|||||||
|
|
||||||
if request.GET.get("today") == "1":
|
if request.GET.get("today") == "1":
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(create_time__gte=now.replace(hour=0, minute=0, second=0, microsecond=0))
|
||||||
create_time__gte=now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
||||||
)
|
|
||||||
|
|
||||||
grade = request.GET.get("grade")
|
grade = request.GET.get("grade")
|
||||||
if grade in ("S", "A", "B", "C"):
|
if grade in ("S", "A", "B", "C"):
|
||||||
@@ -115,11 +107,7 @@ class FlowchartSubmissionRetryAPI(AsyncAPIView):
|
|||||||
return self.error("submission_id is required")
|
return self.error("submission_id is required")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
submission = await (
|
submission = await FlowchartSubmission.objects.select_related("problem").filter(id=submission_id).afirst()
|
||||||
FlowchartSubmission.objects.select_related("problem")
|
|
||||||
.filter(id=submission_id)
|
|
||||||
.afirst()
|
|
||||||
)
|
|
||||||
if submission is None:
|
if submission is None:
|
||||||
raise FlowchartSubmission.DoesNotExist
|
raise FlowchartSubmission.DoesNotExist
|
||||||
except FlowchartSubmission.DoesNotExist:
|
except FlowchartSubmission.DoesNotExist:
|
||||||
@@ -187,7 +175,7 @@ class FlowchartSubmissionDetailAPI(AsyncAPIView):
|
|||||||
else:
|
else:
|
||||||
if page < 0 or page > count:
|
if page < 0 or page > count:
|
||||||
return self.error("Page out of range")
|
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]
|
submission = result[0]
|
||||||
data = await self.async_serialize_data(FlowchartSubmissionSerializer, submission)
|
data = await self.async_serialize_data(FlowchartSubmissionSerializer, submission)
|
||||||
return self.success({"submission": data, "count": count})
|
return self.success({"submission": data, "count": count})
|
||||||
|
|||||||
@@ -35,14 +35,24 @@ class FPSParser(object):
|
|||||||
def _parse_one_problem(self, node):
|
def _parse_one_problem(self, node):
|
||||||
sample_start = True
|
sample_start = True
|
||||||
test_case_start = True
|
test_case_start = True
|
||||||
problem = {"title": "No Title", "description": "No Description",
|
problem = {
|
||||||
"input": "No Input Description",
|
"title": "No Title",
|
||||||
"output": "No Output Description",
|
"description": "No Description",
|
||||||
"memory_limit": {"unit": None, "value": None},
|
"input": "No Input Description",
|
||||||
"time_limit": {"unit": None, "value": None},
|
"output": "No Output Description",
|
||||||
"samples": [], "images": [], "append": [],
|
"memory_limit": {"unit": None, "value": None},
|
||||||
"template": [], "prepend": [], "test_cases": [],
|
"time_limit": {"unit": None, "value": None},
|
||||||
"hint": None, "source": None, "spj": None, "solution": []}
|
"samples": [],
|
||||||
|
"images": [],
|
||||||
|
"append": [],
|
||||||
|
"template": [],
|
||||||
|
"prepend": [],
|
||||||
|
"test_cases": [],
|
||||||
|
"hint": None,
|
||||||
|
"source": None,
|
||||||
|
"spj": None,
|
||||||
|
"solution": [],
|
||||||
|
}
|
||||||
for item in node:
|
for item in node:
|
||||||
tag = item.tag
|
tag = item.tag
|
||||||
if tag in ["title", "description", "input", "output", "hint", "source"]:
|
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:
|
with open(os.path.join(base_dir, str(index + 1) + ".out"), "w", encoding="utf-8") as f:
|
||||||
f.write(output_content)
|
f.write(output_content)
|
||||||
if spj:
|
if spj:
|
||||||
one_info = {
|
one_info = {"input_size": len(input_content), "input_name": f"{index + 1}.in"}
|
||||||
"input_size": len(input_content),
|
|
||||||
"input_name": f"{index + 1}.in"
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
one_info = {
|
one_info = {
|
||||||
"input_size": len(input_content),
|
"input_size": len(input_content),
|
||||||
"input_name": f"{index + 1}.in",
|
"input_name": f"{index + 1}.in",
|
||||||
"output_size": len(output_content),
|
"output_size": len(output_content),
|
||||||
"output_name": f"{index + 1}.out",
|
"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
|
test_cases[index] = one_info
|
||||||
info = {
|
info = {"spj": True if spj else False, "test_cases": test_cases}
|
||||||
"spj": True if spj else False,
|
|
||||||
"test_cases": test_cases
|
|
||||||
}
|
|
||||||
with open(os.path.join(base_dir, "info"), "w", encoding="utf-8") as f:
|
with open(os.path.join(base_dir, "info"), "w", encoding="utf-8") as f:
|
||||||
f.write(json.dumps(info, indent=4))
|
f.write(json.dumps(info, indent=4))
|
||||||
return info
|
return info
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
import django
|
import django
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
|
|
||||||
sys.stdout.write("Django VERSION " + str(django.VERSION) + "\n")
|
sys.stdout.write("Django VERSION " + str(django.VERSION) + "\n")
|
||||||
|
|
||||||
execute_from_command_line(sys.argv)
|
execute_from_command_line(sys.argv)
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ from utils.models import RichTextField
|
|||||||
|
|
||||||
class Message(models.Model):
|
class Message(models.Model):
|
||||||
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name="sender")
|
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name="sender")
|
||||||
recipient = models.ForeignKey(
|
recipient = models.ForeignKey(User, on_delete=models.CASCADE, related_name="recipient")
|
||||||
User, on_delete=models.CASCADE, related_name="recipient"
|
|
||||||
)
|
|
||||||
submission = models.ForeignKey(Submission, on_delete=models.CASCADE)
|
submission = models.ForeignKey(Submission, on_delete=models.CASCADE)
|
||||||
message = RichTextField()
|
message = RichTextField()
|
||||||
create_time = models.DateTimeField(auto_now_add=True)
|
create_time = models.DateTimeField(auto_now_add=True)
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ from utils.api.api import validate_serializer
|
|||||||
class MessageAPI(AsyncAPIView):
|
class MessageAPI(AsyncAPIView):
|
||||||
@login_required
|
@login_required
|
||||||
async def get(self, request):
|
async def get(self, request):
|
||||||
messages = Message.objects.select_related(
|
messages = Message.objects.select_related("recipient", "sender", "submission", "submission__problem").filter(recipient=request.user)
|
||||||
"recipient", "sender", "submission", "submission__problem"
|
|
||||||
).filter(recipient=request.user)
|
|
||||||
return self.success(await self.async_paginate_data(request, messages, MessageSerializer))
|
return self.success(await self.async_paginate_data(request, messages, MessageSerializer))
|
||||||
|
|
||||||
@super_admin_required
|
@super_admin_required
|
||||||
|
|||||||
@@ -28,4 +28,3 @@ application = ProtocolTypeRouter(
|
|||||||
"websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns)),
|
"websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns)),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -13,4 +13,3 @@ websocket_urlpatterns = [
|
|||||||
path("ws/config/", ConfigConsumer.as_asgi()),
|
path("ws/config/", ConfigConsumer.as_asgi()),
|
||||||
path("ws/flowchart/", FlowchartConsumer.as_asgi()),
|
path("ws/flowchart/", FlowchartConsumer.as_asgi()),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class my_property:
|
|||||||
2. ttl is callable,条件缓存
|
2. ttl is callable,条件缓存
|
||||||
3. 缓存 ttl 秒
|
3. 缓存 ttl 秒
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, func=None, fset=None, ttl=None):
|
def __init__(self, func=None, fset=None, ttl=None):
|
||||||
self.fset = fset
|
self.fset = fset
|
||||||
self.local = threading.local()
|
self.local = threading.local()
|
||||||
@@ -118,8 +119,7 @@ class OptionDefaultValue:
|
|||||||
class_list = []
|
class_list = []
|
||||||
smtp_config = {}
|
smtp_config = {}
|
||||||
judge_server_token = default_token
|
judge_server_token = default_token
|
||||||
throttling = {"ip": {"capacity": 100, "fill_rate": 0.1, "default_capacity": 50},
|
throttling = {"ip": {"capacity": 100, "fill_rate": 0.1, "default_capacity": 50}, "user": {"capacity": 20, "fill_rate": 0.03, "default_capacity": 10}}
|
||||||
"user": {"capacity": 20, "fill_rate": 0.03, "default_capacity": 10}}
|
|
||||||
languages = languages
|
languages = languages
|
||||||
enable_maxkb = True
|
enable_maxkb = True
|
||||||
|
|
||||||
@@ -286,7 +286,6 @@ class _SysOptionsMeta(type):
|
|||||||
def enable_maxkb(cls, value):
|
def enable_maxkb(cls, value):
|
||||||
cls._set_option(OptionKeys.enable_maxkb, value)
|
cls._set_option(OptionKeys.enable_maxkb, value)
|
||||||
|
|
||||||
|
|
||||||
def reset_languages(cls):
|
def reset_languages(cls):
|
||||||
cls.languages = languages
|
cls.languages = languages
|
||||||
|
|
||||||
@@ -295,6 +294,7 @@ class SysOptions(metaclass=_SysOptionsMeta):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def aget(cls, key):
|
async def aget(cls, key):
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
|
|
||||||
return await sync_to_async(getattr)(cls, key)
|
return await sync_to_async(getattr)(cls, key)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -303,4 +303,5 @@ class SysOptions(metaclass=_SysOptionsMeta):
|
|||||||
|
|
||||||
def _get_all():
|
def _get_all():
|
||||||
return {k: getattr(cls, k) for k in keys}
|
return {k: getattr(cls, k) for k in keys}
|
||||||
|
|
||||||
return await sync_to_async(_get_all)()
|
return await sync_to_async(_get_all)()
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
fixed_count = 0
|
fixed_count = 0
|
||||||
for progress in progresses:
|
for progress in progresses:
|
||||||
problemset_problems = ProblemSetProblem.objects.filter(
|
problemset_problems = ProblemSetProblem.objects.filter(problemset=progress.problemset).select_related("problem")
|
||||||
problemset=progress.problemset
|
|
||||||
).select_related("problem")
|
|
||||||
|
|
||||||
updated = False
|
updated = False
|
||||||
for psp in problemset_problems:
|
for psp in problemset_problems:
|
||||||
@@ -46,10 +44,7 @@ class Command(BaseCommand):
|
|||||||
if not accepted:
|
if not accepted:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.stdout.write(
|
self.stdout.write(f" 用户 {progress.user.username} | 题单「{progress.problemset.title}」 | 题目 {psp.problem._id} 已AC但进度未记录")
|
||||||
f" 用户 {progress.user.username} | 题单「{progress.problemset.title}」"
|
|
||||||
f" | 题目 {psp.problem._id} 已AC但进度未记录"
|
|
||||||
)
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -16,21 +16,19 @@ def sync_progress_on_problem_change(sender, instance, created, **kwargs):
|
|||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# 获取该题单的所有用户进度
|
# 获取该题单的所有用户进度
|
||||||
progresses = ProblemSetProgress.objects.filter(
|
progresses = ProblemSetProgress.objects.filter(problemset=instance.problemset)
|
||||||
problemset=instance.problemset
|
|
||||||
)
|
|
||||||
|
|
||||||
# 批量更新所有用户的进度
|
# 批量更新所有用户的进度
|
||||||
for progress in progresses:
|
for progress in progresses:
|
||||||
progress.update_progress()
|
progress.update_progress()
|
||||||
|
|
||||||
# 重新计算该题单的所有徽章资格
|
# 重新计算该题单的所有徽章资格
|
||||||
badges = ProblemSetBadge.objects.filter(problemset=instance.problemset)
|
badges = ProblemSetBadge.objects.filter(problemset=instance.problemset)
|
||||||
for badge in badges:
|
for badge in badges:
|
||||||
badge.recalculate_user_badges()
|
badge.recalculate_user_badges()
|
||||||
|
|
||||||
logger.info(f"已同步题单 {instance.problemset.id} 的所有用户进度和徽章资格")
|
logger.info(f"已同步题单 {instance.problemset.id} 的所有用户进度和徽章资格")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"同步题单进度时出错: {e}")
|
logger.error(f"同步题单进度时出错: {e}")
|
||||||
|
|
||||||
@@ -42,27 +40,23 @@ def sync_progress_on_problem_delete(sender, instance, **kwargs):
|
|||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# 清理该题目在题单中的所有提交记录
|
# 清理该题目在题单中的所有提交记录
|
||||||
from .models import ProblemSetSubmission
|
from .models import ProblemSetSubmission
|
||||||
ProblemSetSubmission.objects.filter(
|
|
||||||
problemset=instance.problemset,
|
ProblemSetSubmission.objects.filter(problemset=instance.problemset, problem=instance.problem).delete()
|
||||||
problem=instance.problem
|
|
||||||
).delete()
|
|
||||||
|
|
||||||
# 获取该题单的所有用户进度
|
# 获取该题单的所有用户进度
|
||||||
progresses = ProblemSetProgress.objects.filter(
|
progresses = ProblemSetProgress.objects.filter(problemset=instance.problemset)
|
||||||
problemset=instance.problemset
|
|
||||||
)
|
|
||||||
|
|
||||||
# 批量更新所有用户的进度
|
# 批量更新所有用户的进度
|
||||||
for progress in progresses:
|
for progress in progresses:
|
||||||
progress.update_progress()
|
progress.update_progress()
|
||||||
|
|
||||||
# 重新计算该题单的所有徽章资格
|
# 重新计算该题单的所有徽章资格
|
||||||
badges = ProblemSetBadge.objects.filter(problemset=instance.problemset)
|
badges = ProblemSetBadge.objects.filter(problemset=instance.problemset)
|
||||||
for badge in badges:
|
for badge in badges:
|
||||||
badge.recalculate_user_badges()
|
badge.recalculate_user_badges()
|
||||||
|
|
||||||
logger.info(f"已同步题单 {instance.problemset.id} 的所有用户进度和徽章资格(删除题目后)")
|
logger.info(f"已同步题单 {instance.problemset.id} 的所有用户进度和徽章资格(删除题目后)")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"同步题单进度时出错: {e}")
|
logger.error(f"同步题单进度时出错: {e}")
|
||||||
|
|
||||||
@@ -75,7 +69,7 @@ def sync_badges_on_badge_change(sender, instance, created, **kwargs):
|
|||||||
# 重新计算该奖章的所有用户资格
|
# 重新计算该奖章的所有用户资格
|
||||||
instance.recalculate_user_badges()
|
instance.recalculate_user_badges()
|
||||||
logger.info(f"已重新计算题单 {instance.problemset.id} 的奖章 {instance.id} 的用户资格")
|
logger.info(f"已重新计算题单 {instance.problemset.id} 的奖章 {instance.id} 的用户资格")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"重新计算奖章资格时出错: {e}")
|
logger.error(f"重新计算奖章资格时出错: {e}")
|
||||||
|
|
||||||
@@ -88,6 +82,6 @@ def cleanup_badges_on_badge_delete(sender, instance, **kwargs):
|
|||||||
# 删除该奖章的所有用户奖章记录
|
# 删除该奖章的所有用户奖章记录
|
||||||
UserBadge.objects.filter(badge=instance).delete()
|
UserBadge.objects.filter(badge=instance).delete()
|
||||||
logger.info(f"已清理奖章 {instance.id} 的所有用户奖章记录")
|
logger.info(f"已清理奖章 {instance.id} 的所有用户奖章记录")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"清理用户奖章记录时出错: {e}")
|
logger.error(f"清理用户奖章记录时出错: {e}")
|
||||||
|
|||||||
@@ -76,8 +76,7 @@ class ProblemSetAPI(AsyncAPIView):
|
|||||||
|
|
||||||
# 批量查询用户已获得的奖章ID(这些题单相关的)
|
# 批量查询用户已获得的奖章ID(这些题单相关的)
|
||||||
user_earned_badge_ids = {
|
user_earned_badge_ids = {
|
||||||
badge_id
|
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)
|
||||||
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时也预加载)
|
# 预加载奖章信息(在获取ID之后应用,避免在获取ID时也预加载)
|
||||||
@@ -97,12 +96,7 @@ class ProblemSetDetailAPI(AsyncAPIView):
|
|||||||
async def get(self, request, problem_set_id):
|
async def get(self, request, problem_set_id):
|
||||||
"""获取题单详情"""
|
"""获取题单详情"""
|
||||||
try:
|
try:
|
||||||
problem_set = await (
|
problem_set = await ProblemSet.objects.select_related("created_by").filter(id=problem_set_id, visible=True).exclude(status=ProblemSetStatus.DRAFT).aget()
|
||||||
ProblemSet.objects.select_related("created_by")
|
|
||||||
.filter(id=problem_set_id, visible=True)
|
|
||||||
.exclude(status=ProblemSetStatus.DRAFT)
|
|
||||||
.aget()
|
|
||||||
)
|
|
||||||
except ProblemSet.DoesNotExist:
|
except ProblemSet.DoesNotExist:
|
||||||
return self.error("题单不存在")
|
return self.error("题单不存在")
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,7 @@ def bulk_fetch_problemset_progress(user, problem_ids):
|
|||||||
problemset__status=ProblemSetStatus.ACTIVE,
|
problemset__status=ProblemSetStatus.ACTIVE,
|
||||||
problemset__problemsetproblem__problem_id__in=problem_ids,
|
problemset__problemsetproblem__problem_id__in=problem_ids,
|
||||||
)
|
)
|
||||||
.filter(
|
.filter(models.Q(problemset__end_time__isnull=True) | models.Q(problemset__end_time__gt=timezone.now()))
|
||||||
models.Q(problemset__end_time__isnull=True)
|
|
||||||
| models.Q(problemset__end_time__gt=timezone.now())
|
|
||||||
)
|
|
||||||
.annotate(matched_problem_id=F("problemset__problemsetproblem__problem_id"))
|
.annotate(matched_problem_id=F("problemset__problemsetproblem__problem_id"))
|
||||||
.only("join_time", "progress_detail")
|
.only("join_time", "progress_detail")
|
||||||
)
|
)
|
||||||
@@ -51,7 +48,6 @@ class ShareSubmissionSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
|
|
||||||
class SubmissionModelSerializer(serializers.ModelSerializer):
|
class SubmissionModelSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Submission
|
model = Submission
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
@@ -92,11 +88,7 @@ class SubmissionListSerializer(serializers.ModelSerializer):
|
|||||||
# 如果该题目已在题单中做出来了,则恢复显示
|
# 如果该题目已在题单中做出来了,则恢复显示
|
||||||
if obj.user_id == self.user.id and self.user.is_regular_user():
|
if obj.user_id == self.user.id and self.user.is_regular_user():
|
||||||
progress = self._get_problemset_progress(obj.problem_id)
|
progress = self._get_problemset_progress(obj.problem_id)
|
||||||
if (
|
if progress and obj.create_time < progress.join_time and str(obj.problem_id) not in progress.progress_detail:
|
||||||
progress
|
|
||||||
and obj.create_time < progress.join_time
|
|
||||||
and str(obj.problem_id) not in progress.progress_detail
|
|
||||||
):
|
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -111,10 +103,7 @@ class SubmissionListSerializer(serializers.ModelSerializer):
|
|||||||
problemset__status=ProblemSetStatus.ACTIVE,
|
problemset__status=ProblemSetStatus.ACTIVE,
|
||||||
problemset__problemsetproblem__problem_id=problem_id,
|
problemset__problemsetproblem__problem_id=problem_id,
|
||||||
)
|
)
|
||||||
.filter(
|
.filter(models.Q(problemset__end_time__isnull=True) | models.Q(problemset__end_time__gt=timezone.now()))
|
||||||
models.Q(problemset__end_time__isnull=True)
|
|
||||||
| models.Q(problemset__end_time__gt=timezone.now())
|
|
||||||
)
|
|
||||||
.only("join_time", "progress_detail")
|
.only("join_time", "progress_detail")
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ class SubmissionRejudgeAPI(APIView):
|
|||||||
if not id:
|
if not id:
|
||||||
return self.error("Parameter error, id is required")
|
return self.error("Parameter error, id is required")
|
||||||
try:
|
try:
|
||||||
submission = Submission.objects.select_related("problem").get(
|
submission = Submission.objects.select_related("problem").get(id=id, contest_id__isnull=True)
|
||||||
id=id, contest_id__isnull=True
|
|
||||||
)
|
|
||||||
except Submission.DoesNotExist:
|
except Submission.DoesNotExist:
|
||||||
return self.error("Submission does not exists")
|
return self.error("Submission does not exists")
|
||||||
submission.statistic_info = {}
|
submission.statistic_info = {}
|
||||||
@@ -46,17 +44,13 @@ class SubmissionStatisticsAPI(APIView):
|
|||||||
filters = {"contest_id__isnull": True, "create_time__lte": end}
|
filters = {"contest_id__isnull": True, "create_time__lte": end}
|
||||||
if start:
|
if start:
|
||||||
filters["create_time__gte"] = start
|
filters["create_time__gte"] = start
|
||||||
submissions = Submission.objects.filter(
|
submissions = Submission.objects.filter(**filters).select_related("problem__created_by")
|
||||||
**filters
|
|
||||||
).select_related("problem__created_by")
|
|
||||||
|
|
||||||
problem_id = request.GET.get("problem_id")
|
problem_id = request.GET.get("problem_id")
|
||||||
|
|
||||||
if problem_id:
|
if problem_id:
|
||||||
try:
|
try:
|
||||||
problem = Problem.objects.get(
|
problem = Problem.objects.get(_id__iexact=problem_id, contest_id__isnull=True, visible=True)
|
||||||
_id__iexact=problem_id, contest_id__isnull=True, visible=True
|
|
||||||
)
|
|
||||||
except Problem.DoesNotExist:
|
except Problem.DoesNotExist:
|
||||||
return self.error("Problem doesn't exist")
|
return self.error("Problem doesn't exist")
|
||||||
submissions = submissions.filter(problem=problem)
|
submissions = submissions.filter(problem=problem)
|
||||||
@@ -82,9 +76,7 @@ class SubmissionStatisticsAPI(APIView):
|
|||||||
)
|
)
|
||||||
submission_count = submission_stats["total_count"]
|
submission_count = submission_stats["total_count"]
|
||||||
accepted_count = submission_stats["accepted_count"]
|
accepted_count = submission_stats["accepted_count"]
|
||||||
correct_rate = (
|
correct_rate = round(accepted_count / submission_count * 100, 2) if submission_count else 0
|
||||||
round(accepted_count / submission_count * 100, 2) if submission_count else 0
|
|
||||||
)
|
|
||||||
|
|
||||||
# 优化:获取用户提交统计
|
# 优化:获取用户提交统计
|
||||||
user_submissions = (
|
user_submissions = (
|
||||||
@@ -99,20 +91,13 @@ class SubmissionStatisticsAPI(APIView):
|
|||||||
# 获取所有有提交记录的用户的class_name信息
|
# 获取所有有提交记录的用户的class_name信息
|
||||||
submitted_usernames = {item["username"] for item in user_submissions}
|
submitted_usernames = {item["username"] for item in user_submissions}
|
||||||
if submitted_usernames:
|
if submitted_usernames:
|
||||||
submitted_users_dict = {
|
submitted_users_dict = {user["username"]: user["class_name"] for user in User.objects.filter(username__in=submitted_usernames).values("username", "class_name")}
|
||||||
user["username"]: user["class_name"]
|
|
||||||
for user in User.objects.filter(
|
|
||||||
username__in=submitted_usernames
|
|
||||||
).values("username", "class_name")
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
submitted_users_dict = {}
|
submitted_users_dict = {}
|
||||||
|
|
||||||
# 预先收集每个用户的提交ID和结果,按时间倒序
|
# 预先收集每个用户的提交ID和结果,按时间倒序
|
||||||
submission_items_by_user = {}
|
submission_items_by_user = {}
|
||||||
for submission in submissions.values("username", "id", "result").order_by(
|
for submission in submissions.values("username", "id", "result").order_by("-create_time"):
|
||||||
"-create_time"
|
|
||||||
):
|
|
||||||
username_key = submission["username"]
|
username_key = submission["username"]
|
||||||
submission_id = str(submission["id"])
|
submission_id = str(submission["id"])
|
||||||
submission_items_by_user.setdefault(username_key, []).append(
|
submission_items_by_user.setdefault(username_key, []).append(
|
||||||
@@ -137,9 +122,7 @@ class SubmissionStatisticsAPI(APIView):
|
|||||||
"submission_count": item["submission_count"],
|
"submission_count": item["submission_count"],
|
||||||
"accepted_count": item["accepted_count"],
|
"accepted_count": item["accepted_count"],
|
||||||
"correct_rate": f"{rate}%",
|
"correct_rate": f"{rate}%",
|
||||||
"submission_items": submission_items_by_user.get(
|
"submission_items": submission_items_by_user.get(username_key, []),
|
||||||
username_key, []
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -37,9 +37,7 @@ class SubmissionAPI(AsyncAPIView):
|
|||||||
auth_method = getattr(request, "auth_method", "")
|
auth_method = getattr(request, "auth_method", "")
|
||||||
if auth_method == "api_key":
|
if auth_method == "api_key":
|
||||||
return
|
return
|
||||||
user_bucket = TokenBucket(
|
user_bucket = TokenBucket(key=str(request.user.id), redis_conn=cache, **SysOptions.throttling["user"])
|
||||||
key=str(request.user.id), redis_conn=cache, **SysOptions.throttling["user"]
|
|
||||||
)
|
|
||||||
can_consume, wait = user_bucket.consume()
|
can_consume, wait = user_bucket.consume()
|
||||||
if not can_consume:
|
if not can_consume:
|
||||||
return "Please wait %d seconds" % (int(wait))
|
return "Please wait %d seconds" % (int(wait))
|
||||||
@@ -52,10 +50,7 @@ class SubmissionAPI(AsyncAPIView):
|
|||||||
if not request.user.is_contest_admin(contest):
|
if not request.user.is_contest_admin(contest):
|
||||||
user_ip = ipaddress.ip_address(request.session.get("ip"))
|
user_ip = ipaddress.ip_address(request.session.get("ip"))
|
||||||
if contest.allowed_ip_ranges:
|
if contest.allowed_ip_ranges:
|
||||||
if not any(
|
if not any(user_ip in ipaddress.ip_network(cidr, strict=False) for cidr in contest.allowed_ip_ranges):
|
||||||
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")
|
return self.error("Your IP is not allowed in this contest")
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -79,9 +74,7 @@ class SubmissionAPI(AsyncAPIView):
|
|||||||
return self.error(error)
|
return self.error(error)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
problem = await Problem.objects.aget(
|
problem = await Problem.objects.aget(id=data["problem_id"], contest_id=data.get("contest_id"), visible=True)
|
||||||
id=data["problem_id"], contest_id=data.get("contest_id"), visible=True
|
|
||||||
)
|
|
||||||
except Problem.DoesNotExist:
|
except Problem.DoesNotExist:
|
||||||
return self.error("Problem not exist")
|
return self.error("Problem not exist")
|
||||||
if data["language"] not in problem.languages:
|
if data["language"] not in problem.languages:
|
||||||
@@ -108,9 +101,7 @@ class SubmissionAPI(AsyncAPIView):
|
|||||||
if not submission_id:
|
if not submission_id:
|
||||||
return self.error("Parameter id doesn't exist")
|
return self.error("Parameter id doesn't exist")
|
||||||
try:
|
try:
|
||||||
submission = await Submission.objects.select_related("problem", "contest").aget(
|
submission = await Submission.objects.select_related("problem", "contest").aget(id=submission_id)
|
||||||
id=submission_id
|
|
||||||
)
|
|
||||||
except Submission.DoesNotExist:
|
except Submission.DoesNotExist:
|
||||||
return self.error("Submission doesn't exist")
|
return self.error("Submission doesn't exist")
|
||||||
if not submission.check_user_permission(request.user):
|
if not submission.check_user_permission(request.user):
|
||||||
@@ -120,26 +111,19 @@ class SubmissionAPI(AsyncAPIView):
|
|||||||
submission_data = await self.async_serialize_data(SubmissionModelSerializer, submission)
|
submission_data = await self.async_serialize_data(SubmissionModelSerializer, submission)
|
||||||
else:
|
else:
|
||||||
submission_data = await self.async_serialize_data(SubmissionSafeModelSerializer, submission)
|
submission_data = await self.async_serialize_data(SubmissionSafeModelSerializer, submission)
|
||||||
submission_data["can_unshare"] = submission.check_user_permission(
|
submission_data["can_unshare"] = submission.check_user_permission(request.user, check_share=False)
|
||||||
request.user, check_share=False
|
|
||||||
)
|
|
||||||
return self.success(submission_data)
|
return self.success(submission_data)
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@validate_serializer(ShareSubmissionSerializer)
|
@validate_serializer(ShareSubmissionSerializer)
|
||||||
async def put(self, request):
|
async def put(self, request):
|
||||||
try:
|
try:
|
||||||
submission = await Submission.objects.select_related("problem", "contest").aget(
|
submission = await Submission.objects.select_related("problem", "contest").aget(id=request.data["id"])
|
||||||
id=request.data["id"]
|
|
||||||
)
|
|
||||||
except Submission.DoesNotExist:
|
except Submission.DoesNotExist:
|
||||||
return self.error("Submission doesn't exist")
|
return self.error("Submission doesn't exist")
|
||||||
if not submission.check_user_permission(request.user, check_share=False):
|
if not submission.check_user_permission(request.user, check_share=False):
|
||||||
return self.error("No permission to share the submission")
|
return self.error("No permission to share the submission")
|
||||||
if (
|
if submission.contest and submission.contest.status == ContestStatus.CONTEST_UNDERWAY:
|
||||||
submission.contest
|
|
||||||
and submission.contest.status == ContestStatus.CONTEST_UNDERWAY
|
|
||||||
):
|
|
||||||
return self.error("Can not share submission now")
|
return self.error("Can not share submission now")
|
||||||
submission.shared = request.data["shared"]
|
submission.shared = request.data["shared"]
|
||||||
await submission.asave(update_fields=["shared"])
|
await submission.asave(update_fields=["shared"])
|
||||||
@@ -153,9 +137,7 @@ class SubmissionListAPI(AsyncAPIView):
|
|||||||
if request.GET.get("contest_id"):
|
if request.GET.get("contest_id"):
|
||||||
return self.error("Parameter error")
|
return self.error("Parameter error")
|
||||||
|
|
||||||
submissions = Submission.objects.filter(contest_id__isnull=True).select_related(
|
submissions = Submission.objects.filter(contest_id__isnull=True).select_related("problem").order_by("-create_time")
|
||||||
"problem"
|
|
||||||
).order_by("-create_time")
|
|
||||||
problem_id = request.GET.get("problem_id")
|
problem_id = request.GET.get("problem_id")
|
||||||
myself = request.GET.get("myself")
|
myself = request.GET.get("myself")
|
||||||
result = request.GET.get("result")
|
result = request.GET.get("result")
|
||||||
@@ -163,9 +145,7 @@ class SubmissionListAPI(AsyncAPIView):
|
|||||||
language = request.GET.get("language")
|
language = request.GET.get("language")
|
||||||
if problem_id:
|
if problem_id:
|
||||||
try:
|
try:
|
||||||
problem = await Problem.objects.aget(
|
problem = await Problem.objects.aget(_id__iexact=problem_id, contest_id__isnull=True, visible=True)
|
||||||
_id__iexact=problem_id, contest_id__isnull=True, visible=True
|
|
||||||
)
|
|
||||||
except Problem.DoesNotExist:
|
except Problem.DoesNotExist:
|
||||||
return self.error("Problem doesn't exist")
|
return self.error("Problem doesn't exist")
|
||||||
submissions = submissions.filter(problem=problem)
|
submissions = submissions.filter(problem=problem)
|
||||||
@@ -184,9 +164,7 @@ class SubmissionListAPI(AsyncAPIView):
|
|||||||
submissions = submissions.filter(language=language)
|
submissions = submissions.filter(language=language)
|
||||||
if request.GET.get("today") == "1":
|
if request.GET.get("today") == "1":
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
submissions = submissions.filter(
|
submissions = submissions.filter(create_time__gte=now.replace(hour=0, minute=0, second=0, microsecond=0))
|
||||||
create_time__gte=now.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
||||||
)
|
|
||||||
|
|
||||||
data = await self.async_paginate_data(request, submissions)
|
data = await self.async_paginate_data(request, submissions)
|
||||||
results = data["results"]
|
results = data["results"]
|
||||||
@@ -212,18 +190,14 @@ class ContestSubmissionListAPI(AsyncAPIView):
|
|||||||
return self.error("Limit is needed")
|
return self.error("Limit is needed")
|
||||||
|
|
||||||
contest = self.contest
|
contest = self.contest
|
||||||
submissions = Submission.objects.filter(contest_id=contest.id).select_related(
|
submissions = Submission.objects.filter(contest_id=contest.id).select_related("problem", "contest").order_by("-create_time")
|
||||||
"problem", "contest"
|
|
||||||
).order_by("-create_time")
|
|
||||||
problem_id = request.GET.get("problem_id")
|
problem_id = request.GET.get("problem_id")
|
||||||
myself = request.GET.get("myself")
|
myself = request.GET.get("myself")
|
||||||
result = request.GET.get("result")
|
result = request.GET.get("result")
|
||||||
username = request.GET.get("username")
|
username = request.GET.get("username")
|
||||||
if problem_id:
|
if problem_id:
|
||||||
try:
|
try:
|
||||||
problem = await Problem.objects.aget(
|
problem = await Problem.objects.aget(_id__iexact=problem_id, contest_id=contest.id, visible=True)
|
||||||
_id__iexact=problem_id, contest_id=contest.id, visible=True
|
|
||||||
)
|
|
||||||
except Problem.DoesNotExist:
|
except Problem.DoesNotExist:
|
||||||
return self.error("Problem doesn't exist")
|
return self.error("Problem doesn't exist")
|
||||||
submissions = submissions.filter(problem=problem)
|
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)
|
progress_cache = await sync_to_async(bulk_fetch_problemset_progress)(request.user, problem_ids)
|
||||||
else:
|
else:
|
||||||
progress_cache = {}
|
progress_cache = {}
|
||||||
data["results"] = await self.async_serialize_data(
|
data["results"] = await self.async_serialize_data(SubmissionListSerializer, results, many=True, user=request.user, problemset_progress_cache=progress_cache)
|
||||||
SubmissionListSerializer,
|
|
||||||
results, many=True, user=request.user, problemset_progress_cache=progress_cache
|
|
||||||
)
|
|
||||||
return self.success(data)
|
return self.success(data)
|
||||||
|
|
||||||
|
|
||||||
@@ -257,12 +228,7 @@ class SubmissionExistsAPI(AsyncAPIView):
|
|||||||
async def get(self, request):
|
async def get(self, request):
|
||||||
if not request.GET.get("problem_id"):
|
if not request.GET.get("problem_id"):
|
||||||
return self.error("Parameter error, problem_id is required")
|
return self.error("Parameter error, problem_id is required")
|
||||||
exists = (
|
exists = request.user.is_authenticated and await Submission.objects.filter(problem_id=request.GET["problem_id"], user_id=request.user.id).aexists()
|
||||||
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)
|
return self.success(exists)
|
||||||
|
|
||||||
|
|
||||||
@@ -271,13 +237,9 @@ class SubmissionsTodayCount(AsyncAPIView):
|
|||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
if request.GET.get("language") == "Flowchart":
|
if request.GET.get("language") == "Flowchart":
|
||||||
count = await FlowchartSubmission.objects.filter(
|
count = await FlowchartSubmission.objects.filter(create_time__gte=start).acount()
|
||||||
create_time__gte=start
|
|
||||||
).acount()
|
|
||||||
else:
|
else:
|
||||||
count = await Submission.objects.filter(
|
count = await Submission.objects.filter(contest_id__isnull=True, create_time__gte=start).acount()
|
||||||
contest_id__isnull=True, create_time__gte=start
|
|
||||||
).acount()
|
|
||||||
return self.success(count)
|
return self.success(count)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -53,19 +53,10 @@ class TutorialAdminAPI(APIView):
|
|||||||
return self.success(TutorialSerializer(tutorial).data)
|
return self.success(TutorialSerializer(tutorial).data)
|
||||||
except Tutorial.DoesNotExist:
|
except Tutorial.DoesNotExist:
|
||||||
return self.error("Tutorial does not exist")
|
return self.error("Tutorial does not exist")
|
||||||
|
|
||||||
tutorials = Tutorial.objects.all().order_by("order", "-created_at")
|
tutorials = Tutorial.objects.all().order_by("order", "-created_at")
|
||||||
# 按 type 分组返回数据
|
# 按 type 分组返回数据
|
||||||
result = {
|
result = {"python": TutorialListSerializer(tutorials.filter(type="python"), many=True).data, "c": TutorialListSerializer(tutorials.filter(type="c"), many=True).data}
|
||||||
"python": TutorialListSerializer(
|
|
||||||
tutorials.filter(type="python"),
|
|
||||||
many=True
|
|
||||||
).data,
|
|
||||||
"c": TutorialListSerializer(
|
|
||||||
tutorials.filter(type="c"),
|
|
||||||
many=True
|
|
||||||
).data
|
|
||||||
}
|
|
||||||
return self.success(result)
|
return self.success(result)
|
||||||
|
|
||||||
@super_admin_required
|
@super_admin_required
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ class TutorialAPI(APIView):
|
|||||||
class TutorialTitlesAPI(APIView):
|
class TutorialTitlesAPI(APIView):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
type = request.GET.get("type") or "python"
|
type = request.GET.get("type") or "python"
|
||||||
tutorials = Tutorial.objects.filter(is_public=True, type=type).values(
|
tutorials = Tutorial.objects.filter(is_public=True, type=type).values("id", "title")
|
||||||
"id", "title"
|
|
||||||
)
|
|
||||||
return self.success(list(tutorials))
|
return self.success(list(tutorials))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class APIView(View):
|
|||||||
- self.response 返回一个django HttpResponse, 具体在self.response_class中实现
|
- self.response 返回一个django HttpResponse, 具体在self.response_class中实现
|
||||||
- parse请求的类需要定义在request_parser中, 目前只支持json和urlencoded的类型, 用来解析请求的数据
|
- parse请求的类需要定义在request_parser中, 目前只支持json和urlencoded的类型, 用来解析请求的数据
|
||||||
"""
|
"""
|
||||||
|
|
||||||
request_parsers = (JSONParser, URLEncodedParser)
|
request_parsers = (JSONParser, URLEncodedParser)
|
||||||
response_class = JSONResponse
|
response_class = JSONResponse
|
||||||
|
|
||||||
@@ -134,11 +135,10 @@ class APIView(View):
|
|||||||
offset = 0
|
offset = 0
|
||||||
# 只调用一次 count(),避免重复查询
|
# 只调用一次 count(),避免重复查询
|
||||||
count = query_set.count()
|
count = query_set.count()
|
||||||
results = query_set[offset:offset + limit]
|
results = query_set[offset : offset + limit]
|
||||||
if object_serializer:
|
if object_serializer:
|
||||||
results = object_serializer(results, many=True, context={"request": request}).data
|
results = object_serializer(results, many=True, context={"request": request}).data
|
||||||
data = {"results": results,
|
data = {"results": results, "total": count}
|
||||||
"total": count}
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
@@ -215,8 +215,9 @@ class AsyncAPIView(APIView):
|
|||||||
offset = 0
|
offset = 0
|
||||||
if offset < 0:
|
if offset < 0:
|
||||||
offset = 0
|
offset = 0
|
||||||
|
|
||||||
async def _slice():
|
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(
|
count, results = await asyncio.gather(
|
||||||
query_set.acount(),
|
query_set.acount(),
|
||||||
@@ -245,8 +246,10 @@ def validate_serializer(serializer):
|
|||||||
def post(self, request):
|
def post(self, request):
|
||||||
return self.success(request.data)
|
return self.success(request.data)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def validate(view_method):
|
def validate(view_method):
|
||||||
if inspect.iscoroutinefunction(view_method):
|
if inspect.iscoroutinefunction(view_method):
|
||||||
|
|
||||||
@functools.wraps(view_method)
|
@functools.wraps(view_method)
|
||||||
async def async_handle(*args, **kwargs):
|
async def async_handle(*args, **kwargs):
|
||||||
self = args[0]
|
self = args[0]
|
||||||
@@ -261,6 +264,7 @@ def validate_serializer(serializer):
|
|||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
return self.invalid_serializer(s)
|
return self.invalid_serializer(s)
|
||||||
|
|
||||||
return async_handle
|
return async_handle
|
||||||
|
|
||||||
@functools.wraps(view_method)
|
@functools.wraps(view_method)
|
||||||
@@ -274,6 +278,7 @@ def validate_serializer(serializer):
|
|||||||
return view_method(*args, **kwargs)
|
return view_method(*args, **kwargs)
|
||||||
else:
|
else:
|
||||||
return self.invalid_serializer(s)
|
return self.invalid_serializer(s)
|
||||||
|
|
||||||
return handle
|
return handle
|
||||||
|
|
||||||
return validate
|
return validate
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class Command(BaseCommand):
|
|||||||
password = options["password"]
|
password = options["password"]
|
||||||
action = options["action"]
|
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"))
|
self.stdout.write(self.style.ERROR("Invalid args"))
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
@@ -24,8 +24,7 @@ class Command(BaseCommand):
|
|||||||
self.stdout.write(self.style.SUCCESS(f"User {username} exists, operation ignored"))
|
self.stdout.write(self.style.SUCCESS(f"User {username} exists, operation ignored"))
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
user = User.objects.create(username=username, admin_type=AdminType.SUPER_ADMIN,
|
user = User.objects.create(username=username, admin_type=AdminType.SUPER_ADMIN, problem_permission=ProblemPermission.ALL)
|
||||||
problem_permission=ProblemPermission.ALL)
|
|
||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
user.save()
|
user.save()
|
||||||
UserProfile.objects.create(user=user)
|
UserProfile.objects.create(user=user)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def build_query_string(kv_data, ignore_none=True):
|
|||||||
query_string += "&"
|
query_string += "&"
|
||||||
else:
|
else:
|
||||||
query_string = "?"
|
query_string = "?"
|
||||||
query_string += (k + "=" + str(v))
|
query_string += k + "=" + str(v)
|
||||||
return query_string
|
return query_string
|
||||||
|
|
||||||
|
|
||||||
@@ -60,8 +60,7 @@ def datetime2str(value, format="iso-8601"):
|
|||||||
|
|
||||||
|
|
||||||
def natural_sort_key(s, _nsre=re.compile(r"(\d+)")):
|
def natural_sort_key(s, _nsre=re.compile(r"(\d+)")):
|
||||||
return [int(text) if text.isdigit() else text.lower()
|
return [int(text) if text.isdigit() else text.lower() for text in re.split(_nsre, s)]
|
||||||
for text in re.split(_nsre, s)]
|
|
||||||
|
|
||||||
|
|
||||||
def send_email(smtp_config, from_name, to_email, to_name, subject, content):
|
def send_email(smtp_config, from_name, to_email, to_name, subject, content):
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ class TokenBucket:
|
|||||||
"""
|
"""
|
||||||
注意:对于单个key的操作不是线程安全的
|
注意:对于单个key的操作不是线程安全的
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, key, capacity, fill_rate, default_capacity, redis_conn):
|
def __init__(self, key, capacity, fill_rate, default_capacity, redis_conn):
|
||||||
"""
|
"""
|
||||||
:param capacity: 最大容量
|
:param capacity: 最大容量
|
||||||
|
|||||||
@@ -18,17 +18,11 @@ class SimditorImageUploadAPIView(CSRFExemptAPIView):
|
|||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
img = form.cleaned_data["image"]
|
img = form.cleaned_data["image"]
|
||||||
else:
|
else:
|
||||||
return self.response({
|
return self.response({"success": False, "msg": "Upload failed", "file_path": ""})
|
||||||
"success": False,
|
|
||||||
"msg": "Upload failed",
|
|
||||||
"file_path": ""})
|
|
||||||
|
|
||||||
suffix = os.path.splitext(img.name)[-1].lower()
|
suffix = os.path.splitext(img.name)[-1].lower()
|
||||||
if suffix not in [".gif", ".jpg", ".jpeg", ".bmp", ".png"]:
|
if suffix not in [".gif", ".jpg", ".jpeg", ".bmp", ".png"]:
|
||||||
return self.response({
|
return self.response({"success": False, "msg": "Unsupported file format", "file_path": ""})
|
||||||
"success": False,
|
|
||||||
"msg": "Unsupported file format",
|
|
||||||
"file_path": ""})
|
|
||||||
img_name = rand_str(10) + suffix
|
img_name = rand_str(10) + suffix
|
||||||
try:
|
try:
|
||||||
with open(os.path.join(settings.UPLOAD_DIR, img_name), "wb") as imgFile:
|
with open(os.path.join(settings.UPLOAD_DIR, img_name), "wb") as imgFile:
|
||||||
@@ -36,14 +30,8 @@ class SimditorImageUploadAPIView(CSRFExemptAPIView):
|
|||||||
imgFile.write(chunk)
|
imgFile.write(chunk)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
return self.response({
|
return self.response({"success": False, "msg": "Upload Error", "file_path": ""})
|
||||||
"success": False,
|
return self.response({"success": True, "msg": "Success", "file_path": f"{settings.UPLOAD_PREFIX}/{img_name}"})
|
||||||
"msg": "Upload Error",
|
|
||||||
"file_path": ""})
|
|
||||||
return self.response({
|
|
||||||
"success": True,
|
|
||||||
"msg": "Success",
|
|
||||||
"file_path": f"{settings.UPLOAD_PREFIX}/{img_name}"})
|
|
||||||
|
|
||||||
|
|
||||||
# DEPRECATED: 前端未调用 (2026-05-26)
|
# DEPRECATED: 前端未调用 (2026-05-26)
|
||||||
@@ -55,10 +43,7 @@ class SimditorFileUploadAPIView(CSRFExemptAPIView):
|
|||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
file = form.cleaned_data["file"]
|
file = form.cleaned_data["file"]
|
||||||
else:
|
else:
|
||||||
return self.response({
|
return self.response({"success": False, "msg": "Upload failed"})
|
||||||
"success": False,
|
|
||||||
"msg": "Upload failed"
|
|
||||||
})
|
|
||||||
|
|
||||||
suffix = os.path.splitext(file.name)[-1].lower()
|
suffix = os.path.splitext(file.name)[-1].lower()
|
||||||
file_name = rand_str(10) + suffix
|
file_name = rand_str(10) + suffix
|
||||||
@@ -68,11 +53,5 @@ class SimditorFileUploadAPIView(CSRFExemptAPIView):
|
|||||||
f.write(chunk)
|
f.write(chunk)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
return self.response({
|
return self.response({"success": False, "msg": "Upload Error"})
|
||||||
"success": False,
|
return self.response({"success": True, "msg": "Success", "file_path": f"{settings.UPLOAD_PREFIX}/{file_name}", "file_name": file.name})
|
||||||
"msg": "Upload Error"})
|
|
||||||
return self.response({
|
|
||||||
"success": True,
|
|
||||||
"msg": "Success",
|
|
||||||
"file_path": f"{settings.UPLOAD_PREFIX}/{file_name}",
|
|
||||||
"file_name": file.name})
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
WebSocket utility functions for pushing real-time updates
|
WebSocket utility functions for pushing real-time updates
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
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):
|
def push_submission_update(submission_id: str, user_id: int, data: dict):
|
||||||
"""
|
"""
|
||||||
推送提交状态更新到指定用户的 WebSocket 连接
|
推送提交状态更新到指定用户的 WebSocket 连接
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
submission_id: 提交 ID
|
submission_id: 提交 ID
|
||||||
user_id: 用户 ID
|
user_id: 用户 ID
|
||||||
data: 要发送的数据,应该包含 type, submission_id, result 等字段
|
data: 要发送的数据,应该包含 type, submission_id, result 等字段
|
||||||
"""
|
"""
|
||||||
channel_layer = get_channel_layer()
|
channel_layer = get_channel_layer()
|
||||||
|
|
||||||
if channel_layer is None:
|
if channel_layer is None:
|
||||||
logger.warning("Channel layer is not configured, cannot push submission update")
|
logger.warning("Channel layer is not configured, cannot push submission update")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 构建组名,与 SubmissionConsumer 中的组名一致
|
# 构建组名,与 SubmissionConsumer 中的组名一致
|
||||||
group_name = f"submission_user_{user_id}"
|
group_name = f"submission_user_{user_id}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 向指定用户组发送消息
|
# 向指定用户组发送消息
|
||||||
# type 字段对应 consumer 中的方法名(submission_update)
|
# 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 方法
|
"type": "submission_update", # 对应 SubmissionConsumer.submission_update 方法
|
||||||
"data": data,
|
"data": data,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
logger.info(f"Pushed submission update: submission_id={submission_id}, user_id={user_id}, status={data.get('status')}")
|
logger.info(f"Pushed submission update: submission_id={submission_id}, user_id={user_id}, status={data.get('status')}")
|
||||||
except Exception as e:
|
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):
|
def push_to_user(user_id: int, message_type: str, data: dict):
|
||||||
"""
|
"""
|
||||||
向指定用户推送自定义消息
|
向指定用户推送自定义消息
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_id: 用户 ID
|
user_id: 用户 ID
|
||||||
message_type: 消息类型
|
message_type: 消息类型
|
||||||
data: 消息数据
|
data: 消息数据
|
||||||
"""
|
"""
|
||||||
channel_layer = get_channel_layer()
|
channel_layer = get_channel_layer()
|
||||||
|
|
||||||
if channel_layer is None:
|
if channel_layer is None:
|
||||||
logger.warning("Channel layer is not configured, cannot push message")
|
logger.warning("Channel layer is not configured, cannot push message")
|
||||||
return
|
return
|
||||||
|
|
||||||
group_name = f"submission_user_{user_id}"
|
group_name = f"submission_user_{user_id}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async_to_sync(channel_layer.group_send)(
|
async_to_sync(channel_layer.group_send)(
|
||||||
group_name,
|
group_name,
|
||||||
{
|
{
|
||||||
"type": "submission_update",
|
"type": "submission_update",
|
||||||
"data": {
|
"data": {"type": message_type, **data},
|
||||||
"type": message_type,
|
},
|
||||||
**data
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
logger.info(f"Pushed message to user {user_id}: type={message_type}")
|
logger.info(f"Pushed message to user {user_id}: type={message_type}")
|
||||||
except Exception as e:
|
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):
|
def push_flowchart_evaluation_update(submission_id: str, user_id: int, data: dict):
|
||||||
"""
|
"""
|
||||||
推送流程图评分状态更新到指定用户的 WebSocket 连接
|
推送流程图评分状态更新到指定用户的 WebSocket 连接
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
submission_id: 流程图提交 ID
|
submission_id: 流程图提交 ID
|
||||||
user_id: 用户 ID
|
user_id: 用户 ID
|
||||||
data: 要发送的数据,应该包含 type, submission_id, score, grade, feedback 等字段
|
data: 要发送的数据,应该包含 type, submission_id, score, grade, feedback 等字段
|
||||||
"""
|
"""
|
||||||
channel_layer = get_channel_layer()
|
channel_layer = get_channel_layer()
|
||||||
|
|
||||||
if channel_layer is None:
|
if channel_layer is None:
|
||||||
logger.warning("Channel layer is not configured, cannot push flowchart evaluation update")
|
logger.warning("Channel layer is not configured, cannot push flowchart evaluation update")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 构建组名,与 FlowchartConsumer 中的组名一致
|
# 构建组名,与 FlowchartConsumer 中的组名一致
|
||||||
group_name = f"flowchart_user_{user_id}"
|
group_name = f"flowchart_user_{user_id}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 向指定用户组发送消息
|
# 向指定用户组发送消息
|
||||||
# type 字段对应 consumer 中的方法名(flowchart_evaluation_update)
|
# 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 方法
|
"type": "flowchart_evaluation_update", # 对应 FlowchartConsumer.flowchart_evaluation_update 方法
|
||||||
"data": data,
|
"data": data,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
logger.info(f"Pushed flowchart evaluation update: submission_id={submission_id}, user_id={user_id}, type={data.get('type')}")
|
logger.info(f"Pushed flowchart evaluation update: submission_id={submission_id}, user_id={user_id}, type={data.get('type')}")
|
||||||
except Exception as e:
|
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):
|
def push_config_update(key: str, value):
|
||||||
"""
|
"""
|
||||||
推送配置更新到所有连接的客户端
|
推送配置更新到所有连接的客户端
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
key: 配置键名
|
key: 配置键名
|
||||||
value: 配置值
|
value: 配置值
|
||||||
"""
|
"""
|
||||||
channel_layer = get_channel_layer()
|
channel_layer = get_channel_layer()
|
||||||
|
|
||||||
if channel_layer is None:
|
if channel_layer is None:
|
||||||
logger.warning("Channel layer is not configured, cannot push config update")
|
logger.warning("Channel layer is not configured, cannot push config update")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 使用全局配置组名
|
# 使用全局配置组名
|
||||||
group_name = "config_updates"
|
group_name = "config_updates"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 向所有连接的客户端发送配置更新
|
# 向所有连接的客户端发送配置更新
|
||||||
async_to_sync(channel_layer.group_send)(
|
async_to_sync(channel_layer.group_send)(group_name, {"type": "config_update", "data": {"type": "config_update", "key": key, "value": value}})
|
||||||
group_name,
|
|
||||||
{
|
|
||||||
"type": "config_update",
|
|
||||||
"data": {
|
|
||||||
"type": "config_update",
|
|
||||||
"key": key,
|
|
||||||
"value": value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
logger.info(f"Pushed config update: {key}={value}")
|
logger.info(f"Pushed config update: {key}={value}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to push config update: {key}={value}, error={str(e)}")
|
logger.error(f"Failed to push config update: {key}={value}, error={str(e)}")
|
||||||
|
|||||||
@@ -25,17 +25,52 @@ Python 2.6+ or 3.2+
|
|||||||
Cannot defense xss in browser which is belowed IE7
|
Cannot defense xss in browser which is belowed IE7
|
||||||
浏览器版本:IE7+ 或其他浏览器,无法防御IE6及以下版本浏览器中的XSS
|
浏览器版本:IE7+ 或其他浏览器,无法防御IE6及以下版本浏览器中的XSS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import re
|
import re
|
||||||
from html.parser import HTMLParser
|
from html.parser import HTMLParser
|
||||||
|
|
||||||
|
|
||||||
class XSSHtml(HTMLParser):
|
class XSSHtml(HTMLParser):
|
||||||
allow_tags = ['a', 'img', 'br', 'strong', 'b', 'code', 'pre',
|
allow_tags = [
|
||||||
'p', 'div', 'em', 'span', 'h1', 'h2', 'h3', 'h4',
|
"a",
|
||||||
'h5', 'h6', 'blockquote', 'ul', 'ol', 'tr', 'th', 'td',
|
"img",
|
||||||
'hr', 'li', 'u', 'embed', 's', 'table', 'thead', 'tbody',
|
"br",
|
||||||
'caption', 'small', 'q', 'sup', 'sub', 'font']
|
"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"]
|
common_attrs = ["style", "class", "name"]
|
||||||
nonend_tags = ["img", "hr", "br", "embed"]
|
nonend_tags = ["img", "hr", "br", "embed"]
|
||||||
tags_own_attrs = {
|
tags_own_attrs = {
|
||||||
@@ -43,7 +78,7 @@ class XSSHtml(HTMLParser):
|
|||||||
"a": ["href", "target", "rel", "title"],
|
"a": ["href", "target", "rel", "title"],
|
||||||
"embed": ["src", "width", "height", "type", "allowfullscreen", "loop", "play", "wmode", "menu"],
|
"embed": ["src", "width", "height", "type", "allowfullscreen", "loop", "play", "wmode", "menu"],
|
||||||
"table": ["border", "cellpadding", "cellspacing"],
|
"table": ["border", "cellpadding", "cellspacing"],
|
||||||
"font": ["color"]
|
"font": ["color"],
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, allows=[]):
|
def __init__(self, allows=[]):
|
||||||
@@ -68,9 +103,9 @@ class XSSHtml(HTMLParser):
|
|||||||
Get the safe html code
|
Get the safe html code
|
||||||
"""
|
"""
|
||||||
for i in range(0, len(self.result)):
|
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])
|
self.data.append(self.result[i])
|
||||||
return ''.join(self.data)
|
return "".join(self.data)
|
||||||
|
|
||||||
def handle_startendtag(self, tag, attrs):
|
def handle_startendtag(self, tag, attrs):
|
||||||
self.handle_starttag(tag, attrs)
|
self.handle_starttag(tag, attrs)
|
||||||
@@ -78,7 +113,7 @@ class XSSHtml(HTMLParser):
|
|||||||
def handle_starttag(self, tag, attrs):
|
def handle_starttag(self, tag, attrs):
|
||||||
if tag not in self.allow_tags:
|
if tag not in self.allow_tags:
|
||||||
return
|
return
|
||||||
end_diagonal = ' /' if tag in self.nonend_tags else ''
|
end_diagonal = " /" if tag in self.nonend_tags else ""
|
||||||
if not end_diagonal:
|
if not end_diagonal:
|
||||||
self.start.append(tag)
|
self.start.append(tag)
|
||||||
attdict = {}
|
attdict = {}
|
||||||
@@ -92,14 +127,14 @@ class XSSHtml(HTMLParser):
|
|||||||
attdict = self.node_default(attdict)
|
attdict = self.node_default(attdict)
|
||||||
|
|
||||||
attrs = []
|
attrs = []
|
||||||
for (key, value) in attdict.items():
|
for key, value in attdict.items():
|
||||||
attrs.append('%s="%s"' % (key, self._htmlspecialchars(value)))
|
attrs.append('%s="%s"' % (key, self._htmlspecialchars(value)))
|
||||||
attrs = (' ' + ' '.join(attrs)) if attrs else ''
|
attrs = (" " + " ".join(attrs)) if attrs else ""
|
||||||
self.result.append('<' + tag + attrs + end_diagonal + '>')
|
self.result.append("<" + tag + attrs + end_diagonal + ">")
|
||||||
|
|
||||||
def handle_endtag(self, tag):
|
def handle_endtag(self, tag):
|
||||||
if self.start and tag == self.start[len(self.start) - 1]:
|
if self.start and tag == self.start[len(self.start) - 1]:
|
||||||
self.result.append('</' + tag + '>')
|
self.result.append("</" + tag + ">")
|
||||||
self.start.pop()
|
self.start.pop()
|
||||||
|
|
||||||
def handle_data(self, data):
|
def handle_data(self, data):
|
||||||
@@ -121,22 +156,23 @@ class XSSHtml(HTMLParser):
|
|||||||
attrs = self._common_attr(attrs)
|
attrs = self._common_attr(attrs)
|
||||||
attrs = self._get_link(attrs, "href")
|
attrs = self._get_link(attrs, "href")
|
||||||
attrs = self._set_attr_default(attrs, "target", "_blank")
|
attrs = self._set_attr_default(attrs, "target", "_blank")
|
||||||
attrs = self._limit_attr(attrs, {
|
attrs = self._limit_attr(attrs, {"target": ["_blank", "_self"]})
|
||||||
"target": ["_blank", "_self"]
|
|
||||||
})
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def node_embed(self, attrs):
|
def node_embed(self, attrs):
|
||||||
attrs = self._common_attr(attrs)
|
attrs = self._common_attr(attrs)
|
||||||
attrs = self._get_link(attrs, "src")
|
attrs = self._get_link(attrs, "src")
|
||||||
attrs = self._limit_attr(attrs, {
|
attrs = self._limit_attr(
|
||||||
"type": ["application/x-shockwave-flash"],
|
attrs,
|
||||||
"wmode": ["transparent", "window", "opaque"],
|
{
|
||||||
"play": ["true", "false"],
|
"type": ["application/x-shockwave-flash"],
|
||||||
"loop": ["true", "false"],
|
"wmode": ["transparent", "window", "opaque"],
|
||||||
"menu": ["true", "false"],
|
"play": ["true", "false"],
|
||||||
"allowfullscreen": ["true", "false"]
|
"loop": ["true", "false"],
|
||||||
})
|
"menu": ["true", "false"],
|
||||||
|
"allowfullscreen": ["true", "false"],
|
||||||
|
},
|
||||||
|
)
|
||||||
attrs["allowscriptaccess"] = "never"
|
attrs["allowscriptaccess"] = "never"
|
||||||
attrs["allownetworking"] = "none"
|
attrs["allownetworking"] = "none"
|
||||||
return attrs
|
return attrs
|
||||||
@@ -179,22 +215,19 @@ class XSSHtml(HTMLParser):
|
|||||||
attrs = self._get_style(attrs)
|
attrs = self._get_style(attrs)
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def _set_attr_default(self, attrs, name, default=''):
|
def _set_attr_default(self, attrs, name, default=""):
|
||||||
if name not in attrs:
|
if name not in attrs:
|
||||||
attrs[name] = default
|
attrs[name] = default
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def _limit_attr(self, attrs, limit={}):
|
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:
|
if key in attrs and attrs[key] not in value:
|
||||||
del attrs[key]
|
del attrs[key]
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def _htmlspecialchars(self, html):
|
def _htmlspecialchars(self, html):
|
||||||
return html.replace("<", "<") \
|
return html.replace("<", "<").replace(">", ">").replace('"', """).replace("'", "'")
|
||||||
.replace(">", ">") \
|
|
||||||
.replace('"', """) \
|
|
||||||
.replace("'", "'")
|
|
||||||
|
|
||||||
|
|
||||||
if "__main__" == __name__:
|
if "__main__" == __name__:
|
||||||
|
|||||||
Reference in New Issue
Block a user