def test_purge_command_check(self): """ Invoke the Management Command """ msg_type = self.provider.save_notification_type(self.notification_type) msg1 = self.provider.save_notification_message(NotificationMessage( namespace='namespace1', msg_type=msg_type, payload={ 'foo': 'bar' } )) msg2 = self.provider.save_notification_message(NotificationMessage( namespace='namespace1', msg_type=msg_type, payload={ 'test': 'test' } )) # now reset the time to 66 days ago # in order to save the user notification message in the past. reset_time = datetime.now(pytz.UTC) - timedelta(days=66) with freeze_time(reset_time): self.provider.save_user_notification(UserNotification( user_id=self.test_user_id, msg=msg1 )) self.provider.save_user_notification(UserNotification( user_id=self.test_user_id, msg=msg2 )) # user notifications count self.assertEqual( self.provider.get_num_notifications_for_user( self.test_user_id, filters={ 'namespace': 'namespace1' } ), 2 ) # run the management command for purging notifications. force_purge.Command().handle() # now get the user notification count. # count should be 0 at that moment. because # all the notifications have been deleted. self.assertEqual( self.provider.get_num_notifications_for_user( self.test_user_id, filters={ 'namespace': 'namespace1' } ), 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")
def test_data_object_inequality(self): """ Make sure that we can verify inequality between two objects """ obj1 = DataObjectWithTypedFields(id=1, test_int_field=100, test_dict_field={'foo': 'bar'}, test_class_field=NotificationMessage( msg_type=NotificationType( name='testing', renderer='foo.renderer', ), namespace='namespace', payload={'field': 'value'})) obj2 = DataObjectWithTypedFields(id=1, test_int_field=100, test_dict_field={'foo': 'bar'}, test_class_field=NotificationMessage( msg_type=NotificationType( name='something-different', renderer='foo.renderer', ), namespace='namespace', payload={'field': 'value'})) self.assertNotEqual(obj1, obj2)
def setUp(self): """ start up stuff """ register_user_scope_resolver('list_scope', TestListScopeResolver()) self.store = notification_store() self.msg_type = self.store.save_notification_type( NotificationType( name='foo.bar', renderer='foo', ) ) msg = NotificationMessage( msg_type=self.msg_type, payload={'foo': 'bar'}, ) msg.add_payload( { 'extra': 'stuff' }, channel_name='other_channel' ) self.msg = self.store.save_notification_message(msg)
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'))
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"))
def fire_file_upload_notification(self, notifications_service): try: # this NotificationType is registered in the list of default Open edX Notifications msg_type = notifications_service.get_notification_type( 'open-edx.xblock.group-project.file-uploaded') workgroup_user_ids = [] uploader_username = '' for user in self.workgroup['users']: # don't send to ourselves if user['id'] != self.user_id: workgroup_user_ids.append(user['id']) else: uploader_username = user['username'] # get the activity name which is simply our hosting # Sequence's Display Name, so call out to a new xBlock # runtime Service courseware_info = self.get_courseware_info( self.runtime.service(self, 'courseware_parent_info')) activity_name = courseware_info['activity_name'] activity_location = courseware_info['activity_location'] msg = NotificationMessage(msg_type=msg_type, namespace=unicode(self.course_id), payload={ '_schema_version': 1, 'action_username': uploader_username, 'activity_name': activity_name, }) # # 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 # msg.add_click_link_params({ 'course_id': unicode(self.course_id), 'activity_location': unicode(activity_location) if activity_location else '', }) # NOTE: We're not using Celery here since we are expectating that we # will have only a very small handful of workgroup users notifications_service.bulk_publish_notification_to_users( workgroup_user_ids, msg) except Exception, ex: # While we *should* send notification, if there is some # error here, we don't want to blow the whole thing up. # So log it and continue.... log.exception(ex)
def _set_activity_timed_notification(self, course_id, activity, msg_type, component, milestone_date, send_at_date, services, timer_name_suffix): component_name = component.name notifications_service = services.get('notifications') courseware_parent_info = services.get('courseware_parent_info') courseware_info = self.get_courseware_info(courseware_parent_info) activity_name = courseware_info['activity_name'] activity_location = courseware_info['activity_location'] project_location = courseware_info['project_location'] milestone_date_tz = milestone_date.replace(tzinfo=pytz.UTC) send_at_date_tz = send_at_date.replace(tzinfo=pytz.UTC) msg = NotificationMessage( msg_type=notifications_service.get_notification_type(msg_type), namespace=unicode(course_id), payload={ '_schema_version': 1, 'activity_name': activity_name, 'stage': component_name, 'due_date': milestone_date_tz.strftime('%-m/%-d/%-y'), }) # # 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 # msg.add_click_link_params({ 'course_id': unicode(course_id), 'activity_location': unicode(activity_location), }) notifications_service.publish_timed_notification( msg=msg, send_at=send_at_date_tz, # send to all students participating in this project scope_name='group_project_participants', scope_context={ 'course_id': unicode(course_id), 'content_id': unicode(project_location), }, timer_name=self._get_component_timer_name(component, timer_name_suffix), ignore_if_past_due=True # don't send if we're already late! )
def test_typed_fields(self): """ Assert proper behavior with TypedFields inside of BaseDataObjects """ # make sure we can make proper assignments on initialization msg = NotificationMessage() obj = DataObjectWithTypedFields( id=1, test_int_field=100, test_dict_field={ 'foo': 'bar' }, test_class_field=msg, ) self.assertTrue(isinstance(obj.test_int_field, int)) self.assertTrue(isinstance(obj.test_dict_field, dict)) self.assertTrue(isinstance(obj.test_class_field, NotificationMessage)) self.assertEqual(obj.test_int_field, 100) self.assertEqual(obj.test_dict_field, {'foo': 'bar'}) self.assertEqual(obj.test_class_field, msg) # make sure we work with longs as well obj = DataObjectWithTypedFields( id=long(1), ) self.assertTrue(isinstance(obj.id, long)) # make sure we can set fields after initialization obj = DataObjectWithTypedFields() obj.test_int_field = 100 obj.test_dict_field = { 'foo': 'bar' } obj.test_class_field = NotificationMessage() self.assertTrue(isinstance(obj.test_int_field, int)) self.assertTrue(isinstance(obj.test_dict_field, dict)) self.assertTrue(isinstance(obj.test_class_field, NotificationMessage)) # make sure we can set typed fields as None obj = DataObjectWithTypedFields( test_int_field=None, test_dict_field=None, test_class_field=None, ) self.assertTrue(isinstance(obj.test_int_field, type(None))) self.assertTrue(isinstance(obj.test_dict_field, type(None))) self.assertTrue(isinstance(obj.test_class_field, type(None))) with self.assertRaises(TypeError): # RelatedObjectField can only point to # subclasses of BaseDataObject RelatedObjectField(object)
def fire_file_upload_notification(self, notifications_service): try: # this NotificationType is registered in the list of default Open edX Notifications msg_type = notifications_service.get_notification_type('open-edx.xblock.group-project.file-uploaded') workgroup_user_ids = [] uploader_username = '' for user in self.workgroup['users']: # don't send to ourselves if user['id'] != self.user_id: workgroup_user_ids.append(user['id']) else: uploader_username = user['username'] # get the activity name which is simply our hosting # Sequence's Display Name, so call out to a new xBlock # runtime Service courseware_info = self.get_courseware_info(self.runtime.service(self, 'courseware_parent_info')) activity_name = courseware_info['activity_name'] activity_location = courseware_info['activity_location'] msg = NotificationMessage( msg_type=msg_type, namespace=unicode(self.course_id), payload={ '_schema_version': 1, 'action_username': uploader_username, 'activity_name': activity_name, } ) # # 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 # msg.add_click_link_params({ 'course_id': unicode(self.course_id), 'activity_location': unicode(activity_location) if activity_location else '', }) # NOTE: We're not using Celery here since we are expectating that we # will have only a very small handful of workgroup users notifications_service.bulk_publish_notification_to_users( workgroup_user_ids, msg ) except Exception, ex: # While we *should* send notification, if there is some # error here, we don't want to blow the whole thing up. # So log it and continue.... log.exception(ex)
def test_message_validation(self): """ Make sure validation of NotificationMessage is correct """ msg = NotificationMessage() # intentionally blank with self.assertRaises(ValidationError): msg.validate()
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_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 _set_activity_timed_notification(self, course_id, activity, msg_type, component, milestone_date, send_at_date, services, timer_name_suffix): component_name = component.name notifications_service = services.get('notifications') courseware_parent_info = services.get('courseware_parent_info') courseware_info = self.get_courseware_info(courseware_parent_info) activity_name = courseware_info['activity_name'] activity_location = courseware_info['activity_location'] project_location = courseware_info['project_location'] milestone_date_tz = milestone_date.replace(tzinfo=pytz.UTC) send_at_date_tz = send_at_date.replace(tzinfo=pytz.UTC) msg = NotificationMessage( msg_type=notifications_service.get_notification_type(msg_type), namespace=unicode(course_id), payload={ '_schema_version': 1, 'activity_name': activity_name, 'stage': component_name, 'due_date': milestone_date_tz.strftime('%-m/%-d/%-y'), } ) # # 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 # msg.add_click_link_params({ 'course_id': unicode(course_id), 'activity_location': unicode(activity_location), }) notifications_service.publish_timed_notification( msg=msg, send_at=send_at_date_tz, # send to all students participating in this project scope_name='group_project_participants', scope_context={ 'course_id': unicode(course_id), 'content_id': unicode(project_location), }, timer_name=self._get_component_timer_name(component, timer_name_suffix), ignore_if_past_due=True # don't send if we're already late! )
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 test_purge_expired_read_notifications(self): """ Test to check for the older read messages. If exists delete those messages from the database. """ msg_type = self._save_notification_type() msg1 = self.provider.save_notification_message( NotificationMessage(namespace='namespace1', msg_type=msg_type, payload={'foo': 'bar'})) msg2 = self.provider.save_notification_message( NotificationMessage(namespace='namespace1', msg_type=msg_type, payload={'test': 'test'})) # now reset the time to 10 days ago # in order to save the user notification messages in the past. reset_time = datetime.now(pytz.UTC) - timedelta(days=10) with freeze_time(reset_time): self.provider.save_user_notification( UserNotification(user_id=self.test_user_id, msg=msg1)) # mark the user notification as read. self.provider.mark_user_notifications_read(self.test_user_id) # now reset the time to 2 days ago # in order to save the user notification messages in the past. reset_time = datetime.now(pytz.UTC) - timedelta(days=2) with freeze_time(reset_time): self.provider.save_user_notification( UserNotification(user_id=self.test_user_id, msg=msg2)) # mark the user notification as read. self.provider.mark_user_notifications_read(self.test_user_id) # user notifications count self.assertEqual( self.provider.get_num_notifications_for_user( self.test_user_id, filters={'namespace': 'namespace1'}), 2) # purge older read messages. purge_older_read_messages = datetime.now(pytz.UTC) - timedelta(days=6) self.provider.purge_expired_notifications( purge_read_messages_older_than=purge_older_read_messages) # now get the user notification count. # count should be 1 at that moment. because # only 1 notification has been deleted. self.assertEqual( self.provider.get_num_notifications_for_user( self.test_user_id, filters={'namespace': 'namespace1'}), 1)
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 with freeze_time(self.to_timestamp): 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) with freeze_time(self.to_timestamp): 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)
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 fire_grades_posted_notification(self, group_id, notifications_service): try: # this NotificationType is registered in the list of default Open edX Notifications msg_type = notifications_service.get_notification_type( 'open-edx.xblock.group-project.grades-posted') # get the activity name which is simply our hosting # Sequence's Display Name, so call out to a new xBlock # runtime Service courseware_info = self.get_courseware_info( self.runtime.service(self, 'courseware_parent_info')) activity_name = courseware_info['activity_name'] activity_location = courseware_info['activity_location'] msg = NotificationMessage(msg_type=msg_type, namespace=unicode(self.course_id), payload={ '_schema_version': 1, 'activity_name': activity_name, }) # # 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 # msg.add_click_link_params({ 'course_id': unicode(self.course_id), 'activity_location': unicode(activity_location) if activity_location else '', }) # Bulk publish to the 'group_project_workgroup' user scope notifications_service.bulk_publish_notification_to_scope( 'group_project_workgroup', { # I think self.workgroup['id'] is a string version of an integer 'workgroup_id': group_id, }, msg) except Exception, ex: # While we *should* send notification, if there is some # error here, we don't want to blow the whole thing up. # So log it and continue.... log.exception(ex)
def _setup_user_notifications(self): """ Helper to build out some """ msg_type = self._save_notification_type() # set up some notifications msg1 = self.provider.save_notification_message(NotificationMessage( namespace='namespace1', msg_type=msg_type, payload={ 'foo': 'bar', 'one': 1, 'none': None, 'datetime': datetime.utcnow(), 'iso8601-fakeout': '--T::', # something to throw off the iso8601 parser heuristic } )) map1 = self.provider.save_user_notification(UserNotification( user_id=self.test_user_id, msg=msg1 )) msg_type2 = self.provider.save_notification_type( NotificationType( name='foo.bar.another', renderer='foo.renderer', ) ) msg2 = self.provider.save_notification_message(NotificationMessage( namespace='namespace2', msg_type=msg_type2, payload={ 'foo': 'baz', 'one': 1, 'none': None, 'datetime': datetime.utcnow(), 'iso8601-fakeout': '--T::', # something to throw off the iso8601 parser heuristic } )) map2 = self.provider.save_user_notification(UserNotification( user_id=self.test_user_id, msg=msg2 )) return map1, msg1, map2, msg2
def fire_grades_posted_notification(self, group_id, notifications_service): try: # this NotificationType is registered in the list of default Open edX Notifications msg_type = notifications_service.get_notification_type('open-edx.xblock.group-project.grades-posted') # get the activity name which is simply our hosting # Sequence's Display Name, so call out to a new xBlock # runtime Service courseware_info = self.get_courseware_info(self.runtime.service(self, 'courseware_parent_info')) activity_name = courseware_info['activity_name'] activity_location = courseware_info['activity_location'] msg = NotificationMessage( msg_type=msg_type, namespace=unicode(self.course_id), payload={ '_schema_version': 1, 'activity_name': activity_name, } ) # # 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 # msg.add_click_link_params({ 'course_id': unicode(self.course_id), 'activity_location': unicode(activity_location) if activity_location else '', }) # Bulk publish to the 'group_project_workgroup' user scope notifications_service.bulk_publish_notification_to_scope( 'group_project_workgroup', { # I think self.workgroup['id'] is a string version of an integer 'workgroup_id': group_id, }, msg ) except Exception, ex: # While we *should* send notification, if there is some # error here, we don't want to blow the whole thing up. # So log it 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.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])
def test_click_links_params(self): """ Make sure the helper methods work """ msg = NotificationMessage(payload={'foo': 'bar'}) msg.add_click_link_params({ 'param1': 'val1', 'param2': 'val2', }) click_links = msg.get_click_link_params() self.assertIsNotNone(click_links) self.assertEqual(click_links['param1'], 'val1') self.assertEqual(click_links['param2'], 'val2') msg.add_click_link_params({ 'param3': 'val3', }) click_links = msg.get_click_link_params() self.assertEqual(click_links['param1'], 'val1') self.assertEqual(click_links['param2'], 'val2') self.assertEqual(click_links['param3'], 'val3')
def test_over_limit_counting(self): """ Verifies that our counting operations will work as expected even when our count is greater that the NOTIFICATION_MAX_LIST_SIZE which is the maximum page size """ self.assertEqual(const.NOTIFICATION_MAX_LIST_SIZE, 1) msg_type = self._save_notification_type() for __ in range(10): msg = self.provider.save_notification_message(NotificationMessage( namespace='namespace1', msg_type=msg_type, payload={ 'foo': 'bar' } )) self.provider.save_user_notification(UserNotification( user_id=self.test_user_id, msg=msg )) self.assertEqual( self.provider.get_num_notifications_for_user( self.test_user_id, filters={ 'namespace': 'namespace1', } ), 10 )
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 )
def _save_new_notification(self, payload='This is a test payload'): """ Helper method to create a new notification """ msg_type = self._save_notification_type() msg = NotificationMessage( msg_type=msg_type, payload={ 'foo': 'bar', 'one': 1, 'none': None, 'datetime': datetime.utcnow(), 'iso8601-fakeout': '--T::', # something to throw off the iso8601 parser heuristic }, resolve_links={ 'param1': 'value1' }, object_id='foo-item' ) with self.assertNumQueries(1): result = self.provider.save_notification_message(msg) self.assertIsNotNone(result) self.assertIsNotNone(result.id) return result
def test_mark_user_notification_read(self): """ """ msg_type = self._save_notification_type() for __ in range(10): msg = self.provider.save_notification_message( NotificationMessage(namespace='namespace1', msg_type=msg_type, payload={'foo': 'bar'})) self.provider.save_user_notification( UserNotification(user_id=self.test_user_id, msg=msg)) self.assertEqual( self.provider.get_num_notifications_for_user(self.test_user_id, filters={ 'namespace': 'namespace1', }), 10) self.provider.mark_user_notifications_read(self.test_user_id) self.assertEqual( self.provider.get_num_notifications_for_user(self.test_user_id, filters={ 'namespace': 'namespace1', 'read': False }), 0)
def test_bulk_publish_list_exclude(self): """ Make sure we can bulk publish to a number of users passing in a list, and also pass in an exclusion list to make sure the people in the exclude list does not get the notification """ msg = NotificationMessage(namespace='test-runner', msg_type=self.msg_type, payload={'foo': 'bar'}) user_ids = list( range(1, const.NOTIFICATION_BULK_PUBLISH_CHUNK_SIZE * 2 + 1)) exclude_user_ids = list( range(1, const.NOTIFICATION_BULK_PUBLISH_CHUNK_SIZE * 2 + 1, 2)) # now send to more than our internal chunking size bulk_publish_notification_to_users(user_ids, msg, exclude_user_ids=exclude_user_ids) # now read them all back for user_id in range( 1, const.NOTIFICATION_BULK_PUBLISH_CHUNK_SIZE * 2 + 1): notifications = get_notifications_for_user(user_id) self.assertTrue(isinstance(notifications, list)) self.assertEqual(len(notifications), 1 if user_id not in exclude_user_ids else 0) if user_id not in exclude_user_ids: self.assertTrue(isinstance(notifications[0], UserNotification))
def _set_activity_timed_notification(self, course_id, msg_type, event_date, send_at_date, services, timer_name_suffix): notifications_service = services.get('notifications') activity_date_tz = event_date.replace(tzinfo=pytz.UTC) send_at_date_tz = send_at_date.replace(tzinfo=pytz.UTC) msg = NotificationMessage( msg_type=notifications_service.get_notification_type(msg_type), namespace=unicode(course_id), payload={ '_schema_version': 1, 'activity_name': self.activity.display_name, 'stage': self.display_name, 'due_date': activity_date_tz.strftime('%-m/%-d/%-y'), }) add_click_link_params(msg, course_id, self.location) notifications_service.publish_timed_notification( msg=msg, send_at=send_at_date_tz, # send to all students participating in this project scope_name=NotificationScopes.PARTICIPANTS, scope_context={ 'course_id': unicode(course_id), 'content_id': unicode(self.activity.project.location), }, timer_name=self._get_stage_timer_name(timer_name_suffix), ignore_if_past_due=True # don't send if we're already late! )
def test_publish_to_scope(self): """ Make sure we can bulk publish to a number of users passing in a resultset from a Django ORM query """ register_user_scope_resolver("list_scope", TestListScopeResolver()) msg = NotificationMessage(namespace='test-runner', msg_type=self.msg_type, payload={'foo': 'bar'}) bulk_publish_notification_to_scope( scope_name="list_scope", # the TestListScopeResolver expects a "range" property in the context scope_context={"range": 5}, msg=msg) for user_id in range(4): # have to fudge this a bit as the contract on user_id # says > 0 only allowed user_id = user_id + 1 notifications = get_notifications_for_user(user_id) self.assertTrue(isinstance(notifications, list)) self.assertEqual(len(notifications), 1) self.assertTrue(isinstance(notifications[0], UserNotification))
def test_json_renderer(self): """ Make sure JSON renderer returns correct renderings """ msg_type = NotificationType( name='open-edx.edx_notifications.lib.tests.test_publisher', renderer='edx_notifications.renderers.basic.JsonRenderer', ) register_notification_type(msg_type) msg = NotificationMessage(namespace='test-runner', msg_type=msg_type, payload={ 'subject': 'test subject', 'body': 'test body', }) renderer = JsonRenderer() self.assertTrue(renderer.can_render_format(RENDER_FORMAT_JSON)) self.assertIsNone(renderer.get_template_path(RENDER_FORMAT_JSON)) self.assertEqual( json.loads(renderer.render(msg, RENDER_FORMAT_JSON, None)), msg.payload)
def test_bulk_publish_generator(self): """ Make sure we can bulk publish to a number of users passing in a generator function """ msg = NotificationMessage(namespace='test-runner', msg_type=self.msg_type, payload={'foo': 'bar'}) def _user_id_generator(): """ Just spit our an generator that goes from 1 to 100 """ for user_id in range(1, 100): yield user_id # now send to more than our internal chunking size bulk_publish_notification_to_users(_user_id_generator(), msg) # now read them all back for user_id in range(1, 100): notifications = get_notifications_for_user(user_id) self.assertTrue(isinstance(notifications, list)) self.assertEqual(len(notifications), 1) self.assertTrue(isinstance(notifications[0], UserNotification))
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 test_bulk_publish_list(self): """ Make sure we can bulk publish to a number of users passing in a list """ msg = NotificationMessage( namespace='test-runner', msg_type=self.msg_type, payload={ 'foo': 'bar' } ) # now send to more than our internal chunking size bulk_publish_notification_to_users( list(range(1, const.NOTIFICATION_BULK_PUBLISH_CHUNK_SIZE * 2 + 1)), msg ) # now read them all back for user_id in range(1, const.NOTIFICATION_BULK_PUBLISH_CHUNK_SIZE * 2 + 1): notifications = get_notifications_for_user(user_id) self.assertTrue(isinstance(notifications, list)) self.assertEqual(len(notifications), 1) self.assertTrue(isinstance(notifications[0], UserNotification))
def test_bulk_publish_orm_query(self): """ Make sure we can bulk publish to a number of users passing in a resultset from a Django ORM query """ # set up some test users in Django User's model User(username='******').save() User(username='******').save() User(username='******').save() msg = NotificationMessage(namespace='test-runner', msg_type=self.msg_type, payload={'foo': 'bar'}) resultset = User.objects.values_list('id', flat=True).all() # pylint: disable=no-member num_sent = bulk_publish_notification_to_users(resultset, msg) # make sure we sent 3 self.assertEqual(num_sent, 3) # now read them back for user in User.objects.all(): # pylint: disable=no-member notifications = get_notifications_for_user(user.id) self.assertTrue(isinstance(notifications, list)) self.assertEqual(len(notifications), 1) self.assertTrue(isinstance(notifications[0], UserNotification))
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)
def fire_file_upload_notification(self, notifications_service): log.info('{}.fire_file_upload_notification on location = {}'.format( self.__class__.__name__, self.location)) # this NotificationType is registered in the list of default Open edX Notifications msg_type = notifications_service.get_notification_type( NotificationMessageTypes.FILE_UPLOADED) workgroup_user_ids = [] uploader_username = '' for user in self.workgroup.users: # don't send to ourselves if user.id != self.user_id: workgroup_user_ids.append(user.id) else: uploader_username = user.username msg = NotificationMessage(msg_type=msg_type, namespace=unicode(self.course_id), payload={ '_schema_version': 1, 'action_username': uploader_username, 'activity_name': self.activity.display_name, }) location = unicode(self.location) if self.location else '' add_click_link_params(msg, unicode(self.course_id), location) # NOTE: We're not using Celery here since we are expecting that we # will have only a very small handful of workgroup users notifications_service.bulk_publish_notification_to_users( workgroup_user_ids, msg)
def test_click_links_params(self): """ Make sure the helper methods work """ msg = NotificationMessage( payload={'foo': 'bar'} ) msg.add_click_link_params({ 'param1': 'val1', 'param2': 'val2', }) click_links = msg.get_click_link_params() self.assertIsNotNone(click_links) self.assertEqual(click_links['param1'], 'val1') self.assertEqual(click_links['param2'], 'val2') msg.add_click_link_params({ 'param3': 'val3', }) click_links = msg.get_click_link_params() self.assertEqual(click_links['param1'], 'val1') self.assertEqual(click_links['param2'], 'val2') self.assertEqual(click_links['param3'], 'val3')
def test_click_link(self): """ Tests around the click_link property of NotificationMessages """ msg = NotificationMessage() self.assertIsNone(msg.get_click_link()) msg.set_click_link('/foo/bar/baz') self.assertEqual(msg.get_click_link(), '/foo/bar/baz') msg.set_click_link('/updated') self.assertEqual(msg.get_click_link(), '/updated')
def test_cloning(self): """ Make sure cloning works """ msg = NotificationMessage( payload={'foo': 'bar'} ) clone = NotificationMessage.clone(msg) self.assertEqual(msg, clone) # now change the cloned payload and assert that the original one # did not change clone.payload['foo'] = 'changed' self.assertEqual(msg.payload['foo'], 'bar') self.assertEqual(clone.payload['foo'], 'changed')
def _get_linked_resolved_msg(self, msg): """ This helper will attempt to resolve all links that are present in the message resolve any links that may need conversion into URL paths This uses a subdict named "_resolve_links" in the msg.resolve_links field: resolve_links = { "_resolve_links": { "_click_link": { "param1": "val1", "param2": "param2" }, : }, : } This above will try to resolve the URL for the link named "_click_link" (for example, when a user clicks on a notification, the should go to that URL), with the URL parameters "param1"="val1" and "param2"="val2", and put that link name back in the main payload dictionary as "_click_link" """ if msg.resolve_links: for link_name, link_params in msg.resolve_links.iteritems(): resolved_link = self.resolve_msg_link(msg, link_name, link_params) if resolved_link: # copy the msg because we are going to alter it and we don't want to affect # the passed in version msg = NotificationMessage.clone(msg) # if we could resolve, then store the resolved link in the payload itself msg.payload[link_name] = resolved_link # return the msg which could be a clone of the original one return msg
def test_multi_payloads(self): """ Tests the ability to support multiple payloads in a NotificationMessage """ msg = NotificationMessage() self.assertIsNone(msg.get_payload()) msg.add_payload( { 'foo': 'bar', } ) self.assertEqual(msg.get_payload(), {'foo': 'bar'}) self.assertEqual(msg.get_message_for_channel(), msg) msg.add_payload( { 'bar': 'baz' }, channel_name='channel1' ) self.assertNotEqual(msg.get_message_for_channel(), msg) self.assertEqual(msg.get_message_for_channel().payload, {'foo': 'bar'}) self.assertEqual(msg.get_message_for_channel('channel1').payload, {'bar': 'baz'}) msg.add_payload( { 'one': 'two' }, channel_name='channel2' ) self.assertNotEqual(msg.get_message_for_channel(), msg) self.assertEqual(msg.get_message_for_channel().payload, {'foo': 'bar'}) self.assertEqual(msg.get_message_for_channel('channel1').payload, {'bar': 'baz'}) self.assertEqual(msg.get_message_for_channel('channel2').payload, {'one': 'two'}) self.assertEqual(msg.get_message_for_channel('doesnt-exist').payload, {'foo': 'bar'}) msg.add_payload( { 'updated': 'yes' } ) self.assertEqual(msg.get_message_for_channel().payload, {'updated': 'yes'})
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)