1- """Studies scheduler for managing study session notifications.
2-
3- Handles notifications for participants when there's less than a week before study dates.
1+ """Планировщик уведомлений об обучениях.
2+
3+ Модуль отвечает за управление уведомлениями участников обучений.
4+ Автоматически отправляет напоминания за 2 часа и за 1 час до начала обучения.
5+
6+ Основная функциональность:
7+ - Парсинг Excel-файла с расписанием обучений
8+ - Отслеживание приближающихся обучений
9+ - Автоматическая рассылка уведомлений участникам
10+ - Логирование результатов отправки
11+
12+ Компоненты:
13+ - StudiesScheduler: Основной класс планировщика
14+ - check_upcoming_studies: Проверка приближающихся обучений
15+ - send_study_notifications: Рассылка уведомлений участникам
16+ - create_study_notification_message: Формирование текста уведомления
417"""
518
619import logging
2336
2437
2538class StudiesScheduler (BaseScheduler ):
26- """Studies scheduler for managing study session notifications
39+ """Планировщик уведомлений об обучениях.
40+
41+ Управляет автоматической отправкой уведомлений участникам обучений
42+ за определенное время до начала сессии. Наследует базовый функционал
43+ от BaseScheduler и добавляет специфическую логику для работы с обучениями.
44+
45+ Основные возможности:
46+ - Автоматическая проверка приближающихся обучений каждые 30 минут
47+ - Отправка уведомлений за 2 часа и за 1 час до начала обучения
48+ - Парсинг данных об обучениях из Excel-файла
49+ - Поиск участников в базе данных и отправка персональных уведомлений
50+ - Логирование всех операций с детализацией по участникам
2751
28- Manages notifications for study participants when there's less than a week
29- before the study date.
52+ Attributes:
53+ studies_parser: Экземпляр StudiesScheduleParser для обработки файла обучений
54+
55+ Note:
56+ Класс автоматически запускается при инициализации планировщика
57+ и работает в фоновом режиме, проверяя наличие файла uploads/Обучения.xlsx
3058 """
3159
3260 def __init__ (self ):
3361 super ().__init__ ("Обучения" )
3462 self .studies_parser = StudiesScheduleParser ()
3563
3664 def setup_jobs (self , scheduler : AsyncIOScheduler , session_pool , bot : Bot ):
37- """Setup all studies-related jobs"""
65+ """Настраивает задачи планировщика для уведомлений об обучениях.
66+
67+ Регистрирует в планировщике задачу автоматической проверки
68+ приближающихся обучений. Задача выполняется каждые 30 минут
69+ и проверяет необходимость отправки уведомлений.
70+
71+ Args:
72+ scheduler: Планировщик задач APScheduler для регистрации заданий
73+ session_pool: Пул соединений с базой данных для операций с участниками
74+ bot: Экземпляр бота для отправки уведомлений пользователям
75+
76+ Note:
77+ Метод переопределяет базовую реализацию BaseScheduler
78+ и добавляет специфичную для обучений логику планирования.
79+ """
3880 self .logger .info ("Настройка задач уведомлений об обучениях..." )
3981
40- # Проверка приближающихся обучений
4182 scheduler .add_job (
4283 func = self ._check_upcoming_studies_job ,
4384 args = [session_pool , bot ],
@@ -48,7 +89,23 @@ def setup_jobs(self, scheduler: AsyncIOScheduler, session_pool, bot: Bot):
4889 )
4990
5091 async def _check_upcoming_studies_job (self , session_pool , bot : Bot ):
51- """Wrapper for checking upcoming studies"""
92+ """Обертка для проверки приближающихся обучений.
93+
94+ Выполняет проверку приближающихся обучений с логированием начала
95+ и завершения работы. Обрабатывает исключения и записывает результаты
96+ выполнения задачи.
97+
98+ Args:
99+ session_pool: Пул соединений с базой данных
100+ bot: Экземпляр бота для отправки уведомлений
101+
102+ Returns:
103+ dict: Результат выполнения проверки или None при ошибке
104+
105+ Note:
106+ Метод является оберткой вокруг основной функции check_upcoming_studies
107+ и добавляет логирование согласно стандартам BaseScheduler.
108+ """
52109 self ._log_job_execution_start ("Проверка предстоящих обучений" )
53110 try :
54111 result = await check_upcoming_studies (session_pool , bot )
@@ -61,17 +118,33 @@ async def _check_upcoming_studies_job(self, session_pool, bot: Bot):
61118
62119
63120async def check_upcoming_studies (session_pool , bot : Bot ):
64- """Check for upcoming studies and notify participants if less than a week away
121+ """Проверяет приближающиеся обучения и отправляет уведомления участникам.
122+
123+ Основная функция для проверки обучений из Excel-файла и отправки
124+ уведомлений участникам за 2 часа и за 1 час до начала обучения.
125+ Работает с окном в 10 минут для каждого типа уведомления.
65126
66127 Args:
67- session_pool: Database session pool
68- bot: Bot instance for sending notifications
128+ session_pool: Пул соединений с базой данных для поиска участников
129+ bot: Экземпляр бота Telegram для отправки уведомлений
69130
70131 Returns:
71- Dict with notification results
132+ dict: Словарь с результатами выполнения:
133+ - status: "success" или "error"
134+ - message: Сообщение о результате (при ошибке)
135+ - sessions: Количество обнаруженных обучений (при успехе)
136+ - notifications: Общее количество отправленных уведомлений
137+ - results: Детализация по сессиям
138+
139+ Raises:
140+ Exception: При критических ошибках парсинга файла или отправки уведомлений
141+
142+ Note:
143+ - Ожидает файл по пути uploads/Обучения.xlsx
144+ - Отправляет уведомления в окне ±10 минут от целевого времени
145+ - Логирует все операции для мониторинга работы системы
72146 """
73147 try :
74- # Get all studies from the file
75148 studies_parser = StudiesScheduleParser ()
76149 file_path = Path ("uploads/Обучения.xlsx" )
77150
@@ -85,21 +158,17 @@ async def check_upcoming_studies(session_pool, bot: Bot):
85158 logger .info ("[Обучения] No study sessions found in file" )
86159 return {"status" : "success" , "message" : "No study sessions found" }
87160
88- # Filter sessions for notifications: 2 hours before OR 1 hour before
89161 now = datetime .now ()
90162
91163 upcoming_sessions = []
92164 for session in all_sessions :
93- # Calculate time difference
94165 time_diff = session .date - now
95166
96- # Check if it's exactly 2 hours before (within 10 minutes window)
97167 two_hours_before = timedelta (hours = 2 )
98168 if abs (time_diff - two_hours_before ) <= timedelta (minutes = 10 ):
99169 upcoming_sessions .append (session )
100170 continue
101171
102- # Check if it's exactly 1 hour before (within 10 minutes window)
103172 one_hour_before = timedelta (hours = 1 )
104173 if abs (time_diff - one_hour_before ) <= timedelta (minutes = 10 ):
105174 upcoming_sessions .append (session )
@@ -114,12 +183,10 @@ async def check_upcoming_studies(session_pool, bot: Bot):
114183 f"[Обучения] Найдено { len (upcoming_sessions )} приближающихся обучений"
115184 )
116185
117- # Send notifications to participants
118186 notification_results = await send_study_notifications (
119187 upcoming_sessions , session_pool , bot
120188 )
121189
122- # Log summary
123190 total_notifications = sum (notification_results .values ())
124191 logger .info (
125192 f"[Обучения] Отправлено { total_notifications } уведомлений для { len (upcoming_sessions )} обучений"
@@ -140,15 +207,28 @@ async def check_upcoming_studies(session_pool, bot: Bot):
140207async def send_study_notifications (
141208 sessions : List [StudySession ], session_pool , bot : Bot
142209) -> dict :
143- """Send notifications to study participants
210+ """Отправляет уведомления участникам обучений.
211+
212+ Обрабатывает список сессий обучений и отправляет персональные
213+ уведомления всем участникам, найденным в базе данных.
214+ Избегает дублирования участников и отправляет уведомления
215+ только реальным участникам (не руководителям).
144216
145217 Args:
146- sessions: List of upcoming study sessions
147- session_pool: Database session pool
148- bot: Bot instance
218+ sessions: Список сессий обучений для обработки
219+ session_pool: Пул соединений с базой данных для поиска участников
220+ bot: Экземпляр бота Telegram для отправки сообщений
149221
150222 Returns:
151- Dict with notification results per session
223+ dict: Словарь с результатами отправки по каждой сессии,
224+ где ключ - уникальный идентификатор сессии (дата_название),
225+ значение - количество успешно отправленных уведомлений
226+
227+ Note:
228+ - Извлекает участников только из поля ФИО (исключая руководителей из РГ)
229+ - Использует set для исключения дублирования участников
230+ - Логирует подробную информацию о каждом этапе отправки
231+ - Пропускает участников без user_id в базе данных
152232 """
153233 notification_results = {}
154234
@@ -159,12 +239,8 @@ async def send_study_notifications(
159239 session_key = f"{ session_obj .date .strftime ('%d.%m.%Y' )} _{ session_obj .title } "
160240 notifications_sent = 0
161241
162- # Get unique participant names (avoid duplicates)
163- # Only extract names from the ФИО field (column 2) - these are the actual participants
164- # The РГ field (column 3) contains heads/supervisors who should NOT be notified
165242 participant_names : Set [str ] = set ()
166243 for area , name , rg , attendance , reason in session_obj .participants :
167- # Add name from ФИО field (column 2) - actual participants
168244 if name and name .strip ():
169245 participant_names .add (name .strip ())
170246
@@ -175,11 +251,9 @@ async def send_study_notifications(
175251 f"[Обучения] Найдено { len (participant_names )} уникальных участников: { list (participant_names )} "
176252 )
177253
178- # Send notification to each participant
179254 for participant_name in participant_names :
180255 try :
181- # Find participant in database
182- participant = await stp_repo .employee .get_user (
256+ participant = await stp_repo .employee .get_users (
183257 fullname = participant_name
184258 )
185259
@@ -195,15 +269,12 @@ async def send_study_notifications(
195269 )
196270 continue
197271
198- # Calculate time difference to determine notification type
199272 time_diff = session_obj .date - datetime .now ()
200273
201- # Create notification message
202274 message = await create_study_notification_message (
203275 session_obj , stp_repo , time_diff
204276 )
205277
206- # Send notification
207278 success = await send_message (bot , participant .user_id , message )
208279
209280 if success :
@@ -233,25 +304,38 @@ async def send_study_notifications(
233304async def create_study_notification_message (
234305 session : StudySession , stp_repo , time_diff : timedelta
235306) -> str :
236- """Create notification message for study participant
307+ """Формирует текст уведомления об обучении для участника.
308+
309+ Создает персонализированное сообщение с информацией об обучении,
310+ включая динамический текст времени, ссылку на тренера (если доступна)
311+ и правила посещения обучений.
237312
238313 Args:
239- session: Study session object
240- stp_repo: Repository for database operations
241- time_diff: Time difference until the session
314+ session: Объект сессии обучения с данными о дате, теме, тренере
315+ stp_repo: Репозиторий для операций с базой данных
316+ time_diff: Разница во времени до начала обучения
242317
243318 Returns:
244- Formatted notification message
319+ str: Отформатированное HTML-сообщение для отправки в Telegram
320+
321+ Note:
322+ - Автоматически определяет тип уведомления (2 часа/1 час/другое)
323+ - Пытается создать ссылку на профиль тренера в Telegram
324+ - Включает блок с правилами посещения обучений
325+ - Использует HTML-разметку для красивого отображения
326+
327+ Examples:
328+ Для обучения через 2 часа:
329+ "Напоминаем, что через 2 часа у тебя запланировано обучение..."
330+
331+ Для обучения через 1 час:
332+ "Напоминаем, что через 1 час у тебя запланировано обучение..."
245333 """
246- # Determine notification type based on time difference
247334 if abs (time_diff - timedelta (hours = 2 )) <= timedelta (minutes = 10 ):
248- # 2 hours before notification
249335 time_text = "через 2 часа"
250336 elif abs (time_diff - timedelta (hours = 1 )) <= timedelta (minutes = 10 ):
251- # 1 hour before notification
252337 time_text = "через 1 час"
253338 else :
254- # Fallback (shouldn't happen with new logic)
255339 days_until = (session .date .date () - datetime .now ().date ()).days
256340 if days_until == 0 :
257341 time_text = "сегодня"
@@ -260,11 +344,10 @@ async def create_study_notification_message(
260344 else :
261345 time_text = f"через { days_until } дн."
262346
263- # Get trainer information from database
264347 trainer_text = session .trainer
265348 if session .trainer :
266349 try :
267- trainer_user = await stp_repo .employee .get_user (fullname = session .trainer )
350+ trainer_user = await stp_repo .employee .get_users (fullname = session .trainer )
268351 if trainer_user and trainer_user .username :
269352 trainer_text = (
270353 f"<a href='t.me/{ trainer_user .username } '>{ session .trainer } </a>"
@@ -293,20 +376,34 @@ async def create_study_notification_message(
293376
294377
295378def format_studies_notification_summary (sessions : List [StudySession ]) -> str :
296- """Format brief summary of upcoming studies for logs
379+ """Форматирует краткую сводку предстоящих обучений для логов.
380+
381+ Создает компактное описание списка обучений, группируя их по датам
382+ для удобного анализа в логах системы. Используется для мониторинга
383+ и отладки работы планировщика уведомлений.
297384
298385 Args:
299- sessions: List of upcoming study sessions
386+ sessions: Список предстоящих сессий обучений
300387
301388 Returns:
302- Brief summary string
389+ str: Краткая строка-сводка с количеством обучений по датам
390+
391+ Examples:
392+ Пустой список:
393+ "No upcoming studies found"
394+
395+ Несколько обучений:
396+ "Upcoming studies: 3, • 15.11.2025: 2 session(s), • 16.11.2025: 1 session(s)"
397+
398+ Note:
399+ Функция не отправляет уведомления, а только формирует текст для логирования.
400+ Используется для получения быстрого обзора предстоящих обучений.
303401 """
304402 if not sessions :
305403 return "No upcoming studies found"
306404
307405 summary_parts = [f"Upcoming studies: { len (sessions )} " ]
308406
309- # Group by date
310407 dates = {}
311408 for session in sessions :
312409 date_str = session .date .strftime ("%d.%m.%Y" )
0 commit comments