Untitled

4ae4d avatar
4ae4d
plain_text
02/09/2026 10:59 AM
17.3 KB
8
Indexable
--- src/bot/core/handlers/events_ui.py ---
from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

from dialog_bot_sdk.interactive_media import Button, InteractiveMediaGroup, MediaGroupBuilder

from core.config import bot
from core.handlers.events_pages import EVENTS_PAGE_SIZE, fetch_events_page
from core.markups.event import (
    event_actions_keyboard,
    events_select_keyboard,
    format_event_details,
    my_events_pagination_keyboard,
    user_events_pagination_keyboard,
)
from core.markups.inline import all_events_pagination_keyboard
from core.markups.points import points_events_pagination_keyboard, points_menu_keyboard
from core.schemas.event import EventSchema
from core.schemas.user import UserSchema
from core.services.event import EventService
from core.services.report import ReportService

DEFAULT_CITY_UUID = "be9da3ba-a0e4-42f9-9ae2-9875c8625083"  # SPB
ADMIN_CAN_MANAGE_OTHERS = True  # иммет ли администратор доступ ко всем мероприятиям?


@dataclass(frozen=True)
class EventUIContext:
    name: str
    # list
    list_open_media_id: str
    pagination_builder: Callable[..., list[InteractiveMediaGroup]]
    extra_groups_builder: Callable[[], list[InteractiveMediaGroup]] | None
    # card
    back_value: str
    back_label: str
    enter_code_media_id: str
    sign_up_media_id: str
    sign_out_media_id: str


_UI: dict[str, EventUIContext] = {
    # дом волонтёра -> мероприятия
    "events": EventUIContext(
        name="events",
        list_open_media_id="user_events_open",
        pagination_builder=user_events_pagination_keyboard,
        extra_groups_builder=None,
        back_value="events",
        back_label="⬅️ К списку мероприятий",
        enter_code_media_id="event_user_enter_code",
        sign_up_media_id="event_user_sign_up",
        sign_out_media_id="event_user_sign_out",
    ),
    # дом волонтёра -> посмотреть по uuid
    "volunteer_home": EventUIContext(
        name="volunteer_home",
        list_open_media_id="user_events_open",  # не используется в этом контексте
        pagination_builder=user_events_pagination_keyboard,
        extra_groups_builder=None,
        back_value="volunteer_home",
        back_label="⬅️ В дом волонтёра",
        enter_code_media_id="event_user_enter_code",
        sign_up_media_id="event_user_sign_up",
        sign_out_media_id="event_user_sign_out",
    ),
    # дом модерации -> все мероприятия
    "moderation": EventUIContext(
        name="moderation",
        list_open_media_id="all_events_open",
        pagination_builder=all_events_pagination_keyboard,
        extra_groups_builder=None,
        back_value="all_events",
        back_label="⬅️ К списку мероприятий",
        enter_code_media_id="event_menu_enter_code",
        sign_up_media_id="event_menu_sign_up",
        sign_out_media_id="event_menu_sign_out",
    ),
    # меню баллов -> мероприятия с начисленными баллами
    "points": EventUIContext(
        name="points",
        list_open_media_id="points_event_open",
        pagination_builder=points_events_pagination_keyboard,
        extra_groups_builder=points_menu_keyboard,  # лидерборд + назад
        back_value="points",
        back_label="⬅️ К баллам",
        enter_code_media_id="event_user_enter_code",
        sign_up_media_id="event_user_sign_up",
        sign_out_media_id="event_user_sign_out",
    ),
    # дом модерации -> посмотреть по uuid
    "moderation_menu": EventUIContext(
        name="moderation_menu",
        list_open_media_id="all_events_open",  # не используется в этом контексте
        pagination_builder=all_events_pagination_keyboard,
        extra_groups_builder=None,
        back_value="moderation",
        back_label="⬅️ В меню модерации",
        enter_code_media_id="event_menu_enter_code",
        sign_up_media_id="event_menu_sign_up",
        sign_out_media_id="event_menu_sign_out",
    ),
    # дом модерации -> мои мероприятия
    "my_events": EventUIContext(
        name="my_events",
        list_open_media_id="my_events_open",
        pagination_builder=my_events_pagination_keyboard,
        extra_groups_builder=None,
        back_value="my_events",
        back_label="⬅️ К моим мероприятиям",
        enter_code_media_id="event_menu_enter_code",
        sign_up_media_id="event_menu_sign_up",
        sign_out_media_id="event_menu_sign_out",
    ),
}


def get_ui(ctx_name: str) -> EventUIContext:
    return _UI.get(ctx_name, _UI["events"])


def build_back_keyboard(ctx_name: str) -> list[InteractiveMediaGroup]:
    ui = get_ui(ctx_name)
    return MediaGroupBuilder(
        [Button(media_id="leave", value=ui.back_value, label=ui.back_label)]
    ).build()


def split_event_value(raw: Any) -> tuple[str, str | None]:
    """
    ожидаем:
      - "<event_id>"
      - "<event_id>|<ctx>"
    """
    s = str(raw or "").strip()
    if "|" in s:
        ev_id, ctx = s.split("|", 1)
        return ev_id.strip(), (ctx.strip() or None)
    return s, None


def can_manage_event(ev: EventSchema, user: UserSchema | None) -> bool:
    if getattr(user, "role", None) == "admin" and ADMIN_CAN_MANAGE_OTHERS:
        return True
    uid = getattr(user, "id", None)
    if not uid:
        return False
    return bool(getattr(ev, "creator_id", None)) and (str(ev.creator_id) == str(uid))


def can_view_code(ev: EventSchema, user: UserSchema | None) -> bool:
    # кто может управлять - тот видит код
    return can_manage_event(ev, user)


def _send_events_list_screen(
    peer,
    *,
    ui: EventUIContext,
    offset: int,
    limit: int,
    page_events: list[EventSchema],
    total: int | None,
    has_prev: bool,
    has_next: bool,
    note: str | None = None,
    header: str | None = None,
    empty_text: str | None = None,
    empty_groups: list[InteractiveMediaGroup] | None = None,
):
    prefix = f"{note}\n\n" if note else ""
    header_part = header or ""

    if not page_events:
        text = (
            prefix
            + header_part
            + (
                empty_text
                or "⚠️ Мероприятий не найдено.\n\nПопробуй изменить критерии или вернитесь назад."
            )
        )
        im: list[InteractiveMediaGroup] = []
        footer: list[InteractiveMediaGroup] = []
        if empty_groups is not None:
            footer = empty_groups
        elif ui.extra_groups_builder:
            footer = ui.extra_groups_builder()
        im += ui.pagination_builder(
            offset=offset, limit=limit, has_prev=has_prev, has_next=has_next
        )
        im += footer
        bot.messaging.send_message(peer=peer, text=text, interactive_media_groups=im)
        return

    total_str = str(total) if total is not None else "?"
    text = (
        prefix
        + header_part
        + (
            "📅 Мероприятия\n"
            f"Показаны: {offset + 1} — {offset + len(page_events)} из {total_str}\n\n"
            "Выбери мероприятие:"
        )
    )

    im: list[InteractiveMediaGroup] = []
    im += events_select_keyboard(
        page_events, open_media_id=ui.list_open_media_id, start_index=offset + 1, per_row=1
    )

    im += ui.pagination_builder(offset=offset, limit=limit, has_prev=has_prev, has_next=has_next)

    if ui.extra_groups_builder:
        im += ui.extra_groups_builder()

    bot.messaging.send_message(peer=peer, text=text, interactive_media_groups=im)


def send_city_events_page(
    peer,
    *,
    offset: int,
    event_service: EventService,
    ctx_name: str,
    city_id: str = DEFAULT_CITY_UUID,
    note: str | None = None,
):
    ui = get_ui(ctx_name)
    limit = EVENTS_PAGE_SIZE

    page_events, has_prev, has_next, total = fetch_events_page(
        event_service=event_service, city_id=city_id, offset=offset, limit=limit
    )

    _send_events_list_screen(
        peer,
        ui=ui,
        offset=offset,
        limit=limit,
        page_events=page_events,
        total=total,
        has_prev=has_prev,
        has_next=has_next,
        note=note,
    )


def send_points_page(peer, *, offset: int, event_service: EventService):
    limit = EVENTS_PAGE_SIZE

    claimed_raw, total = event_service.get_claimed_events_page(
        messenger_id=peer.id,
        city_id=DEFAULT_CITY_UUID,
        offset=offset,
        limit=limit + 1,
    )

    points_total = "?"

    _send_events_list_screen(
        peer,
        ui=get_ui("points"),
        offset=offset,
        limit=limit,
        page_events=claimed_raw[:limit],
        total=total,
        has_prev=offset > 0,
        has_next=len(claimed_raw) > limit,
        header=f"🏆 Твои баллы: {points_total}\n\n",
        empty_text=(
            "Пока нет мероприятий, за которые уже начислены баллы.\n\n"
            "Как получить баллы:\n"
            "1) Запишитесь на мероприятие\n"
            "2) После участия введи код\n"
        ),
        empty_groups=points_menu_keyboard(),
    )


def send_my_events_page(
    peer,
    *,
    offset: int,
    event_service: EventService,
    requester_messenger_id: int,
    city_id: str = DEFAULT_CITY_UUID,
    note: str | None = None,
):
    ui = get_ui("my_events")
    limit = EVENTS_PAGE_SIZE

    page_events, has_prev, has_next, total = fetch_events_page(
        event_service=event_service,
        city_id=city_id,
        offset=offset,
        limit=limit,
        requester_messenger_id=requester_messenger_id,
        only_organizer=True,
    )

    _send_events_list_screen(
        peer,
        ui=ui,
        offset=offset,
        limit=limit,
        page_events=page_events,
        total=total,
        has_prev=has_prev,
        has_next=has_next,
        note=note,
        header="📋 Мои мероприятия\n\n",
        empty_text="Пока у тебя нет созданных мероприятий.",
    )


def send_event_card(
    peer,
    *,
    event_id: str,
    event_service: EventService,
    report_service: ReportService | None = None,
    user: UserSchema | None,
    ctx_name: str,
    note: str | None = None,
    show_organizer: bool = True,
):
    ui = get_ui(ctx_name)

    ev = event_service.get_event_by_id(event_id)
    if ev is None:
        text = f"{note}\n\n⚠️ Мероприятие не найдено." if note else "⚠️ Мероприятие не найдено."
        bot.messaging.send_message(
            peer=peer,
            text=text,
            interactive_media_groups=build_back_keyboard(ctx_name),
        )
        return

    part = event_service.get_participation_status(ev.id, peer.id)
    is_participant = part is not None

    can_manage = can_manage_event(ev, user)
    show_code = can_view_code(ev, user)

    role = getattr(user, "role", None) if user else None
    can_access_reports = (role in ("admin", "moderator")) or can_manage

    report_exists: bool | None = None
    if can_access_reports and report_service is not None:
        report_exists = report_service.report_exists(
            event_id=str(ev.id),
            requester_messenger_id=peer.id,
        )
    else:
        report_exists = None

    can_create_report = bool(can_manage and (report_exists is False))
    can_view_report = bool(can_access_reports and (report_exists is True))

    base = format_event_details(
        ev,
        show_code=show_code,
        show_organizer=show_organizer,
        show_report_status=bool(can_access_reports and report_exists is not None),
        report_exists=bool(report_exists) if report_exists is not None else None,
    )
    text = f"{note}\n\n{base}" if note else base

    bot.messaging.send_message(
        peer=peer,
        text=text,
        interactive_media_groups=event_actions_keyboard(
            ev.id,
            is_participant=is_participant,
            can_manage=can_manage,
            back_value=ui.back_value,
            back_label=ui.back_label,
            enter_code_media_id=ui.enter_code_media_id,
            sign_up_media_id=ui.sign_up_media_id,
            sign_out_media_id=ui.sign_out_media_id,
            ctx=ui.name,  # важно: чтобы value был "<event_id>|<ctx>"
            can_create_report=can_create_report,
            can_view_report=can_view_report,
        ),
    )


def build_back_to_event_keyboard(event_id: str, ctx_name: str) -> list[InteractiveMediaGroup]:
    ui = get_ui(ctx_name)
    return MediaGroupBuilder(
        [
            Button(
                media_id=ui.list_open_media_id,
                value=f"{event_id}|{ui.name}",
                label="⬅️ К мероприятию",
            )
        ]
    ).build()

Editor is loading...
Leave a Comment