From 047f1ea664090d6cf91073028f3ae5684a0d8db7 Mon Sep 17 00:00:00 2001
From: Fernando <fernando.gonzalez@edunext.co>
Date: Mon, 16 Oct 2023 11:21:04 -0400
Subject: [PATCH] feat: endpoint /api/ora_staff_grader/initialize upgraded

---
 openassessment/data.py                        | 42 +++++++++++++
 .../serializers/submission_list.py            | 61 +++++++++++++++++--
 .../staffgrader/staff_grader_mixin.py         | 33 +++++++---
 3 files changed, 123 insertions(+), 13 deletions(-)

diff --git a/openassessment/data.py b/openassessment/data.py
index fb8c1823a7..b1ddc7a25c 100644
--- a/openassessment/data.py
+++ b/openassessment/data.py
@@ -95,6 +95,48 @@ def map_anonymized_ids_to_usernames(anonymized_ids):
 
     return anonymous_id_to_username_mapping
 
+def map_anonymized_ids_to_emails(anonymized_ids):
+    """
+    Args:
+        anonymized_ids - list of anonymized user ids.
+    Returns:
+        dictionary, that contains mapping between anonymized user ids and
+        actual user emails.
+    """
+    User = get_user_model()
+
+    users = _use_read_replica(
+        User.objects.filter(anonymoususerid__anonymous_user_id__in=anonymized_ids)
+        .annotate(anonymous_id=F("anonymoususerid__anonymous_user_id"))
+        .values("email", "anonymous_id")
+    )
+    anonymous_id_to_email_mapping = {
+        user["anonymous_id"]: user["email"] for user in users
+    }
+    return anonymous_id_to_email_mapping
+
+
+def map_anonymized_ids_to_fullname(anonymized_ids):
+    """
+    Args:
+        anonymized_ids - list of anonymized user ids.
+    Returns:
+        dictionary, that contains mapping between anonymized user ids and
+        actual user fullname.
+    """
+    User = get_user_model()
+
+    users = _use_read_replica(
+        User.objects.filter(anonymoususerid__anonymous_user_id__in=anonymized_ids)
+        .select_related("profile")
+        .annotate(anonymous_id=F("anonymoususerid__anonymous_user_id"))
+        .values("profile__name", "anonymous_id")
+    )
+
+    anonymous_id_to_fullname_mapping = {
+        user["anonymous_id"]: user["profile__name"] for user in users
+    }
+    return anonymous_id_to_fullname_mapping
 
 class CsvWriter:
     """
diff --git a/openassessment/staffgrader/serializers/submission_list.py b/openassessment/staffgrader/serializers/submission_list.py
index 37c7944eb0..6e90de4d81 100644
--- a/openassessment/staffgrader/serializers/submission_list.py
+++ b/openassessment/staffgrader/serializers/submission_list.py
@@ -30,7 +30,9 @@ class Meta:
             'gradedBy',
             'username',
             'teamName',
-            'score'
+            'score',
+            "email",
+            "fullname",
         ]
         read_only_fields = fields
 
@@ -40,17 +42,23 @@ class Meta:
     CONTEXT_ANON_ID_TO_USERNAME = 'anonymous_id_to_username'
     CONTEXT_SUB_TO_ASSESSMENT = 'submission_uuid_to_assessment'
     CONTEXT_SUB_TO_ANON_ID = 'submission_uuid_to_student_id'
+    CONTEXT_ANON_ID_TO_EMAIL = "anonymous_id_to_email"
+    CONTEXT_ANON_ID_TO_FULLNAME = "anonymous_id_to_fullname"
 
     def _verify_required_context(self, context):
         """Verify that required individual or team context is present for serialization"""
         context_keys = set(context.keys())
 
         # Required context for individual submissions
-        required_context = set([
-            self.CONTEXT_ANON_ID_TO_USERNAME,
-            self.CONTEXT_SUB_TO_ASSESSMENT,
-            self.CONTEXT_SUB_TO_ANON_ID
-        ])
+        required_context = set(
+            [
+                self.CONTEXT_ANON_ID_TO_USERNAME,
+                self.CONTEXT_SUB_TO_ASSESSMENT,
+                self.CONTEXT_SUB_TO_ANON_ID,
+                self.CONTEXT_ANON_ID_TO_EMAIL,
+                self.CONTEXT_ANON_ID_TO_FULLNAME,
+            ]
+        )
 
         missing_context = required_context - context_keys
         if missing_context:
@@ -70,6 +78,8 @@ def __init__(self, *args, **kwargs):
     username = serializers.SerializerMethodField()
     teamName = serializers.SerializerMethodField()
     score = serializers.SerializerMethodField()
+    email = serializers.SerializerMethodField()
+    fullname = serializers.SerializerMethodField()
 
     def _get_username_from_context(self, anonymous_user_id):
         try:
@@ -85,6 +95,22 @@ def _get_anonymous_id_from_context(self, submission_uuid):
                 f"No submitter anonymous user id found for submission uuid {submission_uuid}"
             ) from e
 
+    def _get_email_from_context(self, anonymous_user_id):
+        try:
+            return self.context[self.CONTEXT_ANON_ID_TO_EMAIL][anonymous_user_id]
+        except KeyError as e:
+            raise MissingContextException(
+                f"Email not found for anonymous user id {anonymous_user_id}"
+            ) from e
+
+    def _get_fullname_from_context(self, anonymous_user_id):
+        try:
+            return self.context[self.CONTEXT_ANON_ID_TO_FULLNAME][anonymous_user_id]
+        except KeyError as e:
+            raise MissingContextException(
+                f"fullname not found for anonymous user id {anonymous_user_id}"
+            ) from e
+
     def get_dateGraded(self, workflow):
         return str(workflow.grading_completed_at)
 
@@ -99,6 +125,16 @@ def get_username(self, workflow):
             self._get_anonymous_id_from_context(workflow.identifying_uuid)
         )
 
+    def get_email(self, workflow):
+        return self._get_email_from_context(
+            self._get_anonymous_id_from_context(workflow.identifying_uuid)
+        )
+
+    def get_fullname(self, workflow):
+        return self._get_fullname_from_context(
+            self._get_anonymous_id_from_context(workflow.identifying_uuid)
+        )
+
     def get_teamName(self, workflow):  # pylint: disable=unused-argument
         # For individual submissions, this is intentionally empty
         return None
@@ -123,12 +159,16 @@ class TeamSubmissionListSerializer(SubmissionListSerializer):
     CONTEXT_SUB_TO_ASSESSMENT = 'submission_uuid_to_assessment'
     CONTEXT_SUB_TO_TEAM_ID = 'team_submission_uuid_to_team_id'
     CONTEXT_TEAM_ID_TO_TEAM_NAME = 'team_id_to_team_name'
+    CONTEXT_ANON_ID_TO_EMAIL = "anonymous_id_to_email"
+    CONTEXT_ANON_ID_TO_FULLNAME = "anonymous_id_to_fullname"
 
     REQUIRED_CONTEXT_KEYS = [
         CONTEXT_ANON_ID_TO_USERNAME,
         CONTEXT_SUB_TO_ASSESSMENT,
         CONTEXT_SUB_TO_TEAM_ID,
         CONTEXT_TEAM_ID_TO_TEAM_NAME,
+        CONTEXT_ANON_ID_TO_EMAIL,
+        CONTEXT_ANON_ID_TO_FULLNAME,
     ]
 
     def _verify_required_context(self, context):
@@ -160,6 +200,15 @@ def get_username(self, workflow):  # pylint: disable=unused-argument
         # For team submissions, this is intentionally empty
         return None
 
+    def get_email(self, workflow):  # pylint: disable=unused-argument
+        # For team submissions, this is intentionally empty
+        return None
+
+    def get_fullname(self, workflow):  # pylint: disable=unused-argument
+        # For team submissions, this is intentionally empty
+        return None
+
+
     def get_teamName(self, workflow):
         return self._get_team_name_from_context(
             self._get_team_id_from_context(workflow.identifying_uuid)
diff --git a/openassessment/staffgrader/staff_grader_mixin.py b/openassessment/staffgrader/staff_grader_mixin.py
index ae37eec386..639e3ddda1 100644
--- a/openassessment/staffgrader/staff_grader_mixin.py
+++ b/openassessment/staffgrader/staff_grader_mixin.py
@@ -15,7 +15,8 @@
 
 from openassessment.assessment.models.base import Assessment, AssessmentPart
 from openassessment.assessment.models.staff import StaffWorkflow, TeamStaffWorkflow
-from openassessment.data import map_anonymized_ids_to_usernames, OraSubmissionAnswerFactory, VersionNotFoundException
+from openassessment.data import (OraSubmissionAnswerFactory, VersionNotFoundException, map_anonymized_ids_to_emails,
+                                 map_anonymized_ids_to_fullname, map_anonymized_ids_to_usernames)
 from openassessment.staffgrader.errors.submission_lock import SubmissionLockContestedError
 from openassessment.staffgrader.models.submission_lock import SubmissionGradingLock
 from openassessment.staffgrader.serializers import (
@@ -215,7 +216,15 @@ def _get_list_workflows_serializer_context(self, staff_workflows, is_team_assign
             team_id_to_team_name = self.teams_service.get_team_names(course_id, topic_id)
 
             # Do bulk lookup for scorer anonymous ids (submitting team name is a separate lookup)
-            anonymous_id_to_username = map_anonymized_ids_to_usernames(set(workflow_scorer_ids))
+            anonymous_id_to_username = map_anonymized_ids_to_usernames(
+                set(workflow_scorer_ids)
+            )
+            anonymous_id_to_email = map_anonymized_ids_to_emails(
+                set(workflow_scorer_ids)
+            )
+            anonymous_id_to_fullname = map_anonymized_ids_to_fullname(
+                set(workflow_scorer_ids)
+            )
 
             context = {
                 'team_submission_uuid_to_team_id': team_submission_uuid_to_team_id,
@@ -233,6 +242,13 @@ def _get_list_workflows_serializer_context(self, staff_workflows, is_team_assign
             anonymous_id_to_username = map_anonymized_ids_to_usernames(
                 set(submission_uuid_to_student_id.values()) | workflow_scorer_ids
             )
+            anonymous_id_to_email = map_anonymized_ids_to_emails(
+                set(submission_uuid_to_student_id.values()) | workflow_scorer_ids
+            )
+
+            anonymous_id_to_fullname = map_anonymized_ids_to_fullname(
+                set(submission_uuid_to_student_id.values()) | workflow_scorer_ids
+            )
 
             context = {
                 'submission_uuid_to_student_id': submission_uuid_to_student_id,
@@ -242,11 +258,14 @@ def _get_list_workflows_serializer_context(self, staff_workflows, is_team_assign
         # Rubric, Criteria, and Option models
         submission_uuid_to_assessment = self.bulk_deep_fetch_assessments(staff_workflows)
 
-        context.update({
-            'anonymous_id_to_username': anonymous_id_to_username,
-            'submission_uuid_to_assessment': submission_uuid_to_assessment,
-        })
-
+        context.update(
+            {
+                "anonymous_id_to_username": anonymous_id_to_username,
+                "anonymous_id_to_email": anonymous_id_to_email,
+                "anonymous_id_to_fullname": anonymous_id_to_fullname,
+                "submission_uuid_to_assessment": submission_uuid_to_assessment,
+            }
+        )
         return context
 
     def _bulk_fetch_annotated_staff_workflows(self, is_team_assignment=False):