Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [AXIMST-696] Add calculation for percentage of the completions #2523

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lms/djangoapps/course_home_api/outline/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ def get_blocks(self, block): # pylint: disable=missing-function-docstring
}
if 'special_exam_info' in self.context.get('extra_fields', []) and block.get('special_exam_info'):
serialized[block_key]['special_exam_info'] = block.get('special_exam_info').get('short_description')
if 'completion_stat' in self.context.get('extra_fields', []):
serialized[block_key]['completion_stat'] = block.get('completion_stat', {})

for child in children:
serialized.update(self.get_blocks(child))
Expand Down
120 changes: 115 additions & 5 deletions lms/djangoapps/course_home_api/outline/tests/test_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import ddt # lint-amnesty, pylint: disable=wrong-import-order
import json # lint-amnesty, pylint: disable=wrong-import-order
from completion.models import BlockCompletion
from django.conf import settings # lint-amnesty, pylint: disable=wrong-import-order
from django.urls import reverse # lint-amnesty, pylint: disable=wrong-import-order
from edx_toggles.toggles.testutils import override_waffle_flag # lint-amnesty, pylint: disable=wrong-import-order
Expand Down Expand Up @@ -489,7 +490,7 @@ def add_blocks_to_course(self):
parent_location=self.chapter.location
)
self.vertical = BlockFactory.create(
category='problem',
category='vertical',
graded=True,
has_score=True,
parent_location=self.sequential.location
Expand All @@ -500,11 +501,20 @@ def add_blocks_to_course(self):
parent_location=self.chapter.location
)
self.ungraded_vertical = BlockFactory.create(
category='problem',
category='vertical',
parent_location=self.ungraded_sequential.location
)
update_outline_from_modulestore(self.course.id)

def create_completion(self, problem, completion):
return BlockCompletion.objects.create(
user=self.user,
context_key=problem.context_key,
block_type='problem',
block_key=problem.location,
completion=completion,
)

@ddt.data(CourseMode.AUDIT, CourseMode.VERIFIED)
def test_get_authenticated_enrolled_user(self, enrollment_mode):
"""
Expand Down Expand Up @@ -594,6 +604,18 @@ def test_proctored_exam(self, mock_summary):
hide_after_due=False,
is_onboarding_exam=False,
)
vertical = BlockFactory.create(
parent=sequence,
category='vertical',
graded=True,
has_score=True,
)
BlockFactory.create(
parent=vertical,
category='problem',
graded=True,
has_score=True,
)
sequence.is_proctored_exam = True
update_outline_from_modulestore(course.id)
CourseEnrollment.enroll(self.user, course.id)
Expand All @@ -608,7 +630,7 @@ def test_proctored_exam(self, mock_summary):

exam_data = response.data['blocks'][str(sequence.location)]
assert not exam_data['complete']
assert exam_data['display_name'] == 'Test Proctored Exam'
assert exam_data['display_name'] == 'Test Proctored Exam (1 Question)'
assert exam_data['special_exam_info'] == 'My Exam'
assert exam_data['due'] is not None

Expand All @@ -623,8 +645,8 @@ def test_assignment(self):
assert response.status_code == 200

exam_data = response.data['blocks'][str(self.sequential.location)]
assert exam_data['display_name'] == 'Test (1 Question)'
assert exam_data['icon'] == 'fa-pencil-square-o'
assert exam_data['display_name'] == 'Test'
assert exam_data['icon'] is None
assert str(self.vertical.location) in exam_data['children']

ungraded_data = response.data['blocks'][str(self.ungraded_sequential.location)]
Expand Down Expand Up @@ -686,3 +708,91 @@ def test_hide_learning_sequences(self):
replace_course_outline(new_learning_seq_outline)
blocks = self.client.get(self.url).data['blocks']
assert seq_block_id not in blocks

def test_empty_blocks_complete(self):
"""
Test that the API returns the correct complete state for empty blocks.
"""
self.add_blocks_to_course()
CourseEnrollment.enroll(self.user, self.course.id)
url = reverse('course-home:course-sidebar-blocks', args=[self.course.id])
response = self.client.get(url)
assert response.status_code == 200

sequence_data = response.data['blocks'][str(self.sequential.location)]
vertical_data = response.data['blocks'][str(self.vertical.location)]
assert sequence_data['complete']
assert vertical_data['complete']

@ddt.data(True, False)
def test_blocks_complete_with_problem(self, problem_complete):
self.add_blocks_to_course()
problem = BlockFactory.create(parent=self.vertical, category='problem', graded=True, has_score=True)
CourseEnrollment.enroll(self.user, self.course.id)
self.create_completion(problem, int(problem_complete))

response = self.client.get(reverse('course-home:course-sidebar-blocks', args=[self.course.id]))

sequence_data = response.data['blocks'][str(self.sequential.location)]
vertical_data = response.data['blocks'][str(self.vertical.location)]

assert sequence_data['complete'] == problem_complete
assert vertical_data['complete'] == problem_complete

def test_blocks_completion_stat(self):
"""
Test that the API returns the correct completion statistics for the blocks.
"""
self.add_blocks_to_course()
completed_problem = BlockFactory.create(parent=self.vertical, category='problem', graded=True, has_score=True)
uncompleted_problem = BlockFactory.create(parent=self.vertical, category='problem', graded=True, has_score=True)
update_outline_from_modulestore(self.course.id)
CourseEnrollment.enroll(self.user, self.course.id)
self.create_completion(completed_problem, 1)
self.create_completion(uncompleted_problem, 0)
response = self.client.get(reverse('course-home:course-sidebar-blocks', args=[self.course.id]))

expected_sequence_completion_stat = {
'completion': 0,
'completable_children': 1,
}
expected_vertical_completion_stat = {
'completion': 1,
'completable_children': 2,
}
sequence_data = response.data['blocks'][str(self.sequential.location)]
vertical_data = response.data['blocks'][str(self.vertical.location)]

assert not sequence_data['complete']
assert not vertical_data['complete']
assert sequence_data['completion_stat'] == expected_sequence_completion_stat
assert vertical_data['completion_stat'] == expected_vertical_completion_stat

def test_blocks_completion_stat_all_problem_completed(self):
"""
Test that the API returns the correct completion statistics for the blocks when all problems are completed.
"""
self.add_blocks_to_course()
problem1 = BlockFactory.create(parent=self.vertical, category='problem', graded=True, has_score=True)
problem2 = BlockFactory.create(parent=self.vertical, category='problem', graded=True, has_score=True)
update_outline_from_modulestore(self.course.id)
CourseEnrollment.enroll(self.user, self.course.id)
self.create_completion(problem1, 1)
self.create_completion(problem2, 1)
response = self.client.get(reverse('course-home:course-sidebar-blocks', args=[self.course.id]))

expected_sequence_completion_stat = {
'completion': 1,
'completable_children': 1,
}
expected_vertical_completion_stat = {
'completion': 2,
'completable_children': 2,
}
sequence_data = response.data['blocks'][str(self.sequential.location)]
vertical_data = response.data['blocks'][str(self.vertical.location)]

assert sequence_data['complete']
assert vertical_data['complete']
assert sequence_data['completion_stat'] == expected_sequence_completion_stat
assert vertical_data['completion_stat'] == expected_vertical_completion_stat
46 changes: 37 additions & 9 deletions lms/djangoapps/course_home_api/outline/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,9 +459,7 @@ def get(self, request, *args, **kwargs):
)
if courseware_mfe_navigation_sidebar_blocks_caching_is_enabled():
course_blocks = cache.get(cache_key)
cached = course_blocks is not None
else:
cached = False
course_blocks = None

if not course_blocks:
Expand All @@ -474,15 +472,13 @@ def get(self, request, *args, **kwargs):
cache.set(cache_key, course_blocks, self.COURSE_BLOCKS_CACHE_TIMEOUT)

course_blocks = self.filter_unavailable_blocks(course_blocks, course_key)

if cached:
# If the data was cached, we need to mark the blocks as complete or not complete.
if course_blocks:
course_blocks = self.mark_complete_recursive(course_blocks)

context = self.get_serializer_context()
context.update({
'include_vertical': True,
'extra_fields': ['special_exam_info'],
'extra_fields': ['special_exam_info', 'completion_stat'],
'enable_prerequisite_block_type': True,
})

Expand All @@ -502,7 +498,7 @@ def filter_unavailable_blocks(self, course_blocks, course_key):
for section_data in course_sections:
section_data['children'] = self.get_available_sequences(
user_course_outline,
section_data.get('children', [])
section_data.get('children', ['completion'])
)
accessible_sequence_ids = {str(usage_key) for usage_key in user_course_outline.accessible_sequences}
for sequence_data in section_data['children']:
Expand All @@ -516,11 +512,43 @@ def mark_complete_recursive(self, block):
"""
if 'children' in block:
block['children'] = [self.mark_complete_recursive(child) for child in block['children']]
block['complete'] = all(child['complete'] for child in block['children'] if child['type'] != 'discussion')
completable_children = self.get_completable_children(block)
block['complete'] = completable_children == [] or all(child['complete'] for child in completable_children)
block['completion_stat'] = self.get_block_completion_stat(block, completable_children)
else:
block['complete'] = self.completions_dict.get(block['id'], False)
# If the block is a course, chapter, sequential, or vertical, without children,
# it should be completed by default.
completion = self.completions_dict.get(block['id'], 0)
block['complete'] = bool(completion) or block['type'] in ['course', 'chapter', 'sequential', 'vertical']
block['completion_stat'] = self.get_block_completion_stat(block, completable_children=[])

return block

def get_block_completion_stat(self, block, completable_children):
"""
Get the completion status of a block.
"""
block_type = block['type']

if block_type in ['course', 'chapter', 'sequential']:
completion = sum(child['complete'] for child in completable_children)
elif block_type == 'vertical':
completion = sum(child['completion_stat']['completion'] for child in completable_children)
else:
completion = self.completions_dict.get(block['id'], 0)

return {
'completion': completion,
'completable_children': len(completable_children),
}

@staticmethod
def get_completable_children(block):
"""
Get the completable children of a block.
"""
return [child for child in block.get('children', []) if child['type'] != 'discussion']

@staticmethod
def get_available_sections(user_course_outline, course_sections):
"""
Expand Down
Loading