def test_grades_csv(self): self.course.enable_ccx = True RequestCache.clear_request_cache() url = reverse( 'ccx_grades_csv', kwargs={'course_id': self.ccx_key} ) response = self.client.get(url) self.assertEqual(response.status_code, 200) # Are the grades downloaded as an attachment? self.assertEqual( response['content-disposition'], 'attachment' ) rows = response.content.strip().split('\r') headers = rows[0] # picking first student records data = dict(zip(headers.strip().split(','), rows[1].strip().split(','))) self.assertNotIn('HW 04', data) self.assertEqual(data['HW 01'], '0.75') self.assertEqual(data['HW 02'], '0.5') self.assertEqual(data['HW 03'], '0.25') self.assertEqual(data['HW Avg'], '0.5')
def instrument_course_progress_render(self, course_width, enable_ccx, queries, reads, xblocks): """ Renders the progress page, instrumenting Mongo reads and SQL queries. """ self.setup_course(course_width, enable_ccx) # Switch to published-only mode to simulate the LMS with self.settings(MODULESTORE_BRANCH='published-only'): # Clear all caches before measuring for cache in settings.CACHES: get_cache(cache).clear() # Refill the metadata inheritance cache modulestore().get_course(self.course.id, depth=None) # We clear the request cache to simulate a new request in the LMS. RequestCache.clear_request_cache() # Reset the list of provider classes, so that our django settings changes # can actually take affect. OverrideFieldData.provider_classes = None with self.assertNumQueries(queries): with check_mongo_calls(reads): with check_sum_of_calls(XBlock, ['__init__'], xblocks, xblocks, include_arguments=False): self.grade_course(self.course)
def test_request_context_caching(self): """ Test that the RequestContext is cached in the RequestCache. """ with patch('edxmako.request_context.get_current_request', return_value=None): # requestcontext should be None, because the cache isn't filled self.assertIsNone(get_template_request_context()) with patch('edxmako.request_context.get_current_request', return_value=self.request): # requestcontext should not be None, and should fill the cache self.assertIsNotNone(get_template_request_context()) mock_get_current_request = Mock() with patch('edxmako.request_context.get_current_request', mock_get_current_request): # requestcontext should not be None, because the cache is filled self.assertIsNotNone(get_template_request_context()) mock_get_current_request.assert_not_called() RequestCache.clear_request_cache() with patch('edxmako.request_context.get_current_request', return_value=None): # requestcontext should be None, because the cache isn't filled self.assertIsNone(get_template_request_context())
def test_course_waffle_flag(self, data): """ Tests various combinations of a flag being set in waffle and overridden for a course. """ RequestCache.clear_request_cache() with patch.object(WaffleFlagCourseOverrideModel, 'override_value', return_value=data['course_override']): with override_flag(self.NAMESPACED_FLAG_NAME, active=data['waffle_enabled']): # check twice to test that the result is properly cached self.assertEqual(self.TEST_COURSE_FLAG.is_enabled(self.TEST_COURSE_KEY), data['result']) self.assertEqual(self.TEST_COURSE_FLAG.is_enabled(self.TEST_COURSE_KEY), data['result']) # result is cached, so override check should happen once WaffleFlagCourseOverrideModel.override_value.assert_called_once_with( self.NAMESPACED_FLAG_NAME, self.TEST_COURSE_KEY ) # check flag for a second course if data['course_override'] == WaffleFlagCourseOverrideModel.ALL_CHOICES.unset: # When course override wasn't set for the first course, the second course will get the same # cached value from waffle. self.assertEqual(self.TEST_COURSE_FLAG.is_enabled(self.TEST_COURSE_2_KEY), data['waffle_enabled']) else: # When course override was set for the first course, it should not apply to the second # course which should get the default value of False. self.assertEqual(self.TEST_COURSE_FLAG.is_enabled(self.TEST_COURSE_2_KEY), False)
def test_get_template_path(self): """ Tests to make sure the get_template_path function works as expected. """ # if the current site has associated SiteTheme then get_template_path should return the argument as is. with patch( "openedx.core.djangoapps.theming.helpers.current_request_has_associated_site_theme", Mock(return_value=True), ): with patch( "openedx.core.djangoapps.theming.helpers.microsite.is_request_in_microsite", Mock(return_value=True), ): with patch("microsite_configuration.microsite.TEMPLATES_BACKEND") as mock_microsite_backend: mock_microsite_backend.get_template = Mock(return_value="/microsite/about.html") self.assertEqual(theming_helpers.get_template_path("about.html"), "about.html") RequestCache.clear_request_cache() # if the current site does not have associated SiteTheme then get_template_path should return microsite override with patch( "openedx.core.djangoapps.theming.helpers.current_request_has_associated_site_theme", Mock(return_value=False), ): with patch( "openedx.core.djangoapps.theming.helpers.microsite.is_request_in_microsite", Mock(return_value=True), ): with patch("microsite_configuration.microsite.TEMPLATES_BACKEND") as mock_microsite_backend: mock_microsite_backend.get_template_path = Mock(return_value="/microsite/about.html") self.assertEqual(theming_helpers.get_template_path("about.html"), "/microsite/about.html")
def instrument_course_progress_render( self, course_width, enable_ccx, view_as_ccx, sql_queries, mongo_reads, ): """ Renders the progress page, instrumenting Mongo reads and SQL queries. """ course_key = self.setup_course(course_width, enable_ccx, view_as_ccx) # Switch to published-only mode to simulate the LMS with self.settings(MODULESTORE_BRANCH='published-only'): # Clear all caches before measuring for cache in settings.CACHES: caches[cache].clear() # Refill the metadata inheritance cache get_course_in_cache(course_key) # We clear the request cache to simulate a new request in the LMS. RequestCache.clear_request_cache() # Reset the list of provider classes, so that our django settings changes # can actually take affect. OverrideFieldData.provider_classes = None with self.assertNumQueries(sql_queries, using='default'): with self.assertNumQueries(0, using='student_module_history'): with self.assertMongoCallCount(mongo_reads): with self.assertXBlockInstantiations(1): self.grade_course(course_key)
def test_request_cached_with_caches_despite_changing_wrapped_result(self): """ Ensure that after caching a result, we always send it back, even if the underlying result changes. """ RequestCache.clear_request_cache() to_be_wrapped = Mock() to_be_wrapped.side_effect = [1, 2, 3] self.assertEqual(to_be_wrapped.call_count, 0) def mock_wrapper(*args, **kwargs): """Simple wrapper to let us decorate our mock.""" return to_be_wrapped(*args, **kwargs) wrapped = request_cached(mock_wrapper) result = wrapped() self.assertEqual(result, 1) self.assertEqual(to_be_wrapped.call_count, 1) result = wrapped() self.assertEqual(result, 1) self.assertEqual(to_be_wrapped.call_count, 1) direct_result = mock_wrapper() self.assertEqual(direct_result, 2) self.assertEqual(to_be_wrapped.call_count, 2) result = wrapped() self.assertEqual(result, 1) self.assertEqual(to_be_wrapped.call_count, 2) direct_result = mock_wrapper() self.assertEqual(direct_result, 3) self.assertEqual(to_be_wrapped.call_count, 3)
def test_request_cached_mixed_unicode_str_args(self): """ Ensure that request_cached can work with mixed str and Unicode parameters. """ RequestCache.clear_request_cache() def dummy_function(arg1, arg2): """ A dummy function that expects an str and unicode arguments. """ assert isinstance(arg1, str), 'First parameter has to be of type `str`' assert isinstance( arg2, unicode), 'Second parameter has to be of type `unicode`' return True self.assertTrue(dummy_function('Hello', u'World'), 'Should be callable with ASCII chars') self.assertTrue(dummy_function('H∂llå', u'Wørld'), 'Should be callable with non-ASCII chars') wrapped = request_cached(dummy_function) self.assertTrue(wrapped('Hello', u'World'), 'Wrapper should handle ASCII only chars') self.assertTrue(wrapped('H∂llå', u'Wørld'), 'Wrapper should handle non-ASCII chars')
def test_grades_csv(self): self.course.enable_ccx = True RequestCache.clear_request_cache() url = reverse('ccx_grades_csv', kwargs={'course_id': self.ccx_key}) response = self.client.get(url) self.assertEqual(response.status_code, 200) # Are the grades downloaded as an attachment? self.assertEqual(response['content-disposition'], 'attachment') rows = response.content.strip().split('\r') headers = rows[0] records = dict() for i in range(1, len(rows)): data = dict( zip(headers.strip().split(','), rows[i].strip().split(','))) records[data['username']] = data student_data = records[self.student.username] # pylint: disable=no-member self.assertNotIn('HW 04', student_data) self.assertEqual(student_data['HW 01'], '0.75') self.assertEqual(student_data['HW 02'], '0.5') self.assertEqual(student_data['HW 03'], '0.25') self.assertEqual(student_data['HW Avg'], '0.5')
def test_setting_override(self, is_enabled, override_choice, expected_result): RequestCache.clear_request_cache() self.set_waffle_course_override(override_choice, is_enabled) override_value = WaffleFlagCourseOverrideModel.override_value( self.WAFFLE_TEST_NAME, self.TEST_COURSE_KEY) self.assertEqual(override_value, expected_result)
def test_grades_csv(self): self.course.enable_ccx = True RequestCache.clear_request_cache() url = reverse( 'ccx_grades_csv', kwargs={'course_id': self.ccx_key} ) response = self.client.get(url) self.assertEqual(response.status_code, 200) # Are the grades downloaded as an attachment? self.assertEqual( response['content-disposition'], 'attachment' ) rows = response.content.strip().split('\r') headers = rows[0] records = dict() for i in range(1, len(rows)): data = dict(zip(headers.strip().split(','), rows[i].strip().split(','))) records[data['username']] = data student_data = records[self.student.username] # pylint: disable=no-member self.assertNotIn('HW 04', student_data) self.assertEqual(student_data['HW 01'], '0.75') self.assertEqual(student_data['HW 02'], '0.5') self.assertEqual(student_data['HW 03'], '0.25') self.assertEqual(student_data['HW Avg'], '0.5')
def test_undefined_waffle_flag(self, data): """ Test flag with various defaults provided for undefined waffle flags. """ RequestCache.clear_request_cache() test_course_flag = CourseWaffleFlag( self.TEST_NAMESPACE, self.FLAG_NAME, flag_undefined_default=data['flag_undefined_default'] ) with patch.object( WaffleFlagCourseOverrideModel, 'override_value', return_value=WaffleFlagCourseOverrideModel.ALL_CHOICES.unset ): # check twice to test that the result is properly cached self.assertEqual(test_course_flag.is_enabled(self.TEST_COURSE_KEY), data['result']) self.assertEqual(test_course_flag.is_enabled(self.TEST_COURSE_KEY), data['result']) # result is cached, so override check should happen once WaffleFlagCourseOverrideModel.override_value.assert_called_once_with( self.NAMESPACED_FLAG_NAME, self.TEST_COURSE_KEY )
def test_setting_override(self, is_enabled, override_choice, expected_result): RequestCache.clear_request_cache() self.set_waffle_course_override(override_choice, is_enabled) override_value = WaffleFlagCourseOverrideModel.override_value( self.WAFFLE_TEST_NAME, self.TEST_COURSE_KEY ) self.assertEqual(override_value, expected_result)
def test_setting_override_multiple_times(self): RequestCache.clear_request_cache() self.set_waffle_course_override(self.OVERRIDE_CHOICES.on) self.set_waffle_course_override(self.OVERRIDE_CHOICES.off) override_value = WaffleFlagCourseOverrideModel.override_value( self.WAFFLE_TEST_NAME, self.TEST_COURSE_KEY) self.assertEqual(override_value, self.OVERRIDE_CHOICES.off)
def test_setting_override_multiple_times(self): RequestCache.clear_request_cache() self.set_waffle_course_override(self.OVERRIDE_CHOICES.on) self.set_waffle_course_override(self.OVERRIDE_CHOICES.off) override_value = WaffleFlagCourseOverrideModel.override_value( self.WAFFLE_TEST_NAME, self.TEST_COURSE_KEY ) self.assertEqual(override_value, self.OVERRIDE_CHOICES.off)
def handle(self, *args, **options): # pylint: disable=unused-argument """ Iterates through each course, serializes them into graphs, and saves those graphs to neo4j. """ # first, make sure that there's a valid neo4j configuration if settings.NEO4J_CONFIG is None: raise CommandError( "No neo4j configuration (NEO4J_CONFIG) defined in lms.auth.json." ) auth_params = ["{host}:{https_port}", "{user}", "{password}"] authenticate(*[param.format(**settings.NEO4J_CONFIG) for param in auth_params]) graph = Graph(**settings.NEO4J_CONFIG) mss = ModuleStoreSerializer() total_number_of_courses = len(mss.all_courses) for index, course in enumerate(mss.all_courses): # first, clear the request cache to prevent memory leaks RequestCache.clear_request_cache() log.info( "Now exporting %s to neo4j: course %d of %d total courses", course.id, index + 1, total_number_of_courses ) nodes, relationships = mss.serialize_course(course.id) log.info( "%d nodes and %d relationships in %s", len(nodes), len(relationships), course.id ) transaction = graph.begin() try: # first, delete existing course transaction.run( "MATCH (n:item) WHERE n.course_key='{}' DETACH DELETE n".format( six.text_type(course.id) ) ) # now, re-add it self.add_to_transaction(nodes, transaction) self.add_to_transaction(relationships, transaction) transaction.commit() except Exception: # pylint: disable=broad-except log.exception( "Error trying to dump course %s to neo4j, rolling back", six.text_type(course.id) ) transaction.rollback()
def handle(self, *args, **options): # pylint: disable=unused-argument """ Iterates through each course, serializes them into graphs, and saves those graphs to neo4j. """ host = options['host'] port = options['port'] neo4j_user = options['user'] neo4j_password = options['password'] authenticate( "{host}:{port}".format(host=host, port=port), neo4j_user, neo4j_password, ) graph = Graph(bolt=True, password=neo4j_password, user=neo4j_user, https_port=port, host=host, secure=True) mss = ModuleStoreSerializer() total_number_of_courses = len(mss.all_courses) for index, course in enumerate(mss.all_courses): # first, clear the request cache to prevent memory leaks RequestCache.clear_request_cache() log.info( "Now exporting %s to neo4j: course %d of %d total courses", course.id, index + 1, total_number_of_courses) nodes, relationships = mss.serialize_course(course.id) log.info("%d nodes and %d relationships in %s", len(nodes), len(relationships), course.id) transaction = graph.begin() try: # first, delete existing course transaction.run( "MATCH (n:item) WHERE n.course_key='{}' DETACH DELETE n". format(six.text_type(course.id))) # now, re-add it self.add_to_transaction(nodes, transaction) self.add_to_transaction(relationships, transaction) transaction.commit() except Exception: # pylint: disable=broad-except log.exception( "Error trying to dump course %s to neo4j, rolling back", six.text_type(course.id)) transaction.rollback()
def lti_consumer_fields_editing_flag(course_id, enabled_for_course=False): """ Yields CourseEditLTIFieldsEnabledFlag record for unit tests Arguments: course_id (CourseLocator): course locator to control this feature for. enabled_for_course (bool): whether feature is enabled for 'course_id' """ RequestCache.clear_request_cache() CourseEditLTIFieldsEnabledFlag.objects.create(course_id=course_id, enabled=enabled_for_course) yield
def assert_access_to_gated_content(self, user, expected_access): """ Verifies access to gated content for the given user is as expected. """ # clear the request cache to flush any cached access results RequestCache.clear_request_cache() # access to gating content (seq1) remains constant self.assertTrue(bool(has_access(user, 'load', self.seq1, self.course.id))) # access to gated content (seq2) is as expected self.assertEquals(bool(has_access(user, 'load', self.seq2, self.course.id)), expected_access)
def test_gradebook(self): self.course.enable_ccx = True RequestCache.clear_request_cache() url = reverse("ccx_gradebook", kwargs={"course_id": self.ccx_key}) response = self.client.get(url) self.assertEqual(response.status_code, 200) # Max number of student per page is one. Patched setting MAX_STUDENTS_PER_PAGE_GRADE_BOOK = 1 self.assertEqual(len(response.mako_context["students"]), 1) # pylint: disable=no-member student_info = response.mako_context["students"][0] # pylint: disable=no-member self.assertEqual(student_info["grade_summary"]["percent"], 0.5) self.assertEqual(student_info["grade_summary"]["grade_breakdown"][0]["percent"], 0.5) self.assertEqual(len(student_info["grade_summary"]["section_breakdown"]), 4)
def test_gradebook(self): self.course.enable_ccx = True RequestCache.clear_request_cache() url = reverse('ccx_gradebook', kwargs={'course_id': self.ccx_key}) response = self.client.get(url) self.assertEqual(response.status_code, 200) student_info = response.mako_context['students'][0] # pylint: disable=no-member self.assertEqual(student_info['grade_summary']['percent'], 0.5) self.assertEqual( student_info['grade_summary']['grade_breakdown'][0]['percent'], 0.5) self.assertEqual( len(student_info['grade_summary']['section_breakdown']), 4)
def handle(self, *args, **options): # pylint: disable=unused-argument """ Iterates through each course, serializes them into graphs, and saves those graphs to neo4j. """ # first, make sure that there's a valid neo4j configuration if settings.NEO4J_CONFIG is None: raise CommandError( "No neo4j configuration (NEO4J_CONFIG) defined in lms.auth.json." ) auth_params = ["{host}:{https_port}", "{user}", "{password}"] authenticate( *[param.format(**settings.NEO4J_CONFIG) for param in auth_params]) graph = Graph(**settings.NEO4J_CONFIG) mss = ModuleStoreSerializer() total_number_of_courses = len(mss.all_courses) for index, course in enumerate(mss.all_courses): # first, clear the request cache to prevent memory leaks RequestCache.clear_request_cache() log.info( "Now exporting %s to neo4j: course %d of %d total courses", course.id, index + 1, total_number_of_courses) nodes, relationships = mss.serialize_course(course.id) log.info("%d nodes and %d relationships in %s", len(nodes), len(relationships), course.id) transaction = graph.begin() try: # first, delete existing course transaction.run( "MATCH (n:item) WHERE n.course_key='{}' DETACH DELETE n". format(six.text_type(course.id))) # now, re-add it self.add_to_transaction(nodes, transaction) self.add_to_transaction(relationships, transaction) transaction.commit() except Exception: # pylint: disable=broad-except log.exception( "Error trying to dump course %s to neo4j, rolling back", six.text_type(course.id)) transaction.rollback()
def test_grades_csv(self): self.course.enable_ccx = True RequestCache.clear_request_cache() url = reverse('ccx_grades_csv', kwargs={'course_id': self.ccx_key}) response = self.client.get(url) self.assertEqual(response.status_code, 200) headers, row = (row.strip().split(',') for row in response.content.strip().split('\n')) data = dict(zip(headers, row)) self.assertTrue('HW 04' not in data) self.assertEqual(data['HW 01'], '0.75') self.assertEqual(data['HW 02'], '0.5') self.assertEqual(data['HW 03'], '0.25') self.assertEqual(data['HW Avg'], '0.5')
def new_assets_page_feature_flags(global_flag, enabled_for_all_courses=False, course_id=None, enabled_for_course=False): """ Most test cases will use a single call to this manager, as they need to set the global setting and the course-specific setting for a single course. """ RequestCache.clear_request_cache() NewAssetsPageFlag.objects.create( enabled=global_flag, enabled_for_all_courses=enabled_for_all_courses) if course_id: CourseNewAssetsPageFlag.objects.create(course_id=course_id, enabled=enabled_for_course) yield
def persistent_grades_feature_flags( global_flag, enabled_for_all_courses=False, course_id=None, enabled_for_course=False ): """ Most test cases will use a single call to this manager, as they need to set the global setting and the course-specific setting for a single course. """ RequestCache.clear_request_cache() PersistentGradesEnabledFlag.objects.create(enabled=global_flag, enabled_for_all_courses=enabled_for_all_courses) if course_id: CoursePersistentGradesFlag.objects.create(course_id=course_id, enabled=enabled_for_course) yield
def new_assets_page_feature_flags( global_flag, enabled_for_all_courses=False, course_id=None, enabled_for_course=False ): """ Most test cases will use a single call to this manager, as they need to set the global setting and the course-specific setting for a single course. """ RequestCache.clear_request_cache() NewAssetsPageFlag.objects.create(enabled=global_flag, enabled_for_all_courses=enabled_for_all_courses) if course_id: CourseNewAssetsPageFlag.objects.create(course_id=course_id, enabled=enabled_for_course) yield
def clear_caches(cls): """ Clear all of the caches defined in settings.CACHES. """ # N.B. As of 2016-04-20, Django won't return any caches # from django.core.cache.caches.all() that haven't been # accessed using caches[name] previously, so we loop # over our list of overridden caches, instead. for cache in settings.CACHES: caches[cache].clear() # The sites framework caches in a module-level dictionary. # Clear that. sites.models.SITE_CACHE.clear() RequestCache.clear_request_cache()
def test_gradebook(self): self.course.enable_ccx = True RequestCache.clear_request_cache() url = reverse('ccx_gradebook', kwargs={'course_id': self.ccx_key}) response = self.client.get(url) self.assertEqual(response.status_code, 200) # Max number of student per page is one. Patched setting MAX_STUDENTS_PER_PAGE_GRADE_BOOK = 1 self.assertEqual(len(response.mako_context['students']), 1) # pylint: disable=no-member student_info = response.mako_context['students'][0] # pylint: disable=no-member self.assertEqual(student_info['grade_summary']['percent'], 0.5) self.assertEqual( student_info['grade_summary']['grade_breakdown'][0]['percent'], 0.5) self.assertEqual( len(student_info['grade_summary']['section_breakdown']), 4)
def test_course_waffle_flag(self, data): """ Tests various combinations of a flag being set in waffle and overridden for a course. """ RequestCache.clear_request_cache() with patch.object(WaffleFlagCourseOverrideModel, 'override_value', return_value=data['course_override']): with override_flag(self.NAMESPACED_FLAG_NAME, active=data['waffle_enabled']): # check twice to test that the result is properly cached self.assertEqual(self.TEST_COURSE_FLAG.is_enabled(self.TEST_COURSE_KEY), data['result']) self.assertEqual(self.TEST_COURSE_FLAG.is_enabled(self.TEST_COURSE_KEY), data['result']) # result is cached, so override check should happen once WaffleFlagCourseOverrideModel.override_value.assert_called_once_with( self.NAMESPACED_FLAG_NAME, self.TEST_COURSE_KEY )
def test_gradebook(self): self.course.enable_ccx = True RequestCache.clear_request_cache() url = reverse( 'ccx_gradebook', kwargs={'course_id': self.ccx_key} ) response = self.client.get(url) self.assertEqual(response.status_code, 200) student_info = response.mako_context['students'][0] # pylint: disable=no-member self.assertEqual(student_info['grade_summary']['percent'], 0.5) self.assertEqual( student_info['grade_summary']['grade_breakdown'][0]['percent'], 0.5) self.assertEqual( len(student_info['grade_summary']['section_breakdown']), 4)
def test_request_cached_with_changing_kwargs(self): """ Ensure that calling a decorated function with different keyword arguments will not use a cached value invoked by a previous call with different arguments. """ RequestCache.clear_request_cache() to_be_wrapped = Mock() to_be_wrapped.side_effect = [1, 2, 3, 4, 5, 6] self.assertEqual(to_be_wrapped.call_count, 0) def mock_wrapper(*args, **kwargs): """Simple wrapper to let us decorate our mock.""" return to_be_wrapped(*args, **kwargs) wrapped = request_cached(mock_wrapper) # This will be a miss, and make an underlying call. result = wrapped(1, foo=1) self.assertEqual(result, 1) self.assertEqual(to_be_wrapped.call_count, 1) # This will be a miss, and make an underlying call. result = wrapped(2, foo=2) self.assertEqual(result, 2) self.assertEqual(to_be_wrapped.call_count, 2) # This is bypass of the decorator. direct_result = mock_wrapper(3, foo=3) self.assertEqual(direct_result, 3) self.assertEqual(to_be_wrapped.call_count, 3) # These will be hits, and not make an underlying call. result = wrapped(1, foo=1) self.assertEqual(result, 1) self.assertEqual(to_be_wrapped.call_count, 3) result = wrapped(2, foo=2) self.assertEqual(result, 2) self.assertEqual(to_be_wrapped.call_count, 3) # Since we're changing foo, this will be a miss. result = wrapped(2, foo=5) self.assertEqual(result, 4) self.assertEqual(to_be_wrapped.call_count, 4)
def test_grades_csv(self): self.course.enable_ccx = True RequestCache.clear_request_cache() url = reverse("ccx_grades_csv", kwargs={"course_id": self.ccx_key}) response = self.client.get(url) self.assertEqual(response.status_code, 200) # Are the grades downloaded as an attachment? self.assertEqual(response["content-disposition"], "attachment") rows = response.content.strip().split("\r") headers = rows[0] # picking first student records data = dict(zip(headers.strip().split(","), rows[1].strip().split(","))) self.assertNotIn("HW 04", data) self.assertEqual(data["HW 01"], "0.75") self.assertEqual(data["HW 02"], "0.5") self.assertEqual(data["HW 03"], "0.25") self.assertEqual(data["HW Avg"], "0.5")
def dump_courses_to_neo4j(self, credentials, override_cache=False): """ Method that iterates through a list of courses in a modulestore, serializes them, then submits tasks to write them to neo4j. Arguments: credentials (dict): the necessary credentials to connect to neo4j and create a py2neo `Graph` object override_cache: serialize the courses even if they'be been recently serialized Returns: two lists--one of the courses that were successfully written to neo4j and one of courses that were not. """ total_number_of_courses = len(self.course_keys) submitted_courses = [] skipped_courses = [] graph = authenticate_and_create_graph(credentials) for index, course_key in enumerate(self.course_keys): # first, clear the request cache to prevent memory leaks RequestCache.clear_request_cache() log.info( "Now submitting %s for export to neo4j: course %d of %d total courses", course_key, index + 1, total_number_of_courses, ) if not (override_cache or should_dump_course(course_key, graph)): log.info("skipping submitting %s, since it hasn't changed", course_key) skipped_courses.append(six.text_type(course_key)) continue dump_course_to_neo4j.apply_async( args=[six.text_type(course_key), credentials], ) submitted_courses.append(six.text_type(course_key)) return submitted_courses, skipped_courses
def test_grades_csv(self): self.course.enable_ccx = True RequestCache.clear_request_cache() url = reverse( 'ccx_grades_csv', kwargs={'course_id': self.ccx_key} ) response = self.client.get(url) self.assertEqual(response.status_code, 200) headers, row = ( row.strip().split(',') for row in response.content.strip().split('\n') ) data = dict(zip(headers, row)) self.assertTrue('HW 04' not in data) self.assertEqual(data['HW 01'], '0.75') self.assertEqual(data['HW 02'], '0.5') self.assertEqual(data['HW 03'], '0.25') self.assertEqual(data['HW Avg'], '0.5')
def test_request_cached_with_none_result(self): """ Ensure that calling a decorated function that returns None properly caches the result and doesn't recall the underlying function. """ RequestCache.clear_request_cache() to_be_wrapped = Mock() to_be_wrapped.side_effect = [None, None, None, 1, 1] self.assertEqual(to_be_wrapped.call_count, 0) def mock_wrapper(*args, **kwargs): """Simple wrapper to let us decorate our mock.""" return to_be_wrapped(*args, **kwargs) wrapped = request_cached(mock_wrapper) # This will be a miss, and make an underlying call. result = wrapped(1) self.assertEqual(result, None) self.assertEqual(to_be_wrapped.call_count, 1) # This will be a miss, and make an underlying call. result = wrapped(2) self.assertEqual(result, None) self.assertEqual(to_be_wrapped.call_count, 2) # This is bypass of the decorator. direct_result = mock_wrapper(3) self.assertEqual(direct_result, None) self.assertEqual(to_be_wrapped.call_count, 3) # These will be hits, and not make an underlying call. result = wrapped(1) self.assertEqual(result, None) self.assertEqual(to_be_wrapped.call_count, 3) result = wrapped(2) self.assertEqual(result, None) self.assertEqual(to_be_wrapped.call_count, 3)
def test_get_template_path(self): """ Tests to make sure the get_template_path function works as expected. """ # if the current site has associated SiteTheme then get_template_path should return the argument as is. with patch( "openedx.core.djangoapps.theming.helpers.current_request_has_associated_site_theme", Mock(return_value=True), ): with patch( "openedx.core.djangoapps.theming.helpers.microsite.is_request_in_microsite", Mock(return_value=True), ): with patch( "microsite_configuration.microsite.TEMPLATES_BACKEND" ) as mock_microsite_backend: mock_microsite_backend.get_template = Mock( return_value="/microsite/about.html") self.assertEqual( theming_helpers.get_template_path("about.html"), "about.html") RequestCache.clear_request_cache() # if the current site does not have associated SiteTheme then get_template_path should return microsite override with patch( "openedx.core.djangoapps.theming.helpers.current_request_has_associated_site_theme", Mock(return_value=False), ): with patch( "openedx.core.djangoapps.theming.helpers.microsite.is_request_in_microsite", Mock(return_value=True), ): with patch( "microsite_configuration.microsite.TEMPLATES_BACKEND" ) as mock_microsite_backend: mock_microsite_backend.get_template_path = Mock( return_value="/microsite/about.html") self.assertEqual( theming_helpers.get_template_path("about.html"), "/microsite/about.html")
def test_request_cached_mixed_unicode_str_args(self): """ Ensure that request_cached can work with mixed str and Unicode parameters. """ RequestCache.clear_request_cache() def dummy_function(arg1, arg2): """ A dummy function that expects an str and unicode arguments. """ assert isinstance(arg1, str), 'First parameter has to be of type `str`' assert isinstance(arg2, unicode), 'Second parameter has to be of type `unicode`' return True self.assertTrue(dummy_function('Hello', u'World'), 'Should be callable with ASCII chars') self.assertTrue(dummy_function('H∂llå', u'Wørld'), 'Should be callable with non-ASCII chars') wrapped = request_cached(dummy_function) self.assertTrue(wrapped('Hello', u'World'), 'Wrapper should handle ASCII only chars') self.assertTrue(wrapped('H∂llå', u'Wørld'), 'Wrapper should handle non-ASCII chars')
def dump_courses_to_neo4j(self, graph, override_cache=False): """ Method that iterates through a list of courses in a modulestore, serializes them, then submits tasks to write them to neo4j. Arguments: graph: py2neo graph object override_cache: serialize the courses even if they'be been recently serialized Returns: two lists--one of the courses that were successfully written to neo4j and one of courses that were not. """ total_number_of_courses = len(self.course_keys) submitted_courses = [] skipped_courses = [] for index, course_key in enumerate(self.course_keys): # first, clear the request cache to prevent memory leaks RequestCache.clear_request_cache() log.info( "Now exporting %s to neo4j: course %d of %d total courses", course_key, index + 1, total_number_of_courses, ) if not (override_cache or self.should_dump_course(course_key, graph)): log.info("skipping dumping %s, since it hasn't changed", course_key) skipped_courses.append(unicode(course_key)) else: self.dump_course_to_neo4j(course_key, graph) submitted_courses.append(unicode(course_key)) return submitted_courses, skipped_courses
def test_request_cached_miss_and_then_hit(self): """ Ensure that after a cache miss, we fill the cache and can hit it. """ RequestCache.clear_request_cache() to_be_wrapped = Mock() to_be_wrapped.return_value = 42 self.assertEqual(to_be_wrapped.call_count, 0) def mock_wrapper(*args, **kwargs): """Simple wrapper to let us decorate our mock.""" return to_be_wrapped(*args, **kwargs) wrapped = request_cached(mock_wrapper) result = wrapped() self.assertEqual(result, 42) self.assertEqual(to_be_wrapped.call_count, 1) result = wrapped() self.assertEqual(result, 42) self.assertEqual(to_be_wrapped.call_count, 1)
def instrument_course_progress_render(self, dataset_index, queries, reads, xblocks): """ Renders the progress page, instrumenting Mongo reads and SQL queries. """ self.setup_course(dataset_index + 1) # Switch to published-only mode to simulate the LMS with self.settings(MODULESTORE_BRANCH='published-only'): # Clear all caches before measuring for cache in settings.CACHES: get_cache(cache).clear() # Refill the metadata inheritance cache modulestore().get_course(self.course.id, depth=None) # We clear the request cache to simulate a new request in the LMS. RequestCache.clear_request_cache() with self.assertNumQueries(queries): with check_mongo_calls(reads): with check_sum_of_calls(XBlock, ['__init__'], xblocks): self.grade_course(self.course)
def test_course_waffle_flag(self, data): """ Tests various combinations of a flag being set in waffle and overridden for a course. """ RequestCache.clear_request_cache() with patch.object(WaffleFlagCourseOverrideModel, 'override_value', return_value=data['course_override']): with override_flag(self.NAMESPACED_FLAG_NAME, active=data['waffle_enabled']): # check twice to test that the result is properly cached self.assertEqual( self.TEST_COURSE_FLAG.is_enabled(self.TEST_COURSE_KEY), data['result']) self.assertEqual( self.TEST_COURSE_FLAG.is_enabled(self.TEST_COURSE_KEY), data['result']) # result is cached, so override check should happen once WaffleFlagCourseOverrideModel.override_value.assert_called_once_with( self.NAMESPACED_FLAG_NAME, self.TEST_COURSE_KEY) # check flag for a second course if data['course_override'] == WaffleFlagCourseOverrideModel.ALL_CHOICES.unset: # When course override wasn't set for the first course, the second course will get the same # cached value from waffle. self.assertEqual( self.TEST_COURSE_FLAG.is_enabled(self.TEST_COURSE_2_KEY), data['waffle_enabled']) else: # When course override was set for the first course, it should not apply to the second # course which should get the default value of False. self.assertEqual( self.TEST_COURSE_FLAG.is_enabled(self.TEST_COURSE_2_KEY), False)
def dump_courses_to_neo4j(self, graph, override_cache=False): """ Parameters ---------- graph: py2neo graph object override_cache: serialize the courses even if they'be been recently serialized Returns two lists: one of the courses that were successfully written to neo4j, and one of courses that were not. ------- """ total_number_of_courses = len(self.course_keys) successful_courses = [] unsuccessful_courses = [] for index, course_key in enumerate(self.course_keys): # first, clear the request cache to prevent memory leaks RequestCache.clear_request_cache() log.info( "Now exporting %s to neo4j: course %d of %d total courses", course_key, index + 1, total_number_of_courses, ) if not (override_cache or self.should_dump_course(course_key)): log.info("skipping dumping %s, since it hasn't changed", course_key) continue nodes, relationships = self.serialize_course(course_key) log.info( "%d nodes and %d relationships in %s", len(nodes), len(relationships), course_key, ) transaction = graph.begin() course_string = six.text_type(course_key) try: # first, delete existing course transaction.run( "MATCH (n:item) WHERE n.course_key='{}' DETACH DELETE n". format(course_string)) # now, re-add it self.add_to_transaction(nodes, transaction) self.add_to_transaction(relationships, transaction) transaction.commit() except Exception: # pylint: disable=broad-except log.exception( "Error trying to dump course %s to neo4j, rolling back", course_string) transaction.rollback() unsuccessful_courses.append(course_string) else: COMMAND_LAST_RUN_CACHE.set(course_key) successful_courses.append(course_string) return successful_courses, unsuccessful_courses
def invalidate_course_mode_cache(sender, **kwargs): # pylint: disable=unused-argument """Invalidate the cache of course modes. """ RequestCache.clear_request_cache(name=CourseMode.CACHE_NAMESPACE)
def invalidate_verified_track_cache(sender, **kwargs): # pylint: disable=unused-argument """Invalidate the cache of VerifiedTrackCohortedCourse. """ RequestCache.clear_request_cache(name=VerifiedTrackCohortedCourse.CACHE_NAMESPACE)