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)