""" 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 django.contrib.auth import get_user_model from drf_spectacular.utils import ( OpenApiExample, extend_schema_serializer, ) from rest_framework import serializers from apps.accounts.models import ( NotificationPreference, User, UserNotification, UserProfile, ) from apps.core.choices.serializers import RichChoiceFieldSerializer from apps.lists.models import UserList from apps.rides.models.credits import RideCredit 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() total_credits = serializers.SerializerMethodField() unique_parks = 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", "unit_system", "location", "total_credits", "unique_parks", ] read_only_fields = ["profile_id", "avatar_url", "avatar_variants", "total_credits", "unique_parks"] def get_total_credits(self, obj): """Get the total number of ride credits.""" return RideCredit.objects.filter(user=obj.user).count() def get_unique_parks(self, obj): """Get the number of unique parks visited.""" # This assumes RideCredit -> Ride -> Park relationship return RideCredit.objects.filter(user=obj.user).values("ride__park").distinct().count() 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"] class PublicUserSerializer(serializers.ModelSerializer): """ Public user serializer for viewing other users' profiles. Only exposes public information. """ profile = UserProfileSerializer(read_only=True) class Meta: model = User fields = [ "user_id", "username", "date_joined", "role", "profile", ] read_only_fields = fields # === 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 = RichChoiceFieldSerializer( choice_group="theme_preferences", domain="accounts", 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 = RichChoiceFieldSerializer( choice_group="privacy_levels", domain="accounts", 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 = RichChoiceFieldSerializer( choice_group="privacy_levels", domain="accounts", 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 = RichChoiceFieldSerializer( choice_group="privacy_levels", domain="accounts", 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( "User List Example", summary="User's 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 UserListSerializer(serializers.ModelSerializer): """Serializer for user's lists.""" items_count = serializers.SerializerMethodField() class Meta: model = UserList 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", "unit_system", "location", ] 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: import io from PIL import Image 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