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', []):
-
## 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