- 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>
95 lines
3.5 KiB
Python
95 lines
3.5 KiB
Python
"""
|
|
تبدیلِ تاریخِ شمسی ↔ میلادی برای تاریخِ تولد.
|
|
|
|
الگوریتمِ jalaali (همان jalaali-js، بورکوفسکی): کبیسهها از جدولِ نقطههای شکستِ
|
|
چرخه حساب میشوند، نه از قاعدهی سیوسهسالهی ساده، و برای سالهای ۱۱۷۸ تا ۱۶۳۳
|
|
با تقویمِ رسمی یکی است. تستها (`accounts/tests_birth_date.py`) چند تاریخِ
|
|
شناختهشدهی دو طرفِ اسفندِ کبیسه را میسنجند.
|
|
|
|
چرا در بکاند: فرانت فقط روز/ماه/سالِ شمسی میفرستد. اگر هر دو طرف تبدیل
|
|
میکردند، دو پیادهسازی میتوانست یک روز با هم اختلاف داشته باشد.
|
|
"""
|
|
from datetime import date
|
|
|
|
_BREAKS = [-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210,
|
|
1635, 2060, 2097, 2192, 2262, 2324, 2394, 2456, 3178]
|
|
|
|
|
|
def _cal(jy):
|
|
"""(leap, gy, march) — leap: سالِ مانده در چرخه؛ march: روزِ مارسِ نوروز."""
|
|
if jy < _BREAKS[0] or jy >= _BREAKS[-1]:
|
|
raise ValueError(f'سالِ شمسیِ خارج از بازه: {jy}')
|
|
gy = jy + 621
|
|
leap_j = -14
|
|
jp = _BREAKS[0]
|
|
jump = 0
|
|
for jm in _BREAKS[1:]:
|
|
jump = jm - jp
|
|
if jy < jm:
|
|
break
|
|
leap_j += (jump // 33) * 8 + (jump % 33) // 4
|
|
jp = jm
|
|
n = jy - jp
|
|
leap_j += (n // 33) * 8 + ((n % 33) + 3) // 4
|
|
if jump % 33 == 4 and jump - n == 4:
|
|
leap_j += 1
|
|
leap_g = gy // 4 - ((gy // 100 + 1) * 3) // 4 - 150
|
|
march = 20 + leap_j - leap_g
|
|
if jump - n < 6:
|
|
n = n - jump + ((jump + 4) // 33) * 33
|
|
leap = (((n + 1) % 33) - 1) % 4
|
|
if leap == -1:
|
|
leap = 4
|
|
return leap, gy, march
|
|
|
|
|
|
def is_leap(jy: int) -> bool:
|
|
return _cal(jy)[0] == 0
|
|
|
|
|
|
def month_length(jy: int, jm: int) -> int:
|
|
if jm <= 6:
|
|
return 31
|
|
if jm <= 11:
|
|
return 30
|
|
return 30 if is_leap(jy) else 29
|
|
|
|
|
|
def to_gregorian(jy: int, jm: int, jd: int) -> date:
|
|
"""ValueError برای تاریخِ ناموجود (مثلاً ۳۰ اسفندِ سالِ غیرکبیسه)."""
|
|
if not 1 <= jm <= 12 or not 1 <= jd <= month_length(jy, jm):
|
|
raise ValueError('چنین روزی در تقویمِ شمسی وجود ندارد.')
|
|
_, gy, march = _cal(jy)
|
|
nowruz = date(gy, 3, march)
|
|
day_of_year = (jm - 1) * 31 - (jm - 7) * (jm >= 7) + jd - 1
|
|
return date.fromordinal(nowruz.toordinal() + day_of_year)
|
|
|
|
|
|
def from_gregorian(d: date) -> tuple[int, int, int]:
|
|
jy = d.year - 621
|
|
_, gy, march = _cal(jy)
|
|
k = d.toordinal() - date(gy, 3, march).toordinal()
|
|
if k < 0:
|
|
jy -= 1
|
|
_, gy, march = _cal(jy)
|
|
k = d.toordinal() - date(gy, 3, march).toordinal()
|
|
if k < 186:
|
|
return jy, k // 31 + 1, k % 31 + 1
|
|
k -= 186
|
|
return jy, k // 30 + 7, k % 30 + 1
|
|
|
|
|
|
def parse(value: str) -> date:
|
|
"""«1385-04-12» (ارقامِ فارسی هم) → تاریخِ میلادی."""
|
|
from accounts.phone import _DIGITS
|
|
|
|
parts = str(value).translate(_DIGITS).strip().replace('/', '-').split('-')
|
|
if len(parts) != 3 or not all(p.isdigit() for p in parts):
|
|
raise ValueError('تاریخ باید به شکلِ سال-ماه-روز باشد، مثلاً 1385-04-12.')
|
|
return to_gregorian(int(parts[0]), int(parts[1]), int(parts[2]))
|
|
|
|
|
|
def to_string(d: date) -> str:
|
|
jy, jm, jd = from_gregorian(d)
|
|
return f'{jy:04d}-{jm:02d}-{jd:02d}'
|