Files
thrillwiki_django_no_react/backend/apps/parks/models/media.py
pacnpal 08a4a2d034 feat: Add PrimeProgress, PrimeSelect, and PrimeSkeleton components with customizable styles and props
- Implemented PrimeProgress component with support for labels, helper text, and various styles (size, variant, color).
- Created PrimeSelect component with dropdown functionality, custom templates, and validation states.
- Developed PrimeSkeleton component for loading placeholders with different shapes and animations.
- Updated index.ts to export new components for easy import.
- Enhanced PrimeVueTest.vue to include tests for new components and their functionalities.
- Introduced a custom ThrillWiki theme for PrimeVue with tailored color schemes and component styles.
- Added ambient type declarations for various components to improve TypeScript support.
2025-08-27 21:00:02 -04:00

118 lines
3.6 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"
)
image = models.ImageField(
upload_to=park_photo_upload_path,
max_length=255,
)
caption = models.CharField(max_length=255, blank=True)
alt_text = models.CharField(max_length=255, blank=True)
is_primary = models.BooleanField(default=False)
is_approved = models.BooleanField(default=False)
# 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",
)
class Meta:
app_label = "parks"
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}/"