from datetime import date from django.core.exceptions import ValidationError as DjangoValidationError from django.db import transaction from django.utils import timezone from rest_framework import serializers from locations.models import Atoll, Island from .mobile import normalize_mobile from .models import RegistrationTicket, User MAX_AGE_YEARS = 120 class MobileField(serializers.CharField): """Accepts 7712345 / 9607712345 / +960 771 2345 and stores +9607712345.""" def to_internal_value(self, data): value = super().to_internal_value(data) try: return normalize_mobile(value) except DjangoValidationError as exc: raise serializers.ValidationError(exc.messages) from exc class AuthStartSerializer(serializers.Serializer): mobile = MobileField() class PasswordLoginSerializer(serializers.Serializer): mobile = MobileField() password = serializers.CharField(trim_whitespace=False, write_only=True) class OtpVerifySerializer(serializers.Serializer): mobile = MobileField() code = serializers.RegexField(r"^\d{6}$", write_only=True) class UserSerializer(serializers.ModelSerializer): is_admin = serializers.BooleanField(source="is_admin_user", read_only=True) has_password = serializers.SerializerMethodField() atoll_name = serializers.CharField(source="atoll.name", default=None, read_only=True) island_name = serializers.CharField( source="island.name", default=None, read_only=True ) class Meta: model = User fields = [ "id", "mobile", "full_name", "email", "idnumber", "date_of_birth", "atoll", "atoll_name", "island", "island_name", "auth_method", "status", "rejection_reason", "mobile_verified", "is_admin", "has_password", "date_joined", ] read_only_fields = fields def get_has_password(self, obj) -> bool: return obj.has_usable_password() class RegistrationSerializer(serializers.Serializer): """The registration form. The mobile number is not accepted from the client: it comes from the `registration_token`, which is only issued after that number confirmed an SMS code. So the number on the account is always one the applicant proved they control, and the form's prefilled field can't be tampered with. """ registration_token = serializers.CharField(write_only=True) full_name = serializers.CharField(max_length=255) idnumber = serializers.CharField(max_length=32) date_of_birth = serializers.DateField() atoll = serializers.PrimaryKeyRelatedField( queryset=Atoll.objects.filter(is_active=True) ) island = serializers.PrimaryKeyRelatedField( queryset=Island.objects.filter(is_active=True) ) terms_accepted = serializers.BooleanField() policy_accepted = serializers.BooleanField() def validate_registration_token(self, value): ticket = RegistrationTicket.objects.active().filter(key=value).first() if ticket is None: raise serializers.ValidationError( "Your number needs to be verified again." ) return ticket def validate_full_name(self, value): name = " ".join(value.split()) if len(name) < 3: raise serializers.ValidationError("Enter your full name.") return name def validate_idnumber(self, value): return value.strip().upper() def validate_date_of_birth(self, value): today = date.today() if value > today: raise serializers.ValidationError("Date of birth can't be in the future.") if value.year < today.year - MAX_AGE_YEARS: raise serializers.ValidationError("Enter a valid date of birth.") return value def validate_terms_accepted(self, value): if not value: raise serializers.ValidationError( "You must agree to the terms and conditions." ) return value def validate_policy_accepted(self, value): if not value: raise serializers.ValidationError( "You must confirm you understand the privacy policy." ) return value def validate(self, attrs): island = attrs["island"] if island.atoll_id != attrs["atoll"].pk: raise serializers.ValidationError( {"island": "That island isn't in the selected atoll."} ) ticket = attrs["registration_token"] if User.objects.filter(mobile=ticket.mobile).exists(): raise serializers.ValidationError( {"mobile": "An account already exists for this number."} ) return attrs @transaction.atomic def create(self, validated_data): ticket = validated_data["registration_token"] now = timezone.now() user = User.objects.create_user( mobile=ticket.mobile, full_name=validated_data["full_name"], idnumber=validated_data["idnumber"], date_of_birth=validated_data["date_of_birth"], atoll=validated_data["atoll"], island=validated_data["island"], auth_method=User.AuthMethod.OTP, status=User.Status.PENDING, mobile_verified=True, terms_accepted_at=now, policy_accepted_at=now, ) ticket.consume() return user