From a79f899b4b29b2e8149281ebebab04cd1efd9aaf Mon Sep 17 00:00:00 2001 From: Gregory Martin Date: Mon, 22 Jan 2018 11:41:10 -0500 Subject: [PATCH] Update resume button functionality --- .../migrations/0002_auto_20180125_1510.py | 18 ++ lms/djangoapps/completion/models.py | 33 ++++ .../completion/tests/test_models.py | 68 +++++++- .../completion/tests/test_services.py | 2 +- lms/djangoapps/completion/waffle.py | 71 +++++++- .../course-outline-fragment.html | 4 +- .../tests/views/test_course_outline.py | 160 ++++++++++++++---- openedx/features/course_experience/utils.py | 107 ++++++++++-- .../course_experience/views/course_home.py | 27 +-- 9 files changed, 423 insertions(+), 67 deletions(-) create mode 100644 lms/djangoapps/completion/migrations/0002_auto_20180125_1510.py diff --git a/lms/djangoapps/completion/migrations/0002_auto_20180125_1510.py b/lms/djangoapps/completion/migrations/0002_auto_20180125_1510.py new file mode 100644 index 0000000000..f446854de0 --- /dev/null +++ b/lms/djangoapps/completion/migrations/0002_auto_20180125_1510.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('completion', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='blockcompletion', + options={'get_latest_by': 'modified'}, + ), + ] diff --git a/lms/djangoapps/completion/models.py b/lms/djangoapps/completion/models.py index c4bd5ab6fe..6c6e240f20 100644 --- a/lms/djangoapps/completion/models.py +++ b/lms/djangoapps/completion/models.py @@ -167,6 +167,38 @@ class BlockCompletion(TimeStampedModel, models.Model): objects = BlockCompletionManager() + @classmethod + def get_course_completions(cls, user, course_key): + """ + query all completions for course/user pair + + Return value: + dict[BlockKey] = float + """ + course_block_completions = cls.objects.filter( + user=user, + course_key=course_key, + ) + # will not return if <= 0.0 + return {completion.block_key: completion.completion for completion in course_block_completions} + + @classmethod + def get_latest_block_completed(cls, user, course_key): + """ + query latest completion for course/user pair + + Return value: + obj: block completion + """ + try: + latest_modified_block_completion = cls.objects.filter( + user=user, + course_key=course_key, + ).latest() + except cls.DoesNotExist: + return + return latest_modified_block_completion + class Meta(object): index_together = [ ('course_key', 'block_type', 'user'), @@ -176,6 +208,7 @@ class BlockCompletion(TimeStampedModel, models.Model): unique_together = [ ('course_key', 'block_key', 'user') ] + get_latest_by = 'modified' def __unicode__(self): return 'BlockCompletion: {username}, {course_key}, {block_key}: {completion}'.format( diff --git a/lms/djangoapps/completion/tests/test_models.py b/lms/djangoapps/completion/tests/test_models.py index e926177a41..f3843cbda9 100644 --- a/lms/djangoapps/completion/tests/test_models.py +++ b/lms/djangoapps/completion/tests/test_models.py @@ -6,12 +6,11 @@ from __future__ import absolute_import, division, print_function, unicode_litera from django.core.exceptions import ValidationError from django.test import TestCase -from opaque_keys.edx.keys import UsageKey, CourseKey -from student.tests.factories import UserFactory, CourseEnrollmentFactory +from opaque_keys.edx.keys import CourseKey, UsageKey +from student.tests.factories import CourseEnrollmentFactory, UserFactory -from .. import models -from .. import waffle +from .. import models, waffle class PercentValidatorTestCase(TestCase): @@ -187,3 +186,64 @@ class SubmitBatchCompletionTestCase(TestCase): self.assertEqual(models.BlockCompletion.objects.count(), 1) model = models.BlockCompletion.objects.first() self.assertEqual(model.completion, 1.0) + + +class BatchCompletionMethodTests(TestCase): + + def setUp(self): + super(BatchCompletionMethodTests, self).setUp() + _overrider = waffle.waffle().override(waffle.ENABLE_COMPLETION_TRACKING, True) + _overrider.__enter__() + self.addCleanup(_overrider.__exit__, None, None, None) + + self.user = UserFactory.create() + self.other_user = UserFactory.create() + self.course_key = CourseKey.from_string("edX/MOOC101/2049_T2") + self.other_course_key = CourseKey.from_string("course-v1:ReedX+Hum110+1904") + self.block_keys = [UsageKey.from_string("i4x://edX/MOOC101/video/{}".format(number)) for number in xrange(5)] + + self.submit_faux_completions() + + def submit_faux_completions(self): + # Proper completions for the given runtime + for idx, block_key in enumerate(self.block_keys[0:3]): + models.BlockCompletion.objects.submit_completion( + user=self.user, + course_key=self.course_key, + block_key=block_key, + completion=1.0 - (0.2 * idx), + ) + + # Wrong user + for idx, block_key in enumerate(self.block_keys[2:]): + models.BlockCompletion.objects.submit_completion( + user=self.other_user, + course_key=self.course_key, + block_key=block_key, + completion=0.9 - (0.2 * idx), + ) + + # Wrong course + models.BlockCompletion.objects.submit_completion( + user=self.user, + course_key=self.other_course_key, + block_key=self.block_keys[4], + completion=0.75, + ) + + def test_get_course_completions(self): + + self.assertEqual( + models.BlockCompletion.get_course_completions(self.user, self.course_key), + { + self.block_keys[0]: 1.0, + self.block_keys[1]: 0.8, + self.block_keys[2]: 0.6, + }, + ) + + def test_get_latest_block_completed(self): + self.assertEqual( + models.BlockCompletion.get_latest_block_completed(self.user, self.course_key).block_key, + self.block_keys[2] + ) diff --git a/lms/djangoapps/completion/tests/test_services.py b/lms/djangoapps/completion/tests/test_services.py index 61d84df05b..517f502e68 100644 --- a/lms/djangoapps/completion/tests/test_services.py +++ b/lms/djangoapps/completion/tests/test_services.py @@ -64,7 +64,7 @@ class CompletionServiceTestCase(CompletionWaffleTestMixin, TestCase): self.block_keys[1]: 0.8, self.block_keys[2]: 0.6, self.block_keys[3]: 0.0, - self.block_keys[4]: 0.0, + self.block_keys[4]: 0.0 }, ) diff --git a/lms/djangoapps/completion/waffle.py b/lms/djangoapps/completion/waffle.py index d0195042cc..d86f8cd7f2 100644 --- a/lms/djangoapps/completion/waffle.py +++ b/lms/djangoapps/completion/waffle.py @@ -4,13 +4,15 @@ waffle switches for the completion app. """ from __future__ import absolute_import, division, print_function, unicode_literals -from openedx.core.djangoapps.waffle_utils import WaffleSwitchNamespace +from openedx.core.djangoapps.site_configuration.models import SiteConfiguration +from openedx.core.djangoapps.theming.helpers import get_current_site +from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace, WaffleSwitchNamespace # Namespace WAFFLE_NAMESPACE = 'completion' +WAFFLE_FLAG_NAMESPACE = WaffleFlagNamespace(name='completion') # Switches - # Full name: completion.enable_completion_tracking # Indicates whether or not to track completion of individual blocks. Keeping # this disabled will prevent creation of BlockCompletion objects in the @@ -18,9 +20,74 @@ WAFFLE_NAMESPACE = 'completion' # xblocks. ENABLE_COMPLETION_TRACKING = 'enable_completion_tracking' +# Full name completion.enable_visual_progress +# Overrides completion.enable_course_visual_progress +# Acts as a global override -- enable visual progress indicators +# sitewide. +ENABLE_VISUAL_PROGRESS = 'enable_visual_progress' + +# Full name completion.enable_course_visual_progress +# Acts as a course-by-course enabling of visual progress +# indicators, e.g. updated 'resume button' functionality +ENABLE_COURSE_VISUAL_PROGRESS = 'enable_course_visual_progress' + +# SiteConfiguration visual progress enablement +ENABLE_SITE_VISUAL_PROGRESS = 'enable_site_visual_progress' + def waffle(): """ Returns the namespaced, cached, audited Waffle class for completion. """ return WaffleSwitchNamespace(name=WAFFLE_NAMESPACE, log_prefix='completion: ') + + +def waffle_flag(): + """ + Returns the namespaced, cached, audited Waffle flags dictionary for Completion. + """ + namespace = WaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'completion: ') + return { + # By default, disable visual progress. Can be enabled on a course-by-course basis. + # And overridden site-globally by ENABLE_VISUAL_PROGRESS + ENABLE_COURSE_VISUAL_PROGRESS: CourseWaffleFlag( + namespace, + ENABLE_COURSE_VISUAL_PROGRESS, + flag_undefined_default=False + ) + } + + +def visual_progress_enabled(course_key): + """ + Exposes varia of visual progress feature. + ENABLE_COMPLETION_TRACKING, current_site.configuration, AND + enable_course_visual_progress OR enable_visual_progress + + :return: + + bool -> True if site/course/global enabled for visual progress tracking + """ + if not waffle().is_enabled(ENABLE_COMPLETION_TRACKING): + return + + try: + current_site = get_current_site() + if not current_site.configuration.get_value(ENABLE_SITE_VISUAL_PROGRESS, False): + return + except SiteConfiguration.DoesNotExist: + return + + # Site-aware global override + if not waffle().is_enabled(ENABLE_VISUAL_PROGRESS): + # Course enabled + return waffle_flag()[ENABLE_COURSE_VISUAL_PROGRESS].is_enabled(course_key) + + return True + + +def site_configuration_enabled(): + """ + Helper function to return site-aware feature switch + :return: bool -> True if site enabled for visual progress tracking + """ diff --git a/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html b/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html index 95c5d0fa40..03a69f1769 100644 --- a/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html +++ b/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html @@ -26,7 +26,7 @@ from openedx.core.djangolib.markup import HTML, Text
    % for subsection in section.get('children', []):
  1. ## Resume button (if last visited section) - % if subsection['last_accessed']: + % if subsection['resume_block']: ${ _("Resume Course") } diff --git a/openedx/features/course_experience/tests/views/test_course_outline.py b/openedx/features/course_experience/tests/views/test_course_outline.py index 14e25b25ff..ac31928cf1 100644 --- a/openedx/features/course_experience/tests/views/test_course_outline.py +++ b/openedx/features/course_experience/tests/views/test_course_outline.py @@ -4,21 +4,28 @@ Tests for the Course Outline view and supporting views. import datetime import json +from django.contrib.sites.models import Site from django.core.urlresolvers import reverse -from pyquery import PyQuery as pq +from mock import Mock, patch from six import text_type -from gating import api as lms_gating_api -from mock import patch, Mock from courseware.tests.factories import StaffFactory +from gating import api as lms_gating_api +from lms.djangoapps.completion import waffle +from lms.djangoapps.completion.models import BlockCompletion +from lms.djangoapps.completion.test_utils import CompletionWaffleTestMixin +from lms.djangoapps.course_api.blocks.transformers.milestones import MilestonesAndSpecialExamsTransformer +from milestones.tests.utils import MilestonesTestCaseMixin +from opaque_keys.edx.keys import CourseKey, UsageKey +from openedx.core.djangoapps.site_configuration.models import SiteConfiguration from openedx.core.lib.gating import api as gating_api +from pyquery import PyQuery as pq from student.models import CourseEnrollment from student.tests.factories import UserFactory +from waffle.testutils import override_switch from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory -from milestones.tests.utils import MilestonesTestCaseMixin -from lms.djangoapps.course_api.blocks.transformers.milestones import MilestonesAndSpecialExamsTransformer from .test_course_home import course_home_url @@ -145,11 +152,28 @@ class TestCourseOutlinePageWithPrerequisites(SharedModuleStoreTestCase, Mileston course.enable_subsection_gating = True course_blocks = {} with cls.store.bulk_operations(course.id): - course_blocks['chapter'] = ItemFactory.create(category='chapter', parent_location=course.location) - course_blocks['prerequisite'] = ItemFactory.create(category='sequential', parent_location=course_blocks['chapter'].location, display_name='Prerequisite Exam') - course_blocks['gated_content'] = ItemFactory.create(category='sequential', parent_location=course_blocks['chapter'].location, display_name='Gated Content') - course_blocks['prerequisite_vertical'] = ItemFactory.create(category='vertical', parent_location=course_blocks['prerequisite'].location) - course_blocks['gated_content_vertical'] = ItemFactory.create(category='vertical', parent_location=course_blocks['gated_content'].location) + course_blocks['chapter'] = ItemFactory.create( + category='chapter', + parent_location=course.location + ) + course_blocks['prerequisite'] = ItemFactory.create( + category='sequential', + parent_location=course_blocks['chapter'].location, + display_name='Prerequisite Exam' + ) + course_blocks['gated_content'] = ItemFactory.create( + category='sequential', + parent_location=course_blocks['chapter'].location, + display_name='Gated Content' + ) + course_blocks['prerequisite_vertical'] = ItemFactory.create( + category='vertical', + parent_location=course_blocks['prerequisite'].location + ) + course_blocks['gated_content_vertical'] = ItemFactory.create( + category='vertical', + parent_location=course_blocks['gated_content'].location + ) course.children = [course_blocks['chapter']] course_blocks['chapter'].children = [course_blocks['prerequisite'], course_blocks['gated_content']] course_blocks['prerequisite'].children = [course_blocks['prerequisite_vertical']] @@ -245,7 +269,7 @@ class TestCourseOutlinePageWithPrerequisites(SharedModuleStoreTestCase, Mileston self.assertIn(self.UNLOCKED, subsection.children('.sr').html()) -class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase): +class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleTestMixin): """ Test start course and resume course for the course outline view. @@ -268,6 +292,12 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase): """Set up and enroll our fake user in the course.""" cls.user = UserFactory(password=TEST_PASSWORD) CourseEnrollment.enroll(cls.user, cls.course.id) + cls.site = Site.objects.get_current() + SiteConfiguration.objects.get_or_create( + site=cls.site, + enabled=True, + values={waffle.ENABLE_SITE_VISUAL_PROGRESS: True} + ) @classmethod def create_test_course(cls): @@ -295,6 +325,7 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase): """ super(TestCourseOutlineResumeCourse, self).setUp() self.client.login(username=self.user.username, password=TEST_PASSWORD) + self.override_waffle_switch(False) def visit_sequential(self, course, chapter, sequential): """ @@ -310,6 +341,21 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase): ) self.assertEqual(200, self.client.get(last_accessed_url).status_code) + def visit_course_home(self, course, start_count=0, resume_count=0): + """ + Helper function to navigates to course home page, test for resume buttons + + :param course: course factory object + :param start_count: number of times 'Start Course' should appear + :param resume_count: number of times 'Resume Course' should appear + :return: response object + """ + response = self.client.get(course_home_url(course)) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'Start Course', count=start_count) + self.assertContains(response, 'Resume Course', count=resume_count) + return response + def test_start_course(self): """ Tests that the start course button appears when the course has never been accessed. @@ -320,13 +366,9 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase): """ course = self.course - response = self.client.get(course_home_url(course)) - self.assertEqual(response.status_code, 200) - - self.assertContains(response, 'Start Course', count=1) - self.assertContains(response, 'Resume Course', count=0) - + response = self.visit_course_home(course, start_count=1, resume_count=0) content = pq(response.content) + self.assertTrue(content('.action-resume-course').attr('href').endswith('/course/' + course.url_name)) def test_resume_course(self): @@ -338,17 +380,75 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase): # first navigate to a sequential to make it the last accessed chapter = course.children[0] sequential = chapter.children[0] + vertical = sequential.children[0] self.visit_sequential(course, chapter, sequential) # check resume course buttons - response = self.client.get(course_home_url(course)) - self.assertEqual(response.status_code, 200) - - self.assertContains(response, 'Start Course', count=0) - self.assertContains(response, 'Resume Course', count=2) - + response = self.visit_course_home(course, resume_count=2) content = pq(response.content) - self.assertTrue(content('.action-resume-course').attr('href').endswith('/sequential/' + sequential.url_name)) + self.assertTrue(content('.action-resume-course').attr('href').endswith('/vertical/' + vertical.url_name)) + + @override_switch( + '{}.{}'.format( + waffle.WAFFLE_NAMESPACE, waffle.ENABLE_VISUAL_PROGRESS + ), + active=True + ) + # @patch('lms.djangoapps.completion.waffle.site_configuration_enabled') + @patch('lms.djangoapps.completion.waffle.get_current_site') + def test_resume_course_with_completion_api(self, get_patched_current_site): + """ + Tests completion API resume button functionality + """ + self.override_waffle_switch(True) + get_patched_current_site.return_value = self.site + + # Course tree + course = self.course + course_key = CourseKey.from_string(str(course.id)) + vertical1 = course.children[0].children[0].children[0] + vertical2 = course.children[0].children[1].children[0] + + # Fake a visit to sequence1/vertical1 + block_key = UsageKey.from_string(unicode(vertical1.location)) + completion = 1.0 + BlockCompletion.objects.submit_completion( + user=self.user, + course_key=course_key, + block_key=block_key, + completion=completion + ) + + # Test for 'resume' link + response = self.visit_course_home(course, resume_count=2) + + # Test for 'resume' link URL - should be vertical 1 + content = pq(response.content) + self.assertTrue(content('.action-resume-course').attr('href').endswith('/vertical/' + vertical1.url_name)) + + # Fake a visit to sequence2/vertical2 + block_key = UsageKey.from_string(unicode(vertical2.location)) + completion = 1.0 + BlockCompletion.objects.submit_completion( + user=self.user, + course_key=course_key, + block_key=block_key, + completion=completion + ) + response = self.visit_course_home(course, resume_count=2) + + # Test for 'resume' link URL - should be vertical 2 + content = pq(response.content) + self.assertTrue(content('.action-resume-course').attr('href').endswith('/vertical/' + vertical2.url_name)) + + # visit sequential 1, make sure 'Resume Course' URL is robust against 'Last Visited' + # (even though I visited seq1/vert1, 'Resume Course' still points to seq2/vert2) + self.visit_sequential(course, course.children[0], course.children[0].children[0]) + + # Test for 'resume' link URL - should be vertical 2 (last completed block, NOT last visited) + response = self.visit_course_home(course, resume_count=2) + content = pq(response.content) + self.assertTrue(content('.action-resume-course').attr('href').endswith('/vertical/' + vertical2.url_name)) def test_resume_course_deleted_sequential(self): """ @@ -370,11 +470,7 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase): self.store.delete_item(sequential.location, self.user.id) # check resume course buttons - response = self.client.get(course_home_url(course)) - self.assertEqual(response.status_code, 200) - - self.assertContains(response, 'Start Course', count=0) - self.assertContains(response, 'Resume Course', count=2) + response = self.visit_course_home(course, resume_count=2) content = pq(response.content) self.assertTrue(content('.action-resume-course').attr('href').endswith('/sequential/' + sequential2.url_name)) @@ -399,11 +495,7 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase): self.store.delete_item(sequential.location, self.user.id) # check resume course buttons - response = self.client.get(course_home_url(course)) - self.assertEqual(response.status_code, 200) - - self.assertContains(response, 'Start Course', count=0) - self.assertContains(response, 'Resume Course', count=1) + self.visit_course_home(course, resume_count=1) class TestCourseOutlinePreview(SharedModuleStoreTestCase): diff --git a/openedx/features/course_experience/utils.py b/openedx/features/course_experience/utils.py index 639bb7972b..e8e81d3a14 100644 --- a/openedx/features/course_experience/utils.py +++ b/openedx/features/course_experience/utils.py @@ -1,10 +1,12 @@ """ Common utilities for the course experience, including course outline. """ -from opaque_keys.edx.keys import CourseKey - +from lms.djangoapps.completion.models import BlockCompletion +from lms.djangoapps.completion.waffle import visual_progress_enabled from lms.djangoapps.course_api.blocks.api import get_blocks from lms.djangoapps.course_blocks.utils import get_student_module_as_dict +from opaque_keys.edx.keys import CourseKey, UsageKey +from opaque_keys.edx.locator import BlockUsageLocator from openedx.core.djangoapps.request_cache.middleware import request_cached from xmodule.modulestore.django import modulestore @@ -35,29 +37,84 @@ def get_course_outline_block_tree(request, course_id): def set_last_accessed_default(block): """ - Set default of False for last_accessed on all blocks. + Set default of False for resume_block on all blocks. """ - block['last_accessed'] = False + block['resume_block'] = False + block['complete'] = False for child in block.get('children', []): set_last_accessed_default(child) + def mark_blocks_completed(block, user, course_key): + """ + Walk course tree, marking block completion. + Mark 'most recent completed block as 'resume_block' + + """ + + last_completed_child_position = BlockCompletion.get_latest_block_completed(user, course_key) + + if last_completed_child_position: + # Mutex w/ NOT 'course_block_completions' + recurse_mark_complete( + course_block_completions=BlockCompletion.get_course_completions(user, course_key), + latest_completion=last_completed_child_position, + block=block + ) + + def recurse_mark_complete(course_block_completions, latest_completion, block): + """ + Helper function to walk course tree dict, + marking blocks as 'complete' and 'last_complete' + + If all blocks are complete, mark parent block complete + mark parent blocks of 'last_complete' as 'last_complete' + + :param course_block_completions: dict[course_completion_object] = completion_value + :param latest_completion: course_completion_object + :param block: course_outline_root_block block object or child block + + :return: + block: course_outline_root_block block object or child block + """ + locatable_block_string = BlockUsageLocator.from_string(block['id']) + + if course_block_completions.get(locatable_block_string): + block['complete'] = True + if locatable_block_string == latest_completion.block_key: + block['resume_block'] = True + + if block.get('children'): + for idx in range(len(block['children'])): + recurse_mark_complete( + course_block_completions, + latest_completion, + block=block['children'][idx] + ) + if block['children'][idx]['resume_block'] is True: + block['resume_block'] = True + + if len([child['complete'] for child in block['children'] if child['complete']]) == len(block['children']): + block['complete'] = True + def mark_last_accessed(user, course_key, block): """ Recursively marks the branch to the last accessed block. """ block_key = block.serializer.instance student_module_dict = get_student_module_as_dict(user, course_key, block_key) + last_accessed_child_position = student_module_dict.get('position') if last_accessed_child_position and block.get('children'): - block['last_accessed'] = True + block['resume_block'] = True if last_accessed_child_position <= len(block['children']): last_accessed_child_block = block['children'][last_accessed_child_position - 1] - last_accessed_child_block['last_accessed'] = True + last_accessed_child_block['resume_block'] = True mark_last_accessed(user, course_key, last_accessed_child_block) else: - # We should be using an id in place of position for last accessed. However, while using position, if - # the child block is no longer accessible we'll use the last child. - block['children'][-1]['last_accessed'] = True + # We should be using an id in place of position for last accessed. + # However, while using position, if the child block is no longer accessible + # we'll use the last child. + block['children'][-1]['resume_block'] = True course_key = CourseKey.from_string(course_id) course_usage_key = modulestore().make_course_usage_key(course_key) @@ -67,13 +124,39 @@ def get_course_outline_block_tree(request, course_id): course_usage_key, user=request.user, nav_depth=3, - requested_fields=['children', 'display_name', 'type', 'due', 'graded', 'special_exam_info', 'show_gated_sections', 'format'], - block_types_filter=['course', 'chapter', 'sequential'] + requested_fields=[ + 'children', + 'display_name', + 'type', + 'due', + 'graded', + 'special_exam_info', + 'show_gated_sections', + 'format' + ], + block_types_filter=[ + 'course', + 'chapter', + 'sequential', + 'vertical', + 'html', + 'problem', + 'video', + 'discussion' + ] ) course_outline_root_block = all_blocks['blocks'].get(all_blocks['root'], None) if course_outline_root_block: populate_children(course_outline_root_block, all_blocks['blocks']) set_last_accessed_default(course_outline_root_block) - mark_last_accessed(request.user, course_key, course_outline_root_block) + + if visual_progress_enabled(course_key=course_key): + mark_blocks_completed( + block=course_outline_root_block, + user=request.user, + course_key=course_key + ) + else: + mark_last_accessed(request.user, course_key, course_outline_root_block) return course_outline_root_block diff --git a/openedx/features/course_experience/views/course_home.py b/openedx/features/course_experience/views/course_home.py index b7c9232706..3ce3f3595d 100644 --- a/openedx/features/course_experience/views/course_home.py +++ b/openedx/features/course_experience/views/course_home.py @@ -8,7 +8,7 @@ from django.template.loader import render_to_string from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_control from django.views.decorators.csrf import ensure_csrf_cookie -from opaque_keys.edx.keys import CourseKey +from opaque_keys.edx.keys import CourseKey, UsageKey from web_fragments.fragment import Fragment from course_modes.models import get_cosmetic_verified_display_price @@ -37,6 +37,7 @@ from .course_sock import CourseSockFragmentView from .latest_update import LatestUpdateFragmentView from .welcome_message import WelcomeMessageFragmentView + EMPTY_HANDOUTS_HTML = u'
      ' @@ -76,30 +77,32 @@ class CourseHomeFragmentView(EdxFragmentView): Returns a tuple: (has_visited_course, resume_course_url) has_visited_course: True if the user has ever visted the course, False otherwise. - resume_course_url: The URL of the last accessed block if the user has visited the course, + resume_course_url: The URL of the 'resume course' block if the user has visited the course, otherwise the URL of the course root. """ - def get_last_accessed_block(block): + def get_resume_block(block): """ - Gets the deepest block marked as 'last_accessed'. + Gets the deepest block marked as 'resume_block'. + """ - if not block['last_accessed']: + if not block['resume_block']: return None if not block.get('children'): return block + for child in block['children']: - last_accessed_block = get_last_accessed_block(child) - if last_accessed_block: - return last_accessed_block + resume_block = get_resume_block(child) + if resume_block: + return resume_block return block course_outline_root_block = get_course_outline_block_tree(request, course_id) - last_accessed_block = get_last_accessed_block(course_outline_root_block) if course_outline_root_block else None - has_visited_course = bool(last_accessed_block) - if last_accessed_block: - resume_course_url = last_accessed_block['lms_web_url'] + resume_block = get_resume_block(course_outline_root_block) if course_outline_root_block else None + has_visited_course = bool(resume_block) + if resume_block: + resume_course_url = resume_block['lms_web_url'] else: resume_course_url = course_outline_root_block['lms_web_url'] if course_outline_root_block else None