mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-24 15:29:36 +02:00
refactor(models, views, serializers): rename LocationImage and Attachment to ContentImage and ContentAttachment, update related references
This commit is contained in:
parent
1b841f45a0
commit
7f80dad94b
13 changed files with 371 additions and 86 deletions
|
@ -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'
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
)
|
||||
]
|
|
@ -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(),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
||||
def clean(self):
|
||||
# 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())
|
||||
|
@ -423,33 +463,46 @@ 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'}"
|
||||
content_name = getattr(self.content_object, 'name', 'Unknown')
|
||||
return f"Image for {self.content_type.model}: {content_name}"
|
||||
|
||||
class Attachment(models.Model):
|
||||
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):
|
||||
os.remove(self.file.path)
|
||||
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
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue