429 lines
13 KiB
Python
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 = 'Автор'
|