mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 15:31:08 -05:00
- 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
271 lines
9.5 KiB
Python
271 lines
9.5 KiB
Python
from django.test import TestCase, override_settings
|
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
from django.contrib.auth import get_user_model
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.utils import timezone
|
|
from django.conf import settings
|
|
from django.db import models
|
|
from datetime import datetime
|
|
from PIL import Image
|
|
import piexif # type: ignore
|
|
import io
|
|
import shutil
|
|
import tempfile
|
|
import os
|
|
import logging
|
|
from typing import Optional, Any, Generator, cast
|
|
from contextlib import contextmanager
|
|
from .models import Photo
|
|
from .storage import MediaStorage
|
|
from apps.parks.models import Park, Company as Operator
|
|
|
|
User = get_user_model()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@override_settings(MEDIA_ROOT=tempfile.mkdtemp())
|
|
class PhotoModelTests(TestCase):
|
|
test_media_root: str
|
|
user: models.Model
|
|
park: Park
|
|
content_type: ContentType
|
|
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
super().setUpClass()
|
|
cls.test_media_root = settings.MEDIA_ROOT
|
|
|
|
@classmethod
|
|
def tearDownClass(cls) -> None:
|
|
try:
|
|
shutil.rmtree(cls.test_media_root, ignore_errors=True)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to clean up test media directory: {e}")
|
|
super().tearDownClass()
|
|
|
|
def setUp(self) -> None:
|
|
self.user = self._create_test_user()
|
|
self.park = self._create_test_park()
|
|
self.content_type = ContentType.objects.get_for_model(Park)
|
|
self._setup_test_directory()
|
|
|
|
def tearDown(self) -> None:
|
|
self._cleanup_test_directory()
|
|
Photo.objects.all().delete()
|
|
with self._reset_storage_state():
|
|
pass
|
|
|
|
def _create_test_user(self) -> models.Model:
|
|
"""Create a test user for the tests"""
|
|
return User.objects.create_user(username="testuser", password="testpass123")
|
|
|
|
def _create_test_park(self) -> Park:
|
|
"""Create a test park for the tests"""
|
|
operator = Operator.objects.create(name="Test Operator")
|
|
return Park.objects.create(
|
|
name="Test Park", slug="test-park", operator=operator
|
|
)
|
|
|
|
def _setup_test_directory(self) -> None:
|
|
"""Set up test directory and clean any existing test files"""
|
|
try:
|
|
# Clean up any existing test park directory
|
|
test_park_dir = os.path.join(settings.MEDIA_ROOT, "park", "test-park")
|
|
if os.path.exists(test_park_dir):
|
|
shutil.rmtree(test_park_dir, ignore_errors=True)
|
|
|
|
# Create necessary directories
|
|
os.makedirs(test_park_dir, exist_ok=True)
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Failed to set up test directory: {e}")
|
|
raise
|
|
|
|
def _cleanup_test_directory(self) -> None:
|
|
"""Clean up test directories and files"""
|
|
try:
|
|
test_park_dir = os.path.join(settings.MEDIA_ROOT, "park", "test-park")
|
|
if os.path.exists(test_park_dir):
|
|
shutil.rmtree(test_park_dir, ignore_errors=True)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to clean up test directory: {e}")
|
|
|
|
@contextmanager
|
|
def _reset_storage_state(self) -> Generator[None, None, None]:
|
|
"""Safely reset storage state"""
|
|
try:
|
|
MediaStorage.reset_counters()
|
|
yield
|
|
finally:
|
|
MediaStorage.reset_counters()
|
|
|
|
def create_test_image_with_exif(
|
|
self, date_taken: Optional[datetime] = None, filename: str = "test.jpg"
|
|
) -> SimpleUploadedFile:
|
|
"""Helper method to create a test image with EXIF data"""
|
|
image = Image.new("RGB", (100, 100), color="red")
|
|
image_io = io.BytesIO()
|
|
|
|
# Save image first without EXIF
|
|
image.save(image_io, "JPEG")
|
|
image_io.seek(0)
|
|
|
|
if date_taken:
|
|
# Create EXIF data
|
|
exif_dict = {
|
|
"0th": {},
|
|
"Exif": {
|
|
piexif.ExifIFD.DateTimeOriginal: date_taken.strftime(
|
|
"%Y:%m:%d %H:%M:%S"
|
|
).encode()
|
|
},
|
|
}
|
|
exif_bytes = piexif.dump(exif_dict)
|
|
|
|
# Insert EXIF into image
|
|
image_with_exif = io.BytesIO()
|
|
piexif.insert(exif_bytes, image_io.getvalue(), image_with_exif)
|
|
image_with_exif.seek(0)
|
|
image_data = image_with_exif.getvalue()
|
|
else:
|
|
image_data = image_io.getvalue()
|
|
|
|
return SimpleUploadedFile(filename, image_data, content_type="image/jpeg")
|
|
|
|
def test_filename_normalization(self) -> None:
|
|
"""Test that filenames are properly normalized"""
|
|
with self._reset_storage_state():
|
|
# Test with various problematic filenames
|
|
test_cases = [
|
|
("test with spaces.jpg", "test-park_1.jpg"),
|
|
("TEST_UPPER.JPG", "test-park_2.jpg"),
|
|
("special@#chars.jpeg", "test-park_3.jpg"),
|
|
("no-extension", "test-park_4.jpg"),
|
|
("multiple...dots.jpg", "test-park_5.jpg"),
|
|
("très_açaí.jpg", "test-park_6.jpg"), # Unicode characters
|
|
]
|
|
|
|
for input_name, expected_suffix in test_cases:
|
|
photo = Photo.objects.create(
|
|
image=self.create_test_image_with_exif(filename=input_name),
|
|
uploaded_by=self.user,
|
|
content_type=self.content_type,
|
|
object_id=self.park.pk,
|
|
)
|
|
|
|
# Check that the filename follows the normalized pattern
|
|
self.assertTrue(
|
|
photo.image.name.endswith(expected_suffix),
|
|
f"Expected filename to end with {expected_suffix}, got {
|
|
photo.image.name}",
|
|
)
|
|
|
|
# Verify the path structure
|
|
expected_path = f"park/{self.park.slug}/"
|
|
self.assertTrue(
|
|
photo.image.name.startswith(expected_path),
|
|
f"Expected path to start with {expected_path}, got {
|
|
photo.image.name}",
|
|
)
|
|
|
|
def test_sequential_filename_numbering(self) -> None:
|
|
"""Test that sequential files get proper numbering"""
|
|
with self._reset_storage_state():
|
|
# Create multiple photos and verify numbering
|
|
for i in range(1, 4):
|
|
photo = Photo.objects.create(
|
|
image=self.create_test_image_with_exif(),
|
|
uploaded_by=self.user,
|
|
content_type=self.content_type,
|
|
object_id=self.park.pk,
|
|
)
|
|
|
|
expected_name = f"park/{self.park.slug}/test-park_{i}.jpg"
|
|
self.assertEqual(
|
|
photo.image.name,
|
|
expected_name,
|
|
f"Expected {expected_name}, got {photo.image.name}",
|
|
)
|
|
|
|
def test_exif_date_extraction(self) -> None:
|
|
"""Test EXIF date extraction from uploaded photos"""
|
|
test_date = datetime(2024, 1, 1, 12, 0, 0)
|
|
image_file = self.create_test_image_with_exif(test_date)
|
|
|
|
photo = Photo.objects.create(
|
|
image=image_file,
|
|
uploaded_by=self.user,
|
|
content_type=self.content_type,
|
|
object_id=self.park.pk,
|
|
)
|
|
|
|
if photo.date_taken:
|
|
self.assertEqual(
|
|
photo.date_taken.strftime("%Y-%m-%d %H:%M:%S"),
|
|
test_date.strftime("%Y-%m-%d %H:%M:%S"),
|
|
)
|
|
else:
|
|
self.skipTest("EXIF data extraction not supported in test environment")
|
|
|
|
def test_photo_without_exif(self) -> None:
|
|
"""Test photo upload without EXIF data"""
|
|
image_file = self.create_test_image_with_exif()
|
|
|
|
photo = Photo.objects.create(
|
|
image=image_file,
|
|
uploaded_by=self.user,
|
|
content_type=self.content_type,
|
|
object_id=self.park.pk,
|
|
)
|
|
|
|
self.assertIsNone(photo.date_taken)
|
|
|
|
def test_default_caption(self) -> None:
|
|
"""Test default caption generation"""
|
|
photo = Photo.objects.create(
|
|
image=self.create_test_image_with_exif(),
|
|
uploaded_by=self.user,
|
|
content_type=self.content_type,
|
|
object_id=self.park.pk,
|
|
)
|
|
|
|
expected_prefix = f"Uploaded by {cast(Any, self.user).username} on"
|
|
self.assertTrue(photo.caption.startswith(expected_prefix))
|
|
|
|
def test_primary_photo_toggle(self) -> None:
|
|
"""Test primary photo functionality"""
|
|
photo1 = Photo.objects.create(
|
|
image=self.create_test_image_with_exif(),
|
|
uploaded_by=self.user,
|
|
content_type=self.content_type,
|
|
object_id=self.park.pk,
|
|
is_primary=True,
|
|
)
|
|
|
|
photo2 = Photo.objects.create(
|
|
image=self.create_test_image_with_exif(),
|
|
uploaded_by=self.user,
|
|
content_type=self.content_type,
|
|
object_id=self.park.pk,
|
|
is_primary=True,
|
|
)
|
|
|
|
photo1.refresh_from_db()
|
|
photo2.refresh_from_db()
|
|
|
|
self.assertFalse(photo1.is_primary)
|
|
self.assertTrue(photo2.is_primary)
|
|
|
|
def test_date_taken_field(self) -> None:
|
|
"""Test date_taken field functionality"""
|
|
test_date = timezone.now()
|
|
photo = Photo.objects.create(
|
|
image=self.create_test_image_with_exif(),
|
|
uploaded_by=self.user,
|
|
content_type=self.content_type,
|
|
object_id=self.park.pk,
|
|
date_taken=test_date,
|
|
)
|
|
|
|
self.assertEqual(photo.date_taken, test_date)
|