- remove the family app from the root (separate parent accounts, invite codes, parental settings); progress.0006 drops its tables, permissions and migration history, and Profile.parent_phone, the two family notification types and ?userId= on /api/progress/nodes go with it - GET /api/user/weekly-report: last 7 days next to the 7 before (estimated minutes, questions, accuracy, islands, tutor questions) for the "parent report" page - profile birthDate in Jalali (accounts/jalali.py), ages 5-99, birthYear derived from it - GET /api/payment/invoices; orders now snapshot list price and plan title - tutor answers only math: server pre-filter, scope rule in the prompt (tutor-v11), refusal marker replaced with fixed text; self-harm gets a help line instead - streamed answers now record token usage (it was always 0) - daily tutor quota: AI_DAILY_QUOTA_FREE (default 3) vs Plan.ai_daily_quota - delete_users management command for wiping test accounts across every app - docs/ai-cost-and-guardrails.md: measured cost per question and guardrail results Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
91 lines
4.1 KiB
Python
91 lines
4.1 KiB
Python
"""
|
||
تاریخِ تولدِ شمسی در پروفایل (۲۰۲۶-۱۰-۰۱). اپ روز/ماه/سالِ شمسی میفرستد؛ سرور
|
||
میلادی ذخیره میکند و birthYear را از آن میسازد. راههای شکستن: تاریخِ ناموجود
|
||
(۳۰ اسفندِ غیرکبیسه)، ارقامِ فارسی، سنِ خارج از بازه، و جدا افتادنِ سال از تاریخ.
|
||
"""
|
||
from datetime import date
|
||
|
||
from django.core.cache import cache
|
||
from django.test import SimpleTestCase, TestCase
|
||
from rest_framework.test import APIClient
|
||
|
||
from accounts import jalali
|
||
from accounts.authentication import issue_access_token
|
||
from accounts.models import Profile, User
|
||
|
||
|
||
class JalaliConversionTests(SimpleTestCase):
|
||
KNOWN = [
|
||
((1405, 1, 1), date(2026, 3, 21)),
|
||
((1403, 12, 30), date(2025, 3, 20)), # اسفندِ کبیسه
|
||
((1404, 12, 29), date(2026, 3, 20)), # آخرین روزِ سالِ غیرکبیسه
|
||
((1385, 4, 12), date(2006, 7, 3)),
|
||
((1367, 10, 11), date(1989, 1, 1)),
|
||
]
|
||
|
||
def test_known_dates_both_ways(self):
|
||
for jalali_date, gregorian in self.KNOWN:
|
||
self.assertEqual(jalali.to_gregorian(*jalali_date), gregorian)
|
||
self.assertEqual(jalali.from_gregorian(gregorian), jalali_date)
|
||
|
||
def test_thirtieth_of_esfand_exists_only_in_leap_years(self):
|
||
self.assertTrue(jalali.is_leap(1403))
|
||
self.assertFalse(jalali.is_leap(1404))
|
||
with self.assertRaises(ValueError):
|
||
jalali.to_gregorian(1404, 12, 30)
|
||
|
||
def test_every_day_of_a_century_round_trips(self):
|
||
start = date(1940, 1, 1).toordinal()
|
||
for offset in range(0, 36600, 7):
|
||
day = date.fromordinal(start + offset)
|
||
self.assertEqual(jalali.to_gregorian(*jalali.from_gregorian(day)), day)
|
||
|
||
def test_persian_digits_and_slashes_are_accepted(self):
|
||
self.assertEqual(jalali.parse('۱۳۸۵/۰۴/۱۲'), date(2006, 7, 3))
|
||
|
||
|
||
class ProfileBirthDateApiTests(TestCase):
|
||
def setUp(self):
|
||
cache.clear()
|
||
self.user = User.objects.create(device_id='birth-date-device', is_guest=True)
|
||
self.client = APIClient()
|
||
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {issue_access_token(self.user.id)}')
|
||
|
||
def patch(self, body):
|
||
return self.client.patch('/api/user/profile', body, format='json')
|
||
|
||
def test_date_is_stored_and_returned_in_jalali_with_its_year(self):
|
||
response = self.patch({'birthDate': '1385-04-12'})
|
||
self.assertEqual(response.status_code, 200, response.content)
|
||
self.assertEqual(response.json()['birthDate'], '1385-04-12')
|
||
self.assertEqual(response.json()['birthYear'], 1385)
|
||
self.assertEqual(Profile.objects.get(user=self.user).birth_date, date(2006, 7, 3))
|
||
self.assertEqual(self.client.get('/api/user/profile').json()['birthDate'], '1385-04-12')
|
||
|
||
def test_adults_are_accepted(self):
|
||
self.assertEqual(self.patch({'birthDate': '1360-01-15'}).status_code, 200)
|
||
|
||
def test_impossible_dates_are_rejected(self):
|
||
for bad in ('1404-12-30', '1385-13-01', '1385-07-31', '1385-4', 'دیروز'):
|
||
self.assertEqual(self.patch({'birthDate': bad}).status_code, 400, bad)
|
||
|
||
def test_future_and_too_young_or_old_are_rejected(self):
|
||
for bad in ('1410-01-01', '1403-01-01', '1300-01-01'):
|
||
self.assertEqual(self.patch({'birthDate': bad}).status_code, 400, bad)
|
||
|
||
def test_date_and_year_together_are_ambiguous(self):
|
||
self.assertEqual(self.patch({'birthDate': '1385-04-12', 'birthYear': 1390}).status_code, 400)
|
||
|
||
def test_a_different_year_alone_clears_the_old_date(self):
|
||
self.patch({'birthDate': '1385-04-12'})
|
||
data = self.patch({'birthYear': 1388}).json()
|
||
self.assertEqual((data['birthYear'], data['birthDate']), (1388, None))
|
||
|
||
def test_null_clears_the_date(self):
|
||
self.patch({'birthDate': '1385-04-12'})
|
||
data = self.patch({'birthDate': None}).json()
|
||
self.assertEqual((data['birthYear'], data['birthDate']), (None, None))
|
||
|
||
def test_parent_phone_is_gone(self):
|
||
self.assertNotIn('parentPhone', self.client.get('/api/user/profile').json())
|