Skip to content

Commit 4dfc06a

Browse files
committed
feat: [AXM-297] Add progress to assignments in BlocksInfoInCourseView 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
1 parent 8944d1e commit 4dfc06a

File tree

4 files changed

+118
-1
lines changed

4 files changed

+118
-1
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""
2+
Common constants for the `course_info` API.
3+
"""
4+
5+
BLOCK_STRUCTURE_CACHE_TIMEOUT = 60 * 60 # 1 hour
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""
2+
Common utils for the `course_info` API.
3+
"""
4+
5+
import logging
6+
from typing import List, Optional, Union
7+
8+
from django.core.cache import cache
9+
10+
from lms.djangoapps.courseware.access import has_access
11+
from lms.djangoapps.grades.api import CourseGradeFactory
12+
from openedx.core.djangoapps.content.block_structure.api import get_block_structure_manager
13+
14+
log = logging.getLogger(__name__)
15+
16+
17+
def calculate_progress(
18+
user: 'User', # noqa: F821
19+
course_id: 'CourseLocator', # noqa: F821
20+
cache_timeout: int,
21+
) -> Optional[List[Union['ReadSubsectionGrade', 'ZeroSubsectionGrade']]]: # noqa: F821
22+
"""
23+
Calculate the progress of the user in the course.
24+
"""
25+
is_staff = bool(has_access(user, 'staff', course_id))
26+
27+
try:
28+
cache_key = f'course_block_structure_{str(course_id)}_{user.id}'
29+
collected_block_structure = cache.get(cache_key)
30+
if not collected_block_structure:
31+
collected_block_structure = get_block_structure_manager(course_id).get_collected()
32+
cache.set(cache_key, collected_block_structure, cache_timeout)
33+
34+
course_grade = CourseGradeFactory().read(user, collected_block_structure=collected_block_structure)
35+
36+
# recalculate course grade from visible grades (stored grade was calculated over all grades, visible or not)
37+
course_grade.update(visible_grades_only=True, has_staff_access=is_staff)
38+
subsection_grades = list(course_grade.subsection_grades.values())
39+
except Exception as err: # pylint: disable=broad-except
40+
log.warning(f'Could not get grades for the course: {course_id}, error: {err}')
41+
return []
42+
43+
return subsection_grades

lms/djangoapps/mobile_api/course_info/views.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44

55
import logging
6-
from typing import Optional, Union
6+
from typing import Dict, Optional, Union
77

88
import django
99
from django.contrib.auth import get_user_model
@@ -20,11 +20,13 @@
2020
from lms.djangoapps.courseware.courses import get_course_info_section_block
2121
from lms.djangoapps.course_goals.models import UserActivity
2222
from lms.djangoapps.course_api.blocks.views import BlocksInCourseView
23+
from lms.djangoapps.mobile_api.course_info.constants import BLOCK_STRUCTURE_CACHE_TIMEOUT
2324
from lms.djangoapps.mobile_api.course_info.serializers import (
2425
CourseInfoOverviewSerializer,
2526
CourseAccessSerializer,
2627
MobileCourseEnrollmentSerializer
2728
)
29+
from lms.djangoapps.mobile_api.course_info.utils import calculate_progress
2830
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
2931
from openedx.core.lib.api.view_utils import view_auth_classes
3032
from openedx.core.lib.xblock_utils import get_course_update_items
@@ -357,6 +359,12 @@ def list(self, request, **kwargs): # pylint: disable=W0221
357359

358360
course_info_context = {}
359361
if requested_user := self.get_requested_user(request.user, requested_username):
362+
self._extend_sequential_info_with_assignment_progress(
363+
requested_user,
364+
course_key,
365+
response.data['blocks'],
366+
)
367+
360368
course_info_context = {
361369
'user': requested_user
362370
}
@@ -380,3 +388,36 @@ def list(self, request, **kwargs): # pylint: disable=W0221
380388

381389
response.data.update(course_data)
382390
return response
391+
392+
@staticmethod
393+
def _extend_sequential_info_with_assignment_progress(
394+
requested_user: User,
395+
course_id: CourseKey,
396+
blocks_info_data: Dict[str, Dict],
397+
) -> None:
398+
"""
399+
Extends sequential xblock info with assignment's name and progress.
400+
"""
401+
subsection_grades = calculate_progress(requested_user, course_id, BLOCK_STRUCTURE_CACHE_TIMEOUT)
402+
grades_with_locations = {str(grade.location): grade for grade in subsection_grades}
403+
404+
for block_id, block_info in blocks_info_data.items():
405+
if block_info['type'] == 'sequential':
406+
grade = grades_with_locations.get(block_id)
407+
if grade:
408+
graded_total = grade.graded_total if grade.graded else None
409+
points_earned = graded_total.earned if graded_total else 0
410+
points_possible = graded_total.possible if graded_total else 0
411+
assignment_type = grade.format
412+
else:
413+
points_earned, points_possible, assignment_type = 0, 0, None
414+
415+
block_info.update(
416+
{
417+
'assignment_progress': {
418+
'assignment_type': assignment_type,
419+
'num_points_earned': points_earned,
420+
'num_points_possible': points_possible,
421+
}
422+
}
423+
)

lms/djangoapps/mobile_api/tests/test_course_info_views.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,3 +422,31 @@ def test_course_modes(self):
422422

423423
self.assertEqual(response.status_code, status.HTTP_200_OK)
424424
self.assertListEqual(response.data['course_modes'], expected_course_modes)
425+
426+
def test_extend_sequential_info_with_assignment_progress_get_only_sequential(self) -> None:
427+
response = self.verify_response(url=self.url, params={'block_types_filter': 'sequential'})
428+
429+
expected_results = (
430+
{
431+
'assignment_type': 'Lecture Sequence',
432+
'num_points_earned': 0.0,
433+
'num_points_possible': 0.0
434+
},
435+
{
436+
'assignment_type': None,
437+
'num_points_earned': 0.0,
438+
'num_points_possible': 0.0
439+
},
440+
)
441+
442+
self.assertEqual(response.status_code, status.HTTP_200_OK)
443+
for sequential_info, assignment_progress in zip(response.data['blocks'].values(), expected_results):
444+
self.assertDictEqual(sequential_info['assignment_progress'], assignment_progress)
445+
446+
@ddt.data('chapter', 'vertical', 'problem', 'video', 'html')
447+
def test_extend_sequential_info_with_assignment_progress_for_other_types(self, block_type: 'str') -> None:
448+
response = self.verify_response(url=self.url, params={'block_types_filter': block_type})
449+
450+
self.assertEqual(response.status_code, status.HTTP_200_OK)
451+
for block_info in response.data['blocks'].values():
452+
self.assertNotEqual('assignment_progress', block_info)

0 commit comments

Comments
 (0)