photos fix
@@ -0,0 +1 @@
|
||||
default_app_config = 'media.apps.MediaConfig'
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
from django.apps import AppConfig
|
||||
from django.db.models.signals import post_migrate
|
||||
|
||||
def create_photo_permissions(sender, **kwargs):
|
||||
"""Create custom permissions for photos"""
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from media.models import Photo
|
||||
|
||||
content_type = ContentType.objects.get_for_model(Photo)
|
||||
Permission.objects.get_or_create(
|
||||
codename='add_photo',
|
||||
name='Can add photo',
|
||||
content_type=content_type,
|
||||
)
|
||||
Permission.objects.get_or_create(
|
||||
codename='change_photo',
|
||||
name='Can change photo',
|
||||
content_type=content_type,
|
||||
)
|
||||
Permission.objects.get_or_create(
|
||||
codename='delete_photo',
|
||||
name='Can delete photo',
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
class MediaConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'media'
|
||||
verbose_name = 'Media'
|
||||
|
||||
def ready(self):
|
||||
post_migrate.connect(create_photo_permissions, sender=self)
|
||||
|
||||
114
media/management/commands/download_photos.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import os
|
||||
import requests
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.files import File
|
||||
from django.core.files.temp import NamedTemporaryFile
|
||||
from media.models import Photo
|
||||
from parks.models import Park
|
||||
from rides.models import Ride
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
import json
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Download photos from seed data URLs'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
self.stdout.write('Downloading photos from seed data...')
|
||||
|
||||
# Read seed data
|
||||
with open('parks/management/commands/seed_data.json', 'r') as f:
|
||||
seed_data = json.load(f)
|
||||
|
||||
park_content_type = ContentType.objects.get_for_model(Park)
|
||||
ride_content_type = ContentType.objects.get_for_model(Ride)
|
||||
|
||||
# Process parks and their photos
|
||||
for park_data in seed_data['parks']:
|
||||
try:
|
||||
park = Park.objects.get(name=park_data['name'])
|
||||
|
||||
# Download park photos
|
||||
for idx, photo_url in enumerate(park_data['photos'], 1):
|
||||
try:
|
||||
# Download image
|
||||
self.stdout.write(f'Downloading from URL: {photo_url}')
|
||||
response = requests.get(photo_url)
|
||||
if response.status_code == 200:
|
||||
# Delete any existing photos for this park
|
||||
Photo.objects.filter(
|
||||
content_type=park_content_type,
|
||||
object_id=park.id
|
||||
).delete()
|
||||
|
||||
# Create new photo record
|
||||
photo = Photo(
|
||||
content_type=park_content_type,
|
||||
object_id=park.id,
|
||||
is_primary=idx == 1
|
||||
)
|
||||
|
||||
# Save image content
|
||||
photo.image.save(
|
||||
f"{park.slug}_{idx}.jpg",
|
||||
ContentFile(response.content),
|
||||
save=False
|
||||
)
|
||||
photo.save()
|
||||
|
||||
self.stdout.write(f'Downloaded photo for {park.name}: {photo.image.name}')
|
||||
self.stdout.write(f'Database record created with ID: {photo.id}')
|
||||
else:
|
||||
self.stdout.write(f'Error downloading image. Status code: {response.status_code}')
|
||||
|
||||
except Exception as e:
|
||||
self.stdout.write(f'Error downloading park photo: {str(e)}')
|
||||
|
||||
# Process rides and their photos
|
||||
for ride_data in park_data['rides']:
|
||||
try:
|
||||
ride = Ride.objects.get(name=ride_data['name'], park=park)
|
||||
|
||||
# Download ride photos
|
||||
for idx, photo_url in enumerate(ride_data['photos'], 1):
|
||||
try:
|
||||
# Download image
|
||||
self.stdout.write(f'Downloading from URL: {photo_url}')
|
||||
response = requests.get(photo_url)
|
||||
if response.status_code == 200:
|
||||
# Delete any existing photos for this ride
|
||||
Photo.objects.filter(
|
||||
content_type=ride_content_type,
|
||||
object_id=ride.id
|
||||
).delete()
|
||||
|
||||
# Create new photo record
|
||||
photo = Photo(
|
||||
content_type=ride_content_type,
|
||||
object_id=ride.id,
|
||||
is_primary=idx == 1
|
||||
)
|
||||
|
||||
# Save image content
|
||||
photo.image.save(
|
||||
f"{ride.slug}_{idx}.jpg",
|
||||
ContentFile(response.content),
|
||||
save=False
|
||||
)
|
||||
photo.save()
|
||||
|
||||
self.stdout.write(f'Downloaded photo for {ride.name}: {photo.image.name}')
|
||||
self.stdout.write(f'Database record created with ID: {photo.id}')
|
||||
else:
|
||||
self.stdout.write(f'Error downloading image. Status code: {response.status_code}')
|
||||
|
||||
except Exception as e:
|
||||
self.stdout.write(f'Error downloading ride photo: {str(e)}')
|
||||
|
||||
except Ride.DoesNotExist:
|
||||
self.stdout.write(f'Ride not found: {ride_data["name"]}')
|
||||
|
||||
except Park.DoesNotExist:
|
||||
self.stdout.write(f'Park not found: {park_data["name"]}')
|
||||
|
||||
self.stdout.write('Finished downloading photos')
|
||||
58
media/management/commands/fix_photo_paths.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import os
|
||||
from django.core.management.base import BaseCommand
|
||||
from media.models import Photo
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Fix photo paths in database to match actual file locations'
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
self.stdout.write('Fixing photo paths in database...')
|
||||
|
||||
# Get all photos
|
||||
photos = Photo.objects.all()
|
||||
|
||||
for photo in photos:
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# Get current file path
|
||||
current_name = photo.image.name
|
||||
|
||||
# Remove any 'media/' prefix if it exists
|
||||
if current_name.startswith('media/'):
|
||||
current_name = current_name[6:] # Remove 'media/' prefix
|
||||
|
||||
parts = current_name.split('/')
|
||||
|
||||
if len(parts) >= 2:
|
||||
content_type = parts[0] # 'park' or 'ride'
|
||||
identifier = parts[1] # e.g., 'alton-towers'
|
||||
|
||||
# Look for files in the media directory
|
||||
media_dir = os.path.join('media', content_type, identifier)
|
||||
if os.path.exists(media_dir):
|
||||
files = [f for f in os.listdir(media_dir)
|
||||
if not f.startswith('.') and # Skip hidden files
|
||||
not f.startswith('tmp') and # Skip temp files
|
||||
os.path.isfile(os.path.join(media_dir, f))]
|
||||
|
||||
if files:
|
||||
# Get the first file and update the database record
|
||||
file_path = os.path.join(content_type, identifier, files[0])
|
||||
if os.path.exists(os.path.join('media', file_path)):
|
||||
photo.image.name = file_path
|
||||
photo.save()
|
||||
self.stdout.write(f'Updated path for photo {photo.id} to {file_path}')
|
||||
else:
|
||||
self.stdout.write(f'File not found for photo {photo.id}: {file_path}')
|
||||
else:
|
||||
self.stdout.write(f'No files found in directory for photo {photo.id}: {media_dir}')
|
||||
else:
|
||||
self.stdout.write(f'Directory not found for photo {photo.id}: {media_dir}')
|
||||
|
||||
except Exception as e:
|
||||
self.stdout.write(f'Error updating photo {photo.id}: {str(e)}')
|
||||
continue
|
||||
|
||||
self.stdout.write('Finished fixing photo paths')
|
||||
116
media/management/commands/move_photos.py
Normal file
@@ -0,0 +1,116 @@
|
||||
import os
|
||||
from django.core.management.base import BaseCommand
|
||||
from media.models import Photo
|
||||
from django.conf import settings
|
||||
import shutil
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Move photo files to their normalized locations"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
self.stdout.write("Moving photo files to normalized locations...")
|
||||
|
||||
# Get all photos
|
||||
photos = Photo.objects.all()
|
||||
|
||||
# Track processed files to clean up later
|
||||
processed_files = set()
|
||||
|
||||
for photo in photos:
|
||||
try:
|
||||
# Get current file path
|
||||
current_name = photo.image.name
|
||||
current_path = os.path.join(settings.MEDIA_ROOT, current_name)
|
||||
|
||||
# Try to find the actual file
|
||||
if not os.path.exists(current_path):
|
||||
# Check if file exists in the old location structure
|
||||
parts = current_name.split("/")
|
||||
if len(parts) >= 2:
|
||||
content_type = parts[0] # 'park' or 'ride'
|
||||
identifier = parts[1] # e.g., 'alton-towers'
|
||||
|
||||
# Look for any files in that directory
|
||||
old_dir = os.path.join(
|
||||
settings.MEDIA_ROOT, content_type, identifier
|
||||
)
|
||||
if os.path.exists(old_dir):
|
||||
files = [
|
||||
f
|
||||
for f in os.listdir(old_dir)
|
||||
if not f.startswith(".") # Skip hidden files
|
||||
and not f.startswith("tmp") # Skip temp files
|
||||
and os.path.isfile(os.path.join(old_dir, f))
|
||||
]
|
||||
if files:
|
||||
current_path = os.path.join(old_dir, files[0])
|
||||
|
||||
# Skip if file still not found
|
||||
if not os.path.exists(current_path):
|
||||
self.stdout.write(f"Skipping {current_name} - file not found")
|
||||
continue
|
||||
|
||||
# Get content type and object
|
||||
content_type_model = photo.content_type.model
|
||||
obj = photo.content_object
|
||||
identifier = getattr(obj, "slug", obj.id)
|
||||
|
||||
# Get photo number
|
||||
photo_number = Photo.objects.filter(
|
||||
content_type=photo.content_type,
|
||||
object_id=photo.object_id,
|
||||
created_at__lte=photo.created_at,
|
||||
).count()
|
||||
|
||||
# Create new filename
|
||||
_, ext = os.path.splitext(current_path)
|
||||
if not ext:
|
||||
ext = ".jpg"
|
||||
ext = ext.lower()
|
||||
new_filename = f"{identifier}_{photo_number}{ext}"
|
||||
|
||||
# Create new path
|
||||
new_relative_path = f"{content_type_model}/{identifier}/{new_filename}"
|
||||
new_full_path = os.path.join(settings.MEDIA_ROOT, new_relative_path)
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
os.makedirs(os.path.dirname(new_full_path), exist_ok=True)
|
||||
|
||||
# Move the file
|
||||
if current_path != new_full_path:
|
||||
shutil.copy2(
|
||||
current_path, new_full_path
|
||||
) # Use copy2 to preserve metadata
|
||||
processed_files.add(current_path)
|
||||
else:
|
||||
processed_files.add(current_path)
|
||||
|
||||
# Update database
|
||||
photo.image.name = new_relative_path
|
||||
photo.save()
|
||||
|
||||
self.stdout.write(f"Moved {current_name} to {new_relative_path}")
|
||||
|
||||
except Exception as e:
|
||||
self.stdout.write(f"Error moving photo {photo.id}: {str(e)}")
|
||||
continue
|
||||
|
||||
# Clean up old files
|
||||
self.stdout.write("Cleaning up old files...")
|
||||
for content_type in ["park", "ride"]:
|
||||
base_dir = os.path.join(settings.MEDIA_ROOT, content_type)
|
||||
if os.path.exists(base_dir):
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
if file_path not in processed_files:
|
||||
try:
|
||||
os.remove(file_path)
|
||||
self.stdout.write(f"Removed old file: {file_path}")
|
||||
except Exception as e:
|
||||
self.stdout.write(
|
||||
f"Error removing {file_path}: {str(e)}"
|
||||
)
|
||||
|
||||
self.stdout.write("Finished moving photo files and cleaning up")
|
||||
26
media/migrations/0002_photo_uploaded_by.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 5.1.2 on 2024-11-01 00:24
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("media", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="photo",
|
||||
name="uploaded_by",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="uploaded_photos",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
||||
69
media/migrations/0003_update_photo_field_and_normalize.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from django.db import migrations, models
|
||||
import os
|
||||
from django.db import transaction
|
||||
|
||||
def normalize_filenames(apps, schema_editor):
|
||||
Photo = apps.get_model('media', 'Photo')
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
# Get all photos
|
||||
photos = Photo.objects.using(db_alias).all()
|
||||
|
||||
for photo in photos:
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# Get content type model name
|
||||
content_type_model = photo.content_type.model
|
||||
|
||||
# Get current filename and extension
|
||||
old_path = photo.image.name
|
||||
_, ext = os.path.splitext(old_path)
|
||||
if not ext:
|
||||
ext = '.jpg' # Default to .jpg if no extension
|
||||
ext = ext.lower()
|
||||
|
||||
# Get the photo number (based on creation order)
|
||||
photo_number = Photo.objects.using(db_alias).filter(
|
||||
content_type=photo.content_type,
|
||||
object_id=photo.object_id,
|
||||
created_at__lte=photo.created_at
|
||||
).count()
|
||||
|
||||
# Extract identifier from current path
|
||||
parts = old_path.split('/')
|
||||
if len(parts) >= 2:
|
||||
identifier = parts[1] # e.g., "alton-towers" from "park/alton-towers/..."
|
||||
|
||||
# Create new normalized filename
|
||||
new_filename = f"{identifier}_{photo_number}{ext}"
|
||||
new_path = f"{content_type_model}/{identifier}/{new_filename}"
|
||||
|
||||
# Update the image field if path would change
|
||||
if old_path != new_path:
|
||||
photo.image.name = new_path
|
||||
photo.save(using=db_alias)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error normalizing photo {photo.id}: {str(e)}")
|
||||
# Continue with next photo even if this one fails
|
||||
continue
|
||||
|
||||
def reverse_normalize(apps, schema_editor):
|
||||
# No reverse operation needed since we're just renaming files
|
||||
pass
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('media', '0002_photo_uploaded_by'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# First increase the field length
|
||||
migrations.AlterField(
|
||||
model_name='photo',
|
||||
name='image',
|
||||
field=models.ImageField(max_length=255, upload_to='photos'),
|
||||
),
|
||||
# Then normalize the filenames
|
||||
migrations.RunPython(normalize_filenames, reverse_normalize),
|
||||
]
|
||||
10
media/migrations/0004_update_photo_paths.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.db import migrations
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('media', '0003_update_photo_field_and_normalize'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# No schema changes needed, just need to trigger the new upload_to path
|
||||
]
|
||||
@@ -2,10 +2,13 @@ from django.db import models
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.text import slugify
|
||||
from django.conf import settings
|
||||
import os
|
||||
from .storage import MediaStorage
|
||||
from rides.models import Ride
|
||||
|
||||
def photo_upload_path(instance, filename):
|
||||
"""Generate upload path for photos"""
|
||||
"""Generate upload path for photos using normalized filenames"""
|
||||
# Get the content type and object
|
||||
content_type = instance.content_type.model
|
||||
obj = instance.content_object
|
||||
@@ -13,19 +16,45 @@ def photo_upload_path(instance, filename):
|
||||
# Get object identifier (slug or id)
|
||||
identifier = getattr(obj, 'slug', obj.id)
|
||||
|
||||
# Create path: content_type/identifier/filename
|
||||
base, ext = os.path.splitext(filename)
|
||||
new_filename = f"{slugify(base)}{ext}"
|
||||
return f"{content_type}/{identifier}/{new_filename}"
|
||||
# Get the next available number for this object
|
||||
existing_photos = Photo.objects.filter(
|
||||
content_type=instance.content_type,
|
||||
object_id=instance.object_id
|
||||
).count()
|
||||
next_number = existing_photos + 1
|
||||
|
||||
# Create normalized filename
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
if not ext:
|
||||
ext = '.jpg' # Default to .jpg if no extension
|
||||
new_filename = f"{identifier}_{next_number}{ext}"
|
||||
|
||||
# If it's a ride photo, store it under the park's directory
|
||||
if content_type == 'ride':
|
||||
ride = Ride.objects.get(id=obj.id)
|
||||
return f"park/{ride.park.slug}/{identifier}/{new_filename}"
|
||||
|
||||
# For park photos, store directly in park directory
|
||||
return f"park/{identifier}/{new_filename}"
|
||||
|
||||
class Photo(models.Model):
|
||||
"""Generic photo model that can be attached to any model"""
|
||||
image = models.ImageField(upload_to=photo_upload_path)
|
||||
image = models.ImageField(
|
||||
upload_to=photo_upload_path,
|
||||
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)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=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)
|
||||
@@ -42,6 +71,10 @@ class Photo(models.Model):
|
||||
return f"{self.content_type} - {self.content_object} - {self.caption or 'No caption'}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Set default caption if not provided
|
||||
if not self.caption and self.uploaded_by:
|
||||
self.caption = f"Uploaded by {self.uploaded_by.username} on {self.created_at.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(
|
||||
@@ -49,4 +82,5 @@ class Photo(models.Model):
|
||||
object_id=self.object_id,
|
||||
is_primary=True
|
||||
).exclude(id=self.id).update(is_primary=False)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
BIN
media/park/alton-towers/alton-towers_1.jpg
Normal file
|
After Width: | Height: | Size: 12 MiB |
BIN
media/park/alton-towers/nemesis/nemesis_1.jpg
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
media/park/alton-towers/oblivion/oblivion_1.jpg
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
media/park/cedar-point/cedar-point_1.jpg
Normal file
|
After Width: | Height: | Size: 3.9 MiB |
BIN
media/park/cedar-point/maverick/maverick_1.jpg
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
media/park/cedar-point/millennium-force/millennium-force_1.jpg
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
media/park/cedar-point/steel-vengeance/steel-vengeance_1.jpg
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
BIN
media/park/europa-park/blue-fire/blue-fire_1.jpg
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
media/park/europa-park/europa-park_1.jpg
Normal file
|
After Width: | Height: | Size: 3.9 MiB |
BIN
media/park/europa-park/silver-star/silver-star_1.jpg
Normal file
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 942 KiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 942 KiB |
|
After Width: | Height: | Size: 12 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 770 KiB |
38
media/storage.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
from django.conf import settings
|
||||
import os
|
||||
|
||||
class MediaStorage(FileSystemStorage):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['location'] = settings.MEDIA_ROOT
|
||||
kwargs['base_url'] = settings.MEDIA_URL
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_available_name(self, name, max_length=None):
|
||||
"""
|
||||
Returns a filename that's free on the target storage system.
|
||||
"""
|
||||
# Get the directory and filename
|
||||
directory = os.path.dirname(name)
|
||||
filename = os.path.basename(name)
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
full_dir = os.path.join(self.location, directory)
|
||||
os.makedirs(full_dir, exist_ok=True)
|
||||
|
||||
# Return the name as is since our upload path already handles uniqueness
|
||||
return name
|
||||
|
||||
def _save(self, name, content):
|
||||
"""
|
||||
Save with proper permissions
|
||||
"""
|
||||
# Save the file
|
||||
name = super()._save(name, content)
|
||||
|
||||
# Set proper permissions
|
||||
full_path = self.path(name)
|
||||
os.chmod(full_path, 0o644)
|
||||
os.chmod(os.path.dirname(full_path), 0o755)
|
||||
|
||||
return name
|
||||
18
media/templatetags/json_filters.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from django import template
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
import json
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.filter
|
||||
def serialize_photos(photos):
|
||||
"""Serialize photos queryset to JSON for AlpineJS"""
|
||||
photo_data = []
|
||||
for photo in photos:
|
||||
photo_data.append({
|
||||
'id': photo.id,
|
||||
'url': photo.image.url,
|
||||
'caption': photo.caption or '',
|
||||
'is_primary': photo.is_primary
|
||||
})
|
||||
return json.dumps(photo_data, cls=DjangoJSONEncoder)
|
||||
0
media/test.txt
Normal file
11
media/urls.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'photos'
|
||||
|
||||
urlpatterns = [
|
||||
path('upload/', views.upload_photo, name='upload'),
|
||||
path('upload/<int:photo_id>/', views.delete_photo, name='delete'), # Updated to match frontend
|
||||
path('upload/<int:photo_id>/primary/', views.set_primary_photo, name='set_primary'),
|
||||
path('upload/<int:photo_id>/caption/', views.update_caption, name='update_caption'),
|
||||
]
|
||||
164
media/views.py
Normal file
@@ -0,0 +1,164 @@
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import get_object_or_404
|
||||
import json
|
||||
import logging
|
||||
|
||||
from .models import Photo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def upload_photo(request):
|
||||
"""Handle photo upload for any model"""
|
||||
try:
|
||||
# Get app label, model, and object ID
|
||||
app_label = request.POST.get('app_label')
|
||||
model = request.POST.get('model')
|
||||
object_id = request.POST.get('object_id')
|
||||
|
||||
# Log received data
|
||||
logger.debug(f"Received upload request - app_label: {app_label}, model: {model}, object_id: {object_id}")
|
||||
logger.debug(f"Files in request: {request.FILES}")
|
||||
|
||||
# Validate required fields
|
||||
missing_fields = []
|
||||
if not app_label:
|
||||
missing_fields.append('app_label')
|
||||
if not model:
|
||||
missing_fields.append('model')
|
||||
if not object_id:
|
||||
missing_fields.append('object_id')
|
||||
if 'image' not in request.FILES:
|
||||
missing_fields.append('image')
|
||||
|
||||
if missing_fields:
|
||||
return JsonResponse({
|
||||
'error': f'Missing required fields: {", ".join(missing_fields)}'
|
||||
}, status=400)
|
||||
|
||||
# Get content type
|
||||
try:
|
||||
content_type = ContentType.objects.get(
|
||||
app_label=app_label.lower(),
|
||||
model=model.lower()
|
||||
)
|
||||
except ContentType.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'error': f'Invalid content type: {app_label}.{model}'
|
||||
}, status=400)
|
||||
|
||||
# Get the object instance
|
||||
try:
|
||||
obj = content_type.get_object_for_this_type(id=object_id)
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'error': f'Object not found: {app_label}.{model} with id {object_id}. Error: {str(e)}'
|
||||
}, status=404)
|
||||
|
||||
# Check if user has permission to add photos
|
||||
if not request.user.has_perm('media.add_photo'):
|
||||
logger.warning(f"User {request.user} attempted to upload photo without permission")
|
||||
return JsonResponse({
|
||||
'error': 'You do not have permission to upload photos'
|
||||
}, status=403)
|
||||
|
||||
# Create the photo
|
||||
photo = Photo.objects.create(
|
||||
image=request.FILES['image'],
|
||||
content_type=content_type,
|
||||
object_id=obj.id,
|
||||
uploaded_by=request.user, # Add the user who uploaded the photo
|
||||
# Set as primary if it's the first photo
|
||||
is_primary=not Photo.objects.filter(
|
||||
content_type=content_type,
|
||||
object_id=obj.id
|
||||
).exists()
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'id': photo.id,
|
||||
'url': photo.image.url,
|
||||
'caption': photo.caption,
|
||||
'is_primary': photo.is_primary
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in upload_photo: {str(e)}", exc_info=True)
|
||||
return JsonResponse({
|
||||
'error': f'An error occurred while uploading the photo: {str(e)}'
|
||||
}, status=400)
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def set_primary_photo(request, photo_id):
|
||||
"""Set a photo as primary"""
|
||||
try:
|
||||
photo = get_object_or_404(Photo, id=photo_id)
|
||||
|
||||
# Check if user has permission to edit photos
|
||||
if not request.user.has_perm('media.change_photo'):
|
||||
return JsonResponse({
|
||||
'error': 'You do not have permission to edit photos'
|
||||
}, status=403)
|
||||
|
||||
# Set this photo as primary
|
||||
photo.is_primary = True
|
||||
photo.save() # This will automatically unset other primary photos
|
||||
|
||||
return JsonResponse({'status': 'success'})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in set_primary_photo: {str(e)}", exc_info=True)
|
||||
return JsonResponse({'error': str(e)}, status=400)
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def update_caption(request, photo_id):
|
||||
"""Update a photo's caption"""
|
||||
try:
|
||||
photo = get_object_or_404(Photo, id=photo_id)
|
||||
|
||||
# Check if user has permission to edit photos
|
||||
if not request.user.has_perm('media.change_photo'):
|
||||
return JsonResponse({
|
||||
'error': 'You do not have permission to edit photos'
|
||||
}, status=403)
|
||||
|
||||
# Update caption
|
||||
data = json.loads(request.body)
|
||||
photo.caption = data.get('caption', '')
|
||||
photo.save()
|
||||
|
||||
return JsonResponse({
|
||||
'id': photo.id,
|
||||
'caption': photo.caption
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in update_caption: {str(e)}", exc_info=True)
|
||||
return JsonResponse({'error': str(e)}, status=400)
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["DELETE"])
|
||||
def delete_photo(request, photo_id):
|
||||
"""Delete a photo"""
|
||||
try:
|
||||
photo = get_object_or_404(Photo, id=photo_id)
|
||||
|
||||
# Check if user has permission to delete photos
|
||||
if not request.user.has_perm('media.delete_photo'):
|
||||
return JsonResponse({
|
||||
'error': 'You do not have permission to delete photos'
|
||||
}, status=403)
|
||||
|
||||
photo.delete()
|
||||
return JsonResponse({'status': 'success'})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in delete_photo: {str(e)}", exc_info=True)
|
||||
return JsonResponse({'error': str(e)}, status=400)
|
||||