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:
Nathan Sprenkle
2022-04-04 11:22:22 -04:00
committed by GitHub
parent 681c2f1d6c
commit 3fdd3f9150
8 changed files with 332 additions and 3 deletions

View File

@@ -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})

View File

@@ -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": [

View File

@@ -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

View File

@@ -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(),

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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