Example #1
0
    def test_course_bulk_notification_tests(self):
        # create new users and enroll them in the course.
        startup.startup_notification_subsystem()

        test_user_1 = UserFactory.create(password='******')
        CourseEnrollmentFactory(user=test_user_1, course_id=self.course.id)
        test_user_2 = UserFactory.create(password='******')
        CourseEnrollmentFactory(user=test_user_2, course_id=self.course.id)

        notification_type = get_notification_type(u'open-edx.studio.announcements.new-announcement')
        course = modulestore().get_course(self.course.id, depth=0)
        notification_msg = NotificationMessage(
            msg_type=notification_type,
            namespace=unicode(self.course.id),
            payload={
                '_schema_version': '1',
                'course_name': course.display_name,

            }
        )
        # Send the notification_msg to the Celery task
        publish_course_notifications_task.delay(self.course.id, notification_msg)

        # now the enrolled users should get notification about the
        # course update where they are enrolled as student.
        self.assertTrue(get_notifications_count_for_user(test_user_1.id), 1)
        self.assertTrue(get_notifications_count_for_user(test_user_2.id), 1)
Example #2
0
def _create_notification_message(app_id, payload):
    notification_type = get_notification_type(
        'open-edx.mobileapps.notifications')
    notification_message = NotificationMessage(namespace=str(app_id),
                                               msg_type=notification_type,
                                               payload=payload)
    return notification_message
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, instance.user.id,
            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_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)
Example #5
0
def handle_studentgradebook_post_save_signal(sender, instance, **kwargs):
    """
    Handle the pre-save ORM event on CourseModuleCompletions
    """

    if settings.FEATURES['ENABLE_NOTIFICATIONS']:
        # attach the rank of the user before the save is completed
        data = StudentGradebook.get_user_position(
            instance.course_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 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)
Example #7
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 #8
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 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 #10
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 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 #12
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,
                    }
                })