Skip to content

Commit

Permalink
Add exam score
Browse files Browse the repository at this point in the history
  • Loading branch information
tehreem-sadat committed Nov 15, 2024
1 parent 0ac12ea commit e0a9c68
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 84 deletions.
173 changes: 89 additions & 84 deletions futurex_openedx_extensions/dashboard/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,12 +236,20 @@ class CourseCertificateSerializer(ModelSerializerOptionalFields):
General Serializer for shared fields across different models.
This is a flexible serializer that can be extended for different use cases.
"""
exam_scores = SerializerOptionalMethodField(field_tags=['exam_scores', 'csv_export'])
certificate_available = serializers.BooleanField()
course_score = serializers.DecimalField(max_digits=5, decimal_places=2)
active_in_course = serializers.BooleanField()
progress = SerializerOptionalMethodField(field_tags=['progress', 'csv_export'])
certificate_url = SerializerOptionalMethodField(field_tags=['certificate_url', 'csv_export'])

def __init__(self, *args: Any, **kwargs: Any):
"""Initialize the serializer."""
super().__init__(*args, **kwargs)
self._is_exam_name_in_header = self.context.get('omit_subsection_name', '0') != '1'
self._grading_info: Dict[str, Any] = {}
self._subsection_locations: Dict[str, Any] = {}

def get_certificate_url(self, obj: Any) -> Any:
"""Return the certificate URL."""
return get_certificate_url(self.context.get('request'), self._get_user(obj), self._get_course_id(obj))
Expand All @@ -252,124 +260,124 @@ def get_progress(self, obj: CourseEnrollment) -> Any:
total = progress_info['complete_count'] + progress_info['incomplete_count'] + progress_info['locked_count']
return round(progress_info['complete_count'] / total, 4) if total else 0.0

def collect_grading_info(self, course_ids) -> None:
"""Collect the grading info."""
self._grading_info = {}
self._subsection_locations = {}

if not self.is_optional_field_requested('exam_scores'):
return

for course_id in course_ids:
inc = 0
grading_context = grading_context_for_course(get_course_by_id(course_id))
for assignment_type_name, subsection_infos in grading_context['all_graded_subsections_by_type'].items():
for subsection_index, subsection_info in enumerate(subsection_infos, start=1):
header_enum = f' {subsection_index}' if len(subsection_infos) > 1 else ''
header_name = f'{assignment_type_name}{header_enum}'
if self.is_exam_name_in_header:
header_name += f': {subsection_info["subsection_block"].display_name}'

index = f'{course_id}__{inc}'
self._grading_info[str(index)] = {
'header_name': header_name,
'location': str(subsection_info['subsection_block'].location),
}
self._subsection_locations[str(subsection_info['subsection_block'].location)] = str(index)
inc += 1

def get_exam_scores(self, obj: get_user_model) -> Dict[str, Tuple[float, float] | None]:
"""Return exam scores."""
result: Dict[str, Tuple[float, float] | None] = {__index: None for __index in self.grading_info}
grades = PersistentSubsectionGrade.objects.filter(
user_id=self._get_user(obj).id,
course_id=self._get_course_id(obj),
usage_key__in=self.subsection_locations.keys(),
first_attempted__isnull=False,
).values('usage_key', 'earned_all', 'possible_all')

for grade in grades:
result[self.subsection_locations[str(grade['usage_key'])]] = (grade['earned_all'], grade['possible_all'])

return result

@property
def is_exam_name_in_header(self) -> bool:
"""Check if the exam name is needed in the header."""
return self._is_exam_name_in_header

@property
def grading_info(self) -> Dict[str, Any]:
"""Get the grading info."""
return self._grading_info

@property
def subsection_locations(self) -> Dict[str, Any]:
"""Get the subsection locations."""
return self._subsection_locations

def _extract_exam_scores(self, exam_scores) -> None:
extracted_data = {}
for index, score in exam_scores.items():
earned_key = f'earned - {self.grading_info[index]["header_name"]}'
possible_key = f'possible - {self.grading_info[index]["header_name"]}'
extracted_data[earned_key] = score[0] if score else 'no attempt'
extracted_data[possible_key] = score[1] if score else 'no attempt'
return extracted_data

class Meta:
fields = [
'certificate_available',
'course_score',
'active_in_course',
'progress',
'certificate_url',
'exam_scores'
]


class LearnerDetailsForCourseSerializer(LearnerBasicDetailsSerializer, CourseCertificateSerializer):
"""Serializer for learner details for a course."""
exam_scores = SerializerOptionalMethodField(field_tags=['exam_scores', 'csv_export'])

class Meta:
model = get_user_model()
fields = LearnerBasicDetailsSerializer.Meta.fields + CourseCertificateSerializer.Meta.fields + [
'certificate_available',
'course_score',
'active_in_course',
'progress',
'certificate_url',
'exam_scores',
]
fields = LearnerBasicDetailsSerializer.Meta.fields + CourseCertificateSerializer.Meta.fields

def __init__(self, *args: Any, **kwargs: Any):
"""Initialize the serializer."""
super().__init__(*args, **kwargs)

self._course_id = CourseLocator.from_string(self.context.get('course_id'))
self._is_exam_name_in_header = self.context.get('omit_subsection_name', '0') != '1'

self._grading_info: Dict[str, Any] = {}
self._subsection_locations: Dict[str, Any] = {}
self.collect_grading_info()

def collect_grading_info(self) -> None:
"""Collect the grading info."""
self._grading_info = {}
self._subsection_locations = {}
if not self.is_optional_field_requested('exam_scores'):
return

grading_context = grading_context_for_course(get_course_by_id(self._course_id))
index = 0
for assignment_type_name, subsection_infos in grading_context['all_graded_subsections_by_type'].items():
for subsection_index, subsection_info in enumerate(subsection_infos, start=1):
header_enum = f' {subsection_index}' if len(subsection_infos) > 1 else ''
header_name = f'{assignment_type_name}{header_enum}'
if self.is_exam_name_in_header:
header_name += f': {subsection_info["subsection_block"].display_name}'

self._grading_info[str(index)] = {
'header_name': header_name,
'location': str(subsection_info['subsection_block'].location),
}
self._subsection_locations[str(subsection_info['subsection_block'].location)] = str(index)
index += 1
self.collect_grading_info([self._course_id])

def _get_course_id(self, obj: Any = None) -> CourseLocator:
"""Get the course ID. Its helper method required for CourseCertificateSerializer"""
"""Get the course ID. Its helper method required for CourseCertificateSerializer"""
return self._course_id

def _get_user(self, obj: Any = None) -> get_user_model():
"""Get the User. Its helper method required for CourseCertificateSerializer"""
return obj

@property
def is_exam_name_in_header(self) -> bool:
"""Check if the exam name is needed in the header."""
return self._is_exam_name_in_header

@property
def grading_info(self) -> Dict[str, Any]:
"""Get the grading info."""
return self._grading_info

@property
def subsection_locations(self) -> Dict[str, Any]:
"""Get the subsection locations."""
return self._subsection_locations

def get_exam_scores(self, obj: get_user_model) -> Dict[str, Tuple[float, float] | None]:
"""Return exam scores."""
result: Dict[str, Tuple[float, float] | None] = {__index: None for __index in self.grading_info}
grades = PersistentSubsectionGrade.objects.filter(
user_id=obj.id,
course_id=self._get_course_id(),
usage_key__in=self.subsection_locations.keys(),
first_attempted__isnull=False,
).values('usage_key', 'earned_all', 'possible_all')

for grade in grades:
result[self.subsection_locations[str(grade['usage_key'])]] = (grade['earned_all'], grade['possible_all'])

return result

def to_representation(self, instance: Any) -> Any:
"""Return the representation of the instance."""
def _extract_exam_scores(representation_item: dict[str, Any]) -> None:
exam_scores = representation_item.pop('exam_scores', {})
for index, score in exam_scores.items():
earned_key = f'earned - {self.grading_info[index]["header_name"]}'
possible_key = f'possible - {self.grading_info[index]["header_name"]}'
representation_item[earned_key] = score[0] if score else 'no attempt'
representation_item[possible_key] = score[1] if score else 'no attempt'

representation = super().to_representation(instance)

_extract_exam_scores(representation)

representation.update(self._extract_exam_scores(representation.pop('exam_scores', {})))
return representation


class LearnerEnrollmentSerializer(CourseCertificateSerializer):
"""Serializer for learner details for a course."""
course_id = serializers.SerializerMethodField()

class Meta:
model = CourseEnrollment
fields = CourseCertificateSerializer.Meta.fields + ['course_id']

def __init__(self, *args: Any, **kwargs: Any):
"""Initialize the serializer."""
super().__init__(*args, **kwargs)
course_ids = self.context.get('course_ids')
self.collect_grading_info(course_ids)

def _get_course_id(self, obj: Any = None) -> CourseLocator:
"""Get the course ID. Its helper method required for CourseCertificateSerializer"""
return obj.course_id
Expand All @@ -382,13 +390,10 @@ def get_course_id(self, obj):
"""Get course id"""
return str(self._get_course_id(obj))

class Meta:
model = CourseEnrollment
fields = CourseCertificateSerializer.Meta.fields + ['course_id']

def to_representation(self, instance):
representation = LearnerBasicDetailsSerializer(instance.user).data
representation.update(super().to_representation(instance))
representation.update(self._extract_exam_scores(representation.pop('exam_scores', {})))
return representation


Expand Down
7 changes: 7 additions & 0 deletions futurex_openedx_extensions/dashboard/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,13 @@ def get_queryset(self, *args: Any, **kwargs: Any) -> QuerySet:
course_ids=self.request.query_params.getlist('course_ids', None),
)

def get_serializer_context(self) -> Dict[str, Any]:
"""Get the serializer context"""
context = super().get_serializer_context()
context['course_ids'] = [course_enrollment.course_id for course_enrollment in self.get_queryset()]
context['omit_subsection_name'] = self.request.query_params.get('omit_subsection_name', '0')
return context


class GlobalRatingView(APIView, FXViewRoleInfoMixin):
"""View to get the global rating"""
Expand Down

0 comments on commit e0a9c68

Please sign in to comment.