uchill/backend/apps/board/admin.py

429 lines
13 KiB
Python

"""
Административная панель для интерактивной доски.
"""
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('<a href="{}">{}</a>', 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(
'<span style="background-color: {}; color: white; padding: 3px 10px; border-radius: 3px;">{}</span>',
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('<span style="color: #999;">Нет данных</span>')
files_count = obj.get_files_count()
elements_count = obj.get_elements_count_from_snapshot()
return format_html(
'<div style="display: flex; gap: 10px;">'
'<span style="background-color: #17a2b8; color: white; padding: 3px 8px; border-radius: 3px;">'
'📝 Элементы: <strong>{}</strong>'
'</span>'
'<span style="background-color: #28a745; color: white; padding: 3px 8px; border-radius: 3px;">'
'🖼️ Файлы: <strong>{}</strong>'
'</span>'
'</div>',
elements_count,
files_count
)
snapshot_stats.short_description = 'Статистика Snapshot'
def snapshot_preview(self, obj):
"""Предпросмотр структуры snapshot."""
if not obj.tldraw_snapshot:
return format_html('<p style="color: #999;">Нет данных snapshot</p>')
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(
'<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; border: 1px solid #dee2e6;">'
'<h4 style="margin-top: 0;">📊 Структура данных доски (Excalidraw Snapshot)</h4>'
'<div style="margin-bottom: 15px;">'
'<strong>Элементы:</strong> {} | '
'<strong>Файлы:</strong> {} | '
'<strong>Размер:</strong> {:.2f} KB'
'</div>'
'<details style="margin-top: 10px;">'
'<summary style="cursor: pointer; color: #007bff; font-weight: bold;">📄 Показать полную структуру JSON</summary>'
'<pre style="background: white; padding: 10px; border: 1px solid #ddd; border-radius: 3px; overflow-x: auto; max-height: 500px; overflow-y: auto; margin-top: 10px; font-size: 11px;">{}</pre>'
'</details>'
'</div>',
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('<a href="{}">{}</a>', 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(
'<span style="background-color: {}; color: white; padding: 3px 10px; border-radius: 3px;">{}</span>',
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('<a href="{}">{}</a>', 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('<a href="{}">{}</a>', 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('<a href="{}">{}</a>', url, obj.created_by.get_full_name())
return '-'
created_by_link.short_description = 'Автор'