From 032498a868c36cc1bf47c74811a327ae9e41cade Mon Sep 17 00:00:00 2001 From: Groupr Date: Mon, 21 Jul 2025 01:52:58 +0000 Subject: [PATCH 1/4] Update send_node_digest_notifications.py --- .../scripts/send_node_digest_notifications.py | 59 ++++++++++++------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/remarkbox/scripts/send_node_digest_notifications.py b/remarkbox/scripts/send_node_digest_notifications.py index c1f2f4e..7eb559b 100644 --- a/remarkbox/scripts/send_node_digest_notifications.py +++ b/remarkbox/scripts/send_node_digest_notifications.py @@ -1,37 +1,56 @@ from pyramid.paster import bootstrap, setup_logging -from ..lib.notify import deliver_scheduled_notifications -from . import base_parser import transaction +from ..lib.notify import deliver_scheduled_notifications +from ..models.notification import NodeEventNotification +from ..models.meta import now_timestamp + +from . import base_parser def get_arg_parser(): - parser = base_parser(" Send Node Notification Digests.") + parser = base_parser("Send Node Notification Digests.") return parser + def main(): parser = get_arg_parser() args = parser.parse_args() setup_logging(args.config) with bootstrap(args.config) as env: - try: - deliver_scheduled_notifications(env["request"]) + request = env["request"] - # Fix: mark all unsent notifications as sent and commit - dbsession = env["request"].dbsession - dbsession.query(NodeEventNotification).filter( + try: + with request.tm: + # Check before + dbsession = request.dbsession + before_count = dbsession.query(NodeEventNotification).filter( NodeEventNotification.sent == False - ).update({'sent': True, 'sent_timestamp': now_timestamp()}) + ).count() + print(f"Unsent notifications before: {before_count}") - print("Notifications processed successfully.") - transaction.commit() - raise SystemExit(0) - - except Exception as e: - print("Error processing notifications:", str(e)) - transaction.abort() - raise SystemExit(1) - -if __name__ == "__main__": - main() + deliver_scheduled_notifications(request) + + # Check after deliver function + after_deliver_count = dbsession.query(NodeEventNotification).filter( + NodeEventNotification.sent == False + ).count() + print(f"Unsent notifications after deliver: {after_deliver_count}") + + # Mark remaining as sent + updated = dbsession.query(NodeEventNotification).filter( + NodeEventNotification.sent == False + ).update({'sent': True, 'updated_timestamp': now_timestamp()}) + print(f"Updated {updated} notifications to sent=True") + + # Final check + final_count = dbsession.query(NodeEventNotification).filter( + NodeEventNotification.sent == False + ).count() + print(f"Unsent notifications after update: {final_count}") + + except Exception as e: + print(f"Error occurred: {e}") + transaction.abort() + raise \ No newline at end of file From 2e6d88f19132fdd3180743ab88c6478815e871dc Mon Sep 17 00:00:00 2001 From: Groupr Date: Mon, 21 Jul 2025 19:52:24 +0000 Subject: [PATCH 2/4] Update send_node_digest_notifications.py --- .../scripts/send_node_digest_notifications.py | 72 +++++++++++-------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/remarkbox/scripts/send_node_digest_notifications.py b/remarkbox/scripts/send_node_digest_notifications.py index 7eb559b..f9bb88b 100644 --- a/remarkbox/scripts/send_node_digest_notifications.py +++ b/remarkbox/scripts/send_node_digest_notifications.py @@ -2,6 +2,7 @@ from pyramid.paster import bootstrap, setup_logging import transaction from ..lib.notify import deliver_scheduled_notifications +from ..models import get_tm_session from ..models.notification import NodeEventNotification from ..models.meta import now_timestamp @@ -21,36 +22,51 @@ def main(): with bootstrap(args.config) as env: request = env["request"] + # Check before with fresh session + with transaction.manager: + dbsession = get_tm_session( + request.registry["dbsession_factory"], transaction.manager + ) + before_count = dbsession.query(NodeEventNotification).filter( + NodeEventNotification.sent == False + ).count() + print(f"Unsent notifications before: {before_count}") + + # Let deliver_scheduled_notifications manage its own transaction + deliver_scheduled_notifications(request) + + # Check after and mark as sent with fresh session try: - with request.tm: - # Check before - dbsession = request.dbsession - before_count = dbsession.query(NodeEventNotification).filter( + with transaction.manager: + dbsession = get_tm_session( + request.registry["dbsession_factory"], transaction.manager + ) + + after_count = dbsession.query(NodeEventNotification).filter( NodeEventNotification.sent == False ).count() - print(f"Unsent notifications before: {before_count}") - - deliver_scheduled_notifications(request) - - # Check after deliver function - after_deliver_count = dbsession.query(NodeEventNotification).filter( - NodeEventNotification.sent == False - ).count() - print(f"Unsent notifications after deliver: {after_deliver_count}") - - # Mark remaining as sent - updated = dbsession.query(NodeEventNotification).filter( - NodeEventNotification.sent == False - ).update({'sent': True, 'updated_timestamp': now_timestamp()}) - print(f"Updated {updated} notifications to sent=True") - - # Final check - final_count = dbsession.query(NodeEventNotification).filter( - NodeEventNotification.sent == False - ).count() - print(f"Unsent notifications after update: {final_count}") + print(f"Unsent notifications after deliver: {after_count}") + if after_count > 0: + updated = dbsession.query(NodeEventNotification).filter( + NodeEventNotification.sent == False + ).update({ + 'sent': True, + 'updated_timestamp': now_timestamp() + }) + + print(f"Marked {updated} remaining notifications as sent") + else: + print("All notifications already marked as sent by deliver function") + + dbsession.flush() + transaction.commit() + + + except Exception as e: - print(f"Error occurred: {e}") - transaction.abort() - raise \ No newline at end of file + print(f"Error: {e}") + raise + +if __name__ == "__main__": + main() \ No newline at end of file From 079eb09ad4c02b950f2b9079d773b86b8743aac4 Mon Sep 17 00:00:00 2001 From: Groupr Date: Mon, 21 Jul 2025 19:56:32 +0000 Subject: [PATCH 3/4] Update send_node_digest_notifications.py --- remarkbox/scripts/send_node_digest_notifications.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/remarkbox/scripts/send_node_digest_notifications.py b/remarkbox/scripts/send_node_digest_notifications.py index f9bb88b..6727bcb 100644 --- a/remarkbox/scripts/send_node_digest_notifications.py +++ b/remarkbox/scripts/send_node_digest_notifications.py @@ -34,7 +34,8 @@ def main(): # Let deliver_scheduled_notifications manage its own transaction deliver_scheduled_notifications(request) - + dbsession.flush() # Ensure changes are flushed before commit + transaction.commit() # Check after and mark as sent with fresh session try: with transaction.manager: From 047a6db18b6e58baa37c9227f349cb9079ff1481 Mon Sep 17 00:00:00 2001 From: Groupr Date: Mon, 21 Jul 2025 20:31:11 +0000 Subject: [PATCH 4/4] Update send_node_digest_notifications.py --- .../scripts/send_node_digest_notifications.py | 199 +++++++++++++----- 1 file changed, 149 insertions(+), 50 deletions(-) diff --git a/remarkbox/scripts/send_node_digest_notifications.py b/remarkbox/scripts/send_node_digest_notifications.py index 6727bcb..cf61eec 100644 --- a/remarkbox/scripts/send_node_digest_notifications.py +++ b/remarkbox/scripts/send_node_digest_notifications.py @@ -1,73 +1,172 @@ -from pyramid.paster import bootstrap, setup_logging +#!/usr/bin/env python3 +""" +Send Node Digest Notifications Script + +This script sends scheduled node event notifications via email digest and ensures +all sent notifications are properly marked as sent in the database to prevent +duplicate notifications on subsequent runs. + +Usage: + remarkbox_send_node_digest_notifications production.ini +""" + +import logging import transaction +from pyramid.paster import bootstrap, setup_logging from ..lib.notify import deliver_scheduled_notifications from ..models import get_tm_session from ..models.notification import NodeEventNotification from ..models.meta import now_timestamp - from . import base_parser +log = logging.getLogger(__name__) + def get_arg_parser(): - parser = base_parser("Send Node Notification Digests.") - return parser + """Configure command line argument parser.""" + return base_parser("Send Node Notification Digests.") + + +def should_have_been_sent(notification): + """ + Check if notification should have been sent based on frequency and age. + + Args: + notification: NodeEventNotification object to check + + Returns: + bool: True if notification should have been sent, False otherwise + """ + frequency = notification.frequency + age_ms = now_timestamp() - notification.created_timestamp + + if frequency == "immediately": + return True + elif frequency == "daily": + # Should be sent if older than 24 hours + return age_ms >= (24 * 60 * 60 * 1000) + elif frequency == "weekly": + # Should be sent if older than 7 days + return age_ms >= (7 * 24 * 60 * 60 * 1000) + elif frequency == "never": + return False + + return False + + +def get_unsent_notification_count(request): + """ + Get count of unsent notifications using a fresh database session. + + Args: + request: Pyramid request object + + Returns: + int: Number of unsent notifications + """ + with transaction.manager: + dbsession = get_tm_session( + request.registry["dbsession_factory"], transaction.manager + ) + return dbsession.query(NodeEventNotification).filter( + NodeEventNotification.sent == False + ).count() + + +def mark_ready_notifications_as_sent(request): + """ + Mark notifications as sent if they were ready to be delivered based on frequency rules. + + Args: + request: Pyramid request object + + Returns: + tuple: (total_unsent, marked_as_sent, left_for_later) + """ + with transaction.manager: + dbsession = get_tm_session( + request.registry["dbsession_factory"], transaction.manager + ) + + # Get all unsent notifications + unsent_notifications = dbsession.query(NodeEventNotification).filter( + NodeEventNotification.sent == False + ).all() + + total_unsent = len(unsent_notifications) + + if total_unsent == 0: + return total_unsent, 0, 0 + + # Only mark notifications that should have been sent based on frequency + ready_notification_ids = [ + n.id for n in unsent_notifications + if should_have_been_sent(n) + ] + + marked_as_sent = 0 + if ready_notification_ids: + marked_as_sent = dbsession.query(NodeEventNotification).filter( + NodeEventNotification.id.in_(ready_notification_ids) + ).update({ + 'sent': True, + 'updated_timestamp': now_timestamp() + }, synchronize_session=False) + + # Ensure changes are persisted + dbsession.flush() + transaction.commit() + + left_for_later = total_unsent - marked_as_sent + return total_unsent, marked_as_sent, left_for_later def main(): + """Main entry point for the digest notification script.""" parser = get_arg_parser() args = parser.parse_args() setup_logging(args.config) - with bootstrap(args.config) as env: - request = env["request"] - - # Check before with fresh session - with transaction.manager: - dbsession = get_tm_session( - request.registry["dbsession_factory"], transaction.manager - ) - before_count = dbsession.query(NodeEventNotification).filter( - NodeEventNotification.sent == False - ).count() - print(f"Unsent notifications before: {before_count}") - - # Let deliver_scheduled_notifications manage its own transaction - deliver_scheduled_notifications(request) - dbsession.flush() # Ensure changes are flushed before commit - transaction.commit() - # Check after and mark as sent with fresh session - try: - with transaction.manager: - dbsession = get_tm_session( - request.registry["dbsession_factory"], transaction.manager - ) - - after_count = dbsession.query(NodeEventNotification).filter( - NodeEventNotification.sent == False - ).count() - print(f"Unsent notifications after deliver: {after_count}") - - if after_count > 0: - updated = dbsession.query(NodeEventNotification).filter( - NodeEventNotification.sent == False - ).update({ - 'sent': True, - 'updated_timestamp': now_timestamp() - }) - - print(f"Marked {updated} remaining notifications as sent") - else: - print("All notifications already marked as sent by deliver function") + log.info("Starting node digest notification delivery") + + try: + with bootstrap(args.config) as env: + request = env["request"] - dbsession.flush() + # Check initial state + before_count = get_unsent_notification_count(request) + log.info(f"Found {before_count} unsent notifications before delivery") + + if before_count == 0: + log.info("No notifications to process") + return + + # Deliver scheduled notifications + log.info("Delivering scheduled notifications...") + deliver_scheduled_notifications(request) transaction.commit() + + # Check final state and mark ready notifications as sent + log.info("Checking for notifications that need to be marked as sent...") + total_unsent, marked_as_sent, left_for_later = mark_ready_notifications_as_sent(request) + + # Log results + if total_unsent == 0: + log.info("All notifications were properly marked as sent by delivery function") + elif marked_as_sent > 0: + log.info(f"Marked {marked_as_sent} ready notifications as sent") + if left_for_later > 0: + log.info(f"Left {left_for_later} notifications for later delivery based on frequency rules") + else: + log.info("No notifications were ready to be sent based on frequency rules") + + log.info("Node digest notification delivery completed successfully") + + except Exception as e: + log.error(f"Error during digest notification delivery: {e}") + raise - - except Exception as e: - print(f"Error: {e}") - raise - if __name__ == "__main__": main() \ No newline at end of file