655 lines
21 KiB
Python
655 lines
21 KiB
Python
from django.contrib import admin
|
||
from django import forms
|
||
from django.urls import reverse
|
||
from django.utils.safestring import mark_safe
|
||
from django.utils.html import format_html
|
||
|
||
from .models import *
|
||
|
||
|
||
class HeadphonesAdminForm(forms.ModelForm):
|
||
class Meta:
|
||
model = Headphones
|
||
fields = '__all__'
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
if self.instance and self.instance.pk:
|
||
# Скрываем/показываем поля в зависимости от типа подключения
|
||
if self.instance.connection_type == 'wired':
|
||
self.fields.pop('wireless_details', None)
|
||
self.fields.pop('hybrid_details', None)
|
||
elif self.instance.connection_type == 'wireless':
|
||
self.fields.pop('wired_details', None)
|
||
self.fields.pop('hybrid_details', None)
|
||
elif self.instance.connection_type == 'hybrid':
|
||
self.fields.pop('wired_details', None)
|
||
self.fields.pop('wireless_details', None)
|
||
|
||
|
||
class HeadphonesConnectorInline(admin.TabularInline):
|
||
model = HeadphonesConnector
|
||
extra = 1
|
||
fields = ('connector', 'is_primary', 'notes')
|
||
fk_name = 'wired_headphones'
|
||
|
||
@admin.register(WiredHeadphones)
|
||
|
||
class WiredHeadphonesAdmin(admin.ModelAdmin):
|
||
inlines = [HeadphonesConnectorInline]
|
||
list_display = ('headphones', 'cable_connection_type', 'cable_length')
|
||
list_filter = ('headphones__brand', 'cable_connection_type')
|
||
search_fields = ('headphones__model',)
|
||
|
||
def get_queryset(self, request):
|
||
return super().get_queryset(request).select_related('headphones', 'cable_connection_type')
|
||
|
||
|
||
class WiredHeadphonesInline(admin.StackedInline):
|
||
model = WiredHeadphones
|
||
extra = 0
|
||
fieldsets = [
|
||
(None, {
|
||
'fields': [
|
||
'cable_connection_type',
|
||
'cable_length',
|
||
'custom_connector_name'
|
||
]
|
||
})
|
||
]
|
||
|
||
|
||
|
||
|
||
class WirelessHeadphonesInline(admin.StackedInline):
|
||
model = WirelessHeadphones
|
||
extra = 0
|
||
fieldsets = [
|
||
(None, {
|
||
'fields': [
|
||
('wireless_technology', 'bluetooth_version', 'chip'),
|
||
('battery_life', 'charging_time'),
|
||
('has_charging_case', 'charging_case_battery_life', 'charging_case_time'),
|
||
'charging_interface',
|
||
'supported_codecs'
|
||
]
|
||
})
|
||
]
|
||
|
||
|
||
class HybridHeadphonesInline(admin.StackedInline):
|
||
model = HybridHeadphones
|
||
extra = 0
|
||
fieldsets = [
|
||
('Беспроводная часть', {
|
||
'fields': [
|
||
('wireless_technology', 'bluetooth_version'),
|
||
('battery_life', 'charging_time'),
|
||
'charging_interface',
|
||
('has_charging_case', 'charging_case_battery_life'),
|
||
'supported_codecs'
|
||
]
|
||
}),
|
||
('Проводная часть', {
|
||
'fields': [
|
||
('cable_connection_type', 'connector_to_device'),
|
||
'cable_length'
|
||
]
|
||
}),
|
||
('Режимы работы', {
|
||
'fields': [
|
||
'auto_switch_mode',
|
||
'simultaneous_connection'
|
||
]
|
||
})
|
||
]
|
||
|
||
|
||
class HeadphonesReviewInline(admin.StackedInline):
|
||
model = HeadphonesReview
|
||
extra = 1
|
||
fields = ['resource', 'author', 'language', 'title', 'url', 'date']
|
||
ordering = ['-date']
|
||
autocomplete_fields = ['resource', 'author', 'language']
|
||
|
||
|
||
class HeadphonesImagesInline(admin.TabularInline):
|
||
model = HeadphonesImages
|
||
extra = 1
|
||
fields = ('photo', 'thumbnail_preview', 'admin_image_info', 'caption', 'is_main')
|
||
readonly_fields = ('thumbnail_preview', 'admin_image_info')
|
||
|
||
def thumbnail_preview(self, obj):
|
||
if obj.photo:
|
||
return format_html('<img src="{}" style="max-height: 100px;"/>', obj.photo.url)
|
||
return "-"
|
||
|
||
def admin_image_info(self, obj):
|
||
return obj.image_info
|
||
|
||
admin_image_info.short_description = "Информация о изображении"
|
||
admin_image_info.allow_tags = True
|
||
|
||
thumbnail_preview.short_description = "Превью"
|
||
|
||
|
||
@admin.register(CaseMaterial)
|
||
class CaseMaterialAdmin(admin.ModelAdmin):
|
||
list_display = ('name', 'slug')
|
||
search_fields = ('name',)
|
||
prepopulated_fields = {'slug': ('name',)}
|
||
list_per_page = 20
|
||
|
||
|
||
class HeadphonesDriverInline(admin.TabularInline):
|
||
model = HeadphonesDriver
|
||
extra = 1
|
||
fields = ('driver', 'count')
|
||
autocomplete_fields = ['driver']
|
||
|
||
|
||
# class HeadphonesColorInline(admin.TabularInline):
|
||
# model = Headphones.colors.through # Используем автоматическую промежуточную модель
|
||
# extra = 1
|
||
# # autocomplete_fields = ['casecolors'] # Убедитесь, что это правильное имя related_name
|
||
# verbose_name = "Доступный цвет"
|
||
# verbose_name_plural = "Доступные цвета"
|
||
|
||
|
||
class CaseColorsInline(admin.TabularInline):
|
||
model = CaseColors
|
||
extra = 1
|
||
fields = ('name_ru', 'name_en', 'image', 'image_preview', 'slug')
|
||
readonly_fields = ('image_preview', 'slug')
|
||
|
||
def image_preview(self, obj):
|
||
if obj.image:
|
||
return format_html('<img src="{}" width="50" height="50" />', obj.image.url)
|
||
return "Нет изображения"
|
||
image_preview.short_description = "Превью"
|
||
|
||
def get_formset(self, request, obj=None, **kwargs):
|
||
formset = super().get_formset(request, obj, **kwargs)
|
||
# Устанавливаем начальное значение для slug, если объект новый
|
||
if obj and not obj.pk:
|
||
formset.form.base_fields['slug'].initial = f"{obj.model}_"
|
||
return formset
|
||
|
||
|
||
class FrequencyResponseInline(admin.StackedInline):
|
||
model = FrequencyResponse
|
||
extra = 1
|
||
fields = ('file', 'author', 'source_link', 'is_primary', 'notes', 'file_preview')
|
||
readonly_fields = ('file_preview',)
|
||
|
||
def file_preview(self, obj):
|
||
if obj.file:
|
||
return format_html(
|
||
'<a href="{}" target="_blank">{}</a>',
|
||
obj.file.url,
|
||
os.path.basename(obj.file.name)
|
||
)
|
||
return "-"
|
||
file_preview.short_description = "Файл"
|
||
|
||
|
||
@admin.register(Headphones)
|
||
class HeadphonesAdmin(admin.ModelAdmin):
|
||
form = HeadphonesAdminForm
|
||
list_display = (
|
||
'model', 'brand', 'connection_type', 'get_reviews_count', 'published')
|
||
list_filter = ('brand', 'connection_type', 'published', 'case_type')
|
||
search_fields = ('model', 'brand__name', 'description')
|
||
prepopulated_fields = {'slug': ('brand', 'model')}
|
||
filter_horizontal = ('tags',)
|
||
list_editable = ('published',)
|
||
actions = ['make_published', 'make_unpublished']
|
||
readonly_fields = (
|
||
'get_reviews_count_display',
|
||
'date_added',
|
||
'driver_configuration_display'
|
||
)
|
||
autocomplete_fields = ['case_material', 'noise_cancellation']
|
||
|
||
fieldsets = [
|
||
('Основная информация', {
|
||
'fields': [
|
||
('brand', 'model'),
|
||
'slug',
|
||
'description',
|
||
('connection_type', 'case_type', 'case_material'),
|
||
('headphone_purpose', 'official_price'),
|
||
'tags', # Добавляем теги в основную секцию
|
||
]
|
||
}),
|
||
('Технические характеристики', {
|
||
'fields': [
|
||
('impedance', 'sensitivity'),
|
||
'frequency_range',
|
||
# ('frequency_response_chart', 'frequency_response_chart_author', 'frequency_response_chart_link'),
|
||
('microphone', 'noise_cancellation'),
|
||
'ip_rating',
|
||
]
|
||
}),
|
||
('Внешний вид', {
|
||
'fields': [
|
||
# 'colors',
|
||
('weight', 'release_year')
|
||
]
|
||
}),
|
||
('Метаданные', {
|
||
'fields': [
|
||
('date_added', 'published')
|
||
],
|
||
# 'classes': ('collapse',)
|
||
}),
|
||
]
|
||
|
||
inlines = [HeadphonesDriverInline, HeadphonesImagesInline, CaseColorsInline, FrequencyResponseInline]
|
||
|
||
# Методы для отображения
|
||
def get_reviews_count(self, obj):
|
||
return obj.reviews.count()
|
||
|
||
get_reviews_count.short_description = 'Кол-во обзоров'
|
||
|
||
def get_reviews_count_display(self, obj):
|
||
count = self.get_reviews_count(obj)
|
||
url = reverse('admin:headphones_headphonesreview_changelist') + f'?headphones__id__exact={obj.id}'
|
||
return format_html('<a href="{}">{} обзоров</a>', url, count)
|
||
|
||
get_reviews_count_display.short_description = 'Обзоры'
|
||
|
||
def driver_configuration_display(self, obj):
|
||
drivers = obj.headphonesdriver_set.select_related('driver__driver_type').all()
|
||
return format_html("<br>".join(
|
||
f"{d.driver.driver_type.name} {d.driver.size}mm × {d.count}"
|
||
for d in drivers
|
||
)) if drivers.exists() else "Не указано"
|
||
|
||
driver_configuration_display.short_description = 'Конфигурация драйверов'
|
||
|
||
def get_inline_instances(self, request, obj=None):
|
||
# inlines = [
|
||
# HeadphonesDriverInline(self.model, self.admin_site),
|
||
# HeadphonesImagesInline(self.model, self.admin_site)
|
||
# ]
|
||
inlines = super().get_inline_instances(request, obj)
|
||
|
||
if obj:
|
||
if obj.connection_type == 'wired':
|
||
inlines.append(WiredHeadphonesInline(self.model, self.admin_site))
|
||
elif obj.connection_type == 'wireless':
|
||
inlines.append(WirelessHeadphonesInline(self.model, self.admin_site))
|
||
elif obj.connection_type == 'hybrid':
|
||
inlines.append(HybridHeadphonesInline(self.model, self.admin_site))
|
||
|
||
inlines.append(HeadphonesReviewInline(self.model, self.admin_site))
|
||
return inlines
|
||
|
||
|
||
# Регистрация остальных моделей
|
||
@admin.register(Brand)
|
||
class BrandAdmin(admin.ModelAdmin):
|
||
list_display = ('name', 'country', 'year_founded', 'parent_company')
|
||
list_filter = ('country',)
|
||
search_fields = ('name',)
|
||
prepopulated_fields = {'slug': ('name',)}
|
||
|
||
|
||
class DriverInline(admin.TabularInline):
|
||
model = Driver
|
||
extra = 1
|
||
fields = ('driver_type', 'driver_model', 'size', 'frequency_range')
|
||
autocomplete_fields = ['driver_type', 'driver_model']
|
||
|
||
|
||
@admin.register(DriverModel)
|
||
class DriverModelAdmin(admin.ModelAdmin):
|
||
list_display = ('name', 'slug')
|
||
search_fields = ('name',)
|
||
prepopulated_fields = {'slug': ('name',)}
|
||
inlines = [DriverInline]
|
||
|
||
|
||
@admin.register(Driver)
|
||
class DriverAdmin(admin.ModelAdmin):
|
||
list_display = ('driver_type', 'driver_model', 'size', 'frequency_range')
|
||
list_filter = ('driver_type',)
|
||
search_fields = ('driver_type__name', 'driver_model__name', 'size', 'frequency_range') # Добавлено
|
||
autocomplete_fields = ['driver_type', 'driver_model']
|
||
|
||
def get_queryset(self, request):
|
||
return super().get_queryset(request).select_related('driver_type')
|
||
|
||
|
||
# ======================
|
||
# Регистрация справочников
|
||
# ======================
|
||
|
||
@admin.register(BrandCountry)
|
||
class BrandCountryAdmin(admin.ModelAdmin):
|
||
list_display = ('name', 'slug')
|
||
search_fields = ('name',)
|
||
prepopulated_fields = {'slug': ('name',)}
|
||
|
||
|
||
@admin.register(CaseColors)
|
||
class CaseColorsAdmin(admin.ModelAdmin):
|
||
list_display = ('name_ru', 'name_en', 'headphones', 'image_preview', 'slug')
|
||
list_filter = ('headphones__brand',)
|
||
search_fields = ('name_ru', 'name_en', 'headphones__model')
|
||
readonly_fields = ('image_preview', 'slug')
|
||
list_select_related = ('headphones', 'headphones__brand')
|
||
|
||
fieldsets = [
|
||
('Основная информация', {
|
||
'fields': ['headphones', 'name_ru', 'name_en', 'slug']
|
||
}),
|
||
('Изображение', {
|
||
'fields': ['image', 'image_preview']
|
||
})
|
||
]
|
||
|
||
def get_queryset(self, request):
|
||
return super().get_queryset(request).select_related('headphones__brand')
|
||
|
||
def save_model(self, request, obj, form, change):
|
||
if not obj.slug:
|
||
obj.slug = slugify(f"{obj.headphones.model}_{obj.name_en}")
|
||
super().save_model(request, obj, form, change)
|
||
|
||
|
||
@admin.register(CaseType)
|
||
class CaseTypeAdmin(admin.ModelAdmin):
|
||
list_display = ('name', 'slug')
|
||
search_fields = ('name',)
|
||
prepopulated_fields = {'slug': ('name',)}
|
||
|
||
|
||
@admin.register(HeadphonePurpose)
|
||
class HeadphonePurposeAdmin(admin.ModelAdmin):
|
||
list_display = ('name', 'slug')
|
||
search_fields = ('name',)
|
||
prepopulated_fields = {'slug': ('name',)}
|
||
|
||
|
||
@admin.register(TagCategory)
|
||
class TagCategoryAdmin(admin.ModelAdmin):
|
||
list_display = ('name', 'slug')
|
||
search_fields = ('name',)
|
||
prepopulated_fields = {'slug': ('name',)}
|
||
|
||
|
||
@admin.register(Tags)
|
||
class TagsAdmin(admin.ModelAdmin):
|
||
list_display = ('name', 'category', 'slug')
|
||
list_filter = ('category',)
|
||
search_fields = ('name', 'category__name')
|
||
prepopulated_fields = {'slug': ('name',)}
|
||
|
||
|
||
# ======================
|
||
# Регистрация аудио компонентов
|
||
# ======================
|
||
|
||
@admin.register(DriverType)
|
||
class DriverTypeAdmin(admin.ModelAdmin):
|
||
list_display = ('name', 'slug', 'encoding')
|
||
search_fields = ('name', 'encoding')
|
||
prepopulated_fields = {'slug': ('name',)}
|
||
|
||
|
||
@admin.register(NoiseCancellationType)
|
||
class NoiseCancellationTypeAdmin(admin.ModelAdmin):
|
||
list_display = ('name', 'slug')
|
||
search_fields = ('name',)
|
||
prepopulated_fields = {'slug': ('name',)}
|
||
|
||
|
||
@admin.register(HeadphonesMicrophoneType)
|
||
class HeadphonesMicrophoneTypeAdmin(admin.ModelAdmin):
|
||
list_display = ('type', 'description')
|
||
search_fields = ('type',)
|
||
prepopulated_fields = {'type': ('description',)}
|
||
|
||
|
||
@admin.register(HeadphonesCaseIP)
|
||
class HeadphonesCaseIPAdmin(admin.ModelAdmin):
|
||
list_display = ('code', 'description')
|
||
search_fields = ('code',)
|
||
prepopulated_fields = {'code': ('description',)}
|
||
|
||
|
||
@admin.register(CodecType)
|
||
class CodecTypeAdmin(admin.ModelAdmin):
|
||
list_display = ('name',)
|
||
search_fields = ('name',)
|
||
|
||
|
||
@admin.register(AudioCodec)
|
||
class AudioCodecAdmin(admin.ModelAdmin):
|
||
list_display = ('name', 'codec_type', 'max_bitrate', 'latency')
|
||
list_filter = ('codec_type',)
|
||
search_fields = ('name', 'description')
|
||
filter_horizontal = ()
|
||
|
||
|
||
# ======================
|
||
# Регистрация разъемов и подключений
|
||
# ======================
|
||
@admin.register(CableConnectionType)
|
||
class CableConnectionTypeAdmin(admin.ModelAdmin):
|
||
list_display = ('name', 'is_detachable')
|
||
search_fields = ('name', 'description')
|
||
readonly_fields = ('image_preview',)
|
||
|
||
def image_preview(self, obj):
|
||
if obj.image:
|
||
return mark_safe(f'<img src="{obj.image.url}" width="150" />')
|
||
return "Нет изображения"
|
||
|
||
image_preview.short_description = 'Превью'
|
||
|
||
|
||
@admin.register(DeviceConnectorType)
|
||
class DeviceConnectorTypeAdmin(admin.ModelAdmin):
|
||
list_display = ('name',)
|
||
search_fields = ('name', 'description')
|
||
|
||
|
||
@admin.register(WirelessTechnology)
|
||
class WirelessTechnologyAdmin(admin.ModelAdmin):
|
||
list_display = ('name',)
|
||
search_fields = ('name', 'description')
|
||
|
||
|
||
@admin.register(WirelessHeadphonesChip)
|
||
class WWirelessHeadphonesChipAdmin(admin.ModelAdmin):
|
||
list_display = ('name',)
|
||
search_fields = ('name', 'description')
|
||
|
||
|
||
@admin.register(ChargingInterface)
|
||
class ChargingInterfaceAdmin(admin.ModelAdmin):
|
||
list_display = ('name',)
|
||
search_fields = ('name', 'description')
|
||
|
||
|
||
# ======================
|
||
# Регистрация отзывов и медиа
|
||
# ======================
|
||
|
||
|
||
@admin.register(HeadphonesReviewType)
|
||
class HeadphonesReviewTypeAdmin(admin.ModelAdmin):
|
||
list_display = ('name', 'slug')
|
||
search_fields = ('name',)
|
||
prepopulated_fields = {'slug': ('name',)}
|
||
|
||
|
||
@admin.register(HeadphonesReviewResource)
|
||
class HeadphonesReviewResourceAdmin(admin.ModelAdmin):
|
||
list_display = ('name', 'review_type', 'website_link', 'slug')
|
||
list_filter = ('type',)
|
||
search_fields = ('name', 'type__name')
|
||
|
||
def website_link(self, obj):
|
||
return format_html('<a href="{}" target="_blank">{}</a>', obj.url, obj.url)
|
||
|
||
website_link.short_description = "Ссылка на ресурс"
|
||
|
||
def review_type(self, obj):
|
||
return obj.type.name if obj.type else '-'
|
||
|
||
review_type.short_description = 'Тип обзора'
|
||
review_type.admin_order_field = 'type__name'
|
||
|
||
|
||
@admin.register(HeadphonesConnector)
|
||
class HeadphonesConnectorAdmin(admin.ModelAdmin):
|
||
list_display = ('headphones', 'connector_display', 'is_primary', 'notes')
|
||
list_filter = ('is_primary', 'connector')
|
||
|
||
def connector_display(self, obj):
|
||
return str(obj.connector)
|
||
|
||
connector_display.short_description = 'Разъем'
|
||
connector_display.admin_order_field = 'connector__name'
|
||
|
||
|
||
@admin.register(HeadphonesReviewLanguage)
|
||
class HeadphonesReviewLanguageAdmin(admin.ModelAdmin):
|
||
list_display = ('language',)
|
||
search_fields = ('language',)
|
||
|
||
|
||
@admin.register(HeadphonesReviewAuthor)
|
||
class HeadphonesReviewAuthorAdmin(admin.ModelAdmin):
|
||
list_display = ('name', 'nickname', 'reviews_count')
|
||
search_fields = ('name', 'nickname')
|
||
filter_horizontal = ('resources',)
|
||
|
||
def reviews_count(self, obj):
|
||
return obj.headphonesreview_set.count()
|
||
|
||
reviews_count.short_description = 'Кол-во обзоров'
|
||
|
||
# Добавьте инлайн для отображения обзоров автора
|
||
class ReviewsInline(admin.TabularInline):
|
||
model = HeadphonesReview
|
||
extra = 0
|
||
fields = ('title', 'headphones', 'date')
|
||
readonly_fields = ('title', 'headphones', 'date')
|
||
|
||
inlines = [ReviewsInline]
|
||
|
||
|
||
class HeadphonesReviewAuthorInline(admin.StackedInline):
|
||
model = HeadphonesReviewAuthor
|
||
extra = 1
|
||
fields = ('name', 'nickname', 'resources')
|
||
filter_horizontal = ('resources',)
|
||
|
||
|
||
@admin.register(HeadphonesReview)
|
||
class HeadphonesReviewAdmin(admin.ModelAdmin):
|
||
list_display = (
|
||
'title',
|
||
'headphones_link',
|
||
'resource_link',
|
||
'date',
|
||
# 'author_display',
|
||
'review_link'
|
||
)
|
||
list_filter = (
|
||
'resource__type',
|
||
'headphones__brand',
|
||
'date'
|
||
)
|
||
search_fields = (
|
||
'title',
|
||
'headphones__model',
|
||
# 'author',
|
||
'resource__name'
|
||
)
|
||
raw_id_fields = ('headphones', 'resource')
|
||
date_hierarchy = 'date'
|
||
list_select_related = ('headphones', 'resource')
|
||
|
||
def headphones_link(self, obj):
|
||
if obj.headphones:
|
||
url = reverse('admin:headphones_headphones_change', args=[obj.headphones.id])
|
||
return format_html('<a href="{}">{}</a>', url, obj.headphones.model)
|
||
return "-"
|
||
|
||
headphones_link.short_description = "Наушники"
|
||
headphones_link.admin_order_field = 'headphones__model'
|
||
|
||
def resource_link(self, obj):
|
||
url = reverse('admin:headphones_headphonesreviewresource_change', args=[obj.resource.id])
|
||
return format_html('<a href="{}">{}</a>', url, obj.resource.name)
|
||
|
||
resource_link.short_description = "Ресурс"
|
||
resource_link.admin_order_field = 'resource__name'
|
||
|
||
def review_link(self, obj):
|
||
return format_html('<a href="{}" target="_blank">Открыть</a>', obj.url)
|
||
|
||
review_link.short_description = "Ссылка"
|
||
|
||
def author_display(self, obj):
|
||
if obj.author:
|
||
url = reverse('admin:headphones_headphonesreviewauthor_change', args=[obj.author.id])
|
||
return format_html('<a href="{}">{}</a>', url, obj.author.name)
|
||
return "-"
|
||
|
||
author_display.short_description = "Автор"
|
||
|
||
|
||
@admin.register(HeadphonesImages)
|
||
class HeadphonesImagesAdmin(admin.ModelAdmin):
|
||
list_display = ('product', 'caption')
|
||
list_filter = ('product__brand',)
|
||
search_fields = ('product__model', 'caption')
|
||
readonly_fields = ('image_preview',)
|
||
|
||
def image_preview(self, obj):
|
||
if obj.photo:
|
||
return mark_safe(f'<img src="{obj.photo.url}" width="150" />')
|
||
return "Нет изображения"
|
||
|
||
image_preview.short_description = 'Превью'
|
||
|
||
|
||
# ======================
|
||
# Регистрация вспомогательных моделей
|
||
# ======================
|
||
|
||
@admin.register(HeadphonesDriver)
|
||
class HeadphonesDriverAdmin(admin.ModelAdmin):
|
||
list_display = ('headphones', 'driver_type', 'size', 'count')
|
||
list_filter = ('driver__driver_type',)
|
||
|
||
def driver_type(self, obj):
|
||
return obj.driver.driver_type.name
|
||
|
||
driver_type.short_description = 'Тип драйвера'
|
||
|
||
def size(self, obj):
|
||
return f"{obj.driver.size}mm" if obj.driver.size else '-'
|
||
|
||
size.short_description = 'Размер'
|
||
|
||
|
||
@admin.register(Comment)
|
||
class CommentAdmin(admin.ModelAdmin):
|
||
list_display = ('user', 'post', 'date', 'short_body')
|
||
list_filter = ('date', 'user')
|
||
# search_fields = ('body', 'post__model', 'user__username')
|
||
readonly_fields = ('date',)
|
||
|
||
def short_body(self, obj):
|
||
return obj.body[:50] + '...' if len(obj.body) > 50 else obj.body
|
||
|
||
short_body.short_description = 'Текст'
|