put the whole content pipeline behind the admin panel
سوالها از قبل فلهای وارد میشدند ولی دو شکاف مانده بود: افزودنِ تکی عملاً غیرقابلِ استفاده بود، و خودِ برنامهی درسی هیچ راهی جز ترمینالِ سرور نداشت. افزودنِ تکیِ سوال: انتخابِ درس یک دراپداونِ ۱۳۰۷تایی بود. حالا autocomplete است و بر اساسِ عنوانِ درس، فصل و پایه جستجو میشود. برنامهی درسی (Grade → Unit → Node → Lesson) حالا از /admin/content/grade/ وارد و خارج میشود. تا امروز رساندنش به سرور یعنی اجرای rag_import_collection و بعد seed_curriculum_from_textbook روی همان سرور — یعنی وابستگی به Qdrant و به آدمی با دسترسیِ SSH. حالا یکبار لوکال ساخته میشود و فایلش بالا میرود. همسانسازی با عنوان است نه با id: idها بینِ دو دیتابیس یکی نیستند و تکیه بر آنها یعنی وارد کردن روی سرور محتوای بیربط را بازنویسی میکند. وارد کردنِ برنامهی درسی عمداً چیزی پاک نمیکند و idempotent است — یک فایلِ ناقص نباید محتوای سرور را از بین ببرد، و اجرای دوباره نباید چیزی را دو بار بسازد. (وارد کردنِ سوالها اما replace دارد، چون جایگزینیِ سوالهای یک درس کارِ رایجی است.) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -94,3 +94,89 @@ class HealthEndpointTests(TestCase):
|
||||
|
||||
def test_needs_no_authentication(self):
|
||||
self.assertEqual(self.client.get('/api/health').status_code, 200)
|
||||
|
||||
|
||||
class CurriculumImportTests(TestCase):
|
||||
"""
|
||||
برنامهی درسی باید بدونِ Qdrant و بدونِ ترمینال به سرور برسد.
|
||||
|
||||
مهمترین خاصیتش این است که چیزی پاک نمیکند: یک فایلِ ناقص نباید محتوای
|
||||
سرور را از بین ببرد.
|
||||
"""
|
||||
|
||||
SAMPLE = {
|
||||
'grades': [{
|
||||
'title': 'پایه نهم', 'icon': 'grade-9', 'color': '#FF9800',
|
||||
'units': [{
|
||||
'title': 'فصل ۴: توان و ریشه', 'order': 4,
|
||||
'nodes': [{
|
||||
'title': 'توان صحیح', 'order': 1,
|
||||
'lessons': [{'title': 'ضرب توانها', 'introText': 'پایهها برابرند.'}],
|
||||
}],
|
||||
}],
|
||||
}],
|
||||
}
|
||||
|
||||
def _raw(self, payload=None):
|
||||
return json.dumps(payload or self.SAMPLE, ensure_ascii=False).encode()
|
||||
|
||||
def test_builds_the_whole_tree(self):
|
||||
from content.curriculum_io import import_curriculum
|
||||
from content.curriculum_io import parse as parse_curriculum
|
||||
|
||||
counts = import_curriculum(parse_curriculum(self._raw()))
|
||||
self.assertEqual((counts['grades'], counts['units'], counts['nodes'], counts['lessons']),
|
||||
(1, 1, 1, 1))
|
||||
self.assertEqual(Lesson.objects.get().intro_text, 'پایهها برابرند.')
|
||||
|
||||
def test_running_it_twice_creates_nothing_new(self):
|
||||
from content.curriculum_io import import_curriculum
|
||||
from content.curriculum_io import parse as parse_curriculum
|
||||
|
||||
import_curriculum(parse_curriculum(self._raw()))
|
||||
counts = import_curriculum(parse_curriculum(self._raw()))
|
||||
self.assertEqual(counts['lessons'], 0)
|
||||
self.assertEqual(Lesson.objects.count(), 1)
|
||||
|
||||
def test_a_partial_file_never_deletes_what_is_already_there(self):
|
||||
from content.curriculum_io import import_curriculum
|
||||
from content.curriculum_io import parse as parse_curriculum
|
||||
|
||||
import_curriculum(parse_curriculum(self._raw()))
|
||||
smaller = json.loads(json.dumps(self.SAMPLE))
|
||||
smaller['grades'][0]['units'] = []
|
||||
import_curriculum(parse_curriculum(self._raw(smaller)))
|
||||
self.assertEqual(Lesson.objects.count(), 1, 'محتوای موجود نباید پاک شود')
|
||||
|
||||
def test_a_missing_title_is_rejected_with_a_readable_message(self):
|
||||
from content.curriculum_io import CurriculumError
|
||||
from content.curriculum_io import parse as parse_curriculum
|
||||
|
||||
broken = json.loads(json.dumps(self.SAMPLE))
|
||||
broken['grades'][0]['units'][0]['nodes'][0]['title'] = ''
|
||||
with self.assertRaises(CurriculumError) as ctx:
|
||||
parse_curriculum(self._raw(broken))
|
||||
self.assertIn('درس', str(ctx.exception))
|
||||
|
||||
def test_export_round_trips_back_through_import(self):
|
||||
from content.curriculum_io import export_curriculum, import_curriculum
|
||||
from content.curriculum_io import parse as parse_curriculum
|
||||
|
||||
import_curriculum(parse_curriculum(self._raw()))
|
||||
exported = export_curriculum(Grade.objects.all())
|
||||
Grade.objects.all().delete()
|
||||
import_curriculum(parse_curriculum(json.dumps(exported, ensure_ascii=False).encode()))
|
||||
self.assertEqual(Lesson.objects.get().title, 'ضرب توانها')
|
||||
|
||||
|
||||
class HealthWarningTests(TestCase):
|
||||
def test_debug_mode_is_reported_as_a_warning(self):
|
||||
with self.settings(DEBUG=True, DEV_MASTER_OTP='0000'):
|
||||
body = self.client.get('/api/health').json()
|
||||
joined = ' '.join(body['warnings'])
|
||||
self.assertIn('DEBUG', joined)
|
||||
self.assertIn('0000', joined)
|
||||
|
||||
def test_allowed_origins_include_the_deployed_frontend(self):
|
||||
body = self.client.get('/api/health').json()
|
||||
self.assertIn('https://math.tadrisino2.ir', body['allowedOrigins'])
|
||||
|
||||
Reference in New Issue
Block a user