feat(problem): 新增标签管理接口
列表带题目数,重命名撞名时自动合并并返回受影响题数,删除后清标签缓存。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -109,6 +109,19 @@ class TagSerializer(serializers.ModelSerializer):
|
|||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class TagAdminSerializer(serializers.ModelSerializer):
|
||||||
|
problem_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ProblemTag
|
||||||
|
fields = ["id", "name", "problem_count"]
|
||||||
|
|
||||||
|
|
||||||
|
class EditTagSerializer(serializers.Serializer):
|
||||||
|
id = serializers.IntegerField()
|
||||||
|
name = serializers.CharField(max_length=32)
|
||||||
|
|
||||||
|
|
||||||
class BaseProblemSerializer(serializers.ModelSerializer):
|
class BaseProblemSerializer(serializers.ModelSerializer):
|
||||||
tags = serializers.SlugRelatedField(many=True, slug_field="name", read_only=True)
|
tags = serializers.SlugRelatedField(many=True, slug_field="name", read_only=True)
|
||||||
created_by = UsernameSerializer()
|
created_by = UsernameSerializer()
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from ..views.admin import (
|
|||||||
SQLTestCasePreviewAPI,
|
SQLTestCasePreviewAPI,
|
||||||
SQLTestCaseScriptsAPI,
|
SQLTestCaseScriptsAPI,
|
||||||
StuckProblemsAPI,
|
StuckProblemsAPI,
|
||||||
|
TagAdminAPI,
|
||||||
TestCaseAPI,
|
TestCaseAPI,
|
||||||
TopACTrendAPI,
|
TopACTrendAPI,
|
||||||
)
|
)
|
||||||
@@ -25,6 +26,7 @@ urlpatterns = [
|
|||||||
path("problem/stuck", StuckProblemsAPI.as_view()),
|
path("problem/stuck", StuckProblemsAPI.as_view()),
|
||||||
path("problem/top_ac_trend", TopACTrendAPI.as_view()),
|
path("problem/top_ac_trend", TopACTrendAPI.as_view()),
|
||||||
path("problem/flowchart", ProblemFlowchartAIGen.as_view()),
|
path("problem/flowchart", ProblemFlowchartAIGen.as_view()),
|
||||||
|
path("problem/tag", TagAdminAPI.as_view()),
|
||||||
path("contest/problem", ContestProblemAPI.as_view()),
|
path("contest/problem", ContestProblemAPI.as_view()),
|
||||||
path("contest_problem/make_public", MakeContestProblemPublicAPIView.as_view()),
|
path("contest_problem/make_public", MakeContestProblemPublicAPIView.as_view()),
|
||||||
path("contest/add_problem_from_public", AddContestProblemAPI.as_view()),
|
path("contest/add_problem_from_public", AddContestProblemAPI.as_view()),
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from utils.api import APIError, APIView, CSRFExemptAPIView, validate_serializer
|
|||||||
from utils.openai import get_ai_client
|
from utils.openai import get_ai_client
|
||||||
from utils.shortcuts import natural_sort_key, rand_str
|
from utils.shortcuts import natural_sort_key, rand_str
|
||||||
|
|
||||||
from ..models import Problem
|
from ..models import Problem, ProblemTag
|
||||||
from ..serializers import (
|
from ..serializers import (
|
||||||
AddContestProblemSerializer,
|
AddContestProblemSerializer,
|
||||||
ContestProblemMakePublicSerializer,
|
ContestProblemMakePublicSerializer,
|
||||||
@@ -27,12 +27,14 @@ from ..serializers import (
|
|||||||
CreateProblemSerializer,
|
CreateProblemSerializer,
|
||||||
EditContestProblemSerializer,
|
EditContestProblemSerializer,
|
||||||
EditProblemSerializer,
|
EditProblemSerializer,
|
||||||
|
EditTagSerializer,
|
||||||
ProblemAdminListSerializer,
|
ProblemAdminListSerializer,
|
||||||
ProblemAdminSerializer,
|
ProblemAdminSerializer,
|
||||||
SQLTestCasePreviewSerializer,
|
SQLTestCasePreviewSerializer,
|
||||||
|
TagAdminSerializer,
|
||||||
TestCaseUploadForm,
|
TestCaseUploadForm,
|
||||||
)
|
)
|
||||||
from ..services import resolve_tags
|
from ..services import clear_tag_cache, resolve_tags
|
||||||
from ..utils import generate_sql_display
|
from ..utils import generate_sql_display
|
||||||
|
|
||||||
|
|
||||||
@@ -497,6 +499,61 @@ class AddContestProblemAPI(APIView):
|
|||||||
return self.success()
|
return self.success()
|
||||||
|
|
||||||
|
|
||||||
|
class TagAdminAPI(APIView):
|
||||||
|
@problem_permission_required
|
||||||
|
def get(self, request):
|
||||||
|
tags = ProblemTag.objects.annotate(problem_count=Count("problem"))
|
||||||
|
keyword = request.GET.get("keyword", "").strip()
|
||||||
|
if keyword:
|
||||||
|
tags = tags.filter(name__icontains=keyword)
|
||||||
|
tags = tags.order_by("-problem_count", "name")
|
||||||
|
return self.success(TagAdminSerializer(tags, many=True).data)
|
||||||
|
|
||||||
|
@problem_permission_required
|
||||||
|
@validate_serializer(EditTagSerializer)
|
||||||
|
def put(self, request):
|
||||||
|
data = request.data
|
||||||
|
try:
|
||||||
|
tag = ProblemTag.objects.get(id=data["id"])
|
||||||
|
except ProblemTag.DoesNotExist:
|
||||||
|
return self.error("标签不存在,请刷新后重试")
|
||||||
|
|
||||||
|
name = data["name"].strip()
|
||||||
|
if not name:
|
||||||
|
return self.error("标签名不能为空")
|
||||||
|
|
||||||
|
target = ProblemTag.objects.filter(name__iexact=name).exclude(id=tag.id).first()
|
||||||
|
if target is None:
|
||||||
|
tag.name = name
|
||||||
|
tag.save()
|
||||||
|
clear_tag_cache()
|
||||||
|
return self.success({"merged": False, "id": tag.id, "name": tag.name, "affected_count": 0})
|
||||||
|
|
||||||
|
# 改名撞上已有标签,视为合并:题目关系转移过去,原标签删除
|
||||||
|
affected_count = 0
|
||||||
|
for problem in Problem.objects.filter(tags=tag):
|
||||||
|
problem.tags.add(target)
|
||||||
|
problem.tags.remove(tag)
|
||||||
|
affected_count += 1
|
||||||
|
tag.delete()
|
||||||
|
clear_tag_cache()
|
||||||
|
return self.success({"merged": True, "id": target.id, "name": target.name, "affected_count": affected_count})
|
||||||
|
|
||||||
|
@problem_permission_required
|
||||||
|
def delete(self, request):
|
||||||
|
tag_id = request.GET.get("id")
|
||||||
|
if not tag_id:
|
||||||
|
return self.error("Invalid parameter, id is required")
|
||||||
|
try:
|
||||||
|
tag = ProblemTag.objects.get(id=tag_id)
|
||||||
|
except ProblemTag.DoesNotExist:
|
||||||
|
return self.error("标签不存在,请刷新后重试")
|
||||||
|
# 删除标签行的同时,Django 会级联清掉 problem_tags 中间表里的关系
|
||||||
|
tag.delete()
|
||||||
|
clear_tag_cache()
|
||||||
|
return self.success()
|
||||||
|
|
||||||
|
|
||||||
class ProblemVisibleAPI(APIView):
|
class ProblemVisibleAPI(APIView):
|
||||||
@problem_permission_required
|
@problem_permission_required
|
||||||
def put(self, request):
|
def put(self, request):
|
||||||
|
|||||||
Reference in New Issue
Block a user