def test_multiple_students(self): # Register two students user = actions.login(self.STUDENT_EMAIL) actions.register(self, user.email(), course=self.COURSE) other_user = actions.login('*****@*****.**') actions.register(self, other_user.email(), course=self.COURSE) # Get IDs of those students; make an event for each. with common_utils.Namespace(self.NAMESPACE): student1_id = ( models.Student.get_by_user(user).user_id) student2_id = ( models.Student.get_by_user(other_user).user_id) models.EventEntity(user_id=student1_id, source='test').put() models.EventEntity(user_id=student2_id, source='test').put() # Unregister one of them. actions.login(self.STUDENT_EMAIL) self._unregister_and_request_data_removal(self.COURSE) self._complete_removal() # Unregistered student and his data are gone; still-registered # student's data is still present. with common_utils.Namespace(self.NAMESPACE): self.assertIsNone(models.Student.get_by_user(user)) self.assertIsNotNone(models.Student.get_by_user(other_user)) entities = list(models.EventEntity.all().run()) self.assertEquals(1, len(entities)) self.assertEquals(student2_id, entities[0].user_id)
def test_multiple_courses(self): COURSE_TWO = 'course_two' COURSE_TWO_NS = 'ns_' + COURSE_TWO # Slight cheat: Register gitkit data remover manually, rather than # enabling the entire module, which disrupts normal functional test # user login handling gitkit.EmailMapping.register_for_data_removal() actions.simple_add_course( COURSE_TWO, self.ADMIN_EMAIL, 'Data Removal Test Two') user = actions.login(self.STUDENT_EMAIL) actions.register(self, user.email(), course=self.COURSE) actions.register(self, user.email(), course=COURSE_TWO) # Slight cheat: Rather than enabling gitkit module, just call # the method that will insert the EmailMapping row. gitkit.EmailUpdatePolicy.apply(user) # Global profile object(s) should now exist. profile = models.StudentProfileDAO.get_profile_by_user_id( user.user_id()) self.assertIsNotNone(profile) email_policy = gitkit.EmailMapping.get_by_user_id(user.user_id()) self.assertIsNotNone(email_policy) # Unregister from 'data_removal_test' course. self._unregister_and_request_data_removal(self.COURSE) self._complete_removal() # Student object should be gone from data_removal_test course, but # not from course_two. with common_utils.Namespace(self.NAMESPACE): self.assertIsNone(models.Student.get_by_user(user)) with common_utils.Namespace(COURSE_TWO_NS): self.assertIsNotNone(models.Student.get_by_user(user)) # Global profile object(s) should still exist. profile = models.StudentProfileDAO.get_profile_by_user_id( user.user_id()) self.assertIsNotNone(profile) email_policy = gitkit.EmailMapping.get_by_user_id(user.user_id()) self.assertIsNotNone(email_policy) # Unregister from other course. self._unregister_and_request_data_removal(COURSE_TWO) self._complete_removal() # Both Student objects should now be gone. with common_utils.Namespace(self.NAMESPACE): self.assertIsNone(models.Student.get_by_user(user)) with common_utils.Namespace(COURSE_TWO_NS): self.assertIsNone(models.Student.get_by_user(user)) # Global profile object(s) should also be gone. profile = models.StudentProfileDAO.get_profile_by_user_id( user.user_id()) self.assertIsNone(profile) email_policy = gitkit.EmailMapping.get_by_user_id(user.user_id()) self.assertIsNone(email_policy)
def test_update_wont_clobber_status(self): # This fixture should already have a sync time with utils.Namespace(self.app_context.namespace): dto = drive_models.DriveSyncDAO.load('6') self.assertIsNotNone(dto.last_synced) # update existing record self.assertRestStatus(self.put('rest/modules/drive/item', { 'request': transforms.dumps({ 'xsrf_token': crypto.XsrfTokenManager.create_xsrf_token( 'drive-item-rest'), 'key': '6', 'payload': transforms.dumps({ 'sync_interval': 'hour', 'version': '1.0', 'availability': 'public', }), }), }), 200) # The sync time should still exist with utils.Namespace(self.app_context.namespace): dto = drive_models.DriveSyncDAO.load('6') self.assertIsNotNone(dto.last_synced)
def test_tracked_lessons(self): context = actions.simple_add_course('test', '*****@*****.**', 'Test Course') course = courses.Course(None, context) actions.login('*****@*****.**') actions.register(self, 'Some Admin', 'test') with common_utils.Namespace('ns_test'): foo_id = models.LabelDAO.save(models.LabelDTO( None, {'title': 'Foo', 'descripton': 'foo', 'type': models.LabelDTO.LABEL_TYPE_COURSE_TRACK})) bar_id = models.LabelDAO.save(models.LabelDTO( None, {'title': 'Bar', 'descripton': 'bar', 'type': models.LabelDTO.LABEL_TYPE_COURSE_TRACK})) unit1 = course.add_unit() unit1.now_available = True unit1.labels = str(foo_id) lesson11 = course.add_lesson(unit1) lesson11.objectives = 'common plugh <gcb-youtube videoid="glados">' lesson11.now_available = True lesson11.notes = search_unit_test.VALID_PAGE_URL lesson11.video = 'portal' course.update_unit(unit1) unit2 = course.add_unit() unit2.now_available = True unit1.labels = str(bar_id) lesson21 = course.add_lesson(unit2) lesson21.objectives = 'common plover' lesson21.now_available = True course.update_unit(unit2) course.save() self.index_test_course() # Registered, un-tracked student sees all. response = self.get('/test/search?query=common') self.assertIn('common', response.body) self.assertIn('plugh', response.body) self.assertIn('plover', response.body) response = self.get('/test/search?query=link') # Do see followed links self.assertIn('Partial', response.body) self.assertIn('Absolute', response.body) response = self.get('/test/search?query=lemon') # Do see video refs self.assertIn('v=glados', response.body) # Student with tracks sees filtered view. with common_utils.Namespace('ns_test'): models.Student.set_labels_for_current(str(foo_id)) response = self.get('/test/search?query=common') self.assertIn('common', response.body) self.assertNotIn('plugh', response.body) self.assertIn('plover', response.body) response = self.get('/test/search?query=link') # Links are filtered self.assertNotIn('Partial', response.body) self.assertNotIn('Absolute', response.body) response = self.get('/test/search?query=lemon') # Don't see video refs self.assertNotIn('v=glados', response.body)
def test_reregistration_blocked_during_deletion(self): def assert_cannot_register(): response = self.get('register') self.assertIn('You cannot re-register for this course', response.body) self.assertNotIn('What is your name?', response.body) user_id = None user = actions.login(self.STUDENT_EMAIL) actions.register(self, user.email()) with common_utils.Namespace(self.NAMESPACE): # After registration, we should have a student object, and # a ImmediateRemovalState instance. student = models.Student.get_by_user(user) self.assertIsNotNone(student) user_id = student.user_id self._unregister_and_request_data_removal(self.COURSE) # On submitting the unregister form, the user's ImmediateRemovalState # will have been marked as deltion-in-progress, and so user cannot # re-register yet. assert_cannot_register() # Run the queue to do the cleanup of indexed items, and add the # work-to-do items for batched cleanup. self.execute_all_deferred_tasks( models.StudentLifecycleObserver.QUEUE_NAME) assert_cannot_register() # Run the cron job that launches the map/reduce jobs to clean up # bulk items. Still not able to re-register. self.get(data_removal.DataRemovalCronHandler.URL, headers={'X-AppEngine-Cron': 'True'}) assert_cannot_register() # Run the map/reduce jobs. Bulk items should now be cleaned. self.execute_all_deferred_tasks() with common_utils.Namespace(self.NAMESPACE): student = models.Student.get_by_user(user) self.assertIsNone(student) removal_state = removal_models.ImmediateRemovalState.get_by_user_id( user_id) self.assertIsNotNone(removal_state) assert_cannot_register() # Run the cron job one more time. When no bulk to-do items remain, # we then clean up the ImmediateRemovalState. Re-registration should # now be possible. self.get(data_removal.DataRemovalCronHandler.URL, headers={'X-AppEngine-Cron': 'True'}) with common_utils.Namespace(self.NAMESPACE): student = models.Student.get_by_user(user) self.assertIsNone(student) removal_state = removal_models.ImmediateRemovalState.get_by_user_id( user_id) self.assertIsNone(removal_state) actions.register(self, self.STUDENT_EMAIL)
def test_hook_i18n(self): actions.update_course_config( COURSE_NAME, { 'html_hooks': {'base': {'after_body_tag_begins': 'foozle'}}, 'extra_locales': [ {'locale': 'de', 'availability': 'available'}, ] }) hook_bundle = { 'content': { 'type': 'html', 'source_value': '', 'data': [{ 'source_value': 'foozle', 'target_value': 'FUZEL', }], } } hook_key = i18n_dashboard.ResourceBundleKey( utils.ResourceHtmlHook.TYPE, 'base.after_body_tag_begins', 'de') with common_utils.Namespace(NAMESPACE): i18n_dashboard.ResourceBundleDAO.save( i18n_dashboard.ResourceBundleDTO(str(hook_key), hook_bundle)) # Verify non-translated version. response = self.get(BASE_URL) dom = self.parse_html_string(response.body) html_hook = dom.find('.//div[@id="base-after-body-tag-begins"]') self.assertEquals('foozle', html_hook.text) # Set preference to translated language, and check that that's there. with common_utils.Namespace(NAMESPACE): prefs = models.StudentPreferencesDAO.load_or_default() prefs.locale = 'de' models.StudentPreferencesDAO.save(prefs) response = self.get(BASE_URL) dom = self.parse_html_string(response.body) html_hook = dom.find('.//div[@id="base-after-body-tag-begins"]') self.assertEquals('FUZEL', html_hook.text) # With no translation present, but preference set to foreign language, # verify that we fall back to the original language. # Remove translation bundle, and clear cache. with common_utils.Namespace(NAMESPACE): i18n_dashboard.ResourceBundleDAO.delete( i18n_dashboard.ResourceBundleDTO(str(hook_key), hook_bundle)) model_caching.CacheFactory.get_cache_instance( i18n_dashboard.RESOURCE_BUNDLE_CACHE_NAME).clear() response = self.get(BASE_URL) dom = self.parse_html_string(response.body) html_hook = dom.find('.//div[@id="base-after-body-tag-begins"]') self.assertEquals('foozle', html_hook.text)
def test_student_property_removed(self): """Test a sampling of types whose index contains user ID. Here, indices start with the user ID, but are suffixed with the name of a specific property sub-type. Verify that these are removed. """ user = self.make_test_user(self.STUDENT_EMAIL) user_id = None actions.login(user.email()) actions.register(self, self.STUDENT_EMAIL, course=self.COURSE) # Get IDs of those students; make an event for each. with common_utils.Namespace(self.NAMESPACE): student = models.Student.get_by_user(user) user_id = student.user_id p = models.StudentPropertyEntity.create(student, 'foo') p.value = 'foo' p.put() invitation.InvitationStudentProperty.load_or_create(student) questionnaire.StudentFormEntity.load_or_create(student, 'a_form') cm = competency.BaseCompetencyMeasure(user_id) cm.load(123) cm.save() # Assure ourselves that we have exactly one of the items we just added. with common_utils.Namespace(self.NAMESPACE): l = list(models.StudentPropertyEntity.all().run()) self.assertEquals(2, len(l)) # 'foo', 'linear-course-completion' l = list(invitation.InvitationStudentProperty.all().run()) self.assertEquals(1, len(l)) l = list(questionnaire.StudentFormEntity.all().run()) self.assertEquals(1, len(l)) l = list(competency.CompetencyMeasureEntity.all().run()) self.assertEquals(1, len(l)) actions.unregister(self, self.COURSE, do_data_removal=True) self.execute_all_deferred_tasks( models.StudentLifecycleObserver.QUEUE_NAME) self.get(data_removal.DataRemovalCronHandler.URL, headers={'X-AppEngine-Cron': 'True'}) self.execute_all_deferred_tasks() # Assure ourselves that all added items are now gone. with common_utils.Namespace(self.NAMESPACE): l = list(models.StudentPropertyEntity.all().run()) self.assertEquals(0, len(l)) l = list(invitation.InvitationStudentProperty.all().run()) self.assertEquals(0, len(l)) l = list(questionnaire.StudentFormEntity.all().run()) self.assertEquals(0, len(l)) l = list(competency.CompetencyMeasureEntity.all().run()) self.assertEquals(0, len(l))
def test_dashboard_access_method(self): with utils.Namespace(self.course_with_access.app_context.namespace): self.assertFalse(dashboard.DashboardHandler.current_user_has_access( self.course_with_access.app_context)) with utils.Namespace(self.course_without_access.app_context.namespace): self.assertFalse(dashboard.DashboardHandler.current_user_has_access( self.course_without_access.app_context)) actions.login(self.USER_EMAIL, is_admin=False) with utils.Namespace(self.course_with_access.app_context.namespace): self.assertTrue(dashboard.DashboardHandler.current_user_has_access( self.course_with_access.app_context)) with utils.Namespace(self.course_without_access.app_context.namespace): self.assertFalse(dashboard.DashboardHandler.current_user_has_access( self.course_without_access.app_context)) actions.logout()
def test_remove_by_email(self): user = actions.login(self.STUDENT_EMAIL) actions.register(self, user.email(), course=self.COURSE) # Get IDs of those students; make an event for each. with common_utils.Namespace(self.NAMESPACE): sse = unsubscribe.SubscriptionStateEntity(key_name=user.email()) sse.save() self._unregister_and_request_data_removal(self.COURSE) self._complete_removal() with common_utils.Namespace(self.NAMESPACE): l = list(unsubscribe.SubscriptionStateEntity.all().run()) self.assertEquals(0, len(l))
def _save(cls, dto): # The "save" operation is not public because clients of the enrollments # module should cause Datastore mutations only via set() and inc(). with common_utils.Namespace(appengine_config.DEFAULT_NAMESPACE_NAME): entity = cls.ENTITY(key_name=dto.id) entity.json = dto.marshal(dto.dict) entity.put()
def on_all_data_removed(cls, user_id): """Called back from DataRemovalCronHandler when batch deletion done.""" # Any user_id we are called for has had all wipeout batch jobs run. # This means that all un-indexed items have been removed for that # user. However, analysis map/reduce jobs may have been running in # parallel with wipeout and re-added items indexed by user ID. Do one # more pass of removing indexed items before we declare the user to be # done. cls._remove_per_course_indexed_items(user_id) # Look through peer courses to see if the user is registered in any. # If not, we can also remove any global settings items. in_other_courses = False for app_context in sites.get_course_index().get_all_courses(): with common_utils.Namespace(app_context.get_namespace_name()): student = models.Student.get_by_user_id(user_id) if student is not None: in_other_courses = True if not in_other_courses: cls._remove_sitewide_indexed_items(user_id) # When the foregoing deletion has completed w/o raising any # exceptions, clean up the final two items that have any user-related # PII. If this fails, the BatchRemovalState record will not be # removed, and the next call to the cron handler will again see the # user has no more batch items to remove, and call us again. @db.transactional(xg=True) def remove_deletion_state_records(user_id): removal_models.ImmediateRemovalState.delete_by_user_id(user_id) removal_models.BatchRemovalState.delete_by_user_id(user_id) remove_deletion_state_records(user_id)
def test_unenroll_commanded_with_delete_requested(self): user = actions.login(self.STUDENT_EMAIL) actions.register(self, self.STUDENT_EMAIL, course=self.COURSE) # Verify user is really there. with common_utils.Namespace(self.NAMESPACE): self.assertIsNotNone(models.Student.get_by_user_id(user.user_id())) # Mark user for data deletion upon unenroll removal_models.ImmediateRemovalState.set_deletion_pending( user.user_id()) response = self.post( models.StudentLifecycleObserver.URL, {'user_id': user.user_id(), 'event': models.StudentLifecycleObserver.EVENT_UNENROLL_COMMANDED, 'timestamp': '2015-05-14T10:02:09.758704Z', 'callbacks': appengine_config.CORE_MODULE_NAME}, headers={'X-AppEngine-QueueName': models.StudentLifecycleObserver.QUEUE_NAME}) self.assertEquals(response.status_int, 200) self.assertEquals('', self.get_log()) # User should still be there, but now marked unenrolled. student = models.Student.get_by_user_id(user.user_id()) self.assertFalse(student.is_enrolled) # Running lifecycle queue should cause data removal to delete user. self.execute_all_deferred_tasks( models.StudentLifecycleObserver.QUEUE_NAME) # User should now be gone. self.assertIsNone(models.Student.get_by_user_id(user.user_id()))
def main(self): # By the time main() is invoked, arguments are parsed and available as # self.args. If you need more complicated argument validation than # argparse gives you, do it here: if self.args.batch_size < 1: sys.exit('--batch size must be positive') if os.path.exists(self.args.path): sys.exit('Cannot download to %s; file exists' % self.args.path) # Arguments passed to etl.py are also parsed and available as # self.etl_args. Here we use them to figure out the requested course's # namespace. namespace = etl_lib.get_context( self.etl_args.course_url_prefix).get_namespace_name() # Because our models are namespaced, we need to change to the requested # course's namespace when doing datastore reads. with common_utils.Namespace(namespace): # This base query can be modified to add whatever filters you need. query = models.Student.all() students = common_utils.iter_all(query, self.args.batch_size) # Write the results. Done! with open(self.args.path, 'w') as f: for student in students: f.write(student.email) f.write('\n')
def get_singleton(cls): with common_utils.Namespace(appengine_config.DEFAULT_NAMESPACE_NAME): entity = cls.get_by_key_name(cls.SINGLETON_KEY) if not entity: entity = cls(key_name=cls.SINGLETON_KEY) entity.last_run = datetime.datetime(1970, 1, 1) return entity
def test_modified_blacklist_contents(self): save_blacklist = models.Student._PROPERTY_EXPORT_BLACKLIST models.Student._PROPERTY_EXPORT_BLACKLIST = [ 'name', 'additional_fields.age', 'additional_fields.gender', ] blacklisted = [ {'name': 'age', 'value': '22'}, {'name': 'gender', 'value': 'female'}, ] permitted = [ {'name': 'goal', 'value': 'complete_course'}, {'name': 'timezone', 'value': 'America/Los_Angeles'}, ] additional_fields = transforms.dumps( [[x['name'], x['value']] for x in blacklisted + permitted]) with utils.Namespace('ns_test'): models.Student( user_id='123456', additional_fields=additional_fields).put() response = transforms.loads(self.get( '/test/rest/data/students/items').body) self.assertEquals(permitted, response['data'][0]['additional_fields']) models.Student._PROPERTY_EXPORT_BLACKLIST = save_blacklist
def post_config_override(self): """Handles 'override' property action.""" name = self.request.get('name') # Find item in registry. item = None if name and name in config.Registry.registered.keys(): item = config.Registry.registered[name] if not item: self.redirect('?action=settings' % self.LINK_URL) with common_utils.Namespace(appengine_config.DEFAULT_NAMESPACE_NAME): # Add new entity if does not exist. try: entity = config.ConfigPropertyEntity.get_by_key_name(name) except db.BadKeyError: entity = None if not entity: entity = config.ConfigPropertyEntity(key_name=name) entity.value = str(item.value) entity.is_draft = True entity.put() models.EventEntity.record( 'override-property', users.get_current_user(), transforms.dumps({ 'name': name, 'value': str(entity.value)})) self.redirect('%s?%s' % (self.URL, urllib.urlencode( {'action': 'config_edit', 'name': name})))
def setUp(self): super(DriveTestBase, self).setUp() actions.login(self.ADMIN_EMAIL, is_admin=True) self.app_context = actions.simple_add_course( self.COURSE_NAME, self.ADMIN_EMAIL, 'Drive Course') self.base = '/{}'.format(self.COURSE_NAME) # have a syncing policy in place already with utils.Namespace(self.app_context.namespace): self.setup_schedule_for_file('3') self.setup_schedule_for_file('5') self.setup_schedule_for_file('6', synced=True) # remove all hooks for handler, hooks in self.HANDLER_HOOKS: for hook in hooks: self.swap(handler, hook, []) # Prevent it from consulting the settings or calling out self.swap( drive_api_manager._DriveManager, 'from_app_context', classmethod(mocks.manager_from_mock)) self.swap( drive_api_manager._DriveManager, 'from_code', classmethod(mocks.manager_from_mock)) self.swap( handlers.AbstractDriveDashboardHandler, 'setup_drive', mocks.setup_drive) self.swap( drive_settings, 'get_secrets', mocks.get_secrets)
def setUp(self): super(StudentRedirectTestBase, self).setUp() context = actions.simple_add_course(COURSE_NAME, ADMIN_EMAIL, COURSE_TITLE) course = courses.Course(None, context) self.unit = course.add_unit() self.unit.title = 'The Unit' self.unit.now_available = True self.lesson_one = course.add_lesson(self.unit) self.lesson_one.title = 'Lesson One' self.lesson_one.now_available = True self.lesson_two = course.add_lesson(self.unit) self.lesson_two.title = 'Lesson Two' self.lesson_two.now_available = True self.assessment = course.add_assessment() self.assessment.title = 'The Assessment' self.assessment.now_available = True course.save() actions.login(REGISTERED_STUDENT_EMAIL) actions.register(self, REGISTERED_STUDENT_NAME, COURSE_NAME) # Actions.register views the student's profile page; clear this out. with common_utils.Namespace(NAMESPACE): prefs = models.StudentPreferencesDAO.load_or_create() prefs.last_location = None models.StudentPreferencesDAO.save(prefs)
def test_large_volume_of_random_bytes_is_sharded(self): r = random.Random() r.seed(0) orig_data = ''.join([ chr(r.randrange(256)) for x in xrange(int(vfs._MAX_VFS_SHARD_SIZE + 1)) ]) namespace = 'ns_foo' fs = vfs.DatastoreBackedFileSystem(namespace, '/') filename = '/foo' fs.put(filename, StringIO.StringIO(orig_data)) # fs.put() clears cache, so this will do a direct read. actual = fs.get(filename).read() self.assertEquals(orig_data, actual) # And again, this time from cache. actual = fs.get(filename).read() self.assertEquals(orig_data, actual) # Verify that sharded items exist with appropriate sizes. file_key_names = vfs.DatastoreBackedFileSystem._generate_file_key_names( filename, vfs._MAX_VFS_SHARD_SIZE + 1) self.assertEquals( 2, len(file_key_names), 'Verify attempting to store a too-large file makes multiple shards' ) with common_utils.Namespace(namespace): shard_0 = vfs.FileDataEntity.get_by_key_name(file_key_names[0]) self.assertEquals(vfs._MAX_VFS_SHARD_SIZE, len(shard_0.data)) shard_1 = vfs.FileDataEntity.get_by_key_name(file_key_names[1]) self.assertEquals(1, len(shard_1.data))
def test_set_then_unset_subscription_state(self): with utils.Namespace(self.namespace): self.assertSubscribed(self.EMAIL, self.namespace) unsubscribe.set_subscribed(self.EMAIL, True) self.assertSubscribed(self.EMAIL, self.namespace) unsubscribe.set_subscribed(self.EMAIL, False) self.assertUnsubscribed(self.EMAIL, self.namespace)
def test_assessments_with_tracks_not_settable_as_pre_post(self): self.assessment_one.labels = str(self.track_one_id) self.assessment_two.labels = str(self.track_one_id) self.course.save() unit_rest_handler = unit_lesson_editor.UnitRESTHandler() unit_rest_handler.app_context = self.course.app_context with common_utils.Namespace(NAMESPACE): errors = [] unit_rest_handler.apply_updates( self.unit_no_lessons, { 'title': self.unit_no_lessons.title, 'now_available': self.unit_no_lessons.now_available, 'label_groups': [], 'pre_assessment': self.assessment_one.unit_id, 'post_assessment': self.assessment_two.unit_id, 'show_contents_on_one_page': False, 'manual_progress': False, 'description': None, 'unit_header': None, 'unit_footer': None, }, errors) self.assertEquals([ 'Assessment "Assessment One" has track labels, so it ' 'cannot be used as a pre/post unit element', 'Assessment "Assessment Two" has track labels, so it ' 'cannot be used as a pre/post unit element' ], errors)
def setUp(self): super(MultipleChoiceTagTests, self).setUp() self.base = '/' + COURSE_NAME self.app_context = actions.simple_add_course(COURSE_NAME, ADMIN_EMAIL, 'Assessment Tags') self.namespace = 'ns_%s' % COURSE_NAME with utils.Namespace(self.namespace): dto = models.QuestionDTO(None, transforms.loads(self.MC_1_JSON)) self.mc_1_id = models.QuestionDAO.save(dto) dto = models.QuestionDTO(None, transforms.loads(self.MC_2_JSON)) self.mc_2_id = models.QuestionDAO.save(dto) dto = models.QuestionGroupDTO( None, transforms.loads(self.QG_1_JSON_TEMPLATE % (self.mc_1_id, self.mc_2_id))) self.qg_1_id = models.QuestionGroupDAO.save(dto) self.course = courses.Course(None, self.app_context) self.assessment = self.course.add_assessment() self.assessment.availability = courses.AVAILABILITY_AVAILABLE self.assessment.html_content = ( '<question quid="%s" weight="1" instanceid="q1"></question>' '<question-group qgid="%s" instanceid="qg1"></question-group' % (self.mc_1_id, self.qg_1_id)) self.course.save()
def test_cant_override_reserved_url(self): with common_utils.Namespace(self.app_context.namespace): with self.assertRaises(user_routes.URLReservedError): router = (user_routes.UserCourseRouteManager. from_current_appcontext()) router.add('course', 'test_handler') router.save()
def update_course_config(name, settings): """Merge settings into the saved course.yaml configuration. Args: name: Name of the course. E.g., 'my_test_course'. settings: A nested dict of name/value settings. Names for items here can be found in modules/dashboard/course_settings.py in create_course_registry. See below in simple_add_course() for an example. Returns: Context object for the modified course. """ site_type = 'course' namespace = 'ns_%s' % name slug = '/%s' % name rule = '%s:%s::%s' % (site_type, slug, namespace) context = sites.get_all_courses(rule)[0] environ = courses.deep_dict_merge(settings, courses.Course.get_environ(context)) content = yaml.safe_dump(environ) with common_utils.Namespace(namespace): context.fs.put( context.get_config_filename(), vfs.string_to_stream(unicode(content))) course_config = config.Registry.test_overrides.get( sites.GCB_COURSES_CONFIG.name, 'course:/:/') if rule not in course_config: course_config = '%s, %s' % (rule, course_config) sites.setup_courses(course_config) return context
def test_modified_blacklist_contents(self): # pylint: disable-msg=protected-access save_blacklist = models.Student._PROPERTY_EXPORT_BLACKLIST models.Student._PROPERTY_EXPORT_BLACKLIST = [ 'name', 'additional_fields.age', 'additional_fields.gender', ] blacklisted_fields = [ ['age', 22], ['gender', 'female'], ] permitted_fields = [['goal', 'complete_course'], ['timezone', 'America/Los_Angeles']] additional_fields = transforms.dumps(blacklisted_fields + permitted_fields) with utils.Namespace('ns_test'): models.Student(user_id='123456', additional_fields=additional_fields).put() response = transforms.loads( self.get('/test/rest/data/students/items').body) self.assertEquals({k: v for k, v in permitted_fields}, response['data'][0]['additional_fields']) models.Student._PROPERTY_EXPORT_BLACKLIST = save_blacklist
def test_unenroll_commanded_only_unenrolls_student(self): # Register user with profile enabled, so as to trigger call to # sites.get_course_for_current_request() when profile is updated # from lifecycle queue callback handler. user = actions.login(self.STUDENT_EMAIL) actions.register(self, self.STUDENT_EMAIL) # Verify user is really there. with common_utils.Namespace(self.NAMESPACE): self.assertIsNotNone(models.Student.get_by_user_id(user.user_id())) # Add taskqueue task so that queue callback happens w/o 'self.base' # being added to the URL and implicitly getting the course context # set. task = taskqueue.Task( params={ 'event': models.StudentLifecycleObserver.EVENT_UNENROLL_COMMANDED, 'user_id': user.user_id(), 'timestamp': '2015-05-14T10:02:09.758704Z', 'callbacks': appengine_config.CORE_MODULE_NAME }, target=taskqueue.DEFAULT_APP_VERSION) task.add('user-lifecycle') # Taskqueue add should not have updated student. student = models.Student.get_by_user_id(user.user_id()) self.assertTrue(student.is_enrolled) self.execute_all_deferred_tasks( models.StudentLifecycleObserver.QUEUE_NAME) # User should still be there, but now marked unenrolled. student = models.Student.get_by_user_id(user.user_id()) self.assertFalse(student.is_enrolled)
def test_tag_before_and_after_submission(self): assessment = self.course.add_assessment() assessment.html_content = ( '<text-file-upload-tag ' ' display_length="100" instanceid="this-tag-id">' '</text-file-upload-tag>') self.course.save() response = self.get('assessment?name=%s' % assessment.unit_id) dom = self.parse_html_string(response.body) warning = dom.find( './/*[@class="user-upload-form-warning"]').text.strip() self.assertEquals('Maximum file size is 1MB.', warning) with common_utils.Namespace('ns_' + self._COURSE_NAME): student, _ = models.Student.get_first_by_email(self._STUDENT_EMAIL) student_work.Submission.write(assessment.unit_id, student.get_key(), 'contents', instance_id='this-tag-id') response = self.get('assessment?name=%s' % assessment.unit_id) dom = self.parse_html_string(response.body) warning = dom.find( './/*[@class="user-upload-form-warning"]').text.strip() self.assertEquals( 'You have already submitted; submit again to replace your previous ' 'entry.', warning)
def setUp(self): super(StudentLabelsTest, self).setUp() actions.simple_add_course(COURSE_NAME, ADMIN_EMAIL, COURSE_TITLE) with common_utils.Namespace(NAMESPACE): self.foo_id = models.LabelDAO.save(models.LabelDTO( None, {'title': 'Foo', 'descripton': 'foo', 'type': models.LabelDTO.LABEL_TYPE_COURSE_TRACK})) self.bar_id = models.LabelDAO.save(models.LabelDTO( None, {'title': 'Bar', 'descripton': 'bar', 'type': models.LabelDTO.LABEL_TYPE_COURSE_TRACK})) self.baz_id = models.LabelDAO.save(models.LabelDTO( None, {'title': 'Baz', 'descripton': 'baz', 'type': models.LabelDTO.LABEL_TYPE_COURSE_TRACK})) self.quux_id = models.LabelDAO.save(models.LabelDTO( None, {'title': 'Quux', 'descripton': 'quux', 'type': models.LabelDTO.LABEL_TYPE_GENERAL})) actions.login(REGISTERED_STUDENT_EMAIL) actions.register(self, REGISTERED_STUDENT_NAME, COURSE_NAME) actions.logout()
def test_notifications_succeed(self): actions.login(self.STUDENT_EMAIL) user_id = None actions.register(self, self.STUDENT_EMAIL) self.assertIsNone(self._user_id) self.execute_all_deferred_tasks( models.StudentLifecycleObserver.QUEUE_NAME) self.assertIsNotNone(self._user_id) user_id = self._user_id self.assertEquals(1, self._num_add_calls) self.assertEquals(0, self._num_unenroll_calls) self.assertEquals(0, self._num_reenroll_calls) actions.unregister(self) self.execute_all_deferred_tasks( models.StudentLifecycleObserver.QUEUE_NAME) self.assertEquals(1, self._num_add_calls) self.assertEquals(1, self._num_unenroll_calls) self.assertEquals(0, self._num_reenroll_calls) with common_utils.Namespace(self.NAMESPACE): models.StudentProfileDAO.update(user_id, self.STUDENT_EMAIL, is_enrolled=True) self.execute_all_deferred_tasks( models.StudentLifecycleObserver.QUEUE_NAME) self.assertEquals(1, self._num_add_calls) self.assertEquals(1, self._num_unenroll_calls) self.assertEquals(1, self._num_reenroll_calls)
def post_config_reset(self): """Handles 'reset' property action.""" name = self.request.get('name') # Find item in registry. item = None if name and name in config.Registry.registered.keys(): item = config.Registry.registered[name] if not item: self.redirect('%s?action=settings' % self.LINK_URL) with common_utils.Namespace(appengine_config.DEFAULT_NAMESPACE_NAME): # Delete if exists. try: entity = config.ConfigPropertyEntity.get_by_key_name(name) if entity: old_value = entity.value entity.delete() models.EventEntity.record( 'delete-property', users.get_current_user(), transforms.dumps({ 'name': name, 'value': str(old_value) })) except db.BadKeyError: pass self.redirect('%s?action=settings' % self.URL)