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() 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 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) 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) 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(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 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() 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) parentPhone = serializers.CharField() bio = serializers.CharField() class LogoutRequestSerializer(serializers.Serializer): refreshToken = serializers.CharField(required=False, allow_blank=True) class LogoutResponseSerializer(serializers.Serializer): success = serializers.BooleanField()