Untitled

4ae4d avatar
4ae4d
plain_text
02/16/2026 9:34 AM
36.8 KB
9
Indexable
f=src/bot/core/markups/pagination.py
f=src/bot/core/markups/inline.py
f=src/bot/core/markups/points.py
f=src/bot/core/utils/fsm/storage/memory.py
f=src/backend/app/dependencies/auth/authorization.py
f=src/backend/app/schemas/event.py
f=src/backend/app/schemas/report.py
f=src/backend/app/repositories/base.py
f=src/backend/app/dependencies/auth/authentication.py
f=src/backend/app/api/v1/report.py
--- src/bot/core/markups/pagination.py ---
from __future__ import annotations

from dialog_bot_sdk.entities.messaging import InteractiveMediaStyle
from dialog_bot_sdk.interactive_media import Button, InteractiveMediaGroup, MediaGroupBuilder


def pagination_keyboard(
    *,
    offset: int,
    limit: int,
    has_prev: bool,
    has_next: bool,
    prev_media_id: str,
    next_media_id: str,
    prev_label: str = "⬅️ Назад",
    next_label: str = "➡️ Вперёд",
    include_leave: bool = True,
    leave_media_id: str = "leave",
    leave_value: str | None = None,
    leave_label: str | None = None,
    style: InteractiveMediaStyle | None = None,
    keep_layout: bool = False,
    placeholder_media_id: str = "noop",
    placeholder_value: str = "noop",
    placeholder_label: str = " ",
    hide_when_all_placeholders: bool = False,
    add_view_by_uuid_button: bool = False,
    view_by_uuid_media_id: str = "view_by_uuid",
    view_by_uuid_value: str = "events",
    view_by_uuid_label: str = "🔎 По UUID",
) -> list[InteractiveMediaGroup]:
    """
    унифицированная пагинация.

    keep_layout=True:
      если add_view_by_uuid_button=True:
        - если include_leave=True: 4 слота [prev|ph] [leave|ph] [uuid] [next|ph]
        - если include_leave=False: 3 слота [prev|ph] [uuid] [next|ph]
      если add_view_by_uuid_button=False:
        - если include_leave=True: 3 слота [prev|ph] [leave|ph] [next|ph]
        - если include_leave=False: 2 слота [prev|ph] [next|ph]

    hide_when_all_placeholders=True: если из кнопок только ph - то возвращаем пустой лист.
    """

    def _btn(media_id: str, value: str, label: str) -> Button:
        if style is None:
            return Button(media_id=media_id, value=value, label=label)
        return Button(media_id, value, label, style)

    def _ph(slot: str) -> Button:
        return _btn(
            placeholder_media_id + "_" + slot,
            placeholder_value,
            placeholder_label,
        )

    def _prev() -> Button:
        return _btn(prev_media_id, str(max(offset - limit, 0)), prev_label)

    def _next() -> Button:
        return _btn(next_media_id, str(offset + limit), next_label)

    def _view_by_uuid() -> Button:
        return _btn(
            view_by_uuid_media_id,
            str(view_by_uuid_value),
            str(view_by_uuid_label),
        )

    def _leave() -> Button | None:
        if not (include_leave and leave_value and leave_label):
            return None
        return _btn(leave_media_id, str(leave_value), str(leave_label))

    # --- 3-slot layout for include_leave
    if include_leave and keep_layout:
        buttons: list[Button] = []
        leave_btn = _leave()

        buttons.append(_prev() if has_prev else _ph("prev"))
        buttons.append(leave_btn if leave_btn is not None else _ph("mid"))

        # --- 4-slot: [prev] [leave] [uuid] [next]
        if add_view_by_uuid_button:
            buttons.append(_view_by_uuid())

        buttons.append(_next() if has_next else _ph("next"))

        if hide_when_all_placeholders and all(
            str(getattr(b, "media_id", "")).startswith(placeholder_media_id + "_") for b in buttons
        ):
            return []
        return MediaGroupBuilder(buttons).build()

    # --- default linear layout (как было)
    buttons: list[Button] = []
    if has_prev:
        buttons.append(_prev())
    elif keep_layout:
        buttons.append(_ph("prev"))

    leave_btn = _leave()
    if leave_btn is not None:
        buttons.append(leave_btn)

    if add_view_by_uuid_button:
        buttons.append(_view_by_uuid())

    if has_next:
        buttons.append(_next())
    elif keep_layout:
        buttons.append(_ph("next"))

    if hide_when_all_placeholders and all(
        str(getattr(b, "media_id", "")).startswith(placeholder_media_id + "_") for b in buttons
    ):
        return []

    return MediaGroupBuilder(buttons).build() if buttons else []

--- src/bot/core/markups/inline.py ---
from dialog_bot_sdk.entities.messaging import InteractiveMediaStyle
from dialog_bot_sdk.interactive_media import Button, MediaGroupBuilder

from core.markups.pagination import pagination_keyboard


# Основные клавиатуры
def main_keyboard_admin():
    group_builder_1 = MediaGroupBuilder(
        [
            Button(
                "1",
                "volunteer_home",
                "🏡 Дом волонтёра",
                InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
            ),
            Button(
                "2",
                "ai_assistant",
                "✨ ИИ Волонтёр",
                InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
            ),
        ]
    )
    group_builder_2 = MediaGroupBuilder(
        [
            Button(
                "3",
                "moderation",
                "🛡️ Модерация",
                InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
            ),
            Button(
                "4",
                "administration",
                "⚙️ Администрация",
                InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
            ),
        ]
    )

    return group_builder_1.merge([group_builder_2])


def main_keyboard_moder():
    group_builder_1 = MediaGroupBuilder(
        [
            Button(
                "1",
                "volunteer_home",
                "🏡 Дом волонтёра",
                InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
            ),
            Button(
                "2",
                "ai_assistant",
                "✨ ИИ Волонтёр",
                InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
            ),
        ]
    )
    group_builder_2 = MediaGroupBuilder(
        [
            Button(
                "3", "moderation", "Модерация", InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT
            )
        ]
    )

    return group_builder_1.merge([group_builder_2])


def main_keyboard_user():
    builder = MediaGroupBuilder(
        [
            Button(
                "1",
                "volunteer_home",
                "🏡 Дом волонтёра",
                InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
            ),
            Button(
                "2",
                "ai_assistant",
                "✨ ИИ Волонтёр",
                InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
            ),
        ]
    )
    return builder.build()


def back_to_main_menu_keyboard():
    builder = MediaGroupBuilder(
        [Button("1", "main_menu", "⬅️ В меню", InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT)]
    )
    return builder.build()


# Дом волонтёра
def volunteer_home_keyboard():
    group_builder_1 = MediaGroupBuilder(
        [
            Button(
                "1",
                "my_profile",
                "👤 Мой профиль",
                InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
            ),
            Button(
                "2", "events", "🌐 Мероприятия", InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT
            ),
        ]
    )
    group_builder_2 = MediaGroupBuilder(
        [
            Button("3", "points", "🏆 Баллы", InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT),
            Button(
                "4",
                "part_events",
                "🎉 Мои меропиятия",
                InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
            ),
        ]
    )
    group_builder_3 = MediaGroupBuilder(
        [
            Button(
                "5", "main_menu", "⬅️ В меню", InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT
            ),
        ]
    )

    return group_builder_1.merge([group_builder_2, group_builder_3])


def back_to_moderation_keyboard():
    builder = MediaGroupBuilder(
        [
            Button(
                "1",
                "moderation",
                "⬅️ В меню модерации",
                InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
            ),
        ]
    )
    return builder.build()


def back_to_volunteer_home_keyboard():
    builder = MediaGroupBuilder(
        [
            Button(
                "1",
                "volunteer_home",
                "⬅️ В дом волонтёра",
                InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
            )
        ]
    )
    return builder.build()


# Модерация
def moderation_menu_keyboard():
    group_builder_1 = MediaGroupBuilder(
        [
            Button("1", "create_event", "🗓️ Создать мероприятие"),
            Button("4", "my_events", "📋 Мои мероприятия"),
        ]
    )

    group_builder_2 = MediaGroupBuilder(
        [
            Button(
                "5",
                "all_events",
                "🌐 Все мероприятия",
                InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
            ),
            Button("7", "download_reports_list", "📥 Выгрузить отчёты"),
        ]
    )

    group_builder_3 = MediaGroupBuilder(
        [
            Button("10", "main_menu", "⬅️ В меню"),
        ]
    )

    return group_builder_1.merge([group_builder_2, group_builder_3])


# Администрация
def admin_menu_keyboard():
    group_builder_1 = MediaGroupBuilder(
        [
            Button("1", "add_admin", "Добавить администратора"),
            Button("2", "delete_admin", "Убрать администратора"),
        ]
    )

    group_builder_2 = MediaGroupBuilder(
        [
            Button("3", "add_moderator", "Добавить модератора"),
            Button("4", "delete_moderator", "Убрать модератора"),
        ]
    )

    group_builder_3 = MediaGroupBuilder(
        [
            Button("5", "ban_user", "Забанить пользователя"),
            Button("6", "unban_user", "Разбанить пользователя"),
        ]
    )

    group_builder_4 = MediaGroupBuilder([Button("7", "main_menu", "⬅️ В меню")])

    return group_builder_1.merge([group_builder_2, group_builder_3, group_builder_4])


def reports_export_keyboard():
    """
    value везде = 'download_reports_list', обработчик один
    выбор различаем по media_id
    """

    group_1 = MediaGroupBuilder(
        [
            Button("rep_day", "download_reports_list", "📅 Последний день"),
            Button("rep_week", "download_reports_list", "📆 Неделя"),
        ]
    )
    group_2 = MediaGroupBuilder(
        [
            Button("rep_month", "download_reports_list", "🗓️ Месяц"),
            Button("rep_all", "download_reports_list", "🌐 Всё время"),
        ]
    )
    group_3 = MediaGroupBuilder(
        [
            Button("rep_from", "download_reports_list", "📌 От даты до сегодня"),
            Button("rep_range", "download_reports_list", "🗓️ От даты до даты"),
        ]
    )
    group_4 = MediaGroupBuilder(
        [
            Button("rep_back", "moderation", "⬅️ В меню модерации"),
        ]
    )
    return group_1.merge([group_2, group_3, group_4])


def all_events_pagination_keyboard(*, offset: int, limit: int, has_prev: bool, has_next: bool):
    return pagination_keyboard(
        offset=offset,
        limit=limit,
        has_prev=has_prev,
        has_next=has_next,
        prev_media_id="all_events_prev",
        next_media_id="all_events_next",
        leave_value="moderation",
        leave_label="🛡️ В меню модерации",
        style=InteractiveMediaStyle.INTERACTIVEMEDIASTYLE_DEFAULT,
        keep_layout=True,
        add_view_by_uuid_button=True,
        view_by_uuid_value="moderation",
    )

--- src/bot/core/markups/points.py ---
from dialog_bot_sdk.interactive_media import Button, MediaGroupBuilder

from core.markups.pagination import pagination_keyboard


def points_menu_keyboard():
    return MediaGroupBuilder(
        [
            Button("1", "points_leaderboard", "🏆 Лидерборд"),
            Button("2", "volunteer_home", "⬅️ В дом волонтёра"),
        ]
    ).build()


def back_to_points_menu():
    return MediaGroupBuilder([Button("1", "points", "⬅️ К баллам")]).build()


def leaderboard_keyboard():
    return MediaGroupBuilder(
        [Button("1", "points_leaderboard", "🔂 Обновить"), Button("2", "points", "⬅️ К баллам")]
    ).build()


def points_events_pagination_keyboard(*, offset: int, limit: int, has_prev: bool, has_next: bool):
    return pagination_keyboard(
        offset=offset,
        limit=limit,
        has_prev=has_prev,
        has_next=has_next,
        prev_media_id="points_prev",
        next_media_id="points_next",
        include_leave=False,
        keep_layout=True,
        hide_when_all_placeholders=True,
    )

--- src/bot/core/utils/fsm/storage/memory.py ---
from .base import BaseStorage


class MemoryStorage(BaseStorage):
    def __init__(self):
        self._data = {}

    def set(self, user_id: int, state: str | int, data: dict):
        self._data[user_id] = {"state": state, "data": data or {}}

    def get(self, user_id: int) -> dict:
        return self._data.get(user_id, {"state": None, "data": {}})

    def clear(self, user_id: int):
        self._data.pop(user_id, None)

--- src/backend/app/dependencies/auth/authorization.py ---
from datetime import UTC, datetime, timedelta
from typing import Literal

from jose import jwt
from sqlalchemy.ext.asyncio import AsyncSession

from ...config.settings import settings
from ...schemas import TokenData
from ...services import AdminService
from ...utils.hasher import Hasher


class AuthorizationService:
    @classmethod
    async def login(
        cls, data: str | dict, role: Literal["admin"], session: AsyncSession
    ) -> TokenData | None:
        if role == "admin":
            return await cls._process_authorization_admin(data=data, session=session)
        return None

    @classmethod
    async def _process_authorization_admin(cls, data: dict, session: AsyncSession):
        admin = await AdminService.get_admin_by_login(session, data["login"])

        if admin and Hasher.verify_password(data["password"], admin.password):
            access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
            refresh_token_expires = timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_DAYS)

            access_token = cls.create_access_token(
                data={"sub": admin.login, "scopes": ["admin"]}, expires_delta=access_token_expires
            )
            refresh_token = cls.create_refresh_token(
                data={"sub": admin.login, "scopes": ["admin"]}, expires_delta=refresh_token_expires
            )

            return TokenData(access_token=access_token, refresh_token=refresh_token)
        return None

    @staticmethod
    def create_access_token(data: dict, expires_delta: timedelta | None = None):
        to_encode = data.copy()
        expire = datetime.now(UTC) + (
            expires_delta or timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
        )
        to_encode.update({"exp": expire})
        return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)

    @staticmethod
    def create_refresh_token(data: dict, expires_delta: timedelta | None = None):
        to_encode = data.copy()
        expire = datetime.now(UTC) + (
            expires_delta or timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
        )
        to_encode.update({"exp": expire})
        return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)

    @classmethod
    def refresh_tokens(cls, refresh_token: str):
        payload = jwt.decode(refresh_token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
        new_access_token = cls.create_access_token(
            data={"sub": payload["sub"], "scopes": payload["scopes"]}
        )
        new_refresh_token = cls.create_refresh_token(
            data={"sub": payload["sub"], "scopes": payload["scopes"]}
        )
        return TokenData(access_token=new_access_token, refresh_token=new_refresh_token)

--- src/backend/app/schemas/event.py ---
from datetime import datetime
from uuid import UUID

from pydantic import BaseModel, ConfigDict, Field, constr

from .app_tag import ResponseAppTagSchema
from .base import TimestampSchema


class BaseEvent(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    name: str
    description: str | None = None
    location: str | None = None
    tags: list[ResponseAppTagSchema] = Field(default_factory=list)
    bonus_code: str
    points: int
    date: datetime


class EventCreateSchema(BaseModel):
    """то, что приходит от бота при создании мероприятия"""

    model_config = ConfigDict(extra="forbid")

    name: constr(max_length=128)
    description: str | None = None
    location: constr(max_length=255) | None = None
    app_tag_ids: list[UUID] = Field(default_factory=list)
    bonus_code: constr(max_length=16)
    points: int = 0
    date: datetime
    city_id: UUID
    creator_id: UUID | None = None


class RequestEventSchema(BaseEvent):
    pass


class ResponseEventSchema(BaseEvent, TimestampSchema):
    id: UUID
    creator_id: UUID | None = (
        None  # TODO решить, оставляем ли или всегда приписываем UUID создателя
    )
    creator_name: str | None = None
    city_id: UUID


class EventSignUpSchema(BaseModel):
    event_id: UUID
    messenger_id: int


class EventSignOutSchema(BaseModel):
    participation_id: UUID


class EventCodeSchema(EventSignOutSchema):
    bonus_code: str


class EventUpdateSchema(BaseModel):
    model_config = ConfigDict(extra="forbid")

    name: constr(max_length=128) | None = None
    description: str | None = None
    location: str | None = None
    app_tag_ids: list[UUID] | None = None
    bonus_code: constr(max_length=16) | None = None
    points: int | None = None
    date: datetime | None = None
    city_id: UUID | None = None
    creator_id: UUID | None = None


class ResponseEventParticipantSchema(BaseModel):
    """один участник мероприятия"""

    model_config = ConfigDict(extra="forbid")

    participation_id: UUID
    is_claimed: bool

    user_id: UUID
    firstname: str | None = None
    messenger_id: int
    employee_number: int
    points: int

--- src/backend/app/schemas/report.py ---
from __future__ import annotations

from uuid import UUID

from pydantic import BaseModel, ConfigDict, Field, constr

from .base import TimestampSchema


class ReportCreateSchema(BaseModel):
    model_config = ConfigDict(extra="forbid")

    event_id: UUID
    requester_messenger_id: int
    how_it_went: constr(min_length=1)
    beneficiaries_count: int | None = None


class ResponseReportSchema(TimestampSchema):
    model_config = ConfigDict(from_attributes=True)

    id: UUID
    event_id: UUID
    how_it_went: str
    beneficiaries_count: int | None = None
    participants_count_snapshot: int = Field(..., ge=0)

--- src/backend/app/repositories/base.py ---
from uuid import UUID

from sqlalchemy import asc as sa_asc
from sqlalchemy import delete, insert, select, update
from sqlalchemy import desc as sa_desc
from sqlalchemy.ext.asyncio import AsyncSession

from ..utils.errors import BadRequestError


class SQLRepository:
    model = None

    def __init__(self, session: AsyncSession):
        self.session = session

    async def insert(self, data: dict):
        query = insert(self.model).values(**data).returning(self.model)
        result = await self.session.execute(query)
        instance = result.fetchone()
        if not instance:
            raise BadRequestError("Insert failed")
        return instance[0]

    async def get_by_id(self, id: UUID):
        query = select(self.model).where(self.model.id == id)
        result = await self.session.execute(query)
        return result.scalars().first()

    async def update(self, id: UUID, data: dict):
        query = update(self.model).where(self.model.id == id).values(**data).returning(self.model)
        result = await self.session.execute(query)
        return result.scalar()

    async def delete(self, id: UUID):
        query = delete(self.model).where(self.model.id == id).returning(self.model)
        result = await self.session.execute(query)
        return result.scalar()

    async def get_all(
        self,
        order_by: str | None = None,
        desc: bool | None = None,
        offset: int = 0,
        limit: int = 25,
    ):
        query = select(self.model)
        if order_by:
            column = getattr(self.model, order_by, None)
            if column is None:
                raise ValueError(f"Invalid order_by column: {order_by}")
            query = query.order_by(sa_desc(column) if desc else sa_asc(column))

        query = query.offset(offset).limit(limit)
        result = await self.session.execute(query)
        return result.scalars().all()

--- src/backend/app/dependencies/auth/authentication.py ---
from typing import Annotated

from fastapi import Depends, HTTPException, status
from fastapi.security import SecurityScopes
from jose import JWTError, jwt
from jose.exceptions import ExpiredSignatureError
from pydantic import ValidationError
from sqlalchemy.ext.asyncio import AsyncSession

from ...config.settings import settings
from ...db.connection import get_db
from ...schemas import ResponseAdminSchema, TokenData
from ...utils import logger
from .oauth import CookieAuthorization
from .profile import get_profile

cookie_oauth2_scheme = CookieAuthorization()


async def authentification_process(
    tokens: TokenData, security_scopes: SecurityScopes, session: AsyncSession | None = None
):
    authenticate_value = "Bearer"
    access_token = tokens.access_token
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials. Incorrect authorization value",
        headers={"WWW-Authenticate": authenticate_value},
    )

    if security_scopes.scopes:
        authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'

    try:
        payload = jwt.decode(
            access_token,
            settings.SECRET_KEY,
            algorithms=[settings.ALGORITHM],
        )
        sub: str = payload.get("sub")
        token_scopes: list = payload.get("scopes", [])
        if sub is None:
            raise credentials_exception

        await logger.write(
            f"| {sub} | An attempt to access a private resource. Checking the token for rights"
        )
    except (JWTError, ValidationError, ExpiredSignatureError) as e:
        raise credentials_exception from e

    for scope in token_scopes:
        if scope not in security_scopes.scopes:
            await logger.write(
                f"| {sub} | Profile have not enough permissions for executing this request. Access is denied"
            )
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Not enough permissions",
                headers={"WWW-Authenticate": authenticate_value},
            )

    profile = await get_profile(filter_=sub, role=security_scopes.scopes[0], session=session)
    if not profile:
        raise credentials_exception
    return profile


async def authentificate_admin(
    tokens: TokenData, session: Annotated[AsyncSession, Depends(get_db)]
) -> ResponseAdminSchema:
    admin = await authentification_process(
        tokens=tokens, security_scopes=SecurityScopes(scopes=["admin"]), session=session
    )
    await logger.write(f"The admin {admin.login} passed the checking")
    return admin

--- src/backend/app/api/v1/report.py ---
from __future__ import annotations

from datetime import date, datetime
from io import BytesIO
from typing import Annotated
from uuid import UUID

from fastapi import APIRouter, Depends, Path, Query, status
from fastapi.responses import StreamingResponse
from openpyxl import Workbook
from sqlalchemy.ext.asyncio import AsyncSession

from ...db.connection import get_db
from ...dependencies.administrator import Administrator
from ...schemas.report import ReportCreateSchema, ResponseReportSchema
from ...services.report import ReportService

api_router = APIRouter(prefix="/reports", tags=["Reports"])
SessionDep = Annotated[AsyncSession, Depends(get_db)]

REPORT_COLUMNS: list[tuple[str, str]] = [
    ("event_name", "Название"),
    ("city", "Город"),
    ("location", "Локация"),
    ("date", "Дата"),
    ("time", "Время"),
    ("participants_count", "Кол-во участников"),
    ("points", "Кол-во баллов"),
    ("tags", "Теги"),
    ("organizer_fio", "ФИО организатора"),
    ("beneficiaries_count", "Кол-во благополучателей"),
    ("how_it_went", "Как прошло"),
]


@api_router.get("/export")
async def export_reports_xlsx(
    session: SessionDep,
    date_from: Annotated[date | None, Query(description="YYYY-MM-DD")] = None,
    date_to: Annotated[date | None, Query(description="YYYY-MM-DD")] = None,
):
    """
    Эскпорт отчётов в xlsx из БД за период.
    сортировка по дате мероприятия.
    в колонках нет описания мероприятия
    """
    admin = Administrator(session)
    service = ReportService(admin)

    wb = Workbook()
    ws = wb.active
    ws.title = "Reports"

    ws.append([title for _, title in REPORT_COLUMNS])

    rows = await service.export_reports_rows(date_from=date_from, date_to=date_to)
    for row in rows:
        ws.append(row)

    buf = BytesIO()
    wb.save(buf)
    buf.seek(0)

    export_day = datetime.utcnow().date().isoformat()
    name_parts = ["reports", export_day]
    if date_from and date_to:
        name_parts.append(f"{date_from}_{date_to}")
    elif date_from:
        name_parts.append(f"from_{date_from}")
    elif date_to:
        name_parts.append(f"to_{date_to}")
    filename = "_".join(name_parts) + ".xlsx"

    headers = {"Content-Disposition": f'attachment; filename="{filename}"'}
    return StreamingResponse(
        buf,
        media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        headers=headers,
    )


@api_router.post(
    "",
    status_code=status.HTTP_201_CREATED,
    response_model=ResponseReportSchema,
)
async def create_report(body: ReportCreateSchema, session: SessionDep):
    admin = Administrator(session)
    service = ReportService(admin)
    return await service.create_report(
        event_id=body.event_id,
        requester_messenger_id=body.requester_messenger_id,
        how_it_went=body.how_it_went,
        beneficiaries_count=body.beneficiaries_count,
    )


@api_router.get("/{event_id}", status_code=status.HTTP_200_OK, response_model=ResponseReportSchema)
async def get_report(
    event_id: Annotated[str, Path()],
    requester_messenger_id: Annotated[int, Query()],
    session: SessionDep,
):
    admin = Administrator(session)
    service = ReportService(admin)
    return await service.get_report(
        event_id=UUID(event_id),
        requester_messenger_id=requester_messenger_id,
    )

Editor is loading...
Leave a Comment