mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-01-01 22:07:03 -05:00
feat: Implement initial schema and add various API, service, and management command enhancements across the application.
This commit is contained in:
@@ -8,4 +8,4 @@ including social provider management, user authentication, and profile services.
|
||||
from .social_provider_service import SocialProviderService
|
||||
from .user_deletion_service import UserDeletionService
|
||||
|
||||
__all__ = ['SocialProviderService', 'UserDeletionService']
|
||||
__all__ = ["SocialProviderService", "UserDeletionService"]
|
||||
|
||||
@@ -139,7 +139,9 @@ class NotificationService:
|
||||
UserNotification: The created notification
|
||||
"""
|
||||
title = f"Your {submission_type} needs attention"
|
||||
message = f"Your {submission_type} submission has been reviewed and needs some changes before it can be approved."
|
||||
message = (
|
||||
f"Your {submission_type} submission has been reviewed and needs some changes before it can be approved."
|
||||
)
|
||||
message += f"\n\nReason: {rejection_reason}"
|
||||
|
||||
if additional_message:
|
||||
@@ -216,9 +218,7 @@ class NotificationService:
|
||||
preferences = NotificationPreference.objects.create(user=user)
|
||||
|
||||
# Send email notification if enabled
|
||||
if preferences.should_send_notification(
|
||||
notification.notification_type, "email"
|
||||
):
|
||||
if preferences.should_send_notification(notification.notification_type, "email"):
|
||||
NotificationService._send_email_notification(notification)
|
||||
|
||||
# Toast notifications are always created (the notification object itself)
|
||||
@@ -261,14 +261,10 @@ class NotificationService:
|
||||
notification.email_sent_at = timezone.now()
|
||||
notification.save(update_fields=["email_sent", "email_sent_at"])
|
||||
|
||||
logger.info(
|
||||
f"Email notification sent to {user.email} for notification {notification.id}"
|
||||
)
|
||||
logger.info(f"Email notification sent to {user.email} for notification {notification.id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to send email notification {notification.id}: {str(e)}"
|
||||
)
|
||||
logger.error(f"Failed to send email notification {notification.id}: {str(e)}")
|
||||
|
||||
@staticmethod
|
||||
def get_user_notifications(
|
||||
@@ -298,9 +294,7 @@ class NotificationService:
|
||||
queryset = queryset.filter(notification_type__in=notification_types)
|
||||
|
||||
# Exclude expired notifications
|
||||
queryset = queryset.filter(
|
||||
models.Q(expires_at__isnull=True) | models.Q(expires_at__gt=timezone.now())
|
||||
)
|
||||
queryset = queryset.filter(models.Q(expires_at__isnull=True) | models.Q(expires_at__gt=timezone.now()))
|
||||
|
||||
if limit:
|
||||
queryset = queryset[:limit]
|
||||
@@ -308,9 +302,7 @@ class NotificationService:
|
||||
return list(queryset)
|
||||
|
||||
@staticmethod
|
||||
def mark_notifications_read(
|
||||
user: User, notification_ids: list[int] | None = None
|
||||
) -> int:
|
||||
def mark_notifications_read(user: User, notification_ids: list[int] | None = None) -> int:
|
||||
"""
|
||||
Mark notifications as read for a user.
|
||||
|
||||
@@ -341,9 +333,7 @@ class NotificationService:
|
||||
"""
|
||||
cutoff_date = timezone.now() - timedelta(days=days)
|
||||
|
||||
old_notifications = UserNotification.objects.filter(
|
||||
is_read=True, read_at__lt=cutoff_date
|
||||
)
|
||||
old_notifications = UserNotification.objects.filter(is_read=True, read_at__lt=cutoff_date)
|
||||
|
||||
count = old_notifications.count()
|
||||
old_notifications.delete()
|
||||
|
||||
@@ -40,23 +40,20 @@ class SocialProviderService:
|
||||
"""
|
||||
try:
|
||||
# Count remaining social accounts after disconnection
|
||||
remaining_social_accounts = user.socialaccount_set.exclude(
|
||||
provider=provider
|
||||
).count()
|
||||
remaining_social_accounts = user.socialaccount_set.exclude(provider=provider).count()
|
||||
|
||||
# Check if user has email/password auth
|
||||
has_password_auth = (
|
||||
user.email and
|
||||
user.has_usable_password() and
|
||||
bool(user.password) # Not empty/unusable
|
||||
)
|
||||
has_password_auth = user.email and user.has_usable_password() and bool(user.password) # Not empty/unusable
|
||||
|
||||
# Allow disconnection only if alternative auth exists
|
||||
can_disconnect = remaining_social_accounts > 0 or has_password_auth
|
||||
|
||||
if not can_disconnect:
|
||||
if remaining_social_accounts == 0 and not has_password_auth:
|
||||
return False, "Cannot disconnect your only authentication method. Please set up a password or connect another social provider first."
|
||||
return (
|
||||
False,
|
||||
"Cannot disconnect your only authentication method. Please set up a password or connect another social provider first.",
|
||||
)
|
||||
elif not has_password_auth:
|
||||
return False, "Please set up email/password authentication before disconnecting this provider."
|
||||
else:
|
||||
@@ -65,8 +62,7 @@ class SocialProviderService:
|
||||
return True, "Provider can be safely disconnected."
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error checking disconnect permission for user {user.id}, provider {provider}: {e}")
|
||||
logger.error(f"Error checking disconnect permission for user {user.id}, provider {provider}: {e}")
|
||||
return False, "Unable to verify disconnection safety. Please try again."
|
||||
|
||||
@staticmethod
|
||||
@@ -84,18 +80,16 @@ class SocialProviderService:
|
||||
connected_providers = []
|
||||
|
||||
for social_account in user.socialaccount_set.all():
|
||||
can_disconnect, reason = SocialProviderService.can_disconnect_provider(
|
||||
user, social_account.provider
|
||||
)
|
||||
can_disconnect, reason = SocialProviderService.can_disconnect_provider(user, social_account.provider)
|
||||
|
||||
provider_info = {
|
||||
'provider': social_account.provider,
|
||||
'provider_name': social_account.get_provider().name,
|
||||
'uid': social_account.uid,
|
||||
'date_joined': social_account.date_joined,
|
||||
'can_disconnect': can_disconnect,
|
||||
'disconnect_reason': reason if not can_disconnect else None,
|
||||
'extra_data': social_account.extra_data
|
||||
"provider": social_account.provider,
|
||||
"provider_name": social_account.get_provider().name,
|
||||
"uid": social_account.uid,
|
||||
"date_joined": social_account.date_joined,
|
||||
"can_disconnect": can_disconnect,
|
||||
"disconnect_reason": reason if not can_disconnect else None,
|
||||
"extra_data": social_account.extra_data,
|
||||
}
|
||||
|
||||
connected_providers.append(provider_info)
|
||||
@@ -122,28 +116,25 @@ class SocialProviderService:
|
||||
available_providers = []
|
||||
|
||||
# Get all social apps configured for this site
|
||||
social_apps = SocialApp.objects.filter(sites=site).order_by('provider')
|
||||
social_apps = SocialApp.objects.filter(sites=site).order_by("provider")
|
||||
|
||||
for social_app in social_apps:
|
||||
try:
|
||||
provider = registry.by_id(social_app.provider)
|
||||
|
||||
provider_info = {
|
||||
'id': social_app.provider,
|
||||
'name': provider.name,
|
||||
'auth_url': request.build_absolute_uri(
|
||||
f'/accounts/{social_app.provider}/login/'
|
||||
"id": social_app.provider,
|
||||
"name": provider.name,
|
||||
"auth_url": request.build_absolute_uri(f"/accounts/{social_app.provider}/login/"),
|
||||
"connect_url": request.build_absolute_uri(
|
||||
f"/api/v1/auth/social/connect/{social_app.provider}/"
|
||||
),
|
||||
'connect_url': request.build_absolute_uri(
|
||||
f'/api/v1/auth/social/connect/{social_app.provider}/'
|
||||
)
|
||||
}
|
||||
|
||||
available_providers.append(provider_info)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Error processing provider {social_app.provider}: {e}")
|
||||
logger.warning(f"Error processing provider {social_app.provider}: {e}")
|
||||
continue
|
||||
|
||||
return available_providers
|
||||
@@ -166,8 +157,7 @@ class SocialProviderService:
|
||||
"""
|
||||
try:
|
||||
# First check if disconnection is allowed
|
||||
can_disconnect, reason = SocialProviderService.can_disconnect_provider(
|
||||
user, provider)
|
||||
can_disconnect, reason = SocialProviderService.can_disconnect_provider(user, provider)
|
||||
|
||||
if not can_disconnect:
|
||||
return False, reason
|
||||
@@ -182,8 +172,7 @@ class SocialProviderService:
|
||||
deleted_count = social_accounts.count()
|
||||
social_accounts.delete()
|
||||
|
||||
logger.info(
|
||||
f"User {user.id} disconnected {deleted_count} {provider} account(s)")
|
||||
logger.info(f"User {user.id} disconnected {deleted_count} {provider} account(s)")
|
||||
|
||||
return True, f"{provider.title()} account disconnected successfully."
|
||||
|
||||
@@ -205,31 +194,24 @@ class SocialProviderService:
|
||||
try:
|
||||
connected_providers = SocialProviderService.get_connected_providers(user)
|
||||
|
||||
has_password_auth = (
|
||||
user.email and
|
||||
user.has_usable_password() and
|
||||
bool(user.password)
|
||||
)
|
||||
has_password_auth = user.email and user.has_usable_password() and bool(user.password)
|
||||
|
||||
auth_methods_count = len(connected_providers) + \
|
||||
(1 if has_password_auth else 0)
|
||||
auth_methods_count = len(connected_providers) + (1 if has_password_auth else 0)
|
||||
|
||||
return {
|
||||
'user_id': user.id,
|
||||
'username': user.username,
|
||||
'email': user.email,
|
||||
'has_password_auth': has_password_auth,
|
||||
'connected_providers': connected_providers,
|
||||
'total_auth_methods': auth_methods_count,
|
||||
'can_disconnect_any': auth_methods_count > 1,
|
||||
'requires_password_setup': not has_password_auth and len(connected_providers) == 1
|
||||
"user_id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"has_password_auth": has_password_auth,
|
||||
"connected_providers": connected_providers,
|
||||
"total_auth_methods": auth_methods_count,
|
||||
"can_disconnect_any": auth_methods_count > 1,
|
||||
"requires_password_setup": not has_password_auth and len(connected_providers) == 1,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting auth status for user {user.id}: {e}")
|
||||
return {
|
||||
'error': 'Unable to retrieve authentication status'
|
||||
}
|
||||
return {"error": "Unable to retrieve authentication status"}
|
||||
|
||||
@staticmethod
|
||||
def validate_provider_exists(provider: str) -> tuple[bool, str]:
|
||||
|
||||
@@ -59,7 +59,7 @@ class UserDeletionService:
|
||||
return False, "Cannot delete staff accounts"
|
||||
|
||||
# Check for system users (if you have any special system accounts)
|
||||
if hasattr(user, 'role') and user.role in ['ADMIN', 'MODERATOR']:
|
||||
if hasattr(user, "role") and user.role in ["ADMIN", "MODERATOR"]:
|
||||
return False, "Cannot delete admin or moderator accounts"
|
||||
|
||||
return True, None
|
||||
@@ -84,8 +84,7 @@ class UserDeletionService:
|
||||
raise ValueError(reason)
|
||||
|
||||
# Generate verification code
|
||||
verification_code = ''.join(secrets.choice(
|
||||
string.ascii_uppercase + string.digits) for _ in range(8))
|
||||
verification_code = "".join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(8))
|
||||
|
||||
# Set expiration (24 hours from now)
|
||||
expires_at = timezone.now() + timezone.timedelta(hours=24)
|
||||
@@ -97,8 +96,7 @@ class UserDeletionService:
|
||||
UserDeletionService._deletion_requests[verification_code] = deletion_request
|
||||
|
||||
# Send verification email
|
||||
UserDeletionService._send_deletion_verification_email(
|
||||
user, verification_code, expires_at)
|
||||
UserDeletionService._send_deletion_verification_email(user, verification_code, expires_at)
|
||||
|
||||
return deletion_request
|
||||
|
||||
@@ -136,10 +134,10 @@ class UserDeletionService:
|
||||
del UserDeletionService._deletion_requests[verification_code]
|
||||
|
||||
# Add verification info to result
|
||||
result['deletion_request'] = {
|
||||
'verification_code': verification_code,
|
||||
'created_at': deletion_request.created_at,
|
||||
'verified_at': timezone.now(),
|
||||
result["deletion_request"] = {
|
||||
"verification_code": verification_code,
|
||||
"created_at": deletion_request.created_at,
|
||||
"verified_at": timezone.now(),
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -180,13 +178,13 @@ class UserDeletionService:
|
||||
"""
|
||||
# Get or create the "deleted_user" placeholder
|
||||
deleted_user_placeholder, created = User.objects.get_or_create(
|
||||
username='deleted_user',
|
||||
username="deleted_user",
|
||||
defaults={
|
||||
'email': 'deleted@thrillwiki.com',
|
||||
'first_name': 'Deleted',
|
||||
'last_name': 'User',
|
||||
'is_active': False,
|
||||
}
|
||||
"email": "deleted@thrillwiki.com",
|
||||
"first_name": "Deleted",
|
||||
"last_name": "User",
|
||||
"is_active": False,
|
||||
},
|
||||
)
|
||||
|
||||
# Count submissions before transfer
|
||||
@@ -197,22 +195,22 @@ class UserDeletionService:
|
||||
|
||||
# Store user info before deletion
|
||||
deleted_user_info = {
|
||||
'username': user.username,
|
||||
'user_id': getattr(user, 'user_id', user.id),
|
||||
'email': user.email,
|
||||
'date_joined': user.date_joined,
|
||||
"username": user.username,
|
||||
"user_id": getattr(user, "user_id", user.id),
|
||||
"email": user.email,
|
||||
"date_joined": user.date_joined,
|
||||
}
|
||||
|
||||
# Delete the user account
|
||||
user.delete()
|
||||
|
||||
return {
|
||||
'deleted_user': deleted_user_info,
|
||||
'preserved_submissions': submission_counts,
|
||||
'transferred_to': {
|
||||
'username': deleted_user_placeholder.username,
|
||||
'user_id': getattr(deleted_user_placeholder, 'user_id', deleted_user_placeholder.id),
|
||||
}
|
||||
"deleted_user": deleted_user_info,
|
||||
"preserved_submissions": submission_counts,
|
||||
"transferred_to": {
|
||||
"username": deleted_user_placeholder.username,
|
||||
"user_id": getattr(deleted_user_placeholder, "user_id", deleted_user_placeholder.id),
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -222,20 +220,13 @@ class UserDeletionService:
|
||||
|
||||
# Count different types of submissions
|
||||
# Note: These are placeholder counts - adjust based on your actual models
|
||||
counts['park_reviews'] = getattr(
|
||||
user, 'park_reviews', user.__class__.objects.none()).count()
|
||||
counts['ride_reviews'] = getattr(
|
||||
user, 'ride_reviews', user.__class__.objects.none()).count()
|
||||
counts['uploaded_park_photos'] = getattr(
|
||||
user, 'uploaded_park_photos', user.__class__.objects.none()).count()
|
||||
counts['uploaded_ride_photos'] = getattr(
|
||||
user, 'uploaded_ride_photos', user.__class__.objects.none()).count()
|
||||
counts['top_lists'] = getattr(
|
||||
user, 'top_lists', user.__class__.objects.none()).count()
|
||||
counts['edit_submissions'] = getattr(
|
||||
user, 'edit_submissions', user.__class__.objects.none()).count()
|
||||
counts['photo_submissions'] = getattr(
|
||||
user, 'photo_submissions', user.__class__.objects.none()).count()
|
||||
counts["park_reviews"] = getattr(user, "park_reviews", user.__class__.objects.none()).count()
|
||||
counts["ride_reviews"] = getattr(user, "ride_reviews", user.__class__.objects.none()).count()
|
||||
counts["uploaded_park_photos"] = getattr(user, "uploaded_park_photos", user.__class__.objects.none()).count()
|
||||
counts["uploaded_ride_photos"] = getattr(user, "uploaded_ride_photos", user.__class__.objects.none()).count()
|
||||
counts["top_lists"] = getattr(user, "top_lists", user.__class__.objects.none()).count()
|
||||
counts["edit_submissions"] = getattr(user, "edit_submissions", user.__class__.objects.none()).count()
|
||||
counts["photo_submissions"] = getattr(user, "photo_submissions", user.__class__.objects.none()).count()
|
||||
|
||||
return counts
|
||||
|
||||
@@ -247,30 +238,30 @@ class UserDeletionService:
|
||||
# Note: Adjust these based on your actual model relationships
|
||||
|
||||
# Park reviews
|
||||
if hasattr(user, 'park_reviews'):
|
||||
if hasattr(user, "park_reviews"):
|
||||
user.park_reviews.all().update(user=placeholder_user)
|
||||
|
||||
# Ride reviews
|
||||
if hasattr(user, 'ride_reviews'):
|
||||
if hasattr(user, "ride_reviews"):
|
||||
user.ride_reviews.all().update(user=placeholder_user)
|
||||
|
||||
# Uploaded photos
|
||||
if hasattr(user, 'uploaded_park_photos'):
|
||||
if hasattr(user, "uploaded_park_photos"):
|
||||
user.uploaded_park_photos.all().update(user=placeholder_user)
|
||||
|
||||
if hasattr(user, 'uploaded_ride_photos'):
|
||||
if hasattr(user, "uploaded_ride_photos"):
|
||||
user.uploaded_ride_photos.all().update(user=placeholder_user)
|
||||
|
||||
# Top lists
|
||||
if hasattr(user, 'top_lists'):
|
||||
if hasattr(user, "top_lists"):
|
||||
user.top_lists.all().update(user=placeholder_user)
|
||||
|
||||
# Edit submissions
|
||||
if hasattr(user, 'edit_submissions'):
|
||||
if hasattr(user, "edit_submissions"):
|
||||
user.edit_submissions.all().update(user=placeholder_user)
|
||||
|
||||
# Photo submissions
|
||||
if hasattr(user, 'photo_submissions'):
|
||||
if hasattr(user, "photo_submissions"):
|
||||
user.photo_submissions.all().update(user=placeholder_user)
|
||||
|
||||
@staticmethod
|
||||
@@ -278,18 +269,16 @@ class UserDeletionService:
|
||||
"""Send verification email for account deletion."""
|
||||
try:
|
||||
context = {
|
||||
'user': user,
|
||||
'verification_code': verification_code,
|
||||
'expires_at': expires_at,
|
||||
'site_name': 'ThrillWiki',
|
||||
'site_url': getattr(settings, 'SITE_URL', 'https://thrillwiki.com'),
|
||||
"user": user,
|
||||
"verification_code": verification_code,
|
||||
"expires_at": expires_at,
|
||||
"site_name": "ThrillWiki",
|
||||
"site_url": getattr(settings, "SITE_URL", "https://thrillwiki.com"),
|
||||
}
|
||||
|
||||
subject = 'ThrillWiki: Confirm Account Deletion'
|
||||
html_message = render_to_string(
|
||||
'emails/account_deletion_verification.html', context)
|
||||
plain_message = render_to_string(
|
||||
'emails/account_deletion_verification.txt', context)
|
||||
subject = "ThrillWiki: Confirm Account Deletion"
|
||||
html_message = render_to_string("emails/account_deletion_verification.html", context)
|
||||
plain_message = render_to_string("emails/account_deletion_verification.txt", context)
|
||||
|
||||
send_mail(
|
||||
subject=subject,
|
||||
@@ -303,6 +292,5 @@ class UserDeletionService:
|
||||
logger.info(f"Deletion verification email sent to {user.email}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to send deletion verification email to {user.email}: {str(e)}")
|
||||
logger.error(f"Failed to send deletion verification email to {user.email}: {str(e)}")
|
||||
raise
|
||||
|
||||
Reference in New Issue
Block a user