Skip to content
28 changes: 28 additions & 0 deletions bot/database/Participationdao.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from database.models import Participation


class ParticipationDAO:
def __init__(self, session):
self.session = session

def mark_participation(self, competition_id: int, student_ids: list[int]):
"""Синхронный метод для отметки участия"""
try:
for student_id in student_ids:
existing = (
self.session.query(Participation)
.filter_by(user_id=student_id, competition_id=competition_id)
.first()
)

if not existing:
new_participation = Participation(
user_id=student_id, competition_id=competition_id
)
self.session.add(new_participation)

self.session.commit()
return True
except Exception as e:
self.session.rollback()
raise e
3 changes: 3 additions & 0 deletions bot/database/competition_dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ def add_competition(
self.session.commit()
self.session.refresh(new_competition)
return new_competition

def get_all_competition(self):
return self.session.query(Competition).all()
27 changes: 5 additions & 22 deletions bot/database/user_dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ def create_user(
def get_all_students(self) -> List[User]:
"""Получить всех студентов (исключая учителей по tg_id)."""
return (
self.session.query(User)
.filter(User.tg_id.notin_(config.teacher_ids))
.all()
self.session.query(User).filter(User.tg_id.notin_(config.teacher_ids)).all()
)

def get_all_active_students(self) -> List[User]:
Expand All @@ -47,20 +45,12 @@ def get_all_active_students(self) -> List[User]:

def get_user_id_by_username(self, username: str) -> Optional[int]:
"""Вернуть внутренний ID пользователя по username или None."""
user = (
self.session.query(User)
.filter(User.username == username)
.first()
)
user = self.session.query(User).filter(User.username == username).first()
return user.id if user else None

def get_user_by_tg_id(self, tg_id: int) -> Optional[User]:
"""Получить пользователя по его Telegram ID."""
return (
self.session.query(User)
.filter(User.tg_id == tg_id)
.first()
)
return self.session.query(User).filter(User.tg_id == tg_id).first()

def get_all_students_with_tasks(self) -> List[User]:
"""Получить всех студентов вместе с их невыполненными заданиями."""
Expand All @@ -85,18 +75,11 @@ def heal(self, user: User) -> None:
def get_teachers(self) -> List[User]:
"""Получить всех учителей (старшекурсников) по tg_id."""
teachers = (
self.session.query(User)
.filter(User.tg_id.in_(config.teacher_ids))
.all()
self.session.query(User).filter(User.tg_id.in_(config.teacher_ids)).all()
)
logger.info(f"Получены учителя: {teachers}")
return teachers

def leaderboard(self, limit: int = 20) -> List[User]:
"""Извлечь топ-`limit` пользователей, сортируя по убыванию points."""
return (
self.session.query(User)
.order_by(User.points.desc())
.limit(limit)
.all()
)
return self.session.query(User).order_by(User.points.desc()).limit(limit).all()
1 change: 1 addition & 0 deletions bot/handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
from .heal import heal_router
from .leaderboard import leaderboard_router
from .profiles import my_profile_router
from .marking_students import mark_students_router
8 changes: 4 additions & 4 deletions bot/handlers/leaderboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ def format_user_status(user: User, top_rating: list[User]) -> list[str]:
messages.append(f"🎉 Вы на {rank}-м месте в топ-20!")
else:
if user.points == 0:
messages.append("😔 У вас пока нет баллов. Решайте задачи, чтобы попасть в топ!")
messages.append(
"😔 У вас пока нет баллов. Решайте задачи, чтобы попасть в топ!"
)
else:
last_top_score = top_rating[-1].points if top_rating else 0
needed = last_top_score - user.points + 1
messages.append(
f"👉 Чтобы войти в топ-20, нужно ещё {needed} балл(ов)."
)
messages.append(f"👉 Чтобы войти в топ-20, нужно ещё {needed} балл(ов).")
return messages


Expand Down
189 changes: 189 additions & 0 deletions bot/handlers/marking_students.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
from aiogram import F, Router
from aiogram.types import CallbackQuery, Message, InlineKeyboardButton
from aiogram.filters import Command, StateFilter
from aiogram.fsm.context import FSMContext
from aiogram.utils.keyboard import InlineKeyboardBuilder
from database.user_dao import UserDAO
from database.competition_dao import CompetitionDao
from database.Participationdao import ParticipationDAO
from database.db import get_db
from states.mark_students_states import MarkStudentsState

mark_students_router = Router()

# Инициализация DAO
with get_db() as db:
UserDao = UserDAO(db)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with get_db() as db можно вызывать только внутри хэндлеров, потому что with - это контекстный менеджер, как только интерпретатор питона выйдет из его зоны подключение к БД закроется и получишь ошибку. Хоть это и копипаста, но эту конструкцию нужно писать перед применением db и DAO.

CompetitionDAO = CompetitionDao(db)


@mark_students_router.message(Command("mark_students"))
async def show_events(message: Message, state: FSMContext):
builder = InlineKeyboardBuilder()
EVENTS = CompetitionDAO.get_all_competition() # список объектов Competition
for event in EVENTS:
builder.button(
text=f"{event.name} ({event.date.strftime('%d.%m.%Y')})",
callback_data=f"event_{event.id}",
)
builder.adjust(1)

await state.set_state(MarkStudentsState.selecting_event)
await message.answer("Выберите мероприятие:", reply_markup=builder.as_markup())


@mark_students_router.callback_query(
F.data.startswith("event_"), StateFilter(MarkStudentsState.selecting_event)
)
async def select_students(callback: CallbackQuery, state: FSMContext):
event_id = int(callback.data.split("_")[1])
EVENTS = CompetitionDAO.get_all_competition() # список объектов Competition
competition = next((e for e in EVENTS if e.id == event_id), None)

if not competition:
await callback.answer("Мероприятие не найдено!")
return

await state.update_data(
event_id=event_id,
selected=set(),
competition_name=competition.name,
current_page=0,
)
await state.set_state(MarkStudentsState.selecting_students)

# Отправляем первую страницу студентов
await show_students_page(callback, state)


async def show_students_page(callback: CallbackQuery, state: FSMContext):
data = await state.get_data()
selected = data.get("selected", set())
current_page = data.get("current_page", 0)

# Разбиваем студентов на страницы по 5 человек
students_per_page = 5
start_idx = current_page * students_per_page
STUDENTS = UserDao.get_all_students() # список объектов Student
page_students = STUDENTS[start_idx : start_idx + students_per_page]
total_pages = (len(STUDENTS) + students_per_page - 1) // students_per_page

builder = InlineKeyboardBuilder()
for student in page_students:
status = "✅" if student.id in selected else "◻️"
builder.button(
text=f"{status} {student.full_name} ({student.username})",
callback_data=f"toggle_{student.id}",
)

# Кнопки навигации
nav_buttons = []
if current_page > 0:
nav_buttons.append(
InlineKeyboardButton(text="◀️ Назад", callback_data="page_prev")
)
if (current_page + 1) * students_per_page < len(STUDENTS):
nav_buttons.append(
InlineKeyboardButton(text="▶️ Вперед", callback_data="page_next")
)
nav_buttons.append(
InlineKeyboardButton(text="⏭️ Подтвердить", callback_data="confirm")
)

builder.row(*nav_buttons)

await callback.message.edit_text(
f"Выберите студентов для мероприятия {data['competition_name']} [Страница {current_page + 1} из {total_pages}]",
reply_markup=builder.as_markup(),
)


@mark_students_router.callback_query(
F.data.startswith("toggle_"), StateFilter(MarkStudentsState.selecting_students)
)
async def toggle_student(callback: CallbackQuery, state: FSMContext):
student_id = int(callback.data.split("_")[1])
data = await state.get_data()
selected = set(data.get("selected", set()))

if student_id in selected:
selected.remove(student_id)
else:
selected.add(student_id)

await state.update_data(selected=selected)
await show_students_page(callback, state)


@mark_students_router.callback_query(
F.data.in_(["page_prev", "page_next"]),
StateFilter(MarkStudentsState.selecting_students),
)
async def change_page(callback: CallbackQuery, state: FSMContext):
data = await state.get_data()
current_page = data.get("current_page", 0)

if callback.data == "page_prev":
current_page -= 1
else:
current_page += 1

await state.update_data(current_page=current_page)
await show_students_page(callback, state)


@mark_students_router.callback_query(
F.data == "confirm", StateFilter(MarkStudentsState.selecting_students)
)
async def confirm_selection(callback: CallbackQuery, state: FSMContext):
data = await state.get_data()
selected_ids = data.get("selected", set())
STUDENTS = UserDao.get_all_students() # список объектов Student

selected_students = [
f"{s.full_name} ({s.username})" for s in STUDENTS if s.id in selected_ids
]

if not selected_students:
await callback.answer("Не выбрано ни одного студента!")
return

builder = InlineKeyboardBuilder()
builder.button(text="✅ Подтвердить посещения", callback_data="final_confirm")
builder.button(text="❌ Отменить", callback_data="cancel")

await state.set_state(MarkStudentsState.confirmation)
await callback.message.edit_text(
f"Вы выбрали {len(selected_students)} студентов для мероприятия \"{data['competition_name']}\":\n\n"
+ "\n".join(selected_students),
reply_markup=builder.as_markup(),
)


@mark_students_router.callback_query(
F.data == "final_confirm", StateFilter(MarkStudentsState.confirmation)
)
async def final_confirmation(callback: CallbackQuery, state: FSMContext):
data = await state.get_data()

try:
with get_db() as db:
participation_dao = ParticipationDAO(db)
participation_dao.mark_participation(
data["event_id"], list(data["selected"])
)

await callback.message.edit_text(
f"Готово! {len(data['selected'])} студентов отмечены на мероприятии \"{data['competition_name']}\"."
)
await state.clear()
except Exception as e:
await callback.answer(f"Ошибка: {str(e)}")


@mark_students_router.callback_query(
F.data == "cancel", StateFilter(MarkStudentsState.confirmation)
)
async def cancel_action(callback: CallbackQuery, state: FSMContext):
await state.clear()
await callback.message.edit_text("Действие отменено.")
4 changes: 4 additions & 0 deletions bot/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
heal_router,
leaderboard_router,
my_profile_router,
mark_students_router,
)
from settings import config
from middlewares import AuthMiddleware
Expand All @@ -41,10 +42,12 @@
command="/missed_deadlines", description="Посмотреть пропущенные дедлайны"
),
types.BotCommand(command="/add_competition", description="Создать мероприятие"),
types.BotCommand(command="/mark_students", description="Отметить присутствующих"),
]

dp.include_routers(
start_router,
mark_students_router,
add_task_router,
add_competition_router,
my_tasks_router,
Expand Down Expand Up @@ -91,5 +94,6 @@ async def main():

await dp.start_polling(bot)


if __name__ == "__main__":
asyncio.run(main())
11 changes: 11 additions & 0 deletions bot/middlewares.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from database.db import get_db
from database.user_dao import UserDAO
from logging import getLogger
from settings import Config

logger = getLogger()

Expand Down Expand Up @@ -54,4 +55,14 @@ async def __call__(

data["user"] = user

# Список админских команд
admin_commands = ["/add_competition", "/mark_students", "/add_task"]

# Если команда админская, проверяем права
if event.text and any(event.text.startswith(cmd) for cmd in admin_commands):
teacher_ids = [int(id) for id in Config()._teacher_ids.split(",")]
if event.from_user.id not in teacher_ids:
await event.answer("⛔ У вас нет прав для выполнения этой команды")
return

return await handler(event, data)
7 changes: 7 additions & 0 deletions bot/states/mark_students_states.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from aiogram.fsm.state import State, StatesGroup


class MarkStudentsState(StatesGroup):
selecting_event = State()
selecting_students = State()
confirmation = State()
8 changes: 6 additions & 2 deletions bot/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@ async def sync_education_tasks(bot: Bot):
)
for task in user.tasks:
task.completed = task.name in solved_tasks
if not task.completed and task.is_expired and not task.violation_recorded:
if (
not task.completed
and task.is_expired
and not task.violation_recorded
):
user.lives -= 1
user.violations += 1
task.violation_recorded = True # Отмечаем, что нарушение обработано
task.violation_recorded = True
teacher_message = (
f"Задача {task.name} истека у студента {user}."
)
Expand Down