series of tests added with built-in django test support
18
media/migrations/0007_photo_date_taken.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.2 on 2024-11-05 18:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("media", "0006_photo_is_approved"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="photo",
|
||||
name="date_taken",
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -5,6 +5,9 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.text import slugify
|
||||
from django.conf import settings
|
||||
import os
|
||||
from PIL import Image, ExifTags
|
||||
from PIL.ExifTags import TAGS
|
||||
from datetime import datetime
|
||||
from .storage import MediaStorage
|
||||
from rides.models import Ride
|
||||
from django.utils import timezone
|
||||
@@ -56,6 +59,7 @@ class Photo(models.Model):
|
||||
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,
|
||||
@@ -76,8 +80,29 @@ class Photo(models.Model):
|
||||
|
||||
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()
|
||||
|
||||
BIN
media/submissions/photos/test.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_0kKwOne.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_2wg3j6L.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_4CpBdcl.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_5lfNeAh.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_7RtdCUN.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_86pBpH5.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_BrOnx06.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_IaqAVL6.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_JfXif5A.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_KvWaeSY.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_PS8HKUX.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_U7nTGc5.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_Uf25e5j.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_VxfclDl.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_aNvalWZ.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_bdQ64Pw.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_cUFi8YR.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_cj91lGL.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_doROVXr.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_ed2OKmf.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_iWXuwx6.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_llBhZbJ.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_mjx2aJb.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_o1PpFtd.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_rtW6iWX.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_uK9fein.gif
Normal file
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_wcxglNf.gif
Normal file
|
After Width: | Height: | Size: 35 B |
189
media/tests.py
Normal file
@@ -0,0 +1,189 @@
|
||||
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 datetime import datetime
|
||||
from PIL import Image, ExifTags
|
||||
import io
|
||||
from .models import Photo
|
||||
from parks.models import Park
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
class PhotoModelTests(TestCase):
|
||||
def setUp(self):
|
||||
# Create a test user
|
||||
self.user = User.objects.create_user(
|
||||
username='testuser',
|
||||
password='testpass123'
|
||||
)
|
||||
|
||||
# Create a test park for photo association
|
||||
self.park = Park.objects.create(
|
||||
name='Test Park',
|
||||
slug='test-park'
|
||||
)
|
||||
|
||||
self.content_type = ContentType.objects.get_for_model(Park)
|
||||
|
||||
def create_test_image_with_exif(self, date_taken=None):
|
||||
"""Helper method to create a test image with EXIF data"""
|
||||
# Create a test image
|
||||
image = Image.new('RGB', (100, 100), color='red')
|
||||
image_io = io.BytesIO()
|
||||
|
||||
# Add EXIF data if date_taken is provided
|
||||
if date_taken:
|
||||
exif_dict = {
|
||||
"0th": {},
|
||||
"Exif": {
|
||||
ExifTags.Base.DateTimeOriginal: date_taken.strftime("%Y:%m:%d %H:%M:%S").encode()
|
||||
}
|
||||
}
|
||||
image.save(image_io, 'JPEG', exif=exif_dict)
|
||||
else:
|
||||
image.save(image_io, 'JPEG')
|
||||
|
||||
image_io.seek(0)
|
||||
return SimpleUploadedFile(
|
||||
'test.jpg',
|
||||
image_io.getvalue(),
|
||||
content_type='image/jpeg'
|
||||
)
|
||||
|
||||
def test_photo_creation(self):
|
||||
"""Test basic photo creation"""
|
||||
photo = Photo.objects.create(
|
||||
image=SimpleUploadedFile(
|
||||
'test.jpg',
|
||||
b'dummy image data',
|
||||
content_type='image/jpeg'
|
||||
),
|
||||
caption='Test Caption',
|
||||
uploaded_by=self.user,
|
||||
content_type=self.content_type,
|
||||
object_id=self.park.pk
|
||||
)
|
||||
|
||||
self.assertEqual(photo.caption, 'Test Caption')
|
||||
self.assertEqual(photo.uploaded_by, self.user)
|
||||
self.assertIsNone(photo.date_taken)
|
||||
|
||||
def test_exif_date_extraction(self):
|
||||
"""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):
|
||||
"""Test photo upload without EXIF data"""
|
||||
image_file = self.create_test_image_with_exif() # No date provided
|
||||
|
||||
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):
|
||||
"""Test default caption generation"""
|
||||
photo = Photo.objects.create(
|
||||
image=SimpleUploadedFile(
|
||||
'test.jpg',
|
||||
b'dummy image data',
|
||||
content_type='image/jpeg'
|
||||
),
|
||||
uploaded_by=self.user,
|
||||
content_type=self.content_type,
|
||||
object_id=self.park.pk
|
||||
)
|
||||
|
||||
expected_prefix = f"Uploaded by {self.user.username} on"
|
||||
self.assertTrue(photo.caption.startswith(expected_prefix))
|
||||
|
||||
def test_primary_photo_toggle(self):
|
||||
"""Test primary photo functionality"""
|
||||
# Create two photos
|
||||
photo1 = Photo.objects.create(
|
||||
image=SimpleUploadedFile(
|
||||
'test1.jpg',
|
||||
b'dummy image data',
|
||||
content_type='image/jpeg'
|
||||
),
|
||||
uploaded_by=self.user,
|
||||
content_type=self.content_type,
|
||||
object_id=self.park.pk,
|
||||
is_primary=True
|
||||
)
|
||||
|
||||
photo2 = Photo.objects.create(
|
||||
image=SimpleUploadedFile(
|
||||
'test2.jpg',
|
||||
b'dummy image data',
|
||||
content_type='image/jpeg'
|
||||
),
|
||||
uploaded_by=self.user,
|
||||
content_type=self.content_type,
|
||||
object_id=self.park.pk,
|
||||
is_primary=True
|
||||
)
|
||||
|
||||
# Refresh from database
|
||||
photo1.refresh_from_db()
|
||||
photo2.refresh_from_db()
|
||||
|
||||
# Verify only photo2 is primary
|
||||
self.assertFalse(photo1.is_primary)
|
||||
self.assertTrue(photo2.is_primary)
|
||||
|
||||
@override_settings(MEDIA_ROOT='test_media/')
|
||||
def test_photo_upload_path(self):
|
||||
"""Test photo upload path generation"""
|
||||
photo = Photo.objects.create(
|
||||
image=SimpleUploadedFile(
|
||||
'test.jpg',
|
||||
b'dummy image data',
|
||||
content_type='image/jpeg'
|
||||
),
|
||||
uploaded_by=self.user,
|
||||
content_type=self.content_type,
|
||||
object_id=self.park.pk
|
||||
)
|
||||
|
||||
expected_path = f"park/{self.park.slug}/"
|
||||
self.assertTrue(photo.image.name.startswith(expected_path))
|
||||
|
||||
def test_date_taken_field(self):
|
||||
"""Test date_taken field functionality"""
|
||||
test_date = timezone.now()
|
||||
photo = Photo.objects.create(
|
||||
image=SimpleUploadedFile(
|
||||
'test.jpg',
|
||||
b'dummy image data',
|
||||
content_type='image/jpeg'
|
||||
),
|
||||
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)
|
||||