nice image selection from library

This commit is contained in:
Tobias Brunner 2025-07-08 11:59:19 +02:00
parent 7319709749
commit 7e46dc71ec
No known key found for this signature in database
5 changed files with 274 additions and 1 deletions

View file

@ -8,4 +8,5 @@ from .images import *
from .leads import * from .leads import *
from .pricing import * from .pricing import *
from .providers import * from .providers import *
from .widgets import *
from .services import * from .services import *

View file

@ -7,8 +7,8 @@ from django.utils.html import format_html
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from ..models import Article from ..models import Article
from .widgets import ImageLibraryWidget
class ArticleAdminForm(forms.ModelForm): class ArticleAdminForm(forms.ModelForm):
@ -17,6 +17,9 @@ class ArticleAdminForm(forms.ModelForm):
class Meta: class Meta:
model = Article model = Article
fields = "__all__" fields = "__all__"
widgets = {
'image_library': ImageLibraryWidget(),
}
def clean_title(self): def clean_title(self):
"""Validate title length""" """Validate title length"""

View file

@ -4,9 +4,33 @@ Admin classes for cloud providers and consulting partners
from django.contrib import admin from django.contrib import admin
from django.utils.html import format_html from django.utils.html import format_html
from django import forms
from adminsortable2.admin import SortableAdminMixin from adminsortable2.admin import SortableAdminMixin
from ..models import CloudProvider, ConsultingPartner, ServiceOffering from ..models import CloudProvider, ConsultingPartner, ServiceOffering
from .widgets import ImageLibraryWidget
class CloudProviderAdminForm(forms.ModelForm):
"""Custom form for CloudProvider admin with image widget"""
class Meta:
model = CloudProvider
fields = "__all__"
widgets = {
'image_library': ImageLibraryWidget(),
}
class ConsultingPartnerAdminForm(forms.ModelForm):
"""Custom form for ConsultingPartner admin with image widget"""
class Meta:
model = ConsultingPartner
fields = "__all__"
widgets = {
'image_library': ImageLibraryWidget(),
}
class OfferingInline(admin.StackedInline): class OfferingInline(admin.StackedInline):
@ -34,6 +58,8 @@ class OfferingInline(admin.StackedInline):
class CloudProviderAdmin(SortableAdminMixin, admin.ModelAdmin): class CloudProviderAdmin(SortableAdminMixin, admin.ModelAdmin):
"""Admin configuration for CloudProvider model""" """Admin configuration for CloudProvider model"""
form = CloudProviderAdminForm
list_display = ( list_display = (
"name", "name",
"slug", "slug",
@ -77,6 +103,8 @@ class CloudProviderAdmin(SortableAdminMixin, admin.ModelAdmin):
class ConsultingPartnerAdmin(SortableAdminMixin, admin.ModelAdmin): class ConsultingPartnerAdmin(SortableAdminMixin, admin.ModelAdmin):
"""Admin configuration for ConsultingPartner model""" """Admin configuration for ConsultingPartner model"""
form = ConsultingPartnerAdminForm
list_display = ( list_display = (
"name", "name",
"website", "website",

View file

@ -4,6 +4,7 @@ Admin classes for services and service offerings
from django.contrib import admin from django.contrib import admin
from django.utils.html import format_html from django.utils.html import format_html
from django import forms
from ..models import ( from ..models import (
Service, Service,
@ -13,6 +14,18 @@ from ..models import (
Plan, Plan,
PlanPrice, PlanPrice,
) )
from .widgets import ImageLibraryWidget
class ServiceAdminForm(forms.ModelForm):
"""Custom form for Service admin with image widget"""
class Meta:
model = Service
fields = "__all__"
widgets = {
'image_library': ImageLibraryWidget(),
}
class ExternalLinkInline(admin.TabularInline): class ExternalLinkInline(admin.TabularInline):
@ -79,6 +92,8 @@ class OfferingInline(admin.StackedInline):
class ServiceAdmin(admin.ModelAdmin): class ServiceAdmin(admin.ModelAdmin):
"""Admin configuration for Service model""" """Admin configuration for Service model"""
form = ServiceAdminForm
list_display = ( list_display = (
"name", "name",
"logo_preview", "logo_preview",

View file

@ -0,0 +1,226 @@
"""
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",
)
}