@@ -1624,15 +1624,15 @@ def _parse_course_and_problem(course_id, problem):
16241624 course_key = CourseKey .from_string (course_id )
16251625 except InvalidKeyError :
16261626 return None , Response (
1627- {'error' : 'INVALID_PARAMETER' , 'message' : ' Invalid course key format' , 'status_code' : 400 },
1627+ {'error' : 'Invalid course key' },
16281628 status = status .HTTP_400_BAD_REQUEST
16291629 )
16301630
16311631 try :
16321632 usage_key = UsageKey .from_string (problem ).map_into_course (course_key )
16331633 except InvalidKeyError :
16341634 return None , Response (
1635- {'error' : 'INVALID_PARAMETER' , 'message' : ' Invalid problem location', 'status_code' : 400 },
1635+ {'error' : 'Invalid problem location' },
16361636 status = status .HTTP_400_BAD_REQUEST
16371637 )
16381638
@@ -1643,15 +1643,21 @@ def _resolve_learner(learner_identifier):
16431643 """
16441644 Resolve a learner identifier (username or email) to a User object.
16451645
1646- Returns User on success, or a Response with 404 on failure.
1646+ Returns ( User, None) on success, or (None, Response) on failure.
16471647 """
1648+ UserModel = get_user_model ()
16481649 try :
1649- return get_student_from_identifier (learner_identifier ), None
1650- except Exception : # pylint: disable=broad-except
1650+ return get_user_by_username_or_email (learner_identifier ), None
1651+ except UserModel . DoesNotExist :
16511652 return None , Response (
1652- {'error' : 'RESOURCE_NOT_FOUND' , 'message' : ' Learner not found', 'status_code' : 404 },
1653+ {'error' : 'Learner not found' },
16531654 status = status .HTTP_404_NOT_FOUND
16541655 )
1656+ except UserModel .MultipleObjectsReturned :
1657+ return None , Response (
1658+ {'error' : 'Multiple learners found for the given identifier' },
1659+ status = status .HTTP_400_BAD_REQUEST
1660+ )
16551661
16561662
16571663def _build_async_response (instructor_task , course_id , problem_location , learner_scope = 'all' ):
@@ -1685,6 +1691,33 @@ class ResetAttemptsView(DeveloperErrorViewMixin, APIView):
16851691 permission_classes = (IsAuthenticated , permissions .InstructorPermission )
16861692 permission_name = permissions .GIVE_STUDENT_EXTENSION
16871693
1694+ @apidocs .schema (
1695+ parameters = [
1696+ apidocs .string_parameter (
1697+ 'course_id' ,
1698+ apidocs .ParameterLocation .PATH ,
1699+ description = "Course key for the course." ,
1700+ ),
1701+ apidocs .string_parameter (
1702+ 'problem' ,
1703+ apidocs .ParameterLocation .PATH ,
1704+ description = "Problem block usage key." ,
1705+ ),
1706+ apidocs .string_parameter (
1707+ 'learner' ,
1708+ apidocs .ParameterLocation .QUERY ,
1709+ description = "Optional: Learner username or email. If omitted, resets all learners (async)." ,
1710+ ),
1711+ ],
1712+ responses = {
1713+ 200 : SyncOperationResultSerializer ,
1714+ 202 : AsyncOperationResultSerializer ,
1715+ 400 : "Invalid parameters provided." ,
1716+ 401 : "The requesting user is not authenticated." ,
1717+ 403 : "The requesting user lacks permission." ,
1718+ 404 : "Learner not found." ,
1719+ },
1720+ )
16881721 @method_decorator (transaction .non_atomic_requests )
16891722 def post (self , request , course_id , problem ):
16901723 """Reset problem attempts for one or all learners."""
@@ -1710,16 +1743,12 @@ def post(self, request, course_id, problem):
17101743 )
17111744 except StudentModule .DoesNotExist :
17121745 return Response (
1713- {'error' : 'RESOURCE_NOT_FOUND' ,
1714- 'message' : 'No state found for this learner and problem' ,
1715- 'status_code' : 404 },
1746+ {'error' : 'No state found for this learner and problem' },
17161747 status = status .HTTP_404_NOT_FOUND ,
17171748 )
17181749 except sub_api .SubmissionError :
17191750 return Response (
1720- {'error' : 'INTERNAL_ERROR' ,
1721- 'message' : 'An error occurred while resetting attempts' ,
1722- 'status_code' : 500 },
1751+ {'error' : 'An error occurred while resetting attempts' },
17231752 status = status .HTTP_500_INTERNAL_SERVER_ERROR ,
17241753 )
17251754
@@ -1736,9 +1765,7 @@ def post(self, request, course_id, problem):
17361765 course = get_course_with_access (request .user , 'staff' , course_key , depth = None )
17371766 if not has_access (request .user , 'instructor' , course ):
17381767 return Response (
1739- {'error' : 'PERMISSION_DENIED' ,
1740- 'message' : 'Instructor access required for bulk operations' ,
1741- 'status_code' : 403 },
1768+ {'error' : 'Instructor access required for bulk operations' },
17421769 status = status .HTTP_403_FORBIDDEN ,
17431770 )
17441771
@@ -1748,16 +1775,12 @@ def post(self, request, course_id, problem):
17481775 )
17491776 except AlreadyRunningError :
17501777 return Response (
1751- {'error' : 'ALREADY_RUNNING' ,
1752- 'message' : 'A reset task is already running for this problem' ,
1753- 'status_code' : 409 },
1778+ {'error' : 'A reset task is already running for this problem' },
17541779 status = status .HTTP_409_CONFLICT ,
17551780 )
17561781 except ItemNotFoundError :
17571782 return Response (
1758- {'error' : 'RESOURCE_NOT_FOUND' ,
1759- 'message' : 'Problem not found' ,
1760- 'status_code' : 404 },
1783+ {'error' : 'Problem not found' },
17611784 status = status .HTTP_404_NOT_FOUND ,
17621785 )
17631786
@@ -1774,6 +1797,32 @@ class DeleteStateView(DeveloperErrorViewMixin, APIView):
17741797 permission_classes = (IsAuthenticated , permissions .InstructorPermission )
17751798 permission_name = permissions .GIVE_STUDENT_EXTENSION
17761799
1800+ @apidocs .schema (
1801+ parameters = [
1802+ apidocs .string_parameter (
1803+ 'course_id' ,
1804+ apidocs .ParameterLocation .PATH ,
1805+ description = "Course key for the course." ,
1806+ ),
1807+ apidocs .string_parameter (
1808+ 'problem' ,
1809+ apidocs .ParameterLocation .PATH ,
1810+ description = "Problem block usage key." ,
1811+ ),
1812+ apidocs .string_parameter (
1813+ 'learner' ,
1814+ apidocs .ParameterLocation .QUERY ,
1815+ description = "Learner username or email (required)." ,
1816+ ),
1817+ ],
1818+ responses = {
1819+ 200 : SyncOperationResultSerializer ,
1820+ 400 : "Invalid parameters or missing learner." ,
1821+ 401 : "The requesting user is not authenticated." ,
1822+ 403 : "The requesting user lacks permission." ,
1823+ 404 : "Learner not found." ,
1824+ },
1825+ )
17771826 @method_decorator (transaction .non_atomic_requests )
17781827 def delete (self , request , course_id , problem ):
17791828 """Delete learner problem state."""
@@ -1785,9 +1834,7 @@ def delete(self, request, course_id, problem):
17851834 learner_identifier = request .query_params .get ('learner' )
17861835 if not learner_identifier :
17871836 return Response (
1788- {'error' : 'INVALID_PARAMETER' ,
1789- 'message' : 'The learner parameter is required' ,
1790- 'status_code' : 400 },
1837+ {'error' : 'The learner parameter is required' },
17911838 status = status .HTTP_400_BAD_REQUEST ,
17921839 )
17931840
@@ -1805,16 +1852,12 @@ def delete(self, request, course_id, problem):
18051852 )
18061853 except StudentModule .DoesNotExist :
18071854 return Response (
1808- {'error' : 'RESOURCE_NOT_FOUND' ,
1809- 'message' : 'No state found for this learner and problem' ,
1810- 'status_code' : 404 },
1855+ {'error' : 'No state found for this learner and problem' },
18111856 status = status .HTTP_404_NOT_FOUND ,
18121857 )
18131858 except sub_api .SubmissionError :
18141859 return Response (
1815- {'error' : 'INTERNAL_ERROR' ,
1816- 'message' : 'An error occurred while deleting state' ,
1817- 'status_code' : 500 },
1860+ {'error' : 'An error occurred while deleting state' },
18181861 status = status .HTTP_500_INTERNAL_SERVER_ERROR ,
18191862 )
18201863
@@ -1840,6 +1883,37 @@ class RescoreView(DeveloperErrorViewMixin, APIView):
18401883 permission_classes = (IsAuthenticated , permissions .InstructorPermission )
18411884 permission_name = permissions .OVERRIDE_GRADES
18421885
1886+ @apidocs .schema (
1887+ parameters = [
1888+ apidocs .string_parameter (
1889+ 'course_id' ,
1890+ apidocs .ParameterLocation .PATH ,
1891+ description = "Course key for the course." ,
1892+ ),
1893+ apidocs .string_parameter (
1894+ 'problem' ,
1895+ apidocs .ParameterLocation .PATH ,
1896+ description = "Problem block usage key." ,
1897+ ),
1898+ apidocs .string_parameter (
1899+ 'learner' ,
1900+ apidocs .ParameterLocation .QUERY ,
1901+ description = "Optional: Learner username or email. If omitted, rescores all learners." ,
1902+ ),
1903+ apidocs .string_parameter (
1904+ 'only_if_higher' ,
1905+ apidocs .ParameterLocation .QUERY ,
1906+ description = "Optional: If 'true', only update scores that are higher than current." ,
1907+ ),
1908+ ],
1909+ responses = {
1910+ 202 : AsyncOperationResultSerializer ,
1911+ 400 : "Invalid parameters provided." ,
1912+ 401 : "The requesting user is not authenticated." ,
1913+ 403 : "The requesting user lacks permission." ,
1914+ 404 : "Learner not found." ,
1915+ },
1916+ )
18431917 @method_decorator (transaction .non_atomic_requests )
18441918 def post (self , request , course_id , problem ):
18451919 """Rescore problem submissions."""
@@ -1862,21 +1936,17 @@ def post(self, request, course_id, problem):
18621936 )
18631937 except NotImplementedError as exc :
18641938 return Response (
1865- {'error' : 'INVALID_PARAMETER' , 'message' : str (exc ), 'status_code' : 400 },
1939+ {'error' : str (exc )},
18661940 status = status .HTTP_400_BAD_REQUEST ,
18671941 )
18681942 except ItemNotFoundError :
18691943 return Response (
1870- {'error' : 'RESOURCE_NOT_FOUND' ,
1871- 'message' : 'Problem not found' ,
1872- 'status_code' : 404 },
1944+ {'error' : 'Problem not found' },
18731945 status = status .HTTP_404_NOT_FOUND ,
18741946 )
18751947 except AlreadyRunningError :
18761948 return Response (
1877- {'error' : 'ALREADY_RUNNING' ,
1878- 'message' : 'A rescore task is already running for this learner and problem' ,
1879- 'status_code' : 409 },
1949+ {'error' : 'A rescore task is already running for this learner and problem' },
18801950 status = status .HTTP_409_CONFLICT ,
18811951 )
18821952
@@ -1888,9 +1958,7 @@ def post(self, request, course_id, problem):
18881958 course = get_course_with_access (request .user , 'staff' , course_key )
18891959 if not has_access (request .user , 'instructor' , course ):
18901960 return Response (
1891- {'error' : 'PERMISSION_DENIED' ,
1892- 'message' : 'Instructor access required for bulk operations' ,
1893- 'status_code' : 403 },
1961+ {'error' : 'Instructor access required for bulk operations' },
18941962 status = status .HTTP_403_FORBIDDEN ,
18951963 )
18961964
@@ -1900,21 +1968,17 @@ def post(self, request, course_id, problem):
19001968 )
19011969 except NotImplementedError as exc :
19021970 return Response (
1903- {'error' : 'INVALID_PARAMETER' , 'message' : str (exc ), 'status_code' : 400 },
1971+ {'error' : str (exc )},
19041972 status = status .HTTP_400_BAD_REQUEST ,
19051973 )
19061974 except ItemNotFoundError :
19071975 return Response (
1908- {'error' : 'RESOURCE_NOT_FOUND' ,
1909- 'message' : 'Problem not found' ,
1910- 'status_code' : 404 },
1976+ {'error' : 'Problem not found' },
19111977 status = status .HTTP_404_NOT_FOUND ,
19121978 )
19131979 except AlreadyRunningError :
19141980 return Response (
1915- {'error' : 'ALREADY_RUNNING' ,
1916- 'message' : 'A rescore task is already running for this problem' ,
1917- 'status_code' : 409 },
1981+ {'error' : 'A rescore task is already running for this problem' },
19181982 status = status .HTTP_409_CONFLICT ,
19191983 )
19201984
@@ -1930,6 +1994,32 @@ class ScoreOverrideView(DeveloperErrorViewMixin, APIView):
19301994 permission_classes = (IsAuthenticated , permissions .InstructorPermission )
19311995 permission_name = permissions .OVERRIDE_GRADES
19321996
1997+ @apidocs .schema (
1998+ parameters = [
1999+ apidocs .string_parameter (
2000+ 'course_id' ,
2001+ apidocs .ParameterLocation .PATH ,
2002+ description = "Course key for the course." ,
2003+ ),
2004+ apidocs .string_parameter (
2005+ 'problem' ,
2006+ apidocs .ParameterLocation .PATH ,
2007+ description = "Problem block usage key." ,
2008+ ),
2009+ apidocs .string_parameter (
2010+ 'learner' ,
2011+ apidocs .ParameterLocation .QUERY ,
2012+ description = "Learner username or email (required)." ,
2013+ ),
2014+ ],
2015+ responses = {
2016+ 200 : SyncOperationResultSerializer ,
2017+ 400 : "Invalid parameters or invalid score." ,
2018+ 401 : "The requesting user is not authenticated." ,
2019+ 403 : "The requesting user lacks permission." ,
2020+ 404 : "Learner not found." ,
2021+ },
2022+ )
19332023 @method_decorator (transaction .non_atomic_requests )
19342024 def put (self , request , course_id , problem ):
19352025 """Override a learner's score."""
@@ -1941,9 +2031,7 @@ def put(self, request, course_id, problem):
19412031 learner_identifier = request .query_params .get ('learner' )
19422032 if not learner_identifier :
19432033 return Response (
1944- {'error' : 'INVALID_PARAMETER' ,
1945- 'message' : 'The learner parameter is required' ,
1946- 'status_code' : 400 },
2034+ {'error' : 'The learner parameter is required' },
19472035 status = status .HTTP_400_BAD_REQUEST ,
19482036 )
19492037
@@ -1954,8 +2042,7 @@ def put(self, request, course_id, problem):
19542042 body_serializer = ScoreOverrideRequestSerializer (data = request .data )
19552043 if not body_serializer .is_valid ():
19562044 return Response (
1957- {'error' : 'INVALID_PARAMETER' , 'message' : 'Invalid request body' ,
1958- 'field_errors' : body_serializer .errors , 'status_code' : 400 },
2045+ {'error' : 'Invalid request body' , 'field_errors' : body_serializer .errors },
19592046 status = status .HTTP_400_BAD_REQUEST ,
19602047 )
19612048 score = body_serializer .validated_data ['score' ]
@@ -1964,17 +2051,13 @@ def put(self, request, course_id, problem):
19642051 block = modulestore ().get_item (usage_key )
19652052 except ItemNotFoundError :
19662053 return Response (
1967- {'error' : 'RESOURCE_NOT_FOUND' ,
1968- 'message' : 'Problem not found' ,
1969- 'status_code' : 404 },
2054+ {'error' : 'Problem not found' },
19702055 status = status .HTTP_404_NOT_FOUND ,
19712056 )
19722057
19732058 if not has_access (request .user , 'staff' , block ):
19742059 return Response (
1975- {'error' : 'PERMISSION_DENIED' ,
1976- 'message' : 'You do not have permission to override scores for this problem' ,
1977- 'status_code' : 403 },
2060+ {'error' : 'You do not have permission to override scores for this problem' },
19782061 status = status .HTTP_403_FORBIDDEN ,
19792062 )
19802063
@@ -1984,19 +2067,17 @@ def put(self, request, course_id, problem):
19842067 )
19852068 except NotImplementedError as exc :
19862069 return Response (
1987- {'error' : 'INVALID_PARAMETER' , 'message' : str (exc ), 'status_code' : 400 },
2070+ {'error' : str (exc )},
19882071 status = status .HTTP_400_BAD_REQUEST ,
19892072 )
19902073 except ValueError as exc :
19912074 return Response (
1992- {'error' : 'INVALID_PARAMETER' , 'message' : str (exc ), 'status_code' : 400 },
2075+ {'error' : str (exc )},
19932076 status = status .HTTP_400_BAD_REQUEST ,
19942077 )
19952078 except AlreadyRunningError :
19962079 return Response (
1997- {'error' : 'ALREADY_RUNNING' ,
1998- 'message' : 'A score override task is already running for this learner and problem' ,
1999- 'status_code' : 409 },
2080+ {'error' : 'A score override task is already running for this learner and problem' },
20002081 status = status .HTTP_409_CONFLICT ,
20012082 )
20022083
0 commit comments