def test_wildcard_group_mapping(self):
        """
        Test that adds the default notification type mapping
        """
        msg_type = self.store.save_notification_type(
            NotificationType(
                name='open-edx.lms.discussions.new-discussion-added',
                renderer='open-edx.lms.discussions.new-discussion-added',
            )
        )
        # create cohort notification
        msg = self.store.save_notification_message(
            NotificationMessage(
                msg_type=msg_type,
                namespace='cohort-thread-added',
                payload={'subject': 'foo', 'body': 'bar'},
            )
        )
        publish_notification_to_user(self.test_user_id, msg)

        register_namespace_resolver(TestNamespaceResolver())

        set_user_notification_preference(self.test_user_id, const.NOTIFICATION_DAILY_DIGEST_PREFERENCE_NAME, 'true')

        self.assertEqual(
            send_notifications_digest(
                self.from_timestamp,
                self.to_timestamp,
                const.NOTIFICATION_DAILY_DIGEST_PREFERENCE_NAME,
                'subject',
                '*****@*****.**'
            ),
            3
        )
Example #2
0
    def test_link_resolution(self):
        """
        Go through and set up a notification and publish it but with
        links to resolve
        """

        msg = NotificationMessage(namespace="test-runner", msg_type=self.msg_type, payload={"foo": "bar"})

        # this resolve_links resolutions are defined in settings.py
        # for testing purposes
        msg.add_click_link_params({"param1": "foo_param", "param2": "bar_param"})

        # make sure it asserts that user_id is an integer
        with self.assertRaises(ContractNotRespected):
            publish_notification_to_user("bad-id", msg)

        # now do happy path
        sent_user_msg = publish_notification_to_user(self.test_user_id, msg)

        # now make sure the links got resolved and put into
        # the payload
        self.assertIsNotNone(sent_user_msg.msg.get_click_link())

        # make sure the resolution is what we expect
        # NOTE: the mappings are defined in settings.py for testing purposes
        self.assertEqual(sent_user_msg.msg.get_click_link(), "/path/to/foo_param/url/bar_param")

        # now do it all over again since there is caching of link resolvers
        sent_user_msg = publish_notification_to_user(self.test_user_id, msg)
        self.assertTrue(sent_user_msg.msg.get_click_link())
        self.assertEqual(sent_user_msg.msg.get_click_link(), "/path/to/foo_param/url/bar_param")
Example #3
0
    def test_mark_all_as_read(self):
        """
        Verify proper behavior when marking user notifications as read/unread
        """
        for __ in range(10):
            msg = NotificationMessage(namespace="test-runner", msg_type=self.msg_type, payload={"foo": "bar"})
            publish_notification_to_user(self.test_user_id, msg)

        # make sure we have 10 unreads before we do anything else
        self.assertEquals(
            get_notifications_count_for_user(self.test_user_id, filters={"read": False, "unread": True}), 10
        )

        # now mark msg as read by this user
        mark_all_user_notification_as_read(self.test_user_id)

        # shouldn't be counted in unread counts
        self.assertEquals(
            get_notifications_count_for_user(self.test_user_id, filters={"read": False, "unread": True}), 0
        )

        # Should be counted in read counts
        self.assertEquals(
            get_notifications_count_for_user(self.test_user_id, filters={"read": True, "unread": False}), 10
        )
def handle_progress_post_save_signal(sender, instance, **kwargs):
    """
    Handle the pre-save ORM event on CourseModuleCompletions
    """

    if settings.FEATURES['ENABLE_NOTIFICATIONS']:
        # If notifications feature is enabled, then we need to get the user's
        # rank before the save is made, so that we can compare it to
        # after the save and see if the position changes

        leaderboard_rank = StudentSocialEngagementScore.get_user_leaderboard_position(
            instance.course_id,
            user_id=instance.user.id,
            exclude_users=get_aggregate_exclusion_user_ids(instance.course_id)
        )['position']

        if leaderboard_rank == 0:
            # quick escape when user is not in the leaderboard
            # which means rank = 0. Trouble is 0 < 3, so unfortunately
            # the semantics around 0 don't match the logic below
            return

        # logic for Notification trigger is when a user enters into the Leaderboard
        leaderboard_size = getattr(settings, 'LEADERBOARD_SIZE', 3)
        presave_leaderboard_rank = instance.presave_leaderboard_rank if instance.presave_leaderboard_rank else sys.maxint
        if leaderboard_rank <= leaderboard_size and presave_leaderboard_rank > leaderboard_size:
            try:
                notification_msg = NotificationMessage(
                    msg_type=get_notification_type(u'open-edx.lms.leaderboard.engagement.rank-changed'),
                    namespace=unicode(instance.course_id),
                    payload={
                        '_schema_version': '1',
                        'rank': leaderboard_rank,
                        'leaderboard_name': 'Engagement',
                    }
                )

                #
                # add in all the context parameters we'll need to
                # generate a URL back to the website that will
                # present the new course announcement
                #
                # IMPORTANT: This can be changed to msg.add_click_link() if we
                # have a particular URL that we wish to use. In the initial use case,
                # we need to make the link point to a different front end website
                # so we need to resolve these links at dispatch time
                #
                notification_msg.add_click_link_params({
                    'course_id': unicode(instance.course_id),
                })

                publish_notification_to_user(int(instance.user.id), notification_msg)
            except Exception, ex:
                # Notifications are never critical, so we don't want to disrupt any
                # other logic processing. So log and continue.
                log.exception(ex)
def handle_studentgradebook_post_save_signal(sender, instance, **kwargs):
    """
    Handle the pre-save ORM event on CourseModuleCompletions
    """
    invalid_user_data_cache('grade', instance.course_id, instance.user.id)

    if settings.FEATURES['ENABLE_NOTIFICATIONS']:
        # attach the rank of the user before the save is completed
        data = StudentGradebook.get_user_position(
            instance.course_id,
            user_id=instance.user.id,
            exclude_users=get_aggregate_exclusion_user_ids(instance.course_id)
        )

        leaderboard_rank = data['user_position']
        grade = data['user_grade']

        # logic for Notification trigger is when a user enters into the Leaderboard
        if grade > 0.0:
            leaderboard_size = getattr(settings, 'LEADERBOARD_SIZE', 3)
            presave_leaderboard_rank = instance.presave_leaderboard_rank if instance.presave_leaderboard_rank else sys.maxint
            if leaderboard_rank <= leaderboard_size and presave_leaderboard_rank > leaderboard_size:
                try:
                    notification_msg = NotificationMessage(
                        msg_type=get_notification_type(u'open-edx.lms.leaderboard.gradebook.rank-changed'),
                        namespace=unicode(instance.course_id),
                        payload={
                            '_schema_version': '1',
                            'rank': leaderboard_rank,
                            'leaderboard_name': 'Proficiency',
                        }
                    )

                    #
                    # add in all the context parameters we'll need to
                    # generate a URL back to the website that will
                    # present the new course announcement
                    #
                    # IMPORTANT: This can be changed to msg.add_click_link() if we
                    # have a particular URL that we wish to use. In the initial use case,
                    # we need to make the link point to a different front end website
                    # so we need to resolve these links at dispatch time
                    #
                    notification_msg.add_click_link_params({
                        'course_id': unicode(instance.course_id),
                    })

                    publish_notification_to_user(int(instance.user.id), notification_msg)
                except Exception, ex:
                    # Notifications are never critical, so we don't want to disrupt any
                    # other logic processing. So log and continue.
                    log.exception(ex)
    def test_multiple_notifications(self):
        """
        Test Case for retrieving multiple notifications
        """

        msg1 = NotificationMessage(
            namespace='test-runner',
            msg_type=self.msg_type,
            payload={
                'foo': 'bar'
            }
        )

        msg2 = NotificationMessage(
            namespace='test-runner',
            msg_type=self.msg_type,
            payload={
                'second': 'one'
            }
        )

        # publish
        user_msg1 = publish_notification_to_user(self.user.id, msg1)
        user_msg2 = publish_notification_to_user(self.user.id, msg2)

        response = self.client.get(reverse('edx_notifications.consumer.notifications'))
        self.assertEqual(response.status_code, 200)

        results = json.loads(response.content)
        self.assertEqual(len(results), 2)

        # the last one written should be the first one read
        self._compare_user_msg_to_result(user_msg2, results[0])
        # the first one written should be second one received
        self._compare_user_msg_to_result(user_msg1, results[1])

        # now do query with a namespace filter
        response = self.client.get(
            reverse('edx_notifications.consumer.notifications'),
            {
                'namespace': 'test-runner'
            }
        )
        self.assertEqual(response.status_code, 200)

        results = json.loads(response.content)
        self.assertEqual(len(results), 2)  # did we get two back?

        self._compare_user_msg_to_result(user_msg2, results[0])
        self._compare_user_msg_to_result(user_msg1, results[1])
Example #7
0
    def test_multipayload(self):
        """
        Test that a channel will use the right payload
        """

        msg = NotificationMessage(namespace="test-runner", msg_type=self.msg_type, payload={"foo": "bar"})

        msg.add_payload({"one": "two"}, channel_name="durable")

        # now do happy path
        sent_user_msg = publish_notification_to_user(self.test_user_id, msg)

        # now query back the notification to make sure it got stored
        # and we can retrieve it
        self.assertEquals(get_notifications_count_for_user(self.test_user_id), 1)

        notifications = get_notifications_for_user(self.test_user_id)

        self.assertTrue(isinstance(notifications, list))
        self.assertEqual(len(notifications), 1)
        self.assertTrue(isinstance(notifications[0], UserNotification))

        read_user_msg = notifications[0]
        self.assertEqual(read_user_msg.user_id, self.test_user_id)
        self.assertIsNone(read_user_msg.read_at)  # should be unread

        self.assertEqual(read_user_msg, sent_user_msg)
        # make sure the message that got persisted contains only
        # the default payload
        self.assertEqual(read_user_msg.msg.payload, msg.get_payload(channel_name="durable"))
Example #8
0
    def test_marking_read_state(self):
        """
        Verify proper behavior when marking notfications as read/unread
        """

        msg = NotificationMessage(namespace="test-runner", msg_type=self.msg_type, payload={"foo": "bar"})

        # now do happy path
        sent_user_msg = publish_notification_to_user(self.test_user_id, msg)

        # now mark msg as read by this user
        mark_notification_read(self.test_user_id, sent_user_msg.msg.id)

        # shouldn't be counted in unread counts
        self.assertEquals(
            get_notifications_count_for_user(self.test_user_id, filters={"read": False, "unread": True}), 0
        )

        # Should be counted in read counts
        self.assertEquals(
            get_notifications_count_for_user(self.test_user_id, filters={"read": True, "unread": False}), 1
        )

        # now mark msg as unread by this user
        mark_notification_read(self.test_user_id, sent_user_msg.msg.id, read=False)

        # Should be counted in unread counts
        self.assertEquals(
            get_notifications_count_for_user(self.test_user_id, filters={"read": False, "unread": True}), 1
        )

        # Shouldn't be counted in read counts
        self.assertEquals(
            get_notifications_count_for_user(self.test_user_id, filters={"read": True, "unread": False}), 0
        )
    def test_someone_elses_notification(self):
        """
        Simple test to make sure that we can get counts for someone elses
        notification
        """

        msg = NotificationMessage(
            namespace='test-runner',
            msg_type=self.msg_type,
            payload={
                'foo': 'bar'
            }
        )

        # publish to some other user_id
        user_msg = publish_notification_to_user(99999, msg)
        self.assertIsNotNone(user_msg)

        # now query API
        response = self.client.get(reverse('edx_notifications.consumer.notifications.count'))
        self.assertEqual(response.status_code, 200)

        results = json.loads(response.content)
        self.assertIn('count', results)
        self.assertEqual(results['count'], 0)
    def setUp(self):
        """
        Initialize tests, by creating users and populating some
        unread notifications
        """
        create_default_notification_preferences()
        self.store = notification_store()
        self.test_user_id = 1001
        self.from_timestamp = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=1)
        self.weekly_from_timestamp = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=7)
        self.to_timestamp = datetime.datetime.now(pytz.UTC)

        self.msg_type = self.store.save_notification_type(
            NotificationType(
                name='foo.bar',
                renderer='edx_notifications.renderers.basic.BasicSubjectBodyRenderer',
            )
        )

        self.msg_type_no_renderer = self.store.save_notification_type(
            NotificationType(
                name='foo.baz',
                renderer='foo',
            )
        )

        # create two notifications
        msg = self.store.save_notification_message(
            NotificationMessage(
                msg_type=self.msg_type,
                namespace='foo',
                payload={'subject': 'foo', 'body': 'bar'},
            )
        )
        self.notification1 = publish_notification_to_user(self.test_user_id, msg)

        msg = self.store.save_notification_message(
            NotificationMessage(
                msg_type=self.msg_type_no_renderer,
                namespace='bar',
                payload={'subject': 'foo', 'body': 'bar'},
            )
        )
        self.notification2 = publish_notification_to_user(self.test_user_id, msg)
Example #11
0
    def test_mark_all_as_read(self):
        """
        Verify proper behavior when marking user notifications as read/unread
        """
        for __ in range(10):
            msg = NotificationMessage(namespace='test-runner',
                                      msg_type=self.msg_type,
                                      payload={'foo': 'bar'})
            publish_notification_to_user(self.test_user_id, msg)

        # make sure we have 10 unreads before we do anything else
        self.assertEqual(
            get_notifications_count_for_user(
                self.test_user_id,
                filters={
                    'read': False,
                    'unread': True,
                },
            ), 10)

        # now mark msg as read by this user
        mark_all_user_notification_as_read(self.test_user_id)

        # shouldn't be counted in unread counts
        self.assertEqual(
            get_notifications_count_for_user(
                self.test_user_id,
                filters={
                    'read': False,
                    'unread': True,
                },
            ), 0)

        # Should be counted in read counts
        self.assertEqual(
            get_notifications_count_for_user(
                self.test_user_id,
                filters={
                    'read': True,
                    'unread': False,
                },
            ), 10)
Example #12
0
    def test_multiple_notifications(self):
        """
        Test Case for retrieving multiple notifications
        """

        msg1 = NotificationMessage(namespace='test-runner',
                                   msg_type=self.msg_type,
                                   payload={'foo': 'bar'})

        msg2 = NotificationMessage(namespace='test-runner',
                                   msg_type=self.msg_type,
                                   payload={'second': 'one'})

        # publish
        user_msg1 = publish_notification_to_user(self.user.id, msg1)
        user_msg2 = publish_notification_to_user(self.user.id, msg2)

        response = self.client.get(
            reverse('edx_notifications.consumer.notifications'))
        self.assertEqual(response.status_code, 200)

        results = json.loads(response.content.decode('utf-8'))
        self.assertEqual(len(results), 2)

        # the last one written should be the first one read
        self._compare_user_msg_to_result(user_msg2, results[0])
        # the first one written should be second one received
        self._compare_user_msg_to_result(user_msg1, results[1])

        # now do query with a namespace filter
        response = self.client.get(
            reverse('edx_notifications.consumer.notifications'),
            {'namespace': 'test-runner'})
        self.assertEqual(response.status_code, 200)

        results = json.loads(response.content.decode('utf-8'))
        self.assertEqual(len(results), 2)  # did we get two back?

        self._compare_user_msg_to_result(user_msg2, results[0])
        self._compare_user_msg_to_result(user_msg1, results[1])
Example #13
0
    def test_link_resolution(self):
        """
        Go through and set up a notification and publish it but with
        links to resolve
        """

        msg = NotificationMessage(
            namespace='test-runner',
            msg_type=self.msg_type,
            payload={
                'foo': 'bar'
            }
        )

        # this resolve_links resolutions are defined in settings.py
        # for testing purposes
        msg.add_click_link_params({
            'param1': 'foo_param',
            'param2': 'bar_param',
        })

        # make sure it asserts that user_id is an integer
        with self.assertRaises(ContractNotRespected):
            publish_notification_to_user('bad-id', msg)

        # now do happy path
        sent_user_msg = publish_notification_to_user(self.test_user_id, msg)

        # now make sure the links got resolved and put into
        # the payload
        self.assertIsNotNone(sent_user_msg.msg.get_click_link())

        # make sure the resolution is what we expect
        # NOTE: the mappings are defined in settings.py for testing purposes
        self.assertEqual(sent_user_msg.msg.get_click_link(), '/path/to/foo_param/url/bar_param')

        # now do it all over again since there is caching of link resolvers
        sent_user_msg = publish_notification_to_user(self.test_user_id, msg)
        self.assertTrue(sent_user_msg.msg.get_click_link())
        self.assertEqual(sent_user_msg.msg.get_click_link(), '/path/to/foo_param/url/bar_param')
Example #14
0
    def test_marking_invalid_msg_read(self):
        """
        Makes sure that we can't mark an invalid notification, e.g. someone elses
        """

        msg = NotificationMessage(namespace="test-runner", msg_type=self.msg_type, payload={"foo": "bar"})

        # publish that
        sent_user_msg = publish_notification_to_user(self.test_user_id, msg)

        with self.assertRaises(ItemNotFoundError):
            # this user doesn't have this notification!
            mark_notification_read(100, sent_user_msg.msg.id)
Example #15
0
    def test_publish_notification(self):
        """
        Go through and set up a notification and publish it
        """

        msg = NotificationMessage(namespace="test-runner", msg_type=self.msg_type, payload={"foo": "bar"})

        # make sure it asserts that user_id is an integer
        with self.assertRaises(ContractNotRespected):
            publish_notification_to_user("bad-id", msg)

        # now do happy path
        sent_user_msg = publish_notification_to_user(self.test_user_id, msg)

        # make sure type checking is happening
        with self.assertRaises(ContractNotRespected):
            get_notifications_count_for_user("bad-type")

        # now query back the notification to make sure it got stored
        # and we can retrieve it

        self.assertEquals(get_notifications_count_for_user(self.test_user_id), 1)

        # make sure it asserts that user_id is an integer
        with self.assertRaises(ContractNotRespected):
            get_notifications_for_user("bad-id")

        notifications = get_notifications_for_user(self.test_user_id)

        self.assertTrue(isinstance(notifications, list))
        self.assertEqual(len(notifications), 1)
        self.assertTrue(isinstance(notifications[0], UserNotification))

        read_user_msg = notifications[0]
        self.assertEqual(read_user_msg.user_id, self.test_user_id)
        self.assertIsNone(read_user_msg.read_at)  # should be unread

        self.assertEqual(read_user_msg, sent_user_msg)
        self.assertEqual(read_user_msg.msg, sent_user_msg.msg)
Example #16
0
    def _publish_test_notification(self, namespace='test-runner'):
        """
        Helper method to set up a notification to test against
        """

        msg = NotificationMessage(namespace=namespace,
                                  msg_type=self.msg_type,
                                  payload={'foo': 'bar'})

        # publish
        user_msg = publish_notification_to_user(self.user.id, msg)
        self.assertIsNotNone(user_msg)

        return user_msg
Example #17
0
def _send_to_single_user(msg, scope_context, preferred_channel=None, channel_context=None):
    """
    Helper method to send to just a single user
    """

    # make sure the user_id was passed in
    if 'user_id' not in scope_context:
        err_msg = (
            'Could not find "user_id" in scope_context {context}'
        ).format(context=scope_context)

        raise KeyError(err_msg)

    user_id = int(scope_context['user_id'])

    # finally publish the notification
    publish_notification_to_user(
        user_id,
        msg,
        preferred_channel=preferred_channel,
        channel_context=channel_context
    )

    return 1
Example #18
0
def _send_to_single_user(msg, scope_context, preferred_channel=None, channel_context=None):
    """
    Helper method to send to just a single user
    """

    # make sure the user_id was passed in
    if 'user_id' not in scope_context:
        err_msg = (
            'Could not find "user_id" in scope_context {context}'
        ).format(context=scope_context)

        raise KeyError(err_msg)

    user_id = int(scope_context['user_id'])

    # finally publish the notification
    publish_notification_to_user(
        user_id,
        msg,
        preferred_channel=preferred_channel,
        channel_context=channel_context
    )

    return 1
Example #19
0
    def test_default_group_mapping(self):
        """
        Test that adds the default notification type mapping
        """
        msg_type = self.store.save_notification_type(
            NotificationType(
                name='open-edx.lms.discussions.cohorted-thread-added',
                renderer=
                'edx_notifications.openedx.forums.CohortedThreadAddedRenderer',
            ))
        # create cohort notification
        with freeze_time(self.to_timestamp):
            msg = self.store.save_notification_message(
                NotificationMessage(
                    msg_type=msg_type,
                    namespace='cohort-thread-added',
                    payload={
                        'subject': 'foo',
                        'body': 'bar',
                        'thread_title':
                        'A demo posting to the discussion forums'
                    },
                ))
            publish_notification_to_user(self.test_user_id, msg)

        register_namespace_resolver(TestNamespaceResolver())

        set_user_notification_preference(
            self.test_user_id, const.NOTIFICATION_DAILY_DIGEST_PREFERENCE_NAME,
            'true')

        self.assertEqual(
            send_notifications_digest(
                self.from_timestamp, self.to_timestamp,
                const.NOTIFICATION_DAILY_DIGEST_PREFERENCE_NAME, 'subject',
                '*****@*****.**'), 3)
Example #20
0
    def test_marking_invalid_msg_read(self):
        """
        Makes sure that we can't mark an invalid notification, e.g. someone elses
        """

        msg = NotificationMessage(namespace='test-runner',
                                  msg_type=self.msg_type,
                                  payload={'foo': 'bar'})

        # publish that
        sent_user_msg = publish_notification_to_user(self.test_user_id, msg)

        with self.assertRaises(ItemNotFoundError):
            # this user doesn't have this notification!
            mark_notification_read(100, sent_user_msg.msg.id)
Example #21
0
    def test_publish_multipayloads(self):
        """
        Go through and set up a multi-payload notification and publish it
        make sure we we the default payloads
        """

        msg = NotificationMessage(
            namespace='test-runner',
            msg_type=self.msg_type,
            payload={
                'foo': 'bar'
            }
        )

        msg.add_payload(
            {
                'one': 'two'
            },
            channel_name='anotherchannel'
        )

        # now do happy path
        sent_user_msg = publish_notification_to_user(self.test_user_id, msg)

        # now query back the notification to make sure it got stored
        # and we can retrieve it
        self.assertEqual(
            get_notifications_count_for_user(self.test_user_id),
            1
        )

        notifications = get_notifications_for_user(self.test_user_id)

        self.assertTrue(isinstance(notifications, list))
        self.assertEqual(len(notifications), 1)
        self.assertTrue(isinstance(notifications[0], UserNotification))

        read_user_msg = notifications[0]
        self.assertEqual(read_user_msg.user_id, self.test_user_id)
        self.assertIsNone(read_user_msg.read_at)  # should be unread

        self.assertEqual(read_user_msg, sent_user_msg)
        # make sure the message that got persisted contains only
        # the default payload
        self.assertEqual(read_user_msg.msg.payload, msg.get_payload())
    def _publish_test_notification(self, namespace='test-runner'):
        """
        Helper method to set up a notification to test against
        """

        msg = NotificationMessage(
            namespace=namespace,
            msg_type=self.msg_type,
            payload={
                'foo': 'bar'
            }
        )

        # publish
        user_msg = publish_notification_to_user(self.user.id, msg)
        self.assertIsNotNone(user_msg)

        return user_msg
Example #23
0
    def test_someone_elses_notification(self):
        """
        Simple test to make sure that we can get counts for someone elses
        notification
        """

        msg = NotificationMessage(namespace='test-runner',
                                  msg_type=self.msg_type,
                                  payload={'foo': 'bar'})

        # publish to some other user_id
        user_msg = publish_notification_to_user(99999, msg)
        self.assertIsNotNone(user_msg)

        # now query API
        response = self.client.get(
            reverse('edx_notifications.consumer.notifications.count'))
        self.assertEqual(response.status_code, 200)

        results = json.loads(response.content.decode('utf-8'))
        self.assertIn('count', results)
        self.assertEqual(results['count'], 0)
Example #24
0
    def test_marking_read_state(self):
        """
        Verify proper behavior when marking notfications as read/unread
        """

        msg = NotificationMessage(
            namespace='test-runner',
            msg_type=self.msg_type,
            payload={
                'foo': 'bar'
            }
        )

        # now do happy path
        sent_user_msg = publish_notification_to_user(self.test_user_id, msg)

        # now mark msg as read by this user
        mark_notification_read(self.test_user_id, sent_user_msg.msg.id)

        # shouldn't be counted in unread counts
        self.assertEqual(
            get_notifications_count_for_user(
                self.test_user_id,
                filters={
                    'read': False,
                    'unread': True,
                },
            ),
            0
        )

        # Should be counted in read counts
        self.assertEqual(
            get_notifications_count_for_user(
                self.test_user_id,
                filters={
                    'read': True,
                    'unread': False,
                },
            ),
            1
        )

        # now mark msg as unread by this user
        mark_notification_read(self.test_user_id, sent_user_msg.msg.id, read=False)

        # Should be counted in unread counts
        self.assertEqual(
            get_notifications_count_for_user(
                self.test_user_id,
                filters={
                    'read': False,
                    'unread': True,
                },
            ),
            1
        )

        # Shouldn't be counted in read counts
        self.assertEqual(
            get_notifications_count_for_user(
                self.test_user_id,
                filters={
                    'read': True,
                    'unread': False,
                },
            ),
            0
        )
    def publish_notification(cls, namespace, msg_type_name, payload, parse_channel_ids, send_at=None, timer_name=None):
        """
        Helper class method to hide some of the inner workings of this channel
        This will work with immediate or timer based publishing.

        'namespace' is an instance of NotificationMessage

        'msg_type' is the type name of the NotificationMessage

        'payload' is the raw data dictionary to send over the mobile clients

        'parse_channel_ids' is a list of Parse channel_ids, which are subscription lists,
        not to be confused with edx-notification's NotificationChannels - an unfortunate
        semantic collision.

        'send_at' is a datetime when this notification should be sent. Note that firing of notifications
        is approximate, so it will not fire BEFORE send_at, but there might be a lag, depending
        on how frequent timer polling is configured in a runtime instance.

        'timer_name' can be used in conjunction with 'send_at'. This is to allow for a fixed
        timer identifier in case the timed notification needs to be updated (or deleted)
        """

        try:
            msg_type = get_notification_type(msg_type_name)
        except ItemNotFoundError:
            msg_type = NotificationType(
                name=msg_type_name,
                renderer='edx_notifications.renderers.basic.JsonRenderer'
            )
            register_notification_type(msg_type)

        msg = NotificationMessage(
            namespace=namespace,
            msg_type=msg_type,
            payload=payload
        )

        if not send_at:
            # send immediately
            publish_notification_to_user(
                user_id=_PARSE_SERVICE_USER_ID,
                msg=msg,
                # we want to make sure we always call this channel provider
                preferred_channel=_PARSE_CHANNEL_NAME,
                channel_context={
                    # tunnel through the parse_channel_id through the
                    # channel context
                    'parse_channel_ids': parse_channel_ids,
                }
            )
        else:
            # time-based sending, use a TimedNotification
            publish_timed_notification(
                msg=msg,
                send_at=send_at,
                scope_name='user',
                scope_context={
                    'user_id': _PARSE_SERVICE_USER_ID
                },
                timer_name=timer_name,
                timer_context={
                    # we want to make sure we always call this channel provider
                    'preferred_channel': _PARSE_CHANNEL_NAME,
                    'channel_context': {
                        # tunnel through the parse_channel_id through
                        # through the channel context
                        'parse_channel_ids': parse_channel_ids,
                    }
                }
            )
Example #26
0
def _send_discussion_notification(type_name,
                                  course_id,
                                  thread,
                                  request_user,
                                  excerpt=None,
                                  recipient_user_id=None,
                                  recipient_group_id=None,
                                  recipient_exclude_user_ids=None,
                                  extra_payload=None,
                                  is_anonymous_user=False):
    """
    Helper method to consolidate Notification trigger workflow
    """
    try:
        # is Notifications feature enabled?
        if not settings.FEATURES.get("ENABLE_NOTIFICATIONS", False):
            return

        if is_anonymous_user:
            action_username = _('An anonymous user')
        else:
            action_username = request_user.username

        # get the notification type.
        msg = NotificationMessage(
            msg_type=get_notification_type(type_name),
            namespace=course_id,
            # base payload, other values will be passed in as extra_payload
            payload={
                '_schema_version': '1',
                'action_username': action_username,
                'thread_title': thread.title,
            })

        # add in additional payload info
        # that might be type specific
        if extra_payload:
            msg.payload.update(extra_payload)

        if excerpt:
            msg.payload.update({
                'excerpt': excerpt,
            })

        # Add information so that we can resolve
        # click through links in the Notification
        # rendering, typically this will be used
        # to bring the user back to this part of
        # the discussion forum

        #
        # IMPORTANT: This can be changed to msg.add_click_link() if we
        # have a URL that we wish to use. In the initial use case,
        # we need to make the link point to a different front end
        #
        msg.add_click_link_params({
            'course_id': course_id,
            'commentable_id': thread.commentable_id,
            'thread_id': thread.id,
        })

        if recipient_user_id:
            # send notification to single user
            publish_notification_to_user(recipient_user_id, msg)

        if recipient_group_id:
            # Send the notification_msg to the CourseGroup via Celery
            # But we can also exclude some users from that list
            if settings.FEATURES.get('ENABLE_NOTIFICATIONS_CELERY', False):
                publish_course_group_notification_task.delay(
                    recipient_group_id,
                    msg,
                    exclude_user_ids=recipient_exclude_user_ids)
            else:
                publish_course_group_notification_task(
                    recipient_group_id,
                    msg,
                    exclude_user_ids=recipient_exclude_user_ids)
    except Exception, ex:
        # Notifications are never critical, so we don't want to disrupt any
        # other logic processing. So log and continue.
        log.exception(ex)
    def test_notification_count(self):
        """
        Simple test to make sure that we get the right count back after
        publishing a notification to this test user
        """

        msg = NotificationMessage(
            namespace='test-runner',
            msg_type=self.msg_type,
            payload={
                'foo': 'bar'
            }
        )

        # publish
        user_msg = publish_notification_to_user(self.user.id, msg)
        self.assertIsNotNone(user_msg)

        url = reverse('edx_notifications.consumer.notifications.count')

        # now query API
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

        results = json.loads(response.content)
        self.assertIn('count', results)
        self.assertEqual(results['count'], 1)

        # query just the unread
        response = self.client.get(url, {'read': False, 'unread': True})
        self.assertEqual(response.status_code, 200)

        results = json.loads(response.content)
        self.assertIn('count', results)
        self.assertEqual(results['count'], 1)

        # query just the read, which should be 0
        response = self.client.get(url, {'read': True, 'unread': False})
        self.assertEqual(response.status_code, 200)

        results = json.loads(response.content)
        self.assertIn('count', results)
        self.assertEqual(results['count'], 0)

        # now mark the message as read
        mark_notification_read(self.user.id, user_msg.msg.id)

        # query just the unread, should be 0
        response = self.client.get(url, {'read': False, 'unread': True})
        self.assertEqual(response.status_code, 200)

        results = json.loads(response.content)
        self.assertIn('count', results)
        self.assertEqual(results['count'], 0)

        # query just the read, which should be 1
        response = self.client.get(url, {'read': True, 'unread': False})
        self.assertEqual(response.status_code, 200)

        results = json.loads(response.content)
        self.assertIn('count', results)
        self.assertEqual(results['count'], 1)
    def handle(self, *args, **options):
        if not settings.FEATURES['ENABLE_NOTIFICATIONS']:
            return

        # Increase time range so that users don't miss notification in case cron job is skipped or delayed.
        time_range = timezone.now() - timezone.timedelta(
            minutes=options['time_range'] * 2)
        leaderboard_size = getattr(settings, 'LEADERBOARD_SIZE', 3)
        courses = Aggregator.objects.filter(
            aggregation_name='course',
            last_modified__gte=time_range).distinct().values_list('course_key',
                                                                  flat=True)
        for course_key in courses:
            all_progress = Aggregator.objects.filter(
                aggregation_name='course',
                course_key=course_key,
                percent__gt=0).exclude(user__in=User.objects.filter(
                    courseaccessrole__course_id=course_key,
                    courseaccessrole__role__in=[
                        'staff', 'observer', 'assistant', 'instructor'
                    ])).order_by('-percent',
                                 'last_modified')[:leaderboard_size]

            all_leaders = LeaderBoard.objects.filter(
                course_key=course_key).all()
            leaders = {l.position: l for l in all_leaders}
            positions = {l.user_id: l.position for l in all_leaders}
            for idx, progress in enumerate(all_progress):
                position = idx + 1
                leader = leaders.get(position)
                if not leader:
                    leader = LeaderBoard(course_key=course_key,
                                         position=position)

                old_leader = leader.user_id
                old_position = positions.get(progress.user_id, sys.maxsize)
                leader.user_id = progress.user_id
                leader.save()
                is_new = progress.modified >= time_range

                if old_leader != progress.user_id and position < old_position and is_new:
                    try:
                        notification_msg = NotificationMessage(
                            msg_type=get_notification_type(
                                'open-edx.lms.leaderboard.progress.rank-changed'
                            ),
                            namespace=str(course_key),
                            payload={
                                '_schema_version': '1',
                                'rank': position,
                                'leaderboard_name': 'Progress',
                            })

                        #
                        # add in all the context parameters we'll need to
                        # generate a URL back to the website that will
                        # present the new course announcement
                        #
                        # IMPORTANT: This can be changed to msg.add_click_link() if we
                        # have a particular URL that we wish to use. In the initial use case,
                        # we need to make the link point to a different front end website
                        # so we need to resolve these links at dispatch time
                        #
                        notification_msg.add_click_link_params({
                            'course_id':
                            str(course_key),
                        })

                        publish_notification_to_user(int(leader.user_id),
                                                     notification_msg)
                    except Exception as ex:  # pylint: disable=broad-except
                        # Notifications are never critical, so we don't want to disrupt any
                        # other logic processing. So log and continue.
                        log.exception(ex)
Example #29
0
def _send_discussion_notification(
    type_name,
    course_id,
    thread,
    request_user,
    excerpt=None,
    recipient_user_id=None,
    recipient_group_id=None,
    recipient_exclude_user_ids=None,
    extra_payload=None,
    is_anonymous_user=False
):
    """
    Helper method to consolidate Notification trigger workflow
    """
    try:
        # is Notifications feature enabled?
        if not settings.FEATURES.get("ENABLE_NOTIFICATIONS", False):
            return

        if is_anonymous_user:
            action_username = _('An anonymous user')
        else:
            action_username = request_user.username

        # get the notification type.
        msg = NotificationMessage(
            msg_type=get_notification_type(type_name),
            namespace=course_id,
            # base payload, other values will be passed in as extra_payload
            payload={
                '_schema_version': '1',
                'action_username': action_username,
                'thread_title': thread.title,
            }
        )

        # add in additional payload info
        # that might be type specific
        if extra_payload:
            msg.payload.update(extra_payload)

        if excerpt:
            msg.payload.update({
                'excerpt': excerpt,
            })

        # Add information so that we can resolve
        # click through links in the Notification
        # rendering, typically this will be used
        # to bring the user back to this part of
        # the discussion forum

        #
        # IMPORTANT: This can be changed to msg.add_click_link() if we
        # have a URL that we wish to use. In the initial use case,
        # we need to make the link point to a different front end
        #
        msg.add_click_link_params({
            'course_id': course_id,
            'commentable_id': thread.commentable_id,
            'thread_id': thread.id,
        })

        if recipient_user_id:
            # send notification to single user
            publish_notification_to_user(recipient_user_id, msg)

        if recipient_group_id:
            # Send the notification_msg to the CourseGroup via Celery
            # But we can also exclude some users from that list
            if settings.FEATURES.get('ENABLE_NOTIFICATIONS_CELERY', False):
                publish_course_group_notification_task.delay(
                    recipient_group_id,
                    msg,
                    exclude_user_ids=recipient_exclude_user_ids
                )
            else:
                publish_course_group_notification_task(
                    recipient_group_id,
                    msg,
                    exclude_user_ids=recipient_exclude_user_ids
                )
    except Exception, ex:
        # Notifications are never critical, so we don't want to disrupt any
        # other logic processing. So log and continue.
        log.exception(ex)
Example #30
0
    def publish_notification(cls,
                             namespace,
                             msg_type_name,
                             payload,
                             parse_channel_ids,
                             send_at=None,
                             timer_name=None):
        """
        Helper class method to hide some of the inner workings of this channel
        This will work with immediate or timer based publishing.

        'namespace' is an instance of NotificationMessage

        'msg_type' is the type name of the NotificationMessage

        'payload' is the raw data dictionary to send over the mobile clients

        'parse_channel_ids' is a list of Parse channel_ids, which are subscription lists,
        not to be confused with edx-notification's NotificationChannels - an unfortunate
        semantic collision.

        'send_at' is a datetime when this notification should be sent. Note that firing of notifications
        is approximate, so it will not fire BEFORE send_at, but there might be a lag, depending
        on how frequent timer polling is configured in a runtime instance.

        'timer_name' can be used in conjunction with 'send_at'. This is to allow for a fixed
        timer identifier in case the timed notification needs to be updated (or deleted)
        """

        try:
            msg_type = get_notification_type(msg_type_name)
        except ItemNotFoundError:
            msg_type = NotificationType(
                name=msg_type_name,
                renderer='edx_notifications.renderers.basic.JsonRenderer')
            register_notification_type(msg_type)

        msg = NotificationMessage(namespace=namespace,
                                  msg_type=msg_type,
                                  payload=payload)

        if not send_at:
            # send immediately
            publish_notification_to_user(
                user_id=_PARSE_SERVICE_USER_ID,
                msg=msg,
                # we want to make sure we always call this channel provider
                preferred_channel=_PARSE_CHANNEL_NAME,
                channel_context={
                    # tunnel through the parse_channel_id through the
                    # channel context
                    'parse_channel_ids': parse_channel_ids,
                })
        else:
            # time-based sending, use a TimedNotification
            publish_timed_notification(
                msg=msg,
                send_at=send_at,
                scope_name='user',
                scope_context={'user_id': _PARSE_SERVICE_USER_ID},
                timer_name=timer_name,
                timer_context={
                    # we want to make sure we always call this channel provider
                    'preferred_channel': _PARSE_CHANNEL_NAME,
                    'channel_context': {
                        # tunnel through the parse_channel_id through
                        # through the channel context
                        'parse_channel_ids': parse_channel_ids,
                    }
                })
Example #31
0
def index(request):
    """
    Returns a basic HTML snippet rendering of a notification count
    """
    global NAMESPACE

    if request.method == 'POST':

        register_user_scope_resolver('user_email_resolver', TestUserResolver(request.user))

        if request.POST.get('change_namespace'):
            namespace_str = request.POST['namespace']
            NAMESPACE = namespace_str if namespace_str != "None" else None
        elif request.POST.get('send_digest'):
            send_digest(request, request.POST.get('digest_email'))
        else:
            type_name = request.POST['notification_type']
            channel_name = request.POST['notification_channel']
            if not channel_name:
                channel_name = None
            msg_type = get_notification_type(type_name)

            msg = NotificationMessage(
                msg_type=msg_type,
                namespace=NAMESPACE,
                payload=CANNED_TEST_PAYLOAD[type_name],
            )

            if type_name == 'testserver.msg-with-resolved-click-link':
                msg.add_click_link_params({
                    'param1': 'param_val1',
                    'param2': 'param_val2',
                })

            publish_notification_to_user(request.user.id, msg, preferred_channel=channel_name)

    template = loader.get_template('index.html')


    # call to the helper method to build up all the context we need
    # to render the "notification_widget" that is embedded in our
    # test page
    context_dict = get_notifications_widget_context({
        'user': request.user,
        'notification_types': get_all_notification_types(),
        'global_variables': {
            'app_name': 'Notification Test Server',
            'hide_link_is_visible': settings.HIDE_LINK_IS_VISIBLE,
            'always_show_dates_on_unread': True,
            'notification_preference_tab_is_visible': settings.NOTIFICATION_PREFERENCES_IS_VISIBLE,
        },
        # for test purposes, set up a short-poll which contacts the server
        # every 10 seconds to see if there is a new notification
        #
        # NOTE: short-poll technique should not be used in a production setting with
        # any reasonable number of concurrent users. This is just for
        # testing purposes.
        #
        'refresh_watcher': {
            'name': 'short-poll',
            'args': {
                'poll_period_secs': 10,
            },
        },
        'include_framework_js': True,
        'namespace': NAMESPACE,
    })

    return HttpResponse(template.render(RequestContext(request, context_dict)))
Example #32
0
    def test_notification_count(self):
        """
        Simple test to make sure that we get the right count back after
        publishing a notification to this test user
        """

        msg = NotificationMessage(
            namespace='test-runner',
            msg_type=self.msg_type,
            payload={
                'foo': 'bar'
            }
        )

        # publish
        user_msg = publish_notification_to_user(self.user.id, msg)
        self.assertIsNotNone(user_msg)

        url = reverse('edx_notifications.consumer.notifications.count')

        # now query API
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

        results = json.loads(response.content.decode('utf-8'))
        self.assertIn('count', results)
        self.assertEqual(results['count'], 1)

        # query just the unread
        response = self.client.get(url, {'read': False, 'unread': True})
        self.assertEqual(response.status_code, 200)

        results = json.loads(response.content.decode('utf-8'))
        self.assertIn('count', results)
        self.assertEqual(results['count'], 1)

        # query just the read, which should be 0
        response = self.client.get(url, {'read': True, 'unread': False})
        self.assertEqual(response.status_code, 200)

        results = json.loads(response.content.decode('utf-8'))
        self.assertIn('count', results)
        self.assertEqual(results['count'], 0)

        # now mark the message as read
        mark_notification_read(self.user.id, user_msg.msg.id)

        # query just the unread, should be 0
        response = self.client.get(url, {'read': False, 'unread': True})
        self.assertEqual(response.status_code, 200)

        results = json.loads(response.content.decode('utf-8'))
        self.assertIn('count', results)
        self.assertEqual(results['count'], 0)

        # query just the read, which should be 1
        response = self.client.get(url, {'read': True, 'unread': False})
        self.assertEqual(response.status_code, 200)

        results = json.loads(response.content.decode('utf-8'))
        self.assertIn('count', results)
        self.assertEqual(results['count'], 1)