mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 07:51:09 -05:00
- Added migration to transition avatar data from CloudflareImageField to ForeignKey structure in UserProfile. - Fixed UserProfileEvent avatar field to align with new avatar structure. - Created serializers for social authentication, including connected and available providers. - Developed request logging middleware for comprehensive request/response logging. - Updated moderation and parks migrations to remove outdated triggers and adjust foreign key relationships. - Enhanced rides migrations to ensure proper handling of image uploads and triggers. - Introduced a test script for the 3-step avatar upload process, ensuring functionality with Cloudflare. - Documented the fix for avatar upload issues, detailing root cause, implementation, and verification steps. - Implemented automatic deletion of Cloudflare images upon avatar, park, and ride photo changes or removals.
1627 lines
56 KiB
Python
1627 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
|
|
import json
|
|
|
|
# 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(f"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(f"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,
|
|
)
|