diff --git a/bot/database/Participationdao.py b/bot/database/Participationdao.py new file mode 100644 index 0000000..a7355e1 --- /dev/null +++ b/bot/database/Participationdao.py @@ -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 diff --git a/bot/database/competition_dao.py b/bot/database/competition_dao.py index 154fd3d..cd3999e 100644 --- a/bot/database/competition_dao.py +++ b/bot/database/competition_dao.py @@ -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() diff --git a/bot/database/user_dao.py b/bot/database/user_dao.py index 85991fe..ef791f6 100644 --- a/bot/database/user_dao.py +++ b/bot/database/user_dao.py @@ -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]: @@ -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]: """Получить всех студентов вместе с их невыполненными заданиями.""" @@ -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() diff --git a/bot/handlers/__init__.py b/bot/handlers/__init__.py index 1a7b678..b3f4862 100644 --- a/bot/handlers/__init__.py +++ b/bot/handlers/__init__.py @@ -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 diff --git a/bot/handlers/leaderboard.py b/bot/handlers/leaderboard.py index 5dc3a12..f2d945b 100644 --- a/bot/handlers/leaderboard.py +++ b/bot/handlers/leaderboard.py @@ -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 diff --git a/bot/handlers/marking_students.py b/bot/handlers/marking_students.py new file mode 100644 index 0000000..bf9080e --- /dev/null +++ b/bot/handlers/marking_students.py @@ -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) + 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("Действие отменено.") diff --git a/bot/main.py b/bot/main.py index 440f4be..277a11a 100644 --- a/bot/main.py +++ b/bot/main.py @@ -18,6 +18,7 @@ heal_router, leaderboard_router, my_profile_router, + mark_students_router, ) from settings import config from middlewares import AuthMiddleware @@ -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, @@ -91,5 +94,6 @@ async def main(): await dp.start_polling(bot) + if __name__ == "__main__": asyncio.run(main()) diff --git a/bot/middlewares.py b/bot/middlewares.py index 3306216..3159c03 100644 --- a/bot/middlewares.py +++ b/bot/middlewares.py @@ -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() @@ -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) diff --git a/bot/states/mark_students_states.py b/bot/states/mark_students_states.py new file mode 100644 index 0000000..678beed --- /dev/null +++ b/bot/states/mark_students_states.py @@ -0,0 +1,7 @@ +from aiogram.fsm.state import State, StatesGroup + + +class MarkStudentsState(StatesGroup): + selecting_event = State() + selecting_students = State() + confirmation = State() diff --git a/bot/tasks.py b/bot/tasks.py index 580d0d6..00c9edb 100644 --- a/bot/tasks.py +++ b/bot/tasks.py @@ -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}." )