Skip to content

Commit f1aced3

Browse files
fix: Copilot feedback
1 parent 02da7a8 commit f1aced3

1 file changed

Lines changed: 144 additions & 63 deletions

File tree

lms/djangoapps/instructor/views/api_v2.py

Lines changed: 144 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -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

16571663
def _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

Comments
 (0)