Files
thrillwiki_django_no_react/media/tests.py
pacnpal b5bae44cb8 Add Road Trip Planner template with interactive map and trip management features
- Implemented a new HTML template for the Road Trip Planner.
- Integrated Leaflet.js for interactive mapping and routing.
- Added functionality for searching and selecting parks to include in a trip.
- Enabled drag-and-drop reordering of selected parks.
- Included trip optimization and route calculation features.
- Created a summary display for trip statistics.
- Added functionality to save trips and manage saved trips.
- Enhanced UI with responsive design and dark mode support.
2025-08-15 20:53:00 -04:00

274 lines
9.7 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.test.utils import override_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 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)