add showcase manage
This commit is contained in:
@@ -17,16 +17,23 @@ from django.db.models import (
|
||||
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,
|
||||
@@ -45,6 +52,67 @@ 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):
|
||||
@@ -457,10 +525,141 @@ def list_showcase(request):
|
||||
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).exists():
|
||||
if not SubmissionAward.objects.filter(
|
||||
submission_id=submission_id,
|
||||
award__is_active=True,
|
||||
).exists():
|
||||
raise HttpError(404, "作品不存在或未授奖")
|
||||
|
||||
sub = get_object_or_404(
|
||||
@@ -470,6 +669,7 @@ def get_showcase_detail(request, submission_id: UUID):
|
||||
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)
|
||||
)
|
||||
@@ -529,7 +729,10 @@ def _build_prompt_rounds(source_msg: Message):
|
||||
@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).exists():
|
||||
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)
|
||||
@@ -624,4 +827,3 @@ def update_flag(request, submission_id: UUID, payload: FlagIn):
|
||||
submission.flag = payload.flag
|
||||
submission.save(update_fields=["flag"])
|
||||
return {"flag": submission.flag}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional, Literal
|
||||
from ninja import Schema
|
||||
from uuid import UUID
|
||||
@@ -180,6 +181,56 @@ class AwardOut(Schema):
|
||||
items: list[ShowcaseItemOut]
|
||||
|
||||
|
||||
class AwardManageIn(Schema):
|
||||
name: str
|
||||
description: str = ""
|
||||
sort_order: int = 0
|
||||
is_active: bool = True
|
||||
item_ordering: str = "manual"
|
||||
|
||||
|
||||
class AwardManageOut(Schema):
|
||||
id: int
|
||||
name: str
|
||||
description: str
|
||||
sort_order: int
|
||||
is_active: bool
|
||||
item_ordering: str
|
||||
item_count: int
|
||||
|
||||
|
||||
class AwardItemIn(Schema):
|
||||
submission_id: UUID
|
||||
sort_order: int = 0
|
||||
|
||||
|
||||
class AwardItemUpdateIn(Schema):
|
||||
sort_order: int = 0
|
||||
|
||||
|
||||
class ShowcaseSubmissionLookupOut(Schema):
|
||||
submission_id: UUID
|
||||
username: str
|
||||
task_title: str
|
||||
task_display: int
|
||||
score: float
|
||||
view_count: int
|
||||
has_prompt_chain: bool
|
||||
|
||||
|
||||
class AwardItemManageOut(Schema):
|
||||
id: int
|
||||
submission_id: UUID
|
||||
username: str
|
||||
task_title: str
|
||||
task_display: int
|
||||
score: float
|
||||
view_count: int
|
||||
sort_order: int
|
||||
awarded_at: datetime
|
||||
has_prompt_chain: bool
|
||||
|
||||
|
||||
class ShowcaseDetailOut(Schema):
|
||||
submission_id: UUID
|
||||
username: str
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import TestCase
|
||||
|
||||
from account.models import RoleChoices
|
||||
from prompt.models import Conversation, Message
|
||||
from task.models import Task
|
||||
|
||||
from .models import Submission
|
||||
from .models import Award, Submission, SubmissionAward
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@@ -74,3 +75,177 @@ class SubmissionPromptChainTest(TestCase):
|
||||
self.assertEqual(len(data), 1)
|
||||
self.assertEqual(data[0]["question"], "author prompt")
|
||||
self.assertEqual(data[0]["html"], "<button>author</button>")
|
||||
|
||||
|
||||
class ShowcaseManagementApiTest(TestCase):
|
||||
def setUp(self):
|
||||
self.admin = _make_user("admin")
|
||||
self.admin.role = RoleChoices.ADMIN
|
||||
self.admin.save(update_fields=["role"])
|
||||
self.student = _make_user("student")
|
||||
self.task = _make_task()
|
||||
self.award = Award.objects.create(name="最佳视觉", sort_order=10)
|
||||
self.submission = Submission.objects.create(
|
||||
user=self.student,
|
||||
task=self.task,
|
||||
html="<main>work</main>",
|
||||
css="main { color: red; }",
|
||||
js="",
|
||||
score=4.5,
|
||||
view_count=8,
|
||||
)
|
||||
|
||||
def test_normal_user_cannot_access_management_api(self):
|
||||
self.client.force_login(self.student)
|
||||
|
||||
resp = self.client.get("/api/submission/showcase/manage/awards")
|
||||
lookup_resp = self.client.get(
|
||||
f"/api/submission/showcase/manage/submissions/{self.submission.id}"
|
||||
)
|
||||
|
||||
self.assertIn(resp.status_code, (302, 403))
|
||||
self.assertIn(lookup_resp.status_code, (302, 403))
|
||||
|
||||
def test_admin_can_find_submission_by_id_for_showcase_management(self):
|
||||
self.client.force_login(self.admin)
|
||||
|
||||
resp = self.client.get(
|
||||
f"/api/submission/showcase/manage/submissions/{self.submission.id}"
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = resp.json()
|
||||
self.assertEqual(data["submission_id"], str(self.submission.id))
|
||||
self.assertEqual(data["username"], "student")
|
||||
self.assertEqual(data["task_title"], "Test Challenge")
|
||||
self.assertEqual(data["task_display"], 1)
|
||||
self.assertEqual(data["score"], 4.5)
|
||||
self.assertEqual(data["view_count"], 8)
|
||||
self.assertFalse(data["has_prompt_chain"])
|
||||
self.assertNotIn("html", data)
|
||||
|
||||
def test_admin_can_create_and_update_award(self):
|
||||
self.client.force_login(self.admin)
|
||||
|
||||
create_resp = self.client.post(
|
||||
"/api/submission/showcase/manage/awards",
|
||||
data={
|
||||
"name": "最佳互动",
|
||||
"description": "交互完整",
|
||||
"sort_order": 3,
|
||||
"is_active": True,
|
||||
"item_ordering": "score",
|
||||
},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(create_resp.status_code, 200)
|
||||
created = create_resp.json()
|
||||
self.assertEqual(created["name"], "最佳互动")
|
||||
self.assertEqual(created["item_count"], 0)
|
||||
|
||||
update_resp = self.client.put(
|
||||
f"/api/submission/showcase/manage/awards/{created['id']}",
|
||||
data={
|
||||
"name": "最佳交互",
|
||||
"description": "操作体验完整",
|
||||
"sort_order": 1,
|
||||
"is_active": False,
|
||||
"item_ordering": "view_count",
|
||||
},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(update_resp.status_code, 200)
|
||||
updated = update_resp.json()
|
||||
self.assertEqual(updated["name"], "最佳交互")
|
||||
self.assertEqual(updated["description"], "操作体验完整")
|
||||
self.assertEqual(updated["sort_order"], 1)
|
||||
self.assertFalse(updated["is_active"])
|
||||
self.assertEqual(updated["item_ordering"], "view_count")
|
||||
|
||||
def test_admin_cannot_add_same_submission_twice(self):
|
||||
self.client.force_login(self.admin)
|
||||
payload = {"submission_id": str(self.submission.id), "sort_order": 2}
|
||||
|
||||
first_resp = self.client.post(
|
||||
f"/api/submission/showcase/manage/awards/{self.award.id}/items",
|
||||
data=payload,
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(first_resp.status_code, 200)
|
||||
self.assertEqual(first_resp.json()["submission_id"], str(self.submission.id))
|
||||
|
||||
duplicate_resp = self.client.post(
|
||||
f"/api/submission/showcase/manage/awards/{self.award.id}/items",
|
||||
data=payload,
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
self.assertEqual(duplicate_resp.status_code, 400)
|
||||
self.assertEqual(
|
||||
SubmissionAward.objects.filter(
|
||||
award=self.award,
|
||||
submission=self.submission,
|
||||
).count(),
|
||||
1,
|
||||
)
|
||||
|
||||
def test_public_showcase_hides_removed_or_inactive_items(self):
|
||||
self.client.force_login(self.admin)
|
||||
add_resp = self.client.post(
|
||||
f"/api/submission/showcase/manage/awards/{self.award.id}/items",
|
||||
data={"submission_id": str(self.submission.id), "sort_order": 0},
|
||||
content_type="application/json",
|
||||
)
|
||||
item_id = add_resp.json()["id"]
|
||||
|
||||
self.client.force_login(self.student)
|
||||
visible_resp = self.client.get("/api/submission/showcase/")
|
||||
self.assertEqual(visible_resp.status_code, 200)
|
||||
self.assertEqual(len(visible_resp.json()), 1)
|
||||
detail_resp = self.client.get(
|
||||
f"/api/submission/showcase/{self.submission.id}/"
|
||||
)
|
||||
self.assertEqual(detail_resp.status_code, 200)
|
||||
|
||||
self.client.force_login(self.admin)
|
||||
delete_resp = self.client.delete(
|
||||
f"/api/submission/showcase/manage/items/{item_id}"
|
||||
)
|
||||
self.assertEqual(delete_resp.status_code, 200)
|
||||
|
||||
self.client.force_login(self.student)
|
||||
removed_resp = self.client.get("/api/submission/showcase/")
|
||||
self.assertEqual(removed_resp.status_code, 200)
|
||||
self.assertEqual(removed_resp.json(), [])
|
||||
removed_detail_resp = self.client.get(
|
||||
f"/api/submission/showcase/{self.submission.id}/"
|
||||
)
|
||||
self.assertEqual(removed_detail_resp.status_code, 404)
|
||||
|
||||
self.client.force_login(self.admin)
|
||||
self.client.post(
|
||||
f"/api/submission/showcase/manage/awards/{self.award.id}/items",
|
||||
data={"submission_id": str(self.submission.id), "sort_order": 0},
|
||||
content_type="application/json",
|
||||
)
|
||||
deactivate_resp = self.client.put(
|
||||
f"/api/submission/showcase/manage/awards/{self.award.id}",
|
||||
data={
|
||||
"name": self.award.name,
|
||||
"description": self.award.description,
|
||||
"sort_order": self.award.sort_order,
|
||||
"is_active": False,
|
||||
"item_ordering": self.award.item_ordering,
|
||||
},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(deactivate_resp.status_code, 200)
|
||||
|
||||
self.client.force_login(self.student)
|
||||
inactive_resp = self.client.get("/api/submission/showcase/")
|
||||
self.assertEqual(inactive_resp.status_code, 200)
|
||||
self.assertEqual(inactive_resp.json(), [])
|
||||
inactive_detail_resp = self.client.get(
|
||||
f"/api/submission/showcase/{self.submission.id}/"
|
||||
)
|
||||
self.assertEqual(inactive_detail_resp.status_code, 404)
|
||||
|
||||
Reference in New Issue
Block a user