Compare commits

...

4 Commits
main ... master

Author SHA1 Message Date
itqop 30d43be793 fix time 2025-12-19 14:20:26 +03:00
itqop 59966a2f17 fixes bugs 2025-12-19 14:14:51 +03:00
itqop e675a245e6 add back button 2025-12-19 14:09:04 +03:00
itqop c1ec641ee0 fixes 2025-12-19 13:46:21 +03:00
17 changed files with 75 additions and 21 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -49,12 +49,19 @@ async def check_and_send_reminders(bot: Bot) -> None:
Args:
bot: Bot instance
"""
from bot.db.base import async_session_maker
from bot.db.operations import update_reminder
if not async_session_maker:
logger.error("Session maker not initialized")
return
try:
time_service = get_time_service()
current_time = time_service.get_now()
# Get due reminders from database
async for session in get_session():
# Get due reminders from database using proper async session
async with async_session_maker() as session:
due_reminders = await get_due_reminders(session, current_time)
if not due_reminders:
@ -74,16 +81,15 @@ async def check_and_send_reminders(bot: Bot) -> None:
# Update next_run_at to prevent sending again
# (it will be properly updated when user clicks "Done" or by periodic update)
from bot.db.operations import update_reminder
from datetime import timedelta
# Temporarily set next_run_at to current + interval to avoid duplicate sends
temp_next_run = time_service.calculate_next_occurrence(
current_run=reminder.next_run_at,
days_interval=reminder.days_interval,
)
await update_reminder(session, reminder.id, next_run_at=temp_next_run)
# Commit all updates
await session.commit()
# Small delay to avoid rate limits
await asyncio.sleep(0.5)

View File

@ -176,8 +176,11 @@ async def get_due_reminders(session: AsyncSession, current_time: datetime) -> Li
Returns:
List of due Reminder instances
"""
from sqlalchemy.orm import selectinload
result = await session.execute(
select(Reminder)
.options(selectinload(Reminder.user)) # Eager load user relationship
.where(Reminder.is_active == True)
.where(Reminder.next_run_at <= current_time)
.order_by(Reminder.next_run_at)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -128,6 +128,36 @@ async def show_reminder_details(
await callback.answer()
@router.callback_query(ReminderActionCallback.filter(F.action == "back_to_list"))
async def back_to_reminders_list(
callback: CallbackQuery,
session: AsyncSession,
) -> None:
"""
Return to reminders list from details view.
Args:
callback: Callback query
session: Database session
"""
user = await UserService.ensure_user_exists(session, callback.from_user)
reminders = await reminders_service.get_user_all_reminders(session, user.id)
if not reminders:
await callback.message.edit_text(
"У тебя пока нет напоминаний.\n\n"
"Нажми «➕ Новое напоминание», чтобы создать первое!"
)
await callback.answer()
return
await callback.message.edit_text(
f"📋 Твои напоминания ({len(reminders)}):",
reply_markup=get_reminders_list_keyboard(reminders, page=0),
)
await callback.answer()
# ==================== Edit Reminder Flow ====================

View File

@ -204,6 +204,12 @@ def get_reminder_details_keyboard(reminder_id: int, is_active: bool) -> InlineKe
callback_data=ReminderActionCallback(action="delete", reminder_id=reminder_id).pack()
),
],
[
InlineKeyboardButton(
text="⬅️ Назад к списку",
callback_data=ReminderActionCallback(action="back_to_list", reminder_id=0).pack()
),
],
]
)
return keyboard

View File

@ -38,14 +38,23 @@ async def on_shutdown() -> None:
logger.info("Shutting down reminder bot...")
# Stop scheduler
stop_scheduler()
try:
stop_scheduler()
except Exception as e:
logger.error(f"Error stopping scheduler: {e}")
# Close database
await close_db()
try:
await close_db()
except Exception as e:
logger.error(f"Error closing database: {e}")
# Close bot session
if bot:
await bot.session.close()
if bot and hasattr(bot, 'session'):
try:
await bot.session.close()
except Exception as e:
logger.error(f"Error closing bot session: {e}")
logger.info("Bot shutdown completed")

View File

@ -24,26 +24,27 @@ class TimeService:
def get_now(self) -> datetime:
"""
Get current datetime in configured timezone.
Get current datetime in configured timezone (as naive).
Returns:
Current datetime
Current datetime in local timezone (Moscow), naive
"""
return datetime.now(self.timezone)
# Get aware datetime in configured timezone, then strip timezone
aware_now = datetime.now(self.timezone)
return aware_now.replace(tzinfo=None)
def combine_date_time(self, date: datetime, time_of_day: time) -> datetime:
"""
Combine date and time in configured timezone.
Combine date and time as naive datetime.
Args:
date: Date component
time_of_day: Time component
Returns:
Combined datetime
Combined naive datetime
"""
naive_dt = datetime.combine(date.date(), time_of_day)
return naive_dt.replace(tzinfo=self.timezone)
return datetime.combine(date.date(), time_of_day)
def calculate_next_run_time(
self,
@ -109,13 +110,12 @@ class TimeService:
Format next run datetime for display.
Args:
next_run: Next run datetime
next_run: Next run datetime (naive UTC)
Returns:
Formatted string
"""
now = self.get_now()
delta = next_run - now
# If it's today
if next_run.date() == now.date():

View File

@ -45,11 +45,11 @@ def validate_days_interval(days_str: str) -> Optional[int]:
days_str: Days interval string to validate
Returns:
Integer days if valid (>0), None otherwise
Integer days if valid (>0 and <=365), None otherwise
"""
try:
days = int(days_str.strip())
if days > 0:
if 0 < days <= 365: # Max 1 year
return days
except (ValueError, AttributeError):
pass