def test_recurring_timer(self): """ Make sure recurring timers work """ timer = NotificationCallbackTimer( name='foo', class_name= 'edx_notifications.tests.test_timer.NullNotificationCallbackTimerHandler', callback_at=datetime.now(pytz.UTC) - timedelta(days=1), context={}, is_active=True, periodicity_min=1) self.store.save_notification_timer(timer) poll_and_execute_timers() timer1 = self.store.get_notification_timer(timer.name) self.assertIsNone( timer1.executed_at) # should be marked as still to execute self.assertIsNone(timer1.err_msg) self.assertNotEqual( timer.callback_at, timer1.callback_at) # verify the callback time is incremented
def register_purge_notifications_timer(sender, **kwargs): # pylint: disable=unused-argument """ Register PurgeNotificationsCallbackHandler. This will be called automatically on the Notification subsystem startup (because we are receiving the 'perform_timer_registrations' signal) """ store = notification_store() try: store.get_notification_timer(PURGE_NOTIFICATIONS_TIMER_NAME) except ItemNotFoundError: # Set first execution time at upcoming 1:00 AM (1 hour after midnight). first_execution_at = (datetime.now(pytz.UTC) + timedelta(days=1)).replace(hour=1, minute=0, second=0) purge_notifications_timer = NotificationCallbackTimer( name=PURGE_NOTIFICATIONS_TIMER_NAME, callback_at=first_execution_at, class_name= 'edx_notifications.callbacks.PurgeNotificationsCallbackHandler', is_active=True, periodicity_min=const.MINUTES_IN_A_DAY) store.save_notification_timer(purge_notifications_timer)
def test_update_is_active_timer(self): """ Verify that we can change the is_active flag on a timer """ timer = NotificationCallbackTimer( name='timer1', callback_at=datetime.now(pytz.UTC) - timedelta(0, 1), class_name='foo.bar', context={ 'one': 'two' }, is_active=True, periodicity_min=120, ) timer_saved = self.provider.save_notification_timer(timer) timer_read = self.provider.get_notification_timer(timer_saved.id) timer_read.is_active = False timer_saved_twice = self.provider.save_notification_timer(timer_read) timer_read = self.provider.get_notification_timer(timer_saved_twice.id) self.assertEqual(timer_saved_twice, timer_read)
def setUp(self): """ start up stuff """ register_user_scope_resolver('list_scope', TestListScopeResolver()) self.store = notification_store() self.callback = NotificationDispatchMessageCallback() self.msg_type = self.store.save_notification_type( NotificationType( name='foo.bar', renderer='foo', )) self.msg = self.store.save_notification_message( NotificationMessage( msg_type=self.msg_type, payload={'foo': 'bar'}, )) self.timer_for_user = NotificationCallbackTimer( context={ 'msg_id': self.msg.id, 'distribution_scope': { 'scope_name': 'user', 'scope_context': { 'user_id': 1 } } }) self.timer_for_group = NotificationCallbackTimer( context={ 'msg_id': self.msg.id, 'distribution_scope': { 'scope_name': 'list_scope', 'scope_context': { 'range': 5 } } })
def test_save_timer(self): """ Save, update, and get a simple timer object """ timer = NotificationCallbackTimer( name='timer1', callback_at=datetime.now(pytz.UTC) - timedelta(0, 1), class_name='foo.bar', context={ 'one': 'two' }, is_active=True, periodicity_min=120, ) timer_saved = self.provider.save_notification_timer(timer) timer_executed = NotificationCallbackTimer( name='timer2', callback_at=datetime.now(pytz.UTC) - timedelta(0, 2), class_name='foo.bar', context={ 'one': 'two' }, is_active=True, periodicity_min=120, executed_at=datetime.now(pytz.UTC), err_msg='ooops', ) timer_executed_saved = self.provider.save_notification_timer(timer_executed) timer_read = self.provider.get_notification_timer(timer_saved.name) self.assertEqual(timer_saved, timer_read) self.assertTrue(isinstance(timer_read.context, dict)) timer_executed_read = self.provider.get_notification_timer(timer_executed_saved.name) self.assertEqual(timer_executed_saved, timer_executed_read) timers_not_executed = self.provider.get_all_active_timers() self.assertEqual(len(timers_not_executed), 1) timers_incl_executed = self.provider.get_all_active_timers(include_executed=True) self.assertEqual(len(timers_incl_executed), 2)
def test_bad_context(self): """ Test missing context parameter """ bad_timer = NotificationCallbackTimer( context={ # missing msg_id 'distribution_scope': { 'scope_name': 'user', 'scope_context': { 'user_id': 1 } } }) results = self.callback.notification_timer_callback(bad_timer) self.assertIsNotNone(results) self.assertEqual(results['num_dispatched'], 0) self.assertEqual(len(results['errors']), 1) self.assertIsNone(results['reschedule_in_mins']) bad_timer = NotificationCallbackTimer( context={ 'msg_id': self.msg.id, 'distribution_scope': { 'scope_name': 'user', 'scope_context': { # missing user_id } } }) results = self.callback.notification_timer_callback(bad_timer) self.assertIsNotNone(results) self.assertEqual(results['num_dispatched'], 0) self.assertEqual(len(results['errors']), 1) self.assertIsNone(results['reschedule_in_mins'])
def to_data_object(self, options=None): # pylint: disable=unused-argument """ Generate a NotificationType data object """ return NotificationCallbackTimer( name=self.name, callback_at=self.callback_at, class_name=self.class_name, context=DictField.from_json( self.context), # special case, dict<-->JSON string is_active=self.is_active, periodicity_min=self.periodicity_min, # pylint: disable=no-member executed_at=self.executed_at, err_msg=self.err_msg, created=self.created, modified=self.modified, results=DictField.from_json(self.results))
def test_bad_handler(self): """ Make sure that a timer with a bad class_name doesn't operate """ timer = NotificationCallbackTimer( name='foo', class_name='edx_notifications.badmodule.BadHandler', callback_at=datetime.now(pytz.UTC) - timedelta(days=1), context={}, is_active=True) self.store.save_notification_timer(timer) poll_and_execute_timers() updated_timer = self.store.get_notification_timer(timer.name) self.assertIsNotNone(updated_timer.executed_at) self.assertIsNotNone(updated_timer.err_msg)
def test_error_in_execution(self): """ Make sure recurring timers work """ timer = NotificationCallbackTimer( name='foo', class_name='edx_notifications.tests.test_timer.ExceptionNotificationCallbackTimerHandler', callback_at=datetime.now(pytz.UTC) - timedelta(days=1), context={}, is_active=True ) self.store.save_notification_timer(timer) poll_and_execute_timers() updated_timer = self.store.get_notification_timer(timer.name) self.assertIsNotNone(updated_timer.executed_at) self.assertIsNotNone(updated_timer.err_msg)
def test_cant_find_msg(self): """ Test timer that points to a non-existing msg """ bad_timer = NotificationCallbackTimer( context={ 'msg_id': 9999, 'distribution_scope': { 'scope_name': 'user', 'scope_context': { 'user_id': 1 } } }) results = self.callback.notification_timer_callback(bad_timer) self.assertIsNotNone(results) self.assertEqual(results['num_dispatched'], 0) self.assertEqual(len(results['errors']), 1) self.assertIsNone(results['reschedule_in_mins'])
def test_timer_execution(self): """ Make sure that Django management command runs through the timers """ timer = NotificationCallbackTimer( name='foo', class_name='edx_notifications.tests.test_timer.NullNotificationCallbackTimerHandler', callback_at=datetime.now(pytz.UTC) - timedelta(days=1), context={}, is_active=True, ) notification_store().save_notification_timer(timer) background_notification_check.Command().handle() readback_timer = notification_store().get_notification_timer(timer.name) self.assertIsNotNone(readback_timer.executed_at) self.assertIsNone(readback_timer.err_msg)
def test_save_update_time(self): """ Verify the update case of saving a timer """ timer = NotificationCallbackTimer( name='timer1', callback_at=datetime.now(pytz.UTC) - timedelta(0, 1), class_name='foo.bar', context={'one': 'two'}, is_active=True, periodicity_min=120, ) timer_saved = self.provider.save_notification_timer(timer) timer_saved.executed_at = datetime.now(pytz.UTC) timer_saved.err_msg = "Ooops" timer_saved_twice = self.provider.save_notification_timer(timer) timer_read = self.provider.get_notification_timer(timer_saved_twice.id) self.assertEqual(timer_saved_twice, timer_read)
def publish_timed_notification(msg, send_at, scope_name, scope_context, timer_name=None, ignore_if_past_due=False, timer_context=None): # pylint: disable=too-many-arguments """ Registers a new notification message to be dispatched at a particular time. IMPORTANT: There can only be one timer associated with a notification message. If it is called more than once on the same msg_id, then the existing one is updated. ARGS: send_at: datetime when the message should be sent msg: An instance of a NotificationMessage distribution_scope: enum of three values: 'user', 'course_group', 'course_enrollments' which describe the distribution scope of the message scope_context: if scope='user': then {'user_id': xxxx } if scope='course_group' then {'course_id': xxxx, 'group_id': xxxxx} if scope='course_enrollments' then {'course_id'} timer_name: if we know the name of the timer we want to use rather than auto-generating it. use caution not to mess with other code's timers!!! ignore_if_past_due: If the notification should not be put into the timers, if the send date is in the past RETURNS: instance of NotificationCallbackTimer """ now = datetime.datetime.now(pytz.UTC) if now > send_at and ignore_if_past_due: log.info( 'Timed Notification is past due and the caller said to ignore_if_past_due. Dropping notification...' ) if timer_name: # If timer is named and it is past due, it is possibly being updated # so, then we should remove any previously stored # timed notification cancel_timed_notification(timer_name, exception_on_not_found=False) return None # make sure we can resolve the scope_name if not has_user_scope_resolver(scope_name): err_msg = ( 'There is no registered scope resolver for scope_name "{name}"' ).format(name=scope_name) raise ValueError(err_msg) store = notification_store() # make sure we put the delivery timestamp on the message as well msg.deliver_no_earlier_than = send_at saved_msg = store.save_notification_message(msg) _timer_name = timer_name if timer_name else f'notification-dispatch-timer-{saved_msg.id}' log_msg = ( 'Publishing timed Notification named "{timer_name}" to scope name "{scope_name}" and scope ' 'context {scope_context} to be sent at "{send_at} with message: {msg}' ).format(timer_name=_timer_name, scope_name=scope_name, scope_context=scope_context, send_at=send_at, msg=msg) log.info(log_msg) _timer_context = copy.deepcopy(timer_context) if timer_context else {} # add in the context that is predefined _timer_context.update({ 'msg_id': saved_msg.id, 'distribution_scope': { 'scope_name': scope_name, 'scope_context': scope_context, } }) timer = NotificationCallbackTimer( name=_timer_name, callback_at=send_at, class_name= 'edx_notifications.callbacks.NotificationDispatchMessageCallback', is_active=True, context=_timer_context) saved_timer = store.save_notification_timer(timer) return saved_timer
def register_digest_timers(sender, **kwargs): # pylint: disable=unused-argument """ Register NotificationCallbackTimerHandler. This will be called automatically on the Notification subsystem startup (because we are receiving the 'perform_timer_registrations' signal) """ store = notification_store() # Set first execution time as upcoming midnight after the server is run for the first time. first_execution_at = datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=1) first_execution_at = first_execution_at.replace(hour=0, minute=0, second=0, microsecond=0) periodicity_min = const.MINUTES_IN_A_DAY context = { 'is_daily_digest': True, 'preference_name': const.NOTIFICATION_DAILY_DIGEST_PREFERENCE_NAME, 'subject': const.NOTIFICATION_DAILY_DIGEST_SUBJECT, 'from_email': const.NOTIFICATION_EMAIL_FROM_ADDRESS, 'unread_only': const.NOTIFICATION_DIGEST_UNREAD_ONLY, } # see if we have an existing timer set, and preserve that execution time # that is, don't reset it across startups try: existing_digest_timer = store.get_notification_timer(const.DAILY_DIGEST_TIMER_NAME) if existing_digest_timer: first_execution_at = existing_digest_timer.callback_at periodicity_min = existing_digest_timer.periodicity_min new_context = copy.deepcopy(existing_digest_timer.context) new_context.update(context) context = copy.deepcopy(new_context) except ItemNotFoundError: pass daily_digest_timer = NotificationCallbackTimer( name=const.DAILY_DIGEST_TIMER_NAME, callback_at=first_execution_at, class_name='edx_notifications.digests.NotificationDigestMessageCallback', is_active=True, periodicity_min=periodicity_min, context=context ) store.save_notification_timer(daily_digest_timer) periodicity_min = const.MINUTES_IN_A_WEEK context = { 'is_daily_digest': False, 'preference_name': const.NOTIFICATION_WEEKLY_DIGEST_PREFERENCE_NAME, 'subject': const.NOTIFICATION_WEEKLY_DIGEST_SUBJECT, 'from_email': const.NOTIFICATION_EMAIL_FROM_ADDRESS, 'unread_only': const.NOTIFICATION_DIGEST_UNREAD_ONLY, } # see if we have an existing timer set, and preserve that execution time # that is, don't reset it across startups try: existing_digest_timer = store.get_notification_timer(const.WEEKLY_DIGEST_TIMER_NAME) if existing_digest_timer: first_execution_at = existing_digest_timer.callback_at periodicity_min = existing_digest_timer.periodicity_min new_context = copy.deepcopy(existing_digest_timer.context) new_context.update(context) context = copy.deepcopy(new_context) except ItemNotFoundError: pass weekly_digest_timer = NotificationCallbackTimer( name=const.WEEKLY_DIGEST_TIMER_NAME, callback_at=first_execution_at, class_name='edx_notifications.digests.NotificationDigestMessageCallback', is_active=True, periodicity_min=periodicity_min, context=context ) store.save_notification_timer(weekly_digest_timer)