senior's post-demo changes: parent report in the student's own account, Jalali birth date, invoices, math-only tutor, priced quotas
- 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>
This commit is contained in:
@@ -51,16 +51,10 @@ class SetGradeRequestSerializer(serializers.Serializer):
|
||||
gradeId = serializers.IntegerField()
|
||||
|
||||
|
||||
def _jalali_year_now() -> int:
|
||||
"""
|
||||
سالِ جاریِ شمسی. تبدیلِ سن به سالِ تولد بدونِ این، سالِ میلادی میداد و
|
||||
اعتبارسنجیِ birthYear (بازهی ۱۳۰۰ تا ۱۵۰۰) ردش میکرد.
|
||||
"""
|
||||
from django.utils import timezone
|
||||
|
||||
today = timezone.localdate()
|
||||
# فروردین ۱ برابرِ ۲۱ مارس است؛ پیش از آن هنوز سالِ شمسیِ قبلی است.
|
||||
return today.year - 622 if (today.month, today.day) >= (3, 21) else today.year - 623
|
||||
# همان بازهای که پیشتر برای «سن» پذیرفته میشد. بزرگسال هم میآید (سینیور،
|
||||
# ۲۰۲۶-۱۰-۰۱): کسی که فقط یک مبحثِ ریاضی را میخواهد یاد بگیرد.
|
||||
MIN_AGE = 5
|
||||
MAX_AGE = 99
|
||||
|
||||
|
||||
class UpdateProfileRequestSerializer(serializers.Serializer):
|
||||
@@ -81,27 +75,36 @@ class UpdateProfileRequestSerializer(serializers.Serializer):
|
||||
نمیدهد تا صفحهی پروفایلِ اپ گیر نکند؛ فقط بقیهی نام ذخیره میشود.
|
||||
"""
|
||||
return clean_display_name(value)
|
||||
# فقط سال (روشِ قدیمِ اپ). birthDate دقیقتر است و ترجیح دارد.
|
||||
birthYear = serializers.IntegerField(required=False, allow_null=True,
|
||||
min_value=1300, max_value=1500)
|
||||
parentPhone = serializers.CharField(max_length=15, required=False, allow_blank=True)
|
||||
# تاریخِ تولدِ شمسی «1385-04-12». سرور میلادی ذخیره میکند و birthYear را هم
|
||||
# از آن پر میکند؛ تبدیل فقط در accounts/jalali.py است.
|
||||
birthDate = serializers.CharField(required=False, allow_null=True, max_length=10)
|
||||
bio = serializers.CharField(max_length=200, required=False, allow_blank=True)
|
||||
# صفحهی ثبتنامِ فرانت سن میپرسد، نه سالِ تولد. ذخیرهسازی همچنان
|
||||
# birthYear است چون کهنه نمیشود؛ این فقط ورودی را ترجمه میکند تا فرانت
|
||||
# مجبور نباشد خودش تبدیل کند و دو طرف حسابِ متفاوت درنیاورند.
|
||||
age = serializers.IntegerField(required=False, allow_null=True,
|
||||
min_value=5, max_value=99, write_only=True)
|
||||
|
||||
def validate_birthDate(self, value):
|
||||
if value in (None, ''):
|
||||
return None
|
||||
from django.utils import timezone
|
||||
|
||||
from . import jalali
|
||||
|
||||
try:
|
||||
born = jalali.parse(value)
|
||||
except ValueError as exc:
|
||||
raise serializers.ValidationError(str(exc))
|
||||
today = timezone.localdate()
|
||||
age = today.year - born.year - ((today.month, today.day) < (born.month, born.day))
|
||||
if not MIN_AGE <= age <= MAX_AGE:
|
||||
raise serializers.ValidationError(f'سن باید بینِ {MIN_AGE} و {MAX_AGE} سال باشد.')
|
||||
return born
|
||||
|
||||
def validate(self, attrs):
|
||||
if not attrs:
|
||||
raise serializers.ValidationError('حداقل یکی از فیلدها باید ارسال شود.')
|
||||
|
||||
age = attrs.pop('age', None)
|
||||
if age is not None:
|
||||
if attrs.get('birthYear') is not None:
|
||||
raise serializers.ValidationError(
|
||||
'یکی از age یا birthYear را بفرستید، نه هر دو را.',
|
||||
)
|
||||
attrs['birthYear'] = _jalali_year_now() - age
|
||||
if 'birthDate' in attrs and 'birthYear' in attrs:
|
||||
raise serializers.ValidationError('یکی از birthDate یا birthYear را بفرستید، نه هر دو را.')
|
||||
return attrs
|
||||
|
||||
|
||||
@@ -156,6 +159,8 @@ class RequestOTPResponseSerializer(serializers.Serializer):
|
||||
|
||||
class ProfileResponseSerializer(serializers.Serializer):
|
||||
"""پاسخ GET /api/user/profile — فقط دادهی لازم برای تب Profile اپ موبایل."""
|
||||
# شناسهی کاربر همان شمارهی ثبتِ اوست (کاربرِ چندم)؛ اپ آن را زیرِ نام نشان میدهد
|
||||
# تا در پشتیبانی بشود حساب را بیشمارهی تلفن پیدا کرد.
|
||||
userId = serializers.CharField()
|
||||
phone = serializers.CharField(allow_null=True)
|
||||
name = serializers.CharField()
|
||||
@@ -163,7 +168,7 @@ class ProfileResponseSerializer(serializers.Serializer):
|
||||
isGuest = serializers.BooleanField()
|
||||
gradeId = serializers.IntegerField(allow_null=True)
|
||||
birthYear = serializers.IntegerField(allow_null=True)
|
||||
parentPhone = serializers.CharField()
|
||||
birthDate = serializers.CharField(allow_null=True, help_text='شمسی، «1385-04-12»؛ null اگر فقط سال داده شده.')
|
||||
bio = serializers.CharField()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user