Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ORA staff grader mfe endpoints upgraded (not for review yet) #2094

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 188 additions & 0 deletions openassessment/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from django.utils.translation import gettext as _
import requests

from submissions.models import Submission
from submissions import api as sub_api
from submissions.errors import SubmissionNotFoundError
from openassessment.runtime_imports.classes import import_block_structure_transformers, import_external_id
Expand All @@ -27,6 +28,7 @@
from openassessment.assessment.models import Assessment, AssessmentFeedback, AssessmentPart
from openassessment.fileupload.api import get_download_url
from openassessment.workflow.models import AssessmentWorkflow, TeamAssessmentWorkflow
from openassessment.assessment.score_type_constants import PEER_TYPE, SELF_TYPE, STAFF_TYPE

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -95,6 +97,34 @@ def map_anonymized_ids_to_usernames(anonymized_ids):

return anonymous_id_to_username_mapping

def map_anonymized_ids_to_user_data(anonymized_ids):
"""
Args:
anonymized_ids - list of anonymized user ids.
Returns:
dict {
<anonymous_user_id> : {
'email': (str) <user.email>
'username': (str) <user.username>
'fullname': (str) <user.profile.name>
}
}
"""
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("username", "email", "profile__name", "anonymous_id")

anonymous_id_to_user_info_mapping = {
user["anonymous_id"]: {
"username": user["username"],
"email": user["email"],
"fullname": user["profile__name"]
} for user in users
}
return anonymous_id_to_user_info_mapping

class CsvWriter:
"""
Expand Down Expand Up @@ -1535,3 +1565,161 @@ def get_file_uploads(self, missing_blank=False):
files.append(file_upload)
self.file_uploads = files
return self.file_uploads


def score_type_to_string(score_type):
"""
Converts the given score type into its string representation.

Args:
score_type <str>: System representation of the score type.

Returns:
<str>: string representation of score_type as needed in Staff Grader Template.
"""
SCORE_TYPE_MAP = {
PEER_TYPE: "Peer",
SELF_TYPE: "Self",
STAFF_TYPE: "Staff",
}
return SCORE_TYPE_MAP.get(score_type, "Unknown")

def parts_summary(assessment_obj):
"""
Retrieves a summary of the parts from a given assessment object.

Args:
assessment_obj: assessment object.

Returns:
list[dict]: A list containing assessment parts summary data dictionaries.
"""
return [
{
'criterion_name': part.criterion.name,
'score_earned': part.points_earned,
'score_type': part.option.name if part.option else "None",
}
for part in assessment_obj.parts.all()
]

def get_scorer_data(anonymous_scorer_id, user_data_mapping):
"""
Retrieves the scorer's data (full name, username, and email) based on their anonymous ID.

Args:
anonymous_scorer_id <str>: Scorer's anonymous_user_id.
user_data_mapping: User data by anonymous_user_id
dict {
<anonymous_user_id> : {
'email': (str) <user.email>
'username': (str) <user.username>
'fullname': (str) <user.profile.name>
}
}

Returns:
fullname, username, email <str>: user data values.
"""
scorer_data = user_data_mapping.get(anonymous_scorer_id, {})
return (
scorer_data.get('fullname', ""),
scorer_data.get('username', ""),
scorer_data.get('email', "")
)

def generate_assessment_data(assessment_list, user_data_mapping):
"""
Creates the list of Assessment's data dictionaries.

Args:
assessment_list: assessment objects queryset.
user_data_mapping: User data by anonymous_user_id
dict {
<anonymous_user_id> : {
'email': (str) <user.email>
'username': (str) <user.username>
'fullname': (str) <user.profile.name>
}
}

Returns:
list[dict]: A list containing assessment data dictionaries.
"""
assessment_data_list = []
for assessment in assessment_list:

scorer_name, scorer_username, scorer_email = get_scorer_data(assessment.scorer_id, user_data_mapping)

assessment_data_list.append({
"assessment_id": str(assessment.id),
"scorer_name": scorer_name,
"scorer_username": scorer_username,
"scorer_email": scorer_email,
"assesment_date": assessment.scored_at.strftime('%d-%m-%Y'),
"assesment_scores": parts_summary(assessment),
"problem_step": score_type_to_string(assessment.score_type),
"feedback": assessment.feedback or ''
})
return assessment_data_list

def generate_received_assessment_data(submission_uuid):
"""
Generates a list of received assessments data based on the submission UUID.

Args:
submission_uuid (str, optional): The UUID of the submission. Defaults to None.

Returns:
list[dict]: A list containing assessment data dictionaries.
"""


submission = sub_api.get_submission_and_student(submission_uuid)

if not submission:
return []

assessments = _use_read_replica(
Assessment.objects.prefetch_related('parts').
prefetch_related('rubric').
filter(
submission_uuid=submission['uuid']
)
)
user_data_mapping = map_anonymized_ids_to_user_data([assessment.scorer_id for assessment in assessments])
return generate_assessment_data(assessments, user_data_mapping)


def generate_given_assessment_data(item_id, submission_uuid):
"""
Generates a list of given assessments data based on the submission UUID as scorer.

Args:
item_id (str): The ID of the item.
submission_uuid (str): The UUID of the submission.

Returns:
list[dict]: A list containing assessment data dictionaries.
"""
scorer_submission = sub_api.get_submission_and_student(submission_uuid)

if not scorer_submission:
return []

scorer_id = scorer_submission['student_item']['student_id']

submissions = Submission.objects.filter(student_item__item_id=item_id).values('uuid')
submission_uuids = [sub['uuid'] for sub in submissions]

if not submission_uuids or not submissions:
return []

assessments_made_by_student = _use_read_replica(
Assessment.objects.prefetch_related('parts')
.prefetch_related('rubric')
.filter(scorer_id=scorer_id, submission_uuid__in=submission_uuids)
)

user_data_mapping = map_anonymized_ids_to_user_data([assessment.scorer_id for assessment in assessments_made_by_student])
return generate_assessment_data(assessments_made_by_student, user_data_mapping)
61 changes: 55 additions & 6 deletions openassessment/staffgrader/serializers/submission_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ class Meta:
'gradedBy',
'username',
'teamName',
'score'
'score',
"email",
"fullname",
]
read_only_fields = fields

Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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)

Expand All @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
Loading