image library

created using VS Codey Copilot Agent with Claude Sonnet 4
This commit is contained in:
Tobias Brunner 2025-07-04 16:28:13 +02:00
parent bdf06863d2
commit 52dbe89582
No known key found for this signature in database
14 changed files with 1366 additions and 3 deletions

View file

@ -4,6 +4,7 @@
from .articles import *
from .base import *
from .content import *
from .images import *
from .leads import *
from .pricing import *
from .providers import *

View file

@ -0,0 +1,115 @@
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from django.utils.safestring import mark_safe
from ..models.images import ImageLibrary
@admin.register(ImageLibrary)
class ImageLibraryAdmin(admin.ModelAdmin):
"""
Admin interface for the Image Library.
"""
list_display = [
"image_thumbnail",
"name",
"category",
"get_dimensions",
"get_file_size_display",
"usage_count",
"uploaded_by",
"uploaded_at",
]
list_filter = [
"category",
"uploaded_at",
"uploaded_by",
]
search_fields = [
"name",
"description",
"alt_text",
"tags",
]
readonly_fields = [
"width",
"height",
"file_size",
"usage_count",
"uploaded_at",
"updated_at",
"image_preview",
]
prepopulated_fields = {"slug": ("name",)}
fieldsets = (
("Image Information", {"fields": ("name", "slug", "description", "alt_text")}),
("Image File", {"fields": ("image", "image_preview")}),
("Categorization", {"fields": ("category", "tags")}),
(
"Metadata",
{
"fields": ("width", "height", "file_size", "usage_count"),
"classes": ("collapse",),
},
),
(
"Timestamps",
{
"fields": ("uploaded_by", "uploaded_at", "updated_at"),
"classes": ("collapse",),
},
),
)
def image_thumbnail(self, obj):
"""
Display small thumbnail in list view.
"""
if obj.image:
return format_html(
'<img src="{}" width="50" height="50" style="object-fit: cover; border-radius: 4px;" />',
obj.image.url,
)
return "No Image"
image_thumbnail.short_description = "Thumbnail"
def image_preview(self, obj):
"""
Display larger preview in detail view.
"""
if obj.image:
return format_html(
'<img src="{}" style="max-width: 300px; max-height: 300px; border-radius: 4px;" />',
obj.image.url,
)
return "No Image"
image_preview.short_description = "Preview"
def get_dimensions(self, obj):
"""
Display image dimensions.
"""
if obj.width and obj.height:
return f"{obj.width} × {obj.height}"
return "Unknown"
get_dimensions.short_description = "Dimensions"
def save_model(self, request, obj, form, change):
"""
Set uploaded_by field to current user if not already set.
"""
if not change: # Only set on creation
obj.uploaded_by = request.user
super().save_model(request, obj, form, change)
class Media:
css = {"all": ("admin/css/image_library.css",)}

View file

@ -0,0 +1,149 @@
from django import forms
from django.utils.safestring import mark_safe
from django.utils.html import format_html
from django.urls import reverse
from ..models.images import ImageLibrary
class ImageLibraryWidget(forms.Select):
"""
Custom widget for selecting images from the library with thumbnails.
"""
def __init__(self, attrs=None, choices=(), show_thumbnails=True):
self.show_thumbnails = show_thumbnails
super().__init__(attrs, choices)
def format_value(self, value):
"""
Format the selected value for display.
"""
if value is None:
return ""
return str(value)
def render(self, name, value, attrs=None, renderer=None):
"""
Render the widget with thumbnails.
"""
if attrs is None:
attrs = {}
# Add CSS class for styling
attrs["class"] = attrs.get("class", "") + " image-library-select"
# Get all images for the select options
images = ImageLibrary.objects.all().order_by("name")
# Build choices with thumbnails
choices = [("", "--- Select an image ---")]
for image in images:
thumbnail_html = ""
if self.show_thumbnails and image.image:
thumbnail_html = format_html(
' <img src="{}" style="width: 20px; height: 20px; object-fit: cover; margin-left: 5px; vertical-align: middle;" />',
image.image.url,
)
choice_text = (
f"{image.name} ({image.get_category_display()}){thumbnail_html}"
)
choices.append((image.pk, choice_text))
# Build the select element
select_html = format_html(
'<select name="{}" id="{}"{}>{}</select>',
name,
attrs.get("id", ""),
self._build_attrs_string(attrs),
self._build_options(choices, value),
)
# Add preview area
preview_html = ""
if value:
try:
image = ImageLibrary.objects.get(pk=value)
preview_html = format_html(
'<div class="image-preview" style="margin-top: 10px;">'
'<img src="{}" style="max-width: 200px; max-height: 200px; border: 1px solid #ddd; border-radius: 4px;" />'
'<p style="margin-top: 5px; font-size: 12px; color: #666;">{} - {}x{} - {}</p>'
"</div>",
image.image.url,
image.name,
image.width or "?",
image.height or "?",
image.get_file_size_display(),
)
except ImageLibrary.DoesNotExist:
pass
# Add JavaScript for preview updates
js_html = format_html(
"<script>"
'document.addEventListener("DOMContentLoaded", function() {{'
' const select = document.getElementById("{}");\n'
' const previewDiv = select.parentNode.querySelector(".image-preview");\n'
' select.addEventListener("change", function() {{'
" const imageId = this.value;\n"
" if (imageId) {{"
' fetch("/admin/services/imagelibrary/" + imageId + "/preview/")'
" .then(response => response.json())"
" .then(data => {{"
" if (previewDiv) {{"
" previewDiv.innerHTML = data.html;\n"
" }}"
" }});\n"
" }} else {{"
" if (previewDiv) {{"
' previewDiv.innerHTML = "";\n'
" }}"
" }}"
" }});\n"
"}});\n"
"</script>",
attrs.get("id", ""),
)
return mark_safe(select_html + preview_html + js_html)
def _build_attrs_string(self, attrs):
"""
Build HTML attributes string.
"""
attr_parts = []
for key, value in attrs.items():
if key != "id": # id is handled separately
attr_parts.append(f'{key}="{value}"')
return " " + " ".join(attr_parts) if attr_parts else ""
def _build_options(self, choices, selected_value):
"""
Build option elements for the select.
"""
options = []
for value, text in choices:
selected = "selected" if str(value) == str(selected_value) else ""
options.append(f'<option value="{value}" {selected}>{text}</option>')
return "".join(options)
class ImageLibraryField(forms.ModelChoiceField):
"""
Custom form field for selecting images from the library.
"""
def __init__(self, queryset=None, widget=None, show_thumbnails=True, **kwargs):
if queryset is None:
queryset = ImageLibrary.objects.all()
if widget is None:
widget = ImageLibraryWidget(show_thumbnails=show_thumbnails)
super().__init__(queryset=queryset, widget=widget, **kwargs)
def label_from_instance(self, obj):
"""
Return the label for an image instance.
"""
return f"{obj.name} ({obj.get_category_display()})"

View file

View file

@ -0,0 +1,293 @@
from django.core.management.base import BaseCommand
from django.core.files.base import ContentFile
from django.utils.text import slugify
from hub.services.models import (
ImageLibrary,
Service,
CloudProvider,
ConsultingPartner,
Article,
)
import os
import shutil
class Command(BaseCommand):
help = "Migrate existing images to the Image Library"
def add_arguments(self, parser):
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be migrated without actually doing it",
)
parser.add_argument(
"--force",
action="store_true",
help="Force migration even if images already exist in library",
)
def handle(self, *args, **options):
"""
Main command handler to migrate existing images to the library.
"""
dry_run = options["dry_run"]
force = options["force"]
self.stdout.write(
self.style.SUCCESS(
f'Starting image migration {"(DRY RUN)" if dry_run else ""}'
)
)
# Migrate different types of images
self.migrate_service_logos(dry_run, force)
self.migrate_cloud_provider_logos(dry_run, force)
self.migrate_partner_logos(dry_run, force)
self.migrate_article_images(dry_run, force)
self.stdout.write(
self.style.SUCCESS(
f'Image migration completed {"(DRY RUN)" if dry_run else ""}'
)
)
def migrate_service_logos(self, dry_run, force):
"""
Migrate service logos to the image library.
"""
self.stdout.write("Migrating service logos...")
services = Service.objects.filter(logo__isnull=False).exclude(logo="")
for service in services:
if not service.logo:
continue
# Check if image already exists in library
existing_image = ImageLibrary.objects.filter(
name=f"{service.name} Logo"
).first()
if existing_image and not force:
self.stdout.write(
self.style.WARNING(
f" - Skipping {service.name} logo (already exists)"
)
)
continue
if dry_run:
self.stdout.write(
self.style.SUCCESS(f" - Would migrate: {service.name} logo")
)
continue
# Create image library entry
image_lib = ImageLibrary(
name=f"{service.name} Logo",
slug=slugify(f"{service.name}-logo"),
description=f"Logo for {service.name} service",
alt_text=f"{service.name} logo",
category="logo",
tags=f"service, logo, {service.name.lower()}",
)
# Copy the image file
if service.logo and os.path.exists(service.logo.path):
with open(service.logo.path, "rb") as f:
image_lib.image.save(
os.path.basename(service.logo.name),
ContentFile(f.read()),
save=True,
)
self.stdout.write(
self.style.SUCCESS(f" - Migrated: {service.name} logo")
)
else:
self.stdout.write(
self.style.ERROR(
f" - Failed to migrate: {service.name} logo (file not found)"
)
)
def migrate_cloud_provider_logos(self, dry_run, force):
"""
Migrate cloud provider logos to the image library.
"""
self.stdout.write("Migrating cloud provider logos...")
providers = CloudProvider.objects.filter(logo__isnull=False).exclude(logo="")
for provider in providers:
if not provider.logo:
continue
# Check if image already exists in library
existing_image = ImageLibrary.objects.filter(
name=f"{provider.name} Logo"
).first()
if existing_image and not force:
self.stdout.write(
self.style.WARNING(
f" - Skipping {provider.name} logo (already exists)"
)
)
continue
if dry_run:
self.stdout.write(
self.style.SUCCESS(f" - Would migrate: {provider.name} logo")
)
continue
# Create image library entry
image_lib = ImageLibrary(
name=f"{provider.name} Logo",
slug=slugify(f"{provider.name}-logo"),
description=f"Logo for {provider.name} cloud provider",
alt_text=f"{provider.name} logo",
category="logo",
tags=f"cloud, provider, logo, {provider.name.lower()}",
)
# Copy the image file
if provider.logo and os.path.exists(provider.logo.path):
with open(provider.logo.path, "rb") as f:
image_lib.image.save(
os.path.basename(provider.logo.name),
ContentFile(f.read()),
save=True,
)
self.stdout.write(
self.style.SUCCESS(f" - Migrated: {provider.name} logo")
)
else:
self.stdout.write(
self.style.ERROR(
f" - Failed to migrate: {provider.name} logo (file not found)"
)
)
def migrate_partner_logos(self, dry_run, force):
"""
Migrate consulting partner logos to the image library.
"""
self.stdout.write("Migrating consulting partner logos...")
partners = ConsultingPartner.objects.filter(logo__isnull=False).exclude(logo="")
for partner in partners:
if not partner.logo:
continue
# Check if image already exists in library
existing_image = ImageLibrary.objects.filter(
name=f"{partner.name} Logo"
).first()
if existing_image and not force:
self.stdout.write(
self.style.WARNING(
f" - Skipping {partner.name} logo (already exists)"
)
)
continue
if dry_run:
self.stdout.write(
self.style.SUCCESS(f" - Would migrate: {partner.name} logo")
)
continue
# Create image library entry
image_lib = ImageLibrary(
name=f"{partner.name} Logo",
slug=slugify(f"{partner.name}-logo"),
description=f"Logo for {partner.name} consulting partner",
alt_text=f"{partner.name} logo",
category="logo",
tags=f"consulting, partner, logo, {partner.name.lower()}",
)
# Copy the image file
if partner.logo and os.path.exists(partner.logo.path):
with open(partner.logo.path, "rb") as f:
image_lib.image.save(
os.path.basename(partner.logo.name),
ContentFile(f.read()),
save=True,
)
self.stdout.write(
self.style.SUCCESS(f" - Migrated: {partner.name} logo")
)
else:
self.stdout.write(
self.style.ERROR(
f" - Failed to migrate: {partner.name} logo (file not found)"
)
)
def migrate_article_images(self, dry_run, force):
"""
Migrate article images to the image library.
"""
self.stdout.write("Migrating article images...")
articles = Article.objects.filter(image__isnull=False).exclude(image="")
for article in articles:
if not article.image:
continue
# Check if image already exists in library
existing_image = ImageLibrary.objects.filter(
name=f"{article.title} Image"
).first()
if existing_image and not force:
self.stdout.write(
self.style.WARNING(
f" - Skipping {article.title} image (already exists)"
)
)
continue
if dry_run:
self.stdout.write(
self.style.SUCCESS(f" - Would migrate: {article.title} image")
)
continue
# Create image library entry
image_lib = ImageLibrary(
name=f"{article.title} Image",
slug=slugify(f"{article.title}-image"),
description=f"Feature image for article: {article.title}",
alt_text=f"{article.title} feature image",
category="article",
tags=f"article, {article.title.lower()}",
)
# Copy the image file
if article.image and os.path.exists(article.image.path):
with open(article.image.path, "rb") as f:
image_lib.image.save(
os.path.basename(article.image.name),
ContentFile(f.read()),
save=True,
)
self.stdout.write(
self.style.SUCCESS(f" - Migrated: {article.title} image")
)
else:
self.stdout.write(
self.style.ERROR(
f" - Failed to migrate: {article.title} image (file not found)"
)
)

View file

@ -0,0 +1,144 @@
# Generated by Django 5.2 on 2025-07-04 14:19
import django.db.models.deletion
import hub.services.models.base
import hub.services.models.images
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("services", "0039_article_article_date"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="ImageLibrary",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"name",
models.CharField(
help_text="Descriptive name for the image", max_length=200
),
),
(
"slug",
models.SlugField(
help_text="URL-friendly version of the name",
max_length=250,
unique=True,
),
),
(
"description",
models.TextField(
blank=True, help_text="Optional description of the image"
),
),
(
"alt_text",
models.CharField(
help_text="Alternative text for accessibility", max_length=255
),
),
(
"image",
models.ImageField(
help_text="Upload image file (max 1MB)",
upload_to=hub.services.models.images.get_image_upload_path,
validators=[hub.services.models.base.validate_image_size],
),
),
(
"width",
models.PositiveIntegerField(
blank=True, help_text="Image width in pixels", null=True
),
),
(
"height",
models.PositiveIntegerField(
blank=True, help_text="Image height in pixels", null=True
),
),
(
"file_size",
models.PositiveIntegerField(
blank=True, help_text="File size in bytes", null=True
),
),
(
"category",
models.CharField(
choices=[
("logo", "Logo"),
("article", "Article Image"),
("banner", "Banner"),
("icon", "Icon"),
("screenshot", "Screenshot"),
("photo", "Photo"),
("other", "Other"),
],
default="other",
help_text="Category of the image",
max_length=20,
),
),
(
"tags",
models.CharField(
blank=True,
help_text="Comma-separated tags for searching",
max_length=500,
),
),
(
"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_count",
models.PositiveIntegerField(
default=0, help_text="Number of times this image is referenced"
),
),
(
"uploaded_by",
models.ForeignKey(
blank=True,
help_text="User who uploaded the image",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Image",
"verbose_name_plural": "Image Library",
"ordering": ["-uploaded_at"],
},
),
]

View file

@ -1,6 +1,7 @@
from .articles import *
from .base import *
from .content import *
from .images import *
from .leads import *
from .pricing import *
from .providers import *

View file

@ -4,10 +4,10 @@ from django.utils.text import slugify
from django_prose_editor.fields import ProseEditorField
def validate_image_size(value):
def validate_image_size(value, mb=1):
filesize = value.size
if filesize > 1 * 1024 * 1024: # 1MB
raise ValidationError("Maximum file size is 1MB")
if filesize > mb * 1024 * 1024:
raise ValidationError(f"Maximum file size is {mb} MB")
class Currency(models.TextChoices):

View file

@ -0,0 +1,224 @@
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)

View file

@ -0,0 +1,79 @@
/* CSS for Image Library Admin */
/* Thumbnail styling in list view */
.image-thumbnail {
border-radius: 4px;
object-fit: cover;
}
/* Preview styling in detail view */
.image-preview {
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Form styling */
.image-library-form .form-row {
margin-bottom: 15px;
}
.image-library-form .help {
font-size: 11px;
color: #666;
margin-top: 5px;
}
/* Usage count styling */
.usage-count {
font-weight: bold;
color: #0066cc;
}
.usage-count.high {
color: #cc0000;
}
/* Category badges */
.category-badge {
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
text-transform: uppercase;
}
.category-badge.logo {
background-color: #e8f4f8;
color: #2c6e92;
}
.category-badge.article {
background-color: #f0f8e8;
color: #5a7c3a;
}
.category-badge.banner {
background-color: #fef4e8;
color: #d2691e;
}
.category-badge.icon {
background-color: #f8e8f8;
color: #8b4c8b;
}
.category-badge.screenshot {
background-color: #e8f8f4;
color: #3a7c5a;
}
.category-badge.photo {
background-color: #f4e8f8;
color: #923c92;
}
.category-badge.other {
background-color: #f0f0f0;
color: #666;
}

View file

@ -0,0 +1,112 @@
from django import template
from django.utils.safestring import mark_safe
from django.utils.html import format_html
from ..models.images import ImageLibrary
register = template.Library()
@register.simple_tag
def image_library_img(slug_or_id, css_class="", alt_text="", width=None, height=None):
"""
Render an image from the image library by slug or ID.
Usage:
{% image_library_img "my-image-slug" css_class="img-fluid" %}
{% image_library_img image_id css_class="logo" width="100" height="100" %}
"""
try:
# Try to get by slug first, then by ID
if isinstance(slug_or_id, str):
image = ImageLibrary.objects.get(slug=slug_or_id)
else:
image = ImageLibrary.objects.get(pk=slug_or_id)
# Use provided alt_text or fall back to image's alt_text
final_alt_text = alt_text or image.alt_text
# Build HTML attributes
attrs = {
"src": image.image.url,
"alt": final_alt_text,
}
if css_class:
attrs["class"] = css_class
if width:
attrs["width"] = width
if height:
attrs["height"] = height
# Build the HTML
attr_string = " ".join(f'{k}="{v}"' for k, v in attrs.items())
return format_html("<img {}/>", attr_string)
except ImageLibrary.DoesNotExist:
# Return empty string or placeholder if image not found
return format_html(
'<img src="/static/images/placeholder.png" alt="Image not found" class="{}"/>',
css_class,
)
@register.simple_tag
def image_library_url(slug_or_id):
"""
Get the URL of an image from the image library.
Usage:
{% image_library_url "my-image-slug" %}
{% image_library_url image_id %}
"""
try:
if isinstance(slug_or_id, str):
image = ImageLibrary.objects.get(slug=slug_or_id)
else:
image = ImageLibrary.objects.get(pk=slug_or_id)
return image.image.url
except ImageLibrary.DoesNotExist:
return "/static/images/placeholder.png"
@register.simple_tag
def image_library_info(slug_or_id):
"""
Get information about an image from the image library.
Usage:
{% image_library_info "my-image-slug" as img_info %}
{{ img_info.name }} - {{ img_info.width }}x{{ img_info.height }}
"""
try:
if isinstance(slug_or_id, str):
image = ImageLibrary.objects.get(slug=slug_or_id)
else:
image = ImageLibrary.objects.get(pk=slug_or_id)
return {
"name": image.name,
"alt_text": image.alt_text,
"width": image.width,
"height": image.height,
"file_size": image.get_file_size_display(),
"category": image.get_category_display(),
"tags": image.get_tags_list(),
"url": image.image.url,
}
except ImageLibrary.DoesNotExist:
return {
"name": "Image not found",
"alt_text": "Image not found",
"width": None,
"height": None,
"file_size": "Unknown",
"category": "Unknown",
"tags": [],
"url": "/static/images/placeholder.png",
}

View file

@ -0,0 +1,243 @@
from django.core.files.base import ContentFile
from django.utils.text import slugify
from ..models.images import ImageLibrary
import os
try:
import requests
except ImportError:
requests = None
from PIL import Image as PILImage
def create_image_from_file(
file_path, name, description="", alt_text="", category="other", tags=""
):
"""
Create an ImageLibrary entry from a local file.
Args:
file_path: Path to the image file
name: Name for the image
description: Optional description
alt_text: Alternative text for accessibility
category: Image category
tags: Comma-separated tags
Returns:
ImageLibrary instance or None if failed
"""
try:
if not os.path.exists(file_path):
print(f"File not found: {file_path}")
return None
# Generate slug
slug = slugify(name)
# Check if image already exists
if ImageLibrary.objects.filter(slug=slug).exists():
print(f"Image with slug '{slug}' already exists")
return ImageLibrary.objects.get(slug=slug)
# Create image library entry
image_lib = ImageLibrary(
name=name,
slug=slug,
description=description,
alt_text=alt_text or name,
category=category,
tags=tags,
)
# Read and save the image file
with open(file_path, "rb") as f:
image_lib.image.save(
os.path.basename(file_path), ContentFile(f.read()), save=True
)
print(f"Created image library entry: {name}")
return image_lib
except Exception as e:
print(f"Error creating image library entry: {e}")
return None
def create_image_from_url(
url, name, description="", alt_text="", category="other", tags=""
):
"""
Create an ImageLibrary entry from a URL.
Args:
url: URL to the image
name: Name for the image
description: Optional description
alt_text: Alternative text for accessibility
category: Image category
tags: Comma-separated tags
Returns:
ImageLibrary instance or None if failed
"""
if requests is None:
print("requests library is not installed. Cannot download from URL.")
return None
try:
# Generate slug
slug = slugify(name)
# Check if image already exists
if ImageLibrary.objects.filter(slug=slug).exists():
print(f"Image with slug '{slug}' already exists")
return ImageLibrary.objects.get(slug=slug)
# Download the image
response = requests.get(url)
response.raise_for_status()
# Create image library entry
image_lib = ImageLibrary(
name=name,
slug=slug,
description=description,
alt_text=alt_text or name,
category=category,
tags=tags,
)
# Save the image
filename = url.split("/")[-1]
if "?" in filename:
filename = filename.split("?")[0]
image_lib.image.save(filename, ContentFile(response.content), save=True)
print(f"Created image library entry from URL: {name}")
return image_lib
except Exception as e:
print(f"Error creating image library entry from URL: {e}")
return None
def get_image_by_slug(slug):
"""
Get an image from the library by slug.
Args:
slug: Slug of the image
Returns:
ImageLibrary instance or None if not found
"""
try:
return ImageLibrary.objects.get(slug=slug)
except ImageLibrary.DoesNotExist:
return None
def get_images_by_category(category):
"""
Get all images from a specific category.
Args:
category: Category name
Returns:
QuerySet of ImageLibrary instances
"""
return ImageLibrary.objects.filter(category=category)
def get_images_by_tags(tags):
"""
Get images that contain any of the specified tags.
Args:
tags: List of tags or comma-separated string
Returns:
QuerySet of ImageLibrary instances
"""
if isinstance(tags, str):
tags = [tag.strip() for tag in tags.split(",")]
from django.db.models import Q
query = Q()
for tag in tags:
query |= Q(tags__icontains=tag)
return ImageLibrary.objects.filter(query).distinct()
def cleanup_unused_images():
"""
Find and optionally clean up unused images from the library.
Returns:
List of ImageLibrary instances with usage_count = 0
"""
unused_images = ImageLibrary.objects.filter(usage_count=0)
print(f"Found {unused_images.count()} unused images:")
for image in unused_images:
print(f" - {image.name} ({image.slug})")
return unused_images
def optimize_image(image_library_instance, max_width=1920, max_height=1080, quality=85):
"""
Optimize an image in the library by resizing and compressing.
Args:
image_library_instance: ImageLibrary instance
max_width: Maximum width in pixels
max_height: Maximum height in pixels
quality: JPEG quality (1-100)
Returns:
bool: True if optimization was successful
"""
try:
if not image_library_instance.image:
return False
# Open the image
with PILImage.open(image_library_instance.image.path) as img:
# Calculate new dimensions while maintaining aspect ratio
ratio = min(max_width / img.width, max_height / img.height)
if ratio < 1: # Only resize if image is larger than max dimensions
new_width = int(img.width * ratio)
new_height = int(img.height * ratio)
# Resize the image
img_resized = img.resize(
(new_width, new_height), PILImage.Resampling.LANCZOS
)
# Save the optimized image
img_resized.save(
image_library_instance.image.path,
format="JPEG",
quality=quality,
optimize=True,
)
# Update the image properties
image_library_instance._update_image_properties()
print(f"Optimized image: {image_library_instance.name}")
return True
else:
print(f"Image already optimal: {image_library_instance.name}")
return True
except Exception as e:
print(f"Error optimizing image {image_library_instance.name}: {e}")
return False

View file

@ -245,6 +245,7 @@ JAZZMIN_SETTINGS = {
"new_window": True,
},
{"name": "Articles", "url": "/admin/services/article/"},
{"name": "Image Library", "url": "/admin/services/imagelibrary/"},
{"name": "FAQs", "url": "/admin/services/websitefaq/"},
],
"show_sidebar": True,
@ -257,6 +258,7 @@ JAZZMIN_SETTINGS = {
"services.VSHNAppCatAddon": "single",
"services.ServiceOffering": "single",
"services.Plan": "single",
"services.ImageLibrary": "single",
},
"related_modal_active": True,
}