"""
Административная панель для интерактивной доски.
"""
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 = 'Автор'