648 lines
23 KiB
Python
648 lines
23 KiB
Python
"""
|
|
Views for Enhanced Staff Grader
|
|
"""
|
|
# NOTE: we intentionally do broad exception checking to return a clean error shape
|
|
# pylint: disable=broad-except
|
|
|
|
# NOTE: we intentionally add extra args using @require_params
|
|
# pylint: disable=arguments-differ
|
|
import logging
|
|
|
|
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
|
|
from edx_rest_framework_extensions.auth.session.authentication import (
|
|
SessionAuthenticationAllowInactiveUser,
|
|
)
|
|
from opaque_keys import InvalidKeyError
|
|
from opaque_keys.edx.keys import UsageKey
|
|
from openassessment.xblock.config_mixin import WAFFLE_NAMESPACE, ENHANCED_STAFF_GRADER
|
|
from rest_framework.decorators import action
|
|
from rest_framework.generics import RetrieveAPIView
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.request import Request
|
|
from rest_framework.response import Response
|
|
from rest_framework.viewsets import ViewSet
|
|
from xmodule.modulestore.django import modulestore
|
|
from xmodule.modulestore.exceptions import ItemNotFoundError
|
|
|
|
from lms.djangoapps.ora_staff_grader.constants import (
|
|
PARAM_ORA_LOCATION,
|
|
PARAM_SUBMISSION_ID,
|
|
)
|
|
from lms.djangoapps.ora_staff_grader.errors import (
|
|
BadOraLocationResponse,
|
|
GradeContestedResponse,
|
|
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,
|
|
get_assessment_info,
|
|
get_submission_info,
|
|
get_submissions,
|
|
get_assessments,
|
|
submit_grade,
|
|
)
|
|
from lms.djangoapps.ora_staff_grader.serializers import (
|
|
FileListSerializer,
|
|
InitializeSerializer,
|
|
AssessmentFeedbackSerializer,
|
|
LockStatusSerializer,
|
|
StaffAssessSerializer,
|
|
SubmissionFetchSerializer,
|
|
SubmissionStatusFetchSerializer,
|
|
)
|
|
from lms.djangoapps.ora_staff_grader.utils import require_params
|
|
from openedx.core.djangoapps.content.course_overviews.api import (
|
|
get_course_overview_or_none,
|
|
)
|
|
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
|
|
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class StaffGraderBaseView(RetrieveAPIView):
|
|
"""
|
|
Base view for common auth/permission setup used across ESG views.
|
|
"""
|
|
|
|
authentication_classes = (
|
|
JwtAuthentication,
|
|
BearerAuthenticationAllowInactiveUser,
|
|
SessionAuthenticationAllowInactiveUser,
|
|
)
|
|
|
|
permission_classes = (IsAuthenticated,)
|
|
|
|
|
|
class InitializeView(StaffGraderBaseView):
|
|
"""
|
|
GET course metadata
|
|
|
|
Response: {
|
|
courseMetadata
|
|
oraMetadata
|
|
submissions
|
|
isEnabled
|
|
}
|
|
|
|
Errors:
|
|
- MissingParamResponse (HTTP 400) for missing params
|
|
- BadOraLocationResponse (HTTP 400) for bad ORA location
|
|
- XBlockInternalError (HTTP 500) for an issue with ORA
|
|
- UnknownError (HTTP 500) for other errors
|
|
"""
|
|
|
|
def _is_staff_grader_enabled(self, course_key):
|
|
""" Helper to evaluate if the staff grader flag / overrides are enabled """
|
|
# This toggle is documented on the edx-ora2 repo in openassessment/xblock/config_mixin.py
|
|
# Note: Do not copy this practice of directly using a toggle from a library.
|
|
# Instead, see docs for exposing a wrapper api:
|
|
# https://edx.readthedocs.io/projects/edx-toggles/en/latest/how_to/implement_the_right_toggle_type.html#using-other-toggles pylint: disable=line-too-long
|
|
# pylint: disable=toggle-missing-annotation
|
|
enhanced_staff_grader_flag = CourseWaffleFlag(
|
|
f"{WAFFLE_NAMESPACE}.{ENHANCED_STAFF_GRADER}",
|
|
module_name='openassessment.xblock.config_mixin'
|
|
)
|
|
return enhanced_staff_grader_flag.is_enabled(course_key)
|
|
|
|
@require_params([PARAM_ORA_LOCATION])
|
|
def get(self, request, ora_location, *args, **kwargs):
|
|
try:
|
|
init_data = {}
|
|
|
|
# Get ORA block and config (incl. rubric)
|
|
ora_usage_key = UsageKey.from_string(ora_location)
|
|
init_data["oraMetadata"] = modulestore().get_item(ora_usage_key)
|
|
|
|
# Get course metadata
|
|
course_id = str(ora_usage_key.course_key)
|
|
init_data["courseMetadata"] = get_course_overview_or_none(course_id)
|
|
|
|
# Get list of submissions for this ORA
|
|
init_data["submissions"] = get_submissions(request, ora_location)
|
|
|
|
# Is the Staff Grader enabled for this course?
|
|
init_data["isEnabled"] = self._is_staff_grader_enabled(ora_usage_key.course_key)
|
|
|
|
response_data = InitializeSerializer(init_data).data
|
|
log.info(response_data)
|
|
return Response(response_data)
|
|
|
|
# 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 in case something blows up
|
|
except Exception as ex:
|
|
log.exception(ex)
|
|
return UnknownErrorResponse()
|
|
|
|
|
|
class AssessmentFeedbackView(StaffGraderBaseView, ViewSet):
|
|
"""
|
|
View for fetching assessment feedback for a submission.
|
|
|
|
**Methods**
|
|
|
|
* (GET) `api/ora_staff_grader/assessments/feedback/from`
|
|
List all assessments received by a user (according to
|
|
their submissionUUID) in an ORA assignment.
|
|
|
|
* (GET) `api/ora_staff_grader/assessments/feedback/to`
|
|
List all assessments given by a user (according to
|
|
their submissionUUID) in an ORA assignment.
|
|
|
|
**Query Params**:
|
|
|
|
* `oraLocation` (str): ORA location for XBlock handling
|
|
* `submissionUUID` (str): The ORA submission UUID
|
|
|
|
**Response**:
|
|
|
|
{
|
|
assessments (List[dict]): [
|
|
{
|
|
"assessment_id: (str) Assessment id
|
|
"scorer_name: (str) Scorer name
|
|
"scorer_username: (str) Scorer username
|
|
"scorer_email: (str) Scorer email
|
|
"assessment_date: (str) Assessment date
|
|
"assessment_scores (List[dict]) [
|
|
{
|
|
"criterion_name: (str) Criterion name
|
|
"score_earned: (int) Score earned
|
|
"score_type: (str) Score type
|
|
}
|
|
]
|
|
"problem_step (str) Problem step (Self, Peer, or Staff)
|
|
"feedback: (str) Feedback of the assessment
|
|
}
|
|
]
|
|
}
|
|
|
|
**Errors**:
|
|
|
|
* `MissingParamResponse` (HTTP 400) for missing params
|
|
* `BadOraLocationResponse` (HTTP 400) for bad ORA location
|
|
* `XBlockInternalError` (HTTP 500) for an issue with ORA
|
|
* `UnknownError` (HTTP 500) for other errors
|
|
"""
|
|
@action(methods=["get"], detail=False, url_path="from")
|
|
@require_params([PARAM_ORA_LOCATION, PARAM_SUBMISSION_ID])
|
|
def get_from(self, request: Request, ora_location: str, submission_uuid: str, *args, **kwargs):
|
|
return self._get_assessments(request, ora_location, "list_assessments_from", submission_uuid)
|
|
|
|
@action(methods=["get"], detail=False, url_path="to")
|
|
@require_params([PARAM_ORA_LOCATION, PARAM_SUBMISSION_ID])
|
|
def get_to(self, request: Request, ora_location: str, submission_uuid: str, *args, **kwargs):
|
|
return self._get_assessments(request, ora_location, "list_assessments_to", submission_uuid)
|
|
|
|
def _get_assessments(
|
|
self, request: Request, ora_location: str, handler_name: str, submission_uuid: str
|
|
):
|
|
"""
|
|
Fetches assessment data using the given handler name.
|
|
|
|
Args:
|
|
request (Request): The Django request object.
|
|
ora_location (str): The ORA location for XBlock handling.
|
|
handler_name (str): The name of the XBlock handler to use.
|
|
submission_uuid (str): The ORA submission UUID.
|
|
|
|
Returns:
|
|
A Django response object containing serialized assessment data or an error response.
|
|
"""
|
|
try:
|
|
assessments_data = {
|
|
"assessments": get_assessments(
|
|
request, ora_location, handler_name, submission_uuid
|
|
)
|
|
}
|
|
response_data = AssessmentFeedbackSerializer(assessments_data).data
|
|
return Response(response_data)
|
|
|
|
except (InvalidKeyError, ItemNotFoundError):
|
|
log.error(f"Bad ORA location provided: {ora_location}")
|
|
return BadOraLocationResponse()
|
|
|
|
except XBlockInternalError as ex:
|
|
log.error(ex)
|
|
return InternalErrorResponse(context=ex.context)
|
|
|
|
except Exception as ex:
|
|
log.exception(ex)
|
|
return UnknownErrorResponse()
|
|
|
|
|
|
class SubmissionFetchView(StaffGraderBaseView):
|
|
"""
|
|
GET submission contents and assessment info, if any
|
|
|
|
Response: {
|
|
gradeData: {
|
|
score: (dict or None) {
|
|
pointsEarned: (int) earned points
|
|
pointsPossible: (int) possible points
|
|
}
|
|
overallFeedback: (string) overall feedback
|
|
criteria: (list of dict) [{
|
|
name: (str) name of criterion
|
|
feedback: (str) feedback for criterion
|
|
points: (int) points of selected option or None if feedback-only criterion
|
|
selectedOption: (str) name of selected option or None if feedback-only criterion
|
|
}]
|
|
}
|
|
response: {
|
|
text: (list of string), [the html content of text responses]
|
|
files: (list of dict) [{
|
|
downloadUrl: (string) file download url
|
|
description: (string) file description
|
|
name: (string) filename
|
|
}]
|
|
}
|
|
}
|
|
|
|
Errors:
|
|
- MissingParamResponse (HTTP 400) for missing params
|
|
- XBlockInternalError (HTTP 500) for an issue with ORA
|
|
- UnknownError (HTTP 500) for other errors
|
|
"""
|
|
|
|
@require_params([PARAM_ORA_LOCATION, PARAM_SUBMISSION_ID])
|
|
def get(self, request, ora_location, submission_uuid, *args, **kwargs):
|
|
try:
|
|
submission_info = get_submission_info(
|
|
request, ora_location, submission_uuid
|
|
)
|
|
assessment_info = get_assessment_info(
|
|
request, ora_location, submission_uuid
|
|
)
|
|
lock_info = check_submission_lock(request, ora_location, submission_uuid)
|
|
|
|
response_data = SubmissionFetchSerializer(
|
|
{
|
|
"submission_info": submission_info,
|
|
"assessment_info": assessment_info,
|
|
"lock_info": lock_info,
|
|
}
|
|
).data
|
|
|
|
log.info(response_data)
|
|
return Response(response_data)
|
|
|
|
# Issues with the XBlock handlers
|
|
except XBlockInternalError as ex:
|
|
log.error(ex)
|
|
return InternalErrorResponse(context=ex.context)
|
|
|
|
# Blanket exception handling in case something blows up
|
|
except Exception as ex:
|
|
log.exception(ex)
|
|
return UnknownErrorResponse()
|
|
|
|
|
|
class SubmissionStatusFetchView(StaffGraderBaseView):
|
|
"""
|
|
GET submission grade status, lock status, and grade data
|
|
|
|
Response: {
|
|
gradeStatus: (str) one of [graded, ungraded]
|
|
lockStatus: (str) one of [locked, unlocked, in-progress]
|
|
gradeData: {
|
|
score: (dict or None) {
|
|
pointsEarned: (int) earned points
|
|
pointsPossible: (int) possible points
|
|
}
|
|
overallFeedback: (string) overall feedback
|
|
criteria: (list of dict) [{
|
|
name: (str) name of criterion
|
|
feedback: (str) feedback for criterion
|
|
points: (int) points of selected option or None if feedback-only criterion
|
|
selectedOption: (str) name of selected option or None if feedback-only criterion
|
|
}]
|
|
}
|
|
}
|
|
|
|
Errors:
|
|
- MissingParamResponse (HTTP 400) for missing params
|
|
- XBlockInternalError (HTTP 500) for an issue with ORA
|
|
- UnknownError (HTTP 500) for other errors
|
|
"""
|
|
|
|
@require_params([PARAM_ORA_LOCATION, PARAM_SUBMISSION_ID])
|
|
def get(self, request, ora_location, submission_uuid, *args, **kwargs):
|
|
try:
|
|
assessment_info = get_assessment_info(
|
|
request, ora_location, submission_uuid
|
|
)
|
|
lock_info = check_submission_lock(request, ora_location, submission_uuid)
|
|
|
|
response_data = SubmissionStatusFetchSerializer(
|
|
{
|
|
"assessment_info": assessment_info,
|
|
"lock_info": lock_info,
|
|
}
|
|
).data
|
|
|
|
log.info(response_data)
|
|
return Response(response_data)
|
|
|
|
# Issues with the XBlock handlers
|
|
except XBlockInternalError as ex:
|
|
log.error(ex)
|
|
return InternalErrorResponse(context=ex.context)
|
|
|
|
# Blanket exception handling in case something blows up
|
|
except Exception as ex:
|
|
log.exception(ex)
|
|
return UnknownErrorResponse()
|
|
|
|
|
|
class SubmissionFilesFetchView(StaffGraderBaseView):
|
|
"""
|
|
GET file metadata for a submission.
|
|
|
|
Used to get updated file download links to avoid signed download link expiration
|
|
issues.
|
|
|
|
Response: {
|
|
files: [
|
|
downloadUrl (url),
|
|
description (string),
|
|
name (string),
|
|
size (bytes),
|
|
]
|
|
}
|
|
|
|
Errors:
|
|
- MissingParamResponse (HTTP 400) for missing params
|
|
- XBlockInternalError (HTTP 500) for an issue with ORA
|
|
- UnknownError (HTTP 500) for other errors
|
|
"""
|
|
|
|
@require_params([PARAM_ORA_LOCATION, PARAM_SUBMISSION_ID])
|
|
def get(self, request, ora_location, submission_uuid, *args, **kwargs):
|
|
try:
|
|
submission_info = get_submission_info(
|
|
request, ora_location, submission_uuid
|
|
)
|
|
|
|
response_data = FileListSerializer(submission_info).data
|
|
|
|
log.info(response_data)
|
|
return Response(response_data)
|
|
|
|
# Issues with the XBlock handlers
|
|
except XBlockInternalError as ex:
|
|
log.error(ex)
|
|
return InternalErrorResponse(context=ex.context)
|
|
|
|
# Blanket exception handling in case something blows up
|
|
except Exception as ex:
|
|
log.exception(ex)
|
|
return UnknownErrorResponse()
|
|
|
|
|
|
class UpdateGradeView(StaffGraderBaseView):
|
|
"""
|
|
POST submit a grade for a submission
|
|
|
|
Body: {
|
|
overallFeedback: (string) overall feedback
|
|
criteria: [
|
|
{
|
|
name: (string) name of criterion
|
|
feedback: (string, optional) feedback for criterion
|
|
selectedOption: (string) name of selected option or None if feedback-only criterion
|
|
},
|
|
... (one per criteria)
|
|
]
|
|
}
|
|
|
|
Response: {
|
|
gradeStatus: (string) - One of ['graded', 'ungraded']
|
|
lockStatus: (string) - One of ['unlocked', 'locked', 'in-progress']
|
|
gradeData: {
|
|
score: (dict or None) {
|
|
pointsEarned: (int) earned points
|
|
pointsPossible: (int) possible points
|
|
}
|
|
overallFeedback: (string) overall feedback
|
|
criteria: (list of dict) [{
|
|
name: (str) name of criterion
|
|
feedback: (str) feedback for criterion
|
|
selectedOption: (str) name of selected option or None if feedback-only criterion
|
|
}]
|
|
}
|
|
}
|
|
|
|
Errors:
|
|
- MissingParamResponse (HTTP 400) for missing params
|
|
- GradeContestedResponse (HTTP 409) for trying to submit a grade for a submission you don't have an active lock for
|
|
- XBlockInternalError (HTTP 500) for an issue with ORA
|
|
- UnknownError (HTTP 500) for other errors
|
|
"""
|
|
|
|
@require_params([PARAM_ORA_LOCATION, PARAM_SUBMISSION_ID])
|
|
def post(self, request, ora_location, submission_uuid, *args, **kwargs):
|
|
"""Update a grade"""
|
|
try:
|
|
# Reassert that we have ownership of the submission lock
|
|
lock_info = check_submission_lock(request, ora_location, submission_uuid)
|
|
if not lock_info.get("lock_status") == "in-progress":
|
|
assessment_info = get_assessment_info(
|
|
request, ora_location, submission_uuid
|
|
)
|
|
submission_status = SubmissionStatusFetchSerializer(
|
|
{
|
|
"assessment_info": assessment_info,
|
|
"lock_info": lock_info,
|
|
}
|
|
).data
|
|
log.error(f"Grade contested for submission: {submission_uuid}")
|
|
return GradeContestedResponse(context=submission_status)
|
|
|
|
# Transform grade data and submit assessment, raises on failure
|
|
context = {"submission_uuid": submission_uuid}
|
|
grade_data = StaffAssessSerializer(request.data, context=context).data
|
|
submit_grade(request, ora_location, grade_data)
|
|
|
|
# Clear the lock on the graded submission
|
|
delete_submission_lock(request, ora_location, submission_uuid)
|
|
|
|
# Return submission status info to frontend
|
|
assessment_info = get_assessment_info(
|
|
request, ora_location, submission_uuid
|
|
)
|
|
lock_info = check_submission_lock(request, ora_location, submission_uuid)
|
|
response_data = SubmissionStatusFetchSerializer(
|
|
{
|
|
"assessment_info": assessment_info,
|
|
"lock_info": lock_info,
|
|
}
|
|
).data
|
|
|
|
log.info(response_data)
|
|
return Response(response_data)
|
|
|
|
# Issues with the XBlock handlers
|
|
except XBlockInternalError as ex:
|
|
log.error(ex)
|
|
return InternalErrorResponse(context=ex.context)
|
|
|
|
# Blanket exception handling in case something blows up
|
|
except Exception as ex:
|
|
log.exception(ex)
|
|
return UnknownErrorResponse()
|
|
|
|
|
|
class SubmissionLockView(StaffGraderBaseView):
|
|
"""
|
|
POST claim a submission lock for grading
|
|
DELETE release a submission lock
|
|
|
|
Params:
|
|
- ora_location (str/UsageID): ORA location for XBlock handling
|
|
- submissionUUID (UUID): A submission to lock/unlock
|
|
|
|
Response: {
|
|
lockStatus
|
|
}
|
|
|
|
Errors:
|
|
- MissingParamResponse (HTTP 400) for missing params
|
|
- LockContestedResponse (HTTP 409) for contested lock
|
|
- XBlockInternalError (HTTP 500) for an issue with ORA
|
|
- UnknownError (HTTP 500) for other errors
|
|
"""
|
|
|
|
@require_params([PARAM_ORA_LOCATION, PARAM_SUBMISSION_ID])
|
|
def post(self, request, ora_location, submission_uuid, *args, **kwargs):
|
|
"""Claim a submission lock"""
|
|
try:
|
|
# Validate ORA location
|
|
UsageKey.from_string(ora_location)
|
|
lock_info = claim_submission_lock(request, ora_location, submission_uuid)
|
|
|
|
response_data = LockStatusSerializer(lock_info).data
|
|
log.info(response_data)
|
|
return Response(response_data)
|
|
|
|
# Catch bad ORA location
|
|
except (InvalidKeyError, ItemNotFoundError):
|
|
log.error(f"Bad ORA location provided: {ora_location}")
|
|
return BadOraLocationResponse()
|
|
|
|
# Return updated lock info on error
|
|
except LockContestedError:
|
|
lock_info = check_submission_lock(request, ora_location, submission_uuid)
|
|
lock_status = LockStatusSerializer(lock_info).data
|
|
log.error(f"Lock contested for submission: {submission_uuid}")
|
|
return LockContestedResponse(context=lock_status)
|
|
|
|
# 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()
|
|
|
|
@require_params([PARAM_ORA_LOCATION, PARAM_SUBMISSION_ID])
|
|
def delete(self, request, ora_location, submission_uuid, *args, **kwargs):
|
|
"""Clear a submission lock"""
|
|
try:
|
|
# Validate ORA location
|
|
UsageKey.from_string(ora_location)
|
|
lock_info = delete_submission_lock(request, ora_location, submission_uuid)
|
|
|
|
response_data = LockStatusSerializer(lock_info).data
|
|
log.info(response_data)
|
|
return Response(response_data)
|
|
|
|
# Catch bad ORA location
|
|
except (InvalidKeyError, ItemNotFoundError):
|
|
log.error(f"Bad ORA location provided: {ora_location}")
|
|
return BadOraLocationResponse()
|
|
|
|
# Return updated lock info on error
|
|
except LockContestedError:
|
|
lock_info = check_submission_lock(request, ora_location, submission_uuid)
|
|
lock_status = LockStatusSerializer(lock_info).data
|
|
return LockContestedResponse(context=lock_status)
|
|
|
|
# Issues with the XBlock handlers
|
|
except XBlockInternalError as ex:
|
|
log.error(ex)
|
|
return InternalErrorResponse(context=ex.context)
|
|
|
|
# Blanket exception handling in case something blows up
|
|
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()
|