diff --git a/lms/djangoapps/ora_staff_grader/ora_api.py b/lms/djangoapps/ora_staff_grader/ora_api.py index 852f1a707bcf..07849c505332 100644 --- a/lms/djangoapps/ora_staff_grader/ora_api.py +++ b/lms/djangoapps/ora_staff_grader/ora_api.py @@ -12,6 +12,10 @@ These are checked (usually by checking for a {"success":false} response) and raise errors, possibly with extra context. """ import json +from http import HTTPStatus + +from rest_framework.request import Request + from lms.djangoapps.ora_staff_grader.errors import ( LockContestedError, XBlockInternalError, @@ -33,6 +37,42 @@ def get_submissions(request, usage_id): return json.loads(response.content) +def get_assessments(request: Request, usage_id: str, handler_name: str, submission_uuid: str): + """ + Get a list of assessments according to the handler name from ORA XBlock.json_handler + + * `list_assessments_to` handler + Lists all assessments given by a user (according to their submissionUUID) + in an ORA assignment. Assessments can be Self, Peer and Staff. + + * `list_assessments_from` handler + Lists all assessments received by a user (according to their submissionUUID) + in an ORA assignment. Assessments can be Self, Peer and Staff. + + Args: + request (Request): The request object. + usage_id (str): Usage ID of the XBlock for running the handler + handler_name (str): The name of the handler to call + submission_uuid (str): The ORA submission UUID + """ + data = { + "item_id": usage_id, + "submission_uuid": submission_uuid, + } + + response = call_xblock_json_handler(request, usage_id, handler_name, data) + + if response.status_code != HTTPStatus.OK: + raise XBlockInternalError(context={"handler": handler_name}) + + try: + return json.loads(response.content) + except json.JSONDecodeError as exc: + raise XBlockInternalError( + context={"handler": handler_name, "details": response.content} + ) from exc + + def get_submission_info(request, usage_id, submission_uuid): """ Get submission content from ORA 'get_submission_info' XBlock.json_handler diff --git a/lms/djangoapps/ora_staff_grader/serializers.py b/lms/djangoapps/ora_staff_grader/serializers.py index d3ffb46f1933..525fe7cc393e 100644 --- a/lms/djangoapps/ora_staff_grader/serializers.py +++ b/lms/djangoapps/ora_staff_grader/serializers.py @@ -135,11 +135,13 @@ class ScoreSerializer(serializers.Serializer): class SubmissionMetadataSerializer(serializers.Serializer): """ - Submission metadata for displaying submissions table in ESG + Submission metadata for displaying submissions table in Enhanced Staff Grader (ESG) """ submissionUUID = serializers.CharField(source="submissionUuid") username = serializers.CharField(allow_null=True) + email = serializers.CharField(allow_null=True) + fullname = serializers.CharField(allow_null=True) teamName = serializers.CharField(allow_null=True) dateSubmitted = serializers.DateTimeField() dateGraded = serializers.DateTimeField(allow_null=True) @@ -159,6 +161,57 @@ class Meta: "gradeStatus", "lockStatus", "score", + "email", + "fullname", + ] + read_only_fields = fields + + +class AssessmentScoresSerializer(serializers.Serializer): + """ + Serializer for score information associated with a specific assessment. + + This serializer is included in the `AssessmentSerializer` as a `ListField` + """ + criterion_name = serializers.CharField() + score_earned = serializers.IntegerField() + score_type = serializers.CharField() + + class Meta: + fields = [ + "criterion_name", + "score_earned", + "score_type", + ] + read_only_fields = fields + + +class AssessmentSerializer(serializers.Serializer): + """ + Serializer for the each assessment metadata in the response from the assessments + feedback endpoints (from/to) in Enhanced Staff Grader (ESG) + + This serializer is included in the `AssessmentFeedbackSerializer` as a `ListField` + """ + assessment_id = serializers.CharField() + scorer_name = serializers.CharField(allow_null=True) + scorer_username = serializers.CharField(allow_null=True) + scorer_email = serializers.CharField(allow_null=True) + assessment_date = serializers.DateTimeField() + assessment_scores = serializers.ListField(child=AssessmentScoresSerializer()) + problem_step = serializers.CharField(allow_null=True) + feedback = serializers.CharField(allow_null=True) + + class Meta: + fields = [ + "assessment_id", + "scorer_name", + "scorer_username", + "scorer_email", + "assessment_date", + "assessment_scores", + "problem_step", + "feedback", ] read_only_fields = fields @@ -190,6 +243,19 @@ def get_isEnabled(self, obj): return obj['isEnabled'] and not obj['oraMetadata'].teams_enabled +class AssessmentFeedbackSerializer(serializers.Serializer): + """ + Serializer for a list of assessments for the response from the assessments + feedback endpoints (from/to) in Enhanced Staff Grader (ESG) + """ + + assessments = serializers.ListField(child=AssessmentSerializer()) + + class Meta: + fields = ["assessments"] + read_only_fields = fields + + class UploadedFileSerializer(serializers.Serializer): """Serializer for a file uploaded as a part of a response""" diff --git a/lms/djangoapps/ora_staff_grader/tests/test_serializers.py b/lms/djangoapps/ora_staff_grader/tests/test_serializers.py index d105cd649e89..d6f4abcd5b83 100644 --- a/lms/djangoapps/ora_staff_grader/tests/test_serializers.py +++ b/lms/djangoapps/ora_staff_grader/tests/test_serializers.py @@ -241,6 +241,8 @@ class TestSubmissionMetadataSerializer(TestCase): "submissionUuid": "a", "username": "foo", "teamName": "", + 'email': "jondoes@example.com", + 'fullname': "", "dateSubmitted": "1969-07-16 13:32:00", "dateGraded": "None", "gradedBy": "", @@ -251,6 +253,8 @@ class TestSubmissionMetadataSerializer(TestCase): "b": { "submissionUuid": "b", "username": "", + 'email': "jondoes@example.com", + 'fullname': "Jhon Does", "teamName": "bar", "dateSubmitted": "1969-07-20 20:17:40", "dateGraded": "None", @@ -262,6 +266,8 @@ class TestSubmissionMetadataSerializer(TestCase): "c": { "submissionUuid": "c", "username": "baz", + 'email': "jondoes@example.com", + 'fullname': "Jhon Does", "teamName": "", "dateSubmitted": "1969-07-21 21:35:00", "dateGraded": "1969-07-24 16:44:00", @@ -293,6 +299,8 @@ def test_empty_score(self): submission = { "submissionUuid": "empty-score", "username": "WOPR", + 'email': "jhondoes@example.com", + 'fullname': "", "dateSubmitted": "1983-06-03 00:00:00", "dateGraded": None, "gradedBy": None, @@ -304,6 +312,8 @@ def test_empty_score(self): expected_output = { "submissionUUID": "empty-score", "username": "WOPR", + 'email': "jhondoes@example.com", + 'fullname': "", "teamName": None, "dateSubmitted": "1983-06-03 00:00:00", "dateGraded": None, diff --git a/lms/djangoapps/ora_staff_grader/urls.py b/lms/djangoapps/ora_staff_grader/urls.py index a678009ccfbf..1acfcfa5dcf8 100644 --- a/lms/djangoapps/ora_staff_grader/urls.py +++ b/lms/djangoapps/ora_staff_grader/urls.py @@ -3,7 +3,7 @@ """ from django.urls import include from django.urls import path - +from rest_framework.routers import DefaultRouter from lms.djangoapps.ora_staff_grader.views import ( InitializeView, @@ -13,12 +13,15 @@ SubmissionLockView, SubmissionStatusFetchView, UpdateGradeView, + AssessmentFeedbackView, ) - urlpatterns = [] app_name = "ora-staff-grader" +router = DefaultRouter() +router.register("assessments/feedback", AssessmentFeedbackView, basename="assessment-feedback") + urlpatterns += [ path("mock/", include("lms.djangoapps.ora_staff_grader.mock.urls")), path("initialize", InitializeView.as_view(), name="initialize"), @@ -33,3 +36,5 @@ path("submission/grade", UpdateGradeView.as_view(), name="update-grade"), path("submission", SubmissionFetchView.as_view(), name="fetch-submission"), ] + +urlpatterns += router.urls diff --git a/lms/djangoapps/ora_staff_grader/views.py b/lms/djangoapps/ora_staff_grader/views.py index c68bb65a3068..16c84ca27a9a 100644 --- a/lms/djangoapps/ora_staff_grader/views.py +++ b/lms/djangoapps/ora_staff_grader/views.py @@ -15,9 +15,12 @@ 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 @@ -43,11 +46,13 @@ 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, @@ -147,6 +152,102 @@ def get(self, request, ora_location, *args, **kwargs): 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