# backend/calendar_app/admin.py from __future__ import annotations from typing import Iterable from django.contrib import admin, messages from django.db.models import QuerySet from django.http import HttpRequest from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from .models import CalendarEvent, Recurrence class ScopeListFilter(admin.SimpleListFilter): """Фильтр области события: компания или отдел. Values: - 'company' → department IS NULL - 'dept' → department IS NOT NULL """ title = _("Область") parameter_name = "scope" def lookups(self, request: HttpRequest, model_admin: admin.ModelAdmin) -> Iterable[tuple[str, str]]: """Возвращает варианты выбора фильтра. Args: request: Текущий запрос. model_admin: Админ-класс. Returns: Iterable[tuple[str, str]]: (value, label) """ return ( ("company", _("Компания")), ("dept", _("Отдел")), ) def queryset(self, request: HttpRequest, queryset): """Применяет фильтр к queryset. Args: request: Текущий запрос. queryset: Исходный queryset. Returns: QuerySet: Отфильтрованный queryset. """ val = self.value() if val == "company": return queryset.filter(department__isnull=True) if val == "dept": return queryset.filter(department__isnull=False) return queryset class HasTimeListFilter(admin.SimpleListFilter): """Фильтр наличия времени у события (есть оба time или нет ни одного).""" title = _("Есть время") parameter_name = "has_time" def lookups(self, request: HttpRequest, model_admin: admin.ModelAdmin) -> Iterable[tuple[str, str]]: """Опции фильтра (Да/Нет).""" return ( ("yes", _("Да")), ("no", _("Нет")), ) def queryset(self, request: HttpRequest, queryset): """Применяет фильтр к queryset.""" val = self.value() if val == "yes": return queryset.filter(start_time__isnull=False, end_time__isnull=False) if val == "no": return queryset.filter(start_time__isnull=True, end_time__isnull=True) return queryset @admin.register(CalendarEvent) class CalendarEventAdmin(admin.ModelAdmin): """Админка для модели CalendarEvent (события календаря компании и отделов).""" # ===== Список ===== list_display = ( "id", "title", "scope_display", "start_date", "end_date", "start_time", "end_time", "all_day", "recurrence", "color_swatch", "created_by", "created_at", ) list_select_related = ("department", "created_by") list_per_page = 50 ordering = ("department_id", "start_date", "start_time") # ===== Поиск/Фильтры/Иерархия ===== search_fields = ( "title", "description", "location", "department__name", "department__title", "department__code", ) list_filter = ( "department", "recurrence", "all_day", ("start_date", admin.DateFieldListFilter), ("created_at", admin.DateFieldListFilter), HasTimeListFilter, # <— ПЕРЕДАЁМ КЛАСС, НЕ СТРОКУ ScopeListFilter, # <— ПЕРЕДАЁМ КЛАСС, НЕ СТРОКУ ) date_hierarchy = "start_date" # ===== Поля формы ===== readonly_fields = ("created_at",) raw_id_fields = ("department", "created_by") fieldsets = ( (_("Основное"), { "fields": ("title", "description", "department", "location"), }), (_("Даты и время"), { "fields": ( ("start_date", "end_date"), ("start_time", "end_time"), "all_day", ), }), (_("Отображение"), { "fields": ("recurrence", "color"), }), (_("Служебное"), { "fields": ("created_by", "created_at"), }), ) # ===== Экшены ===== actions = ("make_all_day", "clear_time", "set_annual", "set_one_time") @admin.display(description=_("Область"), ordering="department_id") def scope_display(self, obj: CalendarEvent) -> str: """Возвращает 'Компания' или название отдела для строки списка. Args: obj: Объект CalendarEvent. Returns: str: Человекочитаемое имя области. """ return _("Компания") if obj.department_id is None else str(obj.department) @admin.display(description=_("Цвет")) def color_swatch(self, obj: CalendarEvent) -> str: """Отрисовывает mini-swatch цвета + hex-код. Args: obj: Объект CalendarEvent. Returns: str: HTML. """ if not obj.color: return "—" return format_html( '{}', obj.color, obj.color ) # ===== Экшены логика ===== @admin.action(description=_("Сделать целодневными (очистить время)")) def make_all_day(self, request: HttpRequest, queryset: QuerySet[CalendarEvent]) -> None: """Делает выбранные события целодневными. Args: request: Запрос администратора. queryset: Выбранные события. """ updated = queryset.update(start_time=None, end_time=None, all_day=True) self.message_user(request, _(f"Обновлено событий: {updated}"), level=messages.SUCCESS) @admin.action(description=_("Очистить время (оставить даты как есть)")) def clear_time(self, request: HttpRequest, queryset: QuerySet[CalendarEvent]) -> None: """Очищает время начала/окончания у выбранных событий. Args: request: Запрос администратора. queryset: Выбранные события. """ updated = queryset.update(start_time=None, end_time=None) self.message_user(request, _(f"Очищено время у событий: {updated}"), level=messages.SUCCESS) @admin.action(description=_("Пометить как ежегодные")) def set_annual(self, request: HttpRequest, queryset: QuerySet[CalendarEvent]) -> None: """Ставит повторяемость 'Ежегодно' у выбранных событий.""" updated = queryset.update(recurrence=Recurrence.ANNUAL) self.message_user(request, _(f"Обновлено: {updated}"), level=messages.SUCCESS) @admin.action(description=_("Пометить как одноразовые")) def set_one_time(self, request: HttpRequest, queryset: QuerySet[CalendarEvent]) -> None: """Ставит повторяемость 'Одноразовое' у выбранных событий.""" updated = queryset.update(recurrence=Recurrence.ONE_TIME) self.message_user(request, _(f"Обновлено: {updated}"), level=messages.SUCCESS) # ===== Сохранение ===== def save_model(self, request: HttpRequest, obj: CalendarEvent, form, change: bool) -> None: """Проставляет автора при первом сохранении. Args: request: Текущий запрос. obj: Объект события. form: Форма админки. change: True, если редактирование; False, если создание. """ if not change and obj.created_by_id is None: obj.created_by = request.user super().save_model(request, obj, form, change)