From 5640a6f35803cb47a54ee95b1de941a6c2297ad3 Mon Sep 17 00:00:00 2001 From: Shafqat Farhan Date: Tue, 1 Jan 2019 19:16:04 +0500 Subject: [PATCH] YONK-949 - Reset notification digest timers management command added (#187) * Django management command added to reset notification digest timers * setup.py version update --- .../commands/reset_notification_timer.py | 118 ++++++++++++++++++ .../tests/test_reset_notification_timer.py | 73 +++++++++++ setup.py | 2 +- 3 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 edx_notifications/management/commands/reset_notification_timer.py create mode 100644 edx_notifications/management/commands/tests/test_reset_notification_timer.py diff --git a/edx_notifications/management/commands/reset_notification_timer.py b/edx_notifications/management/commands/reset_notification_timer.py new file mode 100644 index 00000000..19c2dd56 --- /dev/null +++ b/edx_notifications/management/commands/reset_notification_timer.py @@ -0,0 +1,118 @@ +""" +Django management command to fetch records from SQLNotificationCallbackTimer with name +'daily-digest-timer' and 'weekly-digest-timer' and reset their 'callback_at' and 'last_ran' values + to avoid sending old notification digest emails to users. +""" + +import logging.config +import sys +import pytz + +from datetime import datetime, timedelta +from django.core.management.base import BaseCommand + +from edx_notifications.exceptions import ItemNotFoundError +from edx_notifications.stores.store import notification_store +from edx_notifications.timer import PURGE_NOTIFICATIONS_TIMER_NAME +from edx_notifications import const + +# Have all logging go to stdout with management commands +# this must be up at the top otherwise the +# configuration does not appear to take affect +LOGGING = { + 'version': 1, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'stream': sys.stdout, + } + }, + 'root': { + 'handlers': ['console'], + 'level': 'INFO' + } +} +logging.config.dictConfig(LOGGING) + +log = logging.getLogger(__file__) + + +class Command(BaseCommand): + """ + Django management command to fetch records from SQLNotificationCallbackTimer with name + 'daily-digest-timer' and 'weekly-digest-timer' and reset their 'callback_at' and 'last_ran' + values to avoid sending old notification digest emails to users + """ + + help = 'Command to reset notification digest emails timer' + INCLUDED_TIMERS = [ + const.DAILY_DIGEST_TIMER_NAME, + const.WEEKLY_DIGEST_TIMER_NAME, + PURGE_NOTIFICATIONS_TIMER_NAME, + ] + store = notification_store() + + def cancel_old_notification_timers(self): + """ + Get all the old timers, other than daily/weekly digests timers, that are not executed and + cancel them as executing them will generate a lot of notifications. + """ + timers_not_executed = self.store.get_all_active_timers() + + for timer in timers_not_executed: + if timer.name not in self.INCLUDED_TIMERS: + log.info( + 'Cancelling timed Notification named {timer}...'.format(timer=str(timer.name))) + + timer.is_active = False + self.store.save_notification_timer(timer) + + def reset_digest_notification_timer(self): + context = { + 'last_ran': datetime.now(pytz.UTC), + } + + try: + digest_timer = self.store.get_notification_timer(const.DAILY_DIGEST_TIMER_NAME) + if digest_timer: + log.info("Resetting notification digest timer for daily digests.") + + rerun_delta = (digest_timer.periodicity_min + if digest_timer.periodicity_min + else const.MINUTES_IN_A_DAY) + digest_timer.callback_at = datetime.now(pytz.UTC) + timedelta(minutes=rerun_delta) + digest_timer.context.update(context) + + self.store.save_notification_timer(digest_timer) + except ItemNotFoundError: + log.info("Daily digests timer not found.") + + try: + digest_timer = self.store.get_notification_timer(const.WEEKLY_DIGEST_TIMER_NAME) + if digest_timer: + log.info("Resetting notification digest timer for weekly digests.") + + rerun_delta = (digest_timer.periodicity_min + if digest_timer.periodicity_min + else const.MINUTES_IN_A_WEEK) + digest_timer.callback_at = datetime.now(pytz.UTC) + timedelta(minutes=rerun_delta) + digest_timer.context.update(context) + + self.store.save_notification_timer(digest_timer) + except ItemNotFoundError: + log.info("Weekly digests timer not found.") + + def handle(self, *args, **options): + """ + Management command entry point, simply calls: + 1. cancel_old_notification_timers to cancel all old notification timers other than + digest timers + 2. store provider's get_notification_timer to update the callback_at and last_ran values. + """ + + log.info("Running management command to reset notification digest timer.") + + self.cancel_old_notification_timers() + self.reset_digest_notification_timer() + + log.info("Completed reset_notification_timer.") diff --git a/edx_notifications/management/commands/tests/test_reset_notification_timer.py b/edx_notifications/management/commands/tests/test_reset_notification_timer.py new file mode 100644 index 00000000..e94177da --- /dev/null +++ b/edx_notifications/management/commands/tests/test_reset_notification_timer.py @@ -0,0 +1,73 @@ +""" +Tests for the Django management command reset_notification_timer +""" + +import pytz +from datetime import datetime, timedelta + +from django.test import TestCase + +from edx_notifications.management.commands import reset_notification_timer +from edx_notifications.stores.store import notification_store +from edx_notifications.data import NotificationCallbackTimer +from edx_notifications import const + + +class ResetTimerTest(TestCase): + """ + Test suite for the management command + """ + + def setUp(self): + """ + Setup the tests values. + """ + + self.store = notification_store() + self.timer1 = self.register_timer('foo') + self.timer2 = self.register_timer(const.DAILY_DIGEST_TIMER_NAME, const.MINUTES_IN_A_DAY) + self.timer3 = self.register_timer(const.WEEKLY_DIGEST_TIMER_NAME, const.MINUTES_IN_A_WEEK) + + def register_timer(self, timer_name, periodicity_min=60): + timer = NotificationCallbackTimer( + name=timer_name, + class_name='edx_notifications.tests.test_timer.NullNotificationCallbackTimerHandler', + callback_at=datetime.now(pytz.UTC) - timedelta(days=1), + context={ + 'foo': 'bar' + }, + is_active=True, + periodicity_min=periodicity_min, + ) + + return self.store.save_notification_timer(timer) + + def test_timer_execution(self): + """ + Make sure that Django management command runs through the timers + """ + + reset_notification_timer.Command().handle() + + readback_timer = self.store.get_notification_timer(self.timer1.name) + + self.assertIsNone(readback_timer.executed_at) + self.assertFalse(readback_timer.is_active) + self.assertIsNone(readback_timer.err_msg) + + daily_digest_timer = self.store.get_notification_timer(self.timer2.name) + reset_time = datetime.now(pytz.UTC) + timedelta(minutes=daily_digest_timer.periodicity_min) + + self.assertIn('last_ran', daily_digest_timer.context) + self.assertTrue(isinstance(daily_digest_timer.context['last_ran'], datetime)) + self.assertTrue(daily_digest_timer.context['last_ran'] < reset_time) + self.assertIsNone(daily_digest_timer.executed_at) + self.assertTrue(daily_digest_timer.callback_at > datetime.now(pytz.UTC)) + + weekly_digest_timer = self.store.get_notification_timer(self.timer3.name) + reset_time = datetime.now(pytz.UTC) + timedelta(days=6) + + self.assertIsNone(weekly_digest_timer.executed_at) + self.assertTrue(weekly_digest_timer.is_active) + self.assertIn('last_ran', weekly_digest_timer.context) + self.assertTrue(weekly_digest_timer.callback_at > reset_time) diff --git a/setup.py b/setup.py index c178cab3..0739fbfb 100755 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ def load_requirements(*requirements_paths): setup( name='edx-notifications', - version='0.8.1', + version='0.8.2', description='Notification subsystem for Open edX', long_description=open('README.md').read(), author='edX',