aifeilong 13b1ca2b9b Namespace API routes by app and flatten package layout.
Move Django apps out of apps/ and mount them under auth/, common/, content/, and cms/ so endpoint paths map directly to the owning app.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-15 08:42:37 +08:00

495 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import re
from datetime import date
from django.db import transaction
from django.db.models import Max
from rest_framework import serializers
from .models import (
Article,
ArticleComment,
ArticleType,
Column,
Magazine,
MagazineCategory,
Organize,
OrganizeCategory,
Professor,
RuleBand,
Specialty,
StarRule,
Subject,
WorkGroup,
WorkGroupMembership,
WorkGroupTerm,
)
class ColumnSerializer(serializers.ModelSerializer):
class Meta:
model = Column
fields = [
"id",
"uuid",
"name",
"description",
"note",
"template_id",
"position",
"parent",
"sort",
"is_disabled",
"path",
"created_at",
"updated_at",
]
class TaxonomySerializer(serializers.ModelSerializer):
class Meta:
fields = [
"id",
"serial_num",
"name",
"parent",
"sort",
"is_disabled",
"created_at",
"updated_at",
]
class SubjectSerializer(TaxonomySerializer):
article_count = serializers.IntegerField(read_only=True, required=False)
class Meta(TaxonomySerializer.Meta):
model = Subject
fields = TaxonomySerializer.Meta.fields + ["article_count"]
class OrganizeCategorySerializer(TaxonomySerializer):
class Meta(TaxonomySerializer.Meta):
model = OrganizeCategory
class MagazineCategorySerializer(TaxonomySerializer):
class Meta(TaxonomySerializer.Meta):
model = MagazineCategory
class ArticleTypeSerializer(serializers.ModelSerializer):
article_count = serializers.IntegerField(read_only=True, required=False)
class Meta:
model = ArticleType
fields = [
"id",
"serial_num",
"code",
"name",
"note",
"sort",
"is_disabled",
"article_count",
"created_at",
"updated_at",
]
def validate_code(self, value):
code = (value or "").strip().upper()
if not code:
raise serializers.ValidationError("请填写字母标识")
if not re.fullmatch(r"[A-Z]{1,8}", code):
raise serializers.ValidationError("字母标识仅支持 1–8 位大写英文字母")
return code
class OrganizeListSerializer(serializers.ModelSerializer):
article_count = serializers.IntegerField(read_only=True, required=False)
avg_score = serializers.DecimalField(
max_digits=10, decimal_places=2, read_only=True, required=False
)
class Meta:
model = Organize
fields = [
"id",
"serial_num",
"name",
"logo",
"description",
"url",
"category",
"flags",
"collect_num",
"like_num",
"share_num",
"rank",
"is_disabled",
"article_count",
"avg_score",
"created_at",
"updated_at",
]
class OrganizeSerializer(OrganizeListSerializer):
pass
class MagazineSerializer(serializers.ModelSerializer):
article_count = serializers.IntegerField(read_only=True, required=False)
avg_score = serializers.DecimalField(
max_digits=10, decimal_places=2, read_only=True, required=False
)
class Meta:
model = Magazine
fields = [
"id",
"serial_num",
"name",
"logo",
"category",
"subjects",
"is_disabled",
"article_count",
"avg_score",
"created_at",
"updated_at",
]
class SpecialtySerializer(serializers.ModelSerializer):
class Meta:
model = Specialty
fields = [
"id",
"name",
"type",
"description",
"logo",
"content",
"position",
"is_disabled",
"created_at",
"updated_at",
]
class WorkGroupSerializer(serializers.ModelSerializer):
class Meta:
model = WorkGroup
fields = [
"id",
"name",
"description",
"parent",
"sort",
"is_disabled",
"created_at",
"updated_at",
]
class WorkGroupTermSerializer(serializers.ModelSerializer):
workgroup_name = serializers.CharField(source="workgroup.name", read_only=True)
class Meta:
model = WorkGroupTerm
fields = [
"id",
"workgroup",
"workgroup_name",
"parent",
"name",
"start_year",
"end_year",
"is_current",
"sort",
"is_disabled",
"created_at",
"updated_at",
]
class WorkGroupMembershipSerializer(serializers.ModelSerializer):
term_detail = WorkGroupTermSerializer(source="term", read_only=True)
class Meta:
model = WorkGroupMembership
fields = ["id", "professor", "term", "term_detail", "role"]
class ProfessorSerializer(serializers.ModelSerializer):
memberships = WorkGroupMembershipSerializer(many=True, read_only=True)
terms = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = Professor
fields = [
"id",
"serial_num",
"name",
"type",
"role",
"job",
"description",
"email",
"addr",
"logo",
"phone",
"position",
"terms",
"memberships",
"is_disabled",
"created_at",
"updated_at",
]
class RuleBandSerializer(serializers.ModelSerializer):
class Meta:
model = RuleBand
fields = [
"id",
"rule",
"star_low",
"star_high",
"star",
"is_disabled",
"created_at",
"updated_at",
]
def validate_star(self, value):
if value is None:
return value
num = float(value)
if num < 0.5 or num > 5:
raise serializers.ValidationError("星级须在 0.5~5 之间")
# 仅允许 0.5 步进(如 1、1.5、2…)
if abs(num * 2 - round(num * 2)) > 1e-9:
raise serializers.ValidationError("星级须为 0.5 的倍数(如 1、1.5、2)")
return value
def validate_star_low(self, value):
if value is not None and float(value) < 0:
raise serializers.ValidationError("最低分不能为负数")
return value
def validate_star_high(self, value):
if value is not None and float(value) < 0:
raise serializers.ValidationError("最高分不能为负数")
return value
def validate(self, attrs):
attrs = super().validate(attrs)
instance = getattr(self, "instance", None)
low = attrs.get("star_low", getattr(instance, "star_low", None))
high = attrs.get("star_high", getattr(instance, "star_high", None))
star = attrs.get("star", getattr(instance, "star", None))
rule = attrs.get("rule", getattr(instance, "rule", None))
is_disabled = attrs.get(
"is_disabled",
getattr(instance, "is_disabled", False) if instance else False,
)
if low is not None and high is not None and float(low) > float(high):
raise serializers.ValidationError({"star_high": "最高分不能低于最低分"})
if rule is None or is_disabled:
return attrs
rule_id = getattr(rule, "pk", rule)
siblings = RuleBand.objects.filter(rule_id=rule_id, is_disabled=False)
if instance is not None:
siblings = siblings.exclude(pk=instance.pk)
if star is not None:
if siblings.filter(star=star).exists():
raise serializers.ValidationError(
{"star": f"该规则下已存在星级 {star} 的分档"}
)
if low is not None and high is not None:
low_f, high_f = float(low), float(high)
for other in siblings:
o_low, o_high = float(other.star_low), float(other.star_high)
# 闭区间重叠:[a,b] 与 [c,d] 当 a<=d 且 c<=b
if low_f <= o_high and o_low <= high_f:
raise serializers.ValidationError(
{
"star_low": (
f"分数区间与已有分档 {other.star_low}~{other.star_high} "
f"({other.star} 星)重叠"
)
}
)
return attrs
class StarRuleSerializer(serializers.ModelSerializer):
bands = RuleBandSerializer(many=True, read_only=True)
class Meta:
model = StarRule
fields = [
"id",
"name",
"year",
"is_disabled",
"bands",
"created_at",
"updated_at",
]
def validate_year(self, value):
if value is None:
return value
if value < 1900 or value > 2100:
raise serializers.ValidationError("请输入合理年份(1900~2100)")
return value
def validate(self, attrs):
attrs = super().validate(attrs)
instance = getattr(self, "instance", None)
year = attrs.get("year", getattr(instance, "year", None))
is_disabled = attrs.get(
"is_disabled",
getattr(instance, "is_disabled", False) if instance else False,
)
if year is None or is_disabled:
return attrs
qs = StarRule.objects.filter(year=year, is_disabled=False)
if instance is not None:
qs = qs.exclude(pk=instance.pk)
if qs.exists():
raise serializers.ValidationError(
{"year": f"已存在启用中的 {year} 年评分规则,请先禁用或改用其他年份"}
)
return attrs
class ArticleListSerializer(serializers.ModelSerializer):
organizes = OrganizeListSerializer(many=True, read_only=True)
magazines = MagazineSerializer(many=True, read_only=True)
subjects = SubjectSerializer(many=True, read_only=True)
type_name = serializers.CharField(source="type.name", read_only=True)
class Meta:
model = Article
fields = [
"id",
"serial_number",
"title",
"type",
"type_name",
"year",
"score",
"url",
"flags",
"introduction",
"push_date",
"read_num",
"collect_num",
"like_num",
"share_num",
"rank",
"is_disabled",
"organizes",
"magazines",
"subjects",
"created_at",
"updated_at",
]
class ArticleSerializer(ArticleListSerializer):
class Meta(ArticleListSerializer.Meta):
fields = ArticleListSerializer.Meta.fields + ["content"]
def _next_article_serial(article_type, year=None):
"""
生成文献编号:{类型字母标识}{年份}{5位录入序号}
例:指南(G) + 2024 + 第 1 条 → G202400001
序号按「同类型字母 + 同年」的录入顺序递增。
"""
code = (getattr(article_type, "code", None) or "").strip().upper()
if not code:
raise serializers.ValidationError({"type": "该文献类型未配置字母标识"})
year = year or date.today().year
prefix = f"{code}{year}"
latest = (
Article.objects.filter(serial_number__startswith=prefix)
.aggregate(Max("serial_number"))
.get("serial_number__max")
)
seq = 1
if latest and latest.startswith(prefix):
tail = latest[len(prefix) :]
if tail.isdigit():
seq = int(tail) + 1
if seq > 99999:
raise serializers.ValidationError(
{"serial_number": f"{prefix} 当年序号已满,请联系管理员"}
)
return f"{prefix}{seq:05d}"
class ArticleWriteSerializer(serializers.ModelSerializer):
serial_number = serializers.CharField(read_only=True)
class Meta:
model = Article
fields = [
"serial_number",
"title",
"type",
"year",
"score",
"url",
"flags",
"content",
"introduction",
"push_date",
"rank",
"is_disabled",
"organizes",
"magazines",
"subjects",
]
@transaction.atomic
def create(self, validated_data):
article_type = validated_data["type"]
validated_data["serial_number"] = _next_article_serial(
article_type, validated_data.get("year")
)
return super().create(validated_data)
class ArticleCommentSerializer(serializers.ModelSerializer):
member_name = serializers.CharField(source="member.name", read_only=True)
member_phone = serializers.CharField(source="member.phone", read_only=True)
member_organize = serializers.CharField(source="member.organize", read_only=True)
article_title = serializers.CharField(source="article.title", read_only=True)
class Meta:
model = ArticleComment
fields = [
"id",
"article",
"article_title",
"member",
"member_name",
"member_phone",
"member_organize",
"star",
"content",
"review_status",
"created_at",
"updated_at",
]