Compare commits
11 Commits
7734adda8b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 279e0c6a70 | |||
| 5cf9455f76 | |||
| da3af83b3a | |||
| 1be3401d09 | |||
| 343dc8fb5f | |||
| 93a425aea9 | |||
| 9b9e81c61d | |||
| 27249be467 | |||
| 7e5939394d | |||
| ceb8e87939 | |||
| 95747756cc |
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
__pycache__
|
||||||
|
.venv
|
||||||
|
.env
|
||||||
|
*.db
|
||||||
|
# Ignore dynaconf secret files
|
||||||
|
.secrets.*
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
ENV_FOR_DYNACONF=production
|
ENV_FOR_DYNACONF=production
|
||||||
|
|
||||||
BOT_TOKEN=
|
BOT_TOKEN=
|
||||||
|
DATABASE_URL=postgresql+psycopg://user:password@localhost:5432/database
|
||||||
51
Dockerfile
Normal file
51
Dockerfile
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# An example of using standalone Python builds with multistage images.
|
||||||
|
|
||||||
|
# First, build the application in the `/app` directory
|
||||||
|
FROM ghcr.io/astral-sh/uv:bookworm-slim AS builder
|
||||||
|
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
|
||||||
|
|
||||||
|
# Configure the Python directory so it is consistent
|
||||||
|
ENV UV_PYTHON_INSTALL_DIR=/python
|
||||||
|
|
||||||
|
# Only use the managed Python version
|
||||||
|
ENV UV_PYTHON_PREFERENCE=only-managed
|
||||||
|
|
||||||
|
# Install Python before the project for caching
|
||||||
|
RUN uv python install 3.13
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
|
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||||
|
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||||
|
uv sync --locked --no-install-project --no-dev
|
||||||
|
COPY . /app
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
|
uv sync --locked --no-dev
|
||||||
|
|
||||||
|
# Then, use a final image without uv
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
# Setup a non-root user
|
||||||
|
RUN groupadd --system --gid 999 nonroot \
|
||||||
|
&& useradd --system --gid 999 --uid 999 --create-home nonroot
|
||||||
|
|
||||||
|
# Copy the Python version
|
||||||
|
COPY --from=builder --chown=python:python /python /python
|
||||||
|
|
||||||
|
# Copy the application from the builder
|
||||||
|
COPY --from=builder --chown=nonroot:nonroot /app /app
|
||||||
|
|
||||||
|
# Place executables in the environment at the front of the path
|
||||||
|
ENV PATH="/app/.venv/bin:$PATH"
|
||||||
|
ENV ENV_FOR_DYNACONF=production
|
||||||
|
ENV BOT_TOKEN=
|
||||||
|
ENV DATABASE_URL=
|
||||||
|
|
||||||
|
# Use the non-root user to run our application
|
||||||
|
USER nonroot
|
||||||
|
|
||||||
|
# Use `/app` as the working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Run the FastAPI application by default
|
||||||
|
CMD ["python", "-m", "app"]
|
||||||
47
README.md
47
README.md
@@ -1,32 +1,45 @@
|
|||||||
# Бот для записи на курсы и события от школы фасциопатии
|
# Бот для записи на курсы и мероприятия от школы фасциопатии
|
||||||
|
|
||||||
## Текущие возможности
|
## Текущие возможности
|
||||||
|
|
||||||
**Администраторы могут:**
|
**Администраторы могут:**
|
||||||
- [x] Редактировать свой профиль
|
- [x] Редактировать свой профиль
|
||||||
- [ ] Создавать события
|
- [x] Создавать мероприятия
|
||||||
- (указав дату, время, название, описание, картинку)
|
- [x] Создавать мероприятия
|
||||||
- [ ] Редактировать события
|
- (указав дату, время, название, описание)
|
||||||
- Редактировать отдельно любую из характеристик события
|
- [ ] Редактировать мероприятия
|
||||||
- [ ] Удалять события
|
- Редактировать отдельно любую из характеристик мероприятия
|
||||||
- [ ] Просматривать список существующих событий
|
- [ ] Удалять мероприятия
|
||||||
- [ ] Просматривать людей записавшихся на событие
|
- [x] Просматривать список существующих мероприятий
|
||||||
|
- [ ] Просматривать людей записавшихся на мероприятие
|
||||||
|
- [x] Делать рассылку по пользователям
|
||||||
|
- [x] Исключать из рассылки администраторов
|
||||||
|
- [ ] Указать дату и время рассылки
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
**Пользователи могут:**
|
**Пользователи могут:**
|
||||||
- [x] Редактировать свой профиль
|
- [x] Редактировать свой профиль
|
||||||
- [ ] Просматривать список событий
|
- [x] Просматривать список мероприятий
|
||||||
- [ ] Просматривать информацию о событии
|
- [x] Просматривать информацию о мероприятии
|
||||||
- [ ] Регистрироваться на событие
|
- [ ] Регистрироваться на мероприятие
|
||||||
- [ ] Отменять регистрацию на событие
|
- [ ] Отменять регистрацию на мероприятие
|
||||||
|
- [x] Просматривать список мероприятий
|
||||||
|
- [x] Просматривать информацию о мероприятии
|
||||||
|
- [ ] Регистрироваться на мероприятие
|
||||||
|
- [ ] Отменять регистрацию на мероприятие
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
## Планируемые возможности
|
## Планируемые возможности
|
||||||
- Прямая ссылка на событие извне бота
|
- Прямая ссылка на мероприятие извне бота
|
||||||
|
- Прямая ссылка на мероприятие извне бота
|
||||||
- Подтверждение регистрации администратором
|
- Подтверждение регистрации администратором
|
||||||
- Число записавшихся на событие пользователей у администратора
|
- Число записавшихся на мероприятие пользователей у администратора
|
||||||
- Уведомление пользователям о изменении события, на которые они подписаны
|
- Уведомление пользователям о изменении мероприятия, на которые они подписаны
|
||||||
- Значок о регистрации на событие в списке событий у пользователя
|
- Значок о регистрации на мероприятие в списке мероприятий у пользователя
|
||||||
- Выгружать людей записавшихся на событие в виде Excel файла
|
- Выгружать людей записавшихся на мероприятие в виде Excel файла
|
||||||
|
- Число записавшихся на мероприятие пользователей у администратора
|
||||||
|
- Уведомление пользователям о изменении мероприятия, на которые они подписаны
|
||||||
|
- Значок о регистрации на мероприятие в списке мероприятий у пользователя
|
||||||
|
- Выгружать людей записавшихся на мероприятие в виде Excel файла
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from aiogram import Router
|
from aiogram import Router
|
||||||
|
|
||||||
from .events.dialogs import events_dialog
|
from .events.dialogs import events_dialog
|
||||||
|
from .mailing.dialogs import mailing_dialog
|
||||||
from .new_event.dialogs import new_event_dialog
|
from .new_event.dialogs import new_event_dialog
|
||||||
from .profile.dialogs import profile_dialog
|
from .profile.dialogs import profile_dialog
|
||||||
from .start.dialogs import start_dialog
|
from .start.dialogs import start_dialog
|
||||||
@@ -10,3 +11,4 @@ dialogs_router.include_router(start_dialog)
|
|||||||
dialogs_router.include_router(events_dialog)
|
dialogs_router.include_router(events_dialog)
|
||||||
dialogs_router.include_router(profile_dialog)
|
dialogs_router.include_router(profile_dialog)
|
||||||
dialogs_router.include_router(new_event_dialog)
|
dialogs_router.include_router(new_event_dialog)
|
||||||
|
dialogs_router.include_router(mailing_dialog)
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
from aiogram_dialog import Dialog, Window
|
from aiogram_dialog import Dialog, Window
|
||||||
from aiogram_dialog.widgets.kbd import Back, Cancel, Column, Select
|
from aiogram_dialog.widgets.kbd import Back, Button, Cancel, Column, Select
|
||||||
from aiogram_dialog.widgets.text import Const, Format, Jinja
|
from aiogram_dialog.widgets.text import Case, Const, Format, Jinja
|
||||||
|
|
||||||
from app.bot.dialogs.templates import event_template
|
from app.bot.dialogs.templates import event_template
|
||||||
|
|
||||||
from .getters import event_getter, events_list_getter
|
from .getters import event_getter, events_list_getter, registration_getter
|
||||||
from .handlers import on_event_selected
|
from .handlers import change_registration, on_event_selected
|
||||||
from .states import EventsSG
|
from .states import EventsSG
|
||||||
|
|
||||||
events_dialog = Dialog(
|
events_dialog = Dialog(
|
||||||
Window(
|
Window(
|
||||||
Const("События"),
|
Const("Мероприятия"),
|
||||||
Column(
|
Column(
|
||||||
Cancel(Const("Назад")),
|
Cancel(Const("Назад")),
|
||||||
Select(
|
Select(
|
||||||
@@ -26,8 +26,16 @@ events_dialog = Dialog(
|
|||||||
),
|
),
|
||||||
Window(
|
Window(
|
||||||
Jinja(event_template),
|
Jinja(event_template),
|
||||||
|
Button(
|
||||||
|
Case(
|
||||||
|
[Const("зарегистрироваться"), Const("отменить регистрацию")],
|
||||||
|
selector="is_registered_to_event",
|
||||||
|
),
|
||||||
|
id="change_registration_btn",
|
||||||
|
on_click=change_registration,
|
||||||
|
),
|
||||||
Back(Const("Назад")),
|
Back(Const("Назад")),
|
||||||
getter=event_getter,
|
getter=[event_getter, registration_getter],
|
||||||
parse_mode="HTML",
|
parse_mode="HTML",
|
||||||
state=EventsSG.event,
|
state=EventsSG.event,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
from aiogram.types import User
|
from aiogram.types import User
|
||||||
from aiogram_dialog import DialogManager
|
from aiogram_dialog import DialogManager
|
||||||
|
|
||||||
from app.infrastructure.database.crud import get_event_by_id, get_events_list
|
from app.infrastructure.database.crud import (
|
||||||
|
get_event_by_id,
|
||||||
|
get_events_list,
|
||||||
|
)
|
||||||
|
from app.infrastructure.database.enums import UserEventStatus
|
||||||
|
from app.infrastructure.database.models import Event
|
||||||
|
|
||||||
|
|
||||||
async def events_list_getter(
|
async def events_list_getter(
|
||||||
@@ -11,12 +16,29 @@ async def events_list_getter(
|
|||||||
return {"events": [{"title": event.title, "id": event.id} for event in events]}
|
return {"events": [{"title": event.title, "id": event.id} for event in events]}
|
||||||
|
|
||||||
|
|
||||||
async def event_getter(
|
async def event_getter(dialog_manager: DialogManager, **kwargs) -> dict[str, Event]:
|
||||||
dialog_manager: DialogManager, **kwargs
|
|
||||||
) -> dict[str, str]:
|
|
||||||
return {
|
return {
|
||||||
"event_obj": await get_event_by_id(
|
"event_obj": await get_event_by_id(
|
||||||
dialog_manager.middleware_data["session"],
|
dialog_manager.middleware_data["session"],
|
||||||
int(dialog_manager.dialog_data["selected_event"]),
|
dialog_manager.dialog_data["selected_event"],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def registration_getter(dialog_manager: DialogManager, **kwargs):
|
||||||
|
user = dialog_manager.middleware_data["user"]
|
||||||
|
event_id = dialog_manager.dialog_data["selected_event"]
|
||||||
|
return {
|
||||||
|
"is_registered_to_event": any(
|
||||||
|
(ue.event_id == event_id) and (ue.status != UserEventStatus.CANCELLED.value)
|
||||||
|
for ue in user.events
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def registraion_getter(dialog_manager: DialogManager, **kwargs):
|
||||||
|
user = dialog_manager.middleware_data["user"]
|
||||||
|
event_id = dialog_manager.dialog_data["selected_event"]
|
||||||
|
return {
|
||||||
|
"is_registered_to_event": any(ue.event_id == event_id for ue in user.events)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
from aiogram.types import CallbackQuery
|
from aiogram.types import CallbackQuery
|
||||||
from aiogram_dialog import DialogManager
|
from aiogram_dialog import DialogManager
|
||||||
from aiogram_dialog.widgets.kbd import Select
|
from aiogram_dialog.widgets.kbd import Button, Select
|
||||||
|
|
||||||
|
from app.infrastructure.database.crud import (
|
||||||
|
register_user_to_event,
|
||||||
|
unregister_user_to_event,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .getters import registration_getter
|
||||||
|
|
||||||
|
|
||||||
async def on_event_selected(
|
async def on_event_selected(
|
||||||
@@ -9,5 +16,26 @@ async def on_event_selected(
|
|||||||
manager: DialogManager,
|
manager: DialogManager,
|
||||||
item_id: str,
|
item_id: str,
|
||||||
):
|
):
|
||||||
manager.dialog_data["selected_event"] = item_id
|
manager.dialog_data["selected_event"] = int(item_id)
|
||||||
await manager.next()
|
await manager.next()
|
||||||
|
|
||||||
|
|
||||||
|
async def change_registration(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
widget: Button,
|
||||||
|
manager: DialogManager,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
user = manager.middleware_data["user"]
|
||||||
|
event_id = manager.dialog_data["selected_event"]
|
||||||
|
if (await registration_getter(manager))["is_registered_to_event"]:
|
||||||
|
await unregister_user_to_event(
|
||||||
|
manager.middleware_data["session"], user.id, event_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if not user.phone:
|
||||||
|
await callback.answer("Пожалуйста, заполните ваш номер телефона в профиле", show_alert=True)
|
||||||
|
else:
|
||||||
|
await register_user_to_event(
|
||||||
|
manager.middleware_data["session"], user.id, event_id
|
||||||
|
)
|
||||||
|
|||||||
43
app/bot/dialogs/flows/mailing/dialogs.py
Normal file
43
app/bot/dialogs/flows/mailing/dialogs.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from aiogram_dialog import Dialog, Window
|
||||||
|
from aiogram_dialog.widgets.input import MessageInput
|
||||||
|
from aiogram_dialog.widgets.kbd import Back, Button, Cancel
|
||||||
|
from aiogram_dialog.widgets.text import Const
|
||||||
|
|
||||||
|
# from .getters import event_getter, events_list_getter, registration_getter
|
||||||
|
from .handlers import choose_recipients, confirm_mailing, message_data
|
||||||
|
from .states import MailingSG
|
||||||
|
|
||||||
|
mailing_dialog = Dialog(
|
||||||
|
Window(
|
||||||
|
Const("Пришлите сообщение которое хотите разослать"),
|
||||||
|
MessageInput(message_data),
|
||||||
|
Cancel(Const("Отмена")),
|
||||||
|
state=MailingSG.message_data,
|
||||||
|
),
|
||||||
|
Window(
|
||||||
|
Const("Кому отправить сообщение?"),
|
||||||
|
Button(
|
||||||
|
Const("Всем"),
|
||||||
|
id="send_all",
|
||||||
|
on_click=choose_recipients,
|
||||||
|
),
|
||||||
|
Button(
|
||||||
|
Const("Не админам"),
|
||||||
|
id="send_users",
|
||||||
|
on_click=choose_recipients,
|
||||||
|
),
|
||||||
|
Back(Const("Назад")),
|
||||||
|
state=MailingSG.recipients,
|
||||||
|
),
|
||||||
|
Window(
|
||||||
|
Const("Начать рассылку?"),
|
||||||
|
Button(
|
||||||
|
Const("Разослать"),
|
||||||
|
id="start_mailing",
|
||||||
|
on_click=confirm_mailing,
|
||||||
|
),
|
||||||
|
Back(Const("Назад")),
|
||||||
|
Cancel(Const("Отмена")),
|
||||||
|
state=MailingSG.confirm,
|
||||||
|
),
|
||||||
|
)
|
||||||
77
app/bot/dialogs/flows/mailing/handlers.py
Normal file
77
app/bot/dialogs/flows/mailing/handlers.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
from aiogram.types import CallbackQuery, Message
|
||||||
|
from aiogram_dialog import DialogManager
|
||||||
|
from aiogram_dialog.widgets.input import MessageInput
|
||||||
|
from aiogram_dialog.widgets.kbd import Button
|
||||||
|
from structlog import get_logger
|
||||||
|
|
||||||
|
from app.infrastructure.database.crud import get_users
|
||||||
|
|
||||||
|
logger = get_logger()
|
||||||
|
|
||||||
|
async def message_data(
|
||||||
|
message: Message,
|
||||||
|
widget: MessageInput,
|
||||||
|
manager: DialogManager,
|
||||||
|
):
|
||||||
|
manager.dialog_data["message"] = message
|
||||||
|
await manager.next()
|
||||||
|
|
||||||
|
|
||||||
|
async def choose_recipients(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
manager: DialogManager,
|
||||||
|
):
|
||||||
|
manager.dialog_data["recipients"] = button.widget_id
|
||||||
|
await manager.next()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def confirm_mailing(
|
||||||
|
callback: CallbackQuery,
|
||||||
|
button: Button,
|
||||||
|
manager: DialogManager,
|
||||||
|
):
|
||||||
|
print(manager.dialog_data["recipients"])
|
||||||
|
|
||||||
|
users = list(await get_users(
|
||||||
|
manager.middleware_data["session"],
|
||||||
|
exclude_admins=manager.dialog_data["recipients"] == "send_users",
|
||||||
|
))
|
||||||
|
|
||||||
|
message = manager.dialog_data["message"]
|
||||||
|
source_chat_id = message.chat.id
|
||||||
|
source_message_id = message.message_id
|
||||||
|
|
||||||
|
success_count = 0
|
||||||
|
|
||||||
|
# Создаём задачу с обработкой ошибок для каждого пользователя
|
||||||
|
async def send_to_user(user):
|
||||||
|
nonlocal success_count
|
||||||
|
try:
|
||||||
|
await callback.bot.copy_message(
|
||||||
|
chat_id=user.tg_id,
|
||||||
|
from_chat_id=source_chat_id,
|
||||||
|
message_id=source_message_id,
|
||||||
|
)
|
||||||
|
success_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
"не удалось отправить сообщение",
|
||||||
|
user_id=user.tg_id,
|
||||||
|
error=str(e),
|
||||||
|
exc_info=True, # добавит traceback
|
||||||
|
)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
await asyncio.gather(*[send_to_user(user) for user in users])
|
||||||
|
|
||||||
|
await callback.bot.send_message(
|
||||||
|
chat_id=callback.from_user.id,
|
||||||
|
text=f"Рассылка завершена.\nУспешно отправлено: {success_count} из {len(users)}",
|
||||||
|
)
|
||||||
|
await manager.done()
|
||||||
8
app/bot/dialogs/flows/mailing/states.py
Normal file
8
app/bot/dialogs/flows/mailing/states.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
|
|
||||||
|
|
||||||
|
class MailingSG(StatesGroup):
|
||||||
|
message_data = State()
|
||||||
|
recipients = State()
|
||||||
|
# mailing_date = State()
|
||||||
|
confirm = State()
|
||||||
@@ -12,7 +12,7 @@ async def input_title(
|
|||||||
callback: CallbackQuery,
|
callback: CallbackQuery,
|
||||||
widget: ManagedTextInput,
|
widget: ManagedTextInput,
|
||||||
manager: DialogManager,
|
manager: DialogManager,
|
||||||
text: date,
|
text: str,
|
||||||
):
|
):
|
||||||
if "event_obj" not in manager.dialog_data:
|
if "event_obj" not in manager.dialog_data:
|
||||||
manager.dialog_data["event_obj"] = {}
|
manager.dialog_data["event_obj"] = {}
|
||||||
@@ -24,7 +24,7 @@ async def input_description(
|
|||||||
callback: CallbackQuery,
|
callback: CallbackQuery,
|
||||||
widget: ManagedTextInput,
|
widget: ManagedTextInput,
|
||||||
manager: DialogManager,
|
manager: DialogManager,
|
||||||
text: date,
|
text: str,
|
||||||
):
|
):
|
||||||
manager.dialog_data["event_obj"]["description"] = text
|
manager.dialog_data["event_obj"]["description"] = text
|
||||||
await manager.next()
|
await manager.next()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from aiogram_dialog.widgets.kbd import Button, Start
|
|||||||
from aiogram_dialog.widgets.text import Const, Format
|
from aiogram_dialog.widgets.text import Const, Format
|
||||||
|
|
||||||
from app.bot.dialogs.flows.events.dialogs import EventsSG
|
from app.bot.dialogs.flows.events.dialogs import EventsSG
|
||||||
|
from app.bot.dialogs.flows.mailing.dialogs import MailingSG
|
||||||
from app.bot.dialogs.flows.new_event.dialogs import NewEventSG
|
from app.bot.dialogs.flows.new_event.dialogs import NewEventSG
|
||||||
from app.bot.dialogs.flows.profile.dialogs import ProfileSG
|
from app.bot.dialogs.flows.profile.dialogs import ProfileSG
|
||||||
from app.bot.dialogs.widgets.getters import is_admin_getter, user_getter
|
from app.bot.dialogs.widgets.getters import is_admin_getter, user_getter
|
||||||
@@ -12,13 +13,19 @@ from .states import StartSG
|
|||||||
start_dialog = Dialog(
|
start_dialog = Dialog(
|
||||||
Window(
|
Window(
|
||||||
Format("Привет, {user.fullname}"),
|
Format("Привет, {user.fullname}"),
|
||||||
Start(Const("📃 события"), id="events", state=EventsSG.events_list),
|
Start(Const("📃 мероприятия"), id="events", state=EventsSG.events_list),
|
||||||
Start(
|
Start(
|
||||||
Const("✏️ создать событие"),
|
Const("✏️ создать мероприятие"),
|
||||||
id="create_event",
|
id="create_event",
|
||||||
state=NewEventSG.input_title,
|
state=NewEventSG.input_title,
|
||||||
when="is_admin",
|
when="is_admin",
|
||||||
),
|
),
|
||||||
|
Start(
|
||||||
|
Const("📧 разослать сообщение"),
|
||||||
|
id="create_mailing",
|
||||||
|
state=MailingSG.message_data,
|
||||||
|
when="is_admin",
|
||||||
|
),
|
||||||
Start(Const("👤 профиль"), id="profile", state=ProfileSG.profile),
|
Start(Const("👤 профиль"), id="profile", state=ProfileSG.profile),
|
||||||
getter=[user_getter, is_admin_getter],
|
getter=[user_getter, is_admin_getter],
|
||||||
state=StartSG.start,
|
state=StartSG.start,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class GetUserMiddleware(BaseMiddleware):
|
|||||||
user = await create_user(
|
user = await create_user(
|
||||||
session,
|
session,
|
||||||
tg_user.id,
|
tg_user.id,
|
||||||
tg_user.username,
|
tg_user.full_name,
|
||||||
)
|
)
|
||||||
data["user"] = user
|
data["user"] = user
|
||||||
return await handler(event, data)
|
return await handler(event, data)
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ from datetime import datetime
|
|||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from .models import Event, User
|
from .enums import UserEventStatus
|
||||||
|
from .models import Event, User, UserEvent
|
||||||
|
|
||||||
|
|
||||||
async def get_user_by_tg_id(session: AsyncSession, user_tg_id: int) -> User | None:
|
async def get_user_by_tg_id(session: AsyncSession, user_tg_id: int) -> User | None:
|
||||||
result = await session.execute(select(User).where(User.tg_id == user_tg_id))
|
result = await session.execute(
|
||||||
|
select(User).options(selectinload(User.events)).where(User.tg_id == user_tg_id)
|
||||||
|
)
|
||||||
return result.scalar_one_or_none()
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
@@ -37,6 +41,11 @@ async def update_user(
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
async def get_users(session: AsyncSession, exclude_admins=False) -> list[User]:
|
||||||
|
query = select(User).where(User.role == "user") if exclude_admins else select(User)
|
||||||
|
return await session.scalars(query)
|
||||||
|
|
||||||
|
|
||||||
async def get_events_list(session: AsyncSession) -> list[Event]:
|
async def get_events_list(session: AsyncSession) -> list[Event]:
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(Event).where(Event.start_date >= datetime.now())
|
select(Event).where(Event.start_date >= datetime.now())
|
||||||
@@ -94,4 +103,33 @@ async def update_event(
|
|||||||
if end_date:
|
if end_date:
|
||||||
event.end_date = end_date
|
event.end_date = end_date
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return event
|
return event
|
||||||
|
|
||||||
|
|
||||||
|
async def register_user_to_event(session: AsyncSession, user_id: int, event_id: int):
|
||||||
|
user_event = (
|
||||||
|
await session.execute(
|
||||||
|
select(UserEvent).where(
|
||||||
|
(UserEvent.user_id == user_id) & (UserEvent.event_id == event_id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
if user_event is None:
|
||||||
|
user_event = UserEvent(user_id=user_id, event_id=event_id)
|
||||||
|
session.add(user_event)
|
||||||
|
await session.flush()
|
||||||
|
if user_event.status == UserEventStatus.CANCELLED.value:
|
||||||
|
user_event.status = UserEventStatus.NOT_CONFIRMED.value
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def unregister_user_to_event(session: AsyncSession, user_id: int, event_id: int):
|
||||||
|
user_event = (
|
||||||
|
await session.execute(
|
||||||
|
select(UserEvent).where(
|
||||||
|
(UserEvent.user_id == user_id) & (UserEvent.event_id == event_id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).scalar_one()
|
||||||
|
user_event.status = UserEventStatus.CANCELLED.value
|
||||||
|
await session.commit()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class UserRole(Enum):
|
|||||||
TEACHER = "teacher"
|
TEACHER = "teacher"
|
||||||
|
|
||||||
|
|
||||||
class EventUserStatus(Enum):
|
class UserEventStatus(Enum):
|
||||||
NOT_CONFIRMED = "not confirmed"
|
NOT_CONFIRMED = "not confirmed"
|
||||||
CONFIRMED = "confirmed"
|
CONFIRMED = "confirmed"
|
||||||
CANCELLED = "cancelled"
|
CANCELLED = "cancelled"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import datetime as dt
|
|||||||
from sqlalchemy import BigInteger, DateTime, ForeignKey, UniqueConstraint, func
|
from sqlalchemy import BigInteger, DateTime, ForeignKey, UniqueConstraint, func
|
||||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from .enums import EventUserStatus, UserRole
|
from .enums import UserEventStatus, UserRole
|
||||||
|
|
||||||
|
|
||||||
class Base(DeclarativeBase):
|
class Base(DeclarativeBase):
|
||||||
@@ -45,7 +45,7 @@ class UserEvent(Base):
|
|||||||
date: Mapped[dt.datetime] = mapped_column(
|
date: Mapped[dt.datetime] = mapped_column(
|
||||||
DateTime(timezone=True), server_default=func.now()
|
DateTime(timezone=True), server_default=func.now()
|
||||||
)
|
)
|
||||||
status: Mapped[str] = mapped_column(default=EventUserStatus.NOT_CONFIRMED.value)
|
status: Mapped[str] = mapped_column(default=UserEventStatus.NOT_CONFIRMED.value)
|
||||||
|
|
||||||
user_rel = relationship("User", back_populates="events")
|
user_rel = relationship("User", back_populates="events")
|
||||||
event_rel = relationship("Event", back_populates="user_events")
|
event_rel = relationship("Event", back_populates="user_events")
|
||||||
|
|||||||
15
docker-compose.yml
Normal file
15
docker-compose.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
fascioschool-bot:
|
||||||
|
image: git.shevkunov.space/fascioschool/fascioschool-bot:latest
|
||||||
|
environment:
|
||||||
|
BOT_TOKENENV_FOR_DYNACONF: production
|
||||||
|
DATABASE_URL: postgresql+psycopg://admin:password@fascioschool-db:5432/fascioschool
|
||||||
|
BOT_TOKEN:
|
||||||
|
networks:
|
||||||
|
- fascioschool-net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
fascioschool-net:
|
||||||
|
external: true
|
||||||
30
ui.md
30
ui.md
@@ -1,30 +0,0 @@
|
|||||||
Пользователь
|
|
||||||
### Приветствие
|
|
||||||
Здрвствуйте, {Username}\
|
|
||||||
Вот текущие события
|
|
||||||
|
|
||||||
- ✅ Видим руками уровень 1
|
|
||||||
- Видим руками уровень 2
|
|
||||||
- Видим руками уровень 3
|
|
||||||
|
|
||||||
---
|
|
||||||
### Информация о событии
|
|
||||||
|
|
||||||
**Видим руками уровень 1**
|
|
||||||
На этом курсе вы научитесь...
|
|
||||||
|
|
||||||
[Вы записаны] / [Вы не записаны]
|
|
||||||
- Записаться
|
|
||||||
- Отменить запись
|
|
||||||
- Назад
|
|
||||||
---
|
|
||||||
## Регистарация в системе
|
|
||||||
|
|
||||||
### Регистрация шаг 1/2
|
|
||||||
- Укажите свой номер телефона
|
|
||||||
|
|
||||||
### Регистрация шаг 2/2
|
|
||||||
- Укажите свое ФИО
|
|
||||||
|
|
||||||
## Редактировать профиль
|
|
||||||
// Запускаем регистрацию заново
|
|
||||||
Reference in New Issue
Block a user