feat: web UI - dashboard, requirement/submission pages, create & upload forms
This commit is contained in:
@@ -30,6 +30,7 @@ INSTALLED_APPS = [
|
|||||||
'corsheaders',
|
'corsheaders',
|
||||||
'requirements',
|
'requirements',
|
||||||
'submissions',
|
'submissions',
|
||||||
|
'web',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from django.urls import include, path
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
|
path('', include('web.urls')),
|
||||||
path('api/', include('requirements.urls')),
|
path('api/', include('requirements.urls')),
|
||||||
path('api/auth/login/', include('rest_framework.urls')),
|
path('api/auth/login/', include('rest_framework.urls')),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class WebConfig(AppConfig):
|
||||||
|
name = 'web'
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-hans">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{% block title %}OJplatform{% endblock %}</title>
|
||||||
|
<style>
|
||||||
|
:root{--bg:#0f1115;--card:#1a1d24;--border:#2a2e37;--text:#e6e8ee;--muted:#8b90a0;--accent:#4f8cff;--ok:#3fb950;--warn:#d29922;--bad:#f85149;}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{margin:0;font-family:-apple-system,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;background:var(--bg);color:var(--text);line-height:1.6}
|
||||||
|
a{color:var(--accent);text-decoration:none}
|
||||||
|
a:hover{text-decoration:underline}
|
||||||
|
.nav{display:flex;align-items:center;gap:16px;padding:14px 24px;background:var(--card);border-bottom:1px solid var(--border)}
|
||||||
|
.nav b{font-size:18px}
|
||||||
|
.nav .spacer{flex:1}
|
||||||
|
.nav a{color:var(--muted);font-size:14px}
|
||||||
|
.nav a:hover{color:var(--text);text-decoration:none}
|
||||||
|
.wrap{max-width:1000px;margin:0 auto;padding:24px}
|
||||||
|
.card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:20px;margin-bottom:16px}
|
||||||
|
h1{font-size:22px;margin:0 0 16px}
|
||||||
|
h2{font-size:16px;margin:0 0 12px}
|
||||||
|
table{width:100%;border-collapse:collapse;font-size:14px}
|
||||||
|
th,td{text-align:left;padding:10px 12px;border-bottom:1px solid var(--border)}
|
||||||
|
th{color:var(--muted);font-weight:500}
|
||||||
|
.tag{display:inline-block;padding:2px 10px;border-radius:20px;font-size:12px}
|
||||||
|
.tag.pending{background:#3a3f4b;color:#cfd3dd}
|
||||||
|
.tag.running{background:#1f3a5f;color:#7ab8ff}
|
||||||
|
.tag.completed{background:#173a26;color:var(--ok)}
|
||||||
|
.tag.failed{background:#3f1d1d;color:var(--bad)}
|
||||||
|
.muted{color:var(--muted);font-size:13px}
|
||||||
|
pre{background:#0c0e12;border:1px solid var(--border);border-radius:8px;padding:14px;overflow:auto;font-size:13px;line-height:1.5}
|
||||||
|
.grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
||||||
|
@media(max-width:700px){.grid{grid-template-columns:1fr}}
|
||||||
|
.btn{display:inline-block;background:var(--accent);color:#fff;border:0;border-radius:6px;padding:8px 16px;font-size:14px;cursor:pointer}
|
||||||
|
.btn:hover{filter:brightness(1.1);text-decoration:none}
|
||||||
|
input,textarea{width:100%;background:#0c0e12;border:1px solid var(--border);border-radius:6px;color:var(--text);padding:9px 12px;font-size:14px;font-family:inherit}
|
||||||
|
label{display:block;margin:12px 0 6px;font-size:13px;color:var(--muted)}
|
||||||
|
.bar{height:8px;background:#2a2e37;border-radius:4px;overflow:hidden}
|
||||||
|
.bar i{display:block;height:100%;background:var(--ok)}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="nav">
|
||||||
|
<b>OJplatform</b>
|
||||||
|
<span class="spacer"></span>
|
||||||
|
<a href="/">仪表盘</a>
|
||||||
|
<a href="{% url 'admin:index' %}">管理后台</a>
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<form method="post" action="{% url 'logout' %}" style="display:inline">{% csrf_token %}<a class="btn" href="#" onclick="this.closest('form').submit()">退出</a></form>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn" href="{% url 'login' %}">登录</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="wrap">
|
||||||
|
{% if messages %}{% for m in messages %}<div class="card" style="border-color:var(--accent)">{{ m }}</div>{% endfor %}{% endif %}
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{% extends "web/base.html" %}
|
||||||
|
{% block title %}新建作业要求 · OJplatform{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>新建作业要求</h1>
|
||||||
|
<div class="card">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<label>标题</label>
|
||||||
|
<input type="text" name="title" required placeholder="例如: 期末项目 - Web 服务">
|
||||||
|
<label>作业要求(平台将据此用 AI 分析需要检测哪些维度)</label>
|
||||||
|
<textarea name="content" rows="8" required placeholder="例如: 实现一个 web 服务, 首页返回 ok, 稳定度要达到 99%, 支持并发 50, 功能点: 登录、列表、详情"></textarea>
|
||||||
|
|
||||||
|
<label>LLM 模式</label>
|
||||||
|
<select name="llm_mode" id="llm_mode">
|
||||||
|
<option value="default">default(用 .env 全局配置: {{ llm_base_url }} / {{ llm_model }})</option>
|
||||||
|
<option value="custom">custom(本次自定义)</option>
|
||||||
|
</select>
|
||||||
|
<div id="custom_fields" style="display:none">
|
||||||
|
<label>LLM 接口地址(base_url)</label>
|
||||||
|
<input type="text" name="llm_base_url" placeholder="http://host:port/v1">
|
||||||
|
<label>API Key(可留空)</label>
|
||||||
|
<input type="password" name="llm_api_key" placeholder="sk-...">
|
||||||
|
<label>模型名</label>
|
||||||
|
<input type="text" name="llm_model" placeholder="qwen38">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top:20px"><button class="btn" type="submit">创建并分析</button></p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
var sel=document.getElementById('llm_mode');
|
||||||
|
var cf=document.getElementById('custom_fields');
|
||||||
|
sel.addEventListener('change',function(){cf.style.display=sel.value==='custom'?'block':'none';});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{% extends "web/base.html" %}
|
||||||
|
{% block title %}仪表盘 · OJplatform{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>仪表盘</h1>
|
||||||
|
<p style="margin:-8px 0 20px"><a class="btn" href="{% url 'create-requirement' %}">+ 新建作业要求</a></p>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card">
|
||||||
|
<h2>作业要求</h2>
|
||||||
|
{% if requirements %}
|
||||||
|
<table>
|
||||||
|
<tr><th>标题</th><th>提交数</th><th>创建时间</th></tr>
|
||||||
|
{% for r in requirements %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="/requirement/{{ r.pk }}/">{{ r.title }}</a></td>
|
||||||
|
<td>{{ r.sub_count }}</td>
|
||||||
|
<td class="muted">{{ r.created_at|date:"Y-m-d H:i" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% else %}<p class="muted">暂无作业要求。</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>最近提交</h2>
|
||||||
|
{% if recent_submissions %}
|
||||||
|
<table>
|
||||||
|
<tr><th>名称</th><th>状态</th><th>时间</th></tr>
|
||||||
|
{% for s in recent_submissions %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="/submission/{{ s.pk }}">{{ s.name }}</a></td>
|
||||||
|
<td><span class="tag {{ s.status }}">{{ s.get_status_display }}</span></td>
|
||||||
|
<td class="muted">{{ s.created_at|date:"Y-m-d H:i" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% else %}<p class="muted">暂无提交。</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{% extends "web/base.html" %}
|
||||||
|
{% load extras %}
|
||||||
|
{% block title %}{{ requirement.title }} · OJplatform{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ requirement.title }}</h1>
|
||||||
|
<div class="card">
|
||||||
|
<h2>作业要求</h2>
|
||||||
|
<p style="white-space:pre-wrap">{{ requirement.content }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>AI 分析出的检测维度</h2>
|
||||||
|
<pre>{{ requirement.spec|json_filter }}</pre>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>上传项目包</h2>
|
||||||
|
<form method="post" action="{% url 'upload-submission' requirement.pk %}" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
<label>提交名称</label>
|
||||||
|
<input type="text" name="name" placeholder="例如: 张三-第1次">
|
||||||
|
<label>项目包(tar / tar.gz / zip)</label>
|
||||||
|
<input type="file" name="file" required>
|
||||||
|
<p><button class="btn" type="submit">提交并检测</button></p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>提交记录 ({{ submissions|length }})</h2>
|
||||||
|
{% if submissions %}
|
||||||
|
<table>
|
||||||
|
<tr><th>名称</th><th>提交人</th><th>状态</th><th>时间</th></tr>
|
||||||
|
{% for s in submissions %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="/submission/{{ s.pk }}">{{ s.name }}</a></td>
|
||||||
|
<td>{{ s.submitted_by.username|default:"-" }}</td>
|
||||||
|
<td><span class="tag {{ s.status }}">{{ s.get_status_display }}</span></td>
|
||||||
|
<td class="muted">{{ s.created_at|date:"Y-m-d H:i" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% else %}<p class="muted">暂无提交。</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
{% extends "web/base.html" %}
|
||||||
|
{% load extras %}
|
||||||
|
{% block title %}{{ submission.name }} · OJplatform{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ submission.name }}</h1>
|
||||||
|
<p class="muted">作业要求: <a href="/requirement/{{ submission.requirement.pk }}">{{ submission.requirement.title }}</a>
|
||||||
|
· 提交人: {{ submission.submitted_by.username|default:"-" }}
|
||||||
|
· 状态: <span class="tag {{ submission.status }}">{{ submission.get_status_display }}</span></p>
|
||||||
|
|
||||||
|
{% if submission.report %}
|
||||||
|
<div class="card">
|
||||||
|
<h2>检测总览</h2>
|
||||||
|
{% with o=submission.report.overall %}
|
||||||
|
{% if o %}
|
||||||
|
<p>通过 {{ o.passed_checks }} / {{ o.decided_checks }} 项已判定检查</p>
|
||||||
|
<table>
|
||||||
|
<tr><th>检查项</th><th>结果</th><th>说明</th></tr>
|
||||||
|
{% for c in o.checks %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ c.name }}</td>
|
||||||
|
<td>
|
||||||
|
{% if c.pass == True %}<span class="tag completed">通过</span>
|
||||||
|
{% elif c.pass == False %}<span class="tag failed">未通过</span>
|
||||||
|
{% else %}<span class="tag pending">未判定</span>{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="muted">{{ c.note|truncatechars:120 }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>稳定度(压力测试)</h2>
|
||||||
|
{% with s=submission.report.stability %}
|
||||||
|
{% if s.enabled %}
|
||||||
|
<p>要求: {{ s.requirements }}</p>
|
||||||
|
<table>
|
||||||
|
<tr><th>基准错误率</th><th>加压崩溃率</th><th>总请求数</th><th>判定</th></tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ s.base_error_rate|default_if_none:"—" }}</td>
|
||||||
|
<td>{{ s.rampup_crash_rate|default_if_none:"—" }}</td>
|
||||||
|
<td>{{ s.total_requests|default_if_none:"—" }}</td>
|
||||||
|
<td>
|
||||||
|
{% if s.pass == True %}<span class="tag completed">达标</span>
|
||||||
|
{% elif s.pass == False %}<span class="tag failed">未达标</span>
|
||||||
|
{% else %}<span class="tag pending">未判定: {{ s.reason|default:"" }}</span>{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{% else %}<p class="muted">本次未启用稳定度检测。</p>{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>AI 检测</h2>
|
||||||
|
{% with a=submission.report.ai_inspection %}
|
||||||
|
{% if a %}
|
||||||
|
<p>{{ a.summary }}</p>
|
||||||
|
{% if a.issues %}
|
||||||
|
<table>
|
||||||
|
<tr><th>检查点</th><th>状态</th><th>说明</th></tr>
|
||||||
|
{% for i in a.issues %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ i.point }}</td>
|
||||||
|
<td>
|
||||||
|
{% if i.status == "pass" %}<span class="tag completed">pass</span>
|
||||||
|
{% elif i.status == "fail" %}<span class="tag failed">fail</span>
|
||||||
|
{% else %}<span class="tag pending">warn</span>{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="muted">{{ i.detail }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}<p class="muted">无 AI 检测结果。</p>{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>完整报告 (JSON)</h2>
|
||||||
|
<pre>{{ submission.report|json_filter }}</pre>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="card">
|
||||||
|
<p class="muted">报告尚未生成(当前状态: {{ submission.get_status_display }})。<a href="javascript:location.reload()">刷新</a></p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from django import template
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def json_filter(value):
|
||||||
|
return json.dumps(value, ensure_ascii=False, indent=2)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
from django.contrib.auth import views as auth_views
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', views.dashboard, name='dashboard'),
|
||||||
|
path('login/', auth_views.LoginView.as_view(), name='login'),
|
||||||
|
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
|
||||||
|
path('create/', views.create_requirement, name='create-requirement'),
|
||||||
|
path('requirement/<int:pk>/', views.requirement_detail, name='requirement-detail'),
|
||||||
|
path('requirement/<int:pk>/upload/', views.upload_submission, name='upload-submission'),
|
||||||
|
path('submission/<int:pk>/', views.submission_detail, name='submission-detail'),
|
||||||
|
path('password_change/', auth_views.PasswordChangeView.as_view(), name='password_change'),
|
||||||
|
]
|
||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.models import Count
|
||||||
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
|
||||||
|
from requirements.llm import analyze_requirements, _default_spec
|
||||||
|
from requirements.models import Requirement, Submission
|
||||||
|
from requirements.tasks import run_submission
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def dashboard(request):
|
||||||
|
requirements = Requirement.objects.annotate(sub_count=Count("submissions")).order_by("-created_at")
|
||||||
|
recent_submissions = Submission.objects.select_related("requirement").order_by("-created_at")[:15]
|
||||||
|
return render(request, "web/dashboard.html", {
|
||||||
|
"requirements": requirements,
|
||||||
|
"recent_submissions": recent_submissions,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def requirement_detail(request, pk):
|
||||||
|
requirement = get_object_or_404(Requirement, pk=pk)
|
||||||
|
submissions = requirement.submissions.all()
|
||||||
|
return render(request, "web/requirement.html", {
|
||||||
|
"requirement": requirement,
|
||||||
|
"submissions": submissions,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def submission_detail(request, pk):
|
||||||
|
submission = get_object_or_404(Submission, pk=pk)
|
||||||
|
return render(request, "web/submission.html", {
|
||||||
|
"submission": submission,
|
||||||
|
"report_json": json.dumps(submission.report or {}, ensure_ascii=False, indent=2),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def create_requirement(request):
|
||||||
|
from django.conf import settings
|
||||||
|
if request.method != "POST":
|
||||||
|
return render(request, "web/create_requirement.html", {
|
||||||
|
"llm_base_url": settings.LLM_BASE_URL,
|
||||||
|
"llm_model": settings.LLM_MODEL,
|
||||||
|
})
|
||||||
|
title = (request.POST.get("title") or "").strip()
|
||||||
|
content = (request.POST.get("content") or "").strip()
|
||||||
|
if not title or not content:
|
||||||
|
messages.error(request, "标题和内容不能为空")
|
||||||
|
return redirect("create-requirement")
|
||||||
|
llm_mode = request.POST.get("llm_mode", "default")
|
||||||
|
override = {}
|
||||||
|
if llm_mode == "custom":
|
||||||
|
override = {
|
||||||
|
"mode": "custom",
|
||||||
|
"base_url": (request.POST.get("llm_base_url") or "").strip(),
|
||||||
|
"api_key": (request.POST.get("llm_api_key") or "").strip(),
|
||||||
|
"model": (request.POST.get("llm_model") or "").strip(),
|
||||||
|
}
|
||||||
|
if not override["base_url"] or not override["model"]:
|
||||||
|
messages.error(request, "custom 模式必须填写 LLM 地址和模型名")
|
||||||
|
return redirect("create-requirement")
|
||||||
|
try:
|
||||||
|
spec = analyze_requirements(content, override)
|
||||||
|
except Exception:
|
||||||
|
spec = _default_spec()
|
||||||
|
with transaction.atomic():
|
||||||
|
requirement = Requirement.objects.create(
|
||||||
|
title=title, content=content, spec=spec, llm_config=override,
|
||||||
|
)
|
||||||
|
messages.success(request, f"已创建作业要求「{title}」,AI 分析完成")
|
||||||
|
return redirect("requirement-detail", pk=requirement.pk)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def upload_submission(request, pk):
|
||||||
|
requirement = get_object_or_404(Requirement, pk=pk)
|
||||||
|
if request.method != "POST":
|
||||||
|
return redirect("requirement-detail", pk=pk)
|
||||||
|
file = request.FILES.get("file")
|
||||||
|
if not file:
|
||||||
|
messages.error(request, "请选择要上传的项目包(tar/tar.gz/zip)")
|
||||||
|
return redirect("requirement-detail", pk=pk)
|
||||||
|
name = (request.POST.get("name") or "").strip() or file.name
|
||||||
|
with transaction.atomic():
|
||||||
|
submission = Submission.objects.create(
|
||||||
|
requirement=requirement, name=name, file=file, submitted_by=request.user,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
run_submission(submission.pk)
|
||||||
|
except Exception:
|
||||||
|
submission.status = "failed"
|
||||||
|
submission.save(update_fields=["status"])
|
||||||
|
messages.success(request, f"已提交「{name}」,检测完成")
|
||||||
|
return redirect("submission-detail", pk=submission.pk)
|
||||||
Reference in New Issue
Block a user