dashboard: кастомизируемый дашборд из виджетов + шаблоны по умолчанию

Дашборд теперь собирается из виджетов (приветствие, метрики, мои задачи,
новости, быстрый доступ). Сотрудник настраивает состав и порядок под себя
(вкл/выкл + перемещение ↑↓), а для тех, кто не настраивал, действует набор
по умолчанию по типу локации (Офис/Аптека/Склад), задаётся в админке.

- core/dashboard.py: реестр виджетов + сборка раскладки
- core/models/dashboard.py: DashboardLayout (персональная), DashboardDefaults
  (синглтон по типу локации)
- core/views.py: dashboard_view через реестр + dashboard_customize_view
- templates/pages/dashboard_widgets/*: виджеты-партиалы; dashboard.html —
  цикл по виджетам; dashboard_customize.html — настройка (HTMX + ↑↓)
- admin: DashboardDefaults (чекбоксами по локации) + read-only раскладки;
  пункт меню «Дашборд по умолчанию»
- core/tests_dashboard.py: 11 тестов (логика раскладки + view)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 18:42:21 +03:00
parent b2f0c2a1e2
commit d07b9c6e97
16 changed files with 804 additions and 201 deletions
+71 -2
View File
@@ -11,7 +11,11 @@ from unfold.contrib.filters.admin import DropdownFilter
from treenode.forms import TreeNodeForm
from django.forms import TextInput, Textarea
from django.db.models import Count
from core.models import DirectoryType, Directory, ConnectorConfig, AccessSystem
from django import forms
from core.models import (
DirectoryType, Directory, ConnectorConfig, AccessSystem,
DashboardDefaults, DashboardLayout,
)
from import_export.admin import ImportExportModelAdmin
from unfold.contrib.import_export.forms import ExportForm, ImportForm
@@ -289,4 +293,69 @@ class ConnectorConfigAdmin(ModelAdmin):
color, obj.get_connector_type_display()
)
connector_type_display.short_description = _('Коннектор')
connector_type_display.admin_order_field = 'connector_type'
connector_type_display.admin_order_field = 'connector_type'
class DashboardDefaultsForm(forms.ModelForm):
"""Наборы виджетов дашборда по умолчанию — чекбоксами по типу локации."""
class Meta:
model = DashboardDefaults
fields = ('office', 'pharmacy', 'warehouse')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
from core.dashboard import DASHBOARD_WIDGETS, valid_keys
choices = [(w.key, f'{w.label} — {w.description}') for w in DASHBOARD_WIDGETS]
for field in ('office', 'pharmacy', 'warehouse'):
self.fields[field] = forms.MultipleChoiceField(
label=self._meta.model._meta.get_field(field).verbose_name,
required=False, choices=choices, widget=forms.CheckboxSelectMultiple,
)
if self.instance and self.instance.pk:
self.initial[field] = valid_keys(getattr(self.instance, field) or [])
def clean(self):
from core.dashboard import DEFAULT_WIDGET_KEYS
cleaned = super().clean()
for field in ('office', 'pharmacy', 'warehouse'):
selected = set(cleaned.get(field) or [])
# сохраняем в порядке реестра виджетов
cleaned[field] = [k for k in DEFAULT_WIDGET_KEYS if k in selected]
return cleaned
@admin.register(DashboardDefaults)
class DashboardDefaultsAdmin(ModelAdmin):
form = DashboardDefaultsForm
fieldsets = (
(_('Виджеты дашборда по типу локации'), {
'fields': ('office', 'pharmacy', 'warehouse'),
'description': _(
'Набор виджетов, который видит сотрудник, ещё не настроивший '
'свой дашборд. Порядок — как в списке ниже; пользователь может '
'переопределить состав и порядок под себя.'
),
}),
)
def has_add_permission(self, request):
return not DashboardDefaults.objects.exists()
def has_delete_permission(self, request, obj=None):
return False
@admin.register(DashboardLayout)
class DashboardLayoutAdmin(ModelAdmin):
"""Персональные раскладки (read-only; удалить = сбросить к дефолту)."""
list_display = ('employee', 'widgets_display', 'updated_at')
search_fields = ('employee__username', 'employee__last_name', 'employee__first_name')
readonly_fields = ('employee', 'widgets', 'updated_at')
def has_add_permission(self, request):
return False
def widgets_display(self, obj):
return ', '.join(obj.widgets or []) or '—'
widgets_display.short_description = _('Виджеты')