Skip to content

Commit

Permalink
feat: [AXM-40] add courses progress to enrollment endpoint (#2519)
Browse files Browse the repository at this point in the history
* fix: workaround for staticcollection introduced in e40a01c

* feat: [AXM-40] add courses progress to enrollment endpoint

* refactor: [AXM-40] add caching to improve performance

* refactor: [AXM-40] add progress only for primary course

* refactor: [AXM-40] refactor enrollment caching optimization

---------

Co-authored-by: Glib Glugovskiy <[email protected]>
  • Loading branch information
2 people authored and monteri committed Apr 26, 2024
1 parent 711a9f8 commit 50c07b5
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 6 deletions.
31 changes: 31 additions & 0 deletions lms/djangoapps/mobile_api/users/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from typing import Dict, List, Optional, Tuple

from django.core.cache import cache
from completion.exceptions import UnavailableCompletionData
from completion.utilities import get_key_to_last_completed_block
from rest_framework import serializers
Expand All @@ -17,6 +18,8 @@
from lms.djangoapps.courseware.block_render import get_block_for_descriptor
from lms.djangoapps.courseware.courses import get_current_child
from lms.djangoapps.courseware.model_data import FieldDataCache
from lms.djangoapps.grades.api import CourseGradeFactory
from openedx.core.djangoapps.content.block_structure.api import get_block_structure_manager
from openedx.features.course_duration_limits.access import get_user_course_expiration_date
from xmodule.modulestore.django import modulestore

Expand Down Expand Up @@ -154,7 +157,11 @@ class CourseEnrollmentSerializerModifiedForPrimary(CourseEnrollmentSerializer):
Adds `course_status` field into serializer data.
"""

course_status = serializers.SerializerMethodField()
progress = serializers.SerializerMethodField()

BLOCK_STRUCTURE_CACHE_TIMEOUT = 60 * 60 # 1 hour

def get_course_status(self, model: CourseEnrollment) -> Optional[Dict[str, List[str]]]:
"""
Expand Down Expand Up @@ -213,6 +220,29 @@ def _get_last_visited_block_path_and_unit_name(
path.reverse()
return path, unit_name

def get_progress(self, model: CourseEnrollment) -> Dict[str, int]:
"""
Returns the progress of the user in the course.
"""
assert isinstance(model, CourseEnrollment), f'Expected CourseEnrollment, got {type(model)}'
is_staff = bool(has_access(model.user, 'staff', model.course.id))

cache_key = f'course_block_structure_{str(model.course.id)}_{model.user.id}'
collected_block_structure = cache.get(cache_key)
if not collected_block_structure:
collected_block_structure = get_block_structure_manager(model.course.id).get_collected()
cache.set(cache_key, collected_block_structure, self.BLOCK_STRUCTURE_CACHE_TIMEOUT)

course_grade = CourseGradeFactory().read(model.user, collected_block_structure=collected_block_structure)

# recalculate course grade from visible grades (stored grade was calculated over all grades, visible or not)
course_grade.update(visible_grades_only=True, has_staff_access=is_staff)
subsection_grades = list(course_grade.subsection_grades.values())
return {
'num_points_earned': sum(map(lambda x: x.graded_total.earned if x.graded else 0, subsection_grades)),
'num_points_possible': sum(map(lambda x: x.graded_total.possible if x.graded else 0, subsection_grades)),
}

class Meta:
model = CourseEnrollment
fields = (
Expand All @@ -224,6 +254,7 @@ class Meta:
'certificate',
'course_modes',
'course_status',
'progress',
)
lookup_field = 'username'

Expand Down
16 changes: 10 additions & 6 deletions lms/djangoapps/mobile_api/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@


import logging
from functools import cached_property
from typing import List, Optional

from completion.exceptions import UnavailableCompletionData
Expand Down Expand Up @@ -324,7 +325,7 @@ class UserCourseEnrollmentsList(generics.ListAPIView):
certified).
* url: URL to the downloadable version of the certificate, if exists.
"""
queryset = CourseEnrollment.objects.all()

lookup_field = 'username'

# In Django Rest Framework v3, there is a default pagination
Expand Down Expand Up @@ -352,6 +353,13 @@ def get_serializer_class(self):
return CourseEnrollmentSerializerv05
return CourseEnrollmentSerializer

@cached_property
def queryset(self):
return CourseEnrollment.objects.all().select_related('course', 'user').filter(
user__username=self.kwargs['username'],
is_active=True
).order_by('created').reverse()

def get_queryset(self):
api_version = self.kwargs.get('api_version')
mobile_available = self.get_mobile_available_enrollments()
Expand All @@ -377,14 +385,10 @@ def get_mobile_available_enrollments(self) -> List[Optional[CourseEnrollment]]:
"""
Gets list with `CourseEnrollment` for mobile available courses.
"""
enrollments = self.queryset.filter(
user__username=self.kwargs['username'],
is_active=True
).order_by('created').reverse()
org = self.request.query_params.get('org', None)

same_org = (
enrollment for enrollment in enrollments
enrollment for enrollment in self.queryset
if enrollment.course_overview and self.is_org(org, enrollment.course_overview.org)
)
mobile_available = (
Expand Down

0 comments on commit 50c07b5

Please sign in to comment.