mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:11:11 -05:00
- 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.
196 lines
5.6 KiB
Python
196 lines
5.6 KiB
Python
"""
|
|
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
|