diff --git a/backend/apps/api/v1/serializers/accounts.py b/backend/apps/api/v1/serializers/accounts.py deleted file mode 100644 index 6d19305f..00000000 --- a/backend/apps/api/v1/serializers/accounts.py +++ /dev/null @@ -1,491 +0,0 @@ -""" -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)