igor_nadein da7bf54c6c feat: Реализация полной функциональности опросов в чатах
- Исправлен PollSerializer для создания опросов с опциями
- Добавлено поле read_only_fields для корректной валидации
- Реализована отправка WebSocket уведомлений при создании опроса
- Исправлен импорт serialize_message из communications.serialization
- Добавлен обработчик poll_update в UserConsumer для WebSocket событий
- Исправлен эндпоинт /results/ для возврата user_voted_option_ids
- Добавлено поле voters в PollOptionSerializer для списка проголосовавших
- Реализована инициализация poll виджетов после рендера сообщений
- Добавлено игнорирование poll_update в notification-manager
- Исправлены стили для request list badges (height, word-break, flex)
- Восстановлены стили team-wheel (.wheel-focus, .scroll-arc) из git истории
- Реализовано отображение forwarded message info в messageRendererV2
- Множественные улучшения UI и UX для опросов

Fixes: создание опросов, голосование, отображение результатов, список проголосовавших
2026-01-22 01:33:42 +03:00

173 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# backend/requests_app/admin.py
from django import forms
from django.contrib import admin, messages
from django.db import models
from django.utils.translation import gettext_lazy as _
from .enums import RequestStatus
from .models import Request, RequestComment
# Админ-форма: без принуждения вводить approver (мы выставим его автоматически)
class RequestAdminForm(forms.ModelForm):
class Meta:
model = Request
fields = "__all__"
def clean(self):
cleaned = super().clean()
status = cleaned.get("status")
approver = cleaned.get("approver")
# Если статус "Отменено" — согласующий должен быть пустым
if status == RequestStatus.CANCELLED and approver:
cleaned["approver"] = None
# Для approved/rejected НЕ требуем approver — его поставим в save_model.
return cleaned
@admin.register(Request)
class RequestAdmin(admin.ModelAdmin):
form = RequestAdminForm
list_display = (
"id",
"display_title",
"type",
"status",
"employee",
"department",
"date_from",
"date_to",
"decided_at",
"created_at",
)
list_filter = (
"type",
"status",
"department",
("created_at", admin.DateFieldListFilter),
("decided_at", admin.DateFieldListFilter),
)
search_fields = (
"title",
"comment",
"employee__last_name",
"employee__first_name",
"employee__email",
)
autocomplete_fields = ("employee", "department")
# approver только для просмотра (покажем, кто согласовал)
readonly_fields = ("approver", "created_at", "updated_at", "decided_at")
fieldsets = (
(
_("Основное"),
{
"fields": (
"employee",
"department",
"type",
"title",
("date_from", "date_to"),
"comment",
"attachment",
)
},
),
(
_("Статус"),
{
"fields": (
"status",
# показываем, но не редактируем
"approver",
"decided_at",
)
},
),
(
_("Служебное"),
{
"classes": ("collapse",),
"fields": ("created_at", "updated_at"),
},
),
)
# HTML5-пикеры дат/времени в админке
formfield_overrides = {
models.DateField: {"widget": forms.DateInput(attrs={"type": "date"})},
models.DateTimeField: {
"widget": forms.DateTimeInput(attrs={"type": "datetime-local"})
},
}
def save_model(self, request, obj: Request, form, change):
"""
Приводим пару статус/согласующий к валидному виду ДО сохранения:
- approved/rejected -> approver = текущий пользователь, если не задан
- cancelled -> approver = None
Это гарантирует отсутствие конфликтов с БД-constraint.
"""
if obj.status in {RequestStatus.APPROVED, RequestStatus.REJECTED}:
if obj.approver_id is None:
obj.approver = request.user
elif obj.status == RequestStatus.CANCELLED:
if obj.approver_id is not None:
obj.approver = None
super().save_model(request, obj, form, change)
# Массовые действия работают через методы модели (ставят approver и decided_at)
@admin.action(description=_("Одобрить выбранные"))
def action_approve(self, request, queryset):
updated = 0
for obj in queryset:
if obj.status != RequestStatus.APPROVED:
obj.approve(request.user)
updated += 1
self.message_user(
request, _(f"Одобрено заявок: {updated}"), level=messages.SUCCESS
)
@admin.action(description=_("Отклонить выбранные"))
def action_reject(self, request, queryset):
updated = 0
for obj in queryset:
if obj.status != RequestStatus.REJECTED:
obj.reject(request.user)
updated += 1
self.message_user(
request, _(f"Отклонено заявок: {updated}"), level=messages.SUCCESS
)
@admin.action(description=_("Отменить выбранные"))
def action_cancel(self, request, queryset):
updated = 0
for obj in queryset:
if obj.status != RequestStatus.CANCELLED:
obj.cancel()
updated += 1
self.message_user(
request, _(f"Отменено заявок: {updated}"), level=messages.SUCCESS
)
actions = ("action_approve", "action_reject", "action_cancel")
@admin.register(RequestComment)
class RequestCommentAdmin(admin.ModelAdmin):
list_display = ("id", "request", "author", "short_text", "created_at")
list_select_related = ("request", "author")
search_fields = ("text", "request__title", "request__employee__last_name")
autocomplete_fields = ("request", "author")
date_hierarchy = "created_at"
def short_text(self, obj):
s = (obj.text or "").strip()
return (s[:80] + "…") if len(s) > 80 else s
short_text.short_description = _("Комментарий")