""" User accounts and settings serializers for ThrillWiki API v1. This module contains all serializers related to user account management, profile settings, preferences, privacy, notifications, and security. """ from rest_framework import serializers from django.contrib.auth import get_user_model from drf_spectacular.utils import ( extend_schema_serializer, OpenApiExample, ) from apps.accounts.models import ( User, UserProfile, TopList, UserNotification, NotificationPreference, ) UserModel = get_user_model() # === USER PROFILE SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "User Profile Example", summary="Complete user profile", description="Full user profile with all fields", value={ "user_id": "1234", "username": "thrillseeker", "email": "user@example.com", "first_name": "John", "last_name": "Doe", "is_active": True, "date_joined": "2024-01-01T00:00:00Z", "role": "USER", "theme_preference": "dark", "profile": { "profile_id": "5678", "display_name": "Thrill Seeker", "avatar": "https://example.com/avatars/user.jpg", "pronouns": "they/them", "bio": "Love roller coasters and theme parks!", "twitter": "https://twitter.com/thrillseeker", "instagram": "https://instagram.com/thrillseeker", "youtube": "https://youtube.com/thrillseeker", "discord": "thrillseeker#1234", "coaster_credits": 150, "dark_ride_credits": 45, "flat_ride_credits": 89, "water_ride_credits": 23, }, }, ) ] ) class UserProfileSerializer(serializers.ModelSerializer): """Serializer for user profile data.""" avatar_url = serializers.SerializerMethodField() avatar_variants = serializers.SerializerMethodField() class Meta: model = UserProfile fields = [ "profile_id", "display_name", "avatar", "avatar_url", "avatar_variants", "pronouns", "bio", "twitter", "instagram", "youtube", "discord", "coaster_credits", "dark_ride_credits", "flat_ride_credits", "water_ride_credits", ] read_only_fields = ["profile_id", "avatar_url", "avatar_variants"] def get_avatar_url(self, obj): """Get the avatar URL with fallback to default letter-based avatar.""" return obj.get_avatar_url() def get_avatar_variants(self, obj): """Get avatar variants for different use cases.""" return obj.get_avatar_variants() def validate_display_name(self, value): """Validate display name uniqueness - now checks User model first.""" user = self.context["request"].user # Check User model for display_name uniqueness (primary location) if User.objects.filter(display_name=value).exclude(id=user.id).exists(): raise serializers.ValidationError("Display name already taken") # Also check UserProfile for backward compatibility during transition if UserProfile.objects.filter(display_name=value).exclude(user=user).exists(): raise serializers.ValidationError("Display name already taken") return value @extend_schema_serializer( examples=[ OpenApiExample( "Complete User Example", summary="Complete user with profile", description="Full user object with embedded profile", value={ "user_id": "1234", "username": "thrillseeker", "email": "user@example.com", "first_name": "John", "last_name": "Doe", "is_active": True, "date_joined": "2024-01-01T00:00:00Z", "role": "USER", "theme_preference": "dark", "profile": { "profile_id": "5678", "display_name": "Thrill Seeker", "avatar": "https://example.com/avatars/user.jpg", "pronouns": "they/them", "bio": "Love roller coasters and theme parks!", "twitter": "https://twitter.com/thrillseeker", "instagram": "https://instagram.com/thrillseeker", "youtube": "https://youtube.com/thrillseeker", "discord": "thrillseeker#1234", "coaster_credits": 150, "dark_ride_credits": 45, "flat_ride_credits": 89, "water_ride_credits": 23, }, }, ) ] ) class CompleteUserSerializer(serializers.ModelSerializer): """Complete user serializer with profile data.""" profile = UserProfileSerializer(read_only=True) class Meta: model = User fields = [ "user_id", "username", "email", "first_name", "last_name", "is_active", "date_joined", "role", "theme_preference", "profile", ] read_only_fields = ["user_id", "date_joined", "role"] # === USER SETTINGS SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "User Preferences Example", summary="User preferences and settings", description="User's preference settings", value={ "theme_preference": "dark", "email_notifications": True, "push_notifications": False, "privacy_level": "public", "show_email": False, "show_real_name": True, "show_statistics": True, "allow_friend_requests": True, "allow_messages": True, }, ) ] ) class UserPreferencesSerializer(serializers.Serializer): """Serializer for user preferences and settings.""" theme_preference = serializers.ChoiceField( choices=User.ThemePreference.choices, help_text="User's theme preference" ) email_notifications = serializers.BooleanField( default=True, help_text="Whether to receive email notifications" ) push_notifications = serializers.BooleanField( default=False, help_text="Whether to receive push notifications" ) privacy_level = serializers.ChoiceField( choices=[ ("public", "Public"), ("friends", "Friends Only"), ("private", "Private"), ], default="public", help_text="Profile visibility level", ) show_email = serializers.BooleanField( default=False, help_text="Whether to show email on profile" ) show_real_name = serializers.BooleanField( default=True, help_text="Whether to show real name on profile" ) show_statistics = serializers.BooleanField( default=True, help_text="Whether to show ride statistics on profile" ) allow_friend_requests = serializers.BooleanField( default=True, help_text="Whether to allow friend requests" ) allow_messages = serializers.BooleanField( default=True, help_text="Whether to allow direct messages" ) # === NOTIFICATION SETTINGS SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "Notification Settings Example", summary="User notification preferences", description="Detailed notification settings", value={ "email_notifications": { "new_reviews": True, "review_replies": True, "friend_requests": True, "messages": True, "weekly_digest": False, "new_features": True, "security_alerts": True, }, "push_notifications": { "new_reviews": False, "review_replies": True, "friend_requests": True, "messages": True, }, "in_app_notifications": { "new_reviews": True, "review_replies": True, "friend_requests": True, "messages": True, "system_announcements": True, }, }, ) ] ) class NotificationSettingsSerializer(serializers.Serializer): """Serializer for detailed notification settings.""" class EmailNotificationsSerializer(serializers.Serializer): new_reviews = serializers.BooleanField(default=True) review_replies = serializers.BooleanField(default=True) friend_requests = serializers.BooleanField(default=True) messages = serializers.BooleanField(default=True) weekly_digest = serializers.BooleanField(default=False) new_features = serializers.BooleanField(default=True) security_alerts = serializers.BooleanField(default=True) class PushNotificationsSerializer(serializers.Serializer): new_reviews = serializers.BooleanField(default=False) review_replies = serializers.BooleanField(default=True) friend_requests = serializers.BooleanField(default=True) messages = serializers.BooleanField(default=True) class InAppNotificationsSerializer(serializers.Serializer): new_reviews = serializers.BooleanField(default=True) review_replies = serializers.BooleanField(default=True) friend_requests = serializers.BooleanField(default=True) messages = serializers.BooleanField(default=True) system_announcements = serializers.BooleanField(default=True) email_notifications = EmailNotificationsSerializer() push_notifications = PushNotificationsSerializer() in_app_notifications = InAppNotificationsSerializer() # === PRIVACY SETTINGS SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "Privacy Settings Example", summary="User privacy settings", description="Detailed privacy and visibility settings", value={ "profile_visibility": "public", "show_email": False, "show_real_name": True, "show_join_date": True, "show_statistics": True, "show_reviews": True, "show_photos": True, "show_top_lists": True, "allow_friend_requests": True, "allow_messages": True, "allow_profile_comments": False, "search_visibility": True, "activity_visibility": "friends", }, ) ] ) class PrivacySettingsSerializer(serializers.Serializer): """Serializer for privacy and visibility settings.""" profile_visibility = serializers.ChoiceField( choices=[ ("public", "Public"), ("friends", "Friends Only"), ("private", "Private"), ], default="public", help_text="Overall profile visibility", ) show_email = serializers.BooleanField( default=False, help_text="Show email address on profile" ) show_real_name = serializers.BooleanField( default=True, help_text="Show real name on profile" ) show_join_date = serializers.BooleanField( default=True, help_text="Show join date on profile" ) show_statistics = serializers.BooleanField( default=True, help_text="Show ride statistics on profile" ) show_reviews = serializers.BooleanField( default=True, help_text="Show reviews on profile" ) show_photos = serializers.BooleanField( default=True, help_text="Show uploaded photos on profile" ) show_top_lists = serializers.BooleanField( default=True, help_text="Show top lists on profile" ) allow_friend_requests = serializers.BooleanField( default=True, help_text="Allow others to send friend requests" ) allow_messages = serializers.BooleanField( default=True, help_text="Allow others to send direct messages" ) allow_profile_comments = serializers.BooleanField( default=False, help_text="Allow others to comment on profile" ) search_visibility = serializers.BooleanField( default=True, help_text="Allow profile to appear in search results" ) activity_visibility = serializers.ChoiceField( choices=[ ("public", "Public"), ("friends", "Friends Only"), ("private", "Private"), ], default="friends", help_text="Who can see your activity feed", ) # === SECURITY SETTINGS SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "Security Settings Example", summary="User security settings", description="Account security and authentication settings", value={ "two_factor_enabled": False, "login_notifications": True, "session_timeout": 30, "require_password_change": False, "last_password_change": "2024-01-01T00:00:00Z", "active_sessions": 2, "login_history_retention": 90, }, ) ] ) class SecuritySettingsSerializer(serializers.Serializer): """Serializer for security settings.""" two_factor_enabled = serializers.BooleanField( default=False, help_text="Whether two-factor authentication is enabled" ) login_notifications = serializers.BooleanField( default=True, help_text="Send notifications for new logins" ) session_timeout = serializers.IntegerField( default=30, min_value=5, max_value=180, help_text="Session timeout in days" ) require_password_change = serializers.BooleanField( default=False, help_text="Whether password change is required" ) last_password_change = serializers.DateTimeField( read_only=True, help_text="When password was last changed" ) active_sessions = serializers.IntegerField( read_only=True, help_text="Number of active sessions" ) login_history_retention = serializers.IntegerField( default=90, min_value=30, max_value=365, help_text="How long to keep login history (days)", ) # === USER STATISTICS SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "User Statistics Example", summary="User activity statistics", description="Comprehensive user activity and contribution statistics", value={ "ride_credits": { "coaster_credits": 150, "dark_ride_credits": 45, "flat_ride_credits": 89, "water_ride_credits": 23, "total_credits": 307, }, "contributions": { "park_reviews": 25, "ride_reviews": 87, "photos_uploaded": 156, "top_lists_created": 8, "helpful_votes_received": 342, }, "activity": { "days_active": 45, "last_active": "2024-01-15T10:30:00Z", "average_review_rating": 4.2, "most_reviewed_park": "Cedar Point", "favorite_ride_type": "Roller Coaster", }, "achievements": { "first_review": True, "photo_contributor": True, "top_reviewer": False, "park_explorer": True, "coaster_enthusiast": True, }, }, ) ] ) class UserStatisticsSerializer(serializers.Serializer): """Serializer for user statistics and achievements.""" class RideCreditsSerializer(serializers.Serializer): coaster_credits = serializers.IntegerField() dark_ride_credits = serializers.IntegerField() flat_ride_credits = serializers.IntegerField() water_ride_credits = serializers.IntegerField() total_credits = serializers.IntegerField() class ContributionsSerializer(serializers.Serializer): park_reviews = serializers.IntegerField() ride_reviews = serializers.IntegerField() photos_uploaded = serializers.IntegerField() top_lists_created = serializers.IntegerField() helpful_votes_received = serializers.IntegerField() class ActivitySerializer(serializers.Serializer): days_active = serializers.IntegerField() last_active = serializers.DateTimeField() average_review_rating = serializers.FloatField() most_reviewed_park = serializers.CharField() favorite_ride_type = serializers.CharField() class AchievementsSerializer(serializers.Serializer): first_review = serializers.BooleanField() photo_contributor = serializers.BooleanField() top_reviewer = serializers.BooleanField() park_explorer = serializers.BooleanField() coaster_enthusiast = serializers.BooleanField() ride_credits = RideCreditsSerializer() contributions = ContributionsSerializer() activity = ActivitySerializer() achievements = AchievementsSerializer() # === TOP LISTS SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "Top List Example", summary="User's top list", description="A user's ranked list of rides or parks", value={ "id": 1, "title": "My Top 10 Roller Coasters", "category": "RC", "description": "My favorite roller coasters from around the world", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-15T10:30:00Z", "items_count": 10, }, ) ] ) class TopListSerializer(serializers.ModelSerializer): """Serializer for user's top lists.""" items_count = serializers.SerializerMethodField() class Meta: model = TopList fields = [ "id", "title", "category", "description", "created_at", "updated_at", "items_count", ] read_only_fields = ["id", "created_at", "updated_at"] def get_items_count(self, obj): """Get the number of items in the list.""" return obj.items.count() # === ACCOUNT UPDATE SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "Account Update Example", summary="Update account information", description="Update basic account information", value={ "first_name": "John", "last_name": "Doe", "email": "newemail@example.com", }, ) ] ) class AccountUpdateSerializer(serializers.ModelSerializer): """Serializer for updating account information.""" class Meta: model = User fields = [ "first_name", "last_name", "email", ] def validate_email(self, value): """Validate email uniqueness.""" user = self.context["request"].user if User.objects.filter(email=value).exclude(id=user.id).exists(): raise serializers.ValidationError("Email already in use") return value # === PROFILE UPDATE SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "Profile Update Example", summary="Update profile information", description="Update profile information and social links", value={ "display_name": "New Display Name", "pronouns": "they/them", "bio": "Updated bio text", "twitter": "https://twitter.com/newhandle", "instagram": "", "youtube": "https://youtube.com/newchannel", "discord": "newhandle#5678", }, ) ] ) class ProfileUpdateSerializer(serializers.ModelSerializer): """Serializer for updating profile information.""" class Meta: model = UserProfile fields = [ "display_name", "pronouns", "bio", "twitter", "instagram", "youtube", "discord", ] def validate_display_name(self, value): """Validate display name uniqueness - now checks User model first.""" user = self.context["request"].user # Check User model for display_name uniqueness (primary location) if User.objects.filter(display_name=value).exclude(id=user.id).exists(): raise serializers.ValidationError("Display name already taken") # Also check UserProfile for backward compatibility during transition if UserProfile.objects.filter(display_name=value).exclude(user=user).exists(): raise serializers.ValidationError("Display name already taken") return value # === THEME PREFERENCE SERIALIZER === @extend_schema_serializer( examples=[ OpenApiExample( "Theme Update Example", summary="Update theme preference", description="Update user's theme preference", value={ "theme_preference": "dark", }, ) ] ) class ThemePreferenceSerializer(serializers.ModelSerializer): """Serializer for updating theme preference.""" class Meta: model = User fields = ["theme_preference"] # === NOTIFICATION SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "User Notification Example", summary="User notification", description="A notification sent to a user", value={ "id": 1, "notification_type": "submission_approved", "title": "Your submission has been approved!", "message": "Your photo submission for Cedar Point has been approved and is now live on the site.", "priority": "normal", "is_read": False, "read_at": None, "created_at": "2024-01-15T10:30:00Z", "expires_at": None, "extra_data": {"submission_id": 123, "park_name": "Cedar Point"}, }, ) ] ) class UserNotificationSerializer(serializers.ModelSerializer): """Serializer for user notifications.""" class Meta: model = UserNotification fields = [ "id", "notification_type", "title", "message", "priority", "is_read", "read_at", "created_at", "expires_at", "extra_data", ] read_only_fields = [ "id", "notification_type", "title", "message", "priority", "created_at", "expires_at", "extra_data", ] @extend_schema_serializer( examples=[ OpenApiExample( "Notification Preferences Example", summary="User notification preferences", description="Comprehensive notification preferences for all channels", value={ "submission_approved_email": True, "submission_approved_push": True, "submission_approved_inapp": True, "submission_rejected_email": True, "submission_rejected_push": True, "submission_rejected_inapp": True, "submission_pending_email": False, "submission_pending_push": False, "submission_pending_inapp": True, "review_reply_email": True, "review_reply_push": True, "review_reply_inapp": True, "review_helpful_email": False, "review_helpful_push": True, "review_helpful_inapp": True, "friend_request_email": True, "friend_request_push": True, "friend_request_inapp": True, "friend_accepted_email": False, "friend_accepted_push": True, "friend_accepted_inapp": True, "message_received_email": True, "message_received_push": True, "message_received_inapp": True, "system_announcement_email": True, "system_announcement_push": False, "system_announcement_inapp": True, "account_security_email": True, "account_security_push": True, "account_security_inapp": True, "feature_update_email": True, "feature_update_push": False, "feature_update_inapp": True, "achievement_unlocked_email": False, "achievement_unlocked_push": True, "achievement_unlocked_inapp": True, "milestone_reached_email": False, "milestone_reached_push": True, "milestone_reached_inapp": True, }, ) ] ) class NotificationPreferenceSerializer(serializers.ModelSerializer): """Serializer for notification preferences.""" class Meta: model = NotificationPreference fields = [ # Submission notifications "submission_approved_email", "submission_approved_push", "submission_approved_inapp", "submission_rejected_email", "submission_rejected_push", "submission_rejected_inapp", "submission_pending_email", "submission_pending_push", "submission_pending_inapp", # Review notifications "review_reply_email", "review_reply_push", "review_reply_inapp", "review_helpful_email", "review_helpful_push", "review_helpful_inapp", # Social notifications "friend_request_email", "friend_request_push", "friend_request_inapp", "friend_accepted_email", "friend_accepted_push", "friend_accepted_inapp", "message_received_email", "message_received_push", "message_received_inapp", # System notifications "system_announcement_email", "system_announcement_push", "system_announcement_inapp", "account_security_email", "account_security_push", "account_security_inapp", "feature_update_email", "feature_update_push", "feature_update_inapp", # Achievement notifications "achievement_unlocked_email", "achievement_unlocked_push", "achievement_unlocked_inapp", "milestone_reached_email", "milestone_reached_push", "milestone_reached_inapp", ] # === NOTIFICATION ACTIONS SERIALIZERS === @extend_schema_serializer( examples=[ OpenApiExample( "Mark Notifications Read Example", summary="Mark notifications as read", description="Mark specific notifications as read", value={"notification_ids": [1, 2, 3, 4, 5]}, ) ] ) class MarkNotificationsReadSerializer(serializers.Serializer): """Serializer for marking notifications as read.""" notification_ids = serializers.ListField( child=serializers.IntegerField(), help_text="List of notification IDs to mark as read", ) def validate_notification_ids(self, value): """Validate that all notification IDs belong to the requesting user.""" user = self.context["request"].user valid_ids = UserNotification.objects.filter( id__in=value, user=user ).values_list("id", flat=True) invalid_ids = set(value) - set(valid_ids) if invalid_ids: raise serializers.ValidationError( f"Invalid notification IDs: {list(invalid_ids)}" ) return value @extend_schema_serializer( examples=[ OpenApiExample( "Avatar Upload Example", summary="Upload user avatar", description="Upload a new avatar image", value={"avatar": "base64_encoded_image_data_or_file_upload"}, ) ] ) class AvatarUploadSerializer(serializers.Serializer): """Serializer for uploading user avatar.""" # Use FileField instead of ImageField to bypass Django's image validation avatar = serializers.FileField() def validate_avatar(self, value): """Validate avatar file.""" if not value: raise serializers.ValidationError("No file provided") # Check file size constraints (max 10MB for Cloudflare Images) if hasattr(value, 'size') and value.size > 10 * 1024 * 1024: raise serializers.ValidationError( "Image file too large. Maximum size is 10MB.") # Try to validate with PIL try: from PIL import Image import io value.seek(0) image_data = value.read() value.seek(0) # Reset for later use if len(image_data) == 0: raise serializers.ValidationError("File appears to be empty") # Try to open with PIL image = Image.open(io.BytesIO(image_data)) # Verify it's a valid image image.verify() # Check image dimensions (max 12,000x12,000 for Cloudflare Images) if image.size[0] > 12000 or image.size[1] > 12000: raise serializers.ValidationError( "Image dimensions too large. Maximum is 12,000x12,000 pixels.") # Check if it's a supported format if image.format not in ['JPEG', 'PNG', 'GIF', 'WEBP']: raise serializers.ValidationError( f"Unsupported image format: {image.format}. Supported formats: JPEG, PNG, GIF, WebP.") except serializers.ValidationError: raise # Re-raise validation errors except Exception: # PIL validation failed, but let Cloudflare Images try to process it pass return value