""" Административная панель для интерактивной доски. """ from django.contrib import admin from django.utils.html import format_html from django.urls import reverse from .models import Board, BoardElement, BoardSnapshot @admin.register(Board) class BoardAdmin(admin.ModelAdmin): """Админ интерфейс для досок.""" list_display = [ 'title', 'owner_link', 'access_type_badge', 'elements_count', 'snapshot_stats', 'views_count', 'is_active', 'is_template', 'last_edited_at', 'created_at' ] list_filter = [ 'access_type', 'is_active', 'is_template', 'created_at', 'last_edited_at' ] search_fields = [ 'title', 'description', 'board_id', 'owner__email', 'owner__first_name', 'owner__last_name' ] readonly_fields = [ 'board_id', 'views_count', 'elements_count', 'snapshot_stats', 'snapshot_preview', 'last_edited_by', 'last_edited_at', 'created_at', 'updated_at' ] filter_horizontal = ['participants'] fieldsets = ( ('Основная информация', { 'fields': ( 'board_id', 'title', 'description', 'owner', 'mentor', 'student' ) }), ('Доступ', { 'fields': ( 'access_type', 'participants', 'is_active' ) }), ('Настройки', { 'fields': ( 'background_color', 'grid_enabled', 'width', 'height', 'is_template' ) }), ('Статистика', { 'fields': ( 'views_count', 'elements_count', 'snapshot_stats', 'last_edited_by', 'last_edited_at' ) }), ('Данные доски (Excalidraw Snapshot)', { 'fields': ( 'snapshot_preview', ), 'classes': ('collapse',) }), ('Временные метки', { 'fields': ( 'created_at', 'updated_at' ) }) ) actions = ['make_template', 'make_public', 'make_private'] def owner_link(self, obj): """Ссылка на владельца.""" url = reverse('admin:users_user_change', args=[obj.owner.id]) return format_html('{}', url, obj.owner.get_full_name()) owner_link.short_description = 'Владелец' def access_type_badge(self, obj): """Бейдж типа доступа.""" colors = { 'private': '#6c757d', 'mentor_student': '#17a2b8', 'public': '#28a745' } return format_html( '{}', colors.get(obj.access_type, '#000'), obj.get_access_type_display() ) access_type_badge.short_description = 'Доступ' @admin.action(description='Сделать шаблоном') def make_template(self, request, queryset): """Сделать доски шаблонами.""" queryset.update(is_template=True) @admin.action(description='Сделать публичными') def make_public(self, request, queryset): """Сделать доски публичными.""" queryset.update(access_type='public') @admin.action(description='Сделать приватными') def make_private(self, request, queryset): """Сделать доски приватными.""" queryset.update(access_type='private') def snapshot_stats(self, obj): """Статистика по snapshot.""" if not obj.tldraw_snapshot: return format_html('Нет данных') files_count = obj.get_files_count() elements_count = obj.get_elements_count_from_snapshot() return format_html( '
' '' '📝 Элементы: {}' '' '' '🖼️ Файлы: {}' '' '
', elements_count, files_count ) snapshot_stats.short_description = 'Статистика Snapshot' def snapshot_preview(self, obj): """Предпросмотр структуры snapshot.""" if not obj.tldraw_snapshot: return format_html('

Нет данных snapshot

') import json snapshot = obj.tldraw_snapshot elements = snapshot.get('elements', []) files = snapshot.get('files', {}) app_state = snapshot.get('appState', {}) # Форматируем JSON для отображения try: formatted_json = json.dumps(snapshot, ensure_ascii=False, indent=2) except: formatted_json = str(snapshot) # Статистика files_count = len(files) if isinstance(files, dict) else 0 elements_count = len(elements) if isinstance(elements, list) else 0 # Размер данных snapshot_size = len(json.dumps(snapshot, ensure_ascii=False)) size_kb = snapshot_size / 1024 return format_html( '
' '

📊 Структура данных доски (Excalidraw Snapshot)

' '
' 'Элементы: {} | ' 'Файлы: {} | ' 'Размер: {:.2f} KB' '
' '
' '📄 Показать полную структуру JSON' '
{}
' '
' '
', elements_count, files_count, size_kb, formatted_json ) snapshot_preview.short_description = 'Структура данных доски' @admin.register(BoardElement) class BoardElementAdmin(admin.ModelAdmin): """Админ интерфейс для элементов доски.""" list_display = [ 'id', 'board_link', 'element_type_badge', 'position', 'size', 'created_by_link', 'locked', 'is_deleted', 'created_at' ] list_filter = [ 'element_type', 'locked', 'is_deleted', 'created_at' ] search_fields = [ 'board__title', 'content', 'created_by__email' ] readonly_fields = [ 'created_by', 'locked_by', 'created_at', 'updated_at', 'deleted_at' ] fieldsets = ( ('Основная информация', { 'fields': ( 'board', 'element_type', 'created_by' ) }), ('Позиция и размер', { 'fields': ( 'x', 'y', 'width', 'height', 'rotation', 'z_index' ) }), ('Текст', { 'fields': ( 'content', 'font_size', 'font_family', 'font_weight', 'text_align', 'text_color' ), 'classes': ('collapse',) }), ('Фигура', { 'fields': ( 'shape_type', 'fill_color', 'stroke_color', 'stroke_width', 'opacity' ), 'classes': ('collapse',) }), ('Изображение/Рисунок', { 'fields': ( 'image_url', 'drawing_data' ), 'classes': ('collapse',) }), ('Стрелка', { 'fields': ( 'arrow_start_element', 'arrow_end_element' ), 'classes': ('collapse',) }), ('Блокировка', { 'fields': ( 'locked', 'locked_by' ) }), ('Удаление', { 'fields': ( 'is_deleted', 'deleted_at' ) }), ('Временные метки', { 'fields': ( 'created_at', 'updated_at' ) }) ) def board_link(self, obj): """Ссылка на доску.""" url = reverse('admin:board_board_change', args=[obj.board.id]) return format_html('{}', url, obj.board.title) board_link.short_description = 'Доска' def element_type_badge(self, obj): """Бейдж типа элемента.""" colors = { 'text': '#007bff', 'shape': '#28a745', 'image': '#ffc107', 'drawing': '#17a2b8', 'sticky': '#fd7e14', 'arrow': '#6c757d', 'line': '#343a40' } return format_html( '{}', colors.get(obj.element_type, '#000'), obj.get_element_type_display() ) element_type_badge.short_description = 'Тип' def position(self, obj): """Позиция элемента.""" return f"({obj.x:.0f}, {obj.y:.0f})" position.short_description = 'Позиция' def size(self, obj): """Размер элемента.""" return f"{obj.width:.0f}x{obj.height:.0f}" size.short_description = 'Размер' def created_by_link(self, obj): """Ссылка на автора.""" if obj.created_by: url = reverse('admin:users_user_change', args=[obj.created_by.id]) return format_html('{}', url, obj.created_by.get_full_name()) return '-' created_by_link.short_description = 'Автор' @admin.register(BoardSnapshot) class BoardSnapshotAdmin(admin.ModelAdmin): """Админ интерфейс для снимков досок.""" list_display = [ 'id', 'board_link', 'created_by_link', 'description', 'created_at' ] list_filter = [ 'created_at' ] search_fields = [ 'board__title', 'description', 'created_by__email' ] readonly_fields = [ 'board', 'snapshot_data', 'created_by', 'created_at' ] fieldsets = ( ('Основная информация', { 'fields': ( 'board', 'created_by', 'description' ) }), ('Данные', { 'fields': ( 'snapshot_data', ) }), ('Временные метки', { 'fields': ( 'created_at', ) }) ) def board_link(self, obj): """Ссылка на доску.""" url = reverse('admin:board_board_change', args=[obj.board.id]) return format_html('{}', url, obj.board.title) board_link.short_description = 'Доска' def created_by_link(self, obj): """Ссылка на автора.""" if obj.created_by: url = reverse('admin:users_user_change', args=[obj.created_by.id]) return format_html('{}', url, obj.created_by.get_full_name()) return '-' created_by_link.short_description = 'Автор'