feat: add batch lock delete endpoint (#30161)
* feat: add batch lock delete endpoint * chore: bump ORA versions * docs: update example postman collection
This commit is contained in:
@@ -154,3 +154,19 @@ def delete_submission_lock(request, usage_id, submission_uuid):
|
||||
raise XBlockInternalError(context={"handler": handler_name})
|
||||
|
||||
return json.loads(response.content)
|
||||
|
||||
|
||||
def batch_delete_submission_locks(request, usage_id, submission_uuids):
|
||||
"""
|
||||
Batch delete a list of submission locks. Limited only to those in the list the user owns.
|
||||
|
||||
Returns: none
|
||||
"""
|
||||
handler_name = "batch_delete_submission_lock"
|
||||
body = {"submission_uuids": submission_uuids}
|
||||
|
||||
response = call_xblock_json_handler(request, usage_id, handler_name, body)
|
||||
|
||||
# Errors should raise a blanket exception. Otherwise body is empty, 200 is implicit success
|
||||
if response.status_code != 200:
|
||||
raise XBlockInternalError(context={"handler": handler_name})
|
||||
|
||||
@@ -1454,6 +1454,173 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Bulk Unlock",
|
||||
"event": [
|
||||
{
|
||||
"listen": "prerequest",
|
||||
"script": {
|
||||
"exec": [
|
||||
""
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-CSRFToken",
|
||||
"value": "{{csrftoken}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{protocol}}://{{lms_url}}/api/ora_staff_grader{{mock}}/submission/lock?oraLocation={{block_id_encoded}}&submissionUUID={{submission_id}}",
|
||||
"protocol": "{{protocol}}",
|
||||
"host": [
|
||||
"{{lms_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"ora_staff_grader{{mock}}",
|
||||
"submission",
|
||||
"lock"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "oraLocation",
|
||||
"value": "{{block_id_encoded}}",
|
||||
"description": "ORA location"
|
||||
},
|
||||
{
|
||||
"key": "submissionUUID",
|
||||
"value": "{{submission_id}}",
|
||||
"description": "Individual submission UUID"
|
||||
},
|
||||
{
|
||||
"key": "oraLocation",
|
||||
"value": "{{team_block_id_encoded}}",
|
||||
"description": "Team ORA location",
|
||||
"disabled": true
|
||||
},
|
||||
{
|
||||
"key": "submissionUUID",
|
||||
"value": "{{team_submission_id}}",
|
||||
"description": "Team submission UUID",
|
||||
"disabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "Bulk Unlock Success",
|
||||
"originalRequest": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "X-CSRFToken",
|
||||
"value": "{{csrftoken}}",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"submissionUUIDs\": [{{submission_id}}]\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{protocol}}://{{lms_url}}/api/ora_staff_grader{{mock}}/submission/batch/unlock?oraLocation={{block_id_encoded}}",
|
||||
"protocol": "{{protocol}}",
|
||||
"host": [
|
||||
"{{lms_url}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"ora_staff_grader{{mock}}",
|
||||
"submission",
|
||||
"batch",
|
||||
"unlock"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "oraLocation",
|
||||
"value": "{{block_id_encoded}}",
|
||||
"description": "ORA location"
|
||||
},
|
||||
{
|
||||
"key": "oraLocation",
|
||||
"value": "{{team_block_id_encoded}}",
|
||||
"description": "Team ORA location",
|
||||
"disabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"status": "OK",
|
||||
"code": 200,
|
||||
"_postman_previewlanguage": "json",
|
||||
"header": [
|
||||
{
|
||||
"key": "Date",
|
||||
"value": "Thu, 31 Mar 2022 20:52:57 GMT"
|
||||
},
|
||||
{
|
||||
"key": "Server",
|
||||
"value": "WSGIServer/0.2 CPython/3.8.10"
|
||||
},
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "WWW-Authenticate",
|
||||
"value": "JWT realm=\"api\""
|
||||
},
|
||||
{
|
||||
"key": "Vary",
|
||||
"value": "Accept, Accept-Language, Origin, Cookie"
|
||||
},
|
||||
{
|
||||
"key": "Allow",
|
||||
"value": "GET, POST, HEAD, OPTIONS"
|
||||
},
|
||||
{
|
||||
"key": "Server-Timing",
|
||||
"value": "TimerPanel_utime;dur=37.91900000000048;desc=\"User CPU time\", TimerPanel_stime;dur=1.1920000000000819;desc=\"System CPU time\", TimerPanel_total;dur=39.11100000000056;desc=\"Total CPU time\", TimerPanel_total_time;dur=65.8259391784668;desc=\"Elapsed time\", SQLPanel_sql_time;dur=0;desc=\"SQL 0 queries\""
|
||||
},
|
||||
{
|
||||
"key": "X-Frame-Options",
|
||||
"value": "DENY"
|
||||
},
|
||||
{
|
||||
"key": "Content-Language",
|
||||
"value": "en"
|
||||
},
|
||||
{
|
||||
"key": "Content-Length",
|
||||
"value": "58"
|
||||
},
|
||||
{
|
||||
"key": "Set-Cookie",
|
||||
"value": "lms_sessionid=\"\"; expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/; SameSite=Lax"
|
||||
}
|
||||
],
|
||||
"cookie": [
|
||||
{
|
||||
"expires": "Invalid Date"
|
||||
}
|
||||
],
|
||||
"body": "{}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Update Grade Data",
|
||||
"event": [
|
||||
|
||||
@@ -548,6 +548,99 @@ class TestSubmissionLockView(BaseViewTest):
|
||||
assert json.loads(response.content) == {"error": ERR_UNKNOWN}
|
||||
|
||||
|
||||
class TestBatchSubmissionLockView(BaseViewTest):
|
||||
"""
|
||||
Tests for the /lock view, locking or unlocking a submission for grading
|
||||
"""
|
||||
|
||||
view_name = "ora-staff-grader:batch-unlock"
|
||||
|
||||
test_submission_uuids = [str(uuid4()) for _ in range(3)]
|
||||
test_anon_user_id = "anon-user-id"
|
||||
test_other_anon_user_id = "anon-user-id-2"
|
||||
test_timestamp = "2020-08-29T02:14:00-04:00"
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Batch unlock includes the ORA location in the params...
|
||||
self.test_request_params = {
|
||||
PARAM_ORA_LOCATION: self.ora_usage_key,
|
||||
}
|
||||
|
||||
# and a list of submission UUIDs in the body
|
||||
self.test_request_body = {
|
||||
"submissionUUIDs": self.test_submission_uuids
|
||||
}
|
||||
|
||||
self.log_in()
|
||||
|
||||
def batch_unlock(self, params, body):
|
||||
"""Wrapper for easier calling of 'batch_unlock'"""
|
||||
return self.client.post(self.url_with_params(params), body, format="json")
|
||||
|
||||
@patch("lms.djangoapps.ora_staff_grader.views.batch_delete_submission_locks")
|
||||
def test_batch_unlock_invalid_ora(self, mock_batch_delete):
|
||||
"""An invalid ORA returns a 400"""
|
||||
self.test_request_params[PARAM_ORA_LOCATION] = "not_a_real_location"
|
||||
|
||||
response = self.batch_unlock(self.test_request_params, self.test_request_body)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert json.loads(response.content) == {"error": ERR_BAD_ORA_LOCATION}
|
||||
mock_batch_delete.assert_not_called()
|
||||
|
||||
@patch("lms.djangoapps.ora_staff_grader.views.batch_delete_submission_locks")
|
||||
def test_batch_unlock_missing_submisison_list(self, mock_batch_delete):
|
||||
"""An invalid ORA returns a 400"""
|
||||
|
||||
response = self.batch_unlock(self.test_request_params, {})
|
||||
|
||||
assert response.status_code == 400
|
||||
assert json.loads(response.content) == {"error": ERR_MISSING_PARAM}
|
||||
mock_batch_delete.assert_not_called()
|
||||
|
||||
@patch("lms.djangoapps.ora_staff_grader.views.batch_delete_submission_locks")
|
||||
def test_batch_unlock(self, mock_batch_delete):
|
||||
"""POST tries to delete a group of submission locks. Success returns empty 200"""
|
||||
mock_batch_delete.return_value = None
|
||||
|
||||
response = self.batch_unlock(self.test_request_params, self.test_request_body)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert json.loads(response.content) == {}
|
||||
mock_batch_delete.assert_called()
|
||||
|
||||
@patch("lms.djangoapps.ora_staff_grader.views.batch_delete_submission_locks")
|
||||
def test_batch_unlock_internal_error(self, mock_batch_delete):
|
||||
"""Any internal errors to this API get surfaced as an internal error"""
|
||||
mock_batch_delete.side_effect = XBlockInternalError(
|
||||
context={"handler": "batch_delete_submission_locks"}
|
||||
)
|
||||
|
||||
response = self.batch_unlock(self.test_request_params, self.test_request_body)
|
||||
|
||||
assert response.status_code == 500
|
||||
assert json.loads(response.content) == {
|
||||
"error": ERR_INTERNAL,
|
||||
"handler": "batch_delete_submission_locks",
|
||||
}
|
||||
|
||||
@patch("lms.djangoapps.ora_staff_grader.views.batch_delete_submission_locks")
|
||||
def test_batch_unlock_generic_exception(
|
||||
self,
|
||||
mock_batch_delete,
|
||||
):
|
||||
"""In the even more unlikely event of an unhandled error, shrug exuberantly"""
|
||||
# Mock a generic error inside the API
|
||||
mock_batch_delete.side_effect = Exception()
|
||||
|
||||
response = self.batch_unlock(self.test_request_params, self.test_request_body)
|
||||
|
||||
assert response.status_code == 500
|
||||
assert json.loads(response.content) == {"error": ERR_UNKNOWN}
|
||||
|
||||
|
||||
class TestUpdateGradeView(BaseViewTest):
|
||||
"""
|
||||
Tests for updating a grade for a submission
|
||||
|
||||
@@ -7,6 +7,7 @@ from django.urls import path
|
||||
|
||||
from lms.djangoapps.ora_staff_grader.views import (
|
||||
InitializeView,
|
||||
SubmissionBatchUnlockView,
|
||||
SubmissionFetchView,
|
||||
SubmissionLockView,
|
||||
SubmissionStatusFetchView,
|
||||
@@ -20,6 +21,7 @@ app_name = "ora-staff-grader"
|
||||
urlpatterns += [
|
||||
path("mock/", include("lms.djangoapps.ora_staff_grader.mock.urls")),
|
||||
path("initialize", InitializeView.as_view(), name="initialize"),
|
||||
path("submission/batch/unlock", SubmissionBatchUnlockView.as_view(), name="batch-unlock"),
|
||||
path(
|
||||
"submission/status",
|
||||
SubmissionStatusFetchView.as_view(),
|
||||
|
||||
@@ -30,10 +30,12 @@ from lms.djangoapps.ora_staff_grader.errors import (
|
||||
InternalErrorResponse,
|
||||
LockContestedError,
|
||||
LockContestedResponse,
|
||||
MissingParamResponse,
|
||||
UnknownErrorResponse,
|
||||
XBlockInternalError,
|
||||
)
|
||||
from lms.djangoapps.ora_staff_grader.ora_api import (
|
||||
batch_delete_submission_locks,
|
||||
check_submission_lock,
|
||||
claim_submission_lock,
|
||||
delete_submission_lock,
|
||||
@@ -428,3 +430,52 @@ class SubmissionLockView(StaffGraderBaseView):
|
||||
except Exception as ex:
|
||||
log.exception(ex)
|
||||
return UnknownErrorResponse()
|
||||
|
||||
|
||||
class SubmissionBatchUnlockView(StaffGraderBaseView):
|
||||
"""
|
||||
POST delete a group of submission locks, limited to just those in the list that the user owns.
|
||||
|
||||
Params:
|
||||
- ora_location (str/UsageID): ORA location for XBlock handling
|
||||
|
||||
Body:
|
||||
- submissionUUIDs (UUID): A list of submission/team submission UUIDS to lock/unlock
|
||||
|
||||
Response: None
|
||||
|
||||
Errors:
|
||||
- MissingParamResponse (HTTP 400) for missing params
|
||||
- XBlockInternalError (HTTP 500) for an issue within ORA
|
||||
"""
|
||||
|
||||
@require_params([PARAM_ORA_LOCATION])
|
||||
def post(self, request, ora_location, *args, **kwargs):
|
||||
"""Batch delete submission locks"""
|
||||
try:
|
||||
# Validate ORA location
|
||||
UsageKey.from_string(ora_location)
|
||||
|
||||
# Pull submission UUIDs list from request body
|
||||
submission_uuids = request.data.get('submissionUUIDs')
|
||||
if not isinstance(submission_uuids, list):
|
||||
return MissingParamResponse()
|
||||
batch_delete_submission_locks(request, ora_location, submission_uuids)
|
||||
|
||||
# Return empty response
|
||||
return Response({})
|
||||
|
||||
# Catch bad ORA location
|
||||
except (InvalidKeyError, ItemNotFoundError):
|
||||
log.error(f"Bad ORA location provided: {ora_location}")
|
||||
return BadOraLocationResponse()
|
||||
|
||||
# Issues with the XBlock handlers
|
||||
except XBlockInternalError as ex:
|
||||
log.error(ex)
|
||||
return InternalErrorResponse(context=ex.context)
|
||||
|
||||
# Blanket exception handling
|
||||
except Exception as ex:
|
||||
log.exception(ex)
|
||||
return UnknownErrorResponse()
|
||||
|
||||
@@ -715,7 +715,7 @@ openedx-events==0.8.1
|
||||
# via -r requirements/edx/base.in
|
||||
openedx-filters==0.5.0
|
||||
# via -r requirements/edx/base.in
|
||||
ora2==4.0.6
|
||||
ora2==4.1.0
|
||||
# via -r requirements/edx/base.in
|
||||
packaging==21.3
|
||||
# via
|
||||
|
||||
@@ -953,7 +953,7 @@ openedx-events==0.8.1
|
||||
# via -r requirements/edx/testing.txt
|
||||
openedx-filters==0.5.0
|
||||
# via -r requirements/edx/testing.txt
|
||||
ora2==4.0.6
|
||||
ora2==4.1.0
|
||||
# via -r requirements/edx/testing.txt
|
||||
packaging==21.3
|
||||
# via
|
||||
|
||||
@@ -901,7 +901,7 @@ openedx-events==0.8.1
|
||||
# via -r requirements/edx/base.txt
|
||||
openedx-filters==0.5.0
|
||||
# via -r requirements/edx/base.txt
|
||||
ora2==4.0.6
|
||||
ora2==4.1.0
|
||||
# via -r requirements/edx/base.txt
|
||||
packaging==21.3
|
||||
# via
|
||||
|
||||
Reference in New Issue
Block a user