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:
@@ -133,3 +133,30 @@ def convert_figures(obj, _key=None):
|
||||
if isinstance(obj, dict):
|
||||
return {k: convert_figures(v, k) for k, v in obj.items()}
|
||||
return obj
|
||||
|
||||
|
||||
# اعراب (کسره، فتحه، ضمه، تشدید، سکون، همزهی رویِ «ه») و «ی»ِ نوشتهشدهی اضافه («پایهی سوم» → «پایه سوم»):
|
||||
# تصمیمِ شرکت (۲۰۲۶-۱۰-۱۴) این است که هیچکدام به کاربر نرسد. تنوین («قبلاً») عمداً میماند: جزئی از املای
|
||||
# معیار است و بیآن غلطِ تایپی به نظر میرسد. «هی» فقط وقتی برداشته میشود که بعدش حرفِ فارسیِ دیگری نچسبیده باشد.
|
||||
_FA_DIACRITICS = re.compile('[\u064C-\u0652\u0654]')
|
||||
# «ه» + نیمفاصله + «ی» که بعدش حرفِ فارسی یا نیمفاصلهی دیگری نیست؛ با «ه» شروع میشود (نه lookbehind) تا موتورِ regex
|
||||
# با جستجوی سریعِ پیشوند جلو برود.
|
||||
_FA_EZAFE_YE = re.compile('\u0647\u200C\u06CC(?![\u0600-\u06FF\u200C])')
|
||||
# همان دو قاعده روی بایتهای UTF-8 (برای متنِ کاملِ پاسخِ JSON): اعراب D9 8C..D9 92 و D9 94 است، و D9 هرگز بایتِ
|
||||
# ادامه نیست، پس با تکهای از نویسهی دیگر یکی نمیشود. حروفِ فارسی/عربی D8..DB و نیمفاصله E2 80 8C.
|
||||
_FA_DIACRITICS_B = re.compile(rb'\xd9[\x8c-\x92\x94]')
|
||||
_FA_EZAFE_YE_B = re.compile(rb'\xd9\x87\xe2\x80\x8c\xdb\x8c(?![\xd8-\xdb]|\xe2\x80\x8c)')
|
||||
|
||||
|
||||
def plain_persian(text):
|
||||
"""متنِ فارسی بیاعراب و بی «ی»ِ اضافه؛ غیرِ رشته دستنخورده. idempotent."""
|
||||
if not isinstance(text, str) or text.isascii():
|
||||
return text
|
||||
return _FA_EZAFE_YE.sub('\u0647', _FA_DIACRITICS.sub('', text))
|
||||
|
||||
|
||||
def plain_persian_utf8(body: bytes) -> bytes:
|
||||
"""plain_persian روی بایتهای UTF-8، بی decode. روی متنِ کاملِ یک پاسخِ JSON درست است: هیچکدام از این نویسهها
|
||||
جزئی از نحوِ JSON نیستند. روی curriculum.jsonِ محلی (۵ مگابایت JSON) حدودِ ۲۷ میلیثانیه، در برابرِ ۵۷ برای خودِ
|
||||
JSONRenderer؛ پاسخِ معمولِ /api/home چند ده کیلوبایت است."""
|
||||
return _FA_EZAFE_YE_B.sub(b'\xd9\x87', _FA_DIACRITICS_B.sub(b'', body))
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""
|
||||
جزیرهها روی نقشه باید با بازی کردنِ درسها واقعاً تمام شوند (۲۰۲۶-۰۹-۲۵).
|
||||
جزیرهها روی نقشه باید با بازی کردن درسها واقعاً تمام شوند (۲۰۲۶-۰۹-۲۵).
|
||||
|
||||
دو چیز با هم جلوی پیشرفت را گرفته بود:
|
||||
۱. جزیره فقط وقتی تمام میشد که *همهی* درسهایش تمام شده باشند، ولی ۵۸۵ درس
|
||||
هنوز سوال ندارند و اپ برایشان دکمهی شروع ندارد — ۱۸۱ جزیره از ۲۶۹.
|
||||
۲. فرانت با کلیک روی جزیره همیشه lessonIds[0] را باز میکرد، یعنی درسِ اول
|
||||
۱. جزیره فقط وقتی تمام میشد که *همه* درسهایش تمام شده باشند، ولی ۵۸۵ درس
|
||||
هنوز سوال ندارند و اپ برایشان دکمه شروع ندارد — ۱۸۱ جزیره از ۲۶۹.
|
||||
۲. فرانت با کلیک روی جزیره همیشه lessonIds[0] را باز میکرد، یعنی درس اول
|
||||
را بارها و بارها.
|
||||
"""
|
||||
from django.core.cache import cache
|
||||
@@ -23,8 +23,8 @@ class IslandProgressTests(TestCase):
|
||||
cache.clear()
|
||||
grade = Grade.objects.create(title='پایه نهم', icon='g', color='#fff')
|
||||
unit = Unit.objects.create(grade=grade, title='فصل ۱', order=1)
|
||||
self.first = Node.objects.create(unit=unit, title='جزیرهی اول', order=1)
|
||||
self.second = Node.objects.create(unit=unit, title='جزیرهی دوم', order=2)
|
||||
self.first = Node.objects.create(unit=unit, title='جزیره اول', order=1)
|
||||
self.second = Node.objects.create(unit=unit, title='جزیره دوم', order=2)
|
||||
self.a = self._lesson(self.first, 'الف', with_question=True)
|
||||
self.empty = self._lesson(self.first, 'بیسوال', with_question=False)
|
||||
self.b = self._lesson(self.first, 'ب', with_question=True)
|
||||
@@ -60,11 +60,11 @@ class IslandProgressTests(TestCase):
|
||||
|
||||
def test_tapping_the_island_opens_the_next_unfinished_lesson(self):
|
||||
nodes = self._nodes()
|
||||
self.assertEqual(nodes['جزیرهی اول']['nextLessonId'], self.a.id)
|
||||
self.assertEqual(nodes['جزیرهی اول']['lessonIds'][0], self.a.id)
|
||||
self.assertEqual(nodes['جزیره اول']['nextLessonId'], self.a.id)
|
||||
self.assertEqual(nodes['جزیره اول']['lessonIds'][0], self.a.id)
|
||||
|
||||
self._complete(self.a)
|
||||
first = self._nodes()['جزیرهی اول']
|
||||
first = self._nodes()['جزیره اول']
|
||||
# درسِ بیسوال رد میشود؛ lessonIds[0] همان است که فرانت باز میکند.
|
||||
self.assertEqual(first['nextLessonId'], self.b.id)
|
||||
self.assertEqual(first['lessonIds'][0], self.b.id)
|
||||
@@ -74,10 +74,10 @@ class IslandProgressTests(TestCase):
|
||||
self._complete(self.a)
|
||||
self._complete(self.b)
|
||||
nodes = self._nodes()
|
||||
self.assertEqual(nodes['جزیرهی اول']['status'], 'completed')
|
||||
self.assertEqual(nodes['جزیرهی دوم']['status'], 'unlocked')
|
||||
self.assertEqual(nodes['جزیره اول']['status'], 'completed')
|
||||
self.assertEqual(nodes['جزیره دوم']['status'], 'unlocked')
|
||||
# همهی درسها تمام شدهاند: برای مرور، درسِ اول.
|
||||
self.assertEqual(nodes['جزیرهی اول']['nextLessonId'], self.a.id)
|
||||
self.assertEqual(nodes['جزیره اول']['nextLessonId'], self.a.id)
|
||||
|
||||
def test_progress_counts_only_lessons_that_can_be_played(self):
|
||||
self._complete(self.a)
|
||||
@@ -87,12 +87,12 @@ class IslandProgressTests(TestCase):
|
||||
def test_new_content_is_offered_but_progress_never_goes_backwards(self):
|
||||
self._complete(self.a)
|
||||
self._complete(self.b)
|
||||
self.assertEqual(self._nodes()['جزیرهی اول']['status'], 'completed')
|
||||
self.assertEqual(self._nodes()['جزیره اول']['status'], 'completed')
|
||||
|
||||
self._question(self.empty) # حالا این درس هم بازیشدنی است
|
||||
nodes = self._nodes()
|
||||
# نقشهی کششده باطل شد: درسِ تازه پیشنهاد میشود…
|
||||
self.assertEqual(nodes['جزیرهی اول']['nextLessonId'], self.empty.id)
|
||||
self.assertEqual(nodes['جزیره اول']['nextLessonId'], self.empty.id)
|
||||
# …ولی جزیرهی تمامشده باز و جزیرهی بعدی قفل نمیشود.
|
||||
self.assertEqual(nodes['جزیرهی اول']['status'], 'completed')
|
||||
self.assertEqual(nodes['جزیرهی دوم']['status'], 'unlocked')
|
||||
self.assertEqual(nodes['جزیره اول']['status'], 'completed')
|
||||
self.assertEqual(nodes['جزیره دوم']['status'], 'unlocked')
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""
|
||||
پایانِ آزمونِ جزیره → جزیرهی بعدی باز میشود.
|
||||
پایان آزمون جزیره → جزیره بعدی باز میشود.
|
||||
|
||||
شکافی که این تستها میبندند: اپ با آزمونِ چهارسوالیِ هر جزیره بازی
|
||||
میکند، ولی جزیره فقط وقتی «تمام» حساب میشد که تکتکِ درسهایش (۸ تا ۱۳
|
||||
درس) جدا تمام شده باشند؛ دانشآموز بعد از جزیرهی اولِ هر فصل گیر میکرد.
|
||||
شکافی که این تستها میبندند: اپ با آزمون چهارسوالی هر جزیره بازی
|
||||
میکند، ولی جزیره فقط وقتی «تمام» حساب میشد که تکتک درسهایش (۸ تا ۱۳
|
||||
درس) جدا تمام شده باشند؛ دانشآموز بعد از جزیره اول هر فصل گیر میکرد.
|
||||
"""
|
||||
from subscription.models import Subscription
|
||||
from django.core.cache import cache
|
||||
@@ -28,7 +28,7 @@ class NodeQuizCompleteTests(TestCase):
|
||||
self.next_unit = Unit.objects.create(grade=self.grade, title='فصل ۲', order=2)
|
||||
self.first = self._node(self.unit, 'اول', 1, lessons=5)
|
||||
self.second = self._node(self.unit, 'دوم', 2, lessons=3)
|
||||
self.other_unit_first = self._node(self.next_unit, 'فصلِ بعد', 1, lessons=2)
|
||||
self.other_unit_first = self._node(self.next_unit, 'فصل بعد', 1, lessons=2)
|
||||
|
||||
self.user = User.objects.create(phone='09120000009', grade_id=self.grade.id)
|
||||
# فصلِ دوم اشتراک میخواهد (content.views.unit_is_free)؛ این تست ترتیبِ باز شدن را میسنجد، نه پرداخت.
|
||||
@@ -46,7 +46,7 @@ class NodeQuizCompleteTests(TestCase):
|
||||
return node
|
||||
|
||||
def _answer(self, node, correct, wrong=0):
|
||||
"""مثلِ اپ: اول جوابها ثبت میشوند (`correct` تای اول درست، `wrong` تای بعدی غلط)، بعد پایانِ آزمون."""
|
||||
"""مثل اپ: اول جوابها ثبت میشوند (`correct` تای اول درست، `wrong` تای بعدی غلط)، بعد پایان آزمون."""
|
||||
questions = list(Question.objects.filter(lesson__node=node).order_by('id')[:correct + wrong])
|
||||
for index, question in enumerate(questions):
|
||||
choice = question.choices.get(is_correct=index < correct)
|
||||
@@ -122,7 +122,7 @@ class NodeQuizCompleteTests(TestCase):
|
||||
self.assertEqual(body['xpEarned'], 20)
|
||||
|
||||
def test_understating_the_question_count_does_not_buy_stars(self):
|
||||
"""با totalQuestions=1 و یک جوابِ درست ۳ ستاره میشد؛ آزمونِ استانداردِ سرور ۴ سوال است."""
|
||||
"""با totalQuestions=1 و یک جواب درست ۳ ستاره میشد؛ آزمون استاندارد سرور ۴ سوال است."""
|
||||
self._answer(self.first, correct=1, wrong=3)
|
||||
body = self._complete(self.first, correct=1, total=1, answer=False).json()
|
||||
self.assertEqual(body['stars'], 1) # ۱ از ۴
|
||||
@@ -135,7 +135,7 @@ class NodeQuizCompleteTests(TestCase):
|
||||
self.assertEqual((response.json()['answered'], response.json()['required']), (3, 4))
|
||||
|
||||
def test_a_locked_island_cannot_be_finished(self):
|
||||
"""جزیرهی دوم تا اولی تمام نشده قفل است؛ حتی با جوابِ همهی سوالهایش."""
|
||||
"""جزیره دوم تا اولی تمام نشده قفل است؛ حتی با جواب همه سوالهایش."""
|
||||
self._answer(self.second, correct=3)
|
||||
response = self._complete(self.second, correct=3, total=3, answer=False)
|
||||
self.assertEqual((response.status_code, response.json()['code']), (403, 'node_locked'))
|
||||
@@ -143,14 +143,14 @@ class NodeQuizCompleteTests(TestCase):
|
||||
self.assertEqual(self._statuses()['دوم'], 'locked')
|
||||
|
||||
def test_the_first_island_of_every_unit_is_open(self):
|
||||
"""همانطور که نقشه نشان میدهد: اولین جزیرهی فصلها بیقید باز است."""
|
||||
self.assertEqual(self._statuses()['فصلِ بعد'], 'unlocked')
|
||||
"""همانطور که نقشه نشان میدهد: اولین جزیره فصلها بیقید باز است."""
|
||||
self.assertEqual(self._statuses()['فصل بعد'], 'unlocked')
|
||||
self.assertEqual(self._complete(self.other_unit_first, correct=2, total=2).status_code, 200)
|
||||
|
||||
def test_an_open_island_without_questions_passes_without_answers_and_earns_nothing(self):
|
||||
"""چیزی برای جوابدادن نیست؛ رد میشود تا مسیرِ بقیه را نبندد (بیستاره، بیامتیاز)."""
|
||||
"""چیزی برای جوابدادن نیست؛ رد میشود تا مسیر بقیه را نبندد (بیستاره، بیامتیاز)."""
|
||||
empty = Node.objects.create(unit=self.next_unit, title='خالی', order=0) # اولین جزیرهی فصل: باز
|
||||
Lesson.objects.create(node=empty, title='درسِ بیسوال')
|
||||
Lesson.objects.create(node=empty, title='درس بیسوال')
|
||||
response = self.client.post(
|
||||
f'/api/nodes/{empty.pk}/quiz/complete', {'correctCount': 0, 'totalQuestions': 1},
|
||||
content_type='application/json', **self.auth)
|
||||
@@ -159,8 +159,8 @@ class NodeQuizCompleteTests(TestCase):
|
||||
self.assertEqual(response.json()['nextNodeId'], str(self.other_unit_first.pk))
|
||||
|
||||
def test_a_locked_island_without_questions_is_still_locked(self):
|
||||
empty = Node.objects.create(unit=self.unit, title='خالیِ قفل', order=9) # بعد از دو جزیرهی تمامنشده
|
||||
Lesson.objects.create(node=empty, title='درسِ بیسوال')
|
||||
empty = Node.objects.create(unit=self.unit, title='خالی قفل', order=9) # بعد از دو جزیرهی تمامنشده
|
||||
Lesson.objects.create(node=empty, title='درس بیسوال')
|
||||
response = self.client.post(
|
||||
f'/api/nodes/{empty.pk}/quiz/complete', {'correctCount': 0, 'totalQuestions': 1},
|
||||
content_type='application/json', **self.auth)
|
||||
@@ -197,7 +197,7 @@ class NodeQuizCompleteTests(TestCase):
|
||||
self.assertIn('اول', notes.first().body)
|
||||
|
||||
def test_node_progress_is_completed_and_stays_completed(self):
|
||||
"""درسِ تازه در همان جزیره نباید آن را به in_progress برگرداند."""
|
||||
"""درس تازه در همان جزیره نباید آن را به in_progress برگرداند."""
|
||||
self._complete(self.first)
|
||||
lesson = self.first.lessons.first()
|
||||
record_answers(self.user.id, lesson=lesson)
|
||||
@@ -211,7 +211,7 @@ class NodeQuizCompleteTests(TestCase):
|
||||
self.assertEqual(progress.status, 'completed')
|
||||
|
||||
def test_progress_percent_counts_the_islands_lessons(self):
|
||||
"""جزیرهی ۵ درسی از ۱۰ درسِ پایه → ۵۰٪، و فقط در پایهی خودِ کاربر."""
|
||||
"""جزیره ۵ درسی از ۱۰ درس پایه → ۵۰٪، و فقط در پایه خود کاربر."""
|
||||
Grade.objects.create(title='پایه دهم', icon='', color='') # مخرج را بزرگ نکند
|
||||
body = self._complete(self.first).json()
|
||||
|
||||
@@ -234,7 +234,7 @@ class NodeQuizCompleteTests(TestCase):
|
||||
|
||||
|
||||
class NodeQuizStabilityTests(TestCase):
|
||||
"""آزمونِ یک جزیره برای یک کاربر ثابت میماند، حتی بعد از تمامکردنِ آزمون و حتی با استخرِ بزرگ."""
|
||||
"""آزمون یک جزیره برای یک کاربر ثابت میماند، حتی بعد از تمامکردن آزمون و حتی با استخر بزرگ."""
|
||||
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
|
||||
Reference in New Issue
Block a user