add asset
This commit is contained in:
@@ -28,6 +28,7 @@ api.add_router("tutorial/", "task.tutorial.router")
|
|||||||
api.add_router("challenge/", "task.challenge.router")
|
api.add_router("challenge/", "task.challenge.router")
|
||||||
api.add_router("submission/", "submission.api.router")
|
api.add_router("submission/", "submission.api.router")
|
||||||
api.add_router("upload/", "utils.upload.router")
|
api.add_router("upload/", "utils.upload.router")
|
||||||
|
api.add_router("assets/", "task.assets.router")
|
||||||
api.add_router("prompt/", "prompt.api.router")
|
api.add_router("prompt/", "prompt.api.router")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
75
task/assets.py
Normal file
75
task/assets.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
from ninja import Router, File, Form, Schema
|
||||||
|
from ninja.files import UploadedFile
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.conf import settings
|
||||||
|
from account.decorators import admin_required, super_required
|
||||||
|
from .models import Challenge, Tutorial, TaskAsset
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
class AssetOut(Schema):
|
||||||
|
name: str
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
def _asset_url(asset: TaskAsset) -> str:
|
||||||
|
return f"{settings.MEDIA_URL}{asset.file.name}"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Challenge assets ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@router.get("/challenge/{display}", response=list[AssetOut])
|
||||||
|
def list_challenge_assets(request, display: int):
|
||||||
|
challenge = get_object_or_404(Challenge, display=display)
|
||||||
|
return [AssetOut(name=a.name, url=_asset_url(a)) for a in challenge.assets.all()]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/challenge/{display}", response=AssetOut)
|
||||||
|
@admin_required
|
||||||
|
def upload_challenge_asset(request, display: int, name: Form[str], file: File[UploadedFile]):
|
||||||
|
challenge = get_object_or_404(Challenge, display=display)
|
||||||
|
asset, _ = TaskAsset.objects.get_or_create(task=challenge.task_ptr, name=name)
|
||||||
|
if asset.file:
|
||||||
|
asset.file.delete(save=False)
|
||||||
|
asset.file.save(name, file, save=True)
|
||||||
|
return AssetOut(name=asset.name, url=_asset_url(asset))
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/challenge/{display}/{name}")
|
||||||
|
@admin_required
|
||||||
|
def delete_challenge_asset(request, display: int, name: str):
|
||||||
|
challenge = get_object_or_404(Challenge, display=display)
|
||||||
|
asset = get_object_or_404(TaskAsset, task=challenge.task_ptr, name=name)
|
||||||
|
asset.file.delete(save=False)
|
||||||
|
asset.delete()
|
||||||
|
return {"message": "删除成功"}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Tutorial assets ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@router.get("/tutorial/{display}", response=list[AssetOut])
|
||||||
|
def list_tutorial_assets(request, display: int):
|
||||||
|
tutorial = get_object_or_404(Tutorial, display=display)
|
||||||
|
return [AssetOut(name=a.name, url=_asset_url(a)) for a in tutorial.assets.all()]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/tutorial/{display}", response=AssetOut)
|
||||||
|
@super_required
|
||||||
|
def upload_tutorial_asset(request, display: int, name: Form[str], file: File[UploadedFile]):
|
||||||
|
tutorial = get_object_or_404(Tutorial, display=display)
|
||||||
|
asset, _ = TaskAsset.objects.get_or_create(task=tutorial.task_ptr, name=name)
|
||||||
|
if asset.file:
|
||||||
|
asset.file.delete(save=False)
|
||||||
|
asset.file.save(name, file, save=True)
|
||||||
|
return AssetOut(name=asset.name, url=_asset_url(asset))
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/tutorial/{display}/{name}")
|
||||||
|
@super_required
|
||||||
|
def delete_tutorial_asset(request, display: int, name: str):
|
||||||
|
tutorial = get_object_or_404(Tutorial, display=display)
|
||||||
|
asset = get_object_or_404(TaskAsset, task=tutorial.task_ptr, name=name)
|
||||||
|
asset.file.delete(save=False)
|
||||||
|
asset.delete()
|
||||||
|
return {"message": "删除成功"}
|
||||||
29
task/migrations/0007_taskasset.py
Normal file
29
task/migrations/0007_taskasset.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 6.0.1 on 2026-04-13 09:33
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import task.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('task', '0006_challenge_pass_score'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='TaskAsset',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='文件名')),
|
||||||
|
('file', models.FileField(upload_to=task.models.task_asset_upload_to, verbose_name='文件')),
|
||||||
|
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='task.task')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '任务素材',
|
||||||
|
'verbose_name_plural': '任务素材',
|
||||||
|
'unique_together': {('task', 'name')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -49,3 +49,21 @@ class Challenge(Task):
|
|||||||
ordering = ("display",)
|
ordering = ("display",)
|
||||||
verbose_name = "挑战"
|
verbose_name = "挑战"
|
||||||
verbose_name_plural = verbose_name
|
verbose_name_plural = verbose_name
|
||||||
|
|
||||||
|
|
||||||
|
def task_asset_upload_to(instance, filename):
|
||||||
|
return f"tasks/{instance.task.task_type}/{instance.task.display}/{instance.name}"
|
||||||
|
|
||||||
|
|
||||||
|
class TaskAsset(models.Model):
|
||||||
|
task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name="assets")
|
||||||
|
name = models.CharField(max_length=100, verbose_name="文件名")
|
||||||
|
file = models.FileField(upload_to=task_asset_upload_to, verbose_name="文件")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ("task", "name")
|
||||||
|
verbose_name = "任务素材"
|
||||||
|
verbose_name_plural = verbose_name
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.task} / {self.name}"
|
||||||
|
|||||||
Reference in New Issue
Block a user