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
This commit is contained in:
pacnpal
2025-08-29 16:03:51 -04:00
parent 7b9f64be72
commit bb7da85516
92 changed files with 19690 additions and 9076 deletions

View File

@@ -0,0 +1,873 @@
"""
User accounts and settings serializers for ThrillWiki API v1.
This module contains all serializers related to user account management,
profile settings, preferences, privacy, notifications, and security.
"""
from rest_framework import serializers
from django.contrib.auth import get_user_model
from drf_spectacular.utils import (
extend_schema_serializer,
OpenApiExample,
)
from apps.accounts.models import (
User,
UserProfile,
TopList,
UserNotification,
NotificationPreference,
)
UserModel = get_user_model()
# === USER PROFILE SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"User Profile Example",
summary="Complete user profile",
description="Full user profile with all fields",
value={
"user_id": "1234",
"username": "thrillseeker",
"email": "user@example.com",
"first_name": "John",
"last_name": "Doe",
"is_active": True,
"date_joined": "2024-01-01T00:00:00Z",
"role": "USER",
"theme_preference": "dark",
"profile": {
"profile_id": "5678",
"display_name": "Thrill Seeker",
"avatar": "https://example.com/avatars/user.jpg",
"pronouns": "they/them",
"bio": "Love roller coasters and theme parks!",
"twitter": "https://twitter.com/thrillseeker",
"instagram": "https://instagram.com/thrillseeker",
"youtube": "https://youtube.com/thrillseeker",
"discord": "thrillseeker#1234",
"coaster_credits": 150,
"dark_ride_credits": 45,
"flat_ride_credits": 89,
"water_ride_credits": 23,
},
},
)
]
)
class UserProfileSerializer(serializers.ModelSerializer):
"""Serializer for user profile data."""
avatar_url = serializers.SerializerMethodField()
avatar_variants = serializers.SerializerMethodField()
class Meta:
model = UserProfile
fields = [
"profile_id",
"display_name",
"avatar",
"avatar_url",
"avatar_variants",
"pronouns",
"bio",
"twitter",
"instagram",
"youtube",
"discord",
"coaster_credits",
"dark_ride_credits",
"flat_ride_credits",
"water_ride_credits",
]
read_only_fields = ["profile_id", "avatar_url", "avatar_variants"]
def get_avatar_url(self, obj):
"""Get the avatar URL with fallback to default letter-based avatar."""
return obj.get_avatar_url()
def get_avatar_variants(self, obj):
"""Get avatar variants for different use cases."""
return obj.get_avatar_variants()
def validate_display_name(self, value):
"""Validate display name uniqueness - now checks User model first."""
user = self.context["request"].user
# Check User model for display_name uniqueness (primary location)
if User.objects.filter(display_name=value).exclude(id=user.id).exists():
raise serializers.ValidationError("Display name already taken")
# Also check UserProfile for backward compatibility during transition
if UserProfile.objects.filter(display_name=value).exclude(user=user).exists():
raise serializers.ValidationError("Display name already taken")
return value
@extend_schema_serializer(
examples=[
OpenApiExample(
"Complete User Example",
summary="Complete user with profile",
description="Full user object with embedded profile",
value={
"user_id": "1234",
"username": "thrillseeker",
"email": "user@example.com",
"first_name": "John",
"last_name": "Doe",
"is_active": True,
"date_joined": "2024-01-01T00:00:00Z",
"role": "USER",
"theme_preference": "dark",
"profile": {
"profile_id": "5678",
"display_name": "Thrill Seeker",
"avatar": "https://example.com/avatars/user.jpg",
"pronouns": "they/them",
"bio": "Love roller coasters and theme parks!",
"twitter": "https://twitter.com/thrillseeker",
"instagram": "https://instagram.com/thrillseeker",
"youtube": "https://youtube.com/thrillseeker",
"discord": "thrillseeker#1234",
"coaster_credits": 150,
"dark_ride_credits": 45,
"flat_ride_credits": 89,
"water_ride_credits": 23,
},
},
)
]
)
class CompleteUserSerializer(serializers.ModelSerializer):
"""Complete user serializer with profile data."""
profile = UserProfileSerializer(read_only=True)
class Meta:
model = User
fields = [
"user_id",
"username",
"email",
"first_name",
"last_name",
"is_active",
"date_joined",
"role",
"theme_preference",
"profile",
]
read_only_fields = ["user_id", "date_joined", "role"]
# === USER SETTINGS SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"User Preferences Example",
summary="User preferences and settings",
description="User's preference settings",
value={
"theme_preference": "dark",
"email_notifications": True,
"push_notifications": False,
"privacy_level": "public",
"show_email": False,
"show_real_name": True,
"show_statistics": True,
"allow_friend_requests": True,
"allow_messages": True,
},
)
]
)
class UserPreferencesSerializer(serializers.Serializer):
"""Serializer for user preferences and settings."""
theme_preference = serializers.ChoiceField(
choices=User.ThemePreference.choices, help_text="User's theme preference"
)
email_notifications = serializers.BooleanField(
default=True, help_text="Whether to receive email notifications"
)
push_notifications = serializers.BooleanField(
default=False, help_text="Whether to receive push notifications"
)
privacy_level = serializers.ChoiceField(
choices=[
("public", "Public"),
("friends", "Friends Only"),
("private", "Private"),
],
default="public",
help_text="Profile visibility level",
)
show_email = serializers.BooleanField(
default=False, help_text="Whether to show email on profile"
)
show_real_name = serializers.BooleanField(
default=True, help_text="Whether to show real name on profile"
)
show_statistics = serializers.BooleanField(
default=True, help_text="Whether to show ride statistics on profile"
)
allow_friend_requests = serializers.BooleanField(
default=True, help_text="Whether to allow friend requests"
)
allow_messages = serializers.BooleanField(
default=True, help_text="Whether to allow direct messages"
)
# === NOTIFICATION SETTINGS SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Notification Settings Example",
summary="User notification preferences",
description="Detailed notification settings",
value={
"email_notifications": {
"new_reviews": True,
"review_replies": True,
"friend_requests": True,
"messages": True,
"weekly_digest": False,
"new_features": True,
"security_alerts": True,
},
"push_notifications": {
"new_reviews": False,
"review_replies": True,
"friend_requests": True,
"messages": True,
},
"in_app_notifications": {
"new_reviews": True,
"review_replies": True,
"friend_requests": True,
"messages": True,
"system_announcements": True,
},
},
)
]
)
class NotificationSettingsSerializer(serializers.Serializer):
"""Serializer for detailed notification settings."""
class EmailNotificationsSerializer(serializers.Serializer):
new_reviews = serializers.BooleanField(default=True)
review_replies = serializers.BooleanField(default=True)
friend_requests = serializers.BooleanField(default=True)
messages = serializers.BooleanField(default=True)
weekly_digest = serializers.BooleanField(default=False)
new_features = serializers.BooleanField(default=True)
security_alerts = serializers.BooleanField(default=True)
class PushNotificationsSerializer(serializers.Serializer):
new_reviews = serializers.BooleanField(default=False)
review_replies = serializers.BooleanField(default=True)
friend_requests = serializers.BooleanField(default=True)
messages = serializers.BooleanField(default=True)
class InAppNotificationsSerializer(serializers.Serializer):
new_reviews = serializers.BooleanField(default=True)
review_replies = serializers.BooleanField(default=True)
friend_requests = serializers.BooleanField(default=True)
messages = serializers.BooleanField(default=True)
system_announcements = serializers.BooleanField(default=True)
email_notifications = EmailNotificationsSerializer()
push_notifications = PushNotificationsSerializer()
in_app_notifications = InAppNotificationsSerializer()
# === PRIVACY SETTINGS SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Privacy Settings Example",
summary="User privacy settings",
description="Detailed privacy and visibility settings",
value={
"profile_visibility": "public",
"show_email": False,
"show_real_name": True,
"show_join_date": True,
"show_statistics": True,
"show_reviews": True,
"show_photos": True,
"show_top_lists": True,
"allow_friend_requests": True,
"allow_messages": True,
"allow_profile_comments": False,
"search_visibility": True,
"activity_visibility": "friends",
},
)
]
)
class PrivacySettingsSerializer(serializers.Serializer):
"""Serializer for privacy and visibility settings."""
profile_visibility = serializers.ChoiceField(
choices=[
("public", "Public"),
("friends", "Friends Only"),
("private", "Private"),
],
default="public",
help_text="Overall profile visibility",
)
show_email = serializers.BooleanField(
default=False, help_text="Show email address on profile"
)
show_real_name = serializers.BooleanField(
default=True, help_text="Show real name on profile"
)
show_join_date = serializers.BooleanField(
default=True, help_text="Show join date on profile"
)
show_statistics = serializers.BooleanField(
default=True, help_text="Show ride statistics on profile"
)
show_reviews = serializers.BooleanField(
default=True, help_text="Show reviews on profile"
)
show_photos = serializers.BooleanField(
default=True, help_text="Show uploaded photos on profile"
)
show_top_lists = serializers.BooleanField(
default=True, help_text="Show top lists on profile"
)
allow_friend_requests = serializers.BooleanField(
default=True, help_text="Allow others to send friend requests"
)
allow_messages = serializers.BooleanField(
default=True, help_text="Allow others to send direct messages"
)
allow_profile_comments = serializers.BooleanField(
default=False, help_text="Allow others to comment on profile"
)
search_visibility = serializers.BooleanField(
default=True, help_text="Allow profile to appear in search results"
)
activity_visibility = serializers.ChoiceField(
choices=[
("public", "Public"),
("friends", "Friends Only"),
("private", "Private"),
],
default="friends",
help_text="Who can see your activity feed",
)
# === SECURITY SETTINGS SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Security Settings Example",
summary="User security settings",
description="Account security and authentication settings",
value={
"two_factor_enabled": False,
"login_notifications": True,
"session_timeout": 30,
"require_password_change": False,
"last_password_change": "2024-01-01T00:00:00Z",
"active_sessions": 2,
"login_history_retention": 90,
},
)
]
)
class SecuritySettingsSerializer(serializers.Serializer):
"""Serializer for security settings."""
two_factor_enabled = serializers.BooleanField(
default=False, help_text="Whether two-factor authentication is enabled"
)
login_notifications = serializers.BooleanField(
default=True, help_text="Send notifications for new logins"
)
session_timeout = serializers.IntegerField(
default=30, min_value=5, max_value=180, help_text="Session timeout in days"
)
require_password_change = serializers.BooleanField(
default=False, help_text="Whether password change is required"
)
last_password_change = serializers.DateTimeField(
read_only=True, help_text="When password was last changed"
)
active_sessions = serializers.IntegerField(
read_only=True, help_text="Number of active sessions"
)
login_history_retention = serializers.IntegerField(
default=90,
min_value=30,
max_value=365,
help_text="How long to keep login history (days)",
)
# === USER STATISTICS SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"User Statistics Example",
summary="User activity statistics",
description="Comprehensive user activity and contribution statistics",
value={
"ride_credits": {
"coaster_credits": 150,
"dark_ride_credits": 45,
"flat_ride_credits": 89,
"water_ride_credits": 23,
"total_credits": 307,
},
"contributions": {
"park_reviews": 25,
"ride_reviews": 87,
"photos_uploaded": 156,
"top_lists_created": 8,
"helpful_votes_received": 342,
},
"activity": {
"days_active": 45,
"last_active": "2024-01-15T10:30:00Z",
"average_review_rating": 4.2,
"most_reviewed_park": "Cedar Point",
"favorite_ride_type": "Roller Coaster",
},
"achievements": {
"first_review": True,
"photo_contributor": True,
"top_reviewer": False,
"park_explorer": True,
"coaster_enthusiast": True,
},
},
)
]
)
class UserStatisticsSerializer(serializers.Serializer):
"""Serializer for user statistics and achievements."""
class RideCreditsSerializer(serializers.Serializer):
coaster_credits = serializers.IntegerField()
dark_ride_credits = serializers.IntegerField()
flat_ride_credits = serializers.IntegerField()
water_ride_credits = serializers.IntegerField()
total_credits = serializers.IntegerField()
class ContributionsSerializer(serializers.Serializer):
park_reviews = serializers.IntegerField()
ride_reviews = serializers.IntegerField()
photos_uploaded = serializers.IntegerField()
top_lists_created = serializers.IntegerField()
helpful_votes_received = serializers.IntegerField()
class ActivitySerializer(serializers.Serializer):
days_active = serializers.IntegerField()
last_active = serializers.DateTimeField()
average_review_rating = serializers.FloatField()
most_reviewed_park = serializers.CharField()
favorite_ride_type = serializers.CharField()
class AchievementsSerializer(serializers.Serializer):
first_review = serializers.BooleanField()
photo_contributor = serializers.BooleanField()
top_reviewer = serializers.BooleanField()
park_explorer = serializers.BooleanField()
coaster_enthusiast = serializers.BooleanField()
ride_credits = RideCreditsSerializer()
contributions = ContributionsSerializer()
activity = ActivitySerializer()
achievements = AchievementsSerializer()
# === TOP LISTS SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Top List Example",
summary="User's top list",
description="A user's ranked list of rides or parks",
value={
"id": 1,
"title": "My Top 10 Roller Coasters",
"category": "RC",
"description": "My favorite roller coasters from around the world",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-15T10:30:00Z",
"items_count": 10,
},
)
]
)
class TopListSerializer(serializers.ModelSerializer):
"""Serializer for user's top lists."""
items_count = serializers.SerializerMethodField()
class Meta:
model = TopList
fields = [
"id",
"title",
"category",
"description",
"created_at",
"updated_at",
"items_count",
]
read_only_fields = ["id", "created_at", "updated_at"]
def get_items_count(self, obj):
"""Get the number of items in the list."""
return obj.items.count()
# === ACCOUNT UPDATE SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Account Update Example",
summary="Update account information",
description="Update basic account information",
value={
"first_name": "John",
"last_name": "Doe",
"email": "newemail@example.com",
},
)
]
)
class AccountUpdateSerializer(serializers.ModelSerializer):
"""Serializer for updating account information."""
class Meta:
model = User
fields = [
"first_name",
"last_name",
"email",
]
def validate_email(self, value):
"""Validate email uniqueness."""
user = self.context["request"].user
if User.objects.filter(email=value).exclude(id=user.id).exists():
raise serializers.ValidationError("Email already in use")
return value
# === PROFILE UPDATE SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Profile Update Example",
summary="Update profile information",
description="Update profile information and social links",
value={
"display_name": "New Display Name",
"pronouns": "they/them",
"bio": "Updated bio text",
"twitter": "https://twitter.com/newhandle",
"instagram": "",
"youtube": "https://youtube.com/newchannel",
"discord": "newhandle#5678",
},
)
]
)
class ProfileUpdateSerializer(serializers.ModelSerializer):
"""Serializer for updating profile information."""
class Meta:
model = UserProfile
fields = [
"display_name",
"pronouns",
"bio",
"twitter",
"instagram",
"youtube",
"discord",
]
def validate_display_name(self, value):
"""Validate display name uniqueness - now checks User model first."""
user = self.context["request"].user
# Check User model for display_name uniqueness (primary location)
if User.objects.filter(display_name=value).exclude(id=user.id).exists():
raise serializers.ValidationError("Display name already taken")
# Also check UserProfile for backward compatibility during transition
if UserProfile.objects.filter(display_name=value).exclude(user=user).exists():
raise serializers.ValidationError("Display name already taken")
return value
# === THEME PREFERENCE SERIALIZER ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Theme Update Example",
summary="Update theme preference",
description="Update user's theme preference",
value={
"theme_preference": "dark",
},
)
]
)
class ThemePreferenceSerializer(serializers.ModelSerializer):
"""Serializer for updating theme preference."""
class Meta:
model = User
fields = ["theme_preference"]
# === NOTIFICATION SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"User Notification Example",
summary="User notification",
description="A notification sent to a user",
value={
"id": 1,
"notification_type": "submission_approved",
"title": "Your submission has been approved!",
"message": "Your photo submission for Cedar Point has been approved and is now live on the site.",
"priority": "normal",
"is_read": False,
"read_at": None,
"created_at": "2024-01-15T10:30:00Z",
"expires_at": None,
"extra_data": {"submission_id": 123, "park_name": "Cedar Point"},
},
)
]
)
class UserNotificationSerializer(serializers.ModelSerializer):
"""Serializer for user notifications."""
class Meta:
model = UserNotification
fields = [
"id",
"notification_type",
"title",
"message",
"priority",
"is_read",
"read_at",
"created_at",
"expires_at",
"extra_data",
]
read_only_fields = [
"id",
"notification_type",
"title",
"message",
"priority",
"created_at",
"expires_at",
"extra_data",
]
@extend_schema_serializer(
examples=[
OpenApiExample(
"Notification Preferences Example",
summary="User notification preferences",
description="Comprehensive notification preferences for all channels",
value={
"submission_approved_email": True,
"submission_approved_push": True,
"submission_approved_inapp": True,
"submission_rejected_email": True,
"submission_rejected_push": True,
"submission_rejected_inapp": True,
"submission_pending_email": False,
"submission_pending_push": False,
"submission_pending_inapp": True,
"review_reply_email": True,
"review_reply_push": True,
"review_reply_inapp": True,
"review_helpful_email": False,
"review_helpful_push": True,
"review_helpful_inapp": True,
"friend_request_email": True,
"friend_request_push": True,
"friend_request_inapp": True,
"friend_accepted_email": False,
"friend_accepted_push": True,
"friend_accepted_inapp": True,
"message_received_email": True,
"message_received_push": True,
"message_received_inapp": True,
"system_announcement_email": True,
"system_announcement_push": False,
"system_announcement_inapp": True,
"account_security_email": True,
"account_security_push": True,
"account_security_inapp": True,
"feature_update_email": True,
"feature_update_push": False,
"feature_update_inapp": True,
"achievement_unlocked_email": False,
"achievement_unlocked_push": True,
"achievement_unlocked_inapp": True,
"milestone_reached_email": False,
"milestone_reached_push": True,
"milestone_reached_inapp": True,
},
)
]
)
class NotificationPreferenceSerializer(serializers.ModelSerializer):
"""Serializer for notification preferences."""
class Meta:
model = NotificationPreference
fields = [
# Submission notifications
"submission_approved_email",
"submission_approved_push",
"submission_approved_inapp",
"submission_rejected_email",
"submission_rejected_push",
"submission_rejected_inapp",
"submission_pending_email",
"submission_pending_push",
"submission_pending_inapp",
# Review notifications
"review_reply_email",
"review_reply_push",
"review_reply_inapp",
"review_helpful_email",
"review_helpful_push",
"review_helpful_inapp",
# Social notifications
"friend_request_email",
"friend_request_push",
"friend_request_inapp",
"friend_accepted_email",
"friend_accepted_push",
"friend_accepted_inapp",
"message_received_email",
"message_received_push",
"message_received_inapp",
# System notifications
"system_announcement_email",
"system_announcement_push",
"system_announcement_inapp",
"account_security_email",
"account_security_push",
"account_security_inapp",
"feature_update_email",
"feature_update_push",
"feature_update_inapp",
# Achievement notifications
"achievement_unlocked_email",
"achievement_unlocked_push",
"achievement_unlocked_inapp",
"milestone_reached_email",
"milestone_reached_push",
"milestone_reached_inapp",
]
# === NOTIFICATION ACTIONS SERIALIZERS ===
@extend_schema_serializer(
examples=[
OpenApiExample(
"Mark Notifications Read Example",
summary="Mark notifications as read",
description="Mark specific notifications as read",
value={"notification_ids": [1, 2, 3, 4, 5]},
)
]
)
class MarkNotificationsReadSerializer(serializers.Serializer):
"""Serializer for marking notifications as read."""
notification_ids = serializers.ListField(
child=serializers.IntegerField(),
help_text="List of notification IDs to mark as read",
)
def validate_notification_ids(self, value):
"""Validate that all notification IDs belong to the requesting user."""
user = self.context["request"].user
valid_ids = UserNotification.objects.filter(
id__in=value, user=user
).values_list("id", flat=True)
invalid_ids = set(value) - set(valid_ids)
if invalid_ids:
raise serializers.ValidationError(
f"Invalid notification IDs: {list(invalid_ids)}"
)
return value
@extend_schema_serializer(
examples=[
OpenApiExample(
"Avatar Upload Example",
summary="Upload user avatar",
description="Upload a new avatar image",
value={"avatar": "base64_encoded_image_data_or_file_upload"},
)
]
)
class AvatarUploadSerializer(serializers.ModelSerializer):
"""Serializer for uploading user avatar."""
class Meta:
model = UserProfile
fields = ["avatar"]
def validate_avatar(self, value):
"""Validate avatar file."""
if value:
# Add any avatar-specific validation here
# The CloudflareImagesField will handle the upload
pass
return value