From 07aaff69f2ce09eaa3eb5ebe5ec415a00226a264 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Wed, 22 Oct 2025 18:47:40 +0800 Subject: [PATCH] add problemset --- oj/dev_settings.py | 8 +- oj/settings.py | 1 + oj/urls.py | 2 + problemset/__init__.py | 0 problemset/apps.py | 9 + problemset/migrations/0001_initial.py | 115 ++++++++ problemset/migrations/__init__.py | 0 problemset/models.py | 190 +++++++++++++ problemset/serializers.py | 200 ++++++++++++++ problemset/signals.py | 73 +++++ problemset/urls/__init__.py | 0 problemset/urls/admin.py | 64 +++++ problemset/urls/oj.py | 54 ++++ problemset/views/__init__.py | 0 problemset/views/admin.py | 318 ++++++++++++++++++++++ problemset/views/oj.py | 374 ++++++++++++++++++++++++++ 16 files changed, 1404 insertions(+), 4 deletions(-) create mode 100644 problemset/__init__.py create mode 100644 problemset/apps.py create mode 100644 problemset/migrations/0001_initial.py create mode 100644 problemset/migrations/__init__.py create mode 100644 problemset/models.py create mode 100644 problemset/serializers.py create mode 100644 problemset/signals.py create mode 100644 problemset/urls/__init__.py create mode 100644 problemset/urls/admin.py create mode 100644 problemset/urls/oj.py create mode 100644 problemset/views/__init__.py create mode 100644 problemset/views/admin.py create mode 100644 problemset/views/oj.py diff --git a/oj/dev_settings.py b/oj/dev_settings.py index 8db2178..067efe7 100644 --- a/oj/dev_settings.py +++ b/oj/dev_settings.py @@ -6,8 +6,8 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", - "HOST": "150.158.29.156", - "PORT": "5445", + "HOST": "10.13.114.114", + "PORT": "5433", "NAME": "onlinejudge", "USER": "onlinejudge", "PASSWORD": "onlinejudge", @@ -15,8 +15,8 @@ DATABASES = { } REDIS_CONF = { - "host": "150.158.29.156", - "port": 5446, + "host": "10.13.114.114", + "port": 6379, } diff --git a/oj/settings.py b/oj/settings.py index a857dca..bde38da 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -59,6 +59,7 @@ LOCAL_APPS = [ "tutorial", "ai", "flowchart", + "problemset", ] INSTALLED_APPS = VENDOR_APPS + LOCAL_APPS diff --git a/oj/urls.py b/oj/urls.py index 5109374..ec47834 100644 --- a/oj/urls.py +++ b/oj/urls.py @@ -21,4 +21,6 @@ urlpatterns = [ path("api/admin/", include("tutorial.urls.admin")), path("api/", include("ai.urls.oj")), path("api/", include("flowchart.urls.oj")), + path("api/", include("problemset.urls.oj")), + path("api/admin/", include("problemset.urls.admin")), ] diff --git a/problemset/__init__.py b/problemset/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/problemset/apps.py b/problemset/apps.py new file mode 100644 index 0000000..79cd309 --- /dev/null +++ b/problemset/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class ProblemsetConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'problemset' + + def ready(self): + import problemset.signals \ No newline at end of file diff --git a/problemset/migrations/0001_initial.py b/problemset/migrations/0001_initial.py new file mode 100644 index 0000000..673ae17 --- /dev/null +++ b/problemset/migrations/0001_initial.py @@ -0,0 +1,115 @@ +# Generated by Django 5.2.3 on 2025-10-22 10:27 + +import django.db.models.deletion +import utils.models +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('problem', '0005_remove_spj_fields'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ProblemSet', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.TextField(verbose_name='题单标题')), + ('description', utils.models.RichTextField(verbose_name='题单描述')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('last_update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('visible', models.BooleanField(default=True, verbose_name='是否可见')), + ('is_public', models.BooleanField(default=True, verbose_name='是否公开')), + ('difficulty', models.TextField(default='Easy', verbose_name='难度等级')), + ('status', models.TextField(default='active', verbose_name='状态')), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='创建者')), + ], + options={ + 'verbose_name': '题单', + 'verbose_name_plural': '题单', + 'db_table': 'problemset', + 'ordering': ('-create_time',), + }, + ), + migrations.CreateModel( + name='ProblemSetBadge', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.TextField(verbose_name='奖章名称')), + ('description', models.TextField(verbose_name='奖章描述')), + ('icon', models.TextField(verbose_name='奖章图标')), + ('condition_type', models.TextField(verbose_name='获得条件类型')), + ('condition_value', models.IntegerField(default=0, verbose_name='条件值')), + ('level', models.IntegerField(default=1, verbose_name='奖章等级')), + ('problemset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='problemset.problemset', verbose_name='题单')), + ], + options={ + 'verbose_name': '题单奖章', + 'verbose_name_plural': '题单奖章', + 'db_table': 'problemset_badge', + }, + ), + migrations.CreateModel( + name='ProblemSetProblem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.IntegerField(default=0, verbose_name='顺序')), + ('is_required', models.BooleanField(default=True, verbose_name='是否必做')), + ('score', models.IntegerField(default=0, verbose_name='分值')), + ('hint', models.TextField(blank=True, null=True, verbose_name='提示')), + ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='problem.problem', verbose_name='题目')), + ('problemset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='problemset.problemset', verbose_name='题单')), + ], + options={ + 'verbose_name': '题单题目', + 'verbose_name_plural': '题单题目', + 'db_table': 'problemset_problem', + 'ordering': ('order',), + 'unique_together': {('problemset', 'problem')}, + }, + ), + migrations.CreateModel( + name='ProblemSetProgress', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('join_time', models.DateTimeField(auto_now_add=True, verbose_name='加入时间')), + ('complete_time', models.DateTimeField(blank=True, null=True, verbose_name='完成时间')), + ('is_completed', models.BooleanField(default=False, verbose_name='是否完成')), + ('progress_percentage', models.FloatField(default=0.0, verbose_name='完成进度')), + ('completed_problems_count', models.IntegerField(default=0, verbose_name='已完成题目数')), + ('total_problems_count', models.IntegerField(default=0, verbose_name='总题目数')), + ('total_score', models.IntegerField(default=0, verbose_name='总分')), + ('progress_detail', models.JSONField(default=dict, verbose_name='详细进度')), + ('problemset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='problemset.problemset', verbose_name='题单')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')), + ], + options={ + 'verbose_name': '题单进度', + 'verbose_name_plural': '题单进度', + 'db_table': 'problemset_progress', + 'unique_together': {('problemset', 'user')}, + }, + ), + migrations.CreateModel( + name='UserBadge', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('earned_time', models.DateTimeField(auto_now_add=True, verbose_name='获得时间')), + ('is_displayed', models.BooleanField(default=False, verbose_name='是否已展示')), + ('badge', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='problemset.problemsetbadge', verbose_name='奖章')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')), + ], + options={ + 'verbose_name': '用户奖章', + 'verbose_name_plural': '用户奖章', + 'db_table': 'user_badge', + 'unique_together': {('user', 'badge')}, + }, + ), + ] diff --git a/problemset/migrations/__init__.py b/problemset/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/problemset/models.py b/problemset/models.py new file mode 100644 index 0000000..3d74d6a --- /dev/null +++ b/problemset/models.py @@ -0,0 +1,190 @@ +from django.db import models +from django.utils.timezone import now +from account.models import User +from problem.models import Problem +from utils.models import RichTextField, JSONField + + +class ProblemSet(models.Model): + """题单模型""" + + title = models.TextField(verbose_name="题单标题") + description = RichTextField(verbose_name="题单描述") + # 创建者 + created_by = models.ForeignKey( + User, on_delete=models.CASCADE, verbose_name="创建者" + ) + # 创建时间 + create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + # 更新时间 + last_update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间") + # 是否可见 + visible = models.BooleanField(default=True, verbose_name="是否可见") + # 是否公开(所有用户都可以看到) + is_public = models.BooleanField(default=True, verbose_name="是否公开") + # 题单难度等级 + difficulty = models.TextField(default="Easy", verbose_name="难度等级") + # 题单状态 + status = models.TextField( + default="active", verbose_name="状态" + ) # active, archived, draft + + class Meta: + db_table = "problemset" + ordering = ("-create_time",) + verbose_name = "题单" + verbose_name_plural = "题单" + + def __str__(self): + return self.title + + +class ProblemSetProblem(models.Model): + """题单题目关联模型""" + + problemset = models.ForeignKey( + ProblemSet, on_delete=models.CASCADE, verbose_name="题单" + ) + problem = models.ForeignKey(Problem, on_delete=models.CASCADE, verbose_name="题目") + # 在题单中的顺序 + order = models.IntegerField(default=0, verbose_name="顺序") + # 是否为必做题 + is_required = models.BooleanField(default=True, verbose_name="是否必做") + # 题目在题单中的分值 + score = models.IntegerField(default=0, verbose_name="分值") + # 题目提示信息 + hint = models.TextField(null=True, blank=True, verbose_name="提示") + + class Meta: + db_table = "problemset_problem" + unique_together = (("problemset", "problem"),) + ordering = ("order",) + verbose_name = "题单题目" + verbose_name_plural = "题单题目" + + def __str__(self): + return f"{self.problemset.title} - {self.problem.title}" + + +class ProblemSetBadge(models.Model): + """题单奖章模型""" + + problemset = models.ForeignKey( + ProblemSet, on_delete=models.CASCADE, verbose_name="题单" + ) + name = models.TextField(verbose_name="奖章名称") + description = models.TextField(verbose_name="奖章描述") + # 奖章图标路径 + icon = models.TextField(verbose_name="奖章图标") + # 获得条件:完成所有题目、完成指定数量题目、达到指定分数等 + condition_type = models.TextField( + verbose_name="获得条件类型" + ) # all_problems, problem_count, score + condition_value = models.IntegerField(default=0, verbose_name="条件值") + # 奖章等级 + level = models.IntegerField(default=1, verbose_name="奖章等级") + + class Meta: + db_table = "problemset_badge" + verbose_name = "题单奖章" + verbose_name_plural = "题单奖章" + + def __str__(self): + return f"{self.problemset.title} - {self.name}" + + +class ProblemSetProgress(models.Model): + """题单进度模型""" + + problemset = models.ForeignKey( + ProblemSet, on_delete=models.CASCADE, verbose_name="题单" + ) + user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户") + # 加入时间 + join_time = models.DateTimeField(auto_now_add=True, verbose_name="加入时间") + # 完成时间 + complete_time = models.DateTimeField(null=True, blank=True, verbose_name="完成时间") + # 是否完成 + is_completed = models.BooleanField(default=False, verbose_name="是否完成") + # 完成进度百分比 + progress_percentage = models.FloatField(default=0.0, verbose_name="完成进度") + # 已完成的题目数量 + completed_problems_count = models.IntegerField( + default=0, verbose_name="已完成题目数" + ) + # 总题目数量 + total_problems_count = models.IntegerField(default=0, verbose_name="总题目数") + # 获得的总分 + total_score = models.IntegerField(default=0, verbose_name="总分") + # 用户在该题单中的详细进度信息 + # {"problem_id": {"status": "completed", "score": 100, "submit_time": "2024-01-01T00:00:00Z"}} + progress_detail = JSONField(default=dict, verbose_name="详细进度") + + class Meta: + db_table = "problemset_progress" + unique_together = (("problemset", "user"),) + verbose_name = "题单进度" + verbose_name_plural = "题单进度" + + def __str__(self): + return f"{self.user.username} - {self.problemset.title}" + + def update_progress(self): + """更新进度信息""" + # 获取题单中的所有题目 + problemset_problems = ProblemSetProblem.objects.filter( + problemset=self.problemset + ) + self.total_problems_count = problemset_problems.count() + + # 计算已完成题目数 + completed_count = 0 + total_score = 0 + + for psp in problemset_problems: + problem_id = str(psp.problem.id) + if problem_id in self.progress_detail: + problem_progress = self.progress_detail[problem_id] + if problem_progress.get("status") == "completed": + completed_count += 1 + total_score += problem_progress.get("score", 0) + + self.completed_problems_count = completed_count + self.total_score = total_score + + # 计算完成百分比 + if self.total_problems_count > 0: + self.progress_percentage = ( + completed_count / self.total_problems_count + ) * 100 + else: + self.progress_percentage = 0 + + # 检查是否完成 + self.is_completed = completed_count == self.total_problems_count + if self.is_completed and not self.complete_time: + self.complete_time = now() + + self.save() + + +class UserBadge(models.Model): + """用户奖章模型""" + + user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="用户") + badge = models.ForeignKey( + ProblemSetBadge, on_delete=models.CASCADE, verbose_name="奖章" + ) + # 获得时间 + earned_time = models.DateTimeField(auto_now_add=True, verbose_name="获得时间") + # 是否已展示给用户 + is_displayed = models.BooleanField(default=False, verbose_name="是否已展示") + + class Meta: + db_table = "user_badge" + unique_together = (("user", "badge"),) + verbose_name = "用户奖章" + verbose_name_plural = "用户奖章" + + def __str__(self): + return f"{self.user.username} - {self.badge.name}" diff --git a/problemset/serializers.py b/problemset/serializers.py new file mode 100644 index 0000000..422953f --- /dev/null +++ b/problemset/serializers.py @@ -0,0 +1,200 @@ +from utils.api import UsernameSerializer, serializers +from .models import ( + ProblemSet, + ProblemSetProblem, + ProblemSetBadge, + ProblemSetProgress, + UserBadge, +) + + +class ProblemSetSerializer(serializers.ModelSerializer): + """题单序列化器""" + + created_by = UsernameSerializer() + problems_count = serializers.SerializerMethodField() + completed_count = serializers.SerializerMethodField() + + class Meta: + model = ProblemSet + fields = "__all__" + + def get_problems_count(self, obj): + """获取题单中的题目数量""" + return ProblemSetProblem.objects.filter(problemset=obj).count() + + def get_completed_count(self, obj): + """获取当前用户在该题单中完成的题目数量""" + request = self.context.get("request") + if request and request.user.is_authenticated: + try: + progress = ProblemSetProgress.objects.get( + problemset=obj, user=request.user + ) + return progress.completed_problems_count + except ProblemSetProgress.DoesNotExist: + return 0 + return 0 + + +class ProblemSetListSerializer(serializers.ModelSerializer): + """题单列表序列化器""" + + created_by = UsernameSerializer() + problems_count = serializers.SerializerMethodField() + user_progress = serializers.SerializerMethodField() + + class Meta: + model = ProblemSet + fields = [ + "id", + "title", + "description", + "created_by", + "create_time", + "difficulty", + "status", + "problems_count", + "user_progress", + ] + + def get_problems_count(self, obj): + """获取题单中的题目数量""" + return ProblemSetProblem.objects.filter(problemset=obj).count() + + def get_user_progress(self, obj): + """获取当前用户在该题单中的进度""" + request = self.context.get("request") + if request and request.user.is_authenticated: + try: + progress = ProblemSetProgress.objects.get( + problemset=obj, user=request.user + ) + return { + "is_joined": True, + "progress_percentage": progress.progress_percentage, + "completed_count": progress.completed_problems_count, + "total_count": progress.total_problems_count, + "is_completed": progress.is_completed, + } + except ProblemSetProgress.DoesNotExist: + return { + "is_joined": False, + "progress_percentage": 0, + "completed_count": 0, + "total_count": 0, + "is_completed": False, + } + return { + "is_joined": False, + "progress_percentage": 0, + "completed_count": 0, + "total_count": 0, + "is_completed": False, + } + + +class CreateProblemSetSerializer(serializers.Serializer): + """创建题单序列化器""" + + title = serializers.CharField(max_length=200) + description = serializers.CharField() + difficulty = serializers.CharField(default="Easy") + is_public = serializers.BooleanField(default=True) + status = serializers.CharField(default="active") + + +class EditProblemSetSerializer(serializers.Serializer): + """编辑题单序列化器""" + + id = serializers.IntegerField() + title = serializers.CharField(max_length=200, required=False) + description = serializers.CharField(required=False) + difficulty = serializers.CharField(required=False) + is_public = serializers.BooleanField(required=False) + status = serializers.CharField(required=False) + visible = serializers.BooleanField(required=False) + + +class ProblemSetProblemSerializer(serializers.ModelSerializer): + """题单题目序列化器""" + + problem = serializers.SerializerMethodField() + + class Meta: + model = ProblemSetProblem + fields = "__all__" + + def get_problem(self, obj): + """获取题目详细信息""" + from problem.serializers import ProblemSerializer + + return ProblemSerializer(obj.problem, context=self.context).data + + +class AddProblemToSetSerializer(serializers.Serializer): + """添加题目到题单序列化器""" + + problemset_id = serializers.IntegerField() + problem_id = serializers.IntegerField() + order = serializers.IntegerField(default=0) + is_required = serializers.BooleanField(default=True) + score = serializers.IntegerField(default=0) + hint = serializers.CharField(required=False, allow_blank=True) + + +class ProblemSetBadgeSerializer(serializers.ModelSerializer): + """题单奖章序列化器""" + + class Meta: + model = ProblemSetBadge + fields = "__all__" + + +class CreateProblemSetBadgeSerializer(serializers.Serializer): + """创建题单奖章序列化器""" + + problemset_id = serializers.IntegerField() + name = serializers.CharField(max_length=100) + description = serializers.CharField() + icon = serializers.CharField() + condition_type = serializers.CharField() # all_problems, problem_count, score + condition_value = serializers.IntegerField() + level = serializers.IntegerField(default=1) + + +class ProblemSetProgressSerializer(serializers.ModelSerializer): + """题单进度序列化器""" + + problemset = ProblemSetListSerializer() + user = UsernameSerializer() + + class Meta: + model = ProblemSetProgress + fields = "__all__" + + +class UserBadgeSerializer(serializers.ModelSerializer): + """用户奖章序列化器""" + + badge = ProblemSetBadgeSerializer() + + class Meta: + model = UserBadge + fields = "__all__" + + +class JoinProblemSetSerializer(serializers.Serializer): + """加入题单序列化器""" + + problemset_id = serializers.IntegerField() + + +class UpdateProgressSerializer(serializers.Serializer): + """更新进度序列化器""" + + problemset_id = serializers.IntegerField() + problem_id = serializers.IntegerField() + status = serializers.CharField() # completed, attempted, not_started + score = serializers.IntegerField(default=0) + submit_time = serializers.DateTimeField(required=False) diff --git a/problemset/signals.py b/problemset/signals.py new file mode 100644 index 0000000..7e0cd1f --- /dev/null +++ b/problemset/signals.py @@ -0,0 +1,73 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.utils import timezone + +from .models import ProblemSetProgress, ProblemSetBadge, UserBadge +from submission.models import Submission + + +@receiver(post_save, sender=Submission) +def update_problemset_progress(sender, instance, created, **kwargs): + """当提交状态更新时,自动更新题单进度""" + if not created: # 只处理更新,不处理新建 + return + + # 检查该提交是否属于某个题单中的题目 + try: + from .models import ProblemSetProblem + + problemset_problems = ProblemSetProblem.objects.filter(problem=instance.problem) + + for psp in problemset_problems: + # 获取或创建用户在该题单中的进度记录 + progress, created = ProblemSetProgress.objects.get_or_create( + problemset=psp.problemset, user=instance.user + ) + + # 更新详细进度 + problem_id = str(instance.problem.id) + progress.progress_detail[problem_id] = { + "status": "completed" + if instance.result == 0 + else "attempted", # 0表示AC + "score": instance.score if hasattr(instance, "score") else 0, + "submit_time": timezone.now().isoformat(), + } + + # 更新进度 + progress.update_progress() + + # 检查是否获得奖章 + check_and_award_badges(progress) + + except Exception as e: + # 记录错误但不影响主流程 + import logging + + logger = logging.getLogger(__name__) + logger.error(f"更新题单进度时出错: {e}") + + +def check_and_award_badges(progress): + """检查并颁发奖章""" + badges = ProblemSetBadge.objects.filter(problemset=progress.problemset) + + for badge in badges: + # 检查是否已经获得该奖章 + if UserBadge.objects.filter(user=progress.user, badge=badge).exists(): + continue + + # 检查是否满足获得条件 + should_award = False + + if badge.condition_type == "all_problems": + should_award = ( + progress.completed_problems_count == progress.total_problems_count + ) + elif badge.condition_type == "problem_count": + should_award = progress.completed_problems_count >= badge.condition_value + elif badge.condition_type == "score": + should_award = progress.total_score >= badge.condition_value + + if should_award: + UserBadge.objects.create(user=progress.user, badge=badge) diff --git a/problemset/urls/__init__.py b/problemset/urls/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/problemset/urls/admin.py b/problemset/urls/admin.py new file mode 100644 index 0000000..1955436 --- /dev/null +++ b/problemset/urls/admin.py @@ -0,0 +1,64 @@ +from django.urls import path + +from problemset.views.admin import ( + ProblemSetAdminAPI, + ProblemSetBadgeAdminAPI, + ProblemSetDetailAdminAPI, + ProblemSetProblemAdminAPI, + ProblemSetProgressAdminAPI, + ProblemSetStatusAPI, + ProblemSetVisibleAPI, +) + +urlpatterns = [ + # 管理员题单管理API + path("problemset/", ProblemSetAdminAPI.as_view(), name="admin_problemset_api"), + path( + "problemset//", + ProblemSetDetailAdminAPI.as_view(), + name="admin_problemset_detail_api", + ), + path( + "problemset//problems/", + ProblemSetProblemAdminAPI.as_view(), + name="admin_problemset_problems_api", + ), + path( + "problemset//problems//", + ProblemSetProblemAdminAPI.as_view(), + name="admin_problemset_problem_detail_api", + ), + # 管理员奖章管理API + path( + "problemset//badges/", + ProblemSetBadgeAdminAPI.as_view(), + name="admin_problemset_badges_api", + ), + path( + "problemset//badges//", + ProblemSetBadgeAdminAPI.as_view(), + name="admin_problemset_badge_detail_api", + ), + # 管理员进度管理API + path( + "problemset//progress/", + ProblemSetProgressAdminAPI.as_view(), + name="admin_problemset_progress_api", + ), + path( + "problemset//progress//", + ProblemSetProgressAdminAPI.as_view(), + name="admin_problemset_progress_detail_api", + ), + # 题单状态管理API + path( + "problemset/visible/", + ProblemSetVisibleAPI.as_view(), + name="admin_problemset_visible_api", + ), + path( + "problemset/status/", + ProblemSetStatusAPI.as_view(), + name="admin_problemset_status_api", + ), +] diff --git a/problemset/urls/oj.py b/problemset/urls/oj.py new file mode 100644 index 0000000..d21bbc3 --- /dev/null +++ b/problemset/urls/oj.py @@ -0,0 +1,54 @@ +from django.urls import path +from problemset.views.oj import ( + ProblemSetAPI, + ProblemSetDetailAPI, + ProblemSetProblemAPI, + ProblemSetProgressAPI, + UserBadgeAPI, + UserProgressAPI, + ProblemSetBadgeAPI, +) + +urlpatterns = [ + # 题单相关API + path("api/problemset/", ProblemSetAPI.as_view(), name="problemset_api"), + path( + "api/problemset//", + ProblemSetDetailAPI.as_view(), + name="problemset_detail_api", + ), + path( + "api/problemset//problems/", + ProblemSetProblemAPI.as_view(), + name="problemset_problems_api", + ), + path( + "api/problemset//problems//", + ProblemSetProblemAPI.as_view(), + name="problemset_problem_detail_api", + ), + # 进度相关API + path( + "api/problemset/progress/", + ProblemSetProgressAPI.as_view(), + name="problemset_progress_api", + ), + path( + "api/problemset//progress/", + ProblemSetProgressAPI.as_view(), + name="problemset_progress_detail_api", + ), + path("api/user/progress/", UserProgressAPI.as_view(), name="user_progress_api"), + # 奖章相关API + path("api/user/badges/", UserBadgeAPI.as_view(), name="user_badges_api"), + path( + "api/user/badges//", + UserBadgeAPI.as_view(), + name="user_badge_detail_api", + ), + path( + "api/problemset//badges/", + ProblemSetBadgeAPI.as_view(), + name="problemset_badges_api", + ), +] diff --git a/problemset/views/__init__.py b/problemset/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/problemset/views/admin.py b/problemset/views/admin.py new file mode 100644 index 0000000..3e80c38 --- /dev/null +++ b/problemset/views/admin.py @@ -0,0 +1,318 @@ +from django.db.models import Q + +from utils.api import APIView, validate_serializer +from account.decorators import super_admin_required, ensure_created_by + +from problemset.models import ( + ProblemSet, + ProblemSetProblem, + ProblemSetBadge, + ProblemSetProgress, +) +from problemset.serializers import ( + ProblemSetSerializer, + ProblemSetListSerializer, + CreateProblemSetSerializer, + EditProblemSetSerializer, + ProblemSetProblemSerializer, + AddProblemToSetSerializer, + ProblemSetBadgeSerializer, + CreateProblemSetBadgeSerializer, + ProblemSetProgressSerializer, +) +from problem.models import Problem + + +class ProblemSetAdminAPI(APIView): + """题单管理API""" + + @super_admin_required + def get(self, request): + """获取题单列表(管理员)""" + problem_sets = ProblemSet.objects.all().order_by("-create_time") + + # 过滤条件 + keyword = request.GET.get("keyword", "").strip() + if keyword: + problem_sets = problem_sets.filter( + Q(title__icontains=keyword) | Q(description__icontains=keyword) + ) + + difficulty = request.GET.get("difficulty") + if difficulty: + problem_sets = problem_sets.filter(difficulty=difficulty) + + status_filter = request.GET.get("status") + if status_filter: + problem_sets = problem_sets.filter(status=status_filter) + + # 权限过滤:如果不是超级管理员,只能看到自己创建的题单 + if not request.user.is_admin(): + problem_sets = problem_sets.filter(created_by=request.user) + + # 使用统一的分页方法 + data = self.paginate_data(request, problem_sets, ProblemSetListSerializer) + return self.success(data) + + @super_admin_required + @validate_serializer(CreateProblemSetSerializer) + def post(self, request): + """创建题单""" + data = request.data + data["created_by"] = request.user + problem_set = ProblemSet.objects.create(**data) + return self.success(ProblemSetSerializer(problem_set).data) + + @super_admin_required + @validate_serializer(EditProblemSetSerializer) + def put(self, request): + """编辑题单""" + data = request.data + problem_set_id = data.pop("id") + + try: + problem_set = ProblemSet.objects.get(id=problem_set_id) + ensure_created_by(problem_set, request.user) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + # 更新题单信息 + for key, value in data.items(): + if key != "id": + setattr(problem_set, key, value) + problem_set.save() + + return self.success(ProblemSetSerializer(problem_set).data) + + @super_admin_required + def delete(self, request): + """删除题单""" + problem_set_id = request.GET.get("id") + if not problem_set_id: + return self.error("题单ID是必需的") + + try: + problem_set = ProblemSet.objects.get(id=problem_set_id) + ensure_created_by(problem_set, request.user) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + # 软删除:设置为不可见 + problem_set.visible = False + problem_set.save() + + return self.success("题单已删除") + + +class ProblemSetDetailAdminAPI(APIView): + """题单详情管理API""" + + @super_admin_required + def get(self, request, problem_set_id): + """获取题单详情(管理员)""" + try: + problem_set = ProblemSet.objects.get(id=problem_set_id) + ensure_created_by(problem_set, request.user) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + serializer = ProblemSetSerializer(problem_set, context={"request": request}) + return self.success(serializer.data) + + +class ProblemSetProblemAdminAPI(APIView): + """题单题目管理API(管理员)""" + + @super_admin_required + def get(self, request, problem_set_id): + """获取题单中的题目列表(管理员)""" + try: + problem_set = ProblemSet.objects.get(id=problem_set_id) + ensure_created_by(problem_set, request.user) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + problems = ProblemSetProblem.objects.filter(problemset=problem_set).order_by( + "order" + ) + serializer = ProblemSetProblemSerializer( + problems, many=True, context={"request": request} + ) + return self.success(serializer.data) + + @super_admin_required + @validate_serializer(AddProblemToSetSerializer) + def post(self, request, problem_set_id): + """添加题目到题单(管理员)""" + try: + problem_set = ProblemSet.objects.get(id=problem_set_id) + ensure_created_by(problem_set, request.user) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + data = request.data + try: + problem = Problem.objects.get(id=data["problem_id"]) + except Problem.DoesNotExist: + return self.error("题目不存在") + + # 检查题目是否已经在题单中 + if ProblemSetProblem.objects.filter( + problemset=problem_set, problem=problem + ).exists(): + return self.error("题目已在该题单中") + + ProblemSetProblem.objects.create( + problemset=problem_set, + problem=problem, + order=data.get("order", 0), + is_required=data.get("is_required", True), + score=data.get("score", 0), + hint=data.get("hint", ""), + ) + + return self.success("题目已添加到题单") + + @super_admin_required + def delete(self, request, problem_set_id, problem_id): + """从题单中移除题目(管理员)""" + try: + problem_set = ProblemSet.objects.get(id=problem_set_id) + ensure_created_by(problem_set, request.user) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + try: + problem_set_problem = ProblemSetProblem.objects.get( + problemset=problem_set, problem_id=problem_id + ) + problem_set_problem.delete() + return self.success("题目已从题单中移除") + except ProblemSetProblem.DoesNotExist: + return self.error("题目不在该题单中") + + +class ProblemSetBadgeAdminAPI(APIView): + """题单奖章管理API(管理员)""" + + @super_admin_required + def get(self, request, problem_set_id): + """获取题单的奖章列表(管理员)""" + try: + problem_set = ProblemSet.objects.get(id=problem_set_id) + ensure_created_by(problem_set, request.user) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + badges = ProblemSetBadge.objects.filter(problemset=problem_set) + serializer = ProblemSetBadgeSerializer(badges, many=True) + return self.success(serializer.data) + + @super_admin_required + @validate_serializer(CreateProblemSetBadgeSerializer) + def post(self, request, problem_set_id): + """创建题单奖章(管理员)""" + try: + problem_set = ProblemSet.objects.get(id=problem_set_id) + ensure_created_by(problem_set, request.user) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + data = request.data + data["problemset"] = problem_set + badge = ProblemSetBadge.objects.create(**data) + + return self.success(ProblemSetBadgeSerializer(badge).data) + + @super_admin_required + def delete(self, request, problem_set_id, badge_id): + """删除题单奖章(管理员)""" + try: + problem_set = ProblemSet.objects.get(id=problem_set_id) + ensure_created_by(problem_set, request.user) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + try: + badge = ProblemSetBadge.objects.get(id=badge_id, problemset=problem_set) + badge.delete() + return self.success("奖章已删除") + except ProblemSetBadge.DoesNotExist: + return self.error("奖章不存在") + + +class ProblemSetProgressAdminAPI(APIView): + """题单进度管理API(管理员)""" + + @super_admin_required + def get(self, request, problem_set_id): + """获取题单的所有用户进度(管理员)""" + try: + problem_set = ProblemSet.objects.get(id=problem_set_id) + ensure_created_by(problem_set, request.user) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + progress_list = ProblemSetProgress.objects.filter(problemset=problem_set).order_by( + "-join_time" + ) + serializer = ProblemSetProgressSerializer(progress_list, many=True) + return self.success(serializer.data) + + @super_admin_required + def delete(self, request, problem_set_id, user_id): + """移除用户从题单(管理员)""" + try: + problem_set = ProblemSet.objects.get(id=problem_set_id) + ensure_created_by(problem_set, request.user) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + try: + progress = ProblemSetProgress.objects.get( + problemset=problem_set, user_id=user_id + ) + progress.delete() + return self.success("用户已从题单中移除") + except ProblemSetProgress.DoesNotExist: + return self.error("用户未加入该题单") + + +class ProblemSetVisibleAPI(APIView): + """题单可见性管理API""" + + @super_admin_required + def put(self, request): + """切换题单可见性""" + data = request.data + try: + problem_set = ProblemSet.objects.get(id=data["id"]) + ensure_created_by(problem_set, request.user) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + problem_set.visible = not problem_set.visible + problem_set.save() + return self.success() + + +class ProblemSetStatusAPI(APIView): + """题单状态管理API""" + + @super_admin_required + def put(self, request): + """更新题单状态""" + data = request.data + try: + problem_set = ProblemSet.objects.get(id=data["id"]) + ensure_created_by(problem_set, request.user) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + status = data.get("status") + if status not in ["active", "archived", "draft"]: + return self.error("无效的状态") + + problem_set.status = status + problem_set.save() + return self.success() diff --git a/problemset/views/oj.py b/problemset/views/oj.py new file mode 100644 index 0000000..9ebe1ae --- /dev/null +++ b/problemset/views/oj.py @@ -0,0 +1,374 @@ + +from django.db.models import Q +from django.utils import timezone + +from utils.api import APIView, validate_serializer + +from problemset.models import ( + ProblemSet, + ProblemSetProblem, + ProblemSetBadge, + ProblemSetProgress, + UserBadge, +) +from problemset.serializers import ( + ProblemSetSerializer, + ProblemSetListSerializer, + CreateProblemSetSerializer, + EditProblemSetSerializer, + ProblemSetProblemSerializer, + AddProblemToSetSerializer, + ProblemSetBadgeSerializer, + CreateProblemSetBadgeSerializer, + ProblemSetProgressSerializer, + UserBadgeSerializer, + JoinProblemSetSerializer, + UpdateProgressSerializer, +) +from problem.models import Problem + + +class ProblemSetAPI(APIView): + """题单API""" + + def get(self, request): + """获取题单列表""" + problem_sets = ProblemSet.objects.filter(visible=True) + + # 过滤条件 + keyword = request.GET.get("keyword", "").strip() + if keyword: + problem_sets = problem_sets.filter( + Q(title__icontains=keyword) | Q(description__icontains=keyword) + ) + + difficulty = request.GET.get("difficulty") + if difficulty: + problem_sets = problem_sets.filter(difficulty=difficulty) + + status_filter = request.GET.get("status") + if status_filter: + problem_sets = problem_sets.filter(status=status_filter) + + # 只显示公开的题单,除非是管理员 + if not request.user.is_authenticated or not request.user.is_admin_role(): + problem_sets = problem_sets.filter(is_public=True) + + # 排序 + sort = request.GET.get("sort") + if sort: + problem_sets = problem_sets.order_by(sort) + else: + problem_sets = problem_sets.order_by("-create_time") + + data = self.paginate_data(request, problem_sets, ProblemSetListSerializer) + return self.success(data) + + @validate_serializer(CreateProblemSetSerializer) + def post(self, request): + """创建题单""" + data = request.data + data["created_by"] = request.user + problem_set = ProblemSet.objects.create(**data) + return self.success(ProblemSetSerializer(problem_set).data) + + +class ProblemSetDetailAPI(APIView): + """题单详情API""" + + def get(self, request, problem_set_id): + """获取题单详情""" + try: + problem_set = ProblemSet.objects.get(id=problem_set_id, visible=True) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + # 检查权限 + if not problem_set.is_public and not ( + request.user.is_authenticated and request.user.is_admin_role() + ): + return self.error("无权限访问该题单") + + serializer = ProblemSetSerializer(problem_set, context={"request": request}) + return self.success(serializer.data) + + @validate_serializer(EditProblemSetSerializer) + def put(self, request, problem_set_id): + """编辑题单""" + try: + problem_set = ProblemSet.objects.get(id=problem_set_id) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + # 检查权限 + if not request.user.is_admin_role() and problem_set.created_by != request.user: + return self.error("无权限编辑该题单") + + data = request.data + for key, value in data.items(): + if key != "id": + setattr(problem_set, key, value) + problem_set.save() + + return self.success(ProblemSetSerializer(problem_set).data) + + def delete(self, request, problem_set_id): + """删除题单""" + try: + problem_set = ProblemSet.objects.get(id=problem_set_id) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + # 检查权限 + if not request.user.is_admin_role() and problem_set.created_by != request.user: + return self.error("无权限删除该题单") + + problem_set.visible = False + problem_set.save() + + return self.success("题单已删除") + + +class ProblemSetProblemAPI(APIView): + """题单题目管理API""" + + def get(self, request, problem_set_id): + """获取题单中的题目列表""" + try: + problem_set = ProblemSet.objects.get(id=problem_set_id, visible=True) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + # 检查权限 + if not problem_set.is_public and not ( + request.user.is_authenticated and request.user.is_admin_role() + ): + return self.error("无权限访问该题单") + + problems = ProblemSetProblem.objects.filter(problemset=problem_set).order_by( + "order" + ) + serializer = ProblemSetProblemSerializer( + problems, many=True, context={"request": request} + ) + return self.success(serializer.data) + + @validate_serializer(AddProblemToSetSerializer) + def post(self, request, problem_set_id): + """添加题目到题单""" + try: + problem_set = ProblemSet.objects.get(id=problem_set_id) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + # 检查权限 + if not request.user.is_admin_role() and problem_set.created_by != request.user: + return self.error("无权限管理该题单") + + data = request.data + try: + problem = Problem.objects.get(id=data["problem_id"]) + except Problem.DoesNotExist: + return self.error("题目不存在") + + # 检查题目是否已经在题单中 + if ProblemSetProblem.objects.filter( + problemset=problem_set, problem=problem + ).exists(): + return self.error("题目已在该题单中") + + ProblemSetProblem.objects.create( + problemset=problem_set, + problem=problem, + order=data.get("order", 0), + is_required=data.get("is_required", True), + score=data.get("score", 0), + hint=data.get("hint", ""), + ) + + return self.success("题目已添加到题单") + + def delete(self, request, problem_set_id, problem_id): + """从题单中移除题目""" + try: + problem_set = ProblemSet.objects.get(id=problem_set_id) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + # 检查权限 + if not request.user.is_admin_role() and problem_set.created_by != request.user: + return self.error("无权限管理该题单") + + try: + problem_set_problem = ProblemSetProblem.objects.get( + problemset=problem_set, problem_id=problem_id + ) + problem_set_problem.delete() + return self.success("题目已从题单中移除") + except ProblemSetProblem.DoesNotExist: + return self.error("题目不在该题单中") + + +class ProblemSetProgressAPI(APIView): + """题单进度API""" + + @validate_serializer(JoinProblemSetSerializer) + def post(self, request): + """加入题单""" + data = request.data + try: + problem_set = ProblemSet.objects.get(id=data["problemset_id"], visible=True) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + # 检查权限 + if not problem_set.is_public and not request.user.is_admin_role(): + return self.error("无权限加入该题单") + + # 检查是否已经加入 + if ProblemSetProgress.objects.filter( + problemset=problem_set, user=request.user + ).exists(): + return self.error("已经加入该题单") + + # 创建进度记录 + progress = ProblemSetProgress.objects.create( + problemset=problem_set, user=request.user + ) + progress.update_progress() + + return self.success("成功加入题单") + + def get(self, request, problem_set_id): + """获取题单进度""" + try: + problem_set = ProblemSet.objects.get(id=problem_set_id, visible=True) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + try: + progress = ProblemSetProgress.objects.get( + problemset=problem_set, user=request.user + ) + except ProblemSetProgress.DoesNotExist: + return self.error("未加入该题单") + + serializer = ProblemSetProgressSerializer(progress) + return self.success(serializer.data) + + @validate_serializer(UpdateProgressSerializer) + def put(self, request): + """更新进度""" + data = request.data + try: + problem_set = ProblemSet.objects.get(id=data["problemset_id"], visible=True) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + try: + progress = ProblemSetProgress.objects.get( + problemset=problem_set, user=request.user + ) + except ProblemSetProgress.DoesNotExist: + return self.error("未加入该题单") + + # 更新详细进度 + problem_id = str(data["problem_id"]) + progress.progress_detail[problem_id] = { + "status": data["status"], + "score": data.get("score", 0), + "submit_time": data.get("submit_time", timezone.now().isoformat()), + } + + # 更新进度 + progress.update_progress() + + # 检查是否获得奖章 + self._check_badges(progress) + + return self.success("进度已更新") + + def _check_badges(self, progress): + """检查是否获得奖章""" + badges = ProblemSetBadge.objects.filter(problemset=progress.problemset) + + for badge in badges: + # 检查是否已经获得该奖章 + if UserBadge.objects.filter(user=progress.user, badge=badge).exists(): + continue + + # 检查是否满足获得条件 + if badge.condition_type == "all_problems": + if progress.completed_problems_count == progress.total_problems_count: + UserBadge.objects.create(user=progress.user, badge=badge) + elif badge.condition_type == "problem_count": + if progress.completed_problems_count >= badge.condition_value: + UserBadge.objects.create(user=progress.user, badge=badge) + elif badge.condition_type == "score": + if progress.total_score >= badge.condition_value: + UserBadge.objects.create(user=progress.user, badge=badge) + + +class UserProgressAPI(APIView): + """用户进度API""" + + def get(self, request): + """获取用户的题单进度列表""" + progress_list = ProblemSetProgress.objects.filter(user=request.user).order_by( + "-join_time" + ) + serializer = ProblemSetProgressSerializer(progress_list, many=True) + return self.success(serializer.data) + + +class UserBadgeAPI(APIView): + """用户奖章API""" + + def get(self, request): + """获取用户的奖章列表""" + badges = UserBadge.objects.filter(user=request.user).order_by("-earned_time") + serializer = UserBadgeSerializer(badges, many=True) + return self.success(serializer.data) + + def put(self, request, badge_id): + """标记奖章为已展示""" + try: + user_badge = UserBadge.objects.get(id=badge_id, user=request.user) + user_badge.is_displayed = True + user_badge.save() + return self.success("奖章已标记为已展示") + except UserBadge.DoesNotExist: + return self.error("奖章不存在") + + +class ProblemSetBadgeAPI(APIView): + """题单奖章管理API""" + + def get(self, request, problem_set_id): + """获取题单的奖章列表""" + try: + problem_set = ProblemSet.objects.get(id=problem_set_id, visible=True) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + badges = ProblemSetBadge.objects.filter(problemset=problem_set) + serializer = ProblemSetBadgeSerializer(badges, many=True) + return self.success(serializer.data) + + @validate_serializer(CreateProblemSetBadgeSerializer) + def post(self, request, problem_set_id): + """创建题单奖章""" + try: + problem_set = ProblemSet.objects.get(id=problem_set_id) + except ProblemSet.DoesNotExist: + return self.error("题单不存在") + + # 检查权限 + if not request.user.is_admin_role() and problem_set.created_by != request.user: + return self.error("无权限管理该题单") + + data = request.data + data["problemset"] = problem_set + badge = ProblemSetBadge.objects.create(**data) + + return self.success(ProblemSetBadgeSerializer(badge).data)