diff --git a/lms/djangoapps/grades/rest_api/serializers.py b/lms/djangoapps/grades/rest_api/serializers.py index 75c91bc56f..1c1ad900af 100644 --- a/lms/djangoapps/grades/rest_api/serializers.py +++ b/lms/djangoapps/grades/rest_api/serializers.py @@ -102,6 +102,8 @@ class SubsectionGradeResponseSerializer(serializers.Serializer): """ Serializer for subsection grade response. """ + success = serializers.BooleanField() + error_message = serializers.CharField(required=False) subsection_id = serializers.CharField() user_id = serializers.IntegerField() course_id = serializers.CharField() diff --git a/lms/djangoapps/grades/rest_api/v1/gradebook_views.py b/lms/djangoapps/grades/rest_api/v1/gradebook_views.py index 5ea63c1779..05b97cacd5 100644 --- a/lms/djangoapps/grades/rest_api/v1/gradebook_views.py +++ b/lms/djangoapps/grades/rest_api/v1/gradebook_views.py @@ -13,6 +13,7 @@ from django.contrib.auth import get_user_model from django.core.cache import cache from django.db.models import Case, Exists, F, OuterRef, When, Q from django.urls import reverse +from django.utils.translation import ugettext_lazy as _ from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey, UsageKey from rest_framework import status @@ -920,6 +921,10 @@ class GradebookBulkUpdateView(GradeViewMixin, PaginatedAPIView): ) +class SubsectionUnavailableToUserException(Exception): + pass + + @view_auth_classes() class SubsectionGradeView(GradeViewMixin, APIView): """ @@ -1043,6 +1048,8 @@ class SubsectionGradeView(GradeViewMixin, APIView): developer_message='Invalid UserID', error_code='invalid_user_id' ) + success = True + err_msg = "" override = None history = [] history_record_limit = request.GET.get('history_record_limit') @@ -1067,16 +1074,30 @@ class SubsectionGradeView(GradeViewMixin, APIView): 'possible_graded': original_grade.possible_graded, } except PersistentSubsectionGrade.DoesNotExist: - grade_data = self._get_grade_data_for_not_attempted_assignment(user_id, usage_key) + try: + grade_data = self._get_grade_data_for_not_attempted_assignment(user_id, usage_key) + except SubsectionUnavailableToUserException as exc: + success = False + err_msg = str(exc) + grade_data = { + 'earned_all': 0, + 'possible_all': 0, + 'earned_graded': 0, + 'possible_graded': 0, + } - results = SubsectionGradeResponseSerializer({ + response_data = { + 'success': success, 'original_grade': grade_data, 'override': override, 'history': history, 'subsection_id': usage_key, 'user_id': user_id, 'course_id': usage_key.course_key, - }) + } + if not success: + response_data['error_message'] = err_msg + results = SubsectionGradeResponseSerializer(response_data) return Response(results.data) def _get_grade_data_for_not_attempted_assignment(self, user_id, usage_key): @@ -1085,6 +1106,10 @@ class SubsectionGradeView(GradeViewMixin, APIView): """ student = get_user_model().objects.get(id=user_id) course_structure = get_course_blocks(student, usage_key) + if usage_key not in course_structure: + raise SubsectionUnavailableToUserException( + _("Cannot override subsection grade: subsection is not available for target learner.") + ) subsection_grade_factory = SubsectionGradeFactory(student, course_structure=course_structure) grade = subsection_grade_factory.create(course_structure[usage_key], read_only=True, force_calculate=True) grade_data = { diff --git a/lms/djangoapps/grades/rest_api/v1/tests/test_gradebook_views.py b/lms/djangoapps/grades/rest_api/v1/tests/test_gradebook_views.py index 6551c8e1cd..05350fc198 100644 --- a/lms/djangoapps/grades/rest_api/v1/tests/test_gradebook_views.py +++ b/lms/djangoapps/grades/rest_api/v1/tests/test_gradebook_views.py @@ -1706,6 +1706,7 @@ class SubsectionGradeViewTest(GradebookViewTestBase): ) expected_data = { + 'success': True, 'original_grade': OrderedDict([ ('earned_all', 1.0), ('possible_all', 2.0), @@ -1733,6 +1734,7 @@ class SubsectionGradeViewTest(GradebookViewTestBase): ) expected_data = { + 'success': True, 'original_grade': OrderedDict([ ('earned_all', 6.0), ('possible_all', 12.0), @@ -1770,6 +1772,7 @@ class SubsectionGradeViewTest(GradebookViewTestBase): ) expected_data = { + 'success': True, 'original_grade': OrderedDict([ ('earned_all', 6.0), ('possible_all', 12.0), @@ -1827,6 +1830,7 @@ class SubsectionGradeViewTest(GradebookViewTestBase): ) expected_data = { + 'success': True, 'original_grade': OrderedDict([ ('earned_all', 6.0), ('possible_all', 12.0), @@ -1928,6 +1932,7 @@ class SubsectionGradeViewTest(GradebookViewTestBase): self.get_url(subsection_id=self.usage_key, user_id=other_user.id) ) expected_data = { + 'success': True, 'original_grade': OrderedDict([ ('earned_all', 0.0), ('possible_all', 0.0), @@ -1952,3 +1957,35 @@ class SubsectionGradeViewTest(GradebookViewTestBase): ) self.assertEqual(status.HTTP_403_FORBIDDEN, resp.status_code) + + @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False}) + def test_get_override_for_unreleased_block(self): + self.login_course_staff() + unreleased_subsection = ItemFactory.create( + parent_location=self.chapter_1.location, + category='sequential', + graded=True, + start=datetime(2999, 1, 1, tzinfo=UTC), # arbitrary future date + display_name='Unreleased Section', + ) + + resp = self.client.get( + self.get_url(subsection_id=unreleased_subsection.location) + ) + + expected_data = { + 'success': False, + 'error_message': "Cannot override subsection grade: subsection is not available for target learner.", + 'original_grade': OrderedDict([ + ('earned_all', 0.0), + ('possible_all', 0.0), + ('earned_graded', 0.0), + ('possible_graded', 0.0) + ]), + 'user_id': self.user_id, + 'override': None, + 'course_id': text_type(self.usage_key.course_key), + 'subsection_id': text_type(unreleased_subsection.location), + 'history': [] + } + self.assertEqual(expected_data, resp.data)