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:
parent
1b841f45a0
commit
7f80dad94b
13 changed files with 371 additions and 86 deletions
|
@ -1,7 +1,7 @@
|
||||||
import os
|
import os
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.html import mark_safe
|
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 worldtravel.models import Country, Region, VisitedRegion, City, VisitedCity
|
||||||
from allauth.account.decorators import secure_admin_login
|
from allauth.account.decorators import secure_admin_login
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ class CustomUserAdmin(UserAdmin):
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
class LocationImageAdmin(admin.ModelAdmin):
|
class ContentImageImageAdmin(admin.ModelAdmin):
|
||||||
list_display = ('user', 'image_display')
|
list_display = ('user', 'image_display')
|
||||||
|
|
||||||
def image_display(self, obj):
|
def image_display(self, obj):
|
||||||
|
@ -147,11 +147,11 @@ admin.site.register(Transportation)
|
||||||
admin.site.register(Note)
|
admin.site.register(Note)
|
||||||
admin.site.register(Checklist)
|
admin.site.register(Checklist)
|
||||||
admin.site.register(ChecklistItem)
|
admin.site.register(ChecklistItem)
|
||||||
admin.site.register(LocationImage, LocationImageAdmin)
|
admin.site.register(ContentImage, ContentImageImageAdmin)
|
||||||
admin.site.register(Category, CategoryAdmin)
|
admin.site.register(Category, CategoryAdmin)
|
||||||
admin.site.register(City, CityAdmin)
|
admin.site.register(City, CityAdmin)
|
||||||
admin.site.register(VisitedCity)
|
admin.site.register(VisitedCity)
|
||||||
admin.site.register(Attachment)
|
admin.site.register(ContentAttachment)
|
||||||
admin.site.register(Lodging)
|
admin.site.register(Lodging)
|
||||||
|
|
||||||
admin.site.site_header = 'AdventureLog Admin'
|
admin.site.site_header = 'AdventureLog Admin'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import os
|
import os
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from adventures.models import LocationImage, Attachment
|
from adventures.models import ContentImage, ContentAttachment
|
||||||
from users.models import CustomUser
|
from users.models import CustomUser
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,13 +21,13 @@ class Command(BaseCommand):
|
||||||
# Get all image and attachment file paths from database
|
# Get all image and attachment file paths from database
|
||||||
used_files = set()
|
used_files = set()
|
||||||
|
|
||||||
# Get LocationImage file paths
|
# Get ContentImage file paths
|
||||||
for img in LocationImage.objects.all():
|
for img in ContentImage.objects.all():
|
||||||
if img.image and img.image.name:
|
if img.image and img.image.name:
|
||||||
used_files.add(os.path.join(settings.MEDIA_ROOT, img.image.name))
|
used_files.add(os.path.join(settings.MEDIA_ROOT, img.image.name))
|
||||||
|
|
||||||
# Get Attachment file paths
|
# Get Attachment file paths
|
||||||
for attachment in Attachment.objects.all():
|
for attachment in ContentAttachment.objects.all():
|
||||||
if attachment.file and attachment.file.name:
|
if attachment.file and attachment.file.name:
|
||||||
used_files.add(os.path.join(settings.MEDIA_ROOT, 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 django.utils import timezone
|
||||||
from adventures.utils.timezones import TIMEZONES
|
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):
|
def background_geocode_and_assign(location_id: str):
|
||||||
print(f"[Location Geocode Thread] Starting geocode for location {location_id}")
|
print(f"[Location Geocode Thread] Starting geocode for location {location_id}")
|
||||||
try:
|
try:
|
||||||
|
@ -126,42 +130,49 @@ class Visit(models.Model):
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=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):
|
def clean(self):
|
||||||
if self.start_date > self.end_date:
|
if self.start_date > self.end_date:
|
||||||
raise ValidationError('The start date must be before or equal to the 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):
|
def __str__(self):
|
||||||
return f"{self.location.name} - {self.start_date} to {self.end_date}"
|
return f"{self.location.name} - {self.start_date} to {self.end_date}"
|
||||||
|
|
||||||
class Location(models.Model):
|
class Location(models.Model):
|
||||||
#id = models.AutoField(primary_key=True)
|
|
||||||
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(User, on_delete=models.CASCADE, default=default_user)
|
||||||
User, on_delete=models.CASCADE, default=default_user)
|
|
||||||
|
|
||||||
category = models.ForeignKey('Category', on_delete=models.SET_NULL, blank=True, null=True)
|
category = models.ForeignKey('Category', on_delete=models.SET_NULL, blank=True, null=True)
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
location = models.CharField(max_length=200, blank=True, null=True)
|
location = models.CharField(max_length=200, blank=True, null=True)
|
||||||
tags = ArrayField(models.CharField(
|
tags = ArrayField(models.CharField(max_length=100), blank=True, null=True)
|
||||||
max_length=100), blank=True, null=True)
|
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
rating = models.FloatField(blank=True, null=True)
|
rating = models.FloatField(blank=True, null=True)
|
||||||
link = models.URLField(blank=True, null=True, max_length=2083)
|
link = models.URLField(blank=True, null=True, max_length=2083)
|
||||||
is_public = models.BooleanField(default=False)
|
is_public = models.BooleanField(default=False)
|
||||||
|
|
||||||
longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
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)
|
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)
|
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)
|
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)
|
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')
|
collections = models.ManyToManyField('Collection', blank=True, related_name='locations')
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=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()
|
objects = LocationManager()
|
||||||
|
|
||||||
def is_visited_status(self):
|
def is_visited_status(self):
|
||||||
|
@ -236,10 +247,9 @@ class Location(models.Model):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
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():
|
for image in self.images.all():
|
||||||
image.delete()
|
image.delete()
|
||||||
# Delete all associated Attachment files first to trigger their filesystem cleanup
|
|
||||||
for attachment in self.attachments.all():
|
for attachment in self.attachments.all():
|
||||||
attachment.delete()
|
attachment.delete()
|
||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
|
@ -275,10 +285,8 @@ class Collection(models.Model):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
class Transportation(models.Model):
|
class Transportation(models.Model):
|
||||||
#id = models.AutoField(primary_key=True)
|
|
||||||
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(User, on_delete=models.CASCADE, default=default_user)
|
||||||
User, on_delete=models.CASCADE, default=default_user)
|
|
||||||
type = models.CharField(max_length=100, choices=TRANSPORTATION_TYPES)
|
type = models.CharField(max_length=100, choices=TRANSPORTATION_TYPES)
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
|
@ -300,8 +308,11 @@ class Transportation(models.Model):
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=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):
|
def clean(self):
|
||||||
print(self.date)
|
|
||||||
if self.date and self.end_date and self.date > self.end_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))
|
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:
|
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)
|
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):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
class Note(models.Model):
|
class Note(models.Model):
|
||||||
#id = models.AutoField(primary_key=True)
|
|
||||||
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(User, on_delete=models.CASCADE, default=default_user)
|
||||||
User, on_delete=models.CASCADE, default=default_user)
|
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
content = models.TextField(blank=True, null=True)
|
content = models.TextField(blank=True, null=True)
|
||||||
links = ArrayField(models.URLField(), 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)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=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):
|
def clean(self):
|
||||||
if self.collection:
|
if self.collection:
|
||||||
if self.collection.is_public and not self.is_public:
|
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:
|
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):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
@ -391,7 +420,8 @@ class PathAndRename:
|
||||||
filename = f"{uuid.uuid4()}.{ext}"
|
filename = f"{uuid.uuid4()}.{ext}"
|
||||||
return os.path.join(self.path, filename)
|
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)
|
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)
|
||||||
image = ResizedImageField(
|
image = ResizedImageField(
|
||||||
|
@ -402,11 +432,21 @@ class LocationImage(models.Model):
|
||||||
null=True,
|
null=True,
|
||||||
)
|
)
|
||||||
immich_id = models.CharField(max_length=200, null=True, blank=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)
|
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):
|
def clean(self):
|
||||||
|
|
||||||
# One of image or immich_id must be set, but not both
|
# One of image or immich_id must be set, but not both
|
||||||
has_image = bool(self.image and str(self.image).strip())
|
has_image = bool(self.image and str(self.image).strip())
|
||||||
has_immich_id = bool(self.immich_id and str(self.immich_id).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.")
|
raise ValidationError("Cannot have both image file and Immich ID. Please provide only one.")
|
||||||
if not has_image and not has_immich_id:
|
if not has_image and not has_immich_id:
|
||||||
raise ValidationError("Must provide either an image file or an Immich ID.")
|
raise ValidationError("Must provide either an image file or an Immich ID.")
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
# Clean empty strings to None for proper database storage
|
# Clean empty strings to None for proper database storage
|
||||||
if not self.image:
|
if not self.image:
|
||||||
|
@ -423,25 +463,37 @@ class LocationImage(models.Model):
|
||||||
if not self.immich_id or not str(self.immich_id).strip():
|
if not self.immich_id or not str(self.immich_id).strip():
|
||||||
self.immich_id = None
|
self.immich_id = None
|
||||||
|
|
||||||
self.full_clean() # This calls clean() method
|
self.full_clean()
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def delete(self, *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):
|
if self.image and os.path.isfile(self.image.path):
|
||||||
os.remove(self.image.path)
|
os.remove(self.image.path)
|
||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
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)
|
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(User, on_delete=models.CASCADE, default=default_user)
|
||||||
User, on_delete=models.CASCADE, default=default_user)
|
file = models.FileField(upload_to=PathAndRename('attachments/'), validators=[validate_file_extension])
|
||||||
file = models.FileField(upload_to=PathAndRename('attachments/'),validators=[validate_file_extension])
|
|
||||||
location = models.ForeignKey(Location, related_name='attachments', on_delete=models.CASCADE)
|
|
||||||
name = models.CharField(max_length=200, null=True, blank=True)
|
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):
|
def delete(self, *args, **kwargs):
|
||||||
if self.file and os.path.isfile(self.file.path):
|
if self.file and os.path.isfile(self.file.path):
|
||||||
|
@ -449,7 +501,8 @@ class Attachment(models.Model):
|
||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
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):
|
class Category(models.Model):
|
||||||
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
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):
|
class Lodging(models.Model):
|
||||||
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(User, on_delete=models.CASCADE, default=default_user)
|
||||||
User, on_delete=models.CASCADE, default=default_user)
|
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
type = models.CharField(max_length=100, choices=LODGING_TYPES, default='other')
|
type = models.CharField(max_length=100, choices=LODGING_TYPES, default='other')
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
|
@ -494,6 +546,10 @@ class Lodging(models.Model):
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=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):
|
def clean(self):
|
||||||
if self.check_in and self.check_out and self.check_in > self.check_out:
|
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))
|
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:
|
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)
|
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):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
|
@ -1,6 +1,6 @@
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
import os
|
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 rest_framework import serializers
|
||||||
from main.utils import CustomModelSerializer
|
from main.utils import CustomModelSerializer
|
||||||
from users.serializers import CustomUserDetailsSerializer
|
from users.serializers import CustomUserDetailsSerializer
|
||||||
|
@ -9,9 +9,9 @@ from geopy.distance import geodesic
|
||||||
from integrations.models import ImmichIntegration
|
from integrations.models import ImmichIntegration
|
||||||
|
|
||||||
|
|
||||||
class LocationImageSerializer(CustomModelSerializer):
|
class ContentImageSerializer(CustomModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = LocationImage
|
model = ContentImage
|
||||||
fields = ['id', 'image', 'location', 'is_primary', 'user', 'immich_id']
|
fields = ['id', 'image', 'location', 'is_primary', 'user', 'immich_id']
|
||||||
read_only_fields = ['id', 'user']
|
read_only_fields = ['id', 'user']
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ class LocationImageSerializer(CustomModelSerializer):
|
||||||
class AttachmentSerializer(CustomModelSerializer):
|
class AttachmentSerializer(CustomModelSerializer):
|
||||||
extension = serializers.SerializerMethodField()
|
extension = serializers.SerializerMethodField()
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Attachment
|
model = ContentAttachment
|
||||||
fields = ['id', 'file', 'location', 'extension', 'name', 'user']
|
fields = ['id', 'file', 'location', 'extension', 'name', 'user']
|
||||||
read_only_fields = ['id', 'user']
|
read_only_fields = ['id', 'user']
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@ class LocationSerializer(CustomModelSerializer):
|
||||||
return representation
|
return representation
|
||||||
|
|
||||||
def get_images(self, obj):
|
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
|
# Filter out None values from the serialized data
|
||||||
return [image for image in serializer.data if image is not None]
|
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/']
|
protected_paths = ['images/', 'attachments/']
|
||||||
|
|
||||||
|
@ -9,40 +9,69 @@ def checkFilePermission(fileId, user, mediaType):
|
||||||
try:
|
try:
|
||||||
# Construct the full relative path to match the database field
|
# Construct the full relative path to match the database field
|
||||||
image_path = f"images/{fileId}"
|
image_path = f"images/{fileId}"
|
||||||
# Fetch the AdventureImage object
|
# Fetch the ContentImage object
|
||||||
location = LocationImage.objects.get(image=image_path).location
|
content_image = ContentImage.objects.get(image=image_path)
|
||||||
if location.is_public:
|
|
||||||
|
# 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
|
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
|
return True
|
||||||
elif location.collections.exists():
|
|
||||||
# Check if the user is in any collection's shared_with list
|
# Check collection-based permissions
|
||||||
for collection in location.collections.all():
|
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():
|
if collection.shared_with.filter(id=user.id).exists():
|
||||||
return True
|
return True
|
||||||
return False
|
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:
|
else:
|
||||||
return False
|
return False
|
||||||
except LocationImage.DoesNotExist:
|
|
||||||
|
except ContentImage.DoesNotExist:
|
||||||
return False
|
return False
|
||||||
elif mediaType == 'attachments/':
|
elif mediaType == 'attachments/':
|
||||||
try:
|
try:
|
||||||
# Construct the full relative path to match the database field
|
# Construct the full relative path to match the database field
|
||||||
attachment_path = f"attachments/{fileId}"
|
attachment_path = f"attachments/{fileId}"
|
||||||
# Fetch the Attachment object
|
# Fetch the ContentAttachment object
|
||||||
attachment = Attachment.objects.get(file=attachment_path)
|
content_attachment = ContentAttachment.objects.get(file=attachment_path)
|
||||||
location = attachment.location
|
|
||||||
if location.is_public:
|
# 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
|
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
|
return True
|
||||||
elif location.collections.exists():
|
|
||||||
# Check if the user is in any collection's shared_with list
|
# Check collection-based permissions
|
||||||
for collection in location.collections.all():
|
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():
|
if collection.shared_with.filter(id=user.id).exists():
|
||||||
return True
|
return True
|
||||||
return False
|
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:
|
else:
|
||||||
return False
|
return False
|
||||||
except Attachment.DoesNotExist:
|
|
||||||
|
except ContentAttachment.DoesNotExist:
|
||||||
return False
|
return False
|
|
@ -2,7 +2,7 @@ from rest_framework import viewsets, status
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from adventures.models import Location, Attachment
|
from adventures.models import Location, ContentAttachment
|
||||||
from adventures.serializers import AttachmentSerializer
|
from adventures.serializers import AttachmentSerializer
|
||||||
|
|
||||||
class AttachmentViewSet(viewsets.ModelViewSet):
|
class AttachmentViewSet(viewsets.ModelViewSet):
|
||||||
|
@ -10,7 +10,7 @@ class AttachmentViewSet(viewsets.ModelViewSet):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def get_queryset(self):
|
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):
|
def create(self, request, *args, **kwargs):
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
|
|
|
@ -18,7 +18,7 @@ from django.conf import settings
|
||||||
|
|
||||||
from adventures.models import (
|
from adventures.models import (
|
||||||
Location, Collection, Transportation, Note, Checklist, ChecklistItem,
|
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
|
from worldtravel.models import VisitedCity, VisitedRegion, City, Region, Country
|
||||||
|
|
||||||
|
@ -347,7 +347,7 @@ class BackupViewSet(viewsets.ViewSet):
|
||||||
user.lodging_set.all().delete()
|
user.lodging_set.all().delete()
|
||||||
|
|
||||||
# Delete location-related data
|
# Delete location-related data
|
||||||
user.locationimage_set.all().delete()
|
user.contentimage_set.all().delete()
|
||||||
user.attachment_set.all().delete()
|
user.attachment_set.all().delete()
|
||||||
# Visits are deleted via cascade when locations are deleted
|
# Visits are deleted via cascade when locations are deleted
|
||||||
user.location_set.all().delete()
|
user.location_set.all().delete()
|
||||||
|
@ -487,7 +487,7 @@ class BackupViewSet(viewsets.ViewSet):
|
||||||
for img_data in adv_data.get('images', []):
|
for img_data in adv_data.get('images', []):
|
||||||
immich_id = img_data.get('immich_id')
|
immich_id = img_data.get('immich_id')
|
||||||
if immich_id:
|
if immich_id:
|
||||||
LocationImage.objects.create(
|
ContentImage.objects.create(
|
||||||
user=user,
|
user=user,
|
||||||
location=location,
|
location=location,
|
||||||
immich_id=immich_id,
|
immich_id=immich_id,
|
||||||
|
@ -500,7 +500,7 @@ class BackupViewSet(viewsets.ViewSet):
|
||||||
try:
|
try:
|
||||||
img_content = zip_file.read(f'images/{filename}')
|
img_content = zip_file.read(f'images/{filename}')
|
||||||
img_file = ContentFile(img_content, name=filename)
|
img_file = ContentFile(img_content, name=filename)
|
||||||
LocationImage.objects.create(
|
ContentImage.objects.create(
|
||||||
user=user,
|
user=user,
|
||||||
location=location,
|
location=location,
|
||||||
image=img_file,
|
image=img_file,
|
||||||
|
@ -517,7 +517,7 @@ class BackupViewSet(viewsets.ViewSet):
|
||||||
try:
|
try:
|
||||||
att_content = zip_file.read(f'attachments/{filename}')
|
att_content = zip_file.read(f'attachments/{filename}')
|
||||||
att_file = ContentFile(att_content, name=filename)
|
att_file = ContentFile(att_content, name=filename)
|
||||||
Attachment.objects.create(
|
ContentAttachment.objects.create(
|
||||||
user=user,
|
user=user,
|
||||||
location=location,
|
location=location,
|
||||||
file=att_file,
|
file=att_file,
|
||||||
|
|
|
@ -4,14 +4,14 @@ from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from adventures.models import Location, LocationImage
|
from adventures.models import Location, ContentImage
|
||||||
from adventures.serializers import LocationImageSerializer
|
from adventures.serializers import ContentImageSerializer
|
||||||
from integrations.models import ImmichIntegration
|
from integrations.models import ImmichIntegration
|
||||||
import uuid
|
import uuid
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
class AdventureImageViewSet(viewsets.ModelViewSet):
|
class AdventureImageViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = LocationImageSerializer
|
serializer_class = ContentImageSerializer
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
@action(detail=True, methods=['post'])
|
@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)
|
return Response({"error": "Image is already the primary image"}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# Set the current primary image to false
|
# 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
|
# Set the new image to true
|
||||||
instance.is_primary = 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)
|
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
|
# 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__id=location_uuid) & (
|
||||||
Q(location__user=request.user) | # User owns the location
|
Q(location__user=request.user) | # User owns the location
|
||||||
Q(location__collections__shared_with=request.user) # User has shared access via collection
|
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):
|
def get_queryset(self):
|
||||||
# Updated to include images from locations the user owns OR has shared access to
|
# 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__user=self.request.user) | # User owns the location
|
||||||
Q(location__collections__shared_with=self.request.user) # User has shared access via collection
|
Q(location__collections__shared_with=self.request.user) # User has shared access via collection
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
|
@ -8,7 +8,7 @@ from rest_framework.permissions import IsAuthenticated
|
||||||
import requests
|
import requests
|
||||||
from rest_framework.pagination import PageNumberPagination
|
from rest_framework.pagination import PageNumberPagination
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from adventures.models import LocationImage
|
from adventures.models import ContentImage
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
import logging
|
import logging
|
||||||
|
@ -257,7 +257,7 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
||||||
2. Private locations in public collections: accessible by anyone
|
2. Private locations in public collections: accessible by anyone
|
||||||
3. Private locations in private collections shared with user: accessible by shared users
|
3. Private locations in private collections shared with user: accessible by shared users
|
||||||
4. Private locations: accessible only to the owner
|
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:
|
if not imageid or not integration_id:
|
||||||
return Response({
|
return Response({
|
||||||
|
@ -272,7 +272,7 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
||||||
|
|
||||||
# Try to find the image entry with collections and sharing information
|
# Try to find the image entry with collections and sharing information
|
||||||
image_entry = (
|
image_entry = (
|
||||||
LocationImage.objects
|
ContentImage.objects
|
||||||
.filter(immich_id=imageid, user=owner_id)
|
.filter(immich_id=imageid, user=owner_id)
|
||||||
.select_related('location')
|
.select_related('location')
|
||||||
.prefetch_related('location__collections', 'location__collections__shared_with')
|
.prefetch_related('location__collections', 'location__collections__shared_with')
|
||||||
|
@ -313,7 +313,7 @@ class ImmichIntegrationView(viewsets.ViewSet):
|
||||||
'code': 'immich.permission_denied'
|
'code': 'immich.permission_denied'
|
||||||
}, status=status.HTTP_403_FORBIDDEN)
|
}, status=status.HTTP_403_FORBIDDEN)
|
||||||
else:
|
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:
|
if not request.user.is_authenticated or request.user != owner_id:
|
||||||
return Response({
|
return Response({
|
||||||
'message': 'Image is not linked to any location and you are not the owner.',
|
'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