Skip to content

Commit

Permalink
feat: [AXM-297] Add progress to assignments in BlocksInfoInCourseView…
Browse files Browse the repository at this point in the history
… API (#2546)

* feat: [AXM-297, AXM-310] Add progress to assignments and total course progress

* feat: [AXM-297] Add progress to assignments

* style: [AXM-297] Try to fix linters (add docstrings)

* refactor: [AXM-297] Add typing, refactor methods
  • Loading branch information
KyryloKireiev committed Jul 1, 2024
1 parent 9a9ce41 commit 8accb6d
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 1 deletion.
5 changes: 5 additions & 0 deletions lms/djangoapps/mobile_api/course_info/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""
Common constants for the `course_info` API.
"""

BLOCK_STRUCTURE_CACHE_TIMEOUT = 60 * 60 # 1 hour
43 changes: 43 additions & 0 deletions lms/djangoapps/mobile_api/course_info/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""
Common utils for the `course_info` API.
"""

import logging
from typing import List, Optional, Union

from django.core.cache import cache

from lms.djangoapps.courseware.access import has_access
from lms.djangoapps.grades.api import CourseGradeFactory
from openedx.core.djangoapps.content.block_structure.api import get_block_structure_manager

log = logging.getLogger(__name__)


def calculate_progress(
user: 'User', # noqa: F821
course_id: 'CourseLocator', # noqa: F821
cache_timeout: int,
) -> Optional[List[Union['ReadSubsectionGrade', 'ZeroSubsectionGrade']]]: # noqa: F821
"""
Calculate the progress of the user in the course.
"""
is_staff = bool(has_access(user, 'staff', course_id))

try:
cache_key = f'course_block_structure_{str(course_id)}_{user.id}'
collected_block_structure = cache.get(cache_key)
if not collected_block_structure:
collected_block_structure = get_block_structure_manager(course_id).get_collected()
cache.set(cache_key, collected_block_structure, cache_timeout)

course_grade = CourseGradeFactory().read(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())
except Exception as err: # pylint: disable=broad-except
log.warning(f'Could not get grades for the course: {course_id}, error: {err}')
return []

return subsection_grades
43 changes: 42 additions & 1 deletion lms/djangoapps/mobile_api/course_info/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""

import logging
from typing import Optional, Union
from typing import Dict, Optional, Union

import django
from django.contrib.auth import get_user_model
Expand All @@ -20,11 +20,13 @@
from lms.djangoapps.courseware.courses import get_course_info_section_block
from lms.djangoapps.course_goals.models import UserActivity
from lms.djangoapps.course_api.blocks.views import BlocksInCourseView
from lms.djangoapps.mobile_api.course_info.constants import BLOCK_STRUCTURE_CACHE_TIMEOUT
from lms.djangoapps.mobile_api.course_info.serializers import (
CourseInfoOverviewSerializer,
CourseAccessSerializer,
MobileCourseEnrollmentSerializer
)
from lms.djangoapps.mobile_api.course_info.utils import calculate_progress
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.lib.api.view_utils import view_auth_classes
from openedx.core.lib.xblock_utils import get_course_update_items
Expand Down Expand Up @@ -357,6 +359,12 @@ def list(self, request, **kwargs): # pylint: disable=W0221

course_info_context = {}
if requested_user := self.get_requested_user(request.user, requested_username):
self._extend_sequential_info_with_assignment_progress(
requested_user,
course_key,
response.data['blocks'],
)

course_info_context = {
'user': requested_user
}
Expand All @@ -380,3 +388,36 @@ def list(self, request, **kwargs): # pylint: disable=W0221

response.data.update(course_data)
return response

@staticmethod
def _extend_sequential_info_with_assignment_progress(
requested_user: User,
course_id: CourseKey,
blocks_info_data: Dict[str, Dict],
) -> None:
"""
Extends sequential xblock info with assignment's name and progress.
"""
subsection_grades = calculate_progress(requested_user, course_id, BLOCK_STRUCTURE_CACHE_TIMEOUT)
grades_with_locations = {str(grade.location): grade for grade in subsection_grades}

for block_id, block_info in blocks_info_data.items():
if block_info['type'] == 'sequential':
grade = grades_with_locations.get(block_id)
if grade:
graded_total = grade.graded_total if grade.graded else None
points_earned = graded_total.earned if graded_total else 0
points_possible = graded_total.possible if graded_total else 0
assignment_type = grade.format
else:
points_earned, points_possible, assignment_type = 0, 0, None

block_info.update(
{
'assignment_progress': {
'assignment_type': assignment_type,
'num_points_earned': points_earned,
'num_points_possible': points_possible,
}
}
)
28 changes: 28 additions & 0 deletions lms/djangoapps/mobile_api/tests/test_course_info_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,3 +422,31 @@ def test_course_modes(self):

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertListEqual(response.data['course_modes'], expected_course_modes)

def test_extend_sequential_info_with_assignment_progress_get_only_sequential(self) -> None:
response = self.verify_response(url=self.url, params={'block_types_filter': 'sequential'})

expected_results = (
{
'assignment_type': 'Lecture Sequence',
'num_points_earned': 0.0,
'num_points_possible': 0.0
},
{
'assignment_type': None,
'num_points_earned': 0.0,
'num_points_possible': 0.0
},
)

self.assertEqual(response.status_code, status.HTTP_200_OK)
for sequential_info, assignment_progress in zip(response.data['blocks'].values(), expected_results):
self.assertDictEqual(sequential_info['assignment_progress'], assignment_progress)

@ddt.data('chapter', 'vertical', 'problem', 'video', 'html')
def test_extend_sequential_info_with_assignment_progress_for_other_types(self, block_type: 'str') -> None:
response = self.verify_response(url=self.url, params={'block_types_filter': block_type})

self.assertEqual(response.status_code, status.HTTP_200_OK)
for block_info in response.data['blocks'].values():
self.assertNotEqual('assignment_progress', block_info)

0 comments on commit 8accb6d

Please sign in to comment.