Files
thrillwiki_django_no_react/apps/api/v1/accounts/views.py
pacnpal 75cc618c2b update
2025-09-21 20:04:42 -04:00

1626 lines
56 KiB
Python

"""
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,
)