API output without Persian diacritics or written ezafe ی (company decision); tutor answers too

plain_persian() strips kasra/fatha/damma/shadda/sukun/hamza-above and the written «ه‌ی» ezafe, keeping tanwin. Applied
at output, not in data: PlainPersianJSONRenderer runs one byte-level pass over every JSON response (~5 ms per MB
locally), and to_plain() cleans tutor answers including the stream. Rewriting titles in the DB/seed would have broken
question binding by title path and SeedRecord keys. Tests updated to expect the cleaned text; config/tests_plain_persian.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 16:57:17 +03:30
parent 7b241c1aac
commit 1c7c4b8eca
16 changed files with 181 additions and 64 deletions

View File

@@ -1,9 +1,9 @@
"""
تست‌های قراردادِ API با صفحه‌های فرانت.
تست‌های قرارداد API با صفحه‌های فرانت.
این‌ها از روی خودِ کدِ فرانت نوشته شده‌اند، نه از روی حدس: هر صفحه داده‌ی
مشخصی هاردکد داشت و اینجا بررسی می‌شود که API همان شکل را می‌دهد. بدونِ
این‌ها، هر بارِ بعدی که سریالایزری عوض شود، شکستن فقط در مرورگر معلوم می‌شد.
این‌ها از روی خود کد فرانت نوشته شده‌اند، نه از روی حدس: هر صفحه داده
مشخصی هاردکد داشت و اینجا بررسی می‌شود که API همان شکل را می‌دهد. بدون
این‌ها، هر بار بعدی که سریالایزری عوض شود، شکستن فقط در مرورگر معلوم می‌شد.
"""
from django.core.cache import cache
from django.test import TestCase
@@ -16,7 +16,7 @@ from content.models import (
class GradePickerTests(TestCase):
"""صفحه‌ی انتخابِ پایه نباید پایه‌ی خالی را مثلِ پایه‌ی پر نشان دهد."""
"""صفحه انتخاب پایه نباید پایه خالی را مثل پایه پر نشان دهد."""
def setUp(self):
cache.clear()
@@ -34,7 +34,7 @@ class GradePickerTests(TestCase):
class SkillMapTests(TestCase):
"""نقشه‌ی مهارت برای هر قدم عنوان، نوع، زیرنویس و «انجام شد» می‌خواهد."""
"""نقشه مهارت برای هر قدم عنوان، نوع، زیرنویس و «انجام شد» می‌خواهد."""
def setUp(self):
cache.clear()
@@ -86,7 +86,7 @@ class SkillMapTests(TestCase):
class LessonContentTests(TestCase):
"""صفحه‌ی درس سه تکه می‌خواهد: توضیح، مثال، حلِ گام‌به‌گام."""
"""صفحه درس سه تکه می‌خواهد: توضیح، مثال، حل گام‌به‌گام."""
def setUp(self):
cache.clear()
@@ -117,16 +117,16 @@ class LessonContentTests(TestCase):
class HomeGradeParamTests(TestCase):
"""
کلاینتِ فرانت `?gradeId=` می‌فرستد ولی نسخه‌ی اولِ این اندپوینت `?grade=`
می‌خواند — و پارامترِ ناشناخته بی‌صدا نادیده گرفته می‌شد، یعنی کاربر نقشه‌ی
پایه‌ی دیگری را می‌دید بدونِ هیچ خطایی.
کلاینت فرانت `?gradeId=` می‌فرستد ولی نسخه اول این اندپوینت `?grade=`
می‌خواند — و پارامتر ناشناخته بی‌صدا نادیده گرفته می‌شد، یعنی کاربر نقشه
پایه دیگری را می‌دید بدون هیچ خطایی.
"""
def setUp(self):
cache.clear()
self.a = Grade.objects.create(title='پایه هفتم', icon='', color='')
self.b = Grade.objects.create(title='پایه نهم', icon='', color='')
for grade, unit_title in ((self.a, 'فصلِ هفتم'), (self.b, 'فصلِ نهم')):
for grade, unit_title in ((self.a, 'فصل هفتم'), (self.b, 'فصل نهم')):
unit = Unit.objects.create(grade=grade, title=unit_title, order=1)
node = Node.objects.create(unit=unit, title='درس', order=1)
Lesson.objects.create(node=node, title='نوع', intro_text='x')
@@ -140,13 +140,13 @@ class HomeGradeParamTests(TestCase):
return [u['title'] for u in data['units']]
def test_grade_id_selects_that_grade(self):
self.assertEqual(self._titles(f'?gradeId={self.b.id}'), ['فصلِ نهم'])
self.assertEqual(self._titles(f'?gradeId={self.b.id}'), ['فصل نهم'])
def test_the_removed_grade_name_is_a_400_not_a_silent_fallback(self):
self.assertEqual(self.client.get(f'/api/home?grade={self.b.id}', **self.auth).status_code, 400)
def test_without_a_parameter_the_users_own_grade_is_used(self):
self.assertEqual(self._titles(), ['فصلِ هفتم'])
self.assertEqual(self._titles(), ['فصل هفتم'])
def test_an_unknown_grade_is_a_clean_404_not_a_silent_fallback(self):
self.assertEqual(self.client.get('/api/home?gradeId=999999', **self.auth).status_code, 404)
@@ -154,9 +154,9 @@ class HomeGradeParamTests(TestCase):
class NodeVisualDefaultsTests(TestCase):
"""
تا وقتی کسی از پنلِ ادمین رنگ نگذاشته، کارتِ مبحث نباید بی‌رنگ بماند.
تا وقتی کسی از پنل ادمین رنگ نگذاشته، کارت مبحث نباید بی‌رنگ بماند.
پیش‌فرض باید *پایدار* باشد نه تصادفی: رنگی که بینِ دو بارگذاری عوض شود،
پیش‌فرض باید *پایدار* باشد نه تصادفی: رنگی که بین دو بارگذاری عوض شود،
بدتر از بی‌رنگ بودن است.
"""
@@ -205,8 +205,8 @@ class NodeVisualDefaultsTests(TestCase):
class HomeMatchesItsSerializerTests(TestCase):
"""
/api/home برای سرعت dict را مستقیم برمی‌گرداند (content/views.py، HomeView)، ولی HomeResponseSerializer
هنوز قراردادِ Swagger است. اگر کسی کلیدی به پاسخ اضافه کند یا نوعی را عوض کند و سریال‌ساز را نه،
این تست می‌افتد: خروجیِ سریال‌ساز روی همان پاسخ باید عینِ خودش باشد.
هنوز قرارداد Swagger است. اگر کسی کلیدی به پاسخ اضافه کند یا نوعی را عوض کند و سریال‌ساز را نه،
این تست می‌افتد: خروجی سریال‌ساز روی همان پاسخ باید عین خودش باشد.
"""
def test_view_output_is_exactly_the_documented_shape(self):