import re from rest_framework import serializers from .phone import normalize_phone # لینک، آیدیِ شبکه‌ی اجتماعی، و هر رشته‌ی ۵ رقمی یا بیشتر (شماره‌ی تلفن با # فاصله/خط‌تیره هم) — ارقامِ فارسی و عربی هم. _CONTACT = re.compile( r'(https?://\S+|www\.\S+|\S+\.(?:com|ir|org|net|me)\S*|@\w+' r'|(?:[0-9۰-۹٠-٩][\s\-.]?){5,})', re.IGNORECASE, ) def clean_display_name(value: str) -> str: return ' '.join(_CONTACT.sub(' ', value or '').split())[:30] class IranMobileField(serializers.CharField): """CharField که خروجی‌اش همیشه ‎09XXXXXXXXX‎ است.""" def __init__(self, **kwargs): kwargs.setdefault('max_length', 20) super().__init__(**kwargs) def to_internal_value(self, data): phone = normalize_phone(super().to_internal_value(data)) if phone is None: raise serializers.ValidationError('شماره‌ی موبایل معتبر نیست؛ مثلاً 09121234567.') return phone # ---- Request serializers ---- class GuestSessionRequestSerializer(serializers.Serializer): type = serializers.ChoiceField(choices=['guest']) deviceId = serializers.CharField(max_length=64) class LoginRequestSerializer(serializers.Serializer): phone = IranMobileField() otp = serializers.CharField(max_length=6) class RefreshRequestSerializer(serializers.Serializer): refreshToken = serializers.CharField() class SetGradeRequestSerializer(serializers.Serializer): gradeId = serializers.IntegerField() # همان بازه‌ای که پیش‌تر برای «سن» پذیرفته می‌شد. بزرگسال هم می‌آید (سینیور، # ۲۰۲۶-۱۰-۰۱): کسی که فقط یک مبحثِ ریاضی را می‌خواهد یاد بگیرد. MIN_AGE = 5 MAX_AGE = 99 class UpdateProfileRequestSerializer(serializers.Serializer): """ ورودی PATCH /api/user/profile — همه‌ی فیلدها اختیاری‌اند و فقط همان‌هایی که فرستاده شوند تغییر می‌کنند (partial update واقعی). `phone` و `isGuest` عمداً اینجا نیستند: شماره فقط از مسیرِ OTP عوض می‌شود و مهمان‌بودن را کاربر نباید بتواند خودش تغییر دهد. """ name = serializers.CharField(max_length=100, required=False, allow_blank=True) avatar = serializers.CharField(max_length=100, required=False, allow_blank=True) def validate_name(self, value): """ نام در جدولِ رتبه‌بندی به بچه‌های دیگر نشان داده می‌شود؛ شماره، لینک و آیدی (راهِ تماس گرفتن با یک کودکِ غریبه) از آن حذف می‌شود. عمداً خطا نمی‌دهد تا صفحه‌ی پروفایلِ اپ گیر نکند؛ فقط بقیه‌ی نام ذخیره می‌شود. """ return clean_display_name(value) # فقط سال (روشِ قدیمِ اپ). birthDate دقیق‌تر است و ترجیح دارد. birthYear = serializers.IntegerField(required=False, allow_null=True, min_value=1300, max_value=1500) # تاریخِ تولدِ شمسی «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) 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('حداقل یکی از فیلدها باید ارسال شود.') if 'birthDate' in attrs and 'birthYear' in attrs: raise serializers.ValidationError('یکی از birthDate یا birthYear را بفرستید، نه هر دو را.') return attrs class RequestOTPRequestSerializer(serializers.Serializer): """اپ کمکی برای گرفتن OTP (در سند اصلی نبود، برای تست‌پذیری Swagger اضافه شد).""" phone = IranMobileField() # ---- Response serializers ---- class GuestSessionResponseSerializer(serializers.Serializer): userId = serializers.CharField() token = serializers.CharField() refreshToken = serializers.CharField() isNewUser = serializers.BooleanField() isGuest = serializers.BooleanField() class LoginResponseSerializer(serializers.Serializer): userId = serializers.CharField() token = serializers.CharField() refreshToken = serializers.CharField() class RefreshResponseSerializer(serializers.Serializer): token = serializers.CharField() refreshToken = serializers.CharField() expiresIn = serializers.IntegerField() class GradeSerializer(serializers.Serializer): id = serializers.IntegerField() title = serializers.CharField() icon = serializers.CharField() color = serializers.CharField() class GradesListResponseSerializer(serializers.Serializer): grades = GradeSerializer(many=True) class SetGradeResponseSerializer(serializers.Serializer): success = serializers.BooleanField() gradeId = serializers.IntegerField() class RequestOTPResponseSerializer(serializers.Serializer): success = serializers.BooleanField() devOtp = serializers.CharField() note = serializers.CharField() class ProfileResponseSerializer(serializers.Serializer): """پاسخ GET /api/user/profile — فقط داده‌ی لازم برای تب Profile اپ موبایل.""" userId = serializers.CharField() # شناسه‌ی نمایشی («User482917»، accounts/public_id.py)؛ اپ آن را زیرِ نام نشان می‌دهد # و پشتیبانی با همین در پنلِ ادمین حساب را پیدا می‌کند. publicId = serializers.CharField() phone = serializers.CharField(allow_null=True) name = serializers.CharField() avatar = serializers.CharField() isGuest = serializers.BooleanField() gradeId = serializers.IntegerField(allow_null=True) birthYear = serializers.IntegerField(allow_null=True) birthDate = serializers.CharField(allow_null=True, help_text='شمسی، «1385-04-12»؛ null اگر فقط سال داده شده.') bio = serializers.CharField() class DeleteAccountRequestSerializer(serializers.Serializer): # کاربرِ شماره‌دار: کدِ پیامکیِ تازه (از POST /api/auth/otp/request با همان شماره). otp = serializers.CharField(max_length=6, required=False, allow_blank=True) # مهمان شماره‌ای ندارد که کد بگیرد؛ باید همین کلمه را تایپ کرده باشد. confirm = serializers.CharField(max_length=10, required=False, allow_blank=True) class DeleteAccountResponseSerializer(serializers.Serializer): success = serializers.BooleanField() class LogoutRequestSerializer(serializers.Serializer): refreshToken = serializers.CharField(required=False, allow_blank=True) class LogoutResponseSerializer(serializers.Serializer): success = serializers.BooleanField()