You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

493 lines
18 KiB

from django.contrib.auth import get_user_model
from django.contrib.auth.password_validation import validate_password
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from phonenumber_field.serializerfields import PhoneNumberField
from drf_spectacular.utils import extend_schema_field, OpenApiTypes
from apps.account.models.verification import VerificationDocument
User = get_user_model()
class RegisterRequestSerializer(serializers.Serializer):
email = serializers.EmailField(required=True)
password = serializers.CharField(write_only=True, required=True, min_length=6)
first_name = serializers.CharField(required=False, allow_blank=True, max_length=150)
last_name = serializers.CharField(required=False, allow_blank=True, max_length=150)
fullname = serializers.CharField(required=False, allow_blank=True, max_length=255)
phone_number = serializers.CharField(required=False, allow_blank=True, allow_null=True)
user_type = serializers.ChoiceField(
choices=User.UserType.choices,
default=User.UserType.CLIENT,
required=False
)
languages = serializers.ListField(
child=serializers.CharField(max_length=10),
required=False,
default=list
)
skills = serializers.ListField(
child=serializers.CharField(max_length=100),
required=False,
default=list
)
country = serializers.CharField(required=False, allow_blank=True, max_length=255)
city = serializers.CharField(required=False, allow_blank=True, max_length=255)
def validate_email(self, value):
normalized = value.strip().lower()
if User.objects.filter(email__iexact=normalized).exists():
raise serializers.ValidationError("A user with this email address already exists.")
return normalized
def validate_password(self, value):
validate_password(value)
return value
def validate(self, attrs):
fn = attrs.get('first_name', '').strip()
ln = attrs.get('last_name', '').strip()
fl = attrs.get('fullname', '').strip()
if fn or ln:
attrs['first_name'] = fn
attrs['last_name'] = ln
if not fl:
attrs['fullname'] = f"{fn} {ln}".strip()
else:
attrs['fullname'] = fl
elif fl:
parts = fl.split(maxsplit=1)
attrs['first_name'] = parts[0]
attrs['last_name'] = parts[1] if len(parts) > 1 else ""
attrs['fullname'] = fl
else:
raise serializers.ValidationError({"first_name": "Please provide first_name and last_name (or full name)."})
return attrs
def create(self, validated_data):
email = validated_data.pop('email')
password = validated_data.pop('password')
first_name = validated_data.pop('first_name', '')
last_name = validated_data.pop('last_name', '')
fullname = validated_data.pop('fullname', '')
user_type = validated_data.pop('user_type', User.UserType.CLIENT)
user = User.objects.create_user(
email=email,
password=password,
first_name=first_name,
last_name=last_name,
fullname=fullname,
username=email,
user_type=user_type,
**validated_data
)
return user
class UserMeSerializer(serializers.ModelSerializer):
bio = serializers.CharField(source='info', allow_blank=True, required=False, allow_null=True)
roles = serializers.SerializerMethodField()
permissions = serializers.SerializerMethodField()
managed_regions = serializers.SerializerMethodField()
regional_permissions = serializers.ReadOnlyField()
verification_status = serializers.SerializerMethodField()
is_verified = serializers.SerializerMethodField()
first_name = serializers.SerializerMethodField()
last_name = serializers.SerializerMethodField()
name = serializers.SerializerMethodField()
phone = serializers.SerializerMethodField()
preferred_language = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
rank = serializers.SerializerMethodField()
website = serializers.SerializerMethodField()
address = serializers.SerializerMethodField()
position = serializers.SerializerMethodField()
institution = serializers.SerializerMethodField()
institution_id = serializers.SerializerMethodField()
class Meta:
model = User
fields = [
'id',
'email',
'username',
'fullname',
'first_name',
'last_name',
'name',
'phone_number',
'phone',
'user_type',
'avatar',
'bio',
'languages',
'preferred_language',
'skills',
'title',
'rank',
'position',
'institution',
'institution_id',
'website',
'address',
'country',
'city',
'gender',
'birthdate',
'date_joined',
'is_active',
'is_staff',
'roles',
'permissions',
'managed_regions',
'regional_permissions',
'verification_status',
'is_verified',
]
read_only_fields = [
'id',
'email',
'username',
'first_name',
'last_name',
'name',
'phone',
'preferred_language',
'title',
'rank',
'position',
'institution',
'institution_id',
'website',
'address',
'user_type',
'date_joined',
'is_active',
'is_staff',
'roles',
'permissions',
'managed_regions',
'regional_permissions',
'verification_status',
'is_verified',
]
def get_first_name(self, obj) -> str:
if obj.first_name:
return obj.first_name
if obj.fullname:
return obj.fullname.strip().split()[0]
return ""
def get_last_name(self, obj) -> str:
if obj.last_name:
return obj.last_name
if obj.fullname:
parts = obj.fullname.strip().split()
if len(parts) > 1:
return " ".join(parts[1:])
return ""
def get_name(self, obj) -> str:
return obj.fullname or f"{obj.first_name or ''} {obj.last_name or ''}".strip()
def get_phone(self, obj) -> str:
if obj.phone_number:
raw = (getattr(obj.phone_number, 'as_e164', None) or str(obj.phone_number)).replace(' ', '')
if raw.startswith('+98') and len(raw) == 13:
return '0' + raw[3:]
return str(obj.phone_number)
return ""
def get_preferred_language(self, obj) -> str:
if obj.languages and isinstance(obj.languages, list) and len(obj.languages) > 0:
return obj.languages[0]
return obj.language or "fa"
def get_title(self, obj) -> str:
if obj.title:
return obj.title
membership = obj.institution_memberships.first() if hasattr(obj, 'institution_memberships') else None
if membership and membership.title:
return membership.title
return obj.skill or ""
def get_rank(self, obj) -> str:
return obj.rank or ""
def get_website(self, obj) -> str:
return obj.website or ""
def get_address(self, obj) -> str:
return obj.address or ""
def get_institution(self, obj) -> str:
if obj.institution_name:
return obj.institution_name
membership = obj.institution_memberships.first() if hasattr(obj, 'institution_memberships') else None
if membership and membership.institution:
return membership.institution.name
return ""
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_institution_id(self, obj):
membership = obj.institution_memberships.first() if hasattr(obj, 'institution_memberships') else None
if membership and membership.institution:
return membership.institution.id
return None
def get_position(self, obj) -> str:
membership = obj.institution_memberships.first() if hasattr(obj, 'institution_memberships') else None
if membership and membership.role:
return membership.role
return ""
def get_roles(self, obj) -> list:
roles = [obj.user_type] if obj.user_type else []
for group in obj.groups.all():
roles.append(group.name)
return list(set(roles))
def get_permissions(self, obj) -> list:
perms = []
if obj.is_super_admin:
perms.extend(['super_admin', 'manage_all', 'manage_institutions', 'manage_users', 'manage_content', 'approve_verifications'])
elif obj.is_regional_admin:
perms.extend(['regional_admin', 'manage_regional_institutions', 'manage_content', 'approve_verifications'])
elif obj.is_institution_admin:
perms.extend(['institution_admin', 'manage_institution_profile', 'manage_team', 'manage_events', 'manage_projects'])
elif obj.is_editor:
perms.extend(['editor', 'create_posts', 'manage_events', 'edit_projects'])
else:
perms.extend(['client_access', 'view_courses', 'submit_tickets', 'attend_events'])
return perms
def get_verification_status(self, obj) -> str:
return obj.verification_status
def get_is_verified(self, obj) -> bool:
return obj.verification_status == VerificationDocument.Status.VERIFIED
def get_managed_regions(self, obj) -> list:
if not getattr(obj, 'is_authenticated', False):
return []
return [
{
'id': r.id,
'name': r.name,
'slug': r.slug,
'code': r.code,
}
for r in obj.managed_regions.filter(is_active=True)
]
class UserMeUpdateSerializer(serializers.ModelSerializer):
bio = serializers.CharField(source='info', allow_blank=True, required=False, allow_null=True)
first_name = serializers.CharField(required=False, allow_blank=True)
last_name = serializers.CharField(required=False, allow_blank=True)
name = serializers.CharField(required=False, write_only=True, allow_blank=True)
phone = serializers.CharField(required=False, write_only=True, allow_blank=True)
preferred_language = serializers.CharField(required=False, write_only=True, allow_blank=True)
expertise = serializers.ListField(child=serializers.CharField(), required=False, write_only=True)
title = serializers.CharField(required=False, allow_blank=True)
rank = serializers.CharField(required=False, allow_blank=True)
institution = serializers.CharField(required=False, write_only=True, allow_blank=True)
institution_name = serializers.CharField(required=False, allow_blank=True)
website = serializers.CharField(required=False, allow_blank=True)
address = serializers.CharField(required=False, allow_blank=True)
class Meta:
model = User
fields = [
'fullname',
'first_name',
'last_name',
'name',
'phone_number',
'phone',
'avatar',
'bio',
'languages',
'preferred_language',
'skills',
'expertise',
'title',
'rank',
'institution',
'institution_name',
'website',
'address',
'country',
'city',
'gender',
'birthdate',
]
def to_internal_value(self, data):
if hasattr(data, 'copy'):
mutable_data = data.copy()
else:
mutable_data = dict(data)
# Synchronize first_name, last_name, fullname, and name
fn = mutable_data.get('first_name')
ln = mutable_data.get('last_name')
fl = mutable_data.get('fullname') or mutable_data.get('name')
if fn is not None or ln is not None:
if not fl:
mutable_data['fullname'] = f"{fn or ''} {ln or ''}".strip()
elif fl:
parts = fl.strip().split(maxsplit=1)
mutable_data['first_name'] = parts[0] if parts else ""
mutable_data['last_name'] = parts[1] if len(parts) > 1 else ""
mutable_data['fullname'] = fl.strip()
# Harmonize institution -> institution_name
inst = mutable_data.get('institution') or mutable_data.get('institution_name')
if inst:
mutable_data['institution_name'] = inst
# Harmonize title
if 'title' in mutable_data and mutable_data['title']:
mutable_data['title'] = mutable_data['title']
# Harmonize phone / phone_number and normalize local numbers (09... -> +989...)
raw_phone = mutable_data.get('phone') or mutable_data.get('phone_number')
if raw_phone:
raw_phone_str = str(raw_phone).strip()
if raw_phone_str.startswith('09') and len(raw_phone_str) == 11 and raw_phone_str.isdigit():
mutable_data['phone_number'] = '+98' + raw_phone_str[1:]
elif raw_phone_str.startswith('0098'):
mutable_data['phone_number'] = '+98' + raw_phone_str[4:]
elif raw_phone_str.startswith('989') and len(raw_phone_str) == 12 and raw_phone_str.isdigit():
mutable_data['phone_number'] = '+' + raw_phone_str
else:
mutable_data['phone_number'] = raw_phone_str
if 'expertise' in mutable_data and not mutable_data.get('skills'):
mutable_data['skills'] = mutable_data['expertise']
if 'preferred_language' in mutable_data and not mutable_data.get('languages'):
mutable_data['languages'] = [mutable_data['preferred_language']]
# Handle avatar: support File, base64 Data URI, null (remove), and ignore existing URL strings
if 'avatar' in mutable_data:
avatar_val = mutable_data['avatar']
if isinstance(avatar_val, str):
if avatar_val.startswith('data:image/'):
try:
import base64, uuid
from django.core.files.base import ContentFile
format_part, imgstr = avatar_val.split(';base64,', 1)
ext = format_part.split('/')[-1]
if ext == 'jpeg':
ext = 'jpg'
name_id = uuid.uuid4().hex[:8]
mutable_data['avatar'] = ContentFile(base64.b64decode(imgstr), name=f"avatar_{name_id}.{ext}")
except Exception:
mutable_data.pop('avatar', None)
elif not avatar_val.strip() or avatar_val.startswith('http') or avatar_val.startswith('/'):
mutable_data.pop('avatar', None)
elif avatar_val is None:
mutable_data['avatar'] = None
return super().to_internal_value(mutable_data)
class RegisterResponseSerializer(serializers.Serializer):
user = UserMeSerializer()
access = serializers.CharField()
refresh = serializers.CharField()
message = serializers.CharField()
class VerificationDocumentSerializer(serializers.ModelSerializer):
document_type_display = serializers.CharField(source='get_document_type_display', read_only=True)
status_display = serializers.CharField(source='get_status_display', read_only=True)
class Meta:
model = VerificationDocument
fields = [
'id',
'user',
'document_type',
'document_type_display',
'title',
'document_file',
'status',
'status_display',
'admin_notes',
'uploaded_at',
'reviewed_at',
]
read_only_fields = [
'id',
'user',
'document_type_display',
'status',
'status_display',
'admin_notes',
'uploaded_at',
'reviewed_at',
]
class VerificationDocumentUploadSerializer(serializers.Serializer):
document_type = serializers.ChoiceField(
choices=VerificationDocument.DocumentType.choices,
default=VerificationDocument.DocumentType.NATIONAL_ID
)
title = serializers.CharField(required=False, allow_blank=True, max_length=255)
document_file = serializers.FileField(required=True)
class PasswordRecoverRequestSerializer(serializers.Serializer):
email = serializers.EmailField(required=True)
class PasswordResetRequestSerializer(serializers.Serializer):
email = serializers.EmailField(required=True)
token = serializers.CharField(required=True)
new_password = serializers.CharField(required=True, min_length=6)
def validate_new_password(self, value):
validate_password(value)
return value
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
"""
JWT token obtain serializer that accepts either 'username' or 'email' as the credential.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'email' in self.fields:
self.fields['email'].required = False
if 'username' not in self.fields:
self.fields['username'] = serializers.CharField(required=False, write_only=True)
def to_internal_value(self, data):
if isinstance(data, dict):
mutable_data = data.copy()
identifier = mutable_data.get('email') or mutable_data.get('username')
if identifier:
mutable_data['email'] = identifier
mutable_data['username'] = identifier
return super().to_internal_value(mutable_data)
return super().to_internal_value(data)
def validate(self, attrs):
identifier = attrs.get('email') or attrs.get('username')
if not identifier:
raise serializers.ValidationError({"email": _("This field is required.")})
attrs['email'] = identifier
attrs['username'] = identifier
return super().validate(attrs)