/
lizzza
/
labaweb
Обзор
Документация
Войти
/
lizzza
/
labaweb
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
main/admin.py
838 строк
33 KB
ligzzza
все выполненные лабораторные
05 май 2026, 19:57
05 май 2026, 19:57
fa35051
Код
Авторство
О чём код?
from django.contrib import admin from django.utils.html import format_html from django.db.models import Count, Avg, Sum from django.utils import timezone from .forms import BookingAdminForm from .models import ( User, Category, MasterClass, Image, Booking, Review, Favorite, Notification, MasterClassSession ) from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import A4 from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from django.http import HttpResponse import io import os from simple_history.admin import SimpleHistoryAdmin from import_export.admin import ExportActionMixin from import_export import resources, fields from import_export.widgets import ForeignKeyWidget from django.utils.safestring import mark_safe # ============================================================ # 1. ПОЛЬЗОВАТЕЛЬ # ============================================================ class UserAdmin(admin.ModelAdmin): """Настройка отображения пользователей в админке""" # === list_display: какие колонки показывать в таблице === # short_description через @admin.display list_display = [ 'id', 'username', 'email', 'get_full_name', # стандартный метод 'get_role_with_icon', # кастомный метод с декоратором 'get_bookings_count', # кастомный метод 'is_active', ] # === list_display_links: какие колонки сделать ссылками на редактирование === list_display_links = ['id', 'username', 'email'] # === list_filter: фильтры справа === list_filter = [ 'role', # фильтр по роли 'is_active', # активен/заблокирован ] # === search_fields: по каким полям искать === search_fields = ['username', 'email', 'first_name', 'last_name', 'phone', 'organization_name'] # === filter_horizontal: удобный виджет для ManyToMany (если бы были) === # filter_horizontal = ['groups', 'user_permissions'] # === raw_id_fields: вместо выпадающего списка — поле с ID (для ForeignKey с большим кол-вом записей) === # Пока не применяем, но показываю как выглядит # raw_id_fields = ['organizer'] # если бы тут был ForeignKey # === readonly_fields: поля только для чтения при редактировании === readonly_fields = ['date_joined', 'last_login', 'get_bookings_count_display'] # === fieldsets: группировка полей на странице редактирования === fieldsets = ( ('Основная информация', { 'fields': ('username', 'email', 'password') }), ('Персональные данные', { 'fields': ('first_name', 'last_name', 'phone', 'avatar') }), ('Роль и статус', { 'fields': ('role', 'is_active', 'is_staff', 'is_superuser') }), ('Для организатора', { 'fields': ('organization_name', 'organization_description'), 'classes': ('collapse',) # сворачиваемый блок }), ('Статистика', { 'fields': ('date_joined', 'last_login', 'get_bookings_count_display'), }), ('Права доступа', { 'fields': ('groups', 'user_permissions'), 'classes': ('collapse',) }), ) # === Кастомный метод с декоратором @admin.display === @admin.display( description="Роль", ordering='role' # можно сортировать по этому полю ) def get_role_with_icon(self, obj): """Добавляем иконку к роли""" icons = { 'admin': '👑', 'organizer': '📋', 'participant': '👤' } return f"{icons.get(obj.role, '')} {obj.get_role_display()}" @admin.display( description="📊 Бронирований", ) def get_bookings_count(self, obj): """Количество бронирований пользователя""" count = obj.bookings.count() return count # Для readonly_fields нужен метод без декоратора, возвращающий строку def get_bookings_count_display(self, obj): return f"Всего бронирований: {obj.bookings.count()}" get_bookings_count_display.short_description = "Статистика бронирований" # ============================================================ # 2. INLINE ДЛЯ ИЗОБРАЖЕНИЙ (встраивается в MasterClass) # ============================================================ class ImageInline(admin.TabularInline): # TabularInline — табличный вид, StackedInline — блочный """Встроенный редактор изображений прямо на странице мастер-класса""" model = Image extra = 1 # количество пустых форм для добавления fields = ['image', 'is_main', 'uploaded_at'] readonly_fields = ['uploaded_at'] # === Кастомный метод для отображения превью === @admin.display(description="Превью") def image_preview(self, obj): if obj.image: return format_html('<img src="{}" width="50" height="50" style="object-fit: cover;" />', obj.image.url) return "Нет фото" # ============================================================ # 3. INLINE ДЛЯ БРОНИРОВАНИЙ (встраивается в MasterClass) # ============================================================ class BookingInline(admin.TabularInline): model = Booking extra = 0 # не показывать пустые формы fields = ['participant', 'participants_count', 'total_price', 'status', 'payment_status', 'created_at'] readonly_fields = ['total_price', 'created_at'] can_delete = False show_change_link = True # ссылка на полное редактирование # Не даем добавлять новые через inline (только просмотр) def has_add_permission(self, request, obj): return False # ============================================================ # 4. INLINE ДЛЯ ОТЗЫВОВ (встраивается в MasterClass) # ============================================================ class ReviewInline(admin.TabularInline): model = Review extra = 0 fields = ['author', 'rating', 'status', 'created_at'] readonly_fields = ['author', 'rating', 'created_at'] can_delete = False def has_add_permission(self, request, obj): return False class SessionInline(admin.TabularInline): model = MasterClassSession extra = 1 fields = ['start_datetime', 'end_datetime', 'max_participants', 'current_participants', 'status'] readonly_fields = ['current_participants'] verbose_name = "Сеанс" verbose_name_plural = "Сеансы" @admin.action(description="Сгенерировать PDF-отчёт") def generate_pdf_report(modeladmin, request, queryset): buffer = io.BytesIO() p = canvas.Canvas(buffer, pagesize=A4) width, height = A4 # Подключаем шрифт DejaVu (лежит в main/fonts/) font_path = os.path.join(os.path.dirname(__file__), 'fonts', 'DejaVuSans.ttf') if os.path.exists(font_path): pdfmetrics.registerFont(TTFont('DejaVu', font_path)) font_name = 'DejaVu' else: font_name = 'Helvetica' # Заголовок p.setFont(font_name, 18) p.drawString(50, height - 50, "Отчёт по мастер-классам") p.setFont(font_name, 11) p.drawString(50, height - 70, f"Дата: {timezone.now().strftime('%d.%m.%Y %H:%M')}") p.line(50, height - 80, width - 50, height - 80) # Заголовки таблицы y = height - 110 p.setFont(font_name, 12) p.drawString(50, y, "Название") p.drawString(250, y, "Город") p.drawString(370, y, "Цена") p.drawString(460, y, "Участники") y -= 25 p.setFont(font_name, 10) total_price = 0 total_participants = 0 for mk in queryset: if y < 60: p.showPage() y = height - 50 p.setFont(font_name, 10) title = mk.title[:25] + '...' if len(mk.title) > 25 else mk.title p.drawString(50, y, title) p.drawString(250, y, mk.city or '—') p.drawString(370, y, f"{mk.price} ₽") p.drawString(460, y, f"{mk.current_participants}/{mk.max_participants}") total_price += float(mk.price or 0) total_participants += mk.current_participants or 0 y -= 20 # Итоги y -= 30 p.line(50, y, width - 50, y) y -= 25 p.setFont(font_name, 12) p.drawString(50, y, f"Всего мастер-классов: {queryset.count()}") p.drawString(250, y, f"Стоимость: {total_price:.0f} ₽") p.drawString(460, y, f"Участников: {total_participants}") p.save() buffer.seek(0) response = HttpResponse(buffer, content_type='application/pdf') response['Content-Disposition'] = 'attachment; filename="masterclasses_report.pdf"' return response class MasterClassResource(resources.ModelResource): # Русские названия столбцов id = fields.Field(column_name='ID', attribute='id') title = fields.Field(column_name='Название', attribute='title') city = fields.Field(column_name='Город', attribute='city') price = fields.Field(column_name='Цена', attribute='price') status = fields.Field(column_name='Статус', attribute='status') organizer = fields.Field(column_name='Организатор', attribute='organizer__username') created_at = fields.Field(column_name='Дата создания', attribute='created_at') class Meta: model = MasterClass fields = ('id', 'title', 'city', 'price', 'status', 'organizer', 'created_at') export_order = ('id', 'title', 'city', 'price', 'status', 'organizer__username', 'created_at') def dehydrate_status(self, obj): """Переводим статус на русский""" return obj.get_status_display() def dehydrate_price(self, obj): """Цена как число, а не строка""" return float(obj.price) class BookingResource(resources.ModelResource): id = fields.Field(column_name='ID', attribute='id') participant = fields.Field(column_name='Участник', attribute='participant__username') masterclass = fields.Field(column_name='Мастер-класс', attribute='masterclass__title') total_price = fields.Field(column_name='Сумма', attribute='total_price') status = fields.Field(column_name='Статус', attribute='status') created_at = fields.Field(column_name='Дата создания', attribute='created_at') class Meta: model = Booking fields = ('id', 'participant', 'masterclass', 'total_price', 'status', 'created_at') export_order = ('id', 'participant__username', 'masterclass__title', 'total_price', 'status', 'created_at') def dehydrate_status(self, obj): return obj.get_status_display() def dehydrate_total_price(self, obj): return float(obj.total_price) # ============================================================ # 5. МАСТЕР-КЛАСС (главная модель с максимальными настройками) # ============================================================ class MasterClassAdmin(ExportActionMixin, SimpleHistoryAdmin): """Расширенная настройка админки для мастер-классов""" resource_class = MasterClassResource # === list_display с кастомными методами === list_display = [ 'id', 'title', 'category', 'organizer', 'format', 'participants_progress', # кастомный метод с прогрессом 'get_status_badge', # кастомный метод с цветным значком ] # === list_display_links === list_display_links = ['id', 'title'] # === list_filter: фильтры справа === list_filter = [ 'status', 'format', 'category', 'city', ('start_datetime', admin.DateFieldListFilter), # фильтр по дате с выбором "Сегодня/Неделя/Месяц" ] # === date_hierarchy: навигация по датам сверху (ДЛЯ ЭТОГО И НУЖНО) === date_hierarchy = 'start_datetime' # === search_fields === search_fields = ['title', 'description', 'city', 'address', 'organizer__username', 'organizer__email'] # === filter_horizontal: удобный выбор ManyToMany (если добавите теги потом) === # filter_horizontal = ['tags'] # === raw_id_fields: для ForeignKey с большим количеством записей === raw_id_fields = ['organizer'] # вместо выпадающего списка всех пользователей # === readonly_fields === readonly_fields = [ 'created_at', 'updated_at', 'current_participants', 'free_places_display', 'total_revenue_display', 'average_rating_display', 'show_history_diff', ] # === fieldsets: группировка полей === fieldsets = ( ('Основная информация', { 'fields': ('title', 'description', 'category', 'organizer', 'schedule_type', 'program_file') }), ('Место и формат', { 'fields': ('city', 'address', 'format', 'online_link') }), ('Цена и места', { 'fields': ('price', 'max_participants', 'current_participants', 'free_places_display') }), ('Даты', { 'fields': ('start_datetime', 'end_datetime') }), ('Модерация', { 'fields': ('status', 'moderation_comment'), 'classes': ('wide',) }), ('Статистика', { 'fields': ('total_revenue_display', 'average_rating_display', 'created_at', 'updated_at'), }), ('История изменений', { 'fields': ('show_history_diff',), 'classes': ('collapse',), # сворачиваемый блок }), ) # === inlines: встроенные редакторы === inlines = [ImageInline, SessionInline, BookingInline, ReviewInline] # === actions: массовые действия === actions = ['approve_masterclasses', 'reject_masterclasses', generate_pdf_report] #тк эта функция находится не в классе # ===== КАСТОМНЫЕ МЕТОДЫ ДЛЯ list_display ===== def price_display(self, obj): """Форматированный вывод цены""" return f"{obj.price:,.0f} ₽".replace(',', ' ') @admin.display( description="👥 Места", ) # ===== МЕТОД ДЛЯ ОТОБРАЖЕНИЯ ИСТОРИИ ===== def show_history_diff(self, obj): """Показывает изменения с разницей""" history = obj.history.all()[:5] if not history: return "Нет изменений" html = '<div style="font-size:0.9rem;">' for i, record in enumerate(history): html += f'<p style="margin:0.3rem 0;">' html += f'{record.history_date.strftime("%d.%m.%Y %H:%M")} — ' html += f'{record.history_user if record.history_user else "Система"} — ' if record.history_type == '+': html += 'Создан' elif record.history_type == '~': html += 'Изменён' elif record.history_type == '-': html += 'Удалён' if i < len(history) - 1 and record.history_type == '~': delta = record.diff_against(history[i + 1]) changes = [] for change in delta.changes: changes.append(f"{change.field}: {change.old} → {change.new}") if changes: html += f' — {", ".join(changes[:3])}' html += '</p>' html += '</div>' return mark_safe(html) show_history_diff.short_description = "История изменений" def history_status(self, obj): return obj.get_status_display() history_status.short_description = "Статус" def participants_progress(self, obj): """Визуальный прогресс-бар заполнения мест""" if obj.max_participants and obj.max_participants > 0: percent = int((obj.current_participants / obj.max_participants) * 100) else: percent = 0 if percent < 70: color = 'green' elif percent < 90: color = 'orange' else: color = 'red' return format_html( '<div style="width:100px; background:#eee; border-radius:10px;">' '<div style="width:{}%; background:{}; height:10px; border-radius:10px;"></div>' '</div>' '<small>{}/{} ({}%)</small>', str(percent), color, obj.current_participants, obj.max_participants, str(percent) ) @admin.display( description="📌 Статус", ordering='status' ) def get_status_badge(self, obj): """Цветной бейдж статуса""" colors = { 'pending': 'orange', 'approved': 'green', 'rejected': 'red', 'completed': 'blue', 'cancelled': 'gray', } return format_html( '<span style="background:{}; color:white; padding:4px 8px; border-radius:12px; font-size:12px;">{}</span>', colors.get(obj.status, 'gray'), obj.get_status_display() ) # ===== МЕТОДЫ ДЛЯ readonly_fields ===== def free_places_display(self, obj): if obj.pk is None: return "—" free = (obj.max_participants or 0) - (obj.current_participants or 0) if free > 0: return format_html('<span style="color:green; font-weight:bold;">{} мест</span>', str(free)) return format_html('<span style="color:red;">{}</span>', "Мест нет") def total_revenue_display(self, obj): if obj.pk is None: return "—" total = obj.bookings.filter(payment_status='paid').aggregate(Sum('total_price'))['total_price__sum'] or 0 return format_html('<span>{}</span>', f"{total:,.0f} ₽".replace(',', ' ')) total_revenue_display.short_description = "💰 Выручка" def average_rating_display(self, obj): if obj.pk is None: return "—" avg = obj.reviews.filter(status='approved').aggregate(Avg('rating'))['rating__avg'] if avg: stars = '★' * int(avg) + '☆' * (5 - int(avg)) return format_html('<span>{}</span>', f"{stars} ({avg:.1f})") return format_html('<span>{}</span>', "Нет оценок") average_rating_display.short_description = "⭐ Рейтинг" # ===== МАССОВЫЕ ДЕЙСТВИЯ (actions) ===== @admin.action(description="✅ Одобрить выбранные мастер-классы") def approve_masterclasses(self, request, queryset): updated = queryset.update(status='approved') self.message_user(request, f"Одобрено мастер-классов: {updated}") @admin.action(description="❌ Отклонить выбранные мастер-классы") def reject_masterclasses(self, request, queryset): updated = queryset.update(status='rejected') self.message_user(request, f"Отклонено мастер-классов: {updated}", level='WARNING') # ============================================================ # 6. КАТЕГОРИЯ # ============================================================ class CategoryAdmin(admin.ModelAdmin): list_display = ['id', 'name', 'slug', 'get_masterclasses_count', 'created_at'] list_display_links = ['id', 'name'] list_filter = ['created_at'] search_fields = ['name', 'description'] prepopulated_fields = {'slug': ('name',)} # автозаполнение slug из name readonly_fields = ['created_at', 'get_masterclasses_count_display'] fieldsets = ( (None, { 'fields': ('name', 'slug', 'description') }), ('Медиа', { 'fields': ('image',) }), ('Статистика', { 'fields': ('created_at', 'get_masterclasses_count_display') }), ) @admin.display( description="МК", ordering='masterclasses_count' ) def get_masterclasses_count(self, obj): # Аннотация для сортировки return obj.masterclasses.count() def get_masterclasses_count_display(self, obj): return f"Мастер-классов в категории: {obj.masterclasses.count()}" get_masterclasses_count_display.short_description = "Количество МК" # Переопределяем queryset для поддержки сортировки по количеству def get_queryset(self, request): return super().get_queryset(request).annotate( masterclasses_count=Count('masterclasses') ) # ============================================================ # 7. БРОНИРОВАНИЕ # ============================================================ class BookingAdmin(ExportActionMixin, SimpleHistoryAdmin): resource_class = BookingResource form = BookingAdminForm list_display = ['id', 'participant', 'masterclass', 'session'] list_display_links = ['id', 'participant'] list_filter = ['status', 'masterclass', 'session'] date_hierarchy = 'created_at' search_fields = ['participant__username', 'participant__email', 'masterclass__title'] raw_id_fields = ['participant', 'masterclass', 'session'] readonly_fields = ['total_price', 'created_at', 'show_history_diff'] fieldsets = ( ('Информация о бронировании', { 'fields': ('participant', 'masterclass','session', 'participants_count') }), ('Статусы', { 'fields': ('status', 'payment_status') }), ('Финансы', { 'fields': ('total_price',) }), ('Даты', { 'fields': ('created_at',) }), ('История изменений', { 'fields': ('show_history_diff',), 'classes': ('collapse',), }), ) def show_history_diff(self, obj): """Показывает последние 5 изменений с разницей""" history = obj.history.all()[:5] if not history: return "Нет изменений" html = '<div style="font-size:0.9rem;">' for i, record in enumerate(history): html += f'<p style="margin:0.3rem 0;">' html += f'{record.history_date.strftime("%d.%m.%Y %H:%M")} — ' html += f'{record.history_user if record.history_user else "Система"} — ' if record.history_type == '+': html += 'Создано' elif record.history_type == '~': html += 'Изменено' elif record.history_type == '-': html += 'Удалено' if i < len(history) - 1 and record.history_type == '~': delta = record.diff_against(history[i + 1]) changes = [] for change in delta.changes: changes.append(f"{change.field}: {change.old} → {change.new}") if changes: html += f' — {", ".join(changes[:3])}' html += '</p>' html += '</div>' return mark_safe(html) show_history_diff.short_description = "📋 История изменений" # ===== ОГРАНИЧИВАЕМ ВЫБОР СЕАНСОВ ===== def formfield_for_foreignkey(self, db_field, request, **kwargs): if db_field.name == "session": # Если это редактирование существующего бронирования object_id = request.resolver_match.kwargs.get('object_id') if object_id: try: booking = Booking.objects.get(pk=object_id) # Показываем только сеансы этого мастер-класса kwargs["queryset"] = MasterClassSession.objects.filter( masterclass=booking.masterclass ) except Booking.DoesNotExist: kwargs["queryset"] = MasterClassSession.objects.none() else: # Для новой записи — ПУСТОЙ список (нельзя выбрать сеанс без МК) kwargs["queryset"] = MasterClassSession.objects.none() return super().formfield_for_foreignkey(db_field, request, **kwargs) @admin.display(description="Дата проведения") def event_datetime(self, obj): if obj.session: return obj.session.start_datetime.strftime('%d.%m.%Y %H:%M') if obj.masterclass.start_datetime: return obj.masterclass.start_datetime.strftime('%d.%m.%Y %H:%M') return "—" def event_datetime_display(self, obj): if obj.session: return f"{obj.session.start_datetime.strftime('%d.%m.%Y %H:%M')}" if obj.masterclass.start_datetime: return f"{obj.masterclass.start_datetime.strftime('%d.%m.%Y %H:%M')}" return "Не указано" event_datetime_display.short_description = "Дата и время проведения" @admin.display(description="МК") def masterclass_link(self, obj): """Кликабельная ссылка на мастер-класс""" from django.urls import reverse url = reverse('admin:%s_%s_change' % (obj.masterclass._meta.app_label, obj.masterclass._meta.model_name), args=[obj.masterclass.id]) return format_html('<a href="{}">{}</a>', url, obj.masterclass.title) @admin.display(description="Сумма") def total_price_display(self, obj): return f"{obj.total_price:,.0f} ₽".replace(',', ' ') @admin.display(description="Статус") def status_badge(self, obj): colors = {'pending': 'gray', 'confirmed': 'green', 'cancelled': 'red', 'completed': 'blue'} return format_html( '<span style="color:{};">{}</span>', colors.get(obj.status, 'black'), obj.get_status_display() ) @admin.display(description="Оплата") def payment_status_badge(self, obj): colors = {'pending': 'orange', 'paid': 'green', 'refunded': 'purple'} return format_html( '<span style="color:{};">{}</span>', colors.get(obj.payment_status, 'black'), obj.get_payment_status_display() ) # ============================================================ # 8. ОТЗЫВ # ============================================================ class ReviewAdmin(admin.ModelAdmin): list_display = [ 'id', 'author', 'masterclass', 'rating_stars', 'text_preview', 'status_badge', ] list_display_links = ['id', 'author'] list_filter = ['status', 'rating', ('created_at', admin.DateFieldListFilter)] date_hierarchy = 'created_at' search_fields = ['author__username', 'masterclass__title', 'text'] raw_id_fields = ['author', 'masterclass', 'booking'] readonly_fields = ['created_at'] actions = ['approve_reviews', 'reject_reviews'] fieldsets = ( (None, { 'fields': ('author', 'masterclass', 'booking') }), ('Содержание', { 'fields': ('rating', 'text') }), ('Модерация', { 'fields': ('status', 'created_at') }), ) @admin.display(description="⭐ Оценка") def rating_stars(self, obj): return '★' * obj.rating + '☆' * (5 - obj.rating) @admin.display(description="Текст") def text_preview(self, obj): return obj.text[:50] + '...' if len(obj.text) > 50 else obj.text @admin.display(description="Статус") def status_badge(self, obj): colors = {'pending': 'orange', 'approved': 'green', 'rejected': 'red'} return format_html( '<span style="color:{};">{}</span>', colors.get(obj.status, 'black'), obj.get_status_display() ) @admin.action(description="✅ Одобрить выбранные отзывы") def approve_reviews(self, request, queryset): updated = queryset.update(status='approved') self.message_user(request, f"Одобрено отзывов: {updated}") @admin.action(description="❌ Отклонить выбранные отзывы") def reject_reviews(self, request, queryset): updated = queryset.update(status='rejected') self.message_user(request, f"Отклонено отзывов: {updated}", level='WARNING') # ============================================================ # 9. ИЗБРАННОЕ # ============================================================ class FavoriteAdmin(admin.ModelAdmin): list_display = ['id', 'user', 'masterclass', 'created_at'] list_display_links = ['id', 'user'] list_filter = ['created_at'] search_fields = ['user__username', 'masterclass__title'] raw_id_fields = ['user', 'masterclass'] readonly_fields = ['created_at'] # ============================================================ # 10. УВЕДОМЛЕНИЕ # ============================================================ class NotificationAdmin(admin.ModelAdmin): list_display = [ 'id', 'user', 'type_icon', 'title', 'is_read_badge', 'created_at', ] list_display_links = ['id', 'title'] list_filter = ['type', 'is_read', ('created_at', admin.DateFieldListFilter)] date_hierarchy = 'created_at' search_fields = ['user__username', 'title', 'message'] raw_id_fields = ['user'] readonly_fields = ['created_at'] actions = ['mark_as_read', 'mark_as_unread'] @admin.display(description="Тип") def type_icon(self, obj): icons = { 'booking_confirmed': '✅', 'booking_cancelled': '❌', 'moderation_result': '📋', 'new_booking': '🆕', 'reminder': '⏰', } return f"{icons.get(obj.type, '📌')} {obj.get_type_display()}" @admin.display(description="Прочитано") def is_read_badge(self, obj): if obj.is_read: return format_html('<span style="color:green;">✅ Да</span>') return format_html('<span style="color:red;">📌 Нет</span>') @admin.action(description="✅ Отметить как прочитанные") def mark_as_read(self, request, queryset): updated = queryset.update(is_read=True) self.message_user(request, f"Отмечено прочитанными: {updated}") @admin.action(description="📌 Отметить как непрочитанные") def mark_as_unread(self, request, queryset): updated = queryset.update(is_read=False) self.message_user(request, f"Отмечено непрочитанными: {updated}") class MasterClassSessionAdmin(admin.ModelAdmin): list_display = ['id', 'masterclass', 'start_datetime', 'end_datetime', 'max_participants', 'status'] list_filter = ['status', 'masterclass', 'start_datetime'] search_fields = ['masterclass__title'] date_hierarchy = 'start_datetime' ordering = ['start_datetime'] verbose_name = "Сеанс" verbose_name_plural = "Сеансы" # ============================================================ # РЕГИСТРАЦИЯ ВСЕХ МОДЕЛЕЙ # ============================================================ admin.site.register(User, UserAdmin) admin.site.register(Category, CategoryAdmin) admin.site.register(MasterClass, MasterClassAdmin) admin.site.register(Image) admin.site.register(Booking, BookingAdmin) admin.site.register(Review, ReviewAdmin) admin.site.register(Favorite, FavoriteAdmin) admin.site.register(Notification, NotificationAdmin) admin.site.register(MasterClassSession, MasterClassSessionAdmin)