171 lines
5.5 KiB
Python
171 lines
5.5 KiB
Python
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
|