1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-24 07:19:36 +02:00

refactor(models, views, serializers): rename LocationImage and Attachment to ContentImage and ContentAttachment, update related references

This commit is contained in:
Sean Morley 2025-07-10 12:12:03 -04:00
parent 1b841f45a0
commit 7f80dad94b
13 changed files with 371 additions and 86 deletions

View file

@ -1,7 +1,7 @@
import os
from django.contrib import admin
from django.utils.html import mark_safe
from .models import Location, Checklist, ChecklistItem, Collection, Transportation, Note, LocationImage, Visit, Category, Attachment, Lodging
from .models import Location, Checklist, ChecklistItem, Collection, Transportation, Note, ContentImage, Visit, Category, ContentAttachment, Lodging
from worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity
from allauth.account.decorators import secure_admin_login
@ -96,7 +96,7 @@ class CustomUserAdmin(UserAdmin):
else:
return
class LocationImageAdmin(admin.ModelAdmin):
class ContentImageImageAdmin(admin.ModelAdmin):
list_display = ('user', 'image_display')
def image_display(self, obj):
@ -147,11 +147,11 @@ admin.site.register(Transportation)
admin.site.register(Note)
admin.site.register(Checklist)
admin.site.register(ChecklistItem)
admin.site.register(LocationImage, LocationImageAdmin)
admin.site.register(ContentImage, ContentImageImageAdmin)
admin.site.register(Category, CategoryAdmin)
admin.site.register(City, CityAdmin)
admin.site.register(VisitedCity)
admin.site.register(Attachment)
admin.site.register(ContentAttachment)
admin.site.register(Lodging)
admin.site.site_header = 'AdventureLog Admin'

View file

@ -1,7 +1,7 @@
import os
from django.core.management.base import BaseCommand
from django.conf import settings
from adventures.models import LocationImage, Attachment
from adventures.models import ContentImage, ContentAttachment
from users.models import CustomUser
@ -21,13 +21,13 @@ class Command(BaseCommand):
# Get all image and attachment file paths from database
used_files = set()
# Get LocationImage file paths
for img in LocationImage.objects.all():
# Get ContentImage file paths
for img in ContentImage.objects.all():
if img.image and img.image.name:
used_files.add(os.path.join(settings.MEDIA_ROOT, img.image.name))
# Get Attachment file paths
for attachment in Attachment.objects.all():
for attachment in ContentAttachment.objects.all():
if attachment.file and attachment.file.name:
used_files.add(os.path.join(settings.MEDIA_ROOT, attachment.file.name))

View file

@ -0,0 +1,23 @@
# Generated by Django 5.2.1 on 2025-07-10 14:40
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('adventures', '0051_rename_activity_types_location_tags_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RenameModel(
old_name='Attachment',
new_name='ContentAttachment',
),
migrations.RenameModel(
old_name='LocationImage',
new_name='ContentImage',
),
]

View file

@ -0,0 +1,53 @@
# Generated by Django 5.2.1 on 2025-07-10 15:01
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0052_rename_attachment_contentattachment_and_more'),
('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterModelOptions(
name='contentattachment',
options={'verbose_name': 'Content Attachment', 'verbose_name_plural': 'Content Attachments'},
),
migrations.AlterModelOptions(
name='contentimage',
options={'verbose_name': 'Content Image', 'verbose_name_plural': 'Content Images'},
),
migrations.AddField(
model_name='contentattachment',
name='content_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'),
),
migrations.AddField(
model_name='contentattachment',
name='object_id',
field=models.UUIDField(blank=True, null=True),
),
migrations.AddField(
model_name='contentimage',
name='content_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='content_images', to='contenttypes.contenttype'),
),
migrations.AddField(
model_name='contentimage',
name='object_id',
field=models.UUIDField(blank=True, null=True),
),
migrations.AddIndex(
model_name='contentattachment',
index=models.Index(fields=['content_type', 'object_id'], name='adventures__content_e42b72_idx'),
),
migrations.AddIndex(
model_name='contentimage',
index=models.Index(fields=['content_type', 'object_id'], name='adventures__content_aa4984_idx'),
),
]

View file

@ -0,0 +1,73 @@
# Custom migrations to migrate LocationImage and Attachment models to generic ContentImage and ContentAttachment models
from django.db import migrations, models
from django.utils import timezone
def migrate_images_and_attachments_forward(apps, schema_editor):
"""
Migrate existing LocationImage and Attachment records to the new generic ContentImage and ContentAttachment models
"""
# Get the models
ContentImage = apps.get_model('adventures', 'ContentImage')
ContentAttachment = apps.get_model('adventures', 'ContentAttachment')
# Get the ContentType for Location
ContentType = apps.get_model('contenttypes', 'ContentType')
try:
location_ct = ContentType.objects.get(app_label='adventures', model='location')
except ContentType.DoesNotExist:
return
# Update existing ContentImages (which were previously LocationImages)
ContentImage.objects.filter(content_type__isnull=True).update(
content_type=location_ct
)
# Set object_id from location_id for ContentImages
for content_image in ContentImage.objects.filter(object_id__isnull=True):
if hasattr(content_image, 'location_id') and content_image.location_id:
content_image.object_id = content_image.location_id
content_image.save()
# Update existing ContentAttachments (which were previously Attachments)
ContentAttachment.objects.filter(content_type__isnull=True).update(
content_type=location_ct
)
# Set object_id from location_id for ContentAttachments
for content_attachment in ContentAttachment.objects.filter(object_id__isnull=True):
if hasattr(content_attachment, 'location_id') and content_attachment.location_id:
content_attachment.object_id = content_attachment.location_id
content_attachment.save()
def migrate_images_and_attachments_reverse(apps, schema_editor):
"""
Reverse migration to restore location_id fields from object_id
"""
ContentImage = apps.get_model('adventures', 'ContentImage')
ContentAttachment = apps.get_model('adventures', 'ContentAttachment')
# Restore location_id from object_id for ContentImages
for content_image in ContentImage.objects.all():
if content_image.object_id and hasattr(content_image, 'location_id'):
content_image.location_id = content_image.object_id
content_image.save()
# Restore location_id from object_id for ContentAttachments
for content_attachment in ContentAttachment.objects.all():
if content_attachment.object_id and hasattr(content_attachment, 'location_id'):
content_attachment.location_id = content_attachment.object_id
content_attachment.save()
class Migration(migrations.Migration):
dependencies = [
('adventures', '0053_alter_contentattachment_options_and_more'),
]
operations = [
migrations.RunPython(
migrate_images_and_attachments_forward,
migrate_images_and_attachments_reverse,
elidable=True
)
]

View file

@ -0,0 +1,43 @@
# Generated by Django 5.2.1 on 2025-07-10 15:19
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('adventures', '0054_migrate_location_images_generic_relation'),
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.RemoveField(
model_name='contentattachment',
name='location',
),
migrations.RemoveField(
model_name='contentimage',
name='location',
),
migrations.AlterField(
model_name='contentattachment',
name='content_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='content_attachments', to='contenttypes.contenttype'),
),
migrations.AlterField(
model_name='contentattachment',
name='object_id',
field=models.UUIDField(),
),
migrations.AlterField(
model_name='contentimage',
name='content_type',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='content_images', to='contenttypes.contenttype'),
),
migrations.AlterField(
model_name='contentimage',
name='object_id',
field=models.UUIDField(),
),
]

View file

@ -14,6 +14,10 @@ from django.core.exceptions import ValidationError
from django.utils import timezone
from adventures.utils.timezones import TIMEZONES
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericRelation
def background_geocode_and_assign(location_id: str):
print(f"[Location Geocode Thread] Starting geocode for location {location_id}")
try:
@ -126,42 +130,49 @@ class Visit(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Generic relations for images and attachments
images = GenericRelation('ContentImage', related_query_name='visit')
attachments = GenericRelation('ContentAttachment', related_query_name='visit')
def clean(self):
if self.start_date > self.end_date:
raise ValidationError('The start date must be before or equal to the end date.')
def delete(self, *args, **kwargs):
# Delete all associated images and attachments
for image in self.images.all():
image.delete()
for attachment in self.attachments.all():
attachment.delete()
super().delete(*args, **kwargs)
def __str__(self):
return f"{self.location.name} - {self.start_date} to {self.end_date}"
class Location(models.Model):
#id = models.AutoField(primary_key=True)
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user)
user = models.ForeignKey(User, on_delete=models.CASCADE, default=default_user)
category = models.ForeignKey('Category', on_delete=models.SET_NULL, blank=True, null=True)
name = models.CharField(max_length=200)
location = models.CharField(max_length=200, blank=True, null=True)
tags = ArrayField(models.CharField(
max_length=100), blank=True, null=True)
tags = ArrayField(models.CharField(max_length=100), blank=True, null=True)
description = models.TextField(blank=True, null=True)
rating = models.FloatField(blank=True, null=True)
link = models.URLField(blank=True, null=True, max_length=2083)
is_public = models.BooleanField(default=False)
longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
city = models.ForeignKey(City, on_delete=models.SET_NULL, blank=True, null=True)
region = models.ForeignKey(Region, on_delete=models.SET_NULL, blank=True, null=True)
country = models.ForeignKey(Country, on_delete=models.SET_NULL, blank=True, null=True)
# Changed from ForeignKey to ManyToManyField
collections = models.ManyToManyField('Collection', blank=True, related_name='locations')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Generic relations for images and attachments
images = GenericRelation('ContentImage', related_query_name='location')
attachments = GenericRelation('ContentAttachment', related_query_name='location')
objects = LocationManager()
def is_visited_status(self):
@ -236,10 +247,9 @@ class Location(models.Model):
return result
def delete(self, *args, **kwargs):
# Delete all associated AdventureImages first to trigger their filesystem cleanup
# Delete all associated images and attachments (handled by GenericRelation)
for image in self.images.all():
image.delete()
# Delete all associated Attachment files first to trigger their filesystem cleanup
for attachment in self.attachments.all():
attachment.delete()
super().delete(*args, **kwargs)
@ -275,10 +285,8 @@ class Collection(models.Model):
return self.name
class Transportation(models.Model):
#id = models.AutoField(primary_key=True)
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user)
user = models.ForeignKey(User, on_delete=models.CASCADE, default=default_user)
type = models.CharField(max_length=100, choices=TRANSPORTATION_TYPES)
name = models.CharField(max_length=200)
description = models.TextField(blank=True, null=True)
@ -300,8 +308,11 @@ class Transportation(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Generic relations for images and attachments
images = GenericRelation('ContentImage', related_query_name='transportation')
attachments = GenericRelation('ContentAttachment', related_query_name='transportation')
def clean(self):
print(self.date)
if self.date and self.end_date and self.date > self.end_date:
raise ValidationError('The start date must be before the end date. Start date: ' + str(self.date) + ' End date: ' + str(self.end_date))
@ -311,14 +322,20 @@ class Transportation(models.Model):
if self.user != self.collection.user:
raise ValidationError('Transportations must be associated with collections owned by the same user. Collection owner: ' + self.collection.user.username + ' Transportation owner: ' + self.user.username)
def delete(self, *args, **kwargs):
# Delete all associated images and attachments
for image in self.images.all():
image.delete()
for attachment in self.attachments.all():
attachment.delete()
super().delete(*args, **kwargs)
def __str__(self):
return self.name
class Note(models.Model):
#id = models.AutoField(primary_key=True)
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user)
user = models.ForeignKey(User, on_delete=models.CASCADE, default=default_user)
name = models.CharField(max_length=200)
content = models.TextField(blank=True, null=True)
links = ArrayField(models.URLField(), blank=True, null=True)
@ -328,12 +345,24 @@ class Note(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Generic relations for images and attachments
images = GenericRelation('ContentImage', related_query_name='note')
attachments = GenericRelation('ContentAttachment', related_query_name='note')
def clean(self):
if self.collection:
if self.collection.is_public and not self.is_public:
raise ValidationError('Notes associated with a public collection must be public. Collection: ' + self.collection.name + ' Transportation: ' + self.name)
raise ValidationError('Notes associated with a public collection must be public. Collection: ' + self.collection.name + ' Note: ' + self.name)
if self.user != self.collection.user:
raise ValidationError('Notes must be associated with collections owned by the same user. Collection owner: ' + self.collection.user.username + ' Transportation owner: ' + self.user.username)
raise ValidationError('Notes must be associated with collections owned by the same user. Collection owner: ' + self.collection.user.username + ' Note owner: ' + self.user.username)
def delete(self, *args, **kwargs):
# Delete all associated images and attachments
for image in self.images.all():
image.delete()
for attachment in self.attachments.all():
attachment.delete()
super().delete(*args, **kwargs)
def __str__(self):
return self.name
@ -391,7 +420,8 @@ class PathAndRename:
filename = f"{uuid.uuid4()}.{ext}"
return os.path.join(self.path, filename)
class LocationImage(models.Model):
class ContentImage(models.Model):
"""Generic image model that can be attached to any content type"""
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, default=default_user)
image = ResizedImageField(
@ -402,11 +432,21 @@ class LocationImage(models.Model):
null=True,
)
immich_id = models.CharField(max_length=200, null=True, blank=True)
location = models.ForeignKey(Location, related_name='images', on_delete=models.CASCADE)
is_primary = models.BooleanField(default=False)
# Generic foreign key fields
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, related_name='content_images')
object_id = models.UUIDField()
content_object = GenericForeignKey('content_type', 'object_id')
class Meta:
verbose_name = "Content Image"
verbose_name_plural = "Content Images"
indexes = [
models.Index(fields=["content_type", "object_id"]),
]
def clean(self):
# One of image or immich_id must be set, but not both
has_image = bool(self.image and str(self.image).strip())
has_immich_id = bool(self.immich_id and str(self.immich_id).strip())
@ -415,7 +455,7 @@ class LocationImage(models.Model):
raise ValidationError("Cannot have both image file and Immich ID. Please provide only one.")
if not has_image and not has_immich_id:
raise ValidationError("Must provide either an image file or an Immich ID.")
def save(self, *args, **kwargs):
# Clean empty strings to None for proper database storage
if not self.image:
@ -423,25 +463,37 @@ class LocationImage(models.Model):
if not self.immich_id or not str(self.immich_id).strip():
self.immich_id = None
self.full_clean() # This calls clean() method
self.full_clean()
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
# Remove file from disk when deleting AdventureImage
# Remove file from disk when deleting image
if self.image and os.path.isfile(self.image.path):
os.remove(self.image.path)
super().delete(*args, **kwargs)
def __str__(self):
return self.image.url if self.image else f"Immich ID: {self.immich_id or 'No image'}"
class Attachment(models.Model):
content_name = getattr(self.content_object, 'name', 'Unknown')
return f"Image for {self.content_type.model}: {content_name}"
class ContentAttachment(models.Model):
"""Generic attachment model that can be attached to any content type"""
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user)
file = models.FileField(upload_to=PathAndRename('attachments/'),validators=[validate_file_extension])
location = models.ForeignKey(Location, related_name='attachments', on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE, default=default_user)
file = models.FileField(upload_to=PathAndRename('attachments/'), validators=[validate_file_extension])
name = models.CharField(max_length=200, null=True, blank=True)
# Generic foreign key fields
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, related_name='content_attachments')
object_id = models.UUIDField()
content_object = GenericForeignKey('content_type', 'object_id')
class Meta:
verbose_name = "Content Attachment"
verbose_name_plural = "Content Attachments"
indexes = [
models.Index(fields=["content_type", "object_id"]),
]
def delete(self, *args, **kwargs):
if self.file and os.path.isfile(self.file.path):
@ -449,7 +501,8 @@ class Attachment(models.Model):
super().delete(*args, **kwargs)
def __str__(self):
return self.file.url
content_name = getattr(self.content_object, 'name', 'Unknown')
return f"Attachment for {self.content_type.model}: {content_name}"
class Category(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
@ -474,8 +527,7 @@ class Category(models.Model):
class Lodging(models.Model):
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
user = models.ForeignKey(
User, on_delete=models.CASCADE, default=default_user)
user = models.ForeignKey(User, on_delete=models.CASCADE, default=default_user)
name = models.CharField(max_length=200)
type = models.CharField(max_length=100, choices=LODGING_TYPES, default='other')
description = models.TextField(blank=True, null=True)
@ -494,6 +546,10 @@ class Lodging(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# Generic relations for images and attachments
images = GenericRelation('ContentImage', related_query_name='lodging')
attachments = GenericRelation('ContentAttachment', related_query_name='lodging')
def clean(self):
if self.check_in and self.check_out and self.check_in > self.check_out:
raise ValidationError('The start date must be before the end date. Start date: ' + str(self.check_in) + ' End date: ' + str(self.check_out))
@ -504,5 +560,13 @@ class Lodging(models.Model):
if self.user != self.collection.user:
raise ValidationError('Lodging must be associated with collections owned by the same user. Collection owner: ' + self.collection.user.username + ' Lodging owner: ' + self.user.username)
def delete(self, *args, **kwargs):
# Delete all associated images and attachments
for image in self.images.all():
image.delete()
for attachment in self.attachments.all():
attachment.delete()
super().delete(*args, **kwargs)
def __str__(self):
return self.name

View file

@ -1,6 +1,6 @@
from django.utils import timezone
import os
from .models import Location, LocationImage, ChecklistItem, Collection, Note, Transportation, Checklist, Visit, Category, Attachment, Lodging
from .models import Location, ContentImage, ChecklistItem, Collection, Note, Transportation, Checklist, Visit, Category, ContentAttachment, Lodging
from rest_framework import serializers
from main.utils import CustomModelSerializer
from users.serializers import CustomUserDetailsSerializer
@ -9,9 +9,9 @@ from geopy.distance import geodesic
from integrations.models import ImmichIntegration
class LocationImageSerializer(CustomModelSerializer):
class ContentImageSerializer(CustomModelSerializer):
class Meta:
model = LocationImage
model = ContentImage
fields = ['id', 'image', 'location', 'is_primary', 'user', 'immich_id']
read_only_fields = ['id', 'user']
@ -41,7 +41,7 @@ class LocationImageSerializer(CustomModelSerializer):
class AttachmentSerializer(CustomModelSerializer):
extension = serializers.SerializerMethodField()
class Meta:
model = Attachment
model = ContentAttachment
fields = ['id', 'file', 'location', 'extension', 'name', 'user']
read_only_fields = ['id', 'user']
@ -122,7 +122,7 @@ class LocationSerializer(CustomModelSerializer):
return representation
def get_images(self, obj):
serializer = LocationImageSerializer(obj.images.all(), many=True, context=self.context)
serializer = ContentImageSerializer(obj.images.all(), many=True, context=self.context)
# Filter out None values from the serialized data
return [image for image in serializer.data if image is not None]

View file

@ -1,4 +1,4 @@
from adventures.models import LocationImage, Attachment
from adventures.models import ContentImage, ContentAttachment
protected_paths = ['images/', 'attachments/']
@ -9,40 +9,69 @@ def checkFilePermission(fileId, user, mediaType):
try:
# Construct the full relative path to match the database field
image_path = f"images/{fileId}"
# Fetch the AdventureImage object
location = LocationImage.objects.get(image=image_path).location
if location.is_public:
# Fetch the ContentImage object
content_image = ContentImage.objects.get(image=image_path)
# Get the content object (could be Location, Transportation, Note, etc.)
content_object = content_image.content_object
# Check if content object is public
if hasattr(content_object, 'is_public') and content_object.is_public:
return True
elif location.user == user:
# Check if user owns the content object
if hasattr(content_object, 'user') and content_object.user == user:
return True
elif location.collections.exists():
# Check if the user is in any collection's shared_with list
for collection in location.collections.all():
# Check collection-based permissions
if hasattr(content_object, 'collections') and content_object.collections.exists():
# For objects with multiple collections (like Location)
for collection in content_object.collections.all():
if collection.shared_with.filter(id=user.id).exists():
return True
return False
elif hasattr(content_object, 'collection') and content_object.collection:
# For objects with single collection (like Transportation, Note, etc.)
if content_object.collection.shared_with.filter(id=user.id).exists():
return True
return False
else:
return False
except LocationImage.DoesNotExist:
except ContentImage.DoesNotExist:
return False
elif mediaType == 'attachments/':
try:
# Construct the full relative path to match the database field
attachment_path = f"attachments/{fileId}"
# Fetch the Attachment object
attachment = Attachment.objects.get(file=attachment_path)
location = attachment.location
if location.is_public:
# Fetch the ContentAttachment object
content_attachment = ContentAttachment.objects.get(file=attachment_path)
# Get the content object (could be Location, Transportation, Note, etc.)
content_object = content_attachment.content_object
# Check if content object is public
if hasattr(content_object, 'is_public') and content_object.is_public:
return True
elif location.user == user:
# Check if user owns the content object
if hasattr(content_object, 'user') and content_object.user == user:
return True
elif location.collections.exists():
# Check if the user is in any collection's shared_with list
for collection in location.collections.all():
# Check collection-based permissions
if hasattr(content_object, 'collections') and content_object.collections.exists():
# For objects with multiple collections (like Location)
for collection in content_object.collections.all():
if collection.shared_with.filter(id=user.id).exists():
return True
return False
elif hasattr(content_object, 'collection') and content_object.collection:
# For objects with single collection (like Transportation, Note, etc.)
if content_object.collection.shared_with.filter(id=user.id).exists():
return True
return False
else:
return False
except Attachment.DoesNotExist:
except ContentAttachment.DoesNotExist:
return False

View file

@ -2,7 +2,7 @@ from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from adventures.models import Location, Attachment
from adventures.models import Location, ContentAttachment
from adventures.serializers import AttachmentSerializer
class AttachmentViewSet(viewsets.ModelViewSet):
@ -10,7 +10,7 @@ class AttachmentViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Attachment.objects.filter(user=self.request.user)
return ContentAttachment.objects.filter(user=self.request.user)
def create(self, request, *args, **kwargs):
if not request.user.is_authenticated:

View file

@ -18,7 +18,7 @@ from django.conf import settings
from adventures.models import (
Location, Collection, Transportation, Note, Checklist, ChecklistItem,
LocationImage, Attachment, Category, Lodging, Visit
ContentImage, ContentAttachment, Category, Lodging, Visit
)
from worldtravel.models import VisitedCity, VisitedRegion, City, Region, Country
@ -347,7 +347,7 @@ class BackupViewSet(viewsets.ViewSet):
user.lodging_set.all().delete()
# Delete location-related data
user.locationimage_set.all().delete()
user.contentimage_set.all().delete()
user.attachment_set.all().delete()
# Visits are deleted via cascade when locations are deleted
user.location_set.all().delete()
@ -487,7 +487,7 @@ class BackupViewSet(viewsets.ViewSet):
for img_data in adv_data.get('images', []):
immich_id = img_data.get('immich_id')
if immich_id:
LocationImage.objects.create(
ContentImage.objects.create(
user=user,
location=location,
immich_id=immich_id,
@ -500,7 +500,7 @@ class BackupViewSet(viewsets.ViewSet):
try:
img_content = zip_file.read(f'images/{filename}')
img_file = ContentFile(img_content, name=filename)
LocationImage.objects.create(
ContentImage.objects.create(
user=user,
location=location,
image=img_file,
@ -517,7 +517,7 @@ class BackupViewSet(viewsets.ViewSet):
try:
att_content = zip_file.read(f'attachments/{filename}')
att_file = ContentFile(att_content, name=filename)
Attachment.objects.create(
ContentAttachment.objects.create(
user=user,
location=location,
file=att_file,

View file

@ -4,14 +4,14 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django.db.models import Q
from django.core.files.base import ContentFile
from adventures.models import Location, LocationImage
from adventures.serializers import LocationImageSerializer
from adventures.models import Location, ContentImage
from adventures.serializers import ContentImageSerializer
from integrations.models import ImmichIntegration
import uuid
import requests
class AdventureImageViewSet(viewsets.ModelViewSet):
serializer_class = LocationImageSerializer
serializer_class = ContentImageSerializer
permission_classes = [IsAuthenticated]
@action(detail=True, methods=['post'])
@ -34,7 +34,7 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
return Response({"error": "Image is already the primary image"}, status=status.HTTP_400_BAD_REQUEST)
# Set the current primary image to false
LocationImage.objects.filter(location=location, is_primary=True).update(is_primary=False)
ContentImage.objects.filter(location=location, is_primary=True).update(is_primary=False)
# Set the new image to true
instance.is_primary = True
@ -190,7 +190,7 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
return Response({"error": "Invalid location ID"}, status=status.HTTP_400_BAD_REQUEST)
# Updated queryset to include images from locations the user owns OR has shared access to
queryset = LocationImage.objects.filter(
queryset = ContentImage.objects.filter(
Q(location__id=location_uuid) & (
Q(location__user=request.user) | # User owns the location
Q(location__collections__shared_with=request.user) # User has shared access via collection
@ -202,7 +202,7 @@ class AdventureImageViewSet(viewsets.ModelViewSet):
def get_queryset(self):
# Updated to include images from locations the user owns OR has shared access to
return LocationImage.objects.filter(
return ContentImage.objects.filter(
Q(location__user=self.request.user) | # User owns the location
Q(location__collections__shared_with=self.request.user) # User has shared access via collection
).distinct()

View file

@ -8,7 +8,7 @@ from rest_framework.permissions import IsAuthenticated
import requests
from rest_framework.pagination import PageNumberPagination
from django.conf import settings
from adventures.models import LocationImage
from adventures.models import ContentImage
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
import logging
@ -257,7 +257,7 @@ class ImmichIntegrationView(viewsets.ViewSet):
2. Private locations in public collections: accessible by anyone
3. Private locations in private collections shared with user: accessible by shared users
4. Private locations: accessible only to the owner
5. No LocationImage: owner can still view via integration
5. No ContentImage: owner can still view via integration
"""
if not imageid or not integration_id:
return Response({
@ -272,7 +272,7 @@ class ImmichIntegrationView(viewsets.ViewSet):
# Try to find the image entry with collections and sharing information
image_entry = (
LocationImage.objects
ContentImage.objects
.filter(immich_id=imageid, user=owner_id)
.select_related('location')
.prefetch_related('location__collections', 'location__collections__shared_with')
@ -313,7 +313,7 @@ class ImmichIntegrationView(viewsets.ViewSet):
'code': 'immich.permission_denied'
}, status=status.HTTP_403_FORBIDDEN)
else:
# No LocationImage exists; allow only the integration owner
# No ContentImage exists; allow only the integration owner
if not request.user.is_authenticated or request.user != owner_id:
return Response({
'message': 'Image is not linked to any location and you are not the owner.',