""" Tests for the views """ from datetime import datetime from urllib import urlencode import ddt from django.urls import reverse from edx_oauth2_provider.tests.factories import AccessTokenFactory, ClientFactory from mock import patch from opaque_keys import InvalidKeyError from pytz import UTC from rest_framework import status from rest_framework.test import APITestCase from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory from lms.djangoapps.courseware.tests.factories import GlobalStaffFactory, StaffFactory from lms.djangoapps.grades.tests.utils import mock_passing_grade from student.tests.factories import CourseEnrollmentFactory, UserFactory from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.tests.django_utils import TEST_DATA_SPLIT_MODULESTORE, SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory, check_mongo_calls @ddt.ddt class CurrentGradeViewTest(SharedModuleStoreTestCase, APITestCase): """ Tests for the Current Grade View The following tests assume that the grading policy is the edX default one: { "GRADER": [ { "drop_count": 2, "min_count": 12, "short_label": "HW", "type": "Homework", "weight": 0.15 }, { "drop_count": 2, "min_count": 12, "type": "Lab", "weight": 0.15 }, { "drop_count": 0, "min_count": 1, "short_label": "Midterm", "type": "Midterm Exam", "weight": 0.3 }, { "drop_count": 0, "min_count": 1, "short_label": "Final", "type": "Final Exam", "weight": 0.4 } ], "GRADE_CUTOFFS": { "Pass": 0.5 } } """ shard = 4 MODULESTORE = TEST_DATA_SPLIT_MODULESTORE @classmethod def setUpClass(cls): super(CurrentGradeViewTest, cls).setUpClass() cls.course = CourseFactory.create(display_name='test course', run="Testing_course") with cls.store.bulk_operations(cls.course.id): chapter = ItemFactory.create( category='chapter', parent_location=cls.course.location, display_name="Chapter 1", ) # create a problem for each type and minimum count needed by the grading policy # A section is not considered if the student answers less than "min_count" problems for grading_type, min_count in (("Homework", 12), ("Lab", 12), ("Midterm Exam", 1), ("Final Exam", 1)): for num in xrange(min_count): section = ItemFactory.create( category='sequential', parent_location=chapter.location, due=datetime(2013, 9, 18, 11, 30, 00, tzinfo=UTC), display_name='Sequential {} {}'.format(grading_type, num), format=grading_type, graded=True, ) vertical = ItemFactory.create( category='vertical', parent_location=section.location, display_name='Vertical {} {}'.format(grading_type, num), ) ItemFactory.create( category='problem', parent_location=vertical.location, display_name='Problem {} {}'.format(grading_type, num), ) cls.course_key = cls.course.id cls.password = 'test' cls.student = UserFactory(username='dummy', password=cls.password) cls.other_student = UserFactory(username='foo', password=cls.password) cls.other_user = UserFactory(username='bar', password=cls.password) cls.staff = StaffFactory(course_key=cls.course.id, password=cls.password) cls.global_staff = GlobalStaffFactory.create() date = datetime(2013, 1, 22, tzinfo=UTC) for user in (cls.student, cls.other_student, ): CourseEnrollmentFactory( course_id=cls.course.id, user=user, created=date, ) cls.namespaced_url = 'grades_api:user_grade_detail' def setUp(self): super(CurrentGradeViewTest, self).setUp() self.client.login(username=self.student.username, password=self.password) def get_url(self, username): """ Helper function to create the url """ base_url = reverse( self.namespaced_url, kwargs={ 'course_id': self.course_key, } ) query_string = '' if username: query_string = '?' + urlencode(dict(username=username)) return base_url + query_string def test_anonymous(self): """ Test that an anonymous user cannot access the API and an error is received. """ self.client.logout() resp = self.client.get(self.get_url(self.student.username)) self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED) def test_self_get_grade(self): """ Test that a user can successfully request her own grade. """ with check_mongo_calls(3): resp = self.client.get(self.get_url(self.student.username)) self.assertEqual(resp.status_code, status.HTTP_200_OK) # redo with block structure now in the cache with check_mongo_calls(3): resp = self.client.get(self.get_url(self.student.username)) self.assertEqual(resp.status_code, status.HTTP_200_OK) # and again, with the username defaulting to the current user with check_mongo_calls(3): resp = self.client.get(self.get_url(None)) self.assertEqual(resp.status_code, status.HTTP_200_OK) def test_nonexistent_user(self): """ Test that a request for a nonexistent username returns an error. """ resp = self.client.get(self.get_url('IDoNotExist')) self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) self.assertIn('error_code', resp.data) self.assertEqual(resp.data['error_code'], 'user_mismatch') def test_other_get_grade(self): """ Test that if a user requests the grade for another user, she receives an error. """ self.client.logout() self.client.login(username=self.other_student.username, password=self.password) resp = self.client.get(self.get_url(self.student.username)) self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) self.assertIn('error_code', resp.data) self.assertEqual(resp.data['error_code'], 'user_mismatch') def test_self_get_grade_not_enrolled(self): """ Test that a user receives an error if she requests her own grade in a course where she is not enrolled. """ # a user not enrolled in the course cannot request her grade self.client.logout() self.client.login(username=self.other_user.username, password=self.password) resp = self.client.get(self.get_url(self.other_user.username)) self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND) self.assertIn('error_code', resp.data) self.assertEqual( resp.data['error_code'], 'user_or_course_does_not_exist' ) def test_wrong_course_key(self): """ Test that a request for an invalid course key returns an error. """ def mock_from_string(*args, **kwargs): # pylint: disable=unused-argument """Mocked function to always raise an exception""" raise InvalidKeyError('foo', 'bar') with patch('opaque_keys.edx.keys.CourseKey.from_string', side_effect=mock_from_string): resp = self.client.get(self.get_url(self.student.username)) self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND) self.assertIn('error_code', resp.data) self.assertEqual( resp.data['error_code'], 'invalid_course_key' ) def test_course_does_not_exist(self): """ Test that requesting a valid, nonexistent course key returns an error as expected. """ base_url = reverse( self.namespaced_url, kwargs={ 'course_id': 'course-v1:MITx+8.MechCX+2014_T1', } ) url = "{0}?username={1}".format(base_url, self.student.username) resp = self.client.get(url) self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND) self.assertIn('error_code', resp.data) self.assertEqual( resp.data['error_code'], 'user_or_course_does_not_exist' ) @ddt.data( 'staff', 'global_staff' ) def test_staff_can_see_student(self, staff_user): """ Ensure that staff members can see her student's grades. """ self.client.logout() self.client.login(username=getattr(self, staff_user).username, password=self.password) resp = self.client.get(self.get_url(self.student.username)) self.assertEqual(resp.status_code, status.HTTP_200_OK) expected_data = [{ 'username': self.student.username, 'letter_grade': None, 'percent': 0.0, 'course_key': str(self.course_key), 'passed': False }] self.assertEqual(resp.data, expected_data) @ddt.data( 'staff', 'global_staff' ) def test_staff_requests_nonexistent_user(self, staff_user): """ Test that a staff request for a nonexistent username returns an error. """ self.client.logout() self.client.login(username=getattr(self, staff_user).username, password=self.password) resp = self.client.get(self.get_url('IDoNotExist')) self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND) self.assertIn('error_code', resp.data) self.assertEqual(resp.data['error_code'], 'user_does_not_exist') def test_no_grade(self): """ Test the grade for a user who has not answered any test. """ resp = self.client.get(self.get_url(self.student.username)) self.assertEqual(resp.status_code, status.HTTP_200_OK) expected_data = [{ 'username': self.student.username, 'letter_grade': None, 'percent': 0.0, 'course_key': str(self.course_key), 'passed': False }] self.assertEqual(resp.data, expected_data) @ddt.data( ({'letter_grade': None, 'percent': 0.4, 'passed': False}), ({'letter_grade': 'Pass', 'percent': 1, 'passed': True}), ) def test_grade(self, grade): """ Test that the user gets her grade in case she answered tests with an insufficient score. """ with mock_passing_grade(letter_grade=grade['letter_grade'], percent=grade['percent']): resp = self.client.get(self.get_url(self.student.username)) self.assertEqual(resp.status_code, status.HTTP_200_OK) expected_data = { 'username': self.student.username, 'course_key': str(self.course_key), } expected_data.update(grade) self.assertEqual(resp.data, [expected_data]) @ddt.ddt class GradingPolicyTestMixin(object): """ Mixin class for Grading Policy tests """ shard = 4 view_name = None def setUp(self): super(GradingPolicyTestMixin, self).setUp() self.create_user_and_access_token() def create_user_and_access_token(self): self.user = GlobalStaffFactory.create() self.oauth_client = ClientFactory.create() self.access_token = AccessTokenFactory.create(user=self.user, client=self.oauth_client).token @classmethod def create_course_data(cls): cls.invalid_course_id = 'foo/bar/baz' cls.course = CourseFactory.create(display_name='An Introduction to API Testing', raw_grader=cls.raw_grader) cls.course_id = unicode(cls.course.id) with cls.store.bulk_operations(cls.course.id, emit_signals=False): cls.sequential = ItemFactory.create( category="sequential", parent_location=cls.course.location, display_name="Lesson 1", format="Homework", graded=True ) factory = MultipleChoiceResponseXMLFactory() args = {'choices': [False, True, False]} problem_xml = factory.build_xml(**args) cls.problem = ItemFactory.create( category="problem", parent_location=cls.sequential.location, display_name="Problem 1", format="Homework", data=problem_xml, ) cls.video = ItemFactory.create( category="video", parent_location=cls.sequential.location, display_name="Video 1", ) cls.html = ItemFactory.create( category="html", parent_location=cls.sequential.location, display_name="HTML 1", ) def http_get(self, uri, **headers): """ Submit an HTTP GET request """ default_headers = { 'HTTP_AUTHORIZATION': 'Bearer ' + self.access_token } default_headers.update(headers) response = self.client.get(uri, follow=True, **default_headers) return response def assert_get_for_course(self, course_id=None, expected_status_code=200, **headers): """ Submit an HTTP GET request to the view for the given course. Validates the status_code of the response is as expected. """ response = self.http_get( reverse(self.view_name, kwargs={'course_id': course_id or self.course_id}), **headers ) self.assertEqual(response.status_code, expected_status_code) return response def get_auth_header(self, user): """ Returns Bearer auth header with a generated access token for the given user. """ access_token = AccessTokenFactory.create(user=user, client=self.oauth_client).token return 'Bearer ' + access_token def test_get_invalid_course(self): """ The view should return a 404 for an invalid course ID. """ self.assert_get_for_course(course_id=self.invalid_course_id, expected_status_code=404) def test_get(self): """ The view should return a 200 for a valid course ID. """ return self.assert_get_for_course() def test_not_authenticated(self): """ The view should return HTTP status 401 if user is unauthenticated. """ self.assert_get_for_course(expected_status_code=401, HTTP_AUTHORIZATION=None) def test_staff_authorized(self): """ The view should return a 200 when provided an access token for course staff. """ user = StaffFactory(course_key=self.course.id) auth_header = self.get_auth_header(user) self.assert_get_for_course(HTTP_AUTHORIZATION=auth_header) def test_not_authorized(self): """ The view should return HTTP status 404 when provided an access token for an unauthorized user. """ user = UserFactory() auth_header = self.get_auth_header(user) self.assert_get_for_course(expected_status_code=404, HTTP_AUTHORIZATION=auth_header) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) def test_course_keys(self, modulestore_type): """ The view should be addressable by course-keys from both module stores. """ course = CourseFactory.create( start=datetime(2014, 6, 16, 14, 30, tzinfo=UTC), end=datetime(2015, 1, 16, tzinfo=UTC), org="MTD", default_store=modulestore_type, ) self.assert_get_for_course(course_id=unicode(course.id)) class CourseGradingPolicyTests(GradingPolicyTestMixin, SharedModuleStoreTestCase): """ Tests for CourseGradingPolicy view. """ shard = 4 view_name = 'grades_api:course_grading_policy' raw_grader = [ { "min_count": 24, "weight": 0.2, "type": "Homework", "drop_count": 0, "short_label": "HW" }, { "min_count": 4, "weight": 0.8, "type": "Exam", "drop_count": 0, "short_label": "Exam" } ] @classmethod def setUpClass(cls): super(CourseGradingPolicyTests, cls).setUpClass() cls.create_course_data() def test_get(self): """ The view should return grading policy for a course. """ response = super(CourseGradingPolicyTests, self).test_get() expected = [ { "count": 24, "weight": 0.2, "assignment_type": "Homework", "dropped": 0 }, { "count": 4, "weight": 0.8, "assignment_type": "Exam", "dropped": 0 } ] self.assertListEqual(response.data, expected) class CourseGradingPolicyMissingFieldsTests(GradingPolicyTestMixin, SharedModuleStoreTestCase): """ Tests for CourseGradingPolicy view when fields are missing. """ shard = 4 view_name = 'grades_api:course_grading_policy' # Raw grader with missing keys raw_grader = [ { "min_count": 24, "weight": 0.2, "type": "Homework", "drop_count": 0, "short_label": "HW" }, { # Deleted "min_count" key "weight": 0.8, "type": "Exam", "drop_count": 0, "short_label": "Exam" } ] @classmethod def setUpClass(cls): super(CourseGradingPolicyMissingFieldsTests, cls).setUpClass() cls.create_course_data() def test_get(self): """ The view should return grading policy for a course. """ response = super(CourseGradingPolicyMissingFieldsTests, self).test_get() expected = [ { "count": 24, "weight": 0.2, "assignment_type": "Homework", "dropped": 0 }, { "count": None, "weight": 0.8, "assignment_type": "Exam", "dropped": 0 } ] self.assertListEqual(response.data, expected)