v 0.3 Preview: Update admin
CI / test (push) Canceled after 4m2s

This commit is contained in:
2026-08-08 21:47:29 +08:00
parent 1f98f7152d
commit 43e425edbe
24 changed files with 1408 additions and 31 deletions
+189
View File
@@ -0,0 +1,189 @@
from django.contrib.admin import AdminSite
from django.utils import timezone
class HulumathAdminSite(AdminSite):
site_header = "葫芦数学运营后台"
site_title = "葫芦数学后台"
index_title = "运营总览"
site_url = "/"
index_template = "admin/index.html"
login_template = "admin/login.html"
APP_ORDER = {
"accounts": 10,
"math_life": 20,
"contest": 30,
"content": 40,
"latex_lab": 50,
"progression": 60,
"engagement": 70,
"auth": 90,
}
MODEL_LABELS = {
"accounts.user": "用户账号",
"accounts.invitecode": "邀请码",
"accounts.invitecodeusage": "邀请码使用记录",
"accounts.usersession": "登录设备",
"accounts.visitormigration": "游客数据迁移",
"accounts.auditlog": "管理员审计日志",
"math_life.mathidentity": "数学人格",
"math_life.mathbtiassessment": "MathBTI 测评版本",
"math_life.mathbtiresult": "MathBTI 用户结果",
"math_life.character": "剧情人物",
"math_life.story": "数学人生",
"math_life.storyversion": "人生版本",
"math_life.storyrun": "用户人生存档",
"math_life.storychoice": "用户选择记录",
"math_life.userrelationship": "人物关系",
"math_life.skillpackage": "人物 Skill 包",
"contest.question": "题目",
"contest.questionversion": "题目版本",
"contest.contest": "比赛",
"contest.contestattempt": "答题记录",
"contest.contestanswer": "逐题答案",
"contest.realtimematch": "实时对局",
"contest.ratinghistory": "Rating 变化",
"contest.leaderboardsnapshot": "排行榜快照",
"contest.cheatflag": "异常成绩",
"content.contentitem": "视频与内容",
"content.contentinteraction": "内容互动",
"content.videoprogress": "视频观看进度",
"latex_lab.formuladocument": "用户公式文档",
"latex_lab.formularevision": "公式版本",
"latex_lab.latexcourse": "LaTeX 课程",
"latex_lab.latexexercise": "LaTeX 练习",
"latex_lab.latexattempt": "练习记录",
"latex_lab.handwritingrecognitionjob": "手写识别任务",
"progression.userpet": "数学精灵",
"progression.userability": "五维能力",
"progression.card": "数学人物卡",
"progression.usercard": "用户卡册",
"progression.rewardtransaction": "奖励流水",
"auth.group": "后台角色组",
}
MODEL_ORDER = {
label: index
for index, label in enumerate(
(
"用户账号",
"邀请码",
"邀请码使用记录",
"登录设备",
"游客数据迁移",
"管理员审计日志",
"数学人生",
"人生版本",
"剧情人物",
"人物 Skill 包",
"数学人格",
"MathBTI 测评版本",
"用户人生存档",
"人物关系",
"比赛",
"题目",
"题目版本",
"实时对局",
"答题记录",
"异常成绩",
"视频与内容",
"视频观看进度",
"内容互动",
"LaTeX 课程",
"LaTeX 练习",
"用户公式文档",
"数学精灵",
"五维能力",
"数学人物卡",
"用户卡册",
)
)
}
def get_app_list(self, request, app_label=None):
app_list = super().get_app_list(request, app_label)
for app in app_list:
app["models"] = list(app["models"])
for model in app["models"]:
object_name = model["object_name"].lower()
key = f"{app['app_label']}.{object_name}"
model["name"] = self.MODEL_LABELS.get(key, model["name"])
app["models"].sort(
key=lambda model: (
self.MODEL_ORDER.get(model["name"], 999),
model["name"],
)
)
app_list.sort(
key=lambda app: (
self.APP_ORDER.get(app["app_label"], 999),
app["name"],
)
)
return app_list
def each_context(self, request):
context = super().each_context(request)
context["admin_environment"] = "生产运营"
return context
def index(self, request, extra_context=None):
from accounts.models import InviteCode, User
from content.models import ContentItem
from contest.models import CheatFlag, ContestAttempt, RealtimeMatch
from math_life.models import StoryRun
today = timezone.localdate()
extra_context = {
**(extra_context or {}),
"dashboard_metrics": [
{
"label": "总用户",
"value": User.objects.filter(is_active=True).count(),
"hint": f"今日新增 {User.objects.filter(date_joined__date=today).count()}",
"tone": "green",
},
{
"label": "进行中的人生",
"value": StoryRun.objects.filter(status=StoryRun.Status.ACTIVE).count(),
"hint": "用户当前存档",
"tone": "blue",
},
{
"label": "今日比赛完成",
"value": ContestAttempt.objects.filter(
submitted_at__date=today
).count(),
"hint": "含实时与异步",
"tone": "orange",
},
{
"label": "已发布内容",
"value": ContentItem.objects.filter(is_published=True).count(),
"hint": "视频、知识与人物",
"tone": "purple",
},
],
"dashboard_alerts": [
{
"label": "待处理异常成绩",
"value": CheatFlag.objects.filter(
status=CheatFlag.Status.OPEN
).count(),
"url": "/admin/contest/cheatflag/?status__exact=open",
},
{
"label": "等待中的实时对局",
"value": RealtimeMatch.objects.filter(
status=RealtimeMatch.Status.WAITING
).count(),
"url": "/admin/contest/realtimematch/?status__exact=waiting",
},
{
"label": "可用邀请码",
"value": InviteCode.objects.filter(is_active=True).count(),
"url": "/admin/accounts/invitecode/",
},
],
}
return super().index(request, extra_context)
+22 -2
View File
@@ -1,6 +1,26 @@
from django.apps import AppConfig
from django.contrib.admin import apps as admin_apps
class CommonConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'common'
default = True
default_auto_field = "django.db.models.BigAutoField"
name = "common"
verbose_name = "系统"
def ready(self):
from django.contrib import admin
from .admin_site import HulumathAdminSite
for model in admin.site._registry:
key = f"{model._meta.app_label}.{model._meta.model_name}"
label = HulumathAdminSite.MODEL_LABELS.get(key)
if label:
model._meta.verbose_name = label
model._meta.verbose_name_plural = label
class HulumathAdminConfig(admin_apps.AdminConfig):
default = False
default_site = "common.admin_site.HulumathAdminSite"
+71
View File
@@ -0,0 +1,71 @@
import pytest
from django.urls import reverse
from accounts.models import InviteCode, User
from content.models import ContentItem
@pytest.mark.django_db
def test_admin_login_使用自定义运营登录页(client):
response = client.get(reverse("admin:login"))
assert response.status_code == 200
content = response.content.decode()
assert "管理数学人生宇宙" in content
assert "进入运营后台" in content
@pytest.mark.django_db
def test_admin_index_普通用户不能访问(client):
user = User.objects.create_user(
username="normal_user",
password="StrongPass_2026",
nickname="普通用户",
)
client.force_login(user)
response = client.get(reverse("admin:index"))
assert response.status_code == 302
assert reverse("admin:login") in response.url
@pytest.mark.django_db
def test_admin_index_展示运营指标与中文模块(client):
admin_user = User.objects.create_superuser(
username="dashboard_admin",
password="StrongPass_2026",
nickname="后台管理员",
)
InviteCode.objects.create(code="ADMIN-TEST", max_uses=10)
ContentItem.objects.create(
slug="admin-content",
title="后台测试内容",
kind=ContentItem.Kind.KNOWLEDGE,
is_published=True,
)
client.force_login(admin_user)
response = client.get(reverse("admin:index"))
assert response.status_code == 200
content = response.content.decode()
assert "今天从哪里开始?" in content
assert "总用户" in content
assert "用户与权限" in content
assert "用户账号" in content
assert "数学人生与 MathBTI" in content
assert "比赛运营" in content
assert "视频与探索内容" in content
assert "视频与内容" in content
assert "生成邀请码" in content
@pytest.mark.django_db
def test_home_包含按员工身份控制的后台入口(client):
response = client.get("/")
assert response.status_code == 200
content = response.content.decode()
assert 'id="admin-entry"' in content
assert 'href="/admin/"' in content