""" API views for user account management. This module contains API endpoints for user account operations including user deletion while preserving submissions, profile management, settings, preferences, privacy, notifications, and security. """ from rest_framework import status from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated, IsAdminUser from rest_framework.response import Response from drf_spectacular.utils import extend_schema, OpenApiParameter from drf_spectacular.types import OpenApiTypes from django.shortcuts import get_object_or_404 from rest_framework.permissions import AllowAny from django.utils import timezone from apps.accounts.models import ( User, UserProfile, TopList, UserNotification, NotificationPreference, ) from apps.accounts.services import UserDeletionService from apps.api.v1.serializers.accounts import ( CompleteUserSerializer, UserPreferencesSerializer, NotificationSettingsSerializer, PrivacySettingsSerializer, SecuritySettingsSerializer, UserStatisticsSerializer, TopListSerializer, AccountUpdateSerializer, ProfileUpdateSerializer, ThemePreferenceSerializer, UserNotificationSerializer, NotificationPreferenceSerializer, MarkNotificationsReadSerializer, AvatarUploadSerializer, ) @extend_schema( operation_id="delete_user_preserve_submissions", summary="Delete user while preserving submissions", description=( "Delete a user account while preserving all their submissions " "(reviews, photos, top lists, etc.). All submissions are transferred " "to a system 'deleted_user' placeholder. This operation is irreversible." ), parameters=[ OpenApiParameter( name="user_id", type=OpenApiTypes.STR, location=OpenApiParameter.PATH, description="User ID of the user to delete", ), ], responses={ 200: { "description": "User successfully deleted with submissions preserved", "example": { "success": True, "message": "User successfully deleted with submissions preserved", "deleted_user": { "username": "john_doe", "user_id": "1234", "email": "john@example.com", "date_joined": "2024-01-15T10:30:00Z", }, "preserved_submissions": { "park_reviews": 5, "ride_reviews": 12, "uploaded_park_photos": 3, "uploaded_ride_photos": 8, "top_lists": 2, "edit_submissions": 1, "photo_submissions": 0, }, "transferred_to": {"username": "deleted_user", "user_id": "0000"}, }, }, 400: { "description": "Bad request - user cannot be deleted", "example": { "success": False, "error": "Cannot delete user: Cannot delete superuser accounts", }, }, 404: { "description": "User not found", "example": {"success": False, "error": "User not found"}, }, 403: { "description": "Permission denied - admin access required", "example": {"success": False, "error": "Admin access required"}, }, }, tags=["User Management"], ) @api_view(["DELETE"]) @permission_classes([IsAuthenticated, IsAdminUser]) def delete_user_preserve_submissions(request, user_id): """ Delete a user while preserving all their submissions. This endpoint allows administrators to delete user accounts while preserving all user-generated content (reviews, photos, top lists, etc.). All submissions are transferred to a system "deleted_user" placeholder. **Admin Only**: This endpoint requires admin permissions. **Irreversible**: This operation cannot be undone. """ try: user = get_object_or_404(User, user_id=user_id) # Check if user can be deleted can_delete, reason = UserDeletionService.can_delete_user(user) if not can_delete: return Response( {"success": False, "error": f"Cannot delete user: {reason}"}, status=status.HTTP_400_BAD_REQUEST, ) # Perform the deletion result = UserDeletionService.delete_user_preserve_submissions(user) return Response( { "success": True, "message": "User successfully deleted with submissions preserved", **result, }, status=status.HTTP_200_OK, ) except Exception as e: return Response( {"success": False, "error": f"Error deleting user: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) @extend_schema( operation_id="request_account_deletion", summary="Request account deletion with email verification", description=( "Request to delete your own account. A verification code will be sent " "to your email address. The account will only be deleted after you " "provide the correct verification code." ), responses={ 200: { "description": "Deletion request created and verification email sent", "example": { "success": True, "message": "Verification code sent to your email", "expires_at": "2024-01-16T10:30:00Z", "email": "user@example.com", }, }, 400: { "description": "Bad request - user cannot be deleted", "example": { "success": False, "error": "Cannot delete user: Cannot delete superuser accounts", }, }, 401: { "description": "Authentication required", "example": {"success": False, "error": "Authentication required"}, }, }, tags=["Self-Service Account Management"], ) @api_view(["POST"]) @permission_classes([IsAuthenticated]) def request_account_deletion(request): """ Request deletion of your own account with email verification. This endpoint allows authenticated users to request deletion of their own account. A verification code will be sent to their email address, and the account will only be deleted after they provide the correct code. **Authentication Required**: User must be logged in. **Email Verification**: A verification code is sent to the user's email. **Submission Preservation**: All user submissions will be preserved. """ try: user = request.user # Create deletion request and send email deletion_request = UserDeletionService.request_user_deletion(user) return Response( { "success": True, "message": "Verification code sent to your email", "expires_at": deletion_request.expires_at, "email": user.email, }, status=status.HTTP_200_OK, ) except ValueError as e: return Response( {"success": False, "error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) except Exception as e: return Response( {"success": False, "error": f"Error creating deletion request: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) @extend_schema( operation_id="verify_account_deletion", summary="Verify and complete account deletion", description=( "Complete account deletion by providing the verification code sent " "to your email. This action is irreversible." ), request={ "application/json": { "type": "object", "properties": { "verification_code": { "type": "string", "description": "8-character verification code from email", "example": "ABC12345", } }, "required": ["verification_code"], } }, responses={ 200: { "description": "Account successfully deleted", "example": { "success": True, "message": "Account successfully deleted with submissions preserved", "deleted_user": { "username": "john_doe", "user_id": "1234", "email": "john@example.com", "date_joined": "2024-01-15T10:30:00Z", }, "preserved_submissions": { "park_reviews": 5, "ride_reviews": 12, "uploaded_park_photos": 3, "uploaded_ride_photos": 8, "top_lists": 2, "edit_submissions": 1, "photo_submissions": 0, }, "deletion_request": { "verification_code": "ABC12345", "created_at": "2024-01-15T10:30:00Z", "verified_at": "2024-01-15T11:00:00Z", }, }, }, 400: { "description": "Invalid or expired verification code", "example": {"success": False, "error": "Verification code has expired"}, }, }, tags=["Self-Service Account Management"], ) @api_view(["POST"]) @permission_classes([AllowAny]) # No auth required since user might be deleted def verify_account_deletion(request): """ Complete account deletion using verification code. This endpoint completes the account deletion process by verifying the code sent to the user's email. Once verified, the account is permanently deleted but all submissions are preserved. **No Authentication Required**: The verification code serves as authentication. **Irreversible**: This action cannot be undone. **Submission Preservation**: All user submissions will be preserved. """ try: verification_code = request.data.get("verification_code") if not verification_code: return Response( {"success": False, "error": "Verification code is required"}, status=status.HTTP_400_BAD_REQUEST, ) # Verify and delete user result = UserDeletionService.verify_and_delete_user(verification_code) return Response( { "success": True, "message": "Account successfully deleted with submissions preserved", **result, }, status=status.HTTP_200_OK, ) except ValueError as e: return Response( {"success": False, "error": str(e)}, status=status.HTTP_400_BAD_REQUEST ) except Exception as e: return Response( {"success": False, "error": f"Error verifying deletion: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) @extend_schema( operation_id="cancel_account_deletion", summary="Cancel pending account deletion request", description=( "Cancel a pending account deletion request. This will remove the " "deletion request and prevent the account from being deleted." ), responses={ 200: { "description": "Deletion request cancelled or no request found", "example": { "success": True, "message": "Deletion request cancelled", "had_pending_request": True, }, }, 401: { "description": "Authentication required", "example": {"success": False, "error": "Authentication required"}, }, }, tags=["Self-Service Account Management"], ) @api_view(["POST"]) @permission_classes([IsAuthenticated]) def cancel_account_deletion(request): """ Cancel a pending account deletion request. This endpoint allows users to cancel their pending account deletion request if they change their mind before completing the verification. **Authentication Required**: User must be logged in. """ try: user = request.user # Cancel deletion request had_request = UserDeletionService.cancel_deletion_request(user) return Response( { "success": True, "message": ( "Deletion request cancelled" if had_request else "No pending deletion request found" ), "had_pending_request": had_request, }, status=status.HTTP_200_OK, ) except Exception as e: return Response( {"success": False, "error": f"Error cancelling deletion request: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) @extend_schema( operation_id="check_user_deletion_eligibility", summary="Check if user can be deleted", description=( "Check if a user can be safely deleted and get a preview of " "what submissions would be preserved." ), parameters=[ OpenApiParameter( name="user_id", type=OpenApiTypes.STR, location=OpenApiParameter.PATH, description="User ID of the user to check", ), ], responses={ 200: { "description": "User deletion eligibility information", "example": { "can_delete": True, "reason": None, "user_info": { "username": "john_doe", "user_id": "1234", "email": "john@example.com", "date_joined": "2024-01-15T10:30:00Z", "role": "USER", }, "submissions_to_preserve": { "park_reviews": 5, "ride_reviews": 12, "uploaded_park_photos": 3, "uploaded_ride_photos": 8, "top_lists": 2, "edit_submissions": 1, "photo_submissions": 0, }, "total_submissions": 31, }, }, 404: { "description": "User not found", "example": {"success": False, "error": "User not found"}, }, 403: { "description": "Permission denied - admin access required", "example": {"success": False, "error": "Admin access required"}, }, }, tags=["User Management"], ) @api_view(["GET"]) @permission_classes([IsAuthenticated, IsAdminUser]) def check_user_deletion_eligibility(request, user_id): """ Check if a user can be deleted and preview submissions to preserve. This endpoint allows administrators to check if a user can be safely deleted and see what submissions would be preserved before performing the actual deletion. **Admin Only**: This endpoint requires admin permissions. """ try: user = get_object_or_404(User, user_id=user_id) # Check if user can be deleted can_delete, reason = UserDeletionService.can_delete_user(user) # Count submissions submission_counts = { "park_reviews": getattr( user, "park_reviews", user.__class__.objects.none() ).count(), "ride_reviews": getattr( user, "ride_reviews", user.__class__.objects.none() ).count(), "uploaded_park_photos": getattr( user, "uploaded_park_photos", user.__class__.objects.none() ).count(), "uploaded_ride_photos": getattr( user, "uploaded_ride_photos", user.__class__.objects.none() ).count(), "top_lists": getattr( user, "top_lists", user.__class__.objects.none() ).count(), "edit_submissions": getattr( user, "edit_submissions", user.__class__.objects.none() ).count(), "photo_submissions": getattr( user, "photo_submissions", user.__class__.objects.none() ).count(), } total_submissions = sum(submission_counts.values()) return Response( { "can_delete": can_delete, "reason": reason, "user_info": { "username": user.username, "user_id": user.user_id, "email": user.email, "date_joined": user.date_joined, "role": user.role, }, "submissions_to_preserve": submission_counts, "total_submissions": total_submissions, }, status=status.HTTP_200_OK, ) except Exception as e: return Response( {"success": False, "error": f"Error checking user: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) # === USER PROFILE ENDPOINTS === @extend_schema( operation_id="get_user_profile", summary="Get current user's complete profile", description="Get the authenticated user's complete profile including all settings and preferences.", responses={ 200: CompleteUserSerializer, 401: { "description": "Authentication required", "example": {"detail": "Authentication credentials were not provided."}, }, }, tags=["User Profile"], ) @api_view(["GET"]) @permission_classes([IsAuthenticated]) def get_user_profile(request): """Get the authenticated user's complete profile.""" serializer = CompleteUserSerializer(request.user) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( operation_id="update_user_account", summary="Update basic account information", description="Update basic account information like name and email.", request=AccountUpdateSerializer, responses={ 200: CompleteUserSerializer, 400: { "description": "Validation error", "example": {"email": ["Email already in use"]}, }, }, tags=["User Profile"], ) @api_view(["PATCH"]) @permission_classes([IsAuthenticated]) def update_user_account(request): """Update basic account information.""" serializer = AccountUpdateSerializer( request.user, data=request.data, partial=True, context={"request": request} ) if serializer.is_valid(): serializer.save() response_serializer = CompleteUserSerializer(request.user) return Response(response_serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @extend_schema( operation_id="update_user_profile", summary="Update user profile information", description="Update profile information including display name, bio, and social links.", request=ProfileUpdateSerializer, responses={ 200: CompleteUserSerializer, 400: { "description": "Validation error", "example": {"display_name": ["Display name already taken"]}, }, }, tags=["User Profile"], ) @api_view(["PATCH"]) @permission_classes([IsAuthenticated]) def update_user_profile(request): """Update user profile information.""" profile, created = UserProfile.objects.get_or_create(user=request.user) serializer = ProfileUpdateSerializer( profile, data=request.data, partial=True, context={"request": request} ) if serializer.is_valid(): serializer.save() response_serializer = CompleteUserSerializer(request.user) return Response(response_serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # === USER PREFERENCES ENDPOINTS === @extend_schema( operation_id="get_user_preferences", summary="Get user preferences", description="Get the authenticated user's preferences and settings.", responses={ 200: UserPreferencesSerializer, 401: {"description": "Authentication required"}, }, tags=["User Settings"], ) @api_view(["GET"]) @permission_classes([IsAuthenticated]) def get_user_preferences(request): """Get user preferences.""" user = request.user data = { "theme_preference": user.theme_preference, "email_notifications": user.email_notifications, "push_notifications": user.push_notifications, "privacy_level": user.privacy_level, "show_email": user.show_email, "show_real_name": user.show_real_name, "show_statistics": user.show_statistics, "allow_friend_requests": user.allow_friend_requests, "allow_messages": user.allow_messages, } serializer = UserPreferencesSerializer(data=data) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( operation_id="update_user_preferences", summary="Update user preferences", description="Update the authenticated user's preferences and settings.", request=UserPreferencesSerializer, responses={ 200: UserPreferencesSerializer, 400: {"description": "Validation error"}, }, tags=["User Settings"], ) @api_view(["PATCH"]) @permission_classes([IsAuthenticated]) def update_user_preferences(request): """Update user preferences.""" user = request.user current_data = { "theme_preference": user.theme_preference, "email_notifications": user.email_notifications, "push_notifications": user.push_notifications, "privacy_level": user.privacy_level, "show_email": user.show_email, "show_real_name": user.show_real_name, "show_statistics": user.show_statistics, "allow_friend_requests": user.allow_friend_requests, "allow_messages": user.allow_messages, } serializer = UserPreferencesSerializer(data={**current_data, **request.data}) if serializer.is_valid(): # Update user fields for field, value in serializer.validated_data.items(): setattr(user, field, value) user.save() return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @extend_schema( operation_id="update_theme_preference", summary="Update theme preference", description="Update the user's theme preference (light/dark).", request=ThemePreferenceSerializer, responses={ 200: ThemePreferenceSerializer, 400: {"description": "Validation error"}, }, tags=["User Settings"], ) @api_view(["PATCH"]) @permission_classes([IsAuthenticated]) def update_theme_preference(request): """Update theme preference.""" serializer = ThemePreferenceSerializer( request.user, data=request.data, partial=True ) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # === NOTIFICATION SETTINGS ENDPOINTS === @extend_schema( operation_id="get_notification_settings", summary="Get notification settings", description="Get detailed notification preferences for the authenticated user.", responses={ 200: NotificationSettingsSerializer, 401: {"description": "Authentication required"}, }, tags=["User Settings"], ) @api_view(["GET"]) @permission_classes([IsAuthenticated]) def get_notification_settings(request): """Get notification settings.""" user = request.user # Get notification preferences from JSON field or use defaults prefs = user.notification_preferences or {} data = { "email_notifications": { "new_reviews": prefs.get("email_new_reviews", True), "review_replies": prefs.get("email_review_replies", True), "friend_requests": prefs.get("email_friend_requests", True), "messages": prefs.get("email_messages", True), "weekly_digest": prefs.get("email_weekly_digest", False), "new_features": prefs.get("email_new_features", True), "security_alerts": prefs.get("email_security_alerts", True), }, "push_notifications": { "new_reviews": prefs.get("push_new_reviews", False), "review_replies": prefs.get("push_review_replies", True), "friend_requests": prefs.get("push_friend_requests", True), "messages": prefs.get("push_messages", True), }, "in_app_notifications": { "new_reviews": prefs.get("inapp_new_reviews", True), "review_replies": prefs.get("inapp_review_replies", True), "friend_requests": prefs.get("inapp_friend_requests", True), "messages": prefs.get("inapp_messages", True), "system_announcements": prefs.get("inapp_system_announcements", True), }, } serializer = NotificationSettingsSerializer(data) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( operation_id="update_notification_settings", summary="Update notification settings", description="Update detailed notification preferences for the authenticated user.", request=NotificationSettingsSerializer, responses={ 200: NotificationSettingsSerializer, 400: {"description": "Validation error"}, }, tags=["User Settings"], ) @api_view(["PATCH"]) @permission_classes([IsAuthenticated]) def update_notification_settings(request): """Update notification settings.""" user = request.user # Get current preferences current_prefs = user.notification_preferences or {} # Build current data structure current_data = { "email_notifications": { "new_reviews": current_prefs.get("email_new_reviews", True), "review_replies": current_prefs.get("email_review_replies", True), "friend_requests": current_prefs.get("email_friend_requests", True), "messages": current_prefs.get("email_messages", True), "weekly_digest": current_prefs.get("email_weekly_digest", False), "new_features": current_prefs.get("email_new_features", True), "security_alerts": current_prefs.get("email_security_alerts", True), }, "push_notifications": { "new_reviews": current_prefs.get("push_new_reviews", False), "review_replies": current_prefs.get("push_review_replies", True), "friend_requests": current_prefs.get("push_friend_requests", True), "messages": current_prefs.get("push_messages", True), }, "in_app_notifications": { "new_reviews": current_prefs.get("inapp_new_reviews", True), "review_replies": current_prefs.get("inapp_review_replies", True), "friend_requests": current_prefs.get("inapp_friend_requests", True), "messages": current_prefs.get("inapp_messages", True), "system_announcements": current_prefs.get( "inapp_system_announcements", True ), }, } # Merge with request data if "email_notifications" in request.data and request.data["email_notifications"]: current_data["email_notifications"].update(request.data["email_notifications"]) if "push_notifications" in request.data and request.data["push_notifications"]: current_data["push_notifications"].update(request.data["push_notifications"]) if "in_app_notifications" in request.data and request.data["in_app_notifications"]: current_data["in_app_notifications"].update( request.data["in_app_notifications"] ) serializer = NotificationSettingsSerializer(data=current_data) if serializer.is_valid(): # Convert back to flat structure for storage validated_data = serializer.validated_data new_prefs = {} # Email notifications for key, value in validated_data["email_notifications"].items(): new_prefs[f"email_{key}"] = value # Push notifications for key, value in validated_data["push_notifications"].items(): new_prefs[f"push_{key}"] = value # In-app notifications for key, value in validated_data["in_app_notifications"].items(): new_prefs[f"inapp_{key}"] = value # Update user preferences user.notification_preferences = new_prefs user.save() return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # === PRIVACY SETTINGS ENDPOINTS === @extend_schema( operation_id="get_privacy_settings", summary="Get privacy settings", description="Get privacy and visibility settings for the authenticated user.", responses={ 200: PrivacySettingsSerializer, 401: {"description": "Authentication required"}, }, tags=["User Settings"], ) @api_view(["GET"]) @permission_classes([IsAuthenticated]) def get_privacy_settings(request): """Get privacy settings.""" user = request.user data = { "profile_visibility": user.privacy_level, "show_email": user.show_email, "show_real_name": user.show_real_name, "show_join_date": user.show_join_date, "show_statistics": user.show_statistics, "show_reviews": user.show_reviews, "show_photos": user.show_photos, "show_top_lists": user.show_top_lists, "allow_friend_requests": user.allow_friend_requests, "allow_messages": user.allow_messages, "allow_profile_comments": user.allow_profile_comments, "search_visibility": user.search_visibility, "activity_visibility": user.activity_visibility, } serializer = PrivacySettingsSerializer(data) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( operation_id="update_privacy_settings", summary="Update privacy settings", description="Update privacy and visibility settings for the authenticated user.", request=PrivacySettingsSerializer, responses={ 200: PrivacySettingsSerializer, 400: {"description": "Validation error"}, }, tags=["User Settings"], ) @api_view(["PATCH"]) @permission_classes([IsAuthenticated]) def update_privacy_settings(request): """Update privacy settings.""" user = request.user current_data = { "profile_visibility": user.privacy_level, "show_email": user.show_email, "show_real_name": user.show_real_name, "show_join_date": user.show_join_date, "show_statistics": user.show_statistics, "show_reviews": user.show_reviews, "show_photos": user.show_photos, "show_top_lists": user.show_top_lists, "allow_friend_requests": user.allow_friend_requests, "allow_messages": user.allow_messages, "allow_profile_comments": user.allow_profile_comments, "search_visibility": user.search_visibility, "activity_visibility": user.activity_visibility, } serializer = PrivacySettingsSerializer(data={**current_data, **request.data}) if serializer.is_valid(): # Update user fields (map profile_visibility to privacy_level) for field, value in serializer.validated_data.items(): if field == "profile_visibility": user.privacy_level = value else: setattr(user, field, value) user.save() return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # === SECURITY SETTINGS ENDPOINTS === @extend_schema( operation_id="get_security_settings", summary="Get security settings", description="Get security and authentication settings for the authenticated user.", responses={ 200: SecuritySettingsSerializer, 401: {"description": "Authentication required"}, }, tags=["User Settings"], ) @api_view(["GET"]) @permission_classes([IsAuthenticated]) def get_security_settings(request): """Get security settings.""" user = request.user # TODO: Implement active sessions count active_sessions = 1 # Placeholder data = { "two_factor_enabled": user.two_factor_enabled, "login_notifications": user.login_notifications, "session_timeout": user.session_timeout, "require_password_change": False, # TODO: Implement logic "last_password_change": user.last_password_change, "active_sessions": active_sessions, "login_history_retention": user.login_history_retention, } serializer = SecuritySettingsSerializer(data) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( operation_id="update_security_settings", summary="Update security settings", description="Update security and authentication settings for the authenticated user.", request=SecuritySettingsSerializer, responses={ 200: SecuritySettingsSerializer, 400: {"description": "Validation error"}, }, tags=["User Settings"], ) @api_view(["PATCH"]) @permission_classes([IsAuthenticated]) def update_security_settings(request): """Update security settings.""" user = request.user # Get current data active_sessions = 1 # Placeholder current_data = { "two_factor_enabled": user.two_factor_enabled, "login_notifications": user.login_notifications, "session_timeout": user.session_timeout, "require_password_change": False, "last_password_change": user.last_password_change, "active_sessions": active_sessions, "login_history_retention": user.login_history_retention, } serializer = SecuritySettingsSerializer(data={**current_data, **request.data}) if serializer.is_valid(): # Update only writable fields writable_fields = [ "two_factor_enabled", "login_notifications", "session_timeout", "login_history_retention", ] for field in writable_fields: if field in serializer.validated_data: setattr(user, field, serializer.validated_data[field]) user.save() return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # === USER STATISTICS ENDPOINTS === @extend_schema( operation_id="get_user_statistics", summary="Get user statistics", description="Get comprehensive statistics and achievements for the authenticated user.", responses={ 200: UserStatisticsSerializer, 401: {"description": "Authentication required"}, }, tags=["User Profile"], ) @api_view(["GET"]) @permission_classes([IsAuthenticated]) def get_user_statistics(request): """Get user statistics.""" user = request.user profile = getattr(user, "profile", None) # Ride credits ride_credits = { "coaster_credits": profile.coaster_credits if profile else 0, "dark_ride_credits": profile.dark_ride_credits if profile else 0, "flat_ride_credits": profile.flat_ride_credits if profile else 0, "water_ride_credits": profile.water_ride_credits if profile else 0, } ride_credits["total_credits"] = sum(ride_credits.values()) # Contributions (placeholder counts - would need actual related models) contributions = { "park_reviews": getattr( user, "park_reviews", user.__class__.objects.none() ).count(), "ride_reviews": getattr( user, "ride_reviews", user.__class__.objects.none() ).count(), "photos_uploaded": getattr( user, "uploaded_park_photos", user.__class__.objects.none() ).count() + getattr(user, "uploaded_ride_photos", user.__class__.objects.none()).count(), "top_lists_created": user.top_lists.count(), "helpful_votes_received": 0, # TODO: Implement when review voting is added } # Activity activity = { "days_active": (timezone.now().date() - user.date_joined.date()).days, "last_active": user.last_login or user.date_joined, "average_review_rating": 4.0, # TODO: Calculate from actual reviews "most_reviewed_park": "Cedar Point", # TODO: Calculate from actual reviews "favorite_ride_type": "Roller Coaster", # TODO: Calculate from ride credits } # Achievements (placeholder logic) achievements = { "first_review": contributions["park_reviews"] > 0 or contributions["ride_reviews"] > 0, "photo_contributor": contributions["photos_uploaded"] > 0, "top_reviewer": contributions["park_reviews"] + contributions["ride_reviews"] >= 50, "park_explorer": contributions["park_reviews"] >= 10, "coaster_enthusiast": ride_credits["coaster_credits"] >= 100, } data = { "ride_credits": ride_credits, "contributions": contributions, "activity": activity, "achievements": achievements, } serializer = UserStatisticsSerializer(data) return Response(serializer.data, status=status.HTTP_200_OK) # === TOP LISTS ENDPOINTS === @extend_schema( operation_id="get_user_top_lists", summary="Get user's top lists", description="Get all top lists created by the authenticated user.", responses={ 200: { "type": "array", "items": { "type": "object", "properties": { "id": {"type": "integer"}, "title": {"type": "string"}, "category": {"type": "string"}, "description": {"type": "string"}, "created_at": {"type": "string", "format": "date-time"}, "updated_at": {"type": "string", "format": "date-time"}, "items_count": {"type": "integer"}, }, }, }, 401: {"description": "Authentication required"}, }, tags=["User Profile"], ) @api_view(["GET"]) @permission_classes([IsAuthenticated]) def get_user_top_lists(request): """Get user's top lists.""" top_lists = request.user.top_lists.all() serializer = TopListSerializer(top_lists, many=True) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( operation_id="create_top_list", summary="Create a new top list", description="Create a new top list for the authenticated user.", request=TopListSerializer, responses={201: TopListSerializer, 400: {"description": "Validation error"}}, tags=["User Profile"], ) @api_view(["POST"]) @permission_classes([IsAuthenticated]) def create_top_list(request): """Create a new top list.""" serializer = TopListSerializer(data=request.data) if serializer.is_valid(): serializer.save(user=request.user) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @extend_schema( operation_id="update_top_list", summary="Update a top list", description="Update an existing top list owned by the authenticated user.", parameters=[ OpenApiParameter( name="list_id", type=OpenApiTypes.INT, location=OpenApiParameter.PATH, description="ID of the top list to update", ), ], request=TopListSerializer, responses={ 200: TopListSerializer, 400: {"description": "Validation error"}, 404: {"description": "Top list not found"}, }, tags=["User Profile"], ) @api_view(["PATCH"]) @permission_classes([IsAuthenticated]) def update_top_list(request, list_id): """Update a top list.""" try: top_list = TopList.objects.get(id=list_id, user=request.user) except TopList.DoesNotExist: return Response( {"error": "Top list not found"}, status=status.HTTP_404_NOT_FOUND ) serializer = TopListSerializer(top_list, data=request.data, partial=True) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @extend_schema( operation_id="delete_top_list", summary="Delete a top list", description="Delete an existing top list owned by the authenticated user.", parameters=[ OpenApiParameter( name="list_id", type=OpenApiTypes.INT, location=OpenApiParameter.PATH, description="ID of the top list to delete", ), ], responses={ 204: {"description": "Top list deleted successfully"}, 404: {"description": "Top list not found"}, }, tags=["User Profile"], ) @api_view(["DELETE"]) @permission_classes([IsAuthenticated]) def delete_top_list(request, list_id): """Delete a top list.""" try: top_list = TopList.objects.get(id=list_id, user=request.user) top_list.delete() return Response(status=status.HTTP_204_NO_CONTENT) except TopList.DoesNotExist: return Response( {"error": "Top list not found"}, status=status.HTTP_404_NOT_FOUND ) # === NOTIFICATION ENDPOINTS === @extend_schema( operation_id="get_user_notifications", summary="Get user notifications", description="Get paginated list of notifications for the authenticated user.", parameters=[ OpenApiParameter( name="unread_only", type=OpenApiTypes.BOOL, location=OpenApiParameter.QUERY, description="Filter to only unread notifications", default=False, ), OpenApiParameter( name="notification_type", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY, description="Filter by notification type (SUBMISSION, REVIEW, SOCIAL, SYSTEM, ACHIEVEMENT)", required=False, ), OpenApiParameter( name="limit", type=OpenApiTypes.INT, location=OpenApiParameter.QUERY, description="Number of notifications to return (default: 20, max: 100)", default=20, ), OpenApiParameter( name="offset", type=OpenApiTypes.INT, location=OpenApiParameter.QUERY, description="Number of notifications to skip", default=0, ), ], responses={ 200: { "type": "object", "properties": { "count": {"type": "integer"}, "next": {"type": "string", "nullable": True}, "previous": {"type": "string", "nullable": True}, "results": {"type": "array", "items": UserNotificationSerializer}, "unread_count": {"type": "integer"}, }, }, 401: {"description": "Authentication required"}, }, tags=["Notifications"], ) @api_view(["GET"]) @permission_classes([IsAuthenticated]) def get_user_notifications(request): """Get user notifications with filtering and pagination.""" user = request.user # Get query parameters unread_only = request.GET.get("unread_only", "false").lower() == "true" notification_type = request.GET.get("notification_type") limit = min(int(request.GET.get("limit", 20)), 100) offset = int(request.GET.get("offset", 0)) # Build queryset queryset = UserNotification.objects.filter(user=user).order_by("-created_at") if unread_only: queryset = queryset.filter(is_read=False) if notification_type: queryset = queryset.filter(notification_type=notification_type) # Get total count and unread count total_count = queryset.count() unread_count = UserNotification.objects.filter(user=user, is_read=False).count() # Apply pagination notifications = queryset[offset : offset + limit] # Build pagination URLs request_url = request.build_absolute_uri().split("?")[0] next_url = None previous_url = None if offset + limit < total_count: next_params = request.GET.copy() next_params["offset"] = offset + limit next_url = f"{request_url}?{next_params.urlencode()}" if offset > 0: prev_params = request.GET.copy() prev_params["offset"] = max(0, offset - limit) previous_url = f"{request_url}?{prev_params.urlencode()}" # Serialize notifications serializer = UserNotificationSerializer(notifications, many=True) return Response( { "count": total_count, "next": next_url, "previous": previous_url, "results": serializer.data, "unread_count": unread_count, }, status=status.HTTP_200_OK, ) @extend_schema( operation_id="mark_notifications_read", summary="Mark notifications as read", description="Mark one or more notifications as read for the authenticated user.", request=MarkNotificationsReadSerializer, responses={ 200: { "type": "object", "properties": { "success": {"type": "boolean"}, "marked_count": {"type": "integer"}, "message": {"type": "string"}, }, "example": { "success": True, "marked_count": 5, "message": "5 notifications marked as read", }, }, 400: {"description": "Validation error"}, 401: {"description": "Authentication required"}, }, tags=["Notifications"], ) @api_view(["PATCH"]) @permission_classes([IsAuthenticated]) def mark_notifications_read(request): """Mark notifications as read.""" serializer = MarkNotificationsReadSerializer(data=request.data) if serializer.is_valid(): user = request.user notification_ids = serializer.validated_data.get("notification_ids") mark_all = serializer.validated_data.get("mark_all", False) if mark_all: # Mark all unread notifications as read updated_count = UserNotification.objects.filter( user=user, is_read=False ).update(is_read=True, read_at=timezone.now()) message = f"All {updated_count} unread notifications marked as read" elif notification_ids: # Mark specific notifications as read updated_count = UserNotification.objects.filter( user=user, id__in=notification_ids, is_read=False ).update(is_read=True, read_at=timezone.now()) message = f"{updated_count} notifications marked as read" else: return Response( {"error": "Either notification_ids or mark_all must be provided"}, status=status.HTTP_400_BAD_REQUEST, ) return Response( {"success": True, "marked_count": updated_count, "message": message}, status=status.HTTP_200_OK, ) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @extend_schema( operation_id="get_notification_preferences", summary="Get notification preferences", description="Get detailed notification preferences for the authenticated user.", responses={ 200: NotificationPreferenceSerializer, 401: {"description": "Authentication required"}, }, tags=["Notifications"], ) @api_view(["GET"]) @permission_classes([IsAuthenticated]) def get_notification_preferences(request): """Get notification preferences.""" user = request.user # Get or create notification preferences preferences, created = NotificationPreference.objects.get_or_create( user=user, defaults={ "email_enabled": True, "push_enabled": True, "in_app_enabled": True, "submission_notifications": {"email": True, "push": True, "in_app": True}, "review_notifications": {"email": True, "push": False, "in_app": True}, "social_notifications": {"email": False, "push": True, "in_app": True}, "system_notifications": {"email": True, "push": False, "in_app": True}, "achievement_notifications": {"email": False, "push": True, "in_app": True}, }, ) serializer = NotificationPreferenceSerializer(preferences) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( operation_id="update_notification_preferences", summary="Update notification preferences", description="Update detailed notification preferences for the authenticated user.", request=NotificationPreferenceSerializer, responses={ 200: NotificationPreferenceSerializer, 400: {"description": "Validation error"}, 401: {"description": "Authentication required"}, }, tags=["Notifications"], ) @api_view(["PATCH"]) @permission_classes([IsAuthenticated]) def update_notification_preferences(request): """Update notification preferences.""" user = request.user # Get or create notification preferences preferences, created = NotificationPreference.objects.get_or_create(user=user) serializer = NotificationPreferenceSerializer( preferences, data=request.data, partial=True ) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # === AVATAR ENDPOINTS === @extend_schema( operation_id="upload_avatar", summary="Upload user avatar", description="Upload a new avatar image for the authenticated user using Cloudflare Images.", request=AvatarUploadSerializer, responses={ 200: { "type": "object", "properties": { "success": {"type": "boolean"}, "message": {"type": "string"}, "avatar_url": {"type": "string"}, "avatar_variants": { "type": "object", "properties": { "thumbnail": {"type": "string"}, "avatar": {"type": "string"}, "large": {"type": "string"}, }, }, }, "example": { "success": True, "message": "Avatar uploaded successfully", "avatar_url": "https://imagedelivery.net/account-hash/image-id/avatar", "avatar_variants": { "thumbnail": "https://imagedelivery.net/account-hash/image-id/thumbnail", "avatar": "https://imagedelivery.net/account-hash/image-id/avatar", "large": "https://imagedelivery.net/account-hash/image-id/large", }, }, }, 400: {"description": "Validation error or upload failed"}, 401: {"description": "Authentication required"}, }, tags=["User Profile"], ) @api_view(["POST"]) @permission_classes([IsAuthenticated]) def upload_avatar(request): """Upload user avatar.""" user = request.user # Get or create user profile profile, created = UserProfile.objects.get_or_create(user=user) serializer = AvatarUploadSerializer(data=request.data) if serializer.is_valid(): avatar_file = serializer.validated_data["avatar"] try: # Update the profile with the new avatar profile.avatar = avatar_file profile.save() # Get avatar URLs avatar_url = profile.get_avatar_url() avatar_variants = profile.get_avatar_variants() return Response( { "success": True, "message": "Avatar uploaded successfully", "avatar_url": avatar_url, "avatar_variants": avatar_variants, }, status=status.HTTP_200_OK, ) except Exception as e: return Response( {"success": False, "error": f"Failed to upload avatar: {str(e)}"}, status=status.HTTP_400_BAD_REQUEST, ) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @extend_schema( operation_id="delete_avatar", summary="Delete user avatar", description="Delete the current avatar and revert to default letter-based avatar.", responses={ 200: { "type": "object", "properties": { "success": {"type": "boolean"}, "message": {"type": "string"}, "avatar_url": {"type": "string"}, }, "example": { "success": True, "message": "Avatar deleted successfully", "avatar_url": "https://ui-avatars.com/api/?name=J&size=200&background=random&color=fff&bold=true", }, }, 401: {"description": "Authentication required"}, }, tags=["User Profile"], ) @api_view(["DELETE"]) @permission_classes([IsAuthenticated]) def delete_avatar(request): """Delete user avatar.""" user = request.user try: profile = user.profile # Delete the avatar (this will also delete from Cloudflare) if profile.avatar: profile.avatar.delete() profile.avatar = None profile.save() # Get the default avatar URL avatar_url = profile.get_avatar_url() return Response( { "success": True, "message": "Avatar deleted successfully", "avatar_url": avatar_url, }, status=status.HTTP_200_OK, ) except UserProfile.DoesNotExist: return Response( { "success": True, "message": "No avatar to delete", "avatar_url": f"https://ui-avatars.com/api/?name={user.username[0].upper()}&size=200&background=random&color=fff&bold=true", }, status=status.HTTP_200_OK, ) except Exception as e: return Response( {"success": False, "error": f"Failed to delete avatar: {str(e)}"}, status=status.HTTP_400_BAD_REQUEST, )