Files
octopus/myproject/products/admin.py
Andrey Smakotin 8f3d247c3c refactor: Simplify admin photo preview - remove grid display
- Keep only single photo preview per inline (large version 800x800)
- Removed all_versions_preview display from photo inlines
- Cleaner, more focused admin interface
- Confirmed all sizes are stored correctly:
  * Large (800x800) verified
  * Medium (400x400) verified
  * Originals verified
- Use large_url() for preview in admin (best quality/size balance)
2025-10-22 16:44:37 +03:00

311 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.contrib import admin
from django.utils.html import format_html
import nested_admin
from .models import ProductCategory, ProductTag, Product, ProductKit, KitItem
from .models import ProductPhoto, ProductKitPhoto, ProductCategoryPhoto
from .models import ProductVariantGroup, KitItemPriority, SKUCounter
@admin.register(ProductVariantGroup)
class ProductVariantGroupAdmin(admin.ModelAdmin):
list_display = ['name', 'get_products_count', 'created_at']
search_fields = ['name', 'description']
list_filter = ['created_at']
readonly_fields = ['created_at', 'updated_at']
def get_products_count(self, obj):
return obj.products.count()
get_products_count.short_description = 'Товаров'
class ProductCategoryAdmin(admin.ModelAdmin):
list_display = ('photo_preview', 'name', 'sku', 'slug', 'parent', 'is_active')
list_filter = ('is_active', 'parent')
prepopulated_fields = {'slug': ('name',)}
search_fields = ('name', 'sku')
readonly_fields = ('photo_preview_large',)
def photo_preview(self, obj):
"""Превью фото в списке категорий"""
first_photo = obj.photos.first()
if first_photo and first_photo.image:
return format_html(
'<img src="{}" style="width: 50px; height: 50px; object-fit: cover; border-radius: 4px;" />',
first_photo.image.url
)
return "Нет фото"
photo_preview.short_description = "Фото"
def photo_preview_large(self, obj):
"""Большое превью фото в форме редактирования"""
first_photo = obj.photos.first()
if first_photo and first_photo.image:
return format_html(
'<img src="{}" style="max-width: 300px; max-height: 300px; border-radius: 8px;" />',
first_photo.image.url
)
return "Нет фото"
photo_preview_large.short_description = "Превью основного фото"
class ProductTagAdmin(admin.ModelAdmin):
list_display = ('name', 'slug')
prepopulated_fields = {'slug': ('name',)}
search_fields = ('name',)
class ProductAdmin(admin.ModelAdmin):
list_display = ('photo_preview', 'name', 'sku', 'get_categories_display', 'cost_price', 'sale_price', 'get_variant_groups_display', 'is_active')
list_filter = ('is_active', 'categories', 'tags', 'variant_groups')
search_fields = ('name', 'sku', 'description', 'search_keywords')
filter_horizontal = ('categories', 'tags', 'variant_groups')
readonly_fields = ('photo_preview_large',)
autocomplete_fields = []
fieldsets = (
('Основная информация', {
'fields': ('name', 'sku', 'variant_suffix', 'description', 'categories', 'unit')
}),
('Цены', {
'fields': ('cost_price', 'sale_price')
}),
('Дополнительно', {
'fields': ('tags', 'variant_groups', 'is_active')
}),
('Поиск', {
'fields': ('search_keywords',),
'classes': ('collapse',),
'description': 'Поле для улучшенного поиска. Автоматически генерируется при сохранении, но вы можете добавить дополнительные ключевые слова (синонимы, альтернативные названия и т.д.).'
}),
('Фото', {
'fields': ('photo_preview_large',),
'classes': ('collapse',),
}),
)
def get_categories_display(self, obj):
categories = obj.categories.all()[:3]
if not categories:
return "-"
result = ", ".join([cat.name for cat in categories])
if obj.categories.count() > 3:
result += f" (+{obj.categories.count() - 3})"
return result
get_categories_display.short_description = 'Категории'
def get_variant_groups_display(self, obj):
groups = obj.variant_groups.all()[:3]
if not groups:
return "-"
result = ", ".join([g.name for g in groups])
if obj.variant_groups.count() > 3:
result += f" (+{obj.variant_groups.count() - 3})"
return result
get_variant_groups_display.short_description = 'Группы вариантов'
def photo_preview(self, obj):
"""Превью фото в списке товаров"""
first_photo = obj.photos.first()
if first_photo and first_photo.image:
return format_html(
'<img src="{}" style="width: 50px; height: 50px; object-fit: cover; border-radius: 4px;" />',
first_photo.image.url
)
return "Нет фото"
photo_preview.short_description = "Фото"
def photo_preview_large(self, obj):
"""Большое превью фото в форме редактирования"""
first_photo = obj.photos.first()
if first_photo and first_photo.image:
return format_html(
'<img src="{}" style="max-width: 300px; max-height: 300px; border-radius: 8px;" />',
first_photo.image.url
)
return "Нет фото"
photo_preview_large.short_description = "Превью основного фото"
class ProductKitAdmin(admin.ModelAdmin):
list_display = ('photo_preview', 'name', 'slug', 'get_categories_display', 'pricing_method', 'is_active')
list_filter = ('is_active', 'pricing_method', 'categories', 'tags')
prepopulated_fields = {'slug': ('name',)}
filter_horizontal = ('categories', 'tags')
readonly_fields = ('photo_preview_large',)
def get_categories_display(self, obj):
categories = obj.categories.all()[:3]
if not categories:
return "-"
result = ", ".join([cat.name for cat in categories])
if obj.categories.count() > 3:
result += f" (+{obj.categories.count() - 3})"
return result
get_categories_display.short_description = 'Категории'
def photo_preview(self, obj):
"""Превью фото в списке комплектов"""
first_photo = obj.photos.first()
if first_photo and first_photo.image:
return format_html(
'<img src="{}" style="width: 50px; height: 50px; object-fit: cover; border-radius: 4px;" />',
first_photo.image.url
)
return "Нет фото"
photo_preview.short_description = "Фото"
def photo_preview_large(self, obj):
"""Большое превью фото в форме редактирования"""
first_photo = obj.photos.first()
if first_photo and first_photo.image:
return format_html(
'<img src="{}" style="max-width: 300px; max-height: 300px; border-radius: 8px;" />',
first_photo.image.url
)
return "Нет фото"
photo_preview_large.short_description = "Превью основного фото"
class KitItemPriorityInline(nested_admin.NestedTabularInline):
model = KitItemPriority
extra = 0 # Не показывать пустые формы
fields = ['product', 'priority']
autocomplete_fields = ['product']
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('product')
def formfield_for_foreignkey(self, db_field, request, **kwargs):
"""Показывать только товары из выбранной группы вариантов"""
if db_field.name == "product":
# Получаем kit_item из родительского объекта через request
# Это будет работать автоматически с nested_admin
pass
return super().formfield_for_foreignkey(db_field, request, **kwargs)
class KitItemInline(nested_admin.NestedStackedInline):
model = KitItem
extra = 0 # Не показывать пустые формы
fields = ['product', 'variant_group', 'quantity', 'notes']
autocomplete_fields = ['product']
inlines = [KitItemPriorityInline]
class Media:
css = {
'all': ('admin/css/custom_nested.css',)
}
class ProductPhotoInline(admin.TabularInline):
model = ProductPhoto
extra = 1
readonly_fields = ('image_preview',)
fields = ('image', 'image_preview', 'order')
def image_preview(self, obj):
"""Превью основного фото (большой размер 800×800)"""
if obj.image:
return format_html(
'<img src="{}" style="max-width: 250px; max-height: 250px; border-radius: 4px;" />',
obj.get_large_url()
)
return "Нет изображения"
image_preview.short_description = "Превью"
class ProductKitPhotoInline(nested_admin.NestedTabularInline):
model = ProductKitPhoto
extra = 0 # Не показывать пустые формы
readonly_fields = ('image_preview',)
fields = ('image', 'image_preview', 'order')
def image_preview(self, obj):
"""Превью основного фото (большой размер 800×800)"""
if obj.image:
return format_html(
'<img src="{}" style="max-width: 250px; max-height: 250px; border-radius: 4px;" />',
obj.get_large_url()
)
return "Нет изображения"
image_preview.short_description = "Превью"
class ProductCategoryPhotoInline(admin.TabularInline):
model = ProductCategoryPhoto
extra = 1
readonly_fields = ('image_preview',)
fields = ('image', 'image_preview', 'order')
def image_preview(self, obj):
"""Превью основного фото (большой размер 800×800)"""
if obj.image:
return format_html(
'<img src="{}" style="max-width: 250px; max-height: 250px; border-radius: 4px;" />',
obj.get_large_url()
)
return "Нет изображения"
image_preview.short_description = "Превью"
class ProductKitAdminWithItems(ProductKitAdmin):
inlines = [KitItemInline]
# Update admin classes to include photo inlines
class ProductAdminWithPhotos(ProductAdmin):
inlines = [ProductPhotoInline]
class ProductKitAdminWithItemsAndPhotos(nested_admin.NestedModelAdmin, ProductKitAdmin):
inlines = [KitItemInline, ProductKitPhotoInline]
class ProductCategoryAdminWithPhotos(ProductCategoryAdmin):
inlines = [ProductCategoryPhotoInline]
@admin.register(KitItem)
class KitItemAdmin(admin.ModelAdmin):
list_display = ['__str__', 'kit', 'get_type', 'quantity', 'has_priorities']
list_filter = ['kit']
list_select_related = ['kit', 'product', 'variant_group']
inlines = [KitItemPriorityInline]
fields = ['kit', 'product', 'variant_group', 'quantity', 'notes']
def get_type(self, obj):
if obj.variant_group:
return format_html('<span style="color: #0066cc;">Группа: {}</span>', obj.variant_group.name)
return f"Товар: {obj.product.name if obj.product else '-'}"
get_type.short_description = 'Тип'
def has_priorities(self, obj):
return obj.priorities.exists()
has_priorities.boolean = True
has_priorities.short_description = 'Приоритеты настроены'
@admin.register(SKUCounter)
class SKUCounterAdmin(admin.ModelAdmin):
list_display = ['counter_type', 'current_value', 'get_next_preview']
list_filter = ['counter_type']
readonly_fields = ['get_next_preview']
def get_next_preview(self, obj):
"""Показывает, каким будет следующий артикул"""
next_val = obj.current_value + 1
if obj.counter_type == 'product':
return format_html('<strong>PROD-{:06d}</strong>', next_val)
elif obj.counter_type == 'kit':
return format_html('<strong>KIT-{:06d}</strong>', next_val)
elif obj.counter_type == 'category':
return format_html('<strong>CAT-{:04d}</strong>', next_val)
return str(next_val)
get_next_preview.short_description = 'Следующий артикул'
def has_delete_permission(self, request, obj=None):
# Запрещаем удаление счетчиков
return False
admin.site.register(ProductCategory, ProductCategoryAdminWithPhotos)
admin.site.register(ProductTag, ProductTagAdmin)
admin.site.register(Product, ProductAdminWithPhotos)
admin.site.register(ProductKit, ProductKitAdminWithItemsAndPhotos)