From ce81852d2e4aba4a73578ee4f63494fc52ab8f02 Mon Sep 17 00:00:00 2001 From: Dries Peeters Date: Sat, 29 Nov 2025 09:33:56 +0100 Subject: [PATCH] Fix app context handling for expiring quotes scheduled task - Add wrapper function check_expiring_quotes_with_app() to properly handle Flask app context - Refactor check_expiring_quotes() to remove redundant app context wrapper - Ensure consistent pattern with other scheduled tasks (webhook retry, integration sync) - Bump version to 4.1.1 --- app/utils/scheduled_tasks.py | 114 ++++++++++++++++++++--------------- setup.py | 2 +- 2 files changed, 66 insertions(+), 50 deletions(-) diff --git a/app/utils/scheduled_tasks.py b/app/utils/scheduled_tasks.py index a298649..8fadf72 100644 --- a/app/utils/scheduled_tasks.py +++ b/app/utils/scheduled_tasks.py @@ -353,8 +353,22 @@ def register_scheduled_tasks(scheduler, app=None): logger.info("Registered webhook retry task") # Check for expiring quotes daily at 9:30 AM + # Create a closure that captures the app instance + def check_expiring_quotes_with_app(): + """Wrapper that uses the captured app instance""" + app_instance = app + if app_instance is None: + try: + app_instance = current_app._get_current_object() + except RuntimeError: + logger.error("No app instance available for expiring quotes check") + return + + with app_instance.app_context(): + check_expiring_quotes() + scheduler.add_job( - func=check_expiring_quotes, + func=check_expiring_quotes_with_app, trigger="cron", hour=9, minute=30, @@ -490,68 +504,70 @@ def check_expiring_quotes(): This task should be run daily to check for quotes that are expiring within the next 7 days, 3 days, and 1 day, and send reminders. + + Note: This function should be called within an app context. + Use check_expiring_quotes_with_app() wrapper for scheduled tasks. """ - with current_app.app_context(): - try: - from app.utils.timezone import local_now - from datetime import timedelta - from app.utils.email import send_quote_expiring_reminder + try: + from app.utils.timezone import local_now + from datetime import timedelta + from app.utils.email import send_quote_expiring_reminder - logger.info("Checking for expiring quotes...") + logger.info("Checking for expiring quotes...") - today = local_now().date() - seven_days = today + timedelta(days=7) + today = local_now().date() + seven_days = today + timedelta(days=7) - # Get quotes that are sent and expiring soon - expiring_quotes = Quote.query.filter( - Quote.status == "sent", - Quote.valid_until.isnot(None), - Quote.valid_until >= today, - Quote.valid_until <= seven_days, - ).all() + # Get quotes that are sent and expiring soon + expiring_quotes = Quote.query.filter( + Quote.status == "sent", + Quote.valid_until.isnot(None), + Quote.valid_until >= today, + Quote.valid_until <= seven_days, + ).all() - logger.info(f"Found {len(expiring_quotes)} quotes expiring soon") + logger.info(f"Found {len(expiring_quotes)} quotes expiring soon") - notifications_sent = 0 - for quote in expiring_quotes: - if not quote.valid_until: - continue + notifications_sent = 0 + for quote in expiring_quotes: + if not quote.valid_until: + continue - days_until_expiry = (quote.valid_until - today).days + days_until_expiry = (quote.valid_until - today).days - # Send reminders at 7 days, 3 days, and 1 day before expiration - if days_until_expiry not in [7, 3, 1]: - continue + # Send reminders at 7 days, 3 days, and 1 day before expiration + if days_until_expiry not in [7, 3, 1]: + continue - # Get users to notify (creator and admins) - users_to_notify = set() + # Get users to notify (creator and admins) + users_to_notify = set() - # Add the quote creator - if quote.creator: - users_to_notify.add(quote.creator) + # Add the quote creator + if quote.creator: + users_to_notify.add(quote.creator) - # Add all admins - admins = User.query.filter_by(role="admin", is_active=True).all() - users_to_notify.update(admins) + # Add all admins + admins = User.query.filter_by(role="admin", is_active=True).all() + users_to_notify.update(admins) - # Send notifications - for user in users_to_notify: - if user.email and user.email_notifications: - try: - send_quote_expiring_reminder(quote, user, days_until_expiry) - notifications_sent += 1 - logger.info( - f"Sent expiration reminder for quote {quote.quote_number} to {user.username} ({days_until_expiry} days remaining)" - ) - except Exception as e: - logger.error(f"Failed to send reminder to {user.username}: {e}") + # Send notifications + for user in users_to_notify: + if user.email and user.email_notifications: + try: + send_quote_expiring_reminder(quote, user, days_until_expiry) + notifications_sent += 1 + logger.info( + f"Sent expiration reminder for quote {quote.quote_number} to {user.username} ({days_until_expiry} days remaining)" + ) + except Exception as e: + logger.error(f"Failed to send reminder to {user.username}: {e}") - logger.info(f"Sent {notifications_sent} quote expiration reminders") - return notifications_sent + logger.info(f"Sent {notifications_sent} quote expiration reminders") + return notifications_sent - except Exception as e: - logger.error(f"Error checking expiring quotes: {e}") - return 0 + except Exception as e: + logger.error(f"Error checking expiring quotes: {e}") + return 0 def sync_integrations(): diff --git a/setup.py b/setup.py index dadce15..4781d8b 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ from setuptools import setup, find_packages setup( name='timetracker', - version='4.1.0', + version='4.1.1', packages=find_packages(), include_package_data=True, install_requires=[