website/hub/services/admin/widgets.py

232 lines
7 KiB
Python

"""
Custom widgets for Django admin interface
"""
from django import forms
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.urls import reverse
from django.conf import settings
from ..models import ImageLibrary
class ImageLibraryWidget(forms.Select):
"""Custom widget for selecting images from the library with visual preview"""
def __init__(self, attrs=None):
super().__init__(attrs)
self.attrs.update(
{
"class": "image-library-select",
"style": "display: none;", # Hide the original select
}
)
def render(self, name, value, attrs=None, renderer=None):
"""Render the widget with image previews"""
# Get the original select element
original_select = super().render(name, value, attrs, renderer)
# Get all images from the library
images = ImageLibrary.objects.all().order_by("-uploaded_at")
# Create the visual interface
html_parts = [
'<div class="image-library-widget">',
original_select, # Keep the original select for form submission
'<div class="image-library-grid">',
]
# Add "No image" option
no_image_selected = "selected" if not value else ""
html_parts.append(
f"""
<div class="image-option {no_image_selected}" data-value="">
<div class="image-preview no-image">
<i class="fas fa-ban"></i>
<span>No image</span>
</div>
<div class="image-info">
<span class="image-name">No image selected</span>
</div>
</div>
"""
)
# Add each image as an option
for image in images:
selected = "selected" if str(image.pk) == str(value) else ""
image_url = image.image.url if image.image else ""
# Use img tag for all images in widget to maintain clickability
# SVG files will still display correctly with img tag
preview_html = (
f'<img src="{image_url}" alt="{image.alt_text}" loading="lazy">'
)
html_parts.append(
f"""
<div class="image-option {selected}" data-value="{image.pk}">
<div class="image-preview">
{preview_html}
</div>
<div class="image-info">
<span class="image-name">{image.name}</span>
<span class="image-category">{image.get_category_display()}</span>
<span class="image-size">{image.width}x{image.height}</span>
</div>
</div>
"""
)
html_parts.extend(
[
"</div>",
"</div>",
self._get_styles(),
self._get_javascript(),
]
)
return mark_safe("".join(html_parts))
def _get_styles(self):
"""Return CSS styles for the widget"""
return """
<style>
.image-library-widget {
margin: 10px 0;
}
.image-library-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
max-height: 800px;
overflow-y: auto;
border: 1px solid #ddd;
padding: 15px;
background: #f9f9f9;
border-radius: 5px;
}
.image-option {
background: white;
border: 2px solid #ddd;
border-radius: 5px;
padding: 10px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
}
.image-option:hover {
border-color: #007cba;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.image-option.selected {
border-color: #007cba;
background: #e3f2fd;
box-shadow: 0 0 0 2px rgba(0, 124, 186, 0.2);
}
.image-preview {
width: 100%;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
border-radius: 3px;
overflow: hidden;
background: #f5f5f5;
}
.image-preview img {
max-width: 100%;
max-height: 100%;
object-fit: cover;
border-radius: 3px;
}
.image-preview.no-image {
background: #f0f0f0;
color: #666;
flex-direction: column;
}
.image-preview.no-image i {
font-size: 24px;
margin-bottom: 5px;
}
.image-info {
text-align: left;
}
.image-name {
display: block;
font-weight: 600;
font-size: 12px;
color: #333;
margin-bottom: 3px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.image-category {
display: inline-block;
background: #e0e0e0;
color: #666;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
margin-right: 5px;
}
.image-size {
font-size: 10px;
color: #666;
}
</style>
"""
def _get_javascript(self):
"""Return JavaScript for the widget functionality"""
return """
<script>
document.addEventListener('DOMContentLoaded', function() {
// Handle image selection
document.querySelectorAll('.image-option').forEach(function(option) {
option.addEventListener('click', function() {
const widget = this.closest('.image-library-widget');
const select = widget.querySelector('.image-library-select');
const value = this.dataset.value;
// Update the hidden select
select.value = value;
// Update visual selection
widget.querySelectorAll('.image-option').forEach(function(opt) {
opt.classList.remove('selected');
});
this.classList.add('selected');
// Trigger change event
select.dispatchEvent(new Event('change'));
});
});
});
</script>
"""
class Media:
css = {
"all": (
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css",
)
}