""" Accounts domain serializers for ThrillWiki API v1. This module contains all serializers related to user accounts, profiles, authentication, top lists, and user statistics. """ from rest_framework import serializers from drf_spectacular.utils import ( extend_schema_serializer, extend_schema_field, OpenApiExample, ) from django.contrib.auth.password_validation import validate_password from django.utils.crypto import get_random_string from .shared import UserModel, ModelChoices # === USER PROFILE SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "User Profile Example", summary="Example user profile response", description="A user's profile information", value={ "id": 1, "profile_id": "1234", "display_name": "Coaster Enthusiast", "bio": "Love visiting theme parks around the world!", "pronouns": "they/them", "avatar_url": "/media/avatars/user1.jpg", "coaster_credits": 150, "dark_ride_credits": 45, "flat_ride_credits": 80, "water_ride_credits": 25, "user": { "username": "coaster_fan", "date_joined": "2024-01-01T00:00:00Z", }, }, ) ] ) class UserProfileOutputSerializer(serializers.Serializer): """Output serializer for user profiles.""" id = serializers.IntegerField() profile_id = serializers.CharField() display_name = serializers.CharField() bio = serializers.CharField() pronouns = serializers.CharField() avatar_url = serializers.SerializerMethodField() twitter = serializers.URLField() instagram = serializers.URLField() youtube = serializers.URLField() discord = serializers.CharField() # Ride statistics coaster_credits = serializers.IntegerField() dark_ride_credits = serializers.IntegerField() flat_ride_credits = serializers.IntegerField() water_ride_credits = serializers.IntegerField() # User info (limited) user = serializers.SerializerMethodField() @extend_schema_field(serializers.URLField(allow_null=True)) def get_avatar_url(self, obj) -> str | None: return obj.get_avatar() @extend_schema_field(serializers.DictField()) def get_user(self, obj) -> dict: return { "username": obj.user.username, "date_joined": obj.user.date_joined, } class UserProfileCreateInputSerializer(serializers.Serializer): """Input serializer for creating user profiles.""" display_name = serializers.CharField(max_length=50) bio = serializers.CharField(max_length=500, allow_blank=True, default="") pronouns = serializers.CharField(max_length=50, allow_blank=True, default="") twitter = serializers.URLField(required=False, allow_blank=True) instagram = serializers.URLField(required=False, allow_blank=True) youtube = serializers.URLField(required=False, allow_blank=True) discord = serializers.CharField(max_length=100, allow_blank=True, default="") class UserProfileUpdateInputSerializer(serializers.Serializer): """Input serializer for updating user profiles.""" display_name = serializers.CharField(max_length=50, required=False) bio = serializers.CharField(max_length=500, allow_blank=True, required=False) pronouns = serializers.CharField(max_length=50, allow_blank=True, required=False) twitter = serializers.URLField(required=False, allow_blank=True) instagram = serializers.URLField(required=False, allow_blank=True) youtube = serializers.URLField(required=False, allow_blank=True) discord = serializers.CharField(max_length=100, allow_blank=True, required=False) coaster_credits = serializers.IntegerField(required=False) dark_ride_credits = serializers.IntegerField(required=False) flat_ride_credits = serializers.IntegerField(required=False) water_ride_credits = serializers.IntegerField(required=False) # === TOP LIST SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "Top List Example", summary="Example top list response", description="A user's top list of rides or parks", value={ "id": 1, "title": "My Top 10 Roller Coasters", "category": "RC", "description": "My favorite roller coasters ranked", "user": {"username": "coaster_fan", "display_name": "Coaster Fan"}, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-08-15T12:00:00Z", }, ) ] ) class TopListOutputSerializer(serializers.Serializer): """Output serializer for top lists.""" id = serializers.IntegerField() title = serializers.CharField() category = serializers.CharField() description = serializers.CharField() created_at = serializers.DateTimeField() updated_at = serializers.DateTimeField() # User info user = serializers.SerializerMethodField() @extend_schema_field(serializers.DictField()) def get_user(self, obj) -> dict: return { "username": obj.user.username, "display_name": obj.user.get_display_name(), } class TopListCreateInputSerializer(serializers.Serializer): """Input serializer for creating top lists.""" title = serializers.CharField(max_length=100) category = serializers.ChoiceField(choices=ModelChoices.get_top_list_categories()) description = serializers.CharField(allow_blank=True, default="") class TopListUpdateInputSerializer(serializers.Serializer): """Input serializer for updating top lists.""" title = serializers.CharField(max_length=100, required=False) category = serializers.ChoiceField( choices=ModelChoices.get_top_list_categories(), required=False ) description = serializers.CharField(allow_blank=True, required=False) # === TOP LIST ITEM SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "Top List Item Example", summary="Example top list item response", description="An item in a user's top list", value={ "id": 1, "rank": 1, "notes": "Amazing airtime and smooth ride", "object_name": "Steel Vengeance", "object_type": "Ride", "top_list": {"id": 1, "title": "My Top 10 Roller Coasters"}, }, ) ] ) class TopListItemOutputSerializer(serializers.Serializer): """Output serializer for top list items.""" id = serializers.IntegerField() rank = serializers.IntegerField() notes = serializers.CharField() object_name = serializers.SerializerMethodField() object_type = serializers.SerializerMethodField() # Top list info top_list = serializers.SerializerMethodField() @extend_schema_field(serializers.CharField()) def get_object_name(self, obj) -> str: """Get the name of the referenced object.""" # This would need to be implemented based on the generic foreign key return "Object Name" # Placeholder @extend_schema_field(serializers.CharField()) def get_object_type(self, obj) -> str: """Get the type of the referenced object.""" return obj.content_type.model_class().__name__ @extend_schema_field(serializers.DictField()) def get_top_list(self, obj) -> dict: return { "id": obj.top_list.id, "title": obj.top_list.title, } class TopListItemCreateInputSerializer(serializers.Serializer): """Input serializer for creating top list items.""" top_list_id = serializers.IntegerField() content_type_id = serializers.IntegerField() object_id = serializers.IntegerField() rank = serializers.IntegerField(min_value=1) notes = serializers.CharField(allow_blank=True, default="") class TopListItemUpdateInputSerializer(serializers.Serializer): """Input serializer for updating top list items.""" rank = serializers.IntegerField(min_value=1, required=False) notes = serializers.CharField(allow_blank=True, required=False) # === ACCOUNTS SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "User Example", summary="Example user response", description="A typical user object", value={ "id": 1, "username": "john_doe", "email": "john@example.com", "first_name": "John", "last_name": "Doe", "date_joined": "2024-01-01T12:00:00Z", "is_active": True, "avatar_url": "https://example.com/avatars/john.jpg", }, ) ] ) class UserOutputSerializer(serializers.ModelSerializer): """User serializer for API responses.""" avatar_url = serializers.SerializerMethodField() class Meta: model = UserModel fields = [ "id", "username", "email", "first_name", "last_name", "date_joined", "is_active", "avatar_url", ] read_only_fields = ["id", "date_joined", "is_active"] @extend_schema_field(serializers.URLField(allow_null=True)) def get_avatar_url(self, obj) -> str | None: """Get user avatar URL.""" if hasattr(obj, "profile") and obj.profile.avatar: return obj.profile.avatar.url return None class LoginInputSerializer(serializers.Serializer): """Input serializer for user login.""" username = serializers.CharField( max_length=254, help_text="Username or email address" ) password = serializers.CharField( max_length=128, style={"input_type": "password"}, trim_whitespace=False ) def validate(self, attrs): username = attrs.get("username") password = attrs.get("password") if username and password: return attrs raise serializers.ValidationError("Must include username/email and password.") class LoginOutputSerializer(serializers.Serializer): """Output serializer for successful login.""" token = serializers.CharField() user = UserOutputSerializer() message = serializers.CharField() class SignupInputSerializer(serializers.ModelSerializer): """Input serializer for user registration.""" password = serializers.CharField( write_only=True, validators=[validate_password], style={"input_type": "password"}, ) password_confirm = serializers.CharField( write_only=True, style={"input_type": "password"} ) class Meta: model = UserModel fields = [ "username", "email", "first_name", "last_name", "password", "password_confirm", ] extra_kwargs = { "password": {"write_only": True}, "email": {"required": True}, } def validate_email(self, value): """Validate email is unique.""" if UserModel.objects.filter(email=value).exists(): raise serializers.ValidationError("A user with this email already exists.") return value def validate_username(self, value): """Validate username is unique.""" if UserModel.objects.filter(username=value).exists(): raise serializers.ValidationError( "A user with this username already exists." ) return value def validate(self, attrs): """Validate passwords match.""" password = attrs.get("password") password_confirm = attrs.get("password_confirm") if password != password_confirm: raise serializers.ValidationError( {"password_confirm": "Passwords do not match."} ) return attrs def create(self, validated_data): """Create user with validated data.""" validated_data.pop("password_confirm", None) password = validated_data.pop("password") # Use type: ignore for Django's create_user method which isn't properly typed user = UserModel.objects.create_user( # type: ignore[attr-defined] password=password, **validated_data ) return user class SignupOutputSerializer(serializers.Serializer): """Output serializer for successful signup.""" token = serializers.CharField() user = UserOutputSerializer() message = serializers.CharField() class PasswordResetInputSerializer(serializers.Serializer): """Input serializer for password reset request.""" email = serializers.EmailField() def validate_email(self, value): """Validate email exists.""" try: user = UserModel.objects.get(email=value) self.user = user return value except UserModel.DoesNotExist: # Don't reveal if email exists or not for security return value def save(self, **kwargs): """Send password reset email if user exists.""" if hasattr(self, "user"): # Create password reset token token = get_random_string(64) # Note: PasswordReset model would need to be imported # PasswordReset.objects.update_or_create(...) pass class PasswordResetOutputSerializer(serializers.Serializer): """Output serializer for password reset request.""" detail = serializers.CharField() class PasswordChangeInputSerializer(serializers.Serializer): """Input serializer for password change.""" old_password = serializers.CharField( max_length=128, style={"input_type": "password"} ) new_password = serializers.CharField( max_length=128, validators=[validate_password], style={"input_type": "password"}, ) new_password_confirm = serializers.CharField( max_length=128, style={"input_type": "password"} ) def validate_old_password(self, value): """Validate old password is correct.""" user = self.context["request"].user if not user.check_password(value): raise serializers.ValidationError("Old password is incorrect.") return value def validate(self, attrs): """Validate new passwords match.""" new_password = attrs.get("new_password") new_password_confirm = attrs.get("new_password_confirm") if new_password != new_password_confirm: raise serializers.ValidationError( {"new_password_confirm": "New passwords do not match."} ) return attrs def save(self, **kwargs): """Change user password.""" user = self.context["request"].user # validated_data is guaranteed to exist after is_valid() is called new_password = self.validated_data["new_password"] # type: ignore[index] user.set_password(new_password) user.save() return user class PasswordChangeOutputSerializer(serializers.Serializer): """Output serializer for password change.""" detail = serializers.CharField() class LogoutOutputSerializer(serializers.Serializer): """Output serializer for logout.""" message = serializers.CharField() class SocialProviderOutputSerializer(serializers.Serializer): """Output serializer for social authentication providers.""" id = serializers.CharField() name = serializers.CharField() authUrl = serializers.URLField() class AuthStatusOutputSerializer(serializers.Serializer): """Output serializer for authentication status check.""" authenticated = serializers.BooleanField() user = UserOutputSerializer(allow_null=True)