830 lines
26 KiB
Python
830 lines
26 KiB
Python
import threading
|
|
from typing import List, Optional
|
|
from uuid import UUID
|
|
from ninja import Router, Query
|
|
from ninja.errors import HttpError
|
|
from ninja.pagination import paginate
|
|
from django.shortcuts import get_object_or_404
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.db.models import (
|
|
Avg,
|
|
Count,
|
|
Exists,
|
|
F,
|
|
IntegerField,
|
|
Max,
|
|
OuterRef,
|
|
Q,
|
|
Subquery,
|
|
)
|
|
from account.decorators import admin_required
|
|
from prompt.models import Conversation, Message
|
|
|
|
|
|
from .schemas import (
|
|
AwardItemIn,
|
|
AwardItemManageOut,
|
|
AwardItemUpdateIn,
|
|
AwardManageIn,
|
|
AwardManageOut,
|
|
FlagIn,
|
|
FlagStats,
|
|
AwardOut,
|
|
PromptRoundOut,
|
|
ShowcaseDetailOut,
|
|
ShowcaseItemOut,
|
|
ShowcaseSubmissionLookupOut,
|
|
SubmissionCountBucket,
|
|
SubmissionFilter,
|
|
SubmissionIn,
|
|
SubmissionOut,
|
|
RatingScoreIn,
|
|
TaskStatsOut,
|
|
TopViewedItem,
|
|
UserTag,
|
|
)
|
|
|
|
|
|
from .models import Award, ItemOrdering, Rating, Submission, SubmissionAward
|
|
from task.models import Task
|
|
from account.models import RoleChoices, User
|
|
|
|
router = Router()
|
|
|
|
|
|
def _validate_item_ordering(value: str):
|
|
if value not in ItemOrdering.values:
|
|
raise HttpError(400, "无效的作品排序方式")
|
|
|
|
|
|
def _award_manage_out(award: Award):
|
|
return {
|
|
"id": award.id,
|
|
"name": award.name,
|
|
"description": award.description,
|
|
"sort_order": award.sort_order,
|
|
"is_active": award.is_active,
|
|
"item_ordering": award.item_ordering,
|
|
"item_count": getattr(award, "item_count", None)
|
|
if getattr(award, "item_count", None) is not None
|
|
else award.submission_awards.count(),
|
|
}
|
|
|
|
|
|
def _award_item_ordering(award: Award):
|
|
ordering_map = {
|
|
ItemOrdering.MANUAL: ("sort_order", "id"),
|
|
ItemOrdering.AWARDED_AT: ("-awarded_at", "sort_order", "id"),
|
|
ItemOrdering.SCORE: ("-submission__score", "sort_order", "id"),
|
|
ItemOrdering.VIEW_COUNT: ("-submission__view_count", "sort_order", "id"),
|
|
}
|
|
return ordering_map.get(award.item_ordering, ("sort_order", "id"))
|
|
|
|
|
|
def _award_item_manage_out(item: SubmissionAward):
|
|
has_prompt_chain = getattr(item, "has_prompt_chain", None)
|
|
if has_prompt_chain is None:
|
|
has_prompt_chain = Message.objects.filter(
|
|
submission_id=item.submission_id
|
|
).exists()
|
|
return {
|
|
"id": item.id,
|
|
"submission_id": item.submission_id,
|
|
"username": item.submission.user.username,
|
|
"task_title": item.submission.task.title,
|
|
"task_display": item.submission.task.display,
|
|
"score": item.submission.score,
|
|
"view_count": item.submission.view_count,
|
|
"sort_order": item.sort_order,
|
|
"awarded_at": item.awarded_at,
|
|
"has_prompt_chain": has_prompt_chain,
|
|
}
|
|
|
|
|
|
def _showcase_submission_lookup_out(submission: Submission):
|
|
return {
|
|
"submission_id": submission.id,
|
|
"username": submission.user.username,
|
|
"task_title": submission.task.title,
|
|
"task_display": submission.task.display,
|
|
"score": submission.score,
|
|
"view_count": submission.view_count,
|
|
"has_prompt_chain": Message.objects.filter(submission=submission).exists(),
|
|
}
|
|
|
|
|
|
@router.post("/")
|
|
@login_required
|
|
def create_submission(request, payload: SubmissionIn):
|
|
"""
|
|
创建一个新的提交
|
|
"""
|
|
task = get_object_or_404(Task, id=payload.task_id)
|
|
|
|
manual_asst_msg = None
|
|
if payload.prompt:
|
|
conversation = (
|
|
Conversation.objects.filter(user=request.user, task=task)
|
|
.annotate(msg_count=Count("messages"))
|
|
.order_by("-msg_count", "-created")
|
|
.first()
|
|
)
|
|
if not conversation:
|
|
conversation = Conversation.objects.create(
|
|
user=request.user, task=task, is_active=False
|
|
)
|
|
Message.objects.create(
|
|
conversation=conversation, role="user", content=payload.prompt, source="manual"
|
|
)
|
|
manual_asst_msg = Message.objects.create(
|
|
conversation=conversation,
|
|
role="assistant",
|
|
content="",
|
|
code_html=payload.html,
|
|
code_css=payload.css,
|
|
code_js=payload.js,
|
|
source="manual",
|
|
)
|
|
from .classifier import classify_conversation_messages
|
|
threading.Thread(target=classify_conversation_messages, args=(conversation.id,), daemon=True).start()
|
|
else:
|
|
conversation = (
|
|
Conversation.objects.filter(user=request.user, task=task)
|
|
.annotate(msg_count=Count("messages"))
|
|
.order_by("-msg_count", "-created")
|
|
.first()
|
|
)
|
|
if conversation:
|
|
from .classifier import classify_conversation_messages
|
|
threading.Thread(target=classify_conversation_messages, args=(conversation.id,), daemon=True).start()
|
|
|
|
submission = Submission.objects.create(
|
|
user=request.user,
|
|
task=task,
|
|
html=payload.html,
|
|
css=payload.css,
|
|
js=payload.js,
|
|
)
|
|
|
|
# Link assistant message to submission
|
|
if manual_asst_msg:
|
|
manual_asst_msg.submission = submission
|
|
manual_asst_msg.save(update_fields=["submission"])
|
|
elif payload.message_id:
|
|
try:
|
|
msg = Message.objects.get(
|
|
id=payload.message_id,
|
|
role="assistant",
|
|
conversation__user=request.user,
|
|
conversation__task=task,
|
|
)
|
|
msg.submission = submission
|
|
msg.save(update_fields=["submission"])
|
|
except Message.DoesNotExist:
|
|
pass # invalid message_id — submission already created, silently skip
|
|
|
|
return {"id": str(submission.id)}
|
|
|
|
|
|
@router.get("/", response=List[SubmissionOut])
|
|
@paginate
|
|
@login_required
|
|
def list_submissions(request, filters: SubmissionFilter = Query(...)):
|
|
"""
|
|
获取提交列表,支持按任务和用户过滤
|
|
"""
|
|
submissions = (
|
|
Submission.objects.select_related("task", "user")
|
|
.defer("html", "css", "js")
|
|
)
|
|
|
|
if filters.task_id:
|
|
task = get_object_or_404(Task, id=filters.task_id)
|
|
submissions = submissions.filter(task=task)
|
|
elif filters.task_type:
|
|
submissions = submissions.filter(task__task_type=filters.task_type)
|
|
if filters.username:
|
|
submissions = submissions.filter(user__username__icontains=filters.username)
|
|
if filters.user_id:
|
|
submissions = submissions.filter(user_id=filters.user_id)
|
|
if filters.flag:
|
|
if filters.flag == "any":
|
|
submissions = submissions.filter(flag__isnull=False)
|
|
else:
|
|
submissions = submissions.filter(flag=filters.flag)
|
|
if filters.zone:
|
|
submissions = submissions.filter(zone=filters.zone)
|
|
|
|
if filters.score_lt_threshold is not None:
|
|
submissions = submissions.filter(score__lt=filters.score_lt_threshold)
|
|
else:
|
|
if filters.score_min is not None:
|
|
submissions = submissions.filter(score__gte=filters.score_min)
|
|
if filters.score_max_exclusive is not None:
|
|
submissions = submissions.filter(score__lt=filters.score_max_exclusive)
|
|
if filters.ordering in ("-score", "score", "-created"):
|
|
submissions = submissions.order_by(filters.ordering)
|
|
|
|
if filters.grouped:
|
|
# 分组模式:每个 (user, task) 只保留最新一条
|
|
latest_per_group = (
|
|
Submission.objects.filter(user=OuterRef("user"), task=OuterRef("task"))
|
|
.order_by("-created")
|
|
.values("pk")[:1]
|
|
)
|
|
submissions = submissions.filter(pk=Subquery(latest_per_group))
|
|
|
|
user_rating_subquery = Subquery(
|
|
Rating.objects.filter(user=request.user, submission=OuterRef("pk")).values(
|
|
"score"
|
|
)[:1],
|
|
output_field=IntegerField(),
|
|
)
|
|
submissions = submissions.annotate(my_score=user_rating_subquery)
|
|
|
|
# 同一用户同一任务的提交次数
|
|
submit_count_subquery = Subquery(
|
|
Submission.objects.filter(
|
|
user=OuterRef("user"), task=OuterRef("task")
|
|
).values("user", "task").annotate(c=Count("id")).values("c")[:1],
|
|
output_field=IntegerField(),
|
|
)
|
|
submissions = submissions.annotate(submit_count=submit_count_subquery)
|
|
|
|
return submissions
|
|
|
|
|
|
|
|
@router.get("/by-user-task", response=List[SubmissionOut])
|
|
@login_required
|
|
def list_by_user_task(request, user_id: int, task_id: int):
|
|
"""
|
|
获取某用户某任务的所有提交(不分页)
|
|
"""
|
|
user_rating_subquery = Subquery(
|
|
Rating.objects.filter(user=request.user, submission=OuterRef("pk")).values(
|
|
"score"
|
|
)[:1],
|
|
output_field=IntegerField(),
|
|
)
|
|
return (
|
|
Submission.objects.filter(user_id=user_id, task_id=task_id)
|
|
.select_related("task", "user")
|
|
.defer("html", "css", "js")
|
|
.annotate(my_score=user_rating_subquery)
|
|
.order_by("-created")
|
|
)
|
|
|
|
|
|
@router.delete("/flags")
|
|
@login_required
|
|
def clear_all_flags(request):
|
|
"""
|
|
清除所有提交的标记(仅管理员和超级管理员可操作)
|
|
"""
|
|
if request.user.role not in (RoleChoices.SUPER, RoleChoices.ADMIN):
|
|
raise HttpError(403, "没有权限")
|
|
|
|
count = Submission.objects.filter(flag__isnull=False).update(flag=None)
|
|
return {"cleared": count}
|
|
|
|
|
|
@router.delete("/{submission_id}")
|
|
@login_required
|
|
def delete_submission(request, submission_id: UUID):
|
|
submission = get_object_or_404(Submission, id=submission_id)
|
|
if submission.user != request.user and request.user.role != RoleChoices.SUPER:
|
|
raise HttpError(403, "只能删除自己的提交")
|
|
|
|
# 找到关联的助手消息,再找前一条用户消息
|
|
asst_msg = Message.objects.filter(submission=submission).first()
|
|
user_msg = None
|
|
if asst_msg:
|
|
user_msg = (
|
|
Message.objects.filter(
|
|
conversation=asst_msg.conversation,
|
|
created__lt=asst_msg.created,
|
|
role="user",
|
|
)
|
|
.order_by("-created")
|
|
.first()
|
|
)
|
|
|
|
submission.delete() # CASCADE 自动删除关联的 asst_msg
|
|
|
|
if user_msg:
|
|
user_msg.delete()
|
|
|
|
return {"message": "删除成功"}
|
|
|
|
|
|
@router.get("/stats/{task_id}", response=TaskStatsOut)
|
|
@login_required
|
|
def get_task_stats(request, task_id: int, classname: Optional[str] = None):
|
|
"""
|
|
获取某个挑战任务的班级提交统计数据(仅管理员)
|
|
"""
|
|
if request.user.role not in (RoleChoices.SUPER, RoleChoices.ADMIN):
|
|
raise HttpError(403, "没有权限")
|
|
|
|
task = get_object_or_404(Task, id=task_id)
|
|
|
|
# All distinct classnames (unfiltered, for filter buttons in UI)
|
|
all_classes = list(
|
|
User.objects.filter(role=RoleChoices.NORMAL)
|
|
.exclude(classname="")
|
|
.values_list("classname", flat=True)
|
|
.distinct()
|
|
.order_by("classname")
|
|
)
|
|
|
|
# Student universe: Normal users, optionally filtered by classname
|
|
students = User.objects.filter(role=RoleChoices.NORMAL)
|
|
if classname:
|
|
students = students.filter(classname=classname)
|
|
|
|
student_ids = list(students.values_list("id", flat=True))
|
|
total_students = len(student_ids)
|
|
|
|
# Submitted student IDs
|
|
submitted_ids = set(
|
|
Submission.objects.filter(task=task, user_id__in=student_ids)
|
|
.values_list("user_id", flat=True)
|
|
.distinct()
|
|
)
|
|
submitted_count = len(submitted_ids)
|
|
unsubmitted_count = total_students - submitted_count
|
|
|
|
# Unsubmitted users
|
|
unsubmitted_users = [
|
|
UserTag(username=u.username, classname=u.classname)
|
|
for u in students.exclude(id__in=submitted_ids).order_by("classname", "username")
|
|
]
|
|
|
|
# Latest submission per submitted user (SQLite-compatible).
|
|
# Find each user's max created timestamp, then resolve all matching IDs
|
|
# in a single query using OR'd Q objects instead of one query per user.
|
|
latest_per_user = list(
|
|
Submission.objects.filter(task=task, user_id__in=submitted_ids)
|
|
.values("user_id")
|
|
.annotate(max_created=Max("created"))
|
|
)
|
|
latest_sub_ids = []
|
|
if latest_per_user:
|
|
user_time_filter = Q()
|
|
for row in latest_per_user:
|
|
user_time_filter |= Q(user_id=row["user_id"], created=row["max_created"])
|
|
# Fetch all matching submissions in one query; deduplicate by user_id
|
|
seen_users: set = set()
|
|
for sub_id, uid in (
|
|
Submission.objects.filter(user_time_filter, task=task)
|
|
.values_list("id", "user_id")
|
|
):
|
|
if uid not in seen_users:
|
|
seen_users.add(uid)
|
|
latest_sub_ids.append(sub_id)
|
|
latest_subs = list(Submission.objects.filter(id__in=latest_sub_ids))
|
|
|
|
# Average score from latest submissions (None if no submissions have score > 0)
|
|
avg_result = (
|
|
Submission.objects.filter(id__in=latest_sub_ids, score__gt=0)
|
|
.aggregate(avg=Avg("score"))["avg"]
|
|
)
|
|
average_score = round(avg_result, 2) if avg_result is not None else None
|
|
|
|
# Unrated: submitted but no Rating on any of their submissions for this task
|
|
rated_ids = set(
|
|
Rating.objects.filter(
|
|
submission__task=task, submission__user_id__in=submitted_ids
|
|
)
|
|
.values_list("submission__user_id", flat=True)
|
|
.distinct()
|
|
)
|
|
unrated_ids = submitted_ids - rated_ids
|
|
unrated_count = len(unrated_ids)
|
|
unrated_users = [
|
|
UserTag(username=u.username, classname=u.classname)
|
|
for u in students.filter(id__in=unrated_ids).order_by("classname", "username")
|
|
]
|
|
|
|
# Submission count distribution
|
|
sub_counts = dict(
|
|
Submission.objects.filter(task=task, user_id__in=submitted_ids)
|
|
.values("user_id")
|
|
.annotate(c=Count("id"))
|
|
.values_list("user_id", "c")
|
|
)
|
|
dist = {"count_1": 0, "count_2": 0, "count_3": 0, "count_4_plus": 0}
|
|
for c in sub_counts.values():
|
|
if c == 1:
|
|
dist["count_1"] += 1
|
|
elif c == 2:
|
|
dist["count_2"] += 1
|
|
elif c == 3:
|
|
dist["count_3"] += 1
|
|
else:
|
|
dist["count_4_plus"] += 1
|
|
|
|
# Flag stats (all submissions for this task, not grouped by user)
|
|
flag_counts = dict(
|
|
Submission.objects.filter(task=task, flag__isnull=False)
|
|
.values("flag")
|
|
.annotate(c=Count("id"))
|
|
.values_list("flag", "c")
|
|
)
|
|
flag_stats = FlagStats(
|
|
red=flag_counts.get("red", 0),
|
|
blue=flag_counts.get("blue", 0),
|
|
green=flag_counts.get("green", 0),
|
|
yellow=flag_counts.get("yellow", 0),
|
|
)
|
|
|
|
# Top 5 submissions by view_count (within filtered student_ids)
|
|
top_viewed_qs = (
|
|
Submission.objects
|
|
.filter(task=task, user_id__in=student_ids)
|
|
.select_related("user")
|
|
.defer("html", "css", "js")
|
|
.order_by("-view_count")[:5]
|
|
)
|
|
top_viewed = [
|
|
TopViewedItem(
|
|
username=s.user.username,
|
|
classname=s.user.classname,
|
|
view_count=s.view_count,
|
|
submission_id=s.id,
|
|
)
|
|
for s in top_viewed_qs
|
|
]
|
|
|
|
return TaskStatsOut(
|
|
submitted_count=submitted_count,
|
|
unsubmitted_count=unsubmitted_count,
|
|
average_score=average_score,
|
|
unrated_count=unrated_count,
|
|
unsubmitted_users=unsubmitted_users,
|
|
unrated_users=unrated_users,
|
|
submission_count_distribution=SubmissionCountBucket(**dist),
|
|
flag_stats=flag_stats,
|
|
classes=all_classes,
|
|
top_viewed=top_viewed,
|
|
)
|
|
|
|
|
|
@router.get("/showcase/", response=List[AwardOut])
|
|
@login_required
|
|
def list_showcase(request):
|
|
ordering_map = {
|
|
ItemOrdering.MANUAL: "sort_order",
|
|
ItemOrdering.AWARDED_AT: "-awarded_at",
|
|
ItemOrdering.SCORE: "-submission__score",
|
|
ItemOrdering.VIEW_COUNT: "-submission__view_count",
|
|
}
|
|
awards = Award.objects.filter(is_active=True).order_by("sort_order")
|
|
result = []
|
|
|
|
for award in awards:
|
|
order_field = ordering_map.get(award.item_ordering, "sort_order")
|
|
items_qs = (
|
|
SubmissionAward.objects.filter(award=award)
|
|
.select_related("submission", "submission__user", "submission__task")
|
|
.annotate(
|
|
has_prompt_chain=Exists(
|
|
Message.objects.filter(submission_id=OuterRef("submission_id"))
|
|
)
|
|
)
|
|
.order_by(order_field)
|
|
)
|
|
items = list(items_qs)
|
|
if not items:
|
|
continue
|
|
result.append(
|
|
{
|
|
"id": award.id,
|
|
"name": award.name,
|
|
"description": award.description,
|
|
"item_ordering": award.item_ordering,
|
|
"items": [
|
|
{
|
|
"submission_id": sa.submission_id,
|
|
"username": sa.submission.user.username,
|
|
"task_title": sa.submission.task.title,
|
|
"task_display": sa.submission.task.display,
|
|
"score": sa.submission.score,
|
|
"view_count": sa.submission.view_count,
|
|
"html": sa.submission.html,
|
|
"css": sa.submission.css,
|
|
"js": sa.submission.js,
|
|
"has_prompt_chain": sa.has_prompt_chain,
|
|
}
|
|
for sa in items
|
|
],
|
|
}
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
@router.get("/showcase/manage/awards", response=List[AwardManageOut])
|
|
@admin_required
|
|
def list_manage_awards(request):
|
|
awards = Award.objects.annotate(
|
|
item_count=Count("submission_awards")
|
|
).order_by("sort_order", "id")
|
|
return [_award_manage_out(award) for award in awards]
|
|
|
|
|
|
@router.post("/showcase/manage/awards", response=AwardManageOut)
|
|
@admin_required
|
|
def create_manage_award(request, payload: AwardManageIn):
|
|
_validate_item_ordering(payload.item_ordering)
|
|
award = Award.objects.create(**payload.dict())
|
|
award.item_count = 0
|
|
return _award_manage_out(award)
|
|
|
|
|
|
@router.put("/showcase/manage/awards/{award_id}", response=AwardManageOut)
|
|
@admin_required
|
|
def update_manage_award(request, award_id: int, payload: AwardManageIn):
|
|
_validate_item_ordering(payload.item_ordering)
|
|
award = get_object_or_404(Award, id=award_id)
|
|
award.name = payload.name
|
|
award.description = payload.description
|
|
award.sort_order = payload.sort_order
|
|
award.is_active = payload.is_active
|
|
award.item_ordering = payload.item_ordering
|
|
award.save(
|
|
update_fields=[
|
|
"name",
|
|
"description",
|
|
"sort_order",
|
|
"is_active",
|
|
"item_ordering",
|
|
]
|
|
)
|
|
award.item_count = award.submission_awards.count()
|
|
return _award_manage_out(award)
|
|
|
|
|
|
@router.delete("/showcase/manage/awards/{award_id}")
|
|
@admin_required
|
|
def delete_manage_award(request, award_id: int):
|
|
award = get_object_or_404(Award, id=award_id)
|
|
award.delete()
|
|
return {"message": "删除成功"}
|
|
|
|
|
|
@router.get(
|
|
"/showcase/manage/submissions/{submission_id}",
|
|
response=ShowcaseSubmissionLookupOut,
|
|
)
|
|
@admin_required
|
|
def get_manage_submission(request, submission_id: UUID):
|
|
submission = get_object_or_404(
|
|
Submission.objects.select_related("user", "task"),
|
|
id=submission_id,
|
|
)
|
|
return _showcase_submission_lookup_out(submission)
|
|
|
|
|
|
@router.get(
|
|
"/showcase/manage/awards/{award_id}/items",
|
|
response=List[AwardItemManageOut],
|
|
)
|
|
@admin_required
|
|
def list_manage_award_items(request, award_id: int):
|
|
award = get_object_or_404(Award, id=award_id)
|
|
items = (
|
|
SubmissionAward.objects.filter(award=award)
|
|
.select_related("submission", "submission__user", "submission__task")
|
|
.annotate(
|
|
has_prompt_chain=Exists(
|
|
Message.objects.filter(submission_id=OuterRef("submission_id"))
|
|
)
|
|
)
|
|
.order_by(*_award_item_ordering(award))
|
|
)
|
|
return [_award_item_manage_out(item) for item in items]
|
|
|
|
|
|
@router.post(
|
|
"/showcase/manage/awards/{award_id}/items",
|
|
response=AwardItemManageOut,
|
|
)
|
|
@admin_required
|
|
def create_manage_award_item(request, award_id: int, payload: AwardItemIn):
|
|
award = get_object_or_404(Award, id=award_id)
|
|
submission = get_object_or_404(
|
|
Submission.objects.select_related("user", "task"),
|
|
id=payload.submission_id,
|
|
)
|
|
item, created = SubmissionAward.objects.get_or_create(
|
|
award=award,
|
|
submission=submission,
|
|
defaults={"sort_order": payload.sort_order},
|
|
)
|
|
if not created:
|
|
raise HttpError(400, "该作品已在奖项中")
|
|
item.submission = submission
|
|
return _award_item_manage_out(item)
|
|
|
|
|
|
@router.put("/showcase/manage/items/{item_id}", response=AwardItemManageOut)
|
|
@admin_required
|
|
def update_manage_award_item(request, item_id: int, payload: AwardItemUpdateIn):
|
|
item = get_object_or_404(
|
|
SubmissionAward.objects.select_related(
|
|
"submission",
|
|
"submission__user",
|
|
"submission__task",
|
|
),
|
|
id=item_id,
|
|
)
|
|
item.sort_order = payload.sort_order
|
|
item.save(update_fields=["sort_order"])
|
|
return _award_item_manage_out(item)
|
|
|
|
|
|
@router.delete("/showcase/manage/items/{item_id}")
|
|
@admin_required
|
|
def delete_manage_award_item(request, item_id: int):
|
|
item = get_object_or_404(SubmissionAward, id=item_id)
|
|
item.delete()
|
|
return {"message": "删除成功"}
|
|
|
|
|
|
@router.get("/showcase/{submission_id}/", response=ShowcaseDetailOut)
|
|
@login_required
|
|
def get_showcase_detail(request, submission_id: UUID):
|
|
if not SubmissionAward.objects.filter(
|
|
submission_id=submission_id,
|
|
award__is_active=True,
|
|
).exists():
|
|
raise HttpError(404, "作品不存在或未授奖")
|
|
|
|
sub = get_object_or_404(
|
|
Submission.objects.select_related("user", "task"),
|
|
id=submission_id,
|
|
)
|
|
has_chain = Message.objects.filter(submission=sub).exists()
|
|
award_names = list(
|
|
SubmissionAward.objects.filter(submission=sub)
|
|
.filter(award__is_active=True)
|
|
.select_related("award")
|
|
.values_list("award__name", flat=True)
|
|
)
|
|
|
|
return {
|
|
"submission_id": sub.id,
|
|
"username": sub.user.username,
|
|
"task_title": sub.task.title,
|
|
"task_display": sub.task.display,
|
|
"score": sub.score,
|
|
"view_count": sub.view_count,
|
|
"html": sub.html,
|
|
"css": sub.css,
|
|
"js": sub.js,
|
|
"awards": award_names,
|
|
"has_prompt_chain": has_chain,
|
|
}
|
|
|
|
|
|
def _build_prompt_rounds(source_msg: Message):
|
|
messages = list(source_msg.conversation.messages.all().order_by("created", "id"))
|
|
try:
|
|
source_index = messages.index(source_msg)
|
|
except ValueError:
|
|
source_index = len(messages) - 1
|
|
messages = messages[: source_index + 1]
|
|
|
|
rounds = []
|
|
for i, msg in enumerate(messages):
|
|
if msg.role != "user":
|
|
continue
|
|
html = css = js = None
|
|
assistant_msg_id = None
|
|
for reply in messages[i + 1:]:
|
|
if reply.role == "user":
|
|
break
|
|
if reply.role == "assistant":
|
|
assistant_msg_id = reply.id
|
|
html = reply.code_html
|
|
css = reply.code_css
|
|
js = reply.code_js
|
|
break
|
|
rounds.append(
|
|
{
|
|
"question": msg.content,
|
|
"source": msg.source,
|
|
"prompt_level": msg.prompt_level,
|
|
"assistant_msg_id": assistant_msg_id,
|
|
"html": html,
|
|
"css": css,
|
|
"js": js,
|
|
}
|
|
)
|
|
return rounds
|
|
|
|
|
|
@router.get("/showcase/{submission_id}/prompt-chain/", response=List[PromptRoundOut])
|
|
@login_required
|
|
def get_showcase_prompt_chain(request, submission_id: UUID):
|
|
if not SubmissionAward.objects.filter(
|
|
submission_id=submission_id,
|
|
award__is_active=True,
|
|
).exists():
|
|
raise HttpError(404, "作品不存在或未授奖")
|
|
|
|
sub = get_object_or_404(Submission, id=submission_id)
|
|
try:
|
|
source_msg = Message.objects.select_related("conversation").get(submission=sub)
|
|
except Message.DoesNotExist:
|
|
raise HttpError(404, "该作品没有关联提示词链")
|
|
|
|
return _build_prompt_rounds(source_msg)
|
|
|
|
|
|
@router.get("/{submission_id}/prompt-chain", response=List[PromptRoundOut])
|
|
@login_required
|
|
def get_submission_prompt_chain(request, submission_id: UUID):
|
|
sub = get_object_or_404(Submission, id=submission_id)
|
|
try:
|
|
source_msg = Message.objects.select_related("conversation").get(submission=sub)
|
|
except Message.DoesNotExist:
|
|
raise HttpError(404, "该提交没有关联提示词链")
|
|
|
|
return _build_prompt_rounds(source_msg)
|
|
|
|
|
|
@router.get("/{submission_id}", response=SubmissionOut)
|
|
@login_required
|
|
def get_submission(request, submission_id: UUID):
|
|
"""
|
|
获取单个提交的详细信息
|
|
"""
|
|
user_rating_subquery = Subquery(
|
|
Rating.objects.filter(user=request.user, submission=OuterRef("pk")).values(
|
|
"score"
|
|
)[:1],
|
|
output_field=IntegerField(),
|
|
)
|
|
submission = get_object_or_404(
|
|
Submission.objects.select_related("task", "user").annotate(
|
|
my_score=user_rating_subquery
|
|
),
|
|
id=submission_id,
|
|
)
|
|
return submission
|
|
|
|
|
|
@router.post("/{submission_id}/view")
|
|
@login_required
|
|
def increment_view(request, submission_id: UUID):
|
|
"""
|
|
增加提交的浏览次数(仅在全屏预览时调用)
|
|
"""
|
|
updated = Submission.objects.filter(pk=submission_id).update(
|
|
view_count=F("view_count") + 1
|
|
)
|
|
if not updated:
|
|
raise HttpError(404, "提交不存在")
|
|
return {"ok": True}
|
|
|
|
|
|
@router.put("/{submission_id}/score")
|
|
@login_required
|
|
def update_score(request, submission_id: UUID, payload: RatingScoreIn):
|
|
"""
|
|
给提交打分
|
|
"""
|
|
if payload.score <= 0:
|
|
raise HttpError(400, "分数不能为零")
|
|
|
|
submission = get_object_or_404(Submission, id=submission_id)
|
|
|
|
_, created = Rating.objects.get_or_create(
|
|
user=request.user,
|
|
submission=submission,
|
|
defaults={"score": payload.score},
|
|
)
|
|
|
|
if created:
|
|
return {"message": "打分成功"}
|
|
else:
|
|
return {"message": "你已经给这个提交打过分了"}
|
|
|
|
|
|
@router.put("/{submission_id}/flag")
|
|
@login_required
|
|
def update_flag(request, submission_id: UUID, payload: FlagIn):
|
|
"""
|
|
设置或清除提交的标记(仅管理员和超级管理员可操作)
|
|
"""
|
|
if request.user.role not in (RoleChoices.SUPER, RoleChoices.ADMIN):
|
|
raise HttpError(403, "没有权限")
|
|
|
|
submission = get_object_or_404(Submission, id=submission_id)
|
|
submission.flag = payload.flag
|
|
submission.save(update_fields=["flag"])
|
|
return {"flag": submission.flag}
|