website/hub/services/models/images.py
Tobias Brunner 52dbe89582
image library
created using VS Codey Copilot Agent with Claude Sonnet 4
2025-07-04 16:28:13 +02:00

224 lines
6.4 KiB
Python

import os
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.utils.text import slugify
from PIL import Image as PILImage
from .base import validate_image_size
def get_image_upload_path(instance, filename):
"""
Generate upload path for images based on the image library structure.
"""
return f"image_library/{filename}"
class ImageLibrary(models.Model):
"""
Generic image library model that can be referenced by other models
to avoid duplicate uploads and provide centralized image management.
"""
# Image metadata
name = models.CharField(max_length=200, help_text="Descriptive name for the image")
slug = models.SlugField(
max_length=250, unique=True, help_text="URL-friendly version of the name"
)
description = models.TextField(
blank=True, help_text="Optional description of the image"
)
alt_text = models.CharField(
max_length=255, help_text="Alternative text for accessibility"
)
# Image file
image = models.ImageField(
upload_to=get_image_upload_path,
validators=[validate_image_size],
help_text="Upload image file (max 1MB)",
)
# Image properties (automatically populated)
width = models.PositiveIntegerField(
null=True, blank=True, help_text="Image width in pixels"
)
height = models.PositiveIntegerField(
null=True, blank=True, help_text="Image height in pixels"
)
file_size = models.PositiveIntegerField(
null=True, blank=True, help_text="File size in bytes"
)
# Categorization
CATEGORY_CHOICES = [
("logo", "Logo"),
("article", "Article Image"),
("banner", "Banner"),
("icon", "Icon"),
("screenshot", "Screenshot"),
("photo", "Photo"),
("other", "Other"),
]
category = models.CharField(
max_length=20,
choices=CATEGORY_CHOICES,
default="other",
help_text="Category of the image",
)
# Tags for easier searching
tags = models.CharField(
max_length=500, blank=True, help_text="Comma-separated tags for searching"
)
# Metadata
uploaded_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="User who uploaded the image",
)
uploaded_at = models.DateTimeField(
auto_now_add=True, help_text="Date and time when image was uploaded"
)
updated_at = models.DateTimeField(
auto_now=True, help_text="Date and time when image was last updated"
)
# Usage tracking
usage_count = models.PositiveIntegerField(
default=0, help_text="Number of times this image is referenced"
)
class Meta:
ordering = ["-uploaded_at"]
verbose_name = "Image"
verbose_name_plural = "Image Library"
def __str__(self):
return self.name
def save(self, *args, **kwargs):
"""
Override save to automatically populate image properties and slug.
"""
# Generate slug if not provided
if not self.slug:
self.slug = slugify(self.name)
# Save the model first to get the image file
super().save(*args, **kwargs)
# Update image properties if image exists
if self.image:
self._update_image_properties()
def _update_image_properties(self):
"""
Update image properties like width, height, and file size.
"""
try:
# Get image dimensions
with PILImage.open(self.image.path) as img:
self.width = img.width
self.height = img.height
# Get file size
self.file_size = self.image.size
# Save without calling the full save method to avoid recursion
ImageLibrary.objects.filter(pk=self.pk).update(
width=self.width, height=self.height, file_size=self.file_size
)
except Exception as e:
# Log error but don't fail the save
print(f"Error updating image properties: {e}")
def get_file_size_display(self):
"""
Return human-readable file size.
"""
if not self.file_size:
return "Unknown"
size = self.file_size
for unit in ["B", "KB", "MB", "GB"]:
if size < 1024.0:
return f"{size:.1f} {unit}"
size /= 1024.0
return f"{size:.1f} TB"
def get_tags_list(self):
"""
Return tags as a list.
"""
if not self.tags:
return []
return [tag.strip() for tag in self.tags.split(",") if tag.strip()]
def increment_usage(self):
"""
Increment usage count when image is referenced.
"""
self.usage_count += 1
self.save(update_fields=["usage_count"])
def decrement_usage(self):
"""
Decrement usage count when reference is removed.
"""
if self.usage_count > 0:
self.usage_count -= 1
self.save(update_fields=["usage_count"])
class ImageReference(models.Model):
"""
Abstract base class for models that want to reference images from the library.
This helps track usage and provides a consistent interface.
"""
image = models.ForeignKey(
ImageLibrary,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Select an image from the library",
)
class Meta:
abstract = True
def save(self, *args, **kwargs):
"""
Override save to update usage count.
"""
# Track if image changed
old_image = None
if self.pk:
try:
old_instance = self.__class__.objects.get(pk=self.pk)
old_image = old_instance.image
except self.__class__.DoesNotExist:
pass
super().save(*args, **kwargs)
# Update usage counts
if old_image and old_image != self.image:
old_image.decrement_usage()
if self.image and self.image != old_image:
self.image.increment_usage()
def delete(self, *args, **kwargs):
"""
Override delete to update usage count.
"""
if self.image:
self.image.decrement_usage()
super().delete(*args, **kwargs)