226 lines
6.7 KiB
Python
226 lines
6.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 ""
|
|
|
|
html_parts.append(
|
|
f"""
|
|
<div class="image-option {selected}" data-value="{image.pk}">
|
|
<div class="image-preview">
|
|
<img src="{image_url}" alt="{image.alt_text}" loading="lazy">
|
|
</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",
|
|
)
|
|
}
|