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 18, 2024
1 parent 0ac12ea commit e00d91e
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 70 deletions.
146 changes: 76 additions & 70 deletions futurex_openedx_extensions/dashboard/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,93 +231,65 @@ def get_enrolled_courses_count(self, obj: get_user_model) -> Any: # pylint: dis
return obj.courses_count


class CourseCertificateSerializer(ModelSerializerOptionalFields):
class CourseScoreAndCertificateSerializer(ModelSerializerOptionalFields):
"""
General Serializer for shared fields across different models.
This is a flexible serializer that can be extended for different use cases.
Course Score and Certificate Details Serializer
"""
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 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))

def get_progress(self, obj: CourseEnrollment) -> Any:
"""Return the certificate URL."""
progress_info = get_course_blocks_completion_summary( self._get_course_id(obj), self._get_user(obj))
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

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


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',
'exam_scores'
]

def __init__(self, *args: Any, **kwargs: Any):
"""Initialize the serializer."""
super().__init__(*args, **kwargs)
if not hasattr(self, '_get_user'):
raise NotImplementedError("Child class must implement '_get_user' method.")

if not hasattr(self, '_get_course_id'):
raise NotImplementedError("Child class must implement '_get_course_id' method.")

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:
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

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

def _get_course_id(self, obj: Any = None) -> CourseLocator:
"""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
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

@property
def is_exam_name_in_header(self) -> bool:
Expand All @@ -333,20 +305,28 @@ def grading_info(self) -> Dict[str, Any]:
def subsection_locations(self) -> Dict[str, Any]:
"""Get the subsection locations."""
return self._subsection_locations

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))

def get_progress(self, obj: CourseEnrollment) -> Any:
"""Return the certificate URL."""
progress_info = get_course_blocks_completion_summary( self._get_course_id(obj), self._get_user(obj))
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 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(),
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

def to_representation(self, instance: Any) -> Any:
Expand All @@ -360,32 +340,58 @@ def _extract_exam_scores(representation_item: dict[str, Any]) -> None:
representation_item[possible_key] = score[1] if score else 'no attempt'

representation = super().to_representation(instance)

_extract_exam_scores(representation)

return representation


class LearnerEnrollmentSerializer(CourseCertificateSerializer):
class LearnerDetailsForCourseSerializer(LearnerBasicDetailsSerializer, CourseScoreAndCertificateSerializer):
"""Serializer for learner details for a course."""

class Meta:
model = get_user_model()
fields = LearnerBasicDetailsSerializer.Meta.fields + CourseScoreAndCertificateSerializer.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.collect_grading_info([self._course_id])

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

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


class LearnerEnrollmentSerializer(CourseScoreAndCertificateSerializer):
"""Serializer for learner enrollments"""
course_id = serializers.SerializerMethodField()

class Meta:
model = CourseEnrollment
fields = CourseScoreAndCertificateSerializer.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"""
"""Get the course ID. Its helper method required for CourseScoreAndCertificateSerializer"""
return obj.course_id

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

def get_course_id(self, obj):
def get_course_id(self, obj) -> str:
"""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))
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 e00d91e

Please sign in to comment.