""" 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 apps.api.v1.serializers.accounts import ( CompleteUserSerializer, UserPreferencesSerializer, NotificationSettingsSerializer, PrivacySettingsSerializer, SecuritySettingsSerializer, UserStatisticsSerializer, TopListSerializer, AccountUpdateSerializer, ProfileUpdateSerializer, ThemePreferenceSerializer, UserNotificationSerializer, NotificationPreferenceSerializer, MarkNotificationsReadSerializer, AvatarUploadSerializer, ) from apps.accounts.services import UserDeletionService from apps.accounts.models import ( User, UserProfile, TopList, UserNotification, NotificationPreference, ) import logging 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 django_cloudflareimages_toolkit.models import CloudflareImage # Set up logging logger = logging.getLogger(__name__) @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: # Log the attempt for security monitoring logger.warning( f"Admin user {request.user.username} attempted to delete protected user {user.username} (ID: {user_id}). Reason: {reason}", extra={ "admin_user": request.user.username, "target_user": user.username, "target_user_id": user_id, "is_superuser": user.is_superuser, "user_role": user.role, "rejection_reason": reason, } ) # Determine error code based on reason error_code = "DELETION_FORBIDDEN" if "superuser" in reason.lower(): error_code = "SUPERUSER_DELETION_FORBIDDEN" elif "admin" in reason.lower(): error_code = "ADMIN_DELETION_FORBIDDEN" elif "system" in reason.lower(): error_code = "SYSTEM_USER_DELETION_FORBIDDEN" return Response( { "success": False, "error": f"Cannot delete user: {reason}", "error_code": error_code, "user_info": { "username": user.username, "user_id": user.user_id, "role": user.role, "is_superuser": user.is_superuser, "is_staff": user.is_staff, }, "help_text": "Contact system administrator if you need to delete this account type.", }, status=status.HTTP_400_BAD_REQUEST, ) # Log the successful deletion attempt logger.info( f"Admin user {request.user.username} is deleting user {user.username} (ID: {user_id})", extra={ "admin_user": request.user.username, "target_user": user.username, "target_user_id": user_id, "action": "user_deletion", } ) # Perform the deletion result = UserDeletionService.delete_user_preserve_submissions(user) # Log successful deletion logger.info( f"Successfully deleted user {result['deleted_user']['username']} (ID: {user_id}) by admin {request.user.username}", extra={ "admin_user": request.user.username, "deleted_user": result['deleted_user']['username'], "deleted_user_id": user_id, "preserved_submissions": result['preserved_submissions'], "action": "user_deletion_completed", } ) return Response( { "success": True, "message": "User successfully deleted with submissions preserved", **result, }, status=status.HTTP_200_OK, ) except Exception as e: # Log the error for debugging logger.error( f"Error deleting user {user_id} by admin {request.user.username}: {str(e)}", extra={ "admin_user": request.user.username, "target_user_id": user_id, "error": str(e), "action": "user_deletion_error", }, exc_info=True ) return Response( { "success": False, "error": f"Error deleting user: {str(e)}", "error_code": "DELETION_ERROR", "help_text": "Please try again or contact system administrator if the problem persists.", }, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) @extend_schema( operation_id="save_avatar_image", summary="Save uploaded avatar image reference", description="Associate an uploaded Cloudflare image with the user's avatar after direct upload.", request={ "application/json": { "type": "object", "properties": { "cloudflare_image_id": { "type": "string", "description": "Cloudflare image ID from direct upload", "example": "uuid-here", } }, "required": ["cloudflare_image_id"], } }, 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 saved 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 image not found"}, 401: {"description": "Authentication required"}, }, tags=["User Profile"], ) @api_view(["POST"]) @permission_classes([IsAuthenticated]) def save_avatar_image(request): """Save uploaded avatar image reference after direct upload to Cloudflare.""" user = request.user try: cloudflare_image_id = request.data.get("cloudflare_image_id") if not cloudflare_image_id: return Response( {"success": False, "error": "cloudflare_image_id is required"}, status=status.HTTP_400_BAD_REQUEST, ) # Always fetch the latest image data from Cloudflare API from django_cloudflareimages_toolkit.services import CloudflareImagesService try: # Get image details from Cloudflare API service = CloudflareImagesService() image_data = service.get_image(cloudflare_image_id) if not image_data: return Response( {"success": False, "error": "Image not found in Cloudflare"}, status=status.HTTP_400_BAD_REQUEST, ) # Try to find existing CloudflareImage record by cloudflare_id cloudflare_image = None try: cloudflare_image = CloudflareImage.objects.get( cloudflare_id=cloudflare_image_id) # Update existing record with latest data from Cloudflare cloudflare_image.status = 'uploaded' cloudflare_image.uploaded_at = timezone.now() cloudflare_image.metadata = image_data.get('meta', {}) # Extract variants from nested result structure cloudflare_image.variants = image_data.get('result', {}).get('variants', []) cloudflare_image.cloudflare_metadata = image_data cloudflare_image.width = image_data.get('width') cloudflare_image.height = image_data.get('height') cloudflare_image.format = image_data.get('format', '') cloudflare_image.save() except CloudflareImage.DoesNotExist: # Create new CloudflareImage record from API response cloudflare_image = CloudflareImage.objects.create( cloudflare_id=cloudflare_image_id, user=user, status='uploaded', upload_url='', # Not needed for uploaded images expires_at=timezone.now() + timezone.timedelta(days=365), # Set far future expiry uploaded_at=timezone.now(), metadata=image_data.get('meta', {}), # Extract variants from nested result structure variants=image_data.get('result', {}).get('variants', []), cloudflare_metadata=image_data, width=image_data.get('width'), height=image_data.get('height'), format=image_data.get('format', ''), ) except Exception as api_error: logger.error( f"Error fetching image from Cloudflare API: {str(api_error)}", exc_info=True) return Response( {"success": False, "error": f"Failed to fetch image from Cloudflare: {str(api_error)}"}, status=status.HTTP_400_BAD_REQUEST, ) # Get or create user profile profile, created = UserProfile.objects.get_or_create(user=user) # Store reference to old avatar for cleanup after successful upload old_avatar = None if profile.avatar and profile.avatar != cloudflare_image: old_avatar = profile.avatar # Associate the new image with the user's profile first profile.avatar = cloudflare_image profile.save() # Now delete the old avatar after successful association (both from Cloudflare and database) if old_avatar: try: service.delete_image(old_avatar) logger.info(f"Successfully deleted old avatar from Cloudflare: {old_avatar.cloudflare_id}") except Exception as e: logger.error(f"Failed to delete old avatar from Cloudflare: {str(e)}") # Continue with database deletion even if Cloudflare deletion fails old_avatar.delete() # Debug logging to see what's happening with the CloudflareImage logger.info("CloudflareImage debug info:") logger.info(f" ID: {cloudflare_image.id}") logger.info(f" cloudflare_id: {cloudflare_image.cloudflare_id}") logger.info(f" status: {cloudflare_image.status}") logger.info(f" is_uploaded: {cloudflare_image.is_uploaded}") logger.info(f" variants: {cloudflare_image.variants}") logger.info(f" cloudflare_metadata: {cloudflare_image.cloudflare_metadata}") # Get avatar URLs avatar_url = profile.get_avatar_url() avatar_variants = profile.get_avatar_variants() # More debug logging logger.info("Avatar URL generation:") logger.info(f" avatar_url: {avatar_url}") logger.info(f" avatar_variants: {avatar_variants}") return Response( { "success": True, "message": "Avatar saved successfully", "avatar_url": avatar_url, "avatar_variants": avatar_variants, }, status=status.HTTP_200_OK, ) except Exception as e: logger.error(f"Error saving avatar image: {str(e)}", exc_info=True) return Response( {"success": False, "error": f"Failed to save avatar: {str(e)}"}, 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 (both from Cloudflare and database) if profile.avatar: avatar_to_delete = profile.avatar profile.avatar = None profile.save() # Delete from Cloudflare first, then from database try: from django_cloudflareimages_toolkit.services import CloudflareImagesService service = CloudflareImagesService() service.delete_image(avatar_to_delete) logger.info(f"Successfully deleted avatar from Cloudflare: {avatar_to_delete.cloudflare_id}") except Exception as e: logger.error(f"Failed to delete avatar from Cloudflare: {str(e)}") # Continue with database deletion even if Cloudflare deletion fails avatar_to_delete.delete() # 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, ) @extend_schema( operation_id="request_account_deletion", summary="Request account deletion", description="Request deletion of the authenticated user's account.", responses={ 200: {"description": "Deletion request created"}, 400: {"description": "Cannot delete account"}, }, tags=["Self-Service Account Management"], ) @api_view(["POST"]) @permission_classes([IsAuthenticated]) def request_account_deletion(request): """Request account deletion.""" try: user = request.user # Check if user can be deleted can_delete, reason = UserDeletionService.can_delete_user(user) if not can_delete: return Response( {"success": False, "error": reason}, status=status.HTTP_400_BAD_REQUEST, ) # Create deletion request deletion_request = UserDeletionService.create_deletion_request(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: # Log the rejection for security monitoring logger.warning( f"User {request.user.username} (ID: {request.user.user_id}) attempted self-deletion but was rejected: {str(e)}", extra={ "user": request.user.username, "user_id": request.user.user_id, "is_superuser": request.user.is_superuser, "user_role": request.user.role, "rejection_reason": str(e), "action": "self_deletion_rejected", } ) # Determine error code based on reason error_message = str(e) error_code = "DELETION_FORBIDDEN" if "superuser" in error_message.lower(): error_code = "SUPERUSER_DELETION_FORBIDDEN" elif "admin" in error_message.lower(): error_code = "ADMIN_DELETION_FORBIDDEN" elif "system" in error_message.lower(): error_code = "SYSTEM_USER_DELETION_FORBIDDEN" return Response( { "success": False, "error": error_message, "error_code": error_code, "user_info": { "username": request.user.username, "user_id": request.user.user_id, "role": request.user.role, "is_superuser": request.user.is_superuser, "is_staff": request.user.is_staff, }, "help_text": "Superuser and admin accounts cannot be self-deleted for security reasons. Contact system administrator if you need to delete this account.", }, status=status.HTTP_400_BAD_REQUEST, ) except Exception as e: # Log the error for debugging logger.error( f"Error creating deletion request for user {request.user.username} (ID: {request.user.user_id}): {str(e)}", extra={ "user": request.user.username, "user_id": request.user.user_id, "error": str(e), "action": "self_deletion_error", }, exc_info=True ) return Response( { "success": False, "error": f"Error creating deletion request: {str(e)}", "error_code": "DELETION_REQUEST_ERROR", "help_text": "Please try again or contact support if the problem persists.", }, 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), "weekly_digest": prefs.get("push_weekly_digest", False), "new_features": prefs.get("push_new_features", False), "security_alerts": prefs.get("push_security_alerts", 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), "weekly_digest": prefs.get("inapp_weekly_digest", False), "new_features": prefs.get("inapp_new_features", True), "security_alerts": prefs.get("inapp_security_alerts", True), }, } serializer = NotificationSettingsSerializer(data=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 or empty dict current_prefs = user.notification_preferences or {} # Update preferences from request data if "email_notifications" in request.data: email_prefs = request.data["email_notifications"] for key, value in email_prefs.items(): current_prefs[f"email_{key}"] = value if "push_notifications" in request.data: push_prefs = request.data["push_notifications"] for key, value in push_prefs.items(): current_prefs[f"push_{key}"] = value if "in_app_notifications" in request.data: inapp_prefs = request.data["in_app_notifications"] for key, value in inapp_prefs.items(): current_prefs[f"inapp_{key}"] = value # Save updated preferences user.notification_preferences = current_prefs user.save() # Return updated data return get_notification_settings(request) # === PRIVACY SETTINGS ENDPOINTS === @extend_schema( operation_id="get_privacy_settings", summary="Get privacy settings", description="Get privacy 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 = { "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 = PrivacySettingsSerializer(data=data) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( operation_id="update_privacy_settings", summary="Update privacy settings", description="Update privacy 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 = { "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 = PrivacySettingsSerializer(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) # === SECURITY SETTINGS ENDPOINTS === @extend_schema( operation_id="get_security_settings", summary="Get security settings", description="Get security 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 data = { "two_factor_enabled": getattr(user, "two_factor_enabled", False), "login_notifications": getattr(user, "login_notifications", True), "password_last_changed": user.password_last_changed if hasattr(user, "password_last_changed") else None, "active_sessions": getattr(user, "active_sessions", 1), } serializer = SecuritySettingsSerializer(data=data) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( operation_id="update_security_settings", summary="Update security settings", description="Update security 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 # Handle security settings updates if "two_factor_enabled" in request.data: setattr(user, "two_factor_enabled", request.data["two_factor_enabled"]) if "login_notifications" in request.data: setattr(user, "login_notifications", request.data["login_notifications"]) user.save() # Return updated settings return get_security_settings(request) # === USER STATISTICS ENDPOINTS === @extend_schema( operation_id="get_user_statistics", summary="Get user statistics", description="Get statistics 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 # Calculate user statistics data = { "parks_visited": 0, # TODO: Implement based on reviews/check-ins "rides_ridden": 0, # TODO: Implement based on reviews/check-ins "reviews_written": 0, # TODO: Count user's reviews "photos_uploaded": 0, # TODO: Count user's photos "top_lists_created": TopList.objects.filter(user=user).count(), "member_since": user.date_joined, "last_activity": user.last_login, } serializer = UserStatisticsSerializer(data=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: TopListSerializer(many=True), 401: {"description": "Authentication required"}, }, tags=["User Content"], ) @api_view(["GET"]) @permission_classes([IsAuthenticated]) def get_user_top_lists(request): """Get user's top lists.""" top_lists = TopList.objects.filter(user=request.user).order_by("-created_at") 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 Content"], ) @api_view(["POST"]) @permission_classes([IsAuthenticated]) def create_top_list(request): """Create a new top list.""" serializer = TopListSerializer(data=request.data, context={"request": request}) 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 a top list owned by the authenticated user.", request=TopListSerializer, responses={ 200: TopListSerializer, 400: {"description": "Validation error"}, 404: {"description": "Top list not found"}, }, tags=["User Content"], ) @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, context={"request": request} ) 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 a top list owned by the authenticated user.", responses={ 204: None, 404: {"description": "Top list not found"}, }, tags=["User Content"], ) @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 notifications for the authenticated user.", responses={ 200: UserNotificationSerializer(many=True), 401: {"description": "Authentication required"}, }, tags=["User Notifications"], ) @api_view(["GET"]) @permission_classes([IsAuthenticated]) def get_user_notifications(request): """Get user notifications.""" notifications = UserNotification.objects.filter( user=request.user ).order_by("-created_at")[:50] # Limit to 50 most recent serializer = UserNotificationSerializer(notifications, many=True) return Response(serializer.data, 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.", request=MarkNotificationsReadSerializer, responses={ 200: {"description": "Notifications marked as read"}, 400: {"description": "Validation error"}, }, tags=["User 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(): notification_ids = serializer.validated_data.get("notification_ids", []) mark_all = serializer.validated_data.get("mark_all", False) if mark_all: UserNotification.objects.filter( user=request.user, is_read=False ).update(is_read=True, read_at=timezone.now()) count = UserNotification.objects.filter(user=request.user).count() else: count = UserNotification.objects.filter( id__in=notification_ids, user=request.user, is_read=False ).update(is_read=True, read_at=timezone.now()) return Response( {"message": f"Marked {count} notifications as read"}, 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 notification preferences for the authenticated user.", responses={ 200: NotificationPreferenceSerializer, 401: {"description": "Authentication required"}, }, tags=["User Settings"], ) @api_view(["GET"]) @permission_classes([IsAuthenticated]) def get_notification_preferences(request): """Get notification preferences.""" try: preferences = NotificationPreference.objects.get(user=request.user) except NotificationPreference.DoesNotExist: # Create default preferences preferences = NotificationPreference.objects.create(user=request.user) 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 notification preferences for the authenticated user.", request=NotificationPreferenceSerializer, responses={ 200: NotificationPreferenceSerializer, 400: {"description": "Validation error"}, }, tags=["User Settings"], ) @api_view(["PATCH"]) @permission_classes([IsAuthenticated]) def update_notification_preferences(request): """Update notification preferences.""" try: preferences = NotificationPreference.objects.get(user=request.user) except NotificationPreference.DoesNotExist: preferences = NotificationPreference.objects.create(user=request.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.", request=AvatarUploadSerializer, responses={ 200: {"description": "Avatar uploaded successfully"}, 400: {"description": "Validation error"}, }, tags=["User Profile"], ) @api_view(["POST"]) @permission_classes([IsAuthenticated]) def upload_avatar(request): """Upload user avatar.""" serializer = AvatarUploadSerializer(data=request.data) if serializer.is_valid(): # Handle avatar upload logic here # This would typically involve saving the file and updating the user profile return Response( {"message": "Avatar uploaded successfully"}, status=status.HTTP_200_OK ) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # === MISSING FUNCTION IMPLEMENTATIONS === @extend_schema( operation_id="request_account_deletion", summary="Request account deletion", description="Request deletion of the authenticated user's account.", responses={ 200: {"description": "Deletion request created"}, 400: {"description": "Cannot delete account"}, }, tags=["Self-Service Account Management"], ) @api_view(["POST"]) @permission_classes([IsAuthenticated]) def request_account_deletion(request): """Request account deletion.""" try: user = request.user # Check if user can be deleted can_delete, reason = UserDeletionService.can_delete_user(user) if not can_delete: return Response( {"success": False, "error": reason}, status=status.HTTP_400_BAD_REQUEST, ) # Create deletion request deletion_request = UserDeletionService.create_deletion_request(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, )