feat: add showcase backend
This commit is contained in:
@@ -1,3 +1,51 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
# Register your models here.
|
from .models import Award, SubmissionAward
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Award)
|
||||||
|
class AwardAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "sort_order", "is_active", "item_ordering", "created")
|
||||||
|
list_filter = ("is_active", "item_ordering")
|
||||||
|
search_fields = ("name",)
|
||||||
|
ordering = ("sort_order",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(SubmissionAward)
|
||||||
|
class SubmissionAwardAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"award_name",
|
||||||
|
"submission_username",
|
||||||
|
"submission_task_title",
|
||||||
|
"submission_score",
|
||||||
|
"submission_view_count",
|
||||||
|
"sort_order",
|
||||||
|
"awarded_at",
|
||||||
|
)
|
||||||
|
list_filter = ("award", "submission__task", "submission__user__classname")
|
||||||
|
search_fields = (
|
||||||
|
"award__name",
|
||||||
|
"submission__user__username",
|
||||||
|
"submission__task__title",
|
||||||
|
)
|
||||||
|
raw_id_fields = ("submission",)
|
||||||
|
|
||||||
|
@admin.display(description="奖项")
|
||||||
|
def award_name(self, obj):
|
||||||
|
return obj.award.name
|
||||||
|
|
||||||
|
@admin.display(description="提交作者")
|
||||||
|
def submission_username(self, obj):
|
||||||
|
return obj.submission.user.username
|
||||||
|
|
||||||
|
@admin.display(description="挑战标题")
|
||||||
|
def submission_task_title(self, obj):
|
||||||
|
return obj.submission.task.title
|
||||||
|
|
||||||
|
@admin.display(description="评分")
|
||||||
|
def submission_score(self, obj):
|
||||||
|
return obj.submission.score
|
||||||
|
|
||||||
|
@admin.display(description="浏览量")
|
||||||
|
def submission_view_count(self, obj):
|
||||||
|
return obj.submission.view_count
|
||||||
|
|||||||
@@ -6,13 +6,27 @@ from ninja.errors import HttpError
|
|||||||
from ninja.pagination import paginate
|
from ninja.pagination import paginate
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.db.models import Avg, Count, F, IntegerField, Max, OuterRef, Q, Subquery
|
from django.db.models import (
|
||||||
|
Avg,
|
||||||
|
Count,
|
||||||
|
Exists,
|
||||||
|
F,
|
||||||
|
IntegerField,
|
||||||
|
Max,
|
||||||
|
OuterRef,
|
||||||
|
Q,
|
||||||
|
Subquery,
|
||||||
|
)
|
||||||
from prompt.models import Conversation, Message
|
from prompt.models import Conversation, Message
|
||||||
|
|
||||||
|
|
||||||
from .schemas import (
|
from .schemas import (
|
||||||
FlagIn,
|
FlagIn,
|
||||||
FlagStats,
|
FlagStats,
|
||||||
|
AwardOut,
|
||||||
|
PromptRoundOut,
|
||||||
|
ShowcaseDetailOut,
|
||||||
|
ShowcaseItemOut,
|
||||||
SubmissionCountBucket,
|
SubmissionCountBucket,
|
||||||
SubmissionFilter,
|
SubmissionFilter,
|
||||||
SubmissionIn,
|
SubmissionIn,
|
||||||
@@ -24,7 +38,7 @@ from .schemas import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
from .models import Rating, Submission
|
from .models import Award, ItemOrdering, Rating, Submission, SubmissionAward
|
||||||
from task.models import Task
|
from task.models import Task
|
||||||
from account.models import RoleChoices, User
|
from account.models import RoleChoices, User
|
||||||
|
|
||||||
@@ -389,6 +403,132 @@ def get_task_stats(request, task_id: int, classname: Optional[str] = None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@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/{submission_id}/", response=ShowcaseDetailOut)
|
||||||
|
@login_required
|
||||||
|
def get_showcase_detail(request, submission_id: UUID):
|
||||||
|
if not SubmissionAward.objects.filter(submission_id=submission_id).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)
|
||||||
|
.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,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@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():
|
||||||
|
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, "该作品没有关联提示词链")
|
||||||
|
|
||||||
|
messages = list(source_msg.conversation.messages.all().order_by("created"))
|
||||||
|
rounds = []
|
||||||
|
for i, msg in enumerate(messages):
|
||||||
|
if msg.role != "user":
|
||||||
|
continue
|
||||||
|
html = css = js = None
|
||||||
|
for reply in messages[i + 1:]:
|
||||||
|
if reply.role == "user":
|
||||||
|
break
|
||||||
|
if reply.role == "assistant":
|
||||||
|
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,
|
||||||
|
"html": html,
|
||||||
|
"css": css,
|
||||||
|
"js": js,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return rounds
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{submission_id}", response=SubmissionOut)
|
@router.get("/{submission_id}", response=SubmissionOut)
|
||||||
@login_required
|
@login_required
|
||||||
def get_submission(request, submission_id: UUID):
|
def get_submission(request, submission_id: UUID):
|
||||||
@@ -462,5 +602,3 @@ def update_flag(request, submission_id: UUID, payload: FlagIn):
|
|||||||
return {"flag": submission.flag}
|
return {"flag": submission.flag}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
47
submission/migrations/0011_add_award_submissionaward.py
Normal file
47
submission/migrations/0011_add_award_submissionaward.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Generated by Django 6.0.1 on 2026-04-30 14:55
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django_extensions.db.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('submission', '0010_remove_conversation_fk'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Award',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
|
||||||
|
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
|
||||||
|
('name', models.CharField(max_length=100, unique=True, verbose_name='奖项名称')),
|
||||||
|
('description', models.TextField(blank=True, default='', verbose_name='奖项简介')),
|
||||||
|
('sort_order', models.IntegerField(db_index=True, default=0, verbose_name='排序值')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||||
|
('item_ordering', models.CharField(choices=[('manual', '手动排序'), ('awarded_at', '授奖时间倒序'), ('score', '评分倒序'), ('view_count', '浏览量倒序')], default='manual', max_length=20, verbose_name='作品排序方式')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('sort_order',),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SubmissionAward',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')),
|
||||||
|
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')),
|
||||||
|
('sort_order', models.IntegerField(db_index=True, default=0, verbose_name='手动排序值')),
|
||||||
|
('awarded_at', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='授奖时间')),
|
||||||
|
('award', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submission_awards', to='submission.award')),
|
||||||
|
('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='awards', to='submission.submission')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('sort_order',),
|
||||||
|
'unique_together': {('submission', 'award')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -166,3 +166,49 @@ def update_submission_score_on_save(sender, instance, **kwargs):
|
|||||||
当Rating保存时,更新对应的Submission的平均分
|
当Rating保存时,更新对应的Submission的平均分
|
||||||
"""
|
"""
|
||||||
instance.submission.update_score()
|
instance.submission.update_score()
|
||||||
|
|
||||||
|
|
||||||
|
class ItemOrdering(models.TextChoices):
|
||||||
|
MANUAL = "manual", "手动排序"
|
||||||
|
AWARDED_AT = "awarded_at", "授奖时间倒序"
|
||||||
|
SCORE = "score", "评分倒序"
|
||||||
|
VIEW_COUNT = "view_count", "浏览量倒序"
|
||||||
|
|
||||||
|
|
||||||
|
class Award(TimeStampedModel):
|
||||||
|
name = models.CharField(max_length=100, unique=True, verbose_name="奖项名称")
|
||||||
|
description = models.TextField(blank=True, default="", verbose_name="奖项简介")
|
||||||
|
sort_order = models.IntegerField(default=0, db_index=True, verbose_name="排序值")
|
||||||
|
is_active = models.BooleanField(default=True, verbose_name="是否启用")
|
||||||
|
item_ordering = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
choices=ItemOrdering.choices,
|
||||||
|
default=ItemOrdering.MANUAL,
|
||||||
|
verbose_name="作品排序方式",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ("sort_order",)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class SubmissionAward(TimeStampedModel):
|
||||||
|
submission = models.ForeignKey(
|
||||||
|
Submission, on_delete=models.CASCADE, related_name="awards"
|
||||||
|
)
|
||||||
|
award = models.ForeignKey(
|
||||||
|
Award, on_delete=models.CASCADE, related_name="submission_awards"
|
||||||
|
)
|
||||||
|
sort_order = models.IntegerField(default=0, db_index=True, verbose_name="手动排序值")
|
||||||
|
awarded_at = models.DateTimeField(
|
||||||
|
auto_now_add=True, db_index=True, verbose_name="授奖时间"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ("submission", "award")
|
||||||
|
ordering = ("sort_order",)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.award.name} - {self.submission}"
|
||||||
|
|||||||
@@ -158,3 +158,46 @@ class TaskStatsOut(Schema):
|
|||||||
classes: list[str]
|
classes: list[str]
|
||||||
top_viewed: list[TopViewedItem]
|
top_viewed: list[TopViewedItem]
|
||||||
|
|
||||||
|
|
||||||
|
class ShowcaseItemOut(Schema):
|
||||||
|
submission_id: UUID
|
||||||
|
username: str
|
||||||
|
task_title: str
|
||||||
|
task_display: int
|
||||||
|
score: float
|
||||||
|
view_count: int
|
||||||
|
html: Optional[str] = None
|
||||||
|
css: Optional[str] = None
|
||||||
|
js: Optional[str] = None
|
||||||
|
has_prompt_chain: bool
|
||||||
|
|
||||||
|
|
||||||
|
class AwardOut(Schema):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
item_ordering: str
|
||||||
|
items: list[ShowcaseItemOut]
|
||||||
|
|
||||||
|
|
||||||
|
class ShowcaseDetailOut(Schema):
|
||||||
|
submission_id: UUID
|
||||||
|
username: str
|
||||||
|
task_title: str
|
||||||
|
task_display: int
|
||||||
|
score: float
|
||||||
|
view_count: int
|
||||||
|
html: Optional[str] = None
|
||||||
|
css: Optional[str] = None
|
||||||
|
js: Optional[str] = None
|
||||||
|
awards: list[str]
|
||||||
|
has_prompt_chain: bool
|
||||||
|
|
||||||
|
|
||||||
|
class PromptRoundOut(Schema):
|
||||||
|
question: str
|
||||||
|
source: str
|
||||||
|
prompt_level: Optional[int] = None
|
||||||
|
html: Optional[str] = None
|
||||||
|
css: Optional[str] = None
|
||||||
|
js: Optional[str] = None
|
||||||
|
|||||||
316
submission/tests_showcase.py
Normal file
316
submission/tests_showcase.py
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import IntegrityError
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from submission.models import Award, Submission, SubmissionAward
|
||||||
|
from task.models import Task
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
def _make_user(username="student1", role="normal"):
|
||||||
|
return User.objects.create_user(username=username, password="pw", role=role)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_task(display=1):
|
||||||
|
return Task.objects.create(
|
||||||
|
title=f"Task {display}", task_type="challenge", display=display, content=""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_submission(user, task, score=0.0):
|
||||||
|
return Submission.objects.create(
|
||||||
|
user=user, task=task, html="<p>hi</p>", css="", js="", score=score
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_award(name="最佳设计", sort_order=0, is_active=True, item_ordering="manual"):
|
||||||
|
return Award.objects.create(
|
||||||
|
name=name,
|
||||||
|
sort_order=sort_order,
|
||||||
|
is_active=is_active,
|
||||||
|
item_ordering=item_ordering,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AwardModelTest(TestCase):
|
||||||
|
def test_unique_submission_award(self):
|
||||||
|
user = _make_user()
|
||||||
|
task = _make_task()
|
||||||
|
sub = _make_submission(user, task)
|
||||||
|
award = _make_award()
|
||||||
|
SubmissionAward.objects.create(submission=sub, award=award)
|
||||||
|
|
||||||
|
with self.assertRaises(IntegrityError):
|
||||||
|
SubmissionAward.objects.create(submission=sub, award=award)
|
||||||
|
|
||||||
|
def test_submission_can_have_multiple_awards(self):
|
||||||
|
user = _make_user()
|
||||||
|
task = _make_task()
|
||||||
|
sub = _make_submission(user, task)
|
||||||
|
a1 = _make_award("奖1", sort_order=0)
|
||||||
|
a2 = _make_award("奖2", sort_order=1)
|
||||||
|
SubmissionAward.objects.create(submission=sub, award=a1)
|
||||||
|
SubmissionAward.objects.create(submission=sub, award=a2)
|
||||||
|
|
||||||
|
self.assertEqual(sub.awards.count(), 2)
|
||||||
|
|
||||||
|
def test_award_can_have_multiple_submissions(self):
|
||||||
|
user = _make_user()
|
||||||
|
task = _make_task()
|
||||||
|
sub1 = _make_submission(user, task, score=3.0)
|
||||||
|
task2 = _make_task(display=2)
|
||||||
|
sub2 = _make_submission(user, task2, score=4.0)
|
||||||
|
award = _make_award()
|
||||||
|
SubmissionAward.objects.create(submission=sub1, award=award)
|
||||||
|
SubmissionAward.objects.create(submission=sub2, award=award)
|
||||||
|
|
||||||
|
self.assertEqual(award.submission_awards.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class ShowcaseListTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = _make_user("student1")
|
||||||
|
self.task = _make_task()
|
||||||
|
|
||||||
|
def test_unauthenticated_returns_401(self):
|
||||||
|
resp = self.client.get("/api/submission/showcase/")
|
||||||
|
self.assertEqual(resp.status_code, 401)
|
||||||
|
|
||||||
|
def test_authenticated_returns_200(self):
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
resp = self.client.get("/api/submission/showcase/")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
def test_inactive_award_not_returned(self):
|
||||||
|
award = _make_award("停用奖", is_active=False)
|
||||||
|
sub = _make_submission(self.user, self.task)
|
||||||
|
SubmissionAward.objects.create(submission=sub, award=award)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
resp = self.client.get("/api/submission/showcase/")
|
||||||
|
data = resp.json()
|
||||||
|
self.assertEqual(len(data), 0)
|
||||||
|
|
||||||
|
def test_award_with_no_items_not_returned(self):
|
||||||
|
_make_award("空奖项")
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
resp = self.client.get("/api/submission/showcase/")
|
||||||
|
data = resp.json()
|
||||||
|
self.assertEqual(len(data), 0)
|
||||||
|
|
||||||
|
def test_active_award_with_items_returned(self):
|
||||||
|
award = _make_award("最佳设计")
|
||||||
|
sub = _make_submission(self.user, self.task)
|
||||||
|
SubmissionAward.objects.create(submission=sub, award=award)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
resp = self.client.get("/api/submission/showcase/")
|
||||||
|
data = resp.json()
|
||||||
|
self.assertEqual(len(data), 1)
|
||||||
|
self.assertEqual(data[0]["name"], "最佳设计")
|
||||||
|
self.assertEqual(len(data[0]["items"]), 1)
|
||||||
|
item = data[0]["items"][0]
|
||||||
|
self.assertEqual(item["username"], "student1")
|
||||||
|
self.assertEqual(item["has_prompt_chain"], False)
|
||||||
|
|
||||||
|
def test_manual_ordering_uses_sort_order(self):
|
||||||
|
award = _make_award("奖", item_ordering="manual")
|
||||||
|
sub1 = _make_submission(self.user, self.task)
|
||||||
|
task2 = _make_task(display=2)
|
||||||
|
sub2 = _make_submission(self.user, task2)
|
||||||
|
SubmissionAward.objects.create(submission=sub1, award=award, sort_order=2)
|
||||||
|
SubmissionAward.objects.create(submission=sub2, award=award, sort_order=1)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
resp = self.client.get("/api/submission/showcase/")
|
||||||
|
items = resp.json()[0]["items"]
|
||||||
|
self.assertEqual(items[0]["task_display"], task2.display)
|
||||||
|
self.assertEqual(items[1]["task_display"], self.task.display)
|
||||||
|
|
||||||
|
def test_score_ordering(self):
|
||||||
|
award = _make_award("奖", item_ordering="score")
|
||||||
|
sub1 = _make_submission(self.user, self.task, score=2.0)
|
||||||
|
task2 = _make_task(display=2)
|
||||||
|
sub2 = _make_submission(self.user, task2, score=4.0)
|
||||||
|
SubmissionAward.objects.create(submission=sub1, award=award)
|
||||||
|
SubmissionAward.objects.create(submission=sub2, award=award)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
resp = self.client.get("/api/submission/showcase/")
|
||||||
|
items = resp.json()[0]["items"]
|
||||||
|
self.assertGreater(items[0]["score"], items[1]["score"])
|
||||||
|
|
||||||
|
def test_view_count_ordering(self):
|
||||||
|
award = _make_award("奖", item_ordering="view_count")
|
||||||
|
sub1 = _make_submission(self.user, self.task)
|
||||||
|
sub1.view_count = 5
|
||||||
|
sub1.save(update_fields=["view_count"])
|
||||||
|
task2 = _make_task(display=2)
|
||||||
|
sub2 = _make_submission(self.user, task2)
|
||||||
|
sub2.view_count = 20
|
||||||
|
sub2.save(update_fields=["view_count"])
|
||||||
|
SubmissionAward.objects.create(submission=sub1, award=award)
|
||||||
|
SubmissionAward.objects.create(submission=sub2, award=award)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
resp = self.client.get("/api/submission/showcase/")
|
||||||
|
items = resp.json()[0]["items"]
|
||||||
|
self.assertGreater(items[0]["view_count"], items[1]["view_count"])
|
||||||
|
|
||||||
|
def test_has_prompt_chain_true_when_source_message_exists(self):
|
||||||
|
from prompt.models import Conversation, Message
|
||||||
|
|
||||||
|
award = _make_award("奖")
|
||||||
|
sub = _make_submission(self.user, self.task)
|
||||||
|
conv = Conversation.objects.create(user=self.user, task=self.task)
|
||||||
|
Message.objects.create(conversation=conv, role="user", content="做个按钮")
|
||||||
|
Message.objects.create(
|
||||||
|
conversation=conv,
|
||||||
|
role="assistant",
|
||||||
|
content="好的",
|
||||||
|
code_html="<button>OK</button>",
|
||||||
|
code_css="",
|
||||||
|
code_js="",
|
||||||
|
submission=sub,
|
||||||
|
)
|
||||||
|
SubmissionAward.objects.create(submission=sub, award=award)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
resp = self.client.get("/api/submission/showcase/")
|
||||||
|
item = resp.json()[0]["items"][0]
|
||||||
|
self.assertTrue(item["has_prompt_chain"])
|
||||||
|
|
||||||
|
|
||||||
|
class ShowcaseDetailTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = _make_user("student1")
|
||||||
|
self.task = _make_task()
|
||||||
|
self.award = _make_award("最佳设计")
|
||||||
|
self.sub = _make_submission(self.user, self.task, score=4.5)
|
||||||
|
self.sub.view_count = 10
|
||||||
|
self.sub.save(update_fields=["view_count"])
|
||||||
|
SubmissionAward.objects.create(submission=self.sub, award=self.award)
|
||||||
|
|
||||||
|
def test_unauthenticated_returns_401(self):
|
||||||
|
resp = self.client.get(f"/api/submission/showcase/{self.sub.id}/")
|
||||||
|
self.assertEqual(resp.status_code, 401)
|
||||||
|
|
||||||
|
def test_awarded_submission_accessible(self):
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
resp = self.client.get(f"/api/submission/showcase/{self.sub.id}/")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
data = resp.json()
|
||||||
|
self.assertEqual(data["username"], "student1")
|
||||||
|
self.assertEqual(data["score"], 4.5)
|
||||||
|
self.assertEqual(data["view_count"], 10)
|
||||||
|
self.assertIn("最佳设计", data["awards"])
|
||||||
|
self.assertFalse(data["has_prompt_chain"])
|
||||||
|
|
||||||
|
def test_non_awarded_submission_returns_404(self):
|
||||||
|
other_sub = _make_submission(self.user, self.task)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
resp = self.client.get(f"/api/submission/showcase/{other_sub.id}/")
|
||||||
|
self.assertEqual(resp.status_code, 404)
|
||||||
|
|
||||||
|
def test_submission_shows_all_its_awards(self):
|
||||||
|
award2 = _make_award("最佳游戏", sort_order=1)
|
||||||
|
SubmissionAward.objects.create(submission=self.sub, award=award2)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
resp = self.client.get(f"/api/submission/showcase/{self.sub.id}/")
|
||||||
|
data = resp.json()
|
||||||
|
self.assertIn("最佳设计", data["awards"])
|
||||||
|
self.assertIn("最佳游戏", data["awards"])
|
||||||
|
|
||||||
|
|
||||||
|
class ShowcasePromptChainTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
from prompt.models import Conversation, Message as Msg
|
||||||
|
|
||||||
|
self.user = _make_user("student1")
|
||||||
|
self.task = _make_task()
|
||||||
|
self.award = _make_award("最佳设计")
|
||||||
|
self.sub = _make_submission(self.user, self.task)
|
||||||
|
SubmissionAward.objects.create(submission=self.sub, award=self.award)
|
||||||
|
|
||||||
|
self.conv = Conversation.objects.create(user=self.user, task=self.task)
|
||||||
|
Msg.objects.create(
|
||||||
|
conversation=self.conv,
|
||||||
|
role="user",
|
||||||
|
content="做个按钮",
|
||||||
|
source="conversation",
|
||||||
|
prompt_level=3,
|
||||||
|
)
|
||||||
|
Msg.objects.create(
|
||||||
|
conversation=self.conv,
|
||||||
|
role="assistant",
|
||||||
|
content="好的",
|
||||||
|
code_html="<button>OK</button>",
|
||||||
|
code_css="button{color:red}",
|
||||||
|
code_js="console.log(1)",
|
||||||
|
submission=self.sub,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_unauthenticated_returns_401(self):
|
||||||
|
resp = self.client.get(
|
||||||
|
f"/api/submission/showcase/{self.sub.id}/prompt-chain/"
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.status_code, 401)
|
||||||
|
|
||||||
|
def test_no_source_message_returns_404(self):
|
||||||
|
other_sub = _make_submission(self.user, self.task)
|
||||||
|
SubmissionAward.objects.create(submission=other_sub, award=self.award)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
resp = self.client.get(
|
||||||
|
f"/api/submission/showcase/{other_sub.id}/prompt-chain/"
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.status_code, 404)
|
||||||
|
|
||||||
|
def test_non_awarded_submission_returns_404(self):
|
||||||
|
from prompt.models import Conversation, Message as Msg
|
||||||
|
|
||||||
|
other_sub = _make_submission(self.user, self.task)
|
||||||
|
conv = Conversation.objects.create(user=self.user, task=self.task)
|
||||||
|
Msg.objects.create(
|
||||||
|
conversation=conv,
|
||||||
|
role="assistant",
|
||||||
|
content="x",
|
||||||
|
submission=other_sub,
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
resp = self.client.get(
|
||||||
|
f"/api/submission/showcase/{other_sub.id}/prompt-chain/"
|
||||||
|
)
|
||||||
|
self.assertEqual(resp.status_code, 404)
|
||||||
|
|
||||||
|
def test_returns_prompt_rounds(self):
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
resp = self.client.get(f"/api/submission/showcase/{self.sub.id}/prompt-chain/")
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
rounds = resp.json()
|
||||||
|
self.assertEqual(len(rounds), 1)
|
||||||
|
r = rounds[0]
|
||||||
|
self.assertEqual(r["question"], "做个按钮")
|
||||||
|
self.assertEqual(r["source"], "conversation")
|
||||||
|
self.assertEqual(r["prompt_level"], 3)
|
||||||
|
self.assertEqual(r["html"], "<button>OK</button>")
|
||||||
|
self.assertEqual(r["css"], "button{color:red}")
|
||||||
|
self.assertEqual(r["js"], "console.log(1)")
|
||||||
|
|
||||||
|
def test_multiple_rounds(self):
|
||||||
|
from prompt.models import Message as Msg
|
||||||
|
|
||||||
|
Msg.objects.create(
|
||||||
|
conversation=self.conv,
|
||||||
|
role="user",
|
||||||
|
content="再加个标题",
|
||||||
|
source="manual",
|
||||||
|
)
|
||||||
|
Msg.objects.create(
|
||||||
|
conversation=self.conv,
|
||||||
|
role="assistant",
|
||||||
|
content="好",
|
||||||
|
code_html="<h1>标题</h1>",
|
||||||
|
code_css="",
|
||||||
|
code_js="",
|
||||||
|
)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
resp = self.client.get(f"/api/submission/showcase/{self.sub.id}/prompt-chain/")
|
||||||
|
rounds = resp.json()
|
||||||
|
self.assertEqual(len(rounds), 2)
|
||||||
|
self.assertEqual(rounds[1]["question"], "再加个标题")
|
||||||
|
self.assertEqual(rounds[1]["source"], "manual")
|
||||||
Reference in New Issue
Block a user