Files
pacnpal d504d41de2 feat: complete monorepo structure with frontend and shared resources
- Add complete backend/ directory with full Django application
- Add frontend/ directory with Vite + TypeScript setup ready for Next.js
- Add comprehensive shared/ directory with:
  - Complete documentation and memory-bank archives
  - Media files and avatars (letters, park/ride images)
  - Deployment scripts and automation tools
  - Shared types and utilities
- Add architecture/ directory with migration guides
- Configure pnpm workspace for monorepo development
- Update .gitignore to exclude .django_tailwind_cli/ build artifacts
- Preserve all historical documentation in shared/docs/memory-bank/
- Set up proper structure for full-stack development with shared resources
2025-08-23 18:40:07 -04:00

120 lines
4.4 KiB
Python

from typing import Any, Optional, cast
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.conf import settings
from PIL import Image, ExifTags
from datetime import datetime
from .storage import MediaStorage
from apps.rides.models import Ride
from django.utils import timezone
from apps.core.history import TrackedModel
import pghistory
def photo_upload_path(instance: models.Model, filename: str) -> str:
"""Generate upload path for photos using normalized filenames"""
# Get the content type and object
photo = cast(Photo, instance)
content_type = photo.content_type.model
obj = photo.content_object
if obj is None:
raise ValueError("Content object cannot be None")
# Get object identifier (slug or id)
identifier = getattr(obj, "slug", None)
if identifier is None:
identifier = obj.pk # Use pk instead of id as it's guaranteed to exist
# Create normalized filename - always use .jpg extension
base_filename = f"{identifier}.jpg"
# If it's a ride photo, store it under the park's directory
if content_type == "ride":
ride = cast(Ride, obj)
return f"park/{ride.park.slug}/{identifier}/{base_filename}"
# For park photos, store directly in park directory
return f"park/{identifier}/{base_filename}"
@pghistory.track()
class Photo(TrackedModel):
"""Generic photo model that can be attached to any model"""
image = models.ImageField(
upload_to=photo_upload_path, # type: ignore[arg-type]
max_length=255,
storage=MediaStorage(),
)
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) # New field for approval status
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
date_taken = models.DateTimeField(null=True, blank=True)
uploaded_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
related_name="uploaded_photos",
)
# Generic foreign key fields
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey("content_type", "object_id")
class Meta:
ordering = ["-is_primary", "-created_at"]
indexes = [
models.Index(fields=["content_type", "object_id"]),
]
def __str__(self) -> str:
return f"{self.content_type} - {self.content_object} - {self.caption or 'No caption'}"
def extract_exif_date(self) -> Optional[datetime]:
"""Extract the date taken from image EXIF data"""
try:
with Image.open(self.image) as img:
exif = img.getexif()
if exif:
# Find the DateTime tag ID
for tag_id in ExifTags.TAGS:
if ExifTags.TAGS[tag_id] == "DateTimeOriginal":
if tag_id in exif:
# EXIF dates are typically in format:
# '2024:02:15 14:30:00'
date_str = exif[tag_id]
return datetime.strptime(date_str, "%Y:%m:%d %H:%M:%S")
return None
except Exception:
return None
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:
self.date_taken = self.extract_exif_date()
# Set default caption if not provided
if not self.caption and self.uploaded_by:
current_time = timezone.now()
self.caption = f"Uploaded by {
self.uploaded_by.username} on {
current_time.strftime('%B %d, %Y at %I:%M %p')}"
# If this is marked as primary, unmark other primary photos
if self.is_primary:
Photo.objects.filter(
content_type=self.content_type,
object_id=self.object_id,
is_primary=True,
).exclude(pk=self.pk).update(
is_primary=False
) # Use pk instead of id
super().save(*args, **kwargs)