mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 11:51:14 -05:00
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:
195
django/apps/media/validators.py
Normal file
195
django/apps/media/validators.py
Normal file
@@ -0,0 +1,195 @@
|
||||
"""
|
||||
Validators for image uploads.
|
||||
"""
|
||||
|
||||
import magic
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile
|
||||
from PIL import Image
|
||||
from typing import Optional
|
||||
|
||||
|
||||
# Allowed file types
|
||||
ALLOWED_MIME_TYPES = [
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/png',
|
||||
'image/webp',
|
||||
'image/gif',
|
||||
]
|
||||
|
||||
ALLOWED_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.webp', '.gif']
|
||||
|
||||
# Size limits (in bytes)
|
||||
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB
|
||||
MIN_FILE_SIZE = 1024 # 1 KB
|
||||
|
||||
# Dimension limits
|
||||
MIN_WIDTH = 100
|
||||
MIN_HEIGHT = 100
|
||||
MAX_WIDTH = 8000
|
||||
MAX_HEIGHT = 8000
|
||||
|
||||
# Aspect ratio limits (for specific photo types)
|
||||
ASPECT_RATIO_LIMITS = {
|
||||
'banner': {'min': 2.0, 'max': 4.0}, # Wide banners
|
||||
'logo': {'min': 0.5, 'max': 2.0}, # Square-ish logos
|
||||
}
|
||||
|
||||
|
||||
def validate_image_file_type(file: InMemoryUploadedFile | TemporaryUploadedFile) -> None:
|
||||
"""
|
||||
Validate that the uploaded file is an allowed image type.
|
||||
|
||||
Uses python-magic to detect actual file type, not just extension.
|
||||
|
||||
Args:
|
||||
file: The uploaded file object
|
||||
|
||||
Raises:
|
||||
ValidationError: If file type is not allowed
|
||||
"""
|
||||
# Check file extension
|
||||
file_ext = None
|
||||
if hasattr(file, 'name') and file.name:
|
||||
file_ext = '.' + file.name.split('.')[-1].lower()
|
||||
if file_ext not in ALLOWED_EXTENSIONS:
|
||||
raise ValidationError(
|
||||
f"File extension {file_ext} not allowed. "
|
||||
f"Allowed extensions: {', '.join(ALLOWED_EXTENSIONS)}"
|
||||
)
|
||||
|
||||
# Check MIME type from content type
|
||||
if hasattr(file, 'content_type'):
|
||||
if file.content_type not in ALLOWED_MIME_TYPES:
|
||||
raise ValidationError(
|
||||
f"File type {file.content_type} not allowed. "
|
||||
f"Allowed types: {', '.join(ALLOWED_MIME_TYPES)}"
|
||||
)
|
||||
|
||||
# Verify actual file content using python-magic
|
||||
try:
|
||||
file.seek(0)
|
||||
mime = magic.from_buffer(file.read(2048), mime=True)
|
||||
file.seek(0)
|
||||
|
||||
if mime not in ALLOWED_MIME_TYPES:
|
||||
raise ValidationError(
|
||||
f"File content type {mime} does not match allowed types. "
|
||||
"File may be corrupted or incorrectly labeled."
|
||||
)
|
||||
except Exception as e:
|
||||
# If magic fails, we already validated content_type above
|
||||
pass
|
||||
|
||||
|
||||
def validate_image_file_size(file: InMemoryUploadedFile | TemporaryUploadedFile) -> None:
|
||||
"""
|
||||
Validate that the file size is within allowed limits.
|
||||
|
||||
Args:
|
||||
file: The uploaded file object
|
||||
|
||||
Raises:
|
||||
ValidationError: If file size is not within limits
|
||||
"""
|
||||
file_size = file.size
|
||||
|
||||
if file_size < MIN_FILE_SIZE:
|
||||
raise ValidationError(
|
||||
f"File size is too small. Minimum: {MIN_FILE_SIZE / 1024:.0f} KB"
|
||||
)
|
||||
|
||||
if file_size > MAX_FILE_SIZE:
|
||||
raise ValidationError(
|
||||
f"File size is too large. Maximum: {MAX_FILE_SIZE / (1024 * 1024):.0f} MB"
|
||||
)
|
||||
|
||||
|
||||
def validate_image_dimensions(
|
||||
file: InMemoryUploadedFile | TemporaryUploadedFile,
|
||||
photo_type: Optional[str] = None
|
||||
) -> None:
|
||||
"""
|
||||
Validate image dimensions and aspect ratio.
|
||||
|
||||
Args:
|
||||
file: The uploaded file object
|
||||
photo_type: Optional photo type for specific validation
|
||||
|
||||
Raises:
|
||||
ValidationError: If dimensions are not within limits
|
||||
"""
|
||||
try:
|
||||
file.seek(0)
|
||||
image = Image.open(file)
|
||||
width, height = image.size
|
||||
file.seek(0)
|
||||
except Exception as e:
|
||||
raise ValidationError(f"Could not read image dimensions: {str(e)}")
|
||||
|
||||
# Check minimum dimensions
|
||||
if width < MIN_WIDTH or height < MIN_HEIGHT:
|
||||
raise ValidationError(
|
||||
f"Image dimensions too small. Minimum: {MIN_WIDTH}x{MIN_HEIGHT}px, "
|
||||
f"got: {width}x{height}px"
|
||||
)
|
||||
|
||||
# Check maximum dimensions
|
||||
if width > MAX_WIDTH or height > MAX_HEIGHT:
|
||||
raise ValidationError(
|
||||
f"Image dimensions too large. Maximum: {MAX_WIDTH}x{MAX_HEIGHT}px, "
|
||||
f"got: {width}x{height}px"
|
||||
)
|
||||
|
||||
# Check aspect ratio for specific photo types
|
||||
if photo_type and photo_type in ASPECT_RATIO_LIMITS:
|
||||
aspect_ratio = width / height
|
||||
limits = ASPECT_RATIO_LIMITS[photo_type]
|
||||
|
||||
if aspect_ratio < limits['min'] or aspect_ratio > limits['max']:
|
||||
raise ValidationError(
|
||||
f"Invalid aspect ratio for {photo_type}. "
|
||||
f"Expected ratio between {limits['min']:.2f} and {limits['max']:.2f}, "
|
||||
f"got: {aspect_ratio:.2f}"
|
||||
)
|
||||
|
||||
|
||||
def validate_image(
|
||||
file: InMemoryUploadedFile | TemporaryUploadedFile,
|
||||
photo_type: Optional[str] = None
|
||||
) -> None:
|
||||
"""
|
||||
Run all image validations.
|
||||
|
||||
Args:
|
||||
file: The uploaded file object
|
||||
photo_type: Optional photo type for specific validation
|
||||
|
||||
Raises:
|
||||
ValidationError: If any validation fails
|
||||
"""
|
||||
validate_image_file_type(file)
|
||||
validate_image_file_size(file)
|
||||
validate_image_dimensions(file, photo_type)
|
||||
|
||||
|
||||
def validate_image_content_safety(file: InMemoryUploadedFile | TemporaryUploadedFile) -> None:
|
||||
"""
|
||||
Placeholder for content safety validation.
|
||||
|
||||
This could integrate with services like:
|
||||
- AWS Rekognition
|
||||
- Google Cloud Vision
|
||||
- Azure Content Moderator
|
||||
|
||||
For now, this is a no-op but provides extension point.
|
||||
|
||||
Args:
|
||||
file: The uploaded file object
|
||||
|
||||
Raises:
|
||||
ValidationError: If content is deemed unsafe
|
||||
"""
|
||||
# TODO: Integrate with content moderation API
|
||||
pass
|
||||
Reference in New Issue
Block a user