Files
thrillwiki_django_no_react/backend/apps/parks/models/media.py
pacnpal edcd8f2076 Add secret management guide, client-side performance monitoring, and search accessibility enhancements
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols.
- Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage.
- Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
2025-12-23 16:41:42 -05:00

135 lines
4.2 KiB
Python

"""
Park-specific media models for ThrillWiki.
This module contains media models specific to parks domain.
"""
from typing import Any, List, Optional, cast
from django.db import models
from django.conf import settings
from apps.core.history import TrackedModel
from apps.core.services.media_service import MediaService
import pghistory
def park_photo_upload_path(instance: models.Model, filename: str) -> str:
"""Generate upload path for park photos."""
photo = cast("ParkPhoto", instance)
park = photo.park
if park is None:
raise ValueError("Park cannot be None")
return MediaService.generate_upload_path(
domain="park", identifier=park.slug, filename=filename
)
@pghistory.track()
class ParkPhoto(TrackedModel):
"""Photo model specific to parks."""
park = models.ForeignKey(
"parks.Park",
on_delete=models.CASCADE,
related_name="photos",
help_text="Park this photo belongs to",
)
image = models.ForeignKey(
'django_cloudflareimages_toolkit.CloudflareImage',
on_delete=models.CASCADE,
help_text="Park photo stored on Cloudflare Images"
)
caption = models.CharField(
max_length=255, blank=True, help_text="Photo caption or description"
)
alt_text = models.CharField(
max_length=255, blank=True, help_text="Alternative text for accessibility"
)
is_primary = models.BooleanField(
default=False, help_text="Whether this is the primary photo for the park"
)
is_approved = models.BooleanField(
default=False, help_text="Whether this photo has been approved by moderators"
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
date_taken = models.DateTimeField(null=True, blank=True)
# User who uploaded the photo
uploaded_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
related_name="uploaded_park_photos",
help_text="User who uploaded this photo",
)
class Meta(TrackedModel.Meta):
app_label = "parks"
verbose_name = "Park Photo"
verbose_name_plural = "Park Photos"
ordering = ["-is_primary", "-created_at"]
indexes = [
models.Index(fields=["park", "is_primary"]),
models.Index(fields=["park", "is_approved"]),
models.Index(fields=["created_at"]),
]
constraints = [
# Only one primary photo per park
models.UniqueConstraint(
fields=["park"],
condition=models.Q(is_primary=True),
name="unique_primary_park_photo",
)
]
def __str__(self) -> str:
return f"Photo of {self.park.name} - {self.caption or 'No caption'}"
def save(self, *args: Any, **kwargs: Any) -> None:
# Extract EXIF date if this is a new photo
if not self.pk and not self.date_taken and self.image:
self.date_taken = MediaService.extract_exif_date(self.image)
# Set default caption if not provided
if not self.caption and self.uploaded_by:
self.caption = MediaService.generate_default_caption(
self.uploaded_by.username
)
# If this is marked as primary, unmark other primary photos for this park
if self.is_primary:
ParkPhoto.objects.filter(
park=self.park,
is_primary=True,
).exclude(
pk=self.pk
).update(is_primary=False)
super().save(*args, **kwargs)
@property
def file_size(self) -> Optional[int]:
"""Get file size in bytes."""
try:
return self.image.size
except (ValueError, OSError):
return None
@property
def dimensions(self) -> Optional[List[int]]:
"""Get image dimensions as [width, height]."""
try:
return [self.image.width, self.image.height]
except (ValueError, OSError):
return None
def get_absolute_url(self) -> str:
"""Get absolute URL for this photo."""
return f"/parks/{self.park.slug}/photos/{self.pk}/"