Commit 1b3a3a23 by Brian Jacobel Committed by GitHub

Merge pull request #14637 from edx/bjacobel/resume-outline

Add resume indicator to course outline
parents eb34d3fe 22203453
......@@ -16,6 +16,8 @@ class CourseHomePage(CoursePage):
url_path = "course/"
HEADER_RESUME_COURSE_SELECTOR = '.page-header .action-resume-course'
def is_browser_on_page(self):
return self.q(css='.course-outline').present
......@@ -32,6 +34,14 @@ class CourseHomePage(CoursePage):
bookmarks_page = BookmarksPage(self.browser, self.course_id)
bookmarks_page.visit()
def resume_course_from_header(self):
"""
Navigate to courseware using Resume Course button in the header.
"""
self.q(css=self.HEADER_RESUME_COURSE_SELECTOR).first.click()
courseware_page = CoursewarePage(self.browser, self.course_id)
courseware_page.wait_for_page()
class CourseOutlinePage(PageObject):
"""
......@@ -40,10 +50,15 @@ class CourseOutlinePage(PageObject):
url = None
SECTION_SELECTOR = '.outline-item.section:nth-of-type({0})'
SECTION_TITLES_SELECTOR = '.section-name span'
SUBSECTION_SELECTOR = SECTION_SELECTOR + ' .subsection:nth-of-type({1}) .outline-item'
SUBSECTION_TITLES_SELECTOR = SECTION_SELECTOR + ' .subsection a span:first-child'
OUTLINE_RESUME_COURSE_SELECTOR = '.outline-item .resume-right'
def __init__(self, browser, parent_page):
super(CourseOutlinePage, self).__init__(browser)
self.parent_page = parent_page
self.courseware_page = CoursewarePage(self.browser, self.parent_page.course_id)
def is_browser_on_page(self):
return self.parent_page.is_browser_on_page
......@@ -105,43 +120,34 @@ class CourseOutlinePage(PageObject):
return
# Convert list indices (start at zero) to CSS indices (start at 1)
subsection_css = (
".outline-item.section:nth-of-type({0}) .subsection:nth-of-type({1}) .outline-item"
).format(section_index + 1, subsection_index + 1)
subsection_css = self.SUBSECTION_SELECTOR.format(section_index + 1, subsection_index + 1)
# Click the subsection and ensure that the page finishes reloading
self.q(css=subsection_css).first.click()
self.courseware_page.wait_for_page()
# TODO: TNL-6546: Remove this if/visit_unified_course_view
if self.parent_page.unified_course_view:
self.courseware_page.nav.visit_unified_course_view()
self._wait_for_course_section(section_title, subsection_title)
def resume_course_from_outline(self):
"""
Navigate to courseware using Resume Course button in the header.
"""
self.q(css=self.OUTLINE_RESUME_COURSE_SELECTOR).first.click()
courseware_page = CoursewarePage(self.browser, self.parent_page.course_id)
courseware_page.wait_for_page()
def _section_titles(self):
"""
Return a list of all section titles on the page.
"""
section_css = '.section-name span'
return self.q(css=section_css).map(lambda el: el.text.strip()).results
return self.q(css=self.SECTION_TITLES_SELECTOR).map(lambda el: el.text.strip()).results
def _subsection_titles(self, section_index):
"""
Return a list of all subsection titles on the page
for the section at index `section_index` (starts at 1).
"""
# Retrieve the subsection title for the section
# Add one to the list index to get the CSS index, which starts at one
subsection_css = (
# TODO: TNL-6387: Will need to switch to this selector for subsections
# ".outline-item.section:nth-of-type({0}) .subsection span:nth-of-type(1)"
".outline-item.section:nth-of-type({0}) .subsection a"
).format(section_index)
return self.q(
css=subsection_css
).map(
subsection_css = self.SUBSECTION_TITLES_SELECTOR.format(section_index)
return self.q(css=subsection_css).map(
lambda el: el.get_attribute('innerHTML').strip()
).results
......@@ -149,7 +155,14 @@ class CourseOutlinePage(PageObject):
"""
Ensures the user navigates to the course content page with the correct section and subsection.
"""
courseware_page = CoursewarePage(self.browser, self.parent_page.course_id)
courseware_page.wait_for_page()
# TODO: TNL-6546: Remove this if/visit_unified_course_view
if self.parent_page.unified_course_view:
courseware_page.nav.visit_unified_course_view()
self.wait_for(
promise_check_func=lambda: self.courseware_page.nav.is_on_section(section_title, subsection_title),
promise_check_func=lambda: courseware_page.nav.is_on_section(section_title, subsection_title),
description="Waiting for course page with section '{0}' and subsection '{1}'".format(section_title, subsection_title)
)
......@@ -860,6 +860,16 @@ class HighLevelTabTest(UniqueCourseTest):
bookmarks_page = BookmarksPage(self.browser, self.course_id)
self.assertTrue(bookmarks_page.is_browser_on_page())
# Test "Resume Course" button from header
self.course_home_page.visit()
self.course_home_page.resume_course_from_header()
self.assertTrue(self.courseware_page.nav.is_on_section('Test Section 2', 'Test Subsection 3'))
# Test "Resume Course" button from within outline
self.course_home_page.visit()
self.course_home_page.outline.resume_course_from_outline()
self.assertTrue(self.courseware_page.nav.is_on_section('Test Section 2', 'Test Subsection 3'))
@attr('a11y')
def test_course_home_a11y(self):
self.course_home_page.visit()
......
......@@ -392,7 +392,7 @@ def course_info(request, course_id):
# Get the URL of the user's last position in order to display the 'where you were last' message
context['last_accessed_courseware_url'] = None
if SelfPacedConfiguration.current().enable_course_home_improvements:
context['last_accessed_courseware_url'] = get_last_accessed_courseware(course, request, user)
context['last_accessed_courseware_url'], _ = get_last_accessed_courseware(course, request, user)
now = datetime.now(UTC())
effective_start = _adjust_start_date_for_beta_testers(user, course, course_key)
......@@ -427,8 +427,8 @@ def course_info(request, course_id):
def get_last_accessed_courseware(course, request, user):
"""
Return the courseware module URL that the user last accessed,
or None if it cannot be found.
Returns a tuple containing the courseware module (URL, id) that the user last accessed,
or (None, None) if it cannot be found.
"""
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course.id, request.user, course, depth=2
......@@ -445,8 +445,8 @@ def get_last_accessed_courseware(course, request, user):
'chapter': chapter_module.url_name,
'section': section_module.url_name
})
return url
return None
return (url, section_module.url_name)
return (None, None)
class StaticCourseTabView(FragmentView):
......
......@@ -43,6 +43,14 @@
text-decoration: none;
}
}
&.current {
border: 1px solid $lms-active-color;
.resume-right {
@include float(right);
}
}
}
}
}
......
......@@ -27,13 +27,25 @@ from django.utils.translation import ugettext as _
</div>
<ol class="outline-item focusable" role="group" tabindex="0">
% for subsection in section.get('children') or []:
<li class="subsection" role="treeitem" tabindex="-1" aria-expanded="true">
<li
class="subsection ${ 'current' if subsection['current'] else '' }"
role="treeitem"
tabindex="-1"
aria-expanded="true"
>
<a
class="outline-item focusable"
href="${ subsection['lms_web_url'] }"
id="${ subsection['id'] }"
>
${ subsection['display_name'] }
<span>${ subsection['display_name'] }</span>
<span class="sr-only">${ _("This is your last visited course section.") }</span>
% if subsection['current']:
<span class="resume-right">
<b>${ _("Resume Course") }</b>
<span class="icon fa fa-arrow-circle-right" aria-hidden="true"></span>
</span>
%endif
</a>
</li>
% endfor
......
"""
Tests for the Course Outline view and supporting views.
"""
from mock import patch
from django.core.urlresolvers import reverse
from student.models import CourseEnrollment
......@@ -26,6 +27,7 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
chapter = ItemFactory.create(category='chapter', parent_location=course.location)
section = ItemFactory.create(category='sequential', parent_location=chapter.location)
ItemFactory.create(category='vertical', parent_location=section.location)
course.last_accessed = section.url_name
cls.courses.append(course)
......@@ -36,6 +38,7 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
section2 = ItemFactory.create(category='sequential', parent_location=chapter.location)
ItemFactory.create(category='vertical', parent_location=section.location)
ItemFactory.create(category='vertical', parent_location=section2.location)
course.last_accessed = None
@classmethod
def setUpTestData(cls):
......@@ -52,8 +55,10 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
super(TestCourseOutlinePage, self).setUp()
self.client.login(username=self.user.username, password=self.password)
def test_render(self):
@patch('openedx.features.course_experience.views.course_outline.get_last_accessed_courseware')
def test_render(self, patched_get_last_accessed):
for course in self.courses:
patched_get_last_accessed.return_value = (None, course.last_accessed)
url = reverse(
'edx.course_experience.course_home',
kwargs={
......@@ -64,6 +69,10 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase):
self.assertEqual(response.status_code, 200)
response_content = response.content.decode("utf-8")
if course.last_accessed is not None:
self.assertIn('Resume Course', response_content)
else:
self.assertNotIn('Resume Course', response_content)
for chapter in course.children:
self.assertIn(chapter.display_name, response_content)
for section in chapter.children:
......
......@@ -6,6 +6,7 @@ from django.core.context_processors import csrf
from django.template.loader import render_to_string
from courseware.courses import get_course_with_access
from lms.djangoapps.courseware.views.views import get_last_accessed_courseware
from lms.djangoapps.course_api.blocks.api import get_blocks
from opaque_keys.edx.keys import CourseKey
from web_fragments.fragment import Fragment
......@@ -18,7 +19,7 @@ class CourseOutlineFragmentView(FragmentView):
Course outline fragment to be shown in the unified course view.
"""
def populate_children(self, block, all_blocks):
def populate_children(self, block, all_blocks, course_position):
"""
For a passed block, replace each id in its children array with the full representation of that child,
which will be looked up by id in the passed all_blocks dict.
......@@ -28,8 +29,9 @@ class CourseOutlineFragmentView(FragmentView):
for i in range(len(children)):
child_id = block['children'][i]
child_detail = self.populate_children(all_blocks[child_id], all_blocks)
child_detail = self.populate_children(all_blocks[child_id], all_blocks, course_position)
block['children'][i] = child_detail
block['children'][i]['current'] = course_position == child_detail['block_id']
return block
......@@ -39,6 +41,7 @@ class CourseOutlineFragmentView(FragmentView):
"""
course_key = CourseKey.from_string(course_id)
course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True)
_, course_position = get_last_accessed_courseware(course, request, request.user)
course_usage_key = modulestore().make_course_usage_key(course_key)
all_blocks = get_blocks(
request,
......@@ -55,7 +58,7 @@ class CourseOutlineFragmentView(FragmentView):
'csrf': csrf(request)['csrf_token'],
'course': course,
# Recurse through the block tree, fleshing out each child object
'blocks': self.populate_children(course_block_tree, all_blocks['blocks'])
'blocks': self.populate_children(course_block_tree, all_blocks['blocks'], course_position)
}
html = render_to_string('course_experience/course-outline-fragment.html', context)
return Fragment(html)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment