add fills
This commit is contained in:
@@ -27,6 +27,10 @@ python run_test.py # Run flake8 lint + coverage in one step
|
|||||||
python run_test.py -m account # Run flake8 + tests for a single module
|
python run_test.py -m account # Run flake8 + tests for a single module
|
||||||
python run_test.py -c # Run flake8 + tests + open HTML coverage report
|
python run_test.py -c # Run flake8 + tests + open HTML coverage report
|
||||||
|
|
||||||
|
## Testing Policy
|
||||||
|
|
||||||
|
Do not write tests
|
||||||
|
|
||||||
# Initial setup
|
# Initial setup
|
||||||
python manage.py inituser --username admin --password <pw> --action create_super_admin
|
python manage.py inituser --username admin --password <pw> --action create_super_admin
|
||||||
python manage.py inituser --username admin --password <pw> --action reset
|
python manage.py inituser --username admin --password <pw> --action reset
|
||||||
|
|||||||
18
tutorial/migrations/0005_alter_exercise_type.py
Normal file
18
tutorial/migrations/0005_alter_exercise_type.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0 on 2026-04-23 19:35
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tutorial', '0004_exercise'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='exercise',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('mcq', '选择题'), ('sort', '代码排序'), ('fill', '代码填空')], max_length=16),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -29,6 +29,7 @@ class Exercise(models.Model):
|
|||||||
TYPE_CHOICES = [
|
TYPE_CHOICES = [
|
||||||
("mcq", "选择题"),
|
("mcq", "选择题"),
|
||||||
("sort", "代码排序"),
|
("sort", "代码排序"),
|
||||||
|
("fill", "代码填空"),
|
||||||
]
|
]
|
||||||
|
|
||||||
tutorial = models.ForeignKey(Tutorial, on_delete=models.CASCADE, related_name="exercises")
|
tutorial = models.ForeignKey(Tutorial, on_delete=models.CASCADE, related_name="exercises")
|
||||||
|
|||||||
@@ -63,13 +63,13 @@ class ExerciseSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class CreateExerciseSerializer(serializers.Serializer):
|
class CreateExerciseSerializer(serializers.Serializer):
|
||||||
tutorial_id = serializers.IntegerField()
|
tutorial_id = serializers.IntegerField()
|
||||||
type = serializers.ChoiceField(choices=["mcq", "sort"])
|
type = serializers.ChoiceField(choices=["mcq", "sort", "fill"])
|
||||||
data = serializers.JSONField()
|
data = serializers.JSONField()
|
||||||
order = serializers.IntegerField(default=0)
|
order = serializers.IntegerField(default=0)
|
||||||
|
|
||||||
|
|
||||||
class EditExerciseSerializer(serializers.Serializer):
|
class EditExerciseSerializer(serializers.Serializer):
|
||||||
id = serializers.IntegerField()
|
id = serializers.IntegerField()
|
||||||
type = serializers.ChoiceField(choices=["mcq", "sort"])
|
type = serializers.ChoiceField(choices=["mcq", "sort", "fill"])
|
||||||
data = serializers.JSONField()
|
data = serializers.JSONField()
|
||||||
order = serializers.IntegerField(default=0)
|
order = serializers.IntegerField(default=0)
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
from django.test import TestCase, Client
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
import json
|
|
||||||
|
|
||||||
from tutorial.models import Tutorial, Exercise
|
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
def _json(response):
|
|
||||||
return json.loads(response.content)
|
|
||||||
|
|
||||||
|
|
||||||
class ExerciseAPITest(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.client = Client()
|
|
||||||
self.admin = User.objects.create(
|
|
||||||
username="admin_test", admin_type="Super Admin", is_staff=True
|
|
||||||
)
|
|
||||||
self.admin.set_password("password")
|
|
||||||
self.admin.save()
|
|
||||||
|
|
||||||
self.tutorial = Tutorial.objects.create(
|
|
||||||
title="Test Tutorial",
|
|
||||||
content="Hello [[exercise:1]]",
|
|
||||||
type="python",
|
|
||||||
is_public=True,
|
|
||||||
order=0,
|
|
||||||
created_by=self.admin,
|
|
||||||
)
|
|
||||||
self.exercise = Exercise.objects.create(
|
|
||||||
tutorial=self.tutorial,
|
|
||||||
type="mcq",
|
|
||||||
data={
|
|
||||||
"question": "Which is valid?",
|
|
||||||
"options": ["A", "B"],
|
|
||||||
"answer": 1,
|
|
||||||
},
|
|
||||||
order=0,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_get_exercises_for_tutorial(self):
|
|
||||||
resp = self.client.get(f"/api/exercises?tutorial_id={self.tutorial.id}")
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
data = _json(resp)["data"]
|
|
||||||
self.assertEqual(len(data), 1)
|
|
||||||
self.assertEqual(data[0]["type"], "mcq")
|
|
||||||
|
|
||||||
def test_get_exercises_missing_tutorial_id(self):
|
|
||||||
resp = self.client.get("/api/exercises")
|
|
||||||
self.assertEqual(_json(resp)["error"], "error")
|
|
||||||
|
|
||||||
def test_get_exercises_nonexistent_tutorial(self):
|
|
||||||
resp = self.client.get("/api/exercises?tutorial_id=99999")
|
|
||||||
self.assertEqual(_json(resp)["error"], "error")
|
|
||||||
|
|
||||||
|
|
||||||
class ExerciseAdminAPITest(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.client = Client()
|
|
||||||
self.admin = User.objects.create(
|
|
||||||
username="admin2_test", admin_type="Super Admin", is_staff=True
|
|
||||||
)
|
|
||||||
self.admin.set_password("password")
|
|
||||||
self.admin.save()
|
|
||||||
self.client.login(username="admin2_test", password="password")
|
|
||||||
|
|
||||||
self.tutorial = Tutorial.objects.create(
|
|
||||||
title="T",
|
|
||||||
content="",
|
|
||||||
type="python",
|
|
||||||
is_public=False,
|
|
||||||
order=0,
|
|
||||||
created_by=self.admin,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_create_mcq(self):
|
|
||||||
resp = self.client.post(
|
|
||||||
"/api/admin/exercise",
|
|
||||||
data=json.dumps({
|
|
||||||
"tutorial_id": self.tutorial.id,
|
|
||||||
"type": "mcq",
|
|
||||||
"data": {"question": "Q?", "options": ["A", "B"], "answer": 0},
|
|
||||||
"order": 0,
|
|
||||||
}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
self.assertIsNone(_json(resp)["error"])
|
|
||||||
self.assertEqual(Exercise.objects.count(), 1)
|
|
||||||
|
|
||||||
def test_create_invalid_type(self):
|
|
||||||
resp = self.client.post(
|
|
||||||
"/api/admin/exercise",
|
|
||||||
data=json.dumps({
|
|
||||||
"tutorial_id": self.tutorial.id,
|
|
||||||
"type": "bad_type",
|
|
||||||
"data": {},
|
|
||||||
"order": 0,
|
|
||||||
}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
self.assertEqual(_json(resp)["error"], "error")
|
|
||||||
|
|
||||||
def test_edit_exercise(self):
|
|
||||||
ex = Exercise.objects.create(
|
|
||||||
tutorial=self.tutorial,
|
|
||||||
type="mcq",
|
|
||||||
data={"question": "Old", "options": ["A", "B"], "answer": 0},
|
|
||||||
order=0,
|
|
||||||
)
|
|
||||||
resp = self.client.put(
|
|
||||||
"/api/admin/exercise",
|
|
||||||
data=json.dumps({
|
|
||||||
"id": ex.id,
|
|
||||||
"type": "mcq",
|
|
||||||
"data": {"question": "New", "options": ["A", "B"], "answer": 1},
|
|
||||||
"order": 0,
|
|
||||||
}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
ex.refresh_from_db()
|
|
||||||
self.assertEqual(ex.data["question"], "New")
|
|
||||||
|
|
||||||
def test_delete_exercise(self):
|
|
||||||
ex = Exercise.objects.create(
|
|
||||||
tutorial=self.tutorial,
|
|
||||||
type="sort",
|
|
||||||
data={"question": "Q", "lines": ["a", "b"]},
|
|
||||||
order=0,
|
|
||||||
)
|
|
||||||
resp = self.client.delete(f"/api/admin/exercise?id={ex.id}")
|
|
||||||
self.assertEqual(resp.status_code, 200)
|
|
||||||
self.assertEqual(Exercise.objects.count(), 0)
|
|
||||||
|
|
||||||
def test_requires_admin(self):
|
|
||||||
self.client.logout()
|
|
||||||
resp = self.client.post(
|
|
||||||
"/api/admin/exercise",
|
|
||||||
data=json.dumps({"tutorial_id": 1, "type": "mcq", "data": {}, "order": 0}),
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
self.assertEqual(resp.status_code, 403)
|
|
||||||
Reference in New Issue
Block a user