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:
@@ -34,7 +34,7 @@ def release_db_connection() -> None:
|
||||
# هندلرِ خطای DRF (config/exceptions.py؛ هر خطای ناشناخته را خودش ۵۰۰ میکند و به میانافزار نمیرسد)
|
||||
# و DatabaseBusyMiddleware برای viewهای غیرِ DRF مثلِ /admin/.
|
||||
SERVER_BUSY_RETRY_AFTER = 5
|
||||
SERVER_BUSY_BODY = {'detail': 'سرور الان خیلی شلوغ است؛ چند ثانیهی دیگر دوباره امتحان کنید.', 'code': 'server_busy'}
|
||||
SERVER_BUSY_BODY = {'detail': 'سرور الان خیلی شلوغ است؛ چند ثانیه دیگر دوباره امتحان کنید.', 'code': 'server_busy'}
|
||||
|
||||
|
||||
def is_pool_timeout(exc) -> bool:
|
||||
|
||||
24
config/renderers.py
Normal file
24
config/renderers.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
JSONِ همهی پاسخهای API، با متنِ فارسیِ بیاعراب و بی «ی»ِ اضافه (`content.plain_math.plain_persian`).
|
||||
|
||||
چرا اینجا و نه در داده: محتوا (۱۸ هزار سوال، عنوانِ فصل/جزیره/درس) و پیامهای کد اعراب دارند. بازنویسیِ عنوانها
|
||||
در دیتابیس و seed اتصالِ سوال به درس را میشکند (سوالها با مسیرِ عنوان بسته میشوند و دفترِ SeedRecord با هشِ
|
||||
عنوان کلید میخورد). یک لایه در خروجی هر پیام و محتوایی را، تازه یا قدیمی، یکجا میگیرد؛ ادمین و دیتابیس همان
|
||||
متنِ اصلی را نگه میدارند. پاسخِ زندهی دستیار (SSE) از این renderer رد نمیشود و همین کار را در
|
||||
`rag.services.mathtext.to_plain` میکند.
|
||||
|
||||
هزینه: `plain_persian_utf8` را ببین؛ زیرِ بارِ سرور اندازه نشده.
|
||||
"""
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
|
||||
from content.plain_math import plain_persian_utf8
|
||||
|
||||
|
||||
class PlainPersianJSONRenderer(JSONRenderer):
|
||||
"""یک گذر روی بایتهای JSONِ نهایی، نه روی تکتکِ رشتههای داده (که روی کلِ curriculum ۴ برابرِ کندتر بود).
|
||||
ensure_ascii در DRF خاموش است (UNICODE_JSON پیشفرض)، پس حروفِ فارسی خودشان در خروجیاند، نه \\uXXXX؛
|
||||
`ApiOutputTests` همین را میسنجد."""
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
body = super().render(data, accepted_media_type, renderer_context)
|
||||
return plain_persian_utf8(body) if body else body
|
||||
@@ -545,7 +545,7 @@ REST_FRAMEWORK = {
|
||||
# BrowsableAPIRenderer فقط در dev؛ در پروداکشن هم overhead داره هم یک
|
||||
# مرورگرِ HTMLِ API رو در معرض دید میذاره.
|
||||
'DEFAULT_RENDERER_CLASSES': [
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
'config.renderers.PlainPersianJSONRenderer',
|
||||
*(['rest_framework.renderers.BrowsableAPIRenderer'] if DEBUG else []),
|
||||
],
|
||||
'DEFAULT_THROTTLE_CLASSES': [
|
||||
|
||||
49
config/tests_plain_persian.py
Normal file
49
config/tests_plain_persian.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
اعراب و «ی»ِ اضافه به کاربر نمیرسد (تصمیمِ شرکت، ۲۰۲۶-۱۰-۱۴): قاعده (`content.plain_math.plain_persian`) و لایهی
|
||||
خروجیِ API (`config.renderers.PlainPersianJSONRenderer`). دیتابیس متنِ اصلی را نگه میدارد.
|
||||
"""
|
||||
from django.core.cache import cache
|
||||
from django.test import SimpleTestCase, TestCase
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from content.models import Grade
|
||||
from content.plain_math import plain_persian, plain_persian_utf8
|
||||
|
||||
|
||||
class PlainPersianRuleTests(SimpleTestCase):
|
||||
def test_kasra_and_written_ezafe_ye_go(self):
|
||||
self.assertEqual(plain_persian('از پایهی سوم، ریاضیِ کتابِ خودت'), 'از پایه سوم، ریاضی کتاب خودت')
|
||||
|
||||
def test_fatha_damma_shadda_sukun_and_hamza_above_go(self):
|
||||
self.assertEqual(plain_persian('اَوّلُ خانهٔ مْ'), 'اول خانه م')
|
||||
|
||||
def test_tanwin_stays_because_it_is_spelling(self):
|
||||
self.assertEqual(plain_persian('قبلاً، موقتاً'), 'قبلاً، موقتاً')
|
||||
|
||||
def test_ye_that_is_part_of_a_word_stays(self):
|
||||
# «های» (نکره) و «هیی…» که حرفِ دیگری به «ی» چسبیده، اضافه نیستند.
|
||||
self.assertEqual(plain_persian('جزیرهای تازه'), 'جزیرهای تازه')
|
||||
self.assertEqual(plain_persian('خانهیمان'), 'خانهیمان')
|
||||
|
||||
def test_idempotent_and_non_strings_untouched(self):
|
||||
once = plain_persian('همهی جزیرههای پایهی هشتمِ')
|
||||
self.assertEqual(plain_persian(once), once)
|
||||
self.assertIsNone(plain_persian(None))
|
||||
self.assertEqual(plain_persian(5), 5)
|
||||
|
||||
def test_utf8_version_matches_the_text_version(self):
|
||||
sample = '{"t": "همهی جزیرهی", "u": "خانهیمان، قبلاً، اَوّلُ، پایهی ۸", "id": 12}'
|
||||
self.assertEqual(plain_persian_utf8(sample.encode()).decode(), plain_persian(sample))
|
||||
|
||||
|
||||
class ApiOutputTests(TestCase):
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
|
||||
def test_api_json_is_clean_while_the_database_keeps_the_original(self):
|
||||
Grade.objects.create(id=8, number=8, title='پایهی هشتمِ', icon='x', color='#000')
|
||||
body = APIClient().get('/api/grades').content.decode('utf-8')
|
||||
self.assertIn('پایه هشتم', body)
|
||||
self.assertNotIn('ِ', body)
|
||||
self.assertNotIn('هی', body)
|
||||
self.assertEqual(Grade.objects.get(id=8).title, 'پایهی هشتمِ')
|
||||
Reference in New Issue
Block a user