Update resume button functionality

This commit is contained in:
Gregory Martin
2018-01-22 11:41:10 -05:00
parent 61306c8f10
commit a79f899b4b
9 changed files with 423 additions and 67 deletions

View File

@@ -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'},
),
]

View File

@@ -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(

View File

@@ -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]
)

View File

@@ -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
},
)

View File

@@ -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
"""

View File

@@ -26,7 +26,7 @@ from openedx.core.djangolib.markup import HTML, Text
<ol class="outline-item focusable">
% for subsection in section.get('children', []):
<li
class="subsection ${ 'current' if subsection['last_accessed'] else '' }"
class="subsection ${ 'current' if subsection['resume_block'] else '' }"
>
<a
class="outline-item focusable"
@@ -125,7 +125,7 @@ from openedx.core.djangolib.markup import HTML, Text
</div> <!-- /subsection-text -->
<div class="subsection-actions">
## Resume button (if last visited section)
% if subsection['last_accessed']:
% if subsection['resume_block']:
<span class="resume-right">
<b>${ _("Resume Course") }</b>
<span class="icon fa fa-arrow-circle-right" aria-hidden="true"></span>

View File

@@ -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):

View File

@@ -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

View File

@@ -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'<ol></ol>'
@@ -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