def test_form_typo(self): # Munge course id bad_id = SlashSeparatedCourseKey( u'Broken{}'.format(self.course.id.org), '', self.course.id.run + '_typo') form_data = { 'course_id': bad_id.to_deprecated_string(), 'email_enabled': True } form = CourseAuthorizationAdminForm(data=form_data) # Validation shouldn't work self.assertFalse(form.is_valid()) msg = u'COURSE NOT FOUND' msg += u' --- Entered course id was: "{0}". '.format( bad_id.to_deprecated_string()) msg += 'Please recheck that you have supplied a valid course id.' self.assertEquals(msg, form._errors['course_id'][0]) # pylint: disable=protected-access with self.assertRaisesRegexp( ValueError, "The CourseAuthorization could not be created because the data didn't validate." ): form.save()
class TestJumpTo(TestCase): """ Check the jumpto link for a course. """ def setUp(self): # Use toy course from XML self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') def test_jumpto_invalid_location(self): location = self.course_key.make_usage_key(None, 'NoSuchPlace') # This is fragile, but unfortunately the problem is that within the LMS we # can't use the reverse calls from the CMS jumpto_url = '{0}/{1}/jump_to/{2}'.format('/courses', self.course_key.to_deprecated_string(), location.to_deprecated_string()) response = self.client.get(jumpto_url) self.assertEqual(response.status_code, 404) def test_jumpto_from_chapter(self): location = self.course_key.make_usage_key('chapter', 'Overview') jumpto_url = '{0}/{1}/jump_to/{2}'.format('/courses', self.course_key.to_deprecated_string(), location.to_deprecated_string()) expected = 'courses/edX/toy/2012_Fall/courseware/Overview/' response = self.client.get(jumpto_url) self.assertRedirects(response, expected, status_code=302, target_status_code=302) def test_jumpto_id(self): jumpto_url = '{0}/{1}/jump_to_id/{2}'.format('/courses', self.course_key.to_deprecated_string(), 'Overview') expected = 'courses/edX/toy/2012_Fall/courseware/Overview/' response = self.client.get(jumpto_url) self.assertRedirects(response, expected, status_code=302, target_status_code=302) def test_jumpto_id_invalid_location(self): location = Location('edX', 'toy', 'NoSuchPlace', None, None, None) jumpto_url = '{0}/{1}/jump_to_id/{2}'.format('/courses', self.course_key.to_deprecated_string(), location.to_deprecated_string()) response = self.client.get(jumpto_url) self.assertEqual(response.status_code, 404)
def test_xml_course_authorization(self): course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') # Assert this is an XML course self.assertEqual(modulestore().get_modulestore_type(course_id), XML_MODULESTORE_TYPE) form_data = {'course_id': course_id.to_deprecated_string(), 'email_enabled': True} form = CourseAuthorizationAdminForm(data=form_data) # Validation shouldn't work self.assertFalse(form.is_valid()) msg = u"Course Email feature is only available for courses authored in Studio. " msg += u'"{0}" appears to be an XML backed course.'.format(course_id.to_deprecated_string()) self.assertEquals(msg, form._errors['course_id'][0]) # pylint: disable=protected-access with self.assertRaisesRegexp(ValueError, "The CourseAuthorization could not be created because the data didn't validate."): form.save()
def test_form_typo(self): # Munge course id bad_id = SlashSeparatedCourseKey(u'Broken{}'.format(self.course.id.org), '', self.course.id.run + '_typo') form_data = {'course_id': bad_id.to_deprecated_string(), 'email_enabled': True} form = CourseAuthorizationAdminForm(data=form_data) # Validation shouldn't work self.assertFalse(form.is_valid()) msg = u'COURSE NOT FOUND' msg += u' --- Entered course id was: "{0}". '.format(bad_id.to_deprecated_string()) msg += 'Please recheck that you have supplied a valid course id.' self.assertEquals(msg, form._errors['course_id'][0]) # pylint: disable=protected-access with self.assertRaisesRegexp(ValueError, "The CourseAuthorization could not be created because the data didn't validate."): form.save()
def is_enrolled_by_partial(cls, user, course_id_partial): """ Returns `True` if the user is enrolled in a course that starts with `course_id_partial`. Otherwise, returns False. Can be used to determine whether a student is enrolled in a course whose run name is unknown. `user` is a Django User object. If it hasn't been saved yet (no `.id` attribute), this method will automatically save it before adding an enrollment for it. `course_id_partial` (CourseKey) is missing the run component """ assert isinstance(course_id_partial, SlashSeparatedCourseKey) assert not course_id_partial.run # None or empty string course_key = SlashSeparatedCourseKey(course_id_partial.org, course_id_partial.course, '') querystring = unicode(course_key.to_deprecated_string()) try: return CourseEnrollment.objects.filter( user=user, course_id__startswith=querystring, is_active=1 ).exists() except cls.DoesNotExist: return False
class TestNewInstructorDashboardEmailViewXMLBacked(ModuleStoreTestCase): """ Check for email view on the new instructor dashboard """ def setUp(self): self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') # Create instructor account instructor = AdminFactory.create() self.client.login(username=instructor.username, password="******") # URL for instructor dash self.url = reverse('instructor_dashboard', kwargs={'course_id': self.course_key.to_deprecated_string()}) # URL for email view self.email_link = '<a href="" data-section="send_email">Email</a>' # The flag is enabled, and since REQUIRE_COURSE_EMAIL_AUTH is False, all courses should # be authorized to use email. But the course is not Mongo-backed (should not work) @patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False}) def test_email_flag_true_mongo_false(self): response = self.client.get(self.url) self.assertFalse(self.email_link in response.content) # The flag is disabled and the course is not Mongo-backed (should not work) @patch.dict(settings.FEATURES, {'ENABLE_INSTRUCTOR_EMAIL': False, 'REQUIRE_COURSE_EMAIL_AUTH': False}) def test_email_flag_false_mongo_false(self): response = self.client.get(self.url) self.assertFalse(self.email_link in response.content)
class TestJumpTo(TestCase): """ Check the jumpto link for a course. """ def setUp(self): # Use toy course from XML self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') def test_jumpto_invalid_location(self): location = self.course_key.make_usage_key(None, 'NoSuchPlace') # This is fragile, but unfortunately the problem is that within the LMS we # can't use the reverse calls from the CMS jumpto_url = '{0}/{1}/jump_to/{2}'.format( '/courses', self.course_key.to_deprecated_string(), location.to_deprecated_string()) response = self.client.get(jumpto_url) self.assertEqual(response.status_code, 404) def test_jumpto_from_chapter(self): location = self.course_key.make_usage_key('chapter', 'Overview') jumpto_url = '{0}/{1}/jump_to/{2}'.format( '/courses', self.course_key.to_deprecated_string(), location.to_deprecated_string()) expected = 'courses/edX/toy/2012_Fall/courseware/Overview/' response = self.client.get(jumpto_url) self.assertRedirects(response, expected, status_code=302, target_status_code=302) def test_jumpto_id(self): jumpto_url = '{0}/{1}/jump_to_id/{2}'.format( '/courses', self.course_key.to_deprecated_string(), 'Overview') expected = 'courses/edX/toy/2012_Fall/courseware/Overview/' response = self.client.get(jumpto_url) self.assertRedirects(response, expected, status_code=302, target_status_code=302) def test_jumpto_id_invalid_location(self): location = Location('edX', 'toy', 'NoSuchPlace', None, None, None) jumpto_url = '{0}/{1}/jump_to_id/{2}'.format( '/courses', self.course_key.to_deprecated_string(), location.to_deprecated_string()) response = self.client.get(jumpto_url) self.assertEqual(response.status_code, 404)
class TestInstructorEnrollmentStudentModule(TestCase): """ Test student module manipulations. """ def setUp(self): self.course_key = SlashSeparatedCourseKey('fake', 'course', 'id') def test_reset_student_attempts(self): user = UserFactory() msk = self.course_key.make_usage_key('dummy', 'module') original_state = json.dumps({'attempts': 32, 'otherstuff': 'alsorobots'}) StudentModule.objects.create(student=user, course_id=self.course_key, module_state_key=msk, state=original_state) # lambda to reload the module state from the database module = lambda: StudentModule.objects.get(student=user, course_id=self.course_key, module_state_key=msk) self.assertEqual(json.loads(module().state)['attempts'], 32) reset_student_attempts(self.course_key, user, msk) self.assertEqual(json.loads(module().state)['attempts'], 0) def test_delete_student_attempts(self): user = UserFactory() msk = self.course_key.make_usage_key('dummy', 'module') original_state = json.dumps({'attempts': 32, 'otherstuff': 'alsorobots'}) StudentModule.objects.create(student=user, course_id=self.course_key, module_state_key=msk, state=original_state) self.assertEqual(StudentModule.objects.filter(student=user, course_id=self.course_key, module_state_key=msk).count(), 1) reset_student_attempts(self.course_key, user, msk, delete_module=True) self.assertEqual(StudentModule.objects.filter(student=user, course_id=self.course_key, module_state_key=msk).count(), 0) def test_delete_submission_scores(self): user = UserFactory() problem_location = self.course_key.make_usage_key('dummy', 'module') # Create a student module for the user StudentModule.objects.create( student=user, course_id=self.course_key, module_state_key=problem_location, state=json.dumps({}) ) # Create a submission and score for the student using the submissions API student_item = { 'student_id': anonymous_id_for_user(user, self.course_key), 'course_id': self.course_key.to_deprecated_string(), 'item_id': problem_location.to_deprecated_string(), 'item_type': 'openassessment' } submission = sub_api.create_submission(student_item, 'test answer') sub_api.set_score(submission['uuid'], 1, 2) # Delete student state using the instructor dash reset_student_attempts( self.course_key, user, problem_location, delete_module=True ) # Verify that the student's scores have been reset in the submissions API score = sub_api.get_score(student_item) self.assertIs(score, None)
def test_xml_course_authorization(self): course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') # Assert this is an XML course self.assertEqual(modulestore().get_modulestore_type(course_id), XML_MODULESTORE_TYPE) form_data = { 'course_id': course_id.to_deprecated_string(), 'email_enabled': True } form = CourseAuthorizationAdminForm(data=form_data) # Validation shouldn't work self.assertFalse(form.is_valid()) msg = u"Course Email feature is only available for courses authored in Studio. " msg += u'"{0}" appears to be an XML backed course.'.format( course_id.to_deprecated_string()) self.assertEquals(msg, form._errors['course_id'][0]) # pylint: disable=protected-access with self.assertRaisesRegexp( ValueError, "The CourseAuthorization could not be created because the data didn't validate." ): form.save()
class StaticTabDateTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase): """Test cases for Static Tab Dates.""" def setUp(self): self.course = CourseFactory.create() self.page = ItemFactory.create(category="static_tab", parent_location=self.course.location, data="OOGIE BLOOGIE", display_name="new_tab") self.toy_course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') def test_logged_in(self): self.setup_user() url = reverse('static_tab', args=[self.course.id.to_deprecated_string(), 'new_tab']) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertIn("OOGIE BLOOGIE", resp.content) def test_anonymous_user(self): url = reverse('static_tab', args=[self.course.id.to_deprecated_string(), 'new_tab']) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertIn("OOGIE BLOOGIE", resp.content) @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) def test_get_static_tab_contents(self): course = get_course_by_id(self.toy_course_key) request = get_request_for_user(UserFactory.create()) tab = CourseTabList.get_tab_by_slug(course.tabs, 'resources') # Test render works okay tab_content = get_static_tab_contents(request, course, tab) self.assertIn(self.toy_course_key.to_deprecated_string(), tab_content) self.assertIn('static_tab', tab_content) # Test when render raises an exception with patch('courseware.views.get_module') as mock_module_render: mock_module_render.return_value = MagicMock(render=Mock( side_effect=Exception('Render failed!'))) static_tab = get_static_tab_contents(request, course, tab) self.assertIn("this module is temporarily unavailable", static_tab)
class StaticTabDateTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase): """Test cases for Static Tab Dates.""" def setUp(self): self.course = CourseFactory.create() self.page = ItemFactory.create( category="static_tab", parent_location=self.course.location, data="OOGIE BLOOGIE", display_name="new_tab" ) self.toy_course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') def test_logged_in(self): self.setup_user() url = reverse('static_tab', args=[self.course.id.to_deprecated_string(), 'new_tab']) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertIn("OOGIE BLOOGIE", resp.content) def test_anonymous_user(self): url = reverse('static_tab', args=[self.course.id.to_deprecated_string(), 'new_tab']) resp = self.client.get(url) self.assertEqual(resp.status_code, 200) self.assertIn("OOGIE BLOOGIE", resp.content) @override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) def test_get_static_tab_contents(self): course = get_course_by_id(self.toy_course_key) request = get_request_for_user(UserFactory.create()) tab = CourseTabList.get_tab_by_slug(course.tabs, 'resources') # Test render works okay tab_content = get_static_tab_contents(request, course, tab) self.assertIn(self.toy_course_key.to_deprecated_string(), tab_content) self.assertIn('static_tab', tab_content) # Test when render raises an exception with patch('courseware.views.get_module') as mock_module_render: mock_module_render.return_value = MagicMock( render=Mock(side_effect=Exception('Render failed!')) ) static_tab = get_static_tab_contents(request, course, tab) self.assertIn("this module is temporarily unavailable", static_tab)
class TestNewInstructorDashboardEmailViewXMLBacked(ModuleStoreTestCase): """ Check for email view on the new instructor dashboard """ def setUp(self): self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') # Create instructor account instructor = AdminFactory.create() self.client.login(username=instructor.username, password="******") # URL for instructor dash self.url = reverse( 'instructor_dashboard', kwargs={'course_id': self.course_key.to_deprecated_string()}) # URL for email view self.email_link = '<a href="" data-section="send_email">Email</a>' # The flag is enabled, and since REQUIRE_COURSE_EMAIL_AUTH is False, all courses should # be authorized to use email. But the course is not Mongo-backed (should not work) @patch.dict(settings.FEATURES, { 'ENABLE_INSTRUCTOR_EMAIL': True, 'REQUIRE_COURSE_EMAIL_AUTH': False }) def test_email_flag_true_mongo_false(self): response = self.client.get(self.url) self.assertFalse(self.email_link in response.content) # The flag is disabled and the course is not Mongo-backed (should not work) @patch.dict(settings.FEATURES, { 'ENABLE_INSTRUCTOR_EMAIL': False, 'REQUIRE_COURSE_EMAIL_AUTH': False }) def test_email_flag_false_mongo_false(self): response = self.client.get(self.url) self.assertFalse(self.email_link in response.content)
class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Test the handle_xblock_callback function """ def setUp(self): self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') self.location = self.course_key.make_usage_key('chapter', 'Overview') self.toy_course = modulestore().get_course(self.course_key) self.mock_user = UserFactory() self.mock_user.id = 1 self.request_factory = RequestFactory() # Construct a mock module for the modulestore to return self.mock_module = MagicMock() self.mock_module.id = 1 self.dispatch = 'score_update' # Construct a 'standard' xqueue_callback url self.callback_url = reverse('xqueue_callback', kwargs={ 'course_id': self.course_key.to_deprecated_string(), 'userid': str(self.mock_user.id), 'mod_id': self.mock_module.id, 'dispatch': self.dispatch }) def _mock_file(self, name='file', size=10): """Create a mock file object for testing uploads""" mock_file = MagicMock(size=size, read=lambda: 'x' * size) # We can't use `name` as a kwarg to Mock to set the name attribute # because mock uses `name` to name the mock itself mock_file.name = name return mock_file def test_invalid_location(self): request = self.request_factory.post('dummy_url', data={'position': 1}) request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), 'invalid Location', 'dummy_handler' 'dummy_dispatch') def test_too_many_files(self): request = self.request_factory.post( 'dummy_url', data={ 'file_id': (self._mock_file(), ) * (settings.MAX_FILEUPLOADS_PER_INPUT + 1) }) request.user = self.mock_user self.assertEquals( render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.location.to_deprecated_string()), 'dummy_handler').content, json.dumps({ 'success': 'Submission aborted! Maximum %d files may be submitted at once' % settings.MAX_FILEUPLOADS_PER_INPUT })) def test_too_large_file(self): inputfile = self._mock_file(size=1 + settings.STUDENT_FILEUPLOAD_MAX_SIZE) request = self.request_factory.post('dummy_url', data={'file_id': inputfile}) request.user = self.mock_user self.assertEquals( render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.location.to_deprecated_string()), 'dummy_handler').content, json.dumps({ 'success': 'Submission aborted! Your file "%s" is too large (max size: %d MB)' % (inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000**2)) })) def test_xmodule_dispatch(self): request = self.request_factory.post('dummy_url', data={'position': 1}) request.user = self.mock_user response = render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.location.to_deprecated_string()), 'xmodule_handler', 'goto_position', ) self.assertIsInstance(response, HttpResponse) def test_bad_course_id(self): request = self.request_factory.post('dummy_url') request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, 'bad_course_id', quote_slashes(self.location.to_deprecated_string()), 'xmodule_handler', 'goto_position', ) def test_bad_location(self): request = self.request_factory.post('dummy_url') request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes( self.course_key.make_usage_key( 'chapter', 'bad_location').to_deprecated_string()), 'xmodule_handler', 'goto_position', ) def test_bad_xmodule_dispatch(self): request = self.request_factory.post('dummy_url') request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.location.to_deprecated_string()), 'xmodule_handler', 'bad_dispatch', ) def test_missing_handler(self): request = self.request_factory.post('dummy_url') request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.location.to_deprecated_string()), 'bad_handler', 'bad_dispatch', )
class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Tests of courseware.module_render """ def setUp(self): self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') self.location = self.course_key.make_usage_key('chapter', 'Overview') self.toy_course = modulestore().get_course(self.course_key) self.mock_user = UserFactory() self.mock_user.id = 1 self.request_factory = RequestFactory() # Construct a mock module for the modulestore to return self.mock_module = MagicMock() self.mock_module.id = 1 self.dispatch = 'score_update' # Construct a 'standard' xqueue_callback url self.callback_url = reverse( 'xqueue_callback', kwargs=dict(course_id=self.course_key.to_deprecated_string(), userid=str(self.mock_user.id), mod_id=self.mock_module.id, dispatch=self.dispatch)) def test_get_module(self): self.assertEqual( None, render.get_module('dummyuser', None, 'invalid location', None, None)) def test_module_render_with_jump_to_id(self): """ This test validates that the /jump_to_id/<id> shorthand for intracourse linking works assertIn expected. Note there's a HTML element in the 'toy' course with the url_name 'toyjumpto' which defines this linkage """ mock_request = MagicMock() mock_request.user = self.mock_user course = get_course_with_access(self.mock_user, 'load', self.course_key) field_data_cache = FieldDataCache.cache_for_descriptor_descendents( self.course_key, self.mock_user, course, depth=2) module = render.get_module( self.mock_user, mock_request, self.course_key.make_usage_key('html', 'toyjumpto'), field_data_cache, self.course_key) # get the rendered HTML output which should have the rewritten link html = module.render('student_view').content # See if the url got rewritten to the target link # note if the URL mapping changes then this assertion will break self.assertIn( '/courses/' + self.course_key.to_deprecated_string() + '/jump_to_id/vertical_test', html) def test_xqueue_callback_success(self): """ Test for happy-path xqueue_callback """ fake_key = 'fake key' xqueue_header = json.dumps({'lms_key': fake_key}) data = { 'xqueue_header': xqueue_header, 'xqueue_body': 'hello world', } # Patch getmodule to return our mock module with patch('courseware.module_render.find_target_student_module' ) as get_fake_module: get_fake_module.return_value = self.mock_module # call xqueue_callback with our mocked information request = self.request_factory.post(self.callback_url, data) render.xqueue_callback(request, self.course_key, self.mock_user.id, self.mock_module.id, self.dispatch) # Verify that handle ajax is called with the correct data request.POST['queuekey'] = fake_key self.mock_module.handle_ajax.assert_called_once_with( self.dispatch, request.POST) def test_xqueue_callback_missing_header_info(self): data = { 'xqueue_header': '{}', 'xqueue_body': 'hello world', } with patch('courseware.module_render.find_target_student_module' ) as get_fake_module: get_fake_module.return_value = self.mock_module # Test with missing xqueue data with self.assertRaises(Http404): request = self.request_factory.post(self.callback_url, {}) render.xqueue_callback(request, self.course_key, self.mock_user.id, self.mock_module.id, self.dispatch) # Test with missing xqueue_header with self.assertRaises(Http404): request = self.request_factory.post(self.callback_url, data) render.xqueue_callback(request, self.course_key, self.mock_user.id, self.mock_module.id, self.dispatch) def test_get_score_bucket(self): self.assertEquals(render.get_score_bucket(0, 10), 'incorrect') self.assertEquals(render.get_score_bucket(1, 10), 'partial') self.assertEquals(render.get_score_bucket(10, 10), 'correct') # get_score_bucket calls error cases 'incorrect' self.assertEquals(render.get_score_bucket(11, 10), 'incorrect') self.assertEquals(render.get_score_bucket(-1, 10), 'incorrect') def test_anonymous_handle_xblock_callback(self): dispatch_url = reverse( 'xblock_handler', args=[ self.course_key.to_deprecated_string(), quote_slashes( self.course_key.make_usage_key( 'videosequence', 'Toy_Videos').to_deprecated_string()), 'xmodule_handler', 'goto_position' ]) response = self.client.post(dispatch_url, {'position': 2}) self.assertEquals(403, response.status_code)
class ModuleRenderTestCase(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Tests of courseware.module_render """ def setUp(self): self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') self.location = self.course_key.make_usage_key('chapter', 'Overview') self.toy_course = modulestore().get_course(self.course_key) self.mock_user = UserFactory() self.mock_user.id = 1 self.request_factory = RequestFactory() # Construct a mock module for the modulestore to return self.mock_module = MagicMock() self.mock_module.id = 1 self.dispatch = 'score_update' # Construct a 'standard' xqueue_callback url self.callback_url = reverse('xqueue_callback', kwargs=dict(course_id=self.course_key.to_deprecated_string(), userid=str(self.mock_user.id), mod_id=self.mock_module.id, dispatch=self.dispatch)) def test_get_module(self): self.assertEqual( None, render.get_module('dummyuser', None, 'invalid location', None, None) ) def test_module_render_with_jump_to_id(self): """ This test validates that the /jump_to_id/<id> shorthand for intracourse linking works assertIn expected. Note there's a HTML element in the 'toy' course with the url_name 'toyjumpto' which defines this linkage """ mock_request = MagicMock() mock_request.user = self.mock_user course = get_course_with_access(self.mock_user, 'load', self.course_key) field_data_cache = FieldDataCache.cache_for_descriptor_descendents( self.course_key, self.mock_user, course, depth=2) module = render.get_module( self.mock_user, mock_request, self.course_key.make_usage_key('html', 'toyjumpto'), field_data_cache, self.course_key ) # get the rendered HTML output which should have the rewritten link html = module.render('student_view').content # See if the url got rewritten to the target link # note if the URL mapping changes then this assertion will break self.assertIn('/courses/' + self.course_key.to_deprecated_string() + '/jump_to_id/vertical_test', html) def test_xqueue_callback_success(self): """ Test for happy-path xqueue_callback """ fake_key = 'fake key' xqueue_header = json.dumps({'lms_key': fake_key}) data = { 'xqueue_header': xqueue_header, 'xqueue_body': 'hello world', } # Patch getmodule to return our mock module with patch('courseware.module_render.find_target_student_module') as get_fake_module: get_fake_module.return_value = self.mock_module # call xqueue_callback with our mocked information request = self.request_factory.post(self.callback_url, data) render.xqueue_callback(request, self.course_key, self.mock_user.id, self.mock_module.id, self.dispatch) # Verify that handle ajax is called with the correct data request.POST['queuekey'] = fake_key self.mock_module.handle_ajax.assert_called_once_with(self.dispatch, request.POST) def test_xqueue_callback_missing_header_info(self): data = { 'xqueue_header': '{}', 'xqueue_body': 'hello world', } with patch('courseware.module_render.find_target_student_module') as get_fake_module: get_fake_module.return_value = self.mock_module # Test with missing xqueue data with self.assertRaises(Http404): request = self.request_factory.post(self.callback_url, {}) render.xqueue_callback(request, self.course_key, self.mock_user.id, self.mock_module.id, self.dispatch) # Test with missing xqueue_header with self.assertRaises(Http404): request = self.request_factory.post(self.callback_url, data) render.xqueue_callback(request, self.course_key, self.mock_user.id, self.mock_module.id, self.dispatch) def test_get_score_bucket(self): self.assertEquals(render.get_score_bucket(0, 10), 'incorrect') self.assertEquals(render.get_score_bucket(1, 10), 'partial') self.assertEquals(render.get_score_bucket(10, 10), 'correct') # get_score_bucket calls error cases 'incorrect' self.assertEquals(render.get_score_bucket(11, 10), 'incorrect') self.assertEquals(render.get_score_bucket(-1, 10), 'incorrect') def test_anonymous_handle_xblock_callback(self): dispatch_url = reverse( 'xblock_handler', args=[ self.course_key.to_deprecated_string(), quote_slashes(self.course_key.make_usage_key('videosequence', 'Toy_Videos').to_deprecated_string()), 'xmodule_handler', 'goto_position' ] ) response = self.client.post(dispatch_url, {'position': 2}) self.assertEquals(403, response.status_code)
class TestHandleXBlockCallback(ModuleStoreTestCase, LoginEnrollmentTestCase): """ Test the handle_xblock_callback function """ def setUp(self): self.course_key = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall') self.location = self.course_key.make_usage_key('chapter', 'Overview') self.toy_course = modulestore().get_course(self.course_key) self.mock_user = UserFactory() self.mock_user.id = 1 self.request_factory = RequestFactory() # Construct a mock module for the modulestore to return self.mock_module = MagicMock() self.mock_module.id = 1 self.dispatch = 'score_update' # Construct a 'standard' xqueue_callback url self.callback_url = reverse( 'xqueue_callback', kwargs={ 'course_id': self.course_key.to_deprecated_string(), 'userid': str(self.mock_user.id), 'mod_id': self.mock_module.id, 'dispatch': self.dispatch } ) def _mock_file(self, name='file', size=10): """Create a mock file object for testing uploads""" mock_file = MagicMock( size=size, read=lambda: 'x' * size ) # We can't use `name` as a kwarg to Mock to set the name attribute # because mock uses `name` to name the mock itself mock_file.name = name return mock_file def test_invalid_location(self): request = self.request_factory.post('dummy_url', data={'position': 1}) request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), 'invalid Location', 'dummy_handler' 'dummy_dispatch' ) def test_too_many_files(self): request = self.request_factory.post( 'dummy_url', data={'file_id': (self._mock_file(), ) * (settings.MAX_FILEUPLOADS_PER_INPUT + 1)} ) request.user = self.mock_user self.assertEquals( render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.location.to_deprecated_string()), 'dummy_handler' ).content, json.dumps({ 'success': 'Submission aborted! Maximum %d files may be submitted at once' % settings.MAX_FILEUPLOADS_PER_INPUT }) ) def test_too_large_file(self): inputfile = self._mock_file(size=1 + settings.STUDENT_FILEUPLOAD_MAX_SIZE) request = self.request_factory.post( 'dummy_url', data={'file_id': inputfile} ) request.user = self.mock_user self.assertEquals( render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.location.to_deprecated_string()), 'dummy_handler' ).content, json.dumps({ 'success': 'Submission aborted! Your file "%s" is too large (max size: %d MB)' % (inputfile.name, settings.STUDENT_FILEUPLOAD_MAX_SIZE / (1000 ** 2)) }) ) def test_xmodule_dispatch(self): request = self.request_factory.post('dummy_url', data={'position': 1}) request.user = self.mock_user response = render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.location.to_deprecated_string()), 'xmodule_handler', 'goto_position', ) self.assertIsInstance(response, HttpResponse) def test_bad_course_id(self): request = self.request_factory.post('dummy_url') request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, 'bad_course_id', quote_slashes(self.location.to_deprecated_string()), 'xmodule_handler', 'goto_position', ) def test_bad_location(self): request = self.request_factory.post('dummy_url') request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.course_key.make_usage_key('chapter', 'bad_location').to_deprecated_string()), 'xmodule_handler', 'goto_position', ) def test_bad_xmodule_dispatch(self): request = self.request_factory.post('dummy_url') request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.location.to_deprecated_string()), 'xmodule_handler', 'bad_dispatch', ) def test_missing_handler(self): request = self.request_factory.post('dummy_url') request.user = self.mock_user with self.assertRaises(Http404): render.handle_xblock_callback( request, self.course_key.to_deprecated_string(), quote_slashes(self.location.to_deprecated_string()), 'bad_handler', 'bad_dispatch', )
class ApiTest(TestCase): def setUp(self): self.client = Client() # Mocks api.api_enabled = self.mock_api_enabled(True) # Create two accounts self.password = '******' self.student = User.objects.create_user('student', '*****@*****.**', self.password) self.student2 = User.objects.create_user('student2', '*****@*****.**', self.password) self.instructor = User.objects.create_user('instructor', '*****@*****.**', self.password) self.course_key = SlashSeparatedCourseKey('HarvardX', 'CB22x', 'The_Ancient_Greek_Hero') self.note = { 'user': self.student, 'course_id': self.course_key, 'uri': '/', 'text': 'foo', 'quote': 'bar', 'range_start': 0, 'range_start_offset': 0, 'range_end': 100, 'range_end_offset': 0, 'tags': 'a,b,c' } # Make sure no note with this ID ever exists for testing purposes self.NOTE_ID_DOES_NOT_EXIST = 99999 def mock_api_enabled(self, is_enabled): return (lambda request, course_id: is_enabled) def login(self, as_student=None): username = None password = self.password if as_student is None: username = self.student.username else: username = as_student.username self.client.login(username=username, password=password) def url(self, name, args={}): args.update({'course_id': self.course_key.to_deprecated_string()}) return reverse(name, kwargs=args) def create_notes(self, num_notes, create=True): notes = [] for n in range(num_notes): note = models.Note(**self.note) if create: note.save() notes.append(note) return notes def test_root(self): self.login() resp = self.client.get(self.url('notes_api_root')) self.assertEqual(resp.status_code, 200) self.assertNotEqual(resp.content, '') content = json.loads(resp.content) self.assertEqual(set(('name', 'version')), set(content.keys())) self.assertIsInstance(content['version'], int) self.assertEqual(content['name'], 'Notes API') def test_index_empty(self): self.login() resp = self.client.get(self.url('notes_api_notes')) self.assertEqual(resp.status_code, 200) self.assertNotEqual(resp.content, '') content = json.loads(resp.content) self.assertEqual(len(content), 0) def test_index_with_notes(self): num_notes = 3 self.login() self.create_notes(num_notes) resp = self.client.get(self.url('notes_api_notes')) self.assertEqual(resp.status_code, 200) self.assertNotEqual(resp.content, '') content = json.loads(resp.content) self.assertIsInstance(content, list) self.assertEqual(len(content), num_notes) def test_index_max_notes(self): self.login() MAX_LIMIT = api.API_SETTINGS.get('MAX_NOTE_LIMIT') num_notes = MAX_LIMIT + 1 self.create_notes(num_notes) resp = self.client.get(self.url('notes_api_notes')) self.assertEqual(resp.status_code, 200) self.assertNotEqual(resp.content, '') content = json.loads(resp.content) self.assertIsInstance(content, list) self.assertEqual(len(content), MAX_LIMIT) def test_create_note(self): self.login() notes = self.create_notes(1) self.assertEqual(len(notes), 1) note_dict = notes[0].as_dict() excluded_fields = ['id', 'user_id', 'created', 'updated'] note = dict([(k, v) for k, v in note_dict.items() if k not in excluded_fields]) resp = self.client.post(self.url('notes_api_notes'), json.dumps(note), content_type='application/json', HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(resp.status_code, 303) self.assertEqual(len(resp.content), 0) def test_create_empty_notes(self): self.login() for empty_test in [None, [], '']: resp = self.client.post(self.url('notes_api_notes'), json.dumps(empty_test), content_type='application/json', HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(resp.status_code, 400) def test_create_note_missing_ranges(self): self.login() notes = self.create_notes(1) self.assertEqual(len(notes), 1) note_dict = notes[0].as_dict() excluded_fields = ['id', 'user_id', 'created', 'updated'] + ['ranges'] note = dict([(k, v) for k, v in note_dict.items() if k not in excluded_fields]) resp = self.client.post(self.url('notes_api_notes'), json.dumps(note), content_type='application/json', HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(resp.status_code, 400) def test_read_note(self): self.login() notes = self.create_notes(3) self.assertEqual(len(notes), 3) for note in notes: resp = self.client.get( self.url('notes_api_note', {'note_id': note.pk})) self.assertEqual(resp.status_code, 200) self.assertNotEqual(resp.content, '') content = json.loads(resp.content) self.assertEqual(content['id'], note.pk) self.assertEqual(content['user_id'], note.user_id) def test_note_doesnt_exist_to_read(self): self.login() resp = self.client.get( self.url('notes_api_note', {'note_id': self.NOTE_ID_DOES_NOT_EXIST})) self.assertEqual(resp.status_code, 404) self.assertEqual(resp.content, '') def test_student_doesnt_have_permission_to_read_note(self): notes = self.create_notes(1) self.assertEqual(len(notes), 1) note = notes[0] # set the student id to a different student (not the one that created the notes) self.login(as_student=self.student2) resp = self.client.get(self.url('notes_api_note', {'note_id': note.pk})) self.assertEqual(resp.status_code, 403) self.assertEqual(resp.content, '') def test_delete_note(self): self.login() notes = self.create_notes(1) self.assertEqual(len(notes), 1) note = notes[0] resp = self.client.delete( self.url('notes_api_note', {'note_id': note.pk})) self.assertEqual(resp.status_code, 204) self.assertEqual(resp.content, '') with self.assertRaises(models.Note.DoesNotExist): models.Note.objects.get(pk=note.pk) def test_note_does_not_exist_to_delete(self): self.login() resp = self.client.delete( self.url('notes_api_note', {'note_id': self.NOTE_ID_DOES_NOT_EXIST})) self.assertEqual(resp.status_code, 404) self.assertEqual(resp.content, '') def test_student_doesnt_have_permission_to_delete_note(self): notes = self.create_notes(1) self.assertEqual(len(notes), 1) note = notes[0] self.login(as_student=self.student2) resp = self.client.delete( self.url('notes_api_note', {'note_id': note.pk})) self.assertEqual(resp.status_code, 403) self.assertEqual(resp.content, '') try: models.Note.objects.get(pk=note.pk) except models.Note.DoesNotExist: self.fail( 'note should exist and not be deleted because the student does not have permission to do so' ) def test_update_note(self): notes = self.create_notes(1) note = notes[0] updated_dict = note.as_dict() updated_dict.update({ 'text': 'itchy and scratchy', 'tags': ['simpsons', 'cartoons', 'animation'] }) self.login() resp = self.client.put(self.url('notes_api_note', {'note_id': note.pk}), json.dumps(updated_dict), content_type='application/json', HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(resp.status_code, 303) self.assertEqual(resp.content, '') actual = models.Note.objects.get(pk=note.pk) actual_dict = actual.as_dict() for field in ['text', 'tags']: self.assertEqual(actual_dict[field], updated_dict[field]) def test_search_note_params(self): self.login() total = 3 notes = self.create_notes(total) invalid_uri = ''.join([note.uri for note in notes]) tests = [{ 'limit': 0, 'offset': 0, 'expected_rows': total }, { 'limit': 0, 'offset': 2, 'expected_rows': total - 2 }, { 'limit': 0, 'offset': total, 'expected_rows': 0 }, { 'limit': 1, 'offset': 0, 'expected_rows': 1 }, { 'limit': 2, 'offset': 0, 'expected_rows': 2 }, { 'limit': total, 'offset': 2, 'expected_rows': 1 }, { 'limit': total, 'offset': total, 'expected_rows': 0 }, { 'limit': total + 1, 'offset': total + 1, 'expected_rows': 0 }, { 'limit': total + 1, 'offset': 0, 'expected_rows': total }, { 'limit': 0, 'offset': 0, 'uri': invalid_uri, 'expected_rows': 0, 'expected_total': 0 }] for test in tests: params = dict([(k, str(test[k])) for k in ('limit', 'offset', 'uri') if k in test]) resp = self.client.get(self.url('notes_api_search'), params, content_type='application/json', HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(resp.status_code, 200) self.assertNotEqual(resp.content, '') content = json.loads(resp.content) for expected_key in ('total', 'rows'): self.assertTrue(expected_key in content) if 'expected_total' in test: self.assertEqual(content['total'], test['expected_total']) else: self.assertEqual(content['total'], total) self.assertEqual(len(content['rows']), test['expected_rows']) for row in content['rows']: self.assertTrue('id' in row)
class ApiTest(TestCase): def setUp(self): self.client = Client() # Mocks api.api_enabled = self.mock_api_enabled(True) # Create two accounts self.password = '******' self.student = User.objects.create_user('student', '*****@*****.**', self.password) self.student2 = User.objects.create_user('student2', '*****@*****.**', self.password) self.instructor = User.objects.create_user('instructor', '*****@*****.**', self.password) self.course_key = SlashSeparatedCourseKey('HarvardX', 'CB22x', 'The_Ancient_Greek_Hero') self.note = { 'user': self.student, 'course_id': self.course_key, 'uri': '/', 'text': 'foo', 'quote': 'bar', 'range_start': 0, 'range_start_offset': 0, 'range_end': 100, 'range_end_offset': 0, 'tags': 'a,b,c' } # Make sure no note with this ID ever exists for testing purposes self.NOTE_ID_DOES_NOT_EXIST = 99999 def mock_api_enabled(self, is_enabled): return (lambda request, course_id: is_enabled) def login(self, as_student=None): username = None password = self.password if as_student is None: username = self.student.username else: username = as_student.username self.client.login(username=username, password=password) def url(self, name, args={}): args.update({'course_id': self.course_key.to_deprecated_string()}) return reverse(name, kwargs=args) def create_notes(self, num_notes, create=True): notes = [] for n in range(num_notes): note = models.Note(**self.note) if create: note.save() notes.append(note) return notes def test_root(self): self.login() resp = self.client.get(self.url('notes_api_root')) self.assertEqual(resp.status_code, 200) self.assertNotEqual(resp.content, '') content = json.loads(resp.content) self.assertEqual(set(('name', 'version')), set(content.keys())) self.assertIsInstance(content['version'], int) self.assertEqual(content['name'], 'Notes API') def test_index_empty(self): self.login() resp = self.client.get(self.url('notes_api_notes')) self.assertEqual(resp.status_code, 200) self.assertNotEqual(resp.content, '') content = json.loads(resp.content) self.assertEqual(len(content), 0) def test_index_with_notes(self): num_notes = 3 self.login() self.create_notes(num_notes) resp = self.client.get(self.url('notes_api_notes')) self.assertEqual(resp.status_code, 200) self.assertNotEqual(resp.content, '') content = json.loads(resp.content) self.assertIsInstance(content, list) self.assertEqual(len(content), num_notes) def test_index_max_notes(self): self.login() MAX_LIMIT = api.API_SETTINGS.get('MAX_NOTE_LIMIT') num_notes = MAX_LIMIT + 1 self.create_notes(num_notes) resp = self.client.get(self.url('notes_api_notes')) self.assertEqual(resp.status_code, 200) self.assertNotEqual(resp.content, '') content = json.loads(resp.content) self.assertIsInstance(content, list) self.assertEqual(len(content), MAX_LIMIT) def test_create_note(self): self.login() notes = self.create_notes(1) self.assertEqual(len(notes), 1) note_dict = notes[0].as_dict() excluded_fields = ['id', 'user_id', 'created', 'updated'] note = dict([(k, v) for k, v in note_dict.items() if k not in excluded_fields]) resp = self.client.post(self.url('notes_api_notes'), json.dumps(note), content_type='application/json', HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(resp.status_code, 303) self.assertEqual(len(resp.content), 0) def test_create_empty_notes(self): self.login() for empty_test in [None, [], '']: resp = self.client.post(self.url('notes_api_notes'), json.dumps(empty_test), content_type='application/json', HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(resp.status_code, 400) def test_create_note_missing_ranges(self): self.login() notes = self.create_notes(1) self.assertEqual(len(notes), 1) note_dict = notes[0].as_dict() excluded_fields = ['id', 'user_id', 'created', 'updated'] + ['ranges'] note = dict([(k, v) for k, v in note_dict.items() if k not in excluded_fields]) resp = self.client.post(self.url('notes_api_notes'), json.dumps(note), content_type='application/json', HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(resp.status_code, 400) def test_read_note(self): self.login() notes = self.create_notes(3) self.assertEqual(len(notes), 3) for note in notes: resp = self.client.get(self.url('notes_api_note', {'note_id': note.pk})) self.assertEqual(resp.status_code, 200) self.assertNotEqual(resp.content, '') content = json.loads(resp.content) self.assertEqual(content['id'], note.pk) self.assertEqual(content['user_id'], note.user_id) def test_note_doesnt_exist_to_read(self): self.login() resp = self.client.get(self.url('notes_api_note', { 'note_id': self.NOTE_ID_DOES_NOT_EXIST })) self.assertEqual(resp.status_code, 404) self.assertEqual(resp.content, '') def test_student_doesnt_have_permission_to_read_note(self): notes = self.create_notes(1) self.assertEqual(len(notes), 1) note = notes[0] # set the student id to a different student (not the one that created the notes) self.login(as_student=self.student2) resp = self.client.get(self.url('notes_api_note', {'note_id': note.pk})) self.assertEqual(resp.status_code, 403) self.assertEqual(resp.content, '') def test_delete_note(self): self.login() notes = self.create_notes(1) self.assertEqual(len(notes), 1) note = notes[0] resp = self.client.delete(self.url('notes_api_note', { 'note_id': note.pk })) self.assertEqual(resp.status_code, 204) self.assertEqual(resp.content, '') with self.assertRaises(models.Note.DoesNotExist): models.Note.objects.get(pk=note.pk) def test_note_does_not_exist_to_delete(self): self.login() resp = self.client.delete(self.url('notes_api_note', { 'note_id': self.NOTE_ID_DOES_NOT_EXIST })) self.assertEqual(resp.status_code, 404) self.assertEqual(resp.content, '') def test_student_doesnt_have_permission_to_delete_note(self): notes = self.create_notes(1) self.assertEqual(len(notes), 1) note = notes[0] self.login(as_student=self.student2) resp = self.client.delete(self.url('notes_api_note', { 'note_id': note.pk })) self.assertEqual(resp.status_code, 403) self.assertEqual(resp.content, '') try: models.Note.objects.get(pk=note.pk) except models.Note.DoesNotExist: self.fail('note should exist and not be deleted because the student does not have permission to do so') def test_update_note(self): notes = self.create_notes(1) note = notes[0] updated_dict = note.as_dict() updated_dict.update({ 'text': 'itchy and scratchy', 'tags': ['simpsons', 'cartoons', 'animation'] }) self.login() resp = self.client.put(self.url('notes_api_note', {'note_id': note.pk}), json.dumps(updated_dict), content_type='application/json', HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(resp.status_code, 303) self.assertEqual(resp.content, '') actual = models.Note.objects.get(pk=note.pk) actual_dict = actual.as_dict() for field in ['text', 'tags']: self.assertEqual(actual_dict[field], updated_dict[field]) def test_search_note_params(self): self.login() total = 3 notes = self.create_notes(total) invalid_uri = ''.join([note.uri for note in notes]) tests = [{'limit': 0, 'offset': 0, 'expected_rows': total}, {'limit': 0, 'offset': 2, 'expected_rows': total - 2}, {'limit': 0, 'offset': total, 'expected_rows': 0}, {'limit': 1, 'offset': 0, 'expected_rows': 1}, {'limit': 2, 'offset': 0, 'expected_rows': 2}, {'limit': total, 'offset': 2, 'expected_rows': 1}, {'limit': total, 'offset': total, 'expected_rows': 0}, {'limit': total + 1, 'offset': total + 1, 'expected_rows': 0}, {'limit': total + 1, 'offset': 0, 'expected_rows': total}, {'limit': 0, 'offset': 0, 'uri': invalid_uri, 'expected_rows': 0, 'expected_total': 0}] for test in tests: params = dict([(k, str(test[k])) for k in ('limit', 'offset', 'uri') if k in test]) resp = self.client.get(self.url('notes_api_search'), params, content_type='application/json', HTTP_X_REQUESTED_WITH='XMLHttpRequest') self.assertEqual(resp.status_code, 200) self.assertNotEqual(resp.content, '') content = json.loads(resp.content) for expected_key in ('total', 'rows'): self.assertTrue(expected_key in content) if 'expected_total' in test: self.assertEqual(content['total'], test['expected_total']) else: self.assertEqual(content['total'], total) self.assertEqual(len(content['rows']), test['expected_rows']) for row in content['rows']: self.assertTrue('id' in row)
class TestInstructorEnrollmentStudentModule(TestCase): """ Test student module manipulations. """ def setUp(self): self.course_key = SlashSeparatedCourseKey('fake', 'course', 'id') def test_reset_student_attempts(self): user = UserFactory() msk = self.course_key.make_usage_key('dummy', 'module') original_state = json.dumps({ 'attempts': 32, 'otherstuff': 'alsorobots' }) StudentModule.objects.create(student=user, course_id=self.course_key, module_state_key=msk, state=original_state) # lambda to reload the module state from the database module = lambda: StudentModule.objects.get( student=user, course_id=self.course_key, module_state_key=msk) self.assertEqual(json.loads(module().state)['attempts'], 32) reset_student_attempts(self.course_key, user, msk) self.assertEqual(json.loads(module().state)['attempts'], 0) def test_delete_student_attempts(self): user = UserFactory() msk = self.course_key.make_usage_key('dummy', 'module') original_state = json.dumps({ 'attempts': 32, 'otherstuff': 'alsorobots' }) StudentModule.objects.create(student=user, course_id=self.course_key, module_state_key=msk, state=original_state) self.assertEqual( StudentModule.objects.filter(student=user, course_id=self.course_key, module_state_key=msk).count(), 1) reset_student_attempts(self.course_key, user, msk, delete_module=True) self.assertEqual( StudentModule.objects.filter(student=user, course_id=self.course_key, module_state_key=msk).count(), 0) def test_delete_submission_scores(self): user = UserFactory() problem_location = self.course_key.make_usage_key('dummy', 'module') # Create a student module for the user StudentModule.objects.create(student=user, course_id=self.course_key, module_state_key=problem_location, state=json.dumps({})) # Create a submission and score for the student using the submissions API student_item = { 'student_id': anonymous_id_for_user(user, self.course_key), 'course_id': self.course_key.to_deprecated_string(), 'item_id': problem_location.to_deprecated_string(), 'item_type': 'openassessment' } submission = sub_api.create_submission(student_item, 'test answer') sub_api.set_score(submission['uuid'], 1, 2) # Delete student state using the instructor dash reset_student_attempts(self.course_key, user, problem_location, delete_module=True) # Verify that the student's scores have been reset in the submissions API score = sub_api.get_score(student_item) self.assertIs(score, None)
class TestStaffGradingService(ModuleStoreTestCase, LoginEnrollmentTestCase): ''' Check that staff grading service proxy works. Basically just checking the access control and error handling logic -- all the actual work is on the backend. ''' def setUp(self): self.student = '*****@*****.**' self.instructor = '*****@*****.**' self.password = '******' self.create_account('u1', self.student, self.password) self.create_account('u2', self.instructor, self.password) self.activate_user(self.student) self.activate_user(self.instructor) self.course_id = SlashSeparatedCourseKey("edX", "toy", "2012_Fall") self.location_string = self.course_id.make_usage_key('html', 'TestLocation').to_deprecated_string() self.toy = modulestore().get_course(self.course_id) make_instructor(self.toy, self.instructor) self.mock_service = staff_grading_service.staff_grading_service() self.logout() def test_access(self): """ Make sure only staff have access. """ self.login(self.student, self.password) # both get and post should return 404 for view_name in ('staff_grading_get_next', 'staff_grading_save_grade'): url = reverse(view_name, kwargs={'course_id': self.course_id.to_deprecated_string()}) check_for_get_code(self, 404, url) check_for_post_code(self, 404, url) def test_get_next(self): self.login(self.instructor, self.password) url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id.to_deprecated_string()}) data = {'location': self.location_string} response = check_for_post_code(self, 200, url, data) content = json.loads(response.content) self.assertTrue(content['success']) self.assertEquals(content['submission_id'], self.mock_service.cnt) self.assertIsNotNone(content['submission']) self.assertIsNotNone(content['num_graded']) self.assertIsNotNone(content['min_for_ml']) self.assertIsNotNone(content['num_pending']) self.assertIsNotNone(content['prompt']) self.assertIsNotNone(content['ml_error_info']) self.assertIsNotNone(content['max_score']) self.assertIsNotNone(content['rubric']) def save_grade_base(self, skip=False): self.login(self.instructor, self.password) url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id.to_deprecated_string()}) data = {'score': '12', 'feedback': 'great!', 'submission_id': '123', 'location': self.location_string, 'submission_flagged': "true", 'rubric_scores[]': ['1', '2']} if skip: data.update({'skipped': True}) response = check_for_post_code(self, 200, url, data) content = json.loads(response.content) self.assertTrue(content['success'], str(content)) self.assertEquals(content['submission_id'], self.mock_service.cnt) def test_save_grade(self): self.save_grade_base(skip=False) def test_save_grade_skip(self): self.save_grade_base(skip=True) def test_get_problem_list(self): self.login(self.instructor, self.password) url = reverse('staff_grading_get_problem_list', kwargs={'course_id': self.course_id.to_deprecated_string()}) data = {} response = check_for_post_code(self, 200, url, data) content = json.loads(response.content) self.assertTrue(content['success']) self.assertEqual(content['problem_list'], []) @patch('open_ended_grading.staff_grading_service._service', EmptyStaffGradingService()) def test_get_problem_list_missing(self): """ Test to see if a staff grading response missing a problem list is given the appropriate error. Mock the staff grading service to enable the key to be missing. """ # Get a valid user object. instructor = User.objects.get(email=self.instructor) # Mock a request object. request = Mock( user=instructor, ) # Get the response and load its content. response = json.loads(staff_grading_service.get_problem_list(request, self.course_id.to_deprecated_string()).content) # A valid response will have an "error" key. self.assertTrue('error' in response) # Check that the error text is correct. self.assertIn("Cannot find", response['error']) def test_save_grade_with_long_feedback(self): """ Test if feedback is too long save_grade() should return error message. """ self.login(self.instructor, self.password) url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id.to_deprecated_string()}) data = { 'score': '12', 'feedback': '', 'submission_id': '123', 'location': self.location_string, 'submission_flagged': "false", 'rubric_scores[]': ['1', '2'] } feedback_fragment = "This is very long feedback." data["feedback"] = feedback_fragment * ( (staff_grading_service.MAX_ALLOWED_FEEDBACK_LENGTH / len(feedback_fragment) + 1) ) response = check_for_post_code(self, 200, url, data) content = json.loads(response.content) # Should not succeed. self.assertEquals(content['success'], False) self.assertEquals( content['error'], "Feedback is too long, Max length is {0} characters.".format( staff_grading_service.MAX_ALLOWED_FEEDBACK_LENGTH ) )
class TestMidCourseReverifyView(TestCase): """ Tests for the midcourse reverification views """ def setUp(self): self.user = UserFactory.create(username="******", password="******") self.client.login(username="******", password="******") self.course_key = SlashSeparatedCourseKey("Robot", "999", "Test_Course") CourseFactory.create(org='Robot', number='999', display_name='Test Course') patcher = patch('student.models.tracker') self.mock_tracker = patcher.start() self.addCleanup(patcher.stop) @patch('verify_student.views.render_to_response', render_mock) def test_midcourse_reverify_get(self): url = reverse('verify_student_midcourse_reverify', kwargs={"course_id": self.course_key.to_deprecated_string()}) response = self.client.get(url) # Check that user entering the reverify flow was logged self.mock_tracker.emit.assert_called_once_with( # pylint: disable=maybe-no-member 'edx.course.enrollment.reverify.started', { 'user_id': self.user.id, 'course_id': self.course_key.to_deprecated_string(), 'mode': "verified", } ) self.mock_tracker.emit.reset_mock() # pylint: disable=maybe-no-member self.assertEquals(response.status_code, 200) ((_template, context), _kwargs) = render_mock.call_args self.assertFalse(context['error']) @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) def test_midcourse_reverify_post_success(self): window = MidcourseReverificationWindowFactory(course_id=self.course_key) url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': self.course_key.to_deprecated_string()}) response = self.client.post(url, {'face_image': ','}) # Check that submission event was logged self.mock_tracker.emit.assert_called_once_with( # pylint: disable=maybe-no-member 'edx.course.enrollment.reverify.submitted', { 'user_id': self.user.id, 'course_id': self.course_key.to_deprecated_string(), 'mode': "verified", } ) self.mock_tracker.emit.reset_mock() # pylint: disable=maybe-no-member self.assertEquals(response.status_code, 302) try: verification_attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user, window=window) self.assertIsNotNone(verification_attempt) except ObjectDoesNotExist: self.fail('No verification object generated') @patch.dict(settings.FEATURES, {'AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING': True}) def test_midcourse_reverify_post_failure_expired_window(self): window = MidcourseReverificationWindowFactory( course_id=self.course_key, start_date=datetime.now(pytz.UTC) - timedelta(days=100), end_date=datetime.now(pytz.UTC) - timedelta(days=50), ) url = reverse('verify_student_midcourse_reverify', kwargs={'course_id': self.course_key.to_deprecated_string()}) response = self.client.post(url, {'face_image': ','}) self.assertEquals(response.status_code, 302) with self.assertRaises(ObjectDoesNotExist): SoftwareSecurePhotoVerification.objects.get(user=self.user, window=window) @patch('verify_student.views.render_to_response', render_mock) def test_midcourse_reverify_dash(self): url = reverse('verify_student_midcourse_reverify_dash') response = self.client.get(url) # not enrolled in any courses self.assertEquals(response.status_code, 200) enrollment = CourseEnrollment.get_or_create_enrollment(self.user, self.course_key) enrollment.update_enrollment(mode="verified", is_active=True) MidcourseReverificationWindowFactory(course_id=self.course_key) response = self.client.get(url) # enrolled in a verified course, and the window is open self.assertEquals(response.status_code, 200)