mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 11:11:10 -05:00
- Added migration to transition avatar data from CloudflareImageField to ForeignKey structure in UserProfile. - Fixed UserProfileEvent avatar field to align with new avatar structure. - Created serializers for social authentication, including connected and available providers. - Developed request logging middleware for comprehensive request/response logging. - Updated moderation and parks migrations to remove outdated triggers and adjust foreign key relationships. - Enhanced rides migrations to ensure proper handling of image uploads and triggers. - Introduced a test script for the 3-step avatar upload process, ensuring functionality with Cloudflare. - Documented the fix for avatar upload issues, detailing root cause, implementation, and verification steps. - Implemented automatic deletion of Cloudflare images upon avatar, park, and ride photo changes or removals.
911 lines
31 KiB
Python
911 lines
31 KiB
Python
"""
|
|
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.Serializer):
|
|
"""Serializer for uploading user avatar."""
|
|
|
|
# Use FileField instead of ImageField to bypass Django's image validation
|
|
avatar = serializers.FileField()
|
|
|
|
def validate_avatar(self, value):
|
|
"""Validate avatar file."""
|
|
if not value:
|
|
raise serializers.ValidationError("No file provided")
|
|
|
|
# Check file size constraints (max 10MB for Cloudflare Images)
|
|
if hasattr(value, 'size') and value.size > 10 * 1024 * 1024:
|
|
raise serializers.ValidationError(
|
|
"Image file too large. Maximum size is 10MB.")
|
|
|
|
# Try to validate with PIL
|
|
try:
|
|
from PIL import Image
|
|
import io
|
|
|
|
value.seek(0)
|
|
image_data = value.read()
|
|
value.seek(0) # Reset for later use
|
|
|
|
if len(image_data) == 0:
|
|
raise serializers.ValidationError("File appears to be empty")
|
|
|
|
# Try to open with PIL
|
|
image = Image.open(io.BytesIO(image_data))
|
|
|
|
# Verify it's a valid image
|
|
image.verify()
|
|
|
|
# Check image dimensions (max 12,000x12,000 for Cloudflare Images)
|
|
if image.size[0] > 12000 or image.size[1] > 12000:
|
|
raise serializers.ValidationError(
|
|
"Image dimensions too large. Maximum is 12,000x12,000 pixels.")
|
|
|
|
# Check if it's a supported format
|
|
if image.format not in ['JPEG', 'PNG', 'GIF', 'WEBP']:
|
|
raise serializers.ValidationError(
|
|
f"Unsupported image format: {image.format}. Supported formats: JPEG, PNG, GIF, WebP.")
|
|
|
|
except serializers.ValidationError:
|
|
raise # Re-raise validation errors
|
|
except Exception:
|
|
# PIL validation failed, but let Cloudflare Images try to process it
|
|
pass
|
|
|
|
return value
|