from rest_framework import serializers from rest_framework.authtoken.models import Token from django.contrib.auth.models import Group from django.contrib.auth.password_validation import validate_password from django.utils.translation import gettext_lazy as _ from apps.account.models import User from utils import FileFieldSerializer, absolute_url from utils.validators import validate_type_code class UserProfileSerializer(serializers.ModelSerializer): avatar = FileFieldSerializer(required=False, allow_null=True) password = serializers.CharField(write_only=True, required=False, validators=[validate_password]) fullname = serializers.CharField(required=False) gender = serializers.ChoiceField( choices=User.GenderChoices.choices, required=False, help_text="Select the user's gender." ) fcm = serializers.CharField(required=False, help_text="Firebase Cloud Messaging token.") saved_location = serializers.SerializerMethodField() class Meta: model = User fields = ['id', 'device_id', 'fcm', 'fullname', 'slug', 'avatar', 'email', 'phone_number', 'password', 'info', 'skill', 'city', 'country', 'birthdate', 'gender', 'saved_location'] read_only_fields = ['email', 'info', 'skill', 'device_id', 'slug', 'saved_location'] def get_saved_location(self, obj): # Check if user is authenticated and has location_history attribute if not obj.is_authenticated or not hasattr(obj, 'location_history'): return None last_location = obj.location_history.order_by('-at_time').first() if last_location: return { 'lat': last_location.lat, 'lon': last_location.lon, 'city': last_location.city, 'country': last_location.country, 'timezone': last_location.timezone, 'selected_manually': last_location.selected_manually, 'at_time': last_location.at_time, } return None # def validate_email(self, value): # if User.objects.filter(email=value).exists(): # raise serializers.ValidationError("This email is already registered.") # return value def update(self, instance, validated_data): # Pop the password from the data to handle it separately password = validated_data.pop('password', None) # Use the default update logic for all other fields for attr, value in validated_data.items(): if value is not None: setattr(instance, attr, value) # If a new password was provided, hash and set it correctly if password: instance.set_password(password) instance.save() return instance class UserRegisterSerializer(serializers.ModelSerializer): fcm = serializers.CharField(required=False, allow_blank=True, allow_null=True) device_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, write_only=True) email = serializers.EmailField() class Meta: model = User fields = ['id','fullname', 'email', 'fcm', 'device_id'] extra_kwargs = { 'fullname': {'required': True,}, 'email': {'required': True,}, } def create(self, validated_data): device_id = validated_data.pop('device_id', None) user = super().create(validated_data) if device_id: user.device_id = device_id user.save() return user def validate_email(self, value): normalized_email = User.objects.normalize_email(value) if User.objects.filter(email=normalized_email).exists(): raise serializers.ValidationError("This email is already registered.") return normalized_email class UserVerifySerializer(serializers.Serializer): code = serializers.CharField(max_length=5, validators=[validate_type_code]) email = serializers.EmailField() device_id = serializers.CharField(max_length=255, required=False) def validate_email(self, value): """ Normalize the email to ensure the Redis key matches correctly. """ return User.objects.normalize_email(value) class UserLoginSerializer(serializers.Serializer): password = serializers.CharField(write_only=True) token = serializers.CharField(allow_null=True, read_only=True, required=False) fullname = serializers.CharField(allow_null=True, read_only=True, required=False) avatar = serializers.CharField(allow_null=True, read_only=True, required=False) email = serializers.EmailField(write_only=True) password = serializers.CharField(style={'input_type': 'password'}, trim_whitespace=False) fcm = serializers.CharField(required=False) device_id = serializers.CharField(required=False) timezone = serializers.CharField(required=False, allow_null=True, allow_blank=True) def validate(self, data): # Custom validation logic can be added here if needed # data.pop('fcm', None) # data.pop('device_id', None) return data def validate_email(self, value): """ Normalize email for case-insensitive login. """ return User.objects.normalize_email(value) # class UserLoginSerializer(serializers.Serializer): # password = serializers.CharField(write_only=True) # token = serializers.CharField(allow_null=True, read_only=True, required=False) # fullname = serializers.CharField(allow_null=True, read_only=True, required=False) # avatar = serializers.CharField(allow_null=True, read_only=True, required=False) # email = serializers.EmailField(write_only=True) # password = serializers.CharField(style={'input_type': 'password'}, trim_whitespace=False) # fcm = serializers.CharField(required=False) # device_id = serializers.CharField(required=False) # timezone = serializers.CharField(required=False, allow_null=True, allow_blank=True) # class UserRecoverPasswordSerializer(serializers.ModelSerializer): # email = serializers.EmailField() # class Meta: # model = User # fields = ['email',] # extra_kwargs = { # 'email': {'required': True,}, # } class UserRecoverPasswordSerializer(serializers.Serializer): """ Validates that an email is provided and is in a valid format without checking for database uniqueness. """ email = serializers.EmailField(required=True) def validate_email(self, value): """ Normalize the email address to ensure case-insensitive lookups. """ return User.objects.normalize_email(value) class UserResetPasswordSerializer(serializers.ModelSerializer): password = serializers.CharField(write_only=True) class Meta: model = User fields = ['password', ] extra_kwargs = { 'password': {'required': True,}, } class UserGuestSerializer(serializers.ModelSerializer): lat = serializers.CharField(max_length=255, allow_null=True, allow_blank=True, required=False) lon = serializers.CharField(max_length=255, allow_null=True, allow_blank=True, required=False) fcm = serializers.CharField(required=False) device_id = serializers.CharField(required=False) device_os = serializers.ChoiceField(choices=User.DeviceOs.choices, required=False) timezone = serializers.CharField(required=False, allow_null=True, allow_blank=True) class Meta: model = User fields = ['device_id', 'fcm', 'device_os', 'lat', 'lon', 'timezone'] def validate(self, data): # Make sure at least device_id is provided if not data.get('device_id'): raise serializers.ValidationError({"device_id": "Device ID is required for guest users."}) return data class WebUserGuestSerializer(serializers.ModelSerializer): user_agent = serializers.CharField(required=False, allow_null=True, allow_blank=True) client_ip = serializers.CharField(required=False, allow_null=True, allow_blank=True) timezone = serializers.CharField(required=False, allow_null=True, allow_blank=True) class Meta: model = User fields = ['user_agent', 'client_ip', 'timezone', 'device_id', 'device_os'] def validate(self, data): # Ensure device_id is provided (generated by view) if not data.get('device_id'): raise serializers.ValidationError({"device_id": "Device ID is required for web guest users."}) return data class UserFCMSerializer(serializers.ModelSerializer): class Meta: model = User fields = ['fcm'] class AdminUserSerializer(serializers.ModelSerializer): avatar = FileFieldSerializer(required=False, allow_null=True) password = serializers.CharField(write_only=True, required=False, validators=[validate_password]) email = serializers.EmailField(required=True) auth_token = serializers.SerializerMethodField() plain_password = serializers.SerializerMethodField() def get_auth_token(self, obj): token = Token.objects.filter(user=obj).first() return token.key if token else None def get_plain_password(self, obj): request = self.context.get('request') if request and request.user and (request.user.is_superuser or request.user.user_type in ['super_admin', 'admin']): return obj.get_plain_password() return None class Meta: model = User fields = [ 'id', 'fullname', 'email', 'phone_number', 'password', 'avatar', 'gender', 'birthdate', 'info', 'skill', 'city', 'country', 'device_id', 'device_os', 'user_agent', 'client_ip', 'fcm', 'user_type', 'is_active', 'is_staff', 'is_superuser', 'date_joined', 'last_login', 'auth_token', 'plain_password' ] read_only_fields = ['id', 'date_joined', 'last_login', 'auth_token', 'device_id', 'device_os', 'user_agent', 'client_ip'] ROLE_GROUP_MAP = { User.UserType.STUDENT: "Student Group", User.UserType.PROFESSOR: "Professor Group", User.UserType.ADMIN: "Admin Group", User.UserType.SUPER_ADMIN: "Super Admin Group", User.UserType.CONSULTANT: "Consultant Group", } PANEL_GROUPS = {"Professor Group", "Admin Group", "Super admin Group", "Super Admin Group"} def _ensure_group(self, instance, group_name): group, _ = Group.objects.get_or_create(name=group_name) instance.groups.add(group) def _remove_groups(self, instance, group_names): if not group_names: return instance.groups.remove(*Group.objects.filter(name__in=group_names)) def _normalize_role_access(self, instance, requested_user_type): if not requested_user_type: return requested_user_type = str(requested_user_type) if requested_user_type == User.UserType.SUPER_ADMIN: instance.is_staff = True instance.is_superuser = True self._ensure_group(instance, "Super Admin Group") self._remove_groups(instance, {"Professor Group", "Admin Group", "Super admin Group"}) return if requested_user_type == User.UserType.PROFESSOR: instance.is_staff = False instance.is_superuser = False self._ensure_group(instance, "Professor Group") self._remove_groups(instance, {"Admin Group", "Super admin Group", "Super Admin Group"}) return if requested_user_type == User.UserType.ADMIN: instance.is_staff = False instance.is_superuser = False self._ensure_group(instance, "Admin Group") self._remove_groups(instance, {"Professor Group", "Super admin Group", "Super Admin Group"}) return if requested_user_type == User.UserType.STUDENT: instance.is_staff = False instance.is_superuser = False self._ensure_group(instance, "Student Group") self._remove_groups(instance, self.PANEL_GROUPS) return if requested_user_type == User.UserType.CONSULTANT: instance.is_staff = False instance.is_superuser = False self._ensure_group(instance, "Consultant Group") self._remove_groups(instance, self.PANEL_GROUPS) return if requested_user_type == User.UserType.CLIENT: instance.is_staff = False instance.is_superuser = False self._remove_groups(instance, self.PANEL_GROUPS) def create(self, validated_data): password = validated_data.pop('password', None) user = User(**validated_data) if password: user.set_password(password) user.set_plain_password(password) else: user.set_unusable_password() user.save() self._normalize_role_access(user, validated_data.get('user_type', user.user_type)) user.save() return user def update(self, instance, validated_data): password = validated_data.pop('password', None) user = super().update(instance, validated_data) self._normalize_role_access(user, validated_data.get('user_type', user.user_type)) if password: user.set_password(password) user.set_plain_password(password) user.save() return user