nice image selection from library
This commit is contained in:
parent
7319709749
commit
7e46dc71ec
5 changed files with 274 additions and 1 deletions
|
@ -8,4 +8,5 @@ from .images import *
|
|||
from .leads import *
|
||||
from .pricing import *
|
||||
from .providers import *
|
||||
from .widgets import *
|
||||
from .services import *
|
||||
|
|
|
@ -7,8 +7,8 @@ from django.utils.html import format_html
|
|||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
from ..models import Article
|
||||
from .widgets import ImageLibraryWidget
|
||||
|
||||
|
||||
class ArticleAdminForm(forms.ModelForm):
|
||||
|
@ -17,6 +17,9 @@ class ArticleAdminForm(forms.ModelForm):
|
|||
class Meta:
|
||||
model = Article
|
||||
fields = "__all__"
|
||||
widgets = {
|
||||
'image_library': ImageLibraryWidget(),
|
||||
}
|
||||
|
||||
def clean_title(self):
|
||||
"""Validate title length"""
|
||||
|
|
|
@ -4,9 +4,33 @@ Admin classes for cloud providers and consulting partners
|
|||
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django import forms
|
||||
from adminsortable2.admin import SortableAdminMixin
|
||||
|
||||
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):
|
||||
|
@ -34,6 +58,8 @@ class OfferingInline(admin.StackedInline):
|
|||
class CloudProviderAdmin(SortableAdminMixin, admin.ModelAdmin):
|
||||
"""Admin configuration for CloudProvider model"""
|
||||
|
||||
form = CloudProviderAdminForm
|
||||
|
||||
list_display = (
|
||||
"name",
|
||||
"slug",
|
||||
|
@ -77,6 +103,8 @@ class CloudProviderAdmin(SortableAdminMixin, admin.ModelAdmin):
|
|||
class ConsultingPartnerAdmin(SortableAdminMixin, admin.ModelAdmin):
|
||||
"""Admin configuration for ConsultingPartner model"""
|
||||
|
||||
form = ConsultingPartnerAdminForm
|
||||
|
||||
list_display = (
|
||||
"name",
|
||||
"website",
|
||||
|
|
|
@ -4,6 +4,7 @@ Admin classes for services and service offerings
|
|||
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django import forms
|
||||
|
||||
from ..models import (
|
||||
Service,
|
||||
|
@ -13,6 +14,18 @@ from ..models import (
|
|||
Plan,
|
||||
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):
|
||||
|
@ -79,6 +92,8 @@ class OfferingInline(admin.StackedInline):
|
|||
class ServiceAdmin(admin.ModelAdmin):
|
||||
"""Admin configuration for Service model"""
|
||||
|
||||
form = ServiceAdminForm
|
||||
|
||||
list_display = (
|
||||
"name",
|
||||
"logo_preview",
|
||||
|
|
226
hub/services/admin/widgets.py
Normal file
226
hub/services/admin/widgets.py
Normal 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",
|
||||
)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue