Ejemplo n.º 1
0
    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
Ejemplo n.º 2
0
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)
Ejemplo n.º 3
0
    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)
Ejemplo n.º 4
0
    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
                    }
                }
            })
Ejemplo n.º 5
0
    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)
Ejemplo n.º 6
0
    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'])
Ejemplo n.º 7
0
    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))
Ejemplo n.º 8
0
    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)
Ejemplo n.º 9
0
    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)
Ejemplo n.º 10
0
    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)
Ejemplo n.º 12
0
    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)
Ejemplo n.º 13
0
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
Ejemplo n.º 14
0
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)