1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-30 02:09:37 +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

@ -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