Files
thrillwiki_django_no_react/backend/apps/api/v1/accounts/views.py
pacnpal bb7da85516 Refactor API structure and add comprehensive user management features
- Restructure API v1 with improved serializers organization
- Add user deletion requests and moderation queue system
- Implement bulk moderation operations and permissions
- Add user profile enhancements with display names and avatars
- Expand ride and park API endpoints with better filtering
- Add manufacturer API with detailed ride relationships
- Improve authentication flows and error handling
- Update frontend documentation and API specifications
2025-08-29 16:03:51 -04:00

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