mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 06:11:07 -05:00
- Remove first_name and last_name fields from User model - Add user deletion and social provider services - Restructure auth serializers into separate directory - Update avatar upload functionality and API endpoints - Remove django-moderation integration documentation - Add mandatory compliance enforcement rules - Update frontend documentation with API usage examples
1736 lines
59 KiB
Python
1736 lines
59 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
|
|
|
|
# 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="request_account_deletion",
|
|
summary="Request account deletion with email verification",
|
|
description=(
|
|
"Request to delete your own account. A verification code will be sent "
|
|
"to your email address. The account will only be deleted after you "
|
|
"provide the correct verification code."
|
|
),
|
|
responses={
|
|
200: {
|
|
"description": "Deletion request created and verification email sent",
|
|
"example": {
|
|
"success": True,
|
|
"message": "Verification code sent to your email",
|
|
"expires_at": "2024-01-16T10:30:00Z",
|
|
"email": "user@example.com",
|
|
},
|
|
},
|
|
400: {
|
|
"description": "Bad request - user cannot be deleted",
|
|
"example": {
|
|
"success": False,
|
|
"error": "Cannot delete user: Cannot delete superuser accounts",
|
|
},
|
|
},
|
|
401: {
|
|
"description": "Authentication required",
|
|
"example": {"success": False, "error": "Authentication required"},
|
|
},
|
|
},
|
|
tags=["Self-Service Account Management"],
|
|
)
|
|
@api_view(["POST"])
|
|
@permission_classes([IsAuthenticated])
|
|
def request_account_deletion(request):
|
|
"""
|
|
Request deletion of your own account with email verification.
|
|
|
|
This endpoint allows authenticated users to request deletion of their own
|
|
account. A verification code will be sent to their email address, and the
|
|
account will only be deleted after they provide the correct code.
|
|
|
|
**Authentication Required**: User must be logged in .
|
|
|
|
**Email Verification**: A verification code is sent to the user's email.
|
|
|
|
**Submission Preservation**: All user submissions will be preserved.
|
|
"""
|
|
try:
|
|
user = request.user
|
|
|
|
# Create deletion request and send email
|
|
deletion_request = UserDeletionService.request_user_deletion(user)
|
|
|
|
# Log the self-service deletion request
|
|
logger.info(
|
|
f"User {user.username} (ID: {user.user_id}) requested account deletion",
|
|
extra={
|
|
"user": user.username,
|
|
"user_id": user.user_id,
|
|
"email": user.email,
|
|
"action": "self_deletion_request",
|
|
}
|
|
)
|
|
|
|
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),
|
|
},
|
|
"in_app_notifications": {
|
|
"new_reviews": prefs.get("inapp_new_reviews", True),
|
|
"review_replies": prefs.get("inapp_review_replies", True),
|
|
"friend_requests": prefs.get("inapp_friend_requests", True),
|
|
"messages": prefs.get("inapp_messages", True),
|
|
"system_announcements": prefs.get("inapp_system_announcements", True),
|
|
},
|
|
}
|
|
|
|
serializer = NotificationSettingsSerializer(data)
|
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
|
|
|
|
@extend_schema(
|
|
operation_id="update_notification_settings",
|
|
summary="Update notification settings",
|
|
description="Update detailed notification preferences for the authenticated user.",
|
|
request=NotificationSettingsSerializer,
|
|
responses={
|
|
200: NotificationSettingsSerializer,
|
|
400: {"description": "Validation error"},
|
|
},
|
|
tags=["User Settings"],
|
|
)
|
|
@api_view(["PATCH"])
|
|
@permission_classes([IsAuthenticated])
|
|
def update_notification_settings(request):
|
|
"""Update notification settings."""
|
|
user = request.user
|
|
|
|
# Get current preferences
|
|
current_prefs = user.notification_preferences or {}
|
|
|
|
# Build current data structure
|
|
current_data = {
|
|
"email_notifications": {
|
|
"new_reviews": current_prefs.get("email_new_reviews", True),
|
|
"review_replies": current_prefs.get("email_review_replies", True),
|
|
"friend_requests": current_prefs.get("email_friend_requests", True),
|
|
"messages": current_prefs.get("email_messages", True),
|
|
"weekly_digest": current_prefs.get("email_weekly_digest", False),
|
|
"new_features": current_prefs.get("email_new_features", True),
|
|
"security_alerts": current_prefs.get("email_security_alerts", True),
|
|
},
|
|
"push_notifications": {
|
|
"new_reviews": current_prefs.get("push_new_reviews", False),
|
|
"review_replies": current_prefs.get("push_review_replies", True),
|
|
"friend_requests": current_prefs.get("push_friend_requests", True),
|
|
"messages": current_prefs.get("push_messages", True),
|
|
},
|
|
"in_app_notifications": {
|
|
"new_reviews": current_prefs.get("inapp_new_reviews", True),
|
|
"review_replies": current_prefs.get("inapp_review_replies", True),
|
|
"friend_requests": current_prefs.get("inapp_friend_requests", True),
|
|
"messages": current_prefs.get("inapp_messages", True),
|
|
"system_announcements": current_prefs.get(
|
|
"inapp_system_announcements", True
|
|
),
|
|
},
|
|
}
|
|
|
|
# Merge with request data
|
|
if "email_notifications" in request.data and request.data["email_notifications"]:
|
|
current_data["email_notifications"].update(request.data["email_notifications"])
|
|
if "push_notifications" in request.data and request.data["push_notifications"]:
|
|
current_data["push_notifications"].update(request.data["push_notifications"])
|
|
if "in_app_notifications" in request.data and request.data["in_app_notifications"]:
|
|
current_data["in_app_notifications"].update(
|
|
request.data["in_app_notifications"]
|
|
)
|
|
|
|
serializer = NotificationSettingsSerializer(data=current_data)
|
|
|
|
if serializer.is_valid():
|
|
# Convert back to flat structure for storage
|
|
validated_data = serializer.validated_data
|
|
new_prefs = {}
|
|
|
|
# Email notifications
|
|
for key, value in validated_data["email_notifications"].items():
|
|
new_prefs[f"email_{key}"] = value
|
|
|
|
# Push notifications
|
|
for key, value in validated_data["push_notifications"].items():
|
|
new_prefs[f"push_{key}"] = value
|
|
|
|
# In-app notifications
|
|
for key, value in validated_data["in_app_notifications"].items():
|
|
new_prefs[f"inapp_{key}"] = value
|
|
|
|
# Update user preferences
|
|
user.notification_preferences = new_prefs
|
|
user.save()
|
|
|
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
# === PRIVACY SETTINGS ENDPOINTS ===
|
|
|
|
|
|
@extend_schema(
|
|
operation_id="get_privacy_settings",
|
|
summary="Get privacy settings",
|
|
description="Get privacy and visibility settings for the authenticated user.",
|
|
responses={
|
|
200: PrivacySettingsSerializer,
|
|
401: {"description": "Authentication required"},
|
|
},
|
|
tags=["User Settings"],
|
|
)
|
|
@api_view(["GET"])
|
|
@permission_classes([IsAuthenticated])
|
|
def get_privacy_settings(request):
|
|
"""Get privacy settings."""
|
|
user = request.user
|
|
data = {
|
|
"profile_visibility": user.privacy_level,
|
|
"show_email": user.show_email,
|
|
"show_real_name": user.show_real_name,
|
|
"show_join_date": user.show_join_date,
|
|
"show_statistics": user.show_statistics,
|
|
"show_reviews": user.show_reviews,
|
|
"show_photos": user.show_photos,
|
|
"show_top_lists": user.show_top_lists,
|
|
"allow_friend_requests": user.allow_friend_requests,
|
|
"allow_messages": user.allow_messages,
|
|
"allow_profile_comments": user.allow_profile_comments,
|
|
"search_visibility": user.search_visibility,
|
|
"activity_visibility": user.activity_visibility,
|
|
}
|
|
|
|
serializer = PrivacySettingsSerializer(data)
|
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
|
|
|
|
@extend_schema(
|
|
operation_id="update_privacy_settings",
|
|
summary="Update privacy settings",
|
|
description="Update privacy and visibility settings for the authenticated user.",
|
|
request=PrivacySettingsSerializer,
|
|
responses={
|
|
200: PrivacySettingsSerializer,
|
|
400: {"description": "Validation error"},
|
|
},
|
|
tags=["User Settings"],
|
|
)
|
|
@api_view(["PATCH"])
|
|
@permission_classes([IsAuthenticated])
|
|
def update_privacy_settings(request):
|
|
"""Update privacy settings."""
|
|
user = request.user
|
|
current_data = {
|
|
"profile_visibility": user.privacy_level,
|
|
"show_email": user.show_email,
|
|
"show_real_name": user.show_real_name,
|
|
"show_join_date": user.show_join_date,
|
|
"show_statistics": user.show_statistics,
|
|
"show_reviews": user.show_reviews,
|
|
"show_photos": user.show_photos,
|
|
"show_top_lists": user.show_top_lists,
|
|
"allow_friend_requests": user.allow_friend_requests,
|
|
"allow_messages": user.allow_messages,
|
|
"allow_profile_comments": user.allow_profile_comments,
|
|
"search_visibility": user.search_visibility,
|
|
"activity_visibility": user.activity_visibility,
|
|
}
|
|
|
|
serializer = PrivacySettingsSerializer(data={**current_data, **request.data})
|
|
|
|
if serializer.is_valid():
|
|
# Update user fields (map profile_visibility to privacy_level)
|
|
for field, value in serializer.validated_data.items():
|
|
if field == "profile_visibility":
|
|
user.privacy_level = value
|
|
else:
|
|
setattr(user, field, value)
|
|
user.save()
|
|
|
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
# === SECURITY SETTINGS ENDPOINTS ===
|
|
|
|
|
|
@extend_schema(
|
|
operation_id="get_security_settings",
|
|
summary="Get security settings",
|
|
description="Get security and authentication settings for the authenticated user.",
|
|
responses={
|
|
200: SecuritySettingsSerializer,
|
|
401: {"description": "Authentication required"},
|
|
},
|
|
tags=["User Settings"],
|
|
)
|
|
@api_view(["GET"])
|
|
@permission_classes([IsAuthenticated])
|
|
def get_security_settings(request):
|
|
"""Get security settings."""
|
|
user = request.user
|
|
|
|
# TODO: Implement active sessions count
|
|
active_sessions = 1 # Placeholder
|
|
|
|
data = {
|
|
"two_factor_enabled": user.two_factor_enabled,
|
|
"login_notifications": user.login_notifications,
|
|
"session_timeout": user.session_timeout,
|
|
"require_password_change": False, # TODO: Implement logic
|
|
"last_password_change": user.last_password_change,
|
|
"active_sessions": active_sessions,
|
|
"login_history_retention": user.login_history_retention,
|
|
}
|
|
|
|
serializer = SecuritySettingsSerializer(data)
|
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
|
|
|
|
@extend_schema(
|
|
operation_id="update_security_settings",
|
|
summary="Update security settings",
|
|
description="Update security and authentication settings for the authenticated user.",
|
|
request=SecuritySettingsSerializer,
|
|
responses={
|
|
200: SecuritySettingsSerializer,
|
|
400: {"description": "Validation error"},
|
|
},
|
|
tags=["User Settings"],
|
|
)
|
|
@api_view(["PATCH"])
|
|
@permission_classes([IsAuthenticated])
|
|
def update_security_settings(request):
|
|
"""Update security settings."""
|
|
user = request.user
|
|
|
|
# Get current data
|
|
active_sessions = 1 # Placeholder
|
|
current_data = {
|
|
"two_factor_enabled": user.two_factor_enabled,
|
|
"login_notifications": user.login_notifications,
|
|
"session_timeout": user.session_timeout,
|
|
"require_password_change": False,
|
|
"last_password_change": user.last_password_change,
|
|
"active_sessions": active_sessions,
|
|
"login_history_retention": user.login_history_retention,
|
|
}
|
|
|
|
serializer = SecuritySettingsSerializer(data={**current_data, **request.data})
|
|
|
|
if serializer.is_valid():
|
|
# Update only writable fields
|
|
writable_fields = [
|
|
"two_factor_enabled",
|
|
"login_notifications",
|
|
"session_timeout",
|
|
"login_history_retention",
|
|
]
|
|
for field in writable_fields:
|
|
if field in serializer.validated_data:
|
|
setattr(user, field, serializer.validated_data[field])
|
|
user.save()
|
|
|
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
# === USER STATISTICS ENDPOINTS ===
|
|
|
|
|
|
@extend_schema(
|
|
operation_id="get_user_statistics",
|
|
summary="Get user statistics",
|
|
description="Get comprehensive statistics and achievements for the authenticated user.",
|
|
responses={
|
|
200: UserStatisticsSerializer,
|
|
401: {"description": "Authentication required"},
|
|
},
|
|
tags=["User Profile"],
|
|
)
|
|
@api_view(["GET"])
|
|
@permission_classes([IsAuthenticated])
|
|
def get_user_statistics(request):
|
|
"""Get user statistics."""
|
|
user = request.user
|
|
profile = getattr(user, "profile", None)
|
|
|
|
# Ride credits
|
|
ride_credits = {
|
|
"coaster_credits": profile.coaster_credits if profile else 0,
|
|
"dark_ride_credits": profile.dark_ride_credits if profile else 0,
|
|
"flat_ride_credits": profile.flat_ride_credits if profile else 0,
|
|
"water_ride_credits": profile.water_ride_credits if profile else 0,
|
|
}
|
|
ride_credits["total_credits"] = sum(ride_credits.values())
|
|
|
|
# Contributions (placeholder counts - would need actual related models)
|
|
contributions = {
|
|
"park_reviews": getattr(
|
|
user, "park_reviews", user.__class__.objects.none()
|
|
).count(),
|
|
"ride_reviews": getattr(
|
|
user, "ride_reviews", user.__class__.objects.none()
|
|
).count(),
|
|
"photos_uploaded": getattr(
|
|
user, "uploaded_park_photos", user.__class__.objects.none()
|
|
).count()
|
|
+ getattr(user, "uploaded_ride_photos", user.__class__.objects.none()).count(),
|
|
"top_lists_created": user.top_lists.count(),
|
|
"helpful_votes_received": 0, # TODO: Implement when review voting is added
|
|
}
|
|
|
|
# Activity
|
|
activity = {
|
|
"days_active": (timezone.now().date() - user.date_joined.date()).days,
|
|
"last_active": user.last_login or user.date_joined,
|
|
"average_review_rating": 4.0, # TODO: Calculate from actual reviews
|
|
"most_reviewed_park": "Cedar Point", # TODO: Calculate from actual reviews
|
|
"favorite_ride_type": "Roller Coaster", # TODO: Calculate from ride credits
|
|
}
|
|
|
|
# Achievements (placeholder logic)
|
|
achievements = {
|
|
"first_review": contributions["park_reviews"] > 0
|
|
or contributions["ride_reviews"] > 0,
|
|
"photo_contributor": contributions["photos_uploaded"] > 0,
|
|
"top_reviewer": contributions["park_reviews"] + contributions["ride_reviews"]
|
|
>= 50,
|
|
"park_explorer": contributions["park_reviews"] >= 10,
|
|
"coaster_enthusiast": ride_credits["coaster_credits"] >= 100,
|
|
}
|
|
|
|
data = {
|
|
"ride_credits": ride_credits,
|
|
"contributions": contributions,
|
|
"activity": activity,
|
|
"achievements": achievements,
|
|
}
|
|
|
|
serializer = UserStatisticsSerializer(data)
|
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
|
|
|
|
# === TOP LISTS ENDPOINTS ===
|
|
|
|
|
|
@extend_schema(
|
|
operation_id="get_user_top_lists",
|
|
summary="Get user's top lists",
|
|
description="Get all top lists created by the authenticated user.",
|
|
responses={
|
|
200: {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {"type": "integer"},
|
|
"title": {"type": "string"},
|
|
"category": {"type": "string"},
|
|
"description": {"type": "string"},
|
|
"created_at": {"type": "string", "format": "date-time"},
|
|
"updated_at": {"type": "string", "format": "date-time"},
|
|
"items_count": {"type": "integer"},
|
|
},
|
|
},
|
|
},
|
|
401: {"description": "Authentication required"},
|
|
},
|
|
tags=["User Profile"],
|
|
)
|
|
@api_view(["GET"])
|
|
@permission_classes([IsAuthenticated])
|
|
def get_user_top_lists(request):
|
|
"""Get user's top lists."""
|
|
top_lists = request.user.top_lists.all()
|
|
serializer = TopListSerializer(top_lists, many=True)
|
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
|
|
|
|
@extend_schema(
|
|
operation_id="create_top_list",
|
|
summary="Create a new top list",
|
|
description="Create a new top list for the authenticated user.",
|
|
request=TopListSerializer,
|
|
responses={201: TopListSerializer, 400: {"description": "Validation error"}},
|
|
tags=["User Profile"],
|
|
)
|
|
@api_view(["POST"])
|
|
@permission_classes([IsAuthenticated])
|
|
def create_top_list(request):
|
|
"""Create a new top list."""
|
|
serializer = TopListSerializer(data=request.data)
|
|
|
|
if serializer.is_valid():
|
|
serializer.save(user=request.user)
|
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
|
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
@extend_schema(
|
|
operation_id="update_top_list",
|
|
summary="Update a top list",
|
|
description="Update an existing top list owned by the authenticated user.",
|
|
parameters=[
|
|
OpenApiParameter(
|
|
name="list_id",
|
|
type=OpenApiTypes.INT,
|
|
location=OpenApiParameter.PATH,
|
|
description="ID of the top list to update",
|
|
),
|
|
],
|
|
request=TopListSerializer,
|
|
responses={
|
|
200: TopListSerializer,
|
|
400: {"description": "Validation error"},
|
|
404: {"description": "Top list not found"},
|
|
},
|
|
tags=["User Profile"],
|
|
)
|
|
@api_view(["PATCH"])
|
|
@permission_classes([IsAuthenticated])
|
|
def update_top_list(request, list_id):
|
|
"""Update a top list."""
|
|
try:
|
|
top_list = TopList.objects.get(id=list_id, user=request.user)
|
|
except TopList.DoesNotExist:
|
|
return Response(
|
|
{"error": "Top list not found"}, status=status.HTTP_404_NOT_FOUND
|
|
)
|
|
|
|
serializer = TopListSerializer(top_list, data=request.data, partial=True)
|
|
|
|
if serializer.is_valid():
|
|
serializer.save()
|
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
@extend_schema(
|
|
operation_id="delete_top_list",
|
|
summary="Delete a top list",
|
|
description="Delete an existing top list owned by the authenticated user.",
|
|
parameters=[
|
|
OpenApiParameter(
|
|
name="list_id",
|
|
type=OpenApiTypes.INT,
|
|
location=OpenApiParameter.PATH,
|
|
description="ID of the top list to delete",
|
|
),
|
|
],
|
|
responses={
|
|
204: {"description": "Top list deleted successfully"},
|
|
404: {"description": "Top list not found"},
|
|
},
|
|
tags=["User Profile"],
|
|
)
|
|
@api_view(["DELETE"])
|
|
@permission_classes([IsAuthenticated])
|
|
def delete_top_list(request, list_id):
|
|
"""Delete a top list."""
|
|
try:
|
|
top_list = TopList.objects.get(id=list_id, user=request.user)
|
|
top_list.delete()
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
except TopList.DoesNotExist:
|
|
return Response(
|
|
{"error": "Top list not found"}, status=status.HTTP_404_NOT_FOUND
|
|
)
|
|
|
|
|
|
# === NOTIFICATION ENDPOINTS ===
|
|
|
|
|
|
@extend_schema(
|
|
operation_id="get_user_notifications",
|
|
summary="Get user notifications",
|
|
description="Get paginated list of notifications for the authenticated user.",
|
|
parameters=[
|
|
OpenApiParameter(
|
|
name="unread_only",
|
|
type=OpenApiTypes.BOOL,
|
|
location=OpenApiParameter.QUERY,
|
|
description="Filter to only unread notifications",
|
|
default=False,
|
|
),
|
|
OpenApiParameter(
|
|
name="notification_type",
|
|
type=OpenApiTypes.STR,
|
|
location=OpenApiParameter.QUERY,
|
|
description="Filter by notification type (SUBMISSION, REVIEW, SOCIAL, SYSTEM, ACHIEVEMENT)",
|
|
required=False,
|
|
),
|
|
OpenApiParameter(
|
|
name="limit",
|
|
type=OpenApiTypes.INT,
|
|
location=OpenApiParameter.QUERY,
|
|
description="Number of notifications to return (default: 20, max: 100)",
|
|
default=20,
|
|
),
|
|
OpenApiParameter(
|
|
name="offset",
|
|
type=OpenApiTypes.INT,
|
|
location=OpenApiParameter.QUERY,
|
|
description="Number of notifications to skip",
|
|
default=0,
|
|
),
|
|
],
|
|
responses={
|
|
200: {
|
|
"type": "object",
|
|
"properties": {
|
|
"count": {"type": "integer"},
|
|
"next": {"type": "string", "nullable": True},
|
|
"previous": {"type": "string", "nullable": True},
|
|
"results": {"type": "array", "items": UserNotificationSerializer},
|
|
"unread_count": {"type": "integer"},
|
|
},
|
|
},
|
|
401: {"description": "Authentication required"},
|
|
},
|
|
tags=["Notifications"],
|
|
)
|
|
@api_view(["GET"])
|
|
@permission_classes([IsAuthenticated])
|
|
def get_user_notifications(request):
|
|
"""Get user notifications with filtering and pagination."""
|
|
user = request.user
|
|
|
|
# Get query parameters
|
|
unread_only = request.GET.get("unread_only", "false").lower() == "true"
|
|
notification_type = request.GET.get("notification_type")
|
|
limit = min(int(request.GET.get("limit", 20)), 100)
|
|
offset = int(request.GET.get("offset", 0))
|
|
|
|
# Build queryset
|
|
queryset = UserNotification.objects.filter(user=user).order_by("-created_at")
|
|
|
|
if unread_only:
|
|
queryset = queryset.filter(is_read=False)
|
|
|
|
if notification_type:
|
|
queryset = queryset.filter(notification_type=notification_type)
|
|
|
|
# Get total count and unread count
|
|
total_count = queryset.count()
|
|
unread_count = UserNotification.objects.filter(user=user, is_read=False).count()
|
|
|
|
# Apply pagination
|
|
notifications = queryset[offset: offset + limit]
|
|
|
|
# Build pagination URLs
|
|
request_url = request.build_absolute_uri().split("?")[0]
|
|
next_url = None
|
|
previous_url = None
|
|
|
|
if offset + limit < total_count:
|
|
next_params = request.GET.copy()
|
|
next_params["offset"] = offset + limit
|
|
next_url = f"{request_url}?{next_params.urlencode()}"
|
|
|
|
if offset > 0:
|
|
prev_params = request.GET.copy()
|
|
prev_params["offset"] = max(0, offset - limit)
|
|
previous_url = f"{request_url}?{prev_params.urlencode()}"
|
|
|
|
# Serialize notifications
|
|
serializer = UserNotificationSerializer(notifications, many=True)
|
|
|
|
return Response(
|
|
{
|
|
"count": total_count,
|
|
"next": next_url,
|
|
"previous": previous_url,
|
|
"results": serializer.data,
|
|
"unread_count": unread_count,
|
|
},
|
|
status=status.HTTP_200_OK,
|
|
)
|
|
|
|
|
|
@extend_schema(
|
|
operation_id="mark_notifications_read",
|
|
summary="Mark notifications as read",
|
|
description="Mark one or more notifications as read for the authenticated user.",
|
|
request=MarkNotificationsReadSerializer,
|
|
responses={
|
|
200: {
|
|
"type": "object",
|
|
"properties": {
|
|
"success": {"type": "boolean"},
|
|
"marked_count": {"type": "integer"},
|
|
"message": {"type": "string"},
|
|
},
|
|
"example": {
|
|
"success": True,
|
|
"marked_count": 5,
|
|
"message": "5 notifications marked as read",
|
|
},
|
|
},
|
|
400: {"description": "Validation error"},
|
|
401: {"description": "Authentication required"},
|
|
},
|
|
tags=["Notifications"],
|
|
)
|
|
@api_view(["PATCH"])
|
|
@permission_classes([IsAuthenticated])
|
|
def mark_notifications_read(request):
|
|
"""Mark notifications as read."""
|
|
serializer = MarkNotificationsReadSerializer(data=request.data)
|
|
|
|
if serializer.is_valid():
|
|
user = request.user
|
|
notification_ids = serializer.validated_data.get("notification_ids")
|
|
mark_all = serializer.validated_data.get("mark_all", False)
|
|
|
|
if mark_all:
|
|
# Mark all unread notifications as read
|
|
updated_count = UserNotification.objects.filter(
|
|
user=user, is_read=False
|
|
).update(is_read=True, read_at=timezone.now())
|
|
|
|
message = f"All {updated_count} unread notifications marked as read"
|
|
|
|
elif notification_ids:
|
|
# Mark specific notifications as read
|
|
updated_count = UserNotification.objects.filter(
|
|
user=user, id__in=notification_ids, is_read=False
|
|
).update(is_read=True, read_at=timezone.now())
|
|
|
|
message = f"{updated_count} notifications marked as read"
|
|
|
|
else:
|
|
return Response(
|
|
{"error": "Either notification_ids or mark_all must be provided"},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
return Response(
|
|
{"success": True, "marked_count": updated_count, "message": message},
|
|
status=status.HTTP_200_OK,
|
|
)
|
|
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
@extend_schema(
|
|
operation_id="get_notification_preferences",
|
|
summary="Get notification preferences",
|
|
description="Get detailed notification preferences for the authenticated user.",
|
|
responses={
|
|
200: NotificationPreferenceSerializer,
|
|
401: {"description": "Authentication required"},
|
|
},
|
|
tags=["Notifications"],
|
|
)
|
|
@api_view(["GET"])
|
|
@permission_classes([IsAuthenticated])
|
|
def get_notification_preferences(request):
|
|
"""Get notification preferences."""
|
|
user = request.user
|
|
|
|
# Get or create notification preferences
|
|
preferences, created = NotificationPreference.objects.get_or_create(
|
|
user=user,
|
|
defaults={
|
|
"email_enabled": True,
|
|
"push_enabled": True,
|
|
"in_app_enabled": True,
|
|
"submission_notifications": {"email": True, "push": True, "in_app": True},
|
|
"review_notifications": {"email": True, "push": False, "in_app": True},
|
|
"social_notifications": {"email": False, "push": True, "in_app": True},
|
|
"system_notifications": {"email": True, "push": False, "in_app": True},
|
|
"achievement_notifications": {"email": False, "push": True, "in_app": True},
|
|
},
|
|
)
|
|
|
|
serializer = NotificationPreferenceSerializer(preferences)
|
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
|
|
|
|
@extend_schema(
|
|
operation_id="update_notification_preferences",
|
|
summary="Update notification preferences",
|
|
description="Update detailed notification preferences for the authenticated user.",
|
|
request=NotificationPreferenceSerializer,
|
|
responses={
|
|
200: NotificationPreferenceSerializer,
|
|
400: {"description": "Validation error"},
|
|
401: {"description": "Authentication required"},
|
|
},
|
|
tags=["Notifications"],
|
|
)
|
|
@api_view(["PATCH"])
|
|
@permission_classes([IsAuthenticated])
|
|
def update_notification_preferences(request):
|
|
"""Update notification preferences."""
|
|
user = request.user
|
|
|
|
# Get or create notification preferences
|
|
preferences, created = NotificationPreference.objects.get_or_create(user=user)
|
|
|
|
serializer = NotificationPreferenceSerializer(
|
|
preferences, data=request.data, partial=True
|
|
)
|
|
|
|
if serializer.is_valid():
|
|
serializer.save()
|
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
|
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
# === AVATAR ENDPOINTS ===
|
|
|
|
|
|
@extend_schema(
|
|
operation_id="upload_avatar",
|
|
summary="Upload user avatar",
|
|
description="Upload a new avatar image for the authenticated user using Cloudflare Images.",
|
|
request=AvatarUploadSerializer,
|
|
responses={
|
|
200: {
|
|
"type": "object",
|
|
"properties": {
|
|
"success": {"type": "boolean"},
|
|
"message": {"type": "string"},
|
|
"avatar_url": {"type": "string"},
|
|
"avatar_variants": {
|
|
"type": "object",
|
|
"properties": {
|
|
"thumbnail": {"type": "string"},
|
|
"avatar": {"type": "string"},
|
|
"large": {"type": "string"},
|
|
},
|
|
},
|
|
},
|
|
"example": {
|
|
"success": True,
|
|
"message": "Avatar uploaded successfully",
|
|
"avatar_url": "https://imagedelivery.net/account-hash/image-id/avatar",
|
|
"avatar_variants": {
|
|
"thumbnail": "https://imagedelivery.net/account-hash/image-id/thumbnail",
|
|
"avatar": "https://imagedelivery.net/account-hash/image-id/avatar",
|
|
"large": "https://imagedelivery.net/account-hash/image-id/large",
|
|
},
|
|
},
|
|
},
|
|
400: {"description": "Validation error or upload failed"},
|
|
401: {"description": "Authentication required"},
|
|
},
|
|
tags=["User Profile"],
|
|
)
|
|
@api_view(["POST"])
|
|
@permission_classes([IsAuthenticated])
|
|
def upload_avatar(request):
|
|
"""Upload user avatar."""
|
|
user = request.user
|
|
|
|
# Get or create user profile
|
|
profile, created = UserProfile.objects.get_or_create(user=user)
|
|
|
|
serializer = AvatarUploadSerializer(data=request.data)
|
|
|
|
if serializer.is_valid():
|
|
avatar_file = serializer.validated_data["avatar"]
|
|
|
|
try:
|
|
# Update the profile with the new avatar
|
|
profile.avatar = avatar_file
|
|
profile.save()
|
|
|
|
# Get avatar URLs
|
|
avatar_url = profile.get_avatar_url()
|
|
avatar_variants = profile.get_avatar_variants()
|
|
|
|
return Response(
|
|
{
|
|
"success": True,
|
|
"message": "Avatar uploaded successfully",
|
|
"avatar_url": avatar_url,
|
|
"avatar_variants": avatar_variants,
|
|
},
|
|
status=status.HTTP_200_OK,
|
|
)
|
|
|
|
except Exception as e:
|
|
print(f"Upload avatar - Error saving to profile: {e}")
|
|
return Response(
|
|
{"success": False, "error": f"Failed to upload avatar: {str(e)}"},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
print(f"Upload avatar - Serializer errors: {serializer.errors}")
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
@extend_schema(
|
|
operation_id="delete_avatar",
|
|
summary="Delete user avatar",
|
|
description="Delete the current avatar and revert to default letter-based avatar.",
|
|
responses={
|
|
200: {
|
|
"type": "object",
|
|
"properties": {
|
|
"success": {"type": "boolean"},
|
|
"message": {"type": "string"},
|
|
"avatar_url": {"type": "string"},
|
|
},
|
|
"example": {
|
|
"success": True,
|
|
"message": "Avatar deleted successfully",
|
|
"avatar_url": "https://ui-avatars.com/api/?name=J&size=200&background=random&color=fff&bold=true",
|
|
},
|
|
},
|
|
401: {"description": "Authentication required"},
|
|
},
|
|
tags=["User Profile"],
|
|
)
|
|
@api_view(["DELETE"])
|
|
@permission_classes([IsAuthenticated])
|
|
def delete_avatar(request):
|
|
"""Delete user avatar."""
|
|
user = request.user
|
|
|
|
try:
|
|
profile = user.profile
|
|
|
|
# Delete the avatar (this will also delete from Cloudflare)
|
|
if profile.avatar:
|
|
profile.avatar.delete()
|
|
profile.avatar = None
|
|
profile.save()
|
|
|
|
# Get the default avatar URL
|
|
avatar_url = profile.get_avatar_url()
|
|
|
|
return Response(
|
|
{
|
|
"success": True,
|
|
"message": "Avatar deleted successfully",
|
|
"avatar_url": avatar_url,
|
|
},
|
|
status=status.HTTP_200_OK,
|
|
)
|
|
|
|
except UserProfile.DoesNotExist:
|
|
return Response(
|
|
{
|
|
"success": True,
|
|
"message": "No avatar to delete",
|
|
"avatar_url": f"https://ui-avatars.com/api/?name={user.username[0].upper()}&size=200&background=random&color=fff&bold=true",
|
|
},
|
|
status=status.HTTP_200_OK,
|
|
)
|
|
|
|
except Exception as e:
|
|
return Response(
|
|
{"success": False, "error": f"Failed to delete avatar: {str(e)}"},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|