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)
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)
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)
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)))
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, } } )
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)
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, } })