diff --git a/tutorial/migrations/0004_exercise.py b/tutorial/migrations/0004_exercise.py new file mode 100644 index 0000000..be0a915 --- /dev/null +++ b/tutorial/migrations/0004_exercise.py @@ -0,0 +1,29 @@ +# Generated by Django 6.0 on 2026-04-23 07:31 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tutorial', '0003_alter_tutorial_code'), + ] + + operations = [ + migrations.CreateModel( + name='Exercise', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', models.CharField(choices=[('mcq', '选择题'), ('sort', '代码排序')], max_length=16)), + ('data', models.JSONField()), + ('order', models.IntegerField(default=0)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('tutorial', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exercises', to='tutorial.tutorial')), + ], + options={ + 'db_table': 'exercise', + 'ordering': ['order', 'created_at'], + }, + ), + ] diff --git a/tutorial/models.py b/tutorial/models.py index 2936e93..19e75e4 100644 --- a/tutorial/models.py +++ b/tutorial/models.py @@ -22,4 +22,24 @@ class Tutorial(models.Model): ordering = ['order', '-created_at'] def __str__(self): - return self.title \ No newline at end of file + return self.title + + +class Exercise(models.Model): + TYPE_CHOICES = [ + ("mcq", "选择题"), + ("sort", "代码排序"), + ] + + tutorial = models.ForeignKey(Tutorial, on_delete=models.CASCADE, related_name="exercises") + type = models.CharField(max_length=16, choices=TYPE_CHOICES) + data = models.JSONField() + order = models.IntegerField(default=0) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + db_table = "exercise" + ordering = ["order", "created_at"] + + def __str__(self): + return f"{self.get_type_display()} (Order {self.order})" \ No newline at end of file diff --git a/tutorial/serializers.py b/tutorial/serializers.py index df908c4..730c97a 100644 --- a/tutorial/serializers.py +++ b/tutorial/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Tutorial +from .models import Tutorial, Exercise from account.serializers import UserSerializer @@ -53,3 +53,23 @@ class EditTutorialSerializer(serializers.ModelSerializer): class Meta: model = Tutorial fields = ["id", "title", "content", "is_public", "order", "type", "code"] + + +class ExerciseSerializer(serializers.ModelSerializer): + class Meta: + model = Exercise + fields = ["id", "type", "data", "order"] + + +class CreateExerciseSerializer(serializers.Serializer): + tutorial_id = serializers.IntegerField() + type = serializers.ChoiceField(choices=["mcq", "sort"]) + data = serializers.JSONField() + order = serializers.IntegerField(default=0) + + +class EditExerciseSerializer(serializers.Serializer): + id = serializers.IntegerField() + type = serializers.ChoiceField(choices=["mcq", "sort"]) + data = serializers.JSONField() + order = serializers.IntegerField(default=0) diff --git a/tutorial/urls/admin.py b/tutorial/urls/admin.py index 8b6a047..e035689 100644 --- a/tutorial/urls/admin.py +++ b/tutorial/urls/admin.py @@ -1,10 +1,8 @@ from django.urls import path -from ..views.admin import TutorialAdminAPI, TutorialVisibilityAPI +from ..views.admin import TutorialAdminAPI, TutorialVisibilityAPI, ExerciseAdminAPI urlpatterns = [ path("tutorial", TutorialAdminAPI.as_view()), - path( - "tutorial/visibility", - TutorialVisibilityAPI.as_view(), - ), + path("tutorial/visibility", TutorialVisibilityAPI.as_view()), + path("exercise", ExerciseAdminAPI.as_view()), ] diff --git a/tutorial/urls/tutorial.py b/tutorial/urls/tutorial.py index e6c1476..78324f7 100644 --- a/tutorial/urls/tutorial.py +++ b/tutorial/urls/tutorial.py @@ -1,7 +1,8 @@ from django.urls import path -from ..views.oj import TutorialAPI, TutorialTitlesAPI +from ..views.oj import TutorialAPI, TutorialTitlesAPI, ExerciseAPI urlpatterns = [ path("tutorial", TutorialAPI.as_view()), path("tutorials", TutorialTitlesAPI.as_view()), + path("exercises", ExerciseAPI.as_view()), ] diff --git a/tutorial/views/admin.py b/tutorial/views/admin.py index 5f53d41..63eb799 100644 --- a/tutorial/views/admin.py +++ b/tutorial/views/admin.py @@ -1,12 +1,15 @@ from account.decorators import super_admin_required from utils.api import APIView, validate_serializer -from tutorial.models import Tutorial +from tutorial.models import Tutorial, Exercise from tutorial.serializers import ( TutorialSerializer, TutorialListSerializer, CreateTutorialSerializer, EditTutorialSerializer, + ExerciseSerializer, + CreateExerciseSerializer, + EditExerciseSerializer, ) @@ -90,3 +93,51 @@ class TutorialVisibilityAPI(APIView): tutorial.is_public = is_public tutorial.save() return self.success(TutorialSerializer(tutorial).data) + + +class ExerciseAdminAPI(APIView): + @validate_serializer(CreateExerciseSerializer) + @super_admin_required + def post(self, request): + data = request.data + try: + tutorial = Tutorial.objects.get(id=data["tutorial_id"]) + except Tutorial.DoesNotExist: + return self.error("Tutorial does not exist") + exercise = Exercise.objects.create( + tutorial=tutorial, + type=data["type"], + data=data["data"], + order=data.get("order", 0), + ) + return self.success(ExerciseSerializer(exercise).data) + + @validate_serializer(EditExerciseSerializer) + @super_admin_required + def put(self, request): + data = request.data + try: + exercise = Exercise.objects.get(id=data["id"]) + except Exercise.DoesNotExist: + return self.error("Exercise does not exist") + exercise.type = data["type"] + exercise.data = data["data"] + exercise.order = data.get("order", exercise.order) + exercise.save() + return self.success(ExerciseSerializer(exercise).data) + + @super_admin_required + def get(self, request): + tutorial_id = request.GET.get("tutorial_id") + if not tutorial_id: + return self.error("tutorial_id is required") + exercises = Exercise.objects.filter(tutorial_id=tutorial_id) + return self.success(ExerciseSerializer(exercises, many=True).data) + + @super_admin_required + def delete(self, request): + exercise_id = request.GET.get("id") + if not exercise_id: + return self.error("id is required") + Exercise.objects.filter(id=exercise_id).delete() + return self.success() diff --git a/tutorial/views/oj.py b/tutorial/views/oj.py index 4b828ea..176611f 100644 --- a/tutorial/views/oj.py +++ b/tutorial/views/oj.py @@ -1,7 +1,7 @@ from utils.api import APIView -from tutorial.models import Tutorial -from tutorial.serializers import TutorialSerializer +from tutorial.models import Tutorial, Exercise +from tutorial.serializers import TutorialSerializer, ExerciseSerializer class TutorialAPI(APIView): @@ -21,3 +21,16 @@ class TutorialTitlesAPI(APIView): "id", "title" ) return self.success(list(tutorials)) + + +class ExerciseAPI(APIView): + def get(self, request): + tutorial_id = request.GET.get("tutorial_id") + if not tutorial_id: + return self.error("tutorial_id is required") + try: + tutorial = Tutorial.objects.get(id=tutorial_id, is_public=True) + except Tutorial.DoesNotExist: + return self.error("Tutorial does not exist") + exercises = Exercise.objects.filter(tutorial=tutorial) + return self.success(ExerciseSerializer(exercises, many=True).data)