mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 03:51:09 -05:00
- Added migration to convert unique_together constraints to UniqueConstraint for RideModel. - Introduced RideFormMixin for handling entity suggestions in ride forms. - Created comprehensive code standards documentation outlining formatting, docstring requirements, complexity guidelines, and testing requirements. - Established error handling guidelines with a structured exception hierarchy and best practices for API and view error handling. - Documented view pattern guidelines, emphasizing the use of CBVs, FBVs, and ViewSets with examples. - Implemented a benchmarking script for query performance analysis and optimization. - Developed security documentation detailing measures, configurations, and a security checklist. - Compiled a database optimization guide covering indexing strategies, query optimization patterns, and computed fields.
238 lines
7.4 KiB
Python
238 lines
7.4 KiB
Python
"""
|
|
Photo upload and management views for ThrillWiki.
|
|
|
|
Security Note:
|
|
All uploads are validated for file type, size, and content before being saved.
|
|
Rate limiting is enforced to prevent abuse.
|
|
"""
|
|
|
|
from django.http import JsonResponse
|
|
from django.views.decorators.http import require_http_methods
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.shortcuts import get_object_or_404
|
|
import json
|
|
import logging
|
|
|
|
from .models import Photo
|
|
from apps.core.utils.file_scanner import (
|
|
validate_image_upload,
|
|
FileValidationError,
|
|
check_upload_rate_limit,
|
|
increment_upload_count,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def upload_photo(request):
|
|
"""
|
|
Handle photo upload for any model.
|
|
|
|
Security measures:
|
|
- Rate limiting to prevent abuse
|
|
- File type validation (extension, MIME type, magic number)
|
|
- File size validation
|
|
- Image integrity validation
|
|
- Permission checks
|
|
"""
|
|
try:
|
|
# Security: Check rate limiting before processing upload
|
|
is_allowed, rate_limit_message = check_upload_rate_limit(request.user.id)
|
|
if not is_allowed:
|
|
logger.warning(
|
|
f"User {request.user} exceeded upload rate limit"
|
|
)
|
|
return JsonResponse(
|
|
{"error": rate_limit_message},
|
|
status=429, # Too Many Requests
|
|
)
|
|
|
|
# Get app label, model, and object ID
|
|
app_label = request.POST.get("app_label")
|
|
model = request.POST.get("model")
|
|
object_id = request.POST.get("object_id")
|
|
|
|
# Log received data (don't log file contents for security)
|
|
logger.debug(
|
|
f"Received upload request - app_label: {app_label}, model: {model}, object_id: {object_id}"
|
|
)
|
|
|
|
# Validate required fields
|
|
missing_fields = []
|
|
if not app_label:
|
|
missing_fields.append("app_label")
|
|
if not model:
|
|
missing_fields.append("model")
|
|
if not object_id:
|
|
missing_fields.append("object_id")
|
|
if "image" not in request.FILES:
|
|
missing_fields.append("image")
|
|
|
|
if missing_fields:
|
|
return JsonResponse(
|
|
{"error": f'Missing required fields: {", ".join(missing_fields)}'},
|
|
status=400,
|
|
)
|
|
|
|
# Security: Validate uploaded file before processing
|
|
uploaded_file = request.FILES["image"]
|
|
try:
|
|
validate_image_upload(uploaded_file)
|
|
except FileValidationError as e:
|
|
logger.warning(
|
|
f"User {request.user} attempted to upload invalid file: {str(e)}"
|
|
)
|
|
return JsonResponse(
|
|
{"error": str(e)},
|
|
status=400,
|
|
)
|
|
|
|
# Get content type
|
|
try:
|
|
content_type = ContentType.objects.get(
|
|
app_label=app_label.lower(), model=model.lower()
|
|
)
|
|
except ContentType.DoesNotExist:
|
|
return JsonResponse(
|
|
{"error": f"Invalid content type: {app_label}.{model}"},
|
|
status=400,
|
|
)
|
|
|
|
# Get the object instance
|
|
try:
|
|
obj = content_type.get_object_for_this_type(pk=object_id)
|
|
except Exception as e:
|
|
return JsonResponse(
|
|
{
|
|
"error": f"Object not found: {app_label}.{model} with id {object_id}. Error: {
|
|
str(e)}"
|
|
},
|
|
status=404,
|
|
)
|
|
|
|
# Check if user has permission to add photos
|
|
if not request.user.has_perm("media.add_photo"):
|
|
logger.warning(
|
|
f"User {
|
|
request.user} attempted to upload photo without permission"
|
|
)
|
|
return JsonResponse(
|
|
{"error": "You do not have permission to upload photos"},
|
|
status=403,
|
|
)
|
|
|
|
# Determine if the photo should be auto-approved
|
|
is_approved = (
|
|
request.user.is_superuser
|
|
or request.user.is_staff
|
|
or request.user.groups.filter(name="Moderators").exists()
|
|
)
|
|
|
|
# Create the photo
|
|
photo = Photo.objects.create(
|
|
image=uploaded_file,
|
|
content_type=content_type,
|
|
object_id=obj.pk,
|
|
uploaded_by=request.user,
|
|
is_primary=not Photo.objects.filter(
|
|
content_type=content_type, object_id=obj.pk
|
|
).exists(),
|
|
is_approved=is_approved,
|
|
)
|
|
|
|
# Security: Increment upload count for rate limiting
|
|
increment_upload_count(request.user.id)
|
|
|
|
return JsonResponse(
|
|
{
|
|
"id": photo.pk,
|
|
"url": photo.image.url,
|
|
"caption": photo.caption,
|
|
"is_primary": photo.is_primary,
|
|
"is_approved": photo.is_approved,
|
|
}
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in upload_photo: {str(e)}", exc_info=True)
|
|
return JsonResponse(
|
|
{"error": "An error occurred while uploading the photo"},
|
|
status=400,
|
|
)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def set_primary_photo(request, photo_id):
|
|
"""Set a photo as primary"""
|
|
try:
|
|
photo = get_object_or_404(Photo, pk=photo_id)
|
|
|
|
# Check if user has permission to edit photos
|
|
if not request.user.has_perm("media.change_photo"):
|
|
return JsonResponse(
|
|
{"error": "You do not have permission to edit photos"},
|
|
status=403,
|
|
)
|
|
|
|
# Set this photo as primary
|
|
photo.is_primary = True
|
|
photo.save() # This will automatically unset other primary photos
|
|
|
|
return JsonResponse({"status": "success"})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in set_primary_photo: {str(e)}", exc_info=True)
|
|
return JsonResponse({"error": str(e)}, status=400)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def update_caption(request, photo_id):
|
|
"""Update a photo's caption"""
|
|
try:
|
|
photo = get_object_or_404(Photo, pk=photo_id)
|
|
|
|
# Check if user has permission to edit photos
|
|
if not request.user.has_perm("media.change_photo"):
|
|
return JsonResponse(
|
|
{"error": "You do not have permission to edit photos"},
|
|
status=403,
|
|
)
|
|
|
|
# Update caption
|
|
data = json.loads(request.body)
|
|
photo.caption = data.get("caption", "")
|
|
photo.save()
|
|
|
|
return JsonResponse({"id": photo.pk, "caption": photo.caption})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in update_caption: {str(e)}", exc_info=True)
|
|
return JsonResponse({"error": str(e)}, status=400)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["DELETE"])
|
|
def delete_photo(request, photo_id):
|
|
"""Delete a photo"""
|
|
try:
|
|
photo = get_object_or_404(Photo, pk=photo_id)
|
|
|
|
# Check if user has permission to delete photos
|
|
if not request.user.has_perm("media.delete_photo"):
|
|
return JsonResponse(
|
|
{"error": "You do not have permission to delete photos"},
|
|
status=403,
|
|
)
|
|
|
|
photo.delete()
|
|
return JsonResponse({"status": "success"})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in delete_photo: {str(e)}", exc_info=True)
|
|
return JsonResponse({"error": str(e)}, status=400)
|