tutor answers step by step in plain text, islands finish, and the audit's money and login holes are closed
Tutor (prompt tutor-v3-stepwise-plain): plain text instead of LaTeX/markdown, with rag/services/mathtext.py converting whatever still slips through; full step-by-step solutions with a check before the final answer; three tones by grade; the last 6 messages of the conversation go to the model and follow-ups are no longer served from another user's cache; book solutions the checker disputes are kept out of the context; a questionId gives the tutor the practice question and the student's wrong answer. /api/health reports when the AI credit runs out. rag_model_benchmark compares chat models on verified book problems (run on the server; credit ran out mid-run). Map: only lessons that have questions count toward finishing an island, lessonIds[0] / nextLessonId is always the next lesson to play, and a finished island stays finished. The Seyar characters (sigma/delta/pi) are seeded. Audit fixes, each reproduced before fixing: - the plan is stored on the order, so paying a monthly order after starting a yearly checkout no longer grants a year - opening checkout no longer demotes an active subscription; renewals extend from the current expiry; expired subscriptions are not reported active - one discount code per order and only before the gateway; a 100% code completes the purchase; lost Zarinpal confirmations are re-verified - the unused /api/payment/webhook "mark paid" endpoint is removed - XP counts only answers recorded on the server - 6-digit OTP, each code dies after 5 wrong guesses; POST /api/auth/logout - a guest who signs up keeps their account and progress (test phones excluded) - affiliate commission on the first real payment; guests excluded - parents with several children no longer get a 500 - SQLite runs transactions IMMEDIATE, since it ignores select_for_update - admin login is rate-limited; display names drop phone numbers and links; userQuestion is capped at 2000 characters - server-side heart enforcement exists behind HEARTS_ENFORCED (off until the frontend handles 403 no_hearts) Not done on purpose: rotating the server's secret key and moving to Postgres. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
96
content/tests_island_progress.py
Normal file
96
content/tests_island_progress.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
جزیرهها روی نقشه باید با بازی کردنِ درسها واقعاً تمام شوند (۲۰۲۶-۰۹-۲۵).
|
||||
|
||||
دو چیز با هم جلوی پیشرفت را گرفته بود:
|
||||
۱. جزیره فقط وقتی تمام میشد که *همهی* درسهایش تمام شده باشند، ولی ۵۸۵ درس
|
||||
هنوز سوال ندارند و اپ برایشان دکمهی شروع ندارد — ۱۸۱ جزیره از ۲۶۹.
|
||||
۲. فرانت با کلیک روی جزیره همیشه lessonIds[0] را باز میکرد، یعنی درسِ اول
|
||||
را بارها و بارها.
|
||||
"""
|
||||
from django.core.cache import cache
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from accounts.authentication import issue_access_token
|
||||
from accounts.models import User
|
||||
|
||||
from .models import Choice, Grade, Lesson, Node, Question, Unit
|
||||
|
||||
|
||||
class IslandProgressTests(TestCase):
|
||||
def setUp(self):
|
||||
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.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)
|
||||
self._lesson(self.second, 'ج', with_question=True)
|
||||
|
||||
user = User.objects.create(phone='09120000950', grade_id=grade.id)
|
||||
self.client = APIClient()
|
||||
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(user.id)}')
|
||||
|
||||
def tearDown(self):
|
||||
cache.clear()
|
||||
|
||||
def _lesson(self, node, title, *, with_question):
|
||||
lesson = Lesson.objects.create(node=node, title=title, intro_text='x', mascot_animation='x')
|
||||
if with_question:
|
||||
self._question(lesson)
|
||||
return lesson
|
||||
|
||||
def _question(self, lesson):
|
||||
question = Question.objects.create(lesson=lesson, type='multiple_choice', prompt='؟')
|
||||
Choice.objects.create(question=question, value='۱', is_correct=True)
|
||||
Choice.objects.create(question=question, value='۲', is_correct=False)
|
||||
|
||||
def _nodes(self):
|
||||
units = self.client.get('/api/home').json()['units']
|
||||
return {n['title']: n for n in units[0]['nodes']}
|
||||
|
||||
def _complete(self, lesson):
|
||||
response = self.client.post(f'/api/lessons/{lesson.id}/complete',
|
||||
{'correctCount': 1, 'totalQuestions': 1}, format='json')
|
||||
self.assertEqual(response.status_code, 200, response.content)
|
||||
|
||||
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._complete(self.a)
|
||||
first = self._nodes()['جزیرهی اول']
|
||||
# درسِ بیسوال رد میشود؛ lessonIds[0] همان است که فرانت باز میکند.
|
||||
self.assertEqual(first['nextLessonId'], self.b.id)
|
||||
self.assertEqual(first['lessonIds'][0], self.b.id)
|
||||
self.assertCountEqual(first['lessonIds'], [self.a.id, self.empty.id, self.b.id])
|
||||
|
||||
def test_a_lesson_without_questions_does_not_hold_the_island(self):
|
||||
self._complete(self.a)
|
||||
self._complete(self.b)
|
||||
nodes = self._nodes()
|
||||
self.assertEqual(nodes['جزیرهی اول']['status'], 'completed')
|
||||
self.assertEqual(nodes['جزیرهی دوم']['status'], 'unlocked')
|
||||
# همهی درسها تمام شدهاند: برای مرور، درسِ اول.
|
||||
self.assertEqual(nodes['جزیرهی اول']['nextLessonId'], self.a.id)
|
||||
|
||||
def test_progress_counts_only_lessons_that_can_be_played(self):
|
||||
self._complete(self.a)
|
||||
percent = self.client.get('/api/home').json()['stats']['progressPercent']
|
||||
self.assertAlmostEqual(percent, 33.33, places=2) # ۱ از ۳ درسِ سوالدار
|
||||
|
||||
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._question(self.empty) # حالا این درس هم بازیشدنی است
|
||||
nodes = self._nodes()
|
||||
# نقشهی کششده باطل شد: درسِ تازه پیشنهاد میشود…
|
||||
self.assertEqual(nodes['جزیرهی اول']['nextLessonId'], self.empty.id)
|
||||
# …ولی جزیرهی تمامشده باز و جزیرهی بعدی قفل نمیشود.
|
||||
self.assertEqual(nodes['جزیرهی اول']['status'], 'completed')
|
||||
self.assertEqual(nodes['جزیرهی دوم']['status'], 'unlocked')
|
||||
Reference in New Issue
Block a user