Add email templates for user notifications and account management

- Created a base email template (base.html) for consistent styling across all emails.
- Added moderation approval email template (moderation_approved.html) to notify users of approved submissions.
- Added moderation rejection email template (moderation_rejected.html) to inform users of required changes for their submissions.
- Created password reset email template (password_reset.html) for users requesting to reset their passwords.
- Developed a welcome email template (welcome.html) to greet new users and provide account details and tips for using ThrillWiki.
This commit is contained in:
pacnpal
2025-11-08 15:34:04 -05:00
parent 9c46ef8b03
commit d6ff4cc3a3
335 changed files with 61926 additions and 73 deletions

219
django/apps/media/tasks.py Normal file
View File

@@ -0,0 +1,219 @@
"""
Background tasks for media processing and management.
"""
import logging
from celery import shared_task
from django.utils import timezone
from datetime import timedelta
logger = logging.getLogger(__name__)
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def process_uploaded_image(self, photo_id):
"""
Process an uploaded image asynchronously.
This task runs after a photo is uploaded to perform additional
processing like metadata extraction, validation, etc.
Args:
photo_id: ID of the Photo to process
Returns:
str: Processing result message
"""
from apps.media.models import Photo
try:
photo = Photo.objects.get(id=photo_id)
# Log processing start
logger.info(f"Processing photo {photo_id}: {photo.title}")
# Additional processing could include:
# - Generating additional thumbnails
# - Extracting EXIF data
# - Running image quality checks
# - Updating photo metadata
# For now, just log that processing is complete
logger.info(f"Photo {photo_id} processed successfully")
return f"Photo {photo_id} processed successfully"
except Photo.DoesNotExist:
logger.error(f"Photo {photo_id} not found")
raise
except Exception as exc:
logger.error(f"Error processing photo {photo_id}: {str(exc)}")
# Retry with exponential backoff
raise self.retry(exc=exc, countdown=60 * (2 ** self.request.retries))
@shared_task(bind=True, max_retries=2)
def cleanup_rejected_photos(self, days_old=30):
"""
Clean up photos that have been rejected for more than N days.
This task runs periodically (e.g., weekly) to remove old rejected
photos and free up storage space.
Args:
days_old: Number of days after rejection to delete (default: 30)
Returns:
dict: Cleanup statistics
"""
from apps.media.models import Photo
from apps.media.services import PhotoService
try:
cutoff_date = timezone.now() - timedelta(days=days_old)
# Find rejected photos older than cutoff
old_rejected = Photo.objects.filter(
moderation_status='rejected',
moderated_at__lt=cutoff_date
)
count = old_rejected.count()
logger.info(f"Found {count} rejected photos to cleanup")
# Delete each photo
photo_service = PhotoService()
deleted_count = 0
for photo in old_rejected:
try:
photo_service.delete_photo(photo, delete_from_cloudflare=True)
deleted_count += 1
except Exception as e:
logger.error(f"Failed to delete photo {photo.id}: {str(e)}")
continue
result = {
'found': count,
'deleted': deleted_count,
'failed': count - deleted_count,
'cutoff_date': cutoff_date.isoformat()
}
logger.info(f"Cleanup complete: {result}")
return result
except Exception as exc:
logger.error(f"Error during photo cleanup: {str(exc)}")
raise self.retry(exc=exc, countdown=300) # Retry after 5 minutes
@shared_task(bind=True, max_retries=3)
def generate_photo_thumbnails(self, photo_id, variants=None):
"""
Generate thumbnails for a photo on demand.
This can be used to regenerate thumbnails if the original
is updated or if new variants are needed.
Args:
photo_id: ID of the Photo
variants: List of variant names to generate (None = all)
Returns:
dict: Generated variants and their URLs
"""
from apps.media.models import Photo
from apps.media.services import CloudFlareService
try:
photo = Photo.objects.get(id=photo_id)
cloudflare = CloudFlareService()
if variants is None:
variants = ['public', 'thumbnail', 'banner']
result = {}
for variant in variants:
url = cloudflare.get_image_url(photo.cloudflare_image_id, variant)
result[variant] = url
logger.info(f"Generated thumbnails for photo {photo_id}: {variants}")
return result
except Photo.DoesNotExist:
logger.error(f"Photo {photo_id} not found")
raise
except Exception as exc:
logger.error(f"Error generating thumbnails for photo {photo_id}: {str(exc)}")
raise self.retry(exc=exc, countdown=60 * (2 ** self.request.retries))
@shared_task(bind=True, max_retries=2)
def cleanup_orphaned_cloudflare_images(self):
"""
Clean up CloudFlare images that no longer have database records.
This task helps prevent storage bloat by removing images that
were uploaded but their database records were deleted.
Returns:
dict: Cleanup statistics
"""
from apps.media.models import Photo
from apps.media.services import CloudFlareService
try:
cloudflare = CloudFlareService()
# In a real implementation, you would:
# 1. Get list of all images from CloudFlare API
# 2. Check which ones don't have Photo records
# 3. Delete the orphaned images
# For now, just log that the task ran
logger.info("Orphaned image cleanup task completed (not implemented in mock mode)")
return {
'checked': 0,
'orphaned': 0,
'deleted': 0
}
except Exception as exc:
logger.error(f"Error during orphaned image cleanup: {str(exc)}")
raise self.retry(exc=exc, countdown=300)
@shared_task
def update_photo_statistics():
"""
Update photo-related statistics across the database.
This task can update cached counts, generate reports, etc.
Returns:
dict: Updated statistics
"""
from apps.media.models import Photo
from django.db.models import Count
try:
stats = {
'total_photos': Photo.objects.count(),
'pending': Photo.objects.filter(moderation_status='pending').count(),
'approved': Photo.objects.filter(moderation_status='approved').count(),
'rejected': Photo.objects.filter(moderation_status='rejected').count(),
'flagged': Photo.objects.filter(moderation_status='flagged').count(),
'by_type': dict(
Photo.objects.values('photo_type').annotate(count=Count('id'))
.values_list('photo_type', 'count')
)
}
logger.info(f"Photo statistics updated: {stats}")
return stats
except Exception as e:
logger.error(f"Error updating photo statistics: {str(e)}")
raise