diff --git a/common/test/acceptance/fixtures/course.py b/common/test/acceptance/fixtures/course.py
index c5b1a05849..00321fd672 100644
--- a/common/test/acceptance/fixtures/course.py
+++ b/common/test/acceptance/fixtures/course.py
@@ -227,9 +227,9 @@ class CourseFixture(XBlockContainerFixture):
self._configure_course()
@property
- def course_outline(self):
+ def studio_course_outline_as_json(self):
"""
- Retrieves course outline in JSON format.
+ Retrieves Studio course outline in JSON format.
"""
url = STUDIO_BASE_URL + '/course/' + self._course_key + "?format=json"
response = self.session.get(url, headers=self.headers)
diff --git a/common/test/acceptance/pages/lms/course_home.py b/common/test/acceptance/pages/lms/course_home.py
new file mode 100644
index 0000000000..1b34680e6e
--- /dev/null
+++ b/common/test/acceptance/pages/lms/course_home.py
@@ -0,0 +1,148 @@
+"""
+LMS Course Home page object
+"""
+
+from bok_choy.page_object import PageObject
+
+from common.test.acceptance.pages.lms.course_page import CoursePage
+from common.test.acceptance.pages.lms.courseware import CoursewarePage
+
+
+class CourseHomePage(CoursePage):
+ """
+ Course home page, including course outline.
+ """
+
+ url_path = "course/"
+
+ def is_browser_on_page(self):
+ return self.q(css='.course-outline').present
+
+ def __init__(self, browser, course_id):
+ super(CourseHomePage, self).__init__(browser, course_id)
+ self.course_id = course_id
+ self.outline = CourseOutlinePage(browser, self)
+ # TODO: TNL-6546: Remove the following
+ self.unified_course_view = False
+
+
+class CourseOutlinePage(PageObject):
+ """
+ Course outline fragment of page.
+ """
+
+ url = None
+
+ 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
+
+ @property
+ def sections(self):
+ """
+ Return a dictionary representation of sections and subsections.
+
+ Example:
+
+ {
+ 'Introduction': ['Course Overview'],
+ 'Week 1': ['Lesson 1', 'Lesson 2', 'Homework']
+ 'Final Exam': ['Final Exam']
+ }
+
+ You can use these titles in `go_to_section` to navigate to the section.
+ """
+ # Dict to store the result
+ outline_dict = dict()
+
+ section_titles = self._section_titles()
+
+ # Get the section titles for each chapter
+ for sec_index, sec_title in enumerate(section_titles):
+
+ if len(section_titles) < 1:
+ self.warning("Could not find subsections for '{0}'".format(sec_title))
+ else:
+ # Add one to convert list index (starts at 0) to CSS index (starts at 1)
+ outline_dict[sec_title] = self._subsection_titles(sec_index + 1)
+
+ return outline_dict
+
+ def go_to_section(self, section_title, subsection_title):
+ """
+ Go to the section in the courseware.
+ Every section must have at least one subsection, so specify
+ both the section and subsection title.
+
+ Example:
+ go_to_section("Week 1", "Lesson 1")
+ """
+
+ # Get the section by index
+ try:
+ section_index = self._section_titles().index(section_title)
+ except ValueError:
+ self.warning("Could not find section '{0}'".format(section_title))
+ return
+
+ # Get the subsection by index
+ try:
+ subsection_index = self._subsection_titles(section_index + 1).index(subsection_title)
+ except ValueError:
+ msg = "Could not find subsection '{0}' in section '{1}'".format(subsection_title, section_title)
+ self.warning(msg)
+ 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)
+
+ # 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 _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
+
+ 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(
+ lambda el: el.get_attribute('innerHTML').strip()
+ ).results
+
+ def _wait_for_course_section(self, section_title, subsection_title):
+ """
+ Ensures the user navigates to the course content page with the correct section and subsection.
+ """
+ self.wait_for(
+ promise_check_func=lambda: self.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)
+ )
diff --git a/common/test/acceptance/pages/lms/course_nav.py b/common/test/acceptance/pages/lms/course_nav.py
deleted file mode 100644
index 154e1fb2f0..0000000000
--- a/common/test/acceptance/pages/lms/course_nav.py
+++ /dev/null
@@ -1,212 +0,0 @@
-"""
-Course navigation page object
-"""
-
-import re
-from bok_choy.page_object import PageObject, unguarded
-from bok_choy.promise import EmptyPromise
-
-
-class CourseNavPage(PageObject):
- """
- Navigate sections and sequences in the courseware.
- """
-
- url = None
-
- def is_browser_on_page(self):
- return self.q(css='div.course-index').present
-
- @property
- def sections(self):
- """
- Return a dictionary representation of sections and subsections.
-
- Example:
-
- {
- 'Introduction': ['Course Overview'],
- 'Week 1': ['Lesson 1', 'Lesson 2', 'Homework']
- 'Final Exam': ['Final Exam']
- }
-
- You can use these titles in `go_to_section` to navigate to the section.
- """
- # Dict to store the result
- nav_dict = dict()
-
- section_titles = self._section_titles()
-
- # Get the section titles for each chapter
- for sec_index, sec_title in enumerate(section_titles):
-
- if len(section_titles) < 1:
- self.warning("Could not find subsections for '{0}'".format(sec_title))
- else:
- # Add one to convert list index (starts at 0) to CSS index (starts at 1)
- nav_dict[sec_title] = self._subsection_titles(sec_index + 1)
-
- return nav_dict
-
- @property
- def sequence_items(self):
- """
- Return a list of sequence items on the page.
- Sequence items are one level below subsections in the course nav.
-
- Example return value:
- ['Chemical Bonds Video', 'Practice Problems', 'Homework']
- """
- seq_css = 'ol#sequence-list>li>.nav-item>.sequence-tooltip'
- return self.q(css=seq_css).map(self._clean_seq_titles).results
-
- def go_to_section(self, section_title, subsection_title):
- """
- Go to the section in the courseware.
- Every section must have at least one subsection, so specify
- both the section and subsection title.
-
- Example:
- go_to_section("Week 1", "Lesson 1")
- """
-
- # For test stability, disable JQuery animations (opening / closing menus)
- self.browser.execute_script("jQuery.fx.off = true;")
-
- # Get the section by index
- try:
- sec_index = self._section_titles().index(section_title)
- except ValueError:
- self.warning("Could not find section '{0}'".format(section_title))
- return
-
- # Click the section to ensure it's open (no harm in clicking twice if it's already open)
- # Add one to convert from list index to CSS index
- section_css = '.course-navigation .chapter:nth-of-type({0})'.format(sec_index + 1)
- self.q(css=section_css).first.click()
-
- # Get the subsection by index
- try:
- subsec_index = self._subsection_titles(sec_index + 1).index(subsection_title)
- except ValueError:
- msg = "Could not find subsection '{0}' in section '{1}'".format(subsection_title, section_title)
- self.warning(msg)
- return
-
- # Convert list indices (start at zero) to CSS indices (start at 1)
- subsection_css = (
- ".course-navigation .chapter-content-container:nth-of-type({0}) "
- ".menu-item:nth-of-type({1})"
- ).format(sec_index + 1, subsec_index + 1)
-
- # Click the subsection and ensure that the page finishes reloading
- self.q(css=subsection_css).first.click()
- self._on_section_promise(section_title, subsection_title).fulfill()
-
- def go_to_vertical(self, vertical_title):
- """
- Within a section/subsection, navigate to the vertical with `vertical_title`.
- """
-
- # Get the index of the item in the sequence
- all_items = self.sequence_items
-
- try:
- seq_index = all_items.index(vertical_title)
-
- except ValueError:
- msg = "Could not find sequential '{0}'. Available sequentials: [{1}]".format(
- vertical_title, ", ".join(all_items)
- )
- self.warning(msg)
-
- else:
-
- # Click on the sequence item at the correct index
- # Convert the list index (starts at 0) to a CSS index (starts at 1)
- seq_css = "ol#sequence-list>li:nth-of-type({0})>.nav-item".format(seq_index + 1)
- self.q(css=seq_css).first.click()
- # Click triggers an ajax event
- self.wait_for_ajax()
-
- def _section_titles(self):
- """
- Return a list of all section titles on the page.
- """
- chapter_css = '.course-navigation .chapter .group-heading'
- return self.q(css=chapter_css).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 = (
- ".course-navigation .chapter-content-container:nth-of-type({0}) "
- ".menu-item a p:nth-of-type(1)"
- ).format(section_index)
-
- # If the element is visible, we can get its text directly
- # Otherwise, we need to get the HTML
- # It *would* make sense to always get the HTML, but unfortunately
- # the open tab has some child tags that we don't want.
- return self.q(
- css=subsection_css
- ).map(
- lambda el: el.text.strip().split('\n')[0] if el.is_displayed() else el.get_attribute('innerHTML').strip()
- ).results
-
- def _on_section_promise(self, section_title, subsection_title):
- """
- Return a `Promise` that is fulfilled when the user is on
- the correct section and subsection.
- """
- desc = "currently at section '{0}' and subsection '{1}'".format(section_title, subsection_title)
- return EmptyPromise(
- lambda: self.is_on_section(section_title, subsection_title), desc
- )
-
- @unguarded
- def is_on_section(self, section_title, subsection_title):
- """
- Return a boolean indicating whether the user is on the section and subsection
- with the specified titles.
-
- This assumes that the currently expanded section is the one we're on
- That's true right after we click the section/subsection, but not true in general
- (the user could go to a section, then expand another tab).
- """
- current_section_list = self.q(css='.course-navigation .chapter.is-open .group-heading').text
- current_subsection_list = self.q(css='.course-navigation .chapter-content-container .menu-item.active a p').text
-
- if len(current_section_list) == 0:
- self.warning("Could not find the current section")
- return False
-
- elif len(current_subsection_list) == 0:
- self.warning("Could not find current subsection")
- return False
-
- else:
- return (
- current_section_list[0].strip() == section_title and
- current_subsection_list[0].strip().split('\n')[0] == subsection_title
- )
-
- # Regular expression to remove HTML span tags from a string
- REMOVE_SPAN_TAG_RE = re.compile(r'(.+) tags that we don't want.
+ return self.q(
+ css=subsection_css
+ ).map(
+ lambda el: el.text.strip().split('\n')[0] if el.is_displayed() else el.get_attribute('innerHTML').strip()
+ ).results
+
+ # TODO: TNL-6546: Remove method, outline no longer on courseware page
+ def _on_section_promise(self, section_title, subsection_title):
+ """
+ Return a `Promise` that is fulfilled when the user is on
+ the correct section and subsection.
+ """
+ desc = "currently at section '{0}' and subsection '{1}'".format(section_title, subsection_title)
+ return EmptyPromise(
+ lambda: self.is_on_section(section_title, subsection_title), desc
+ )
+
+ def go_to_outline(self):
+ """
+ Navigates using breadcrumb to the course outline on the course home page.
+
+ Returns CourseHomePage page object.
+ """
+ # To avoid circular dependency, importing inside the function
+ from common.test.acceptance.pages.lms.course_home import CourseHomePage
+
+ course_home_page = CourseHomePage(self.browser, self.parent_page.course_id)
+ self.q(css='.path a').click()
+ course_home_page.wait_for_page()
+ return course_home_page
+
+ @unguarded
+ def is_on_section(self, section_title, subsection_title):
+ """
+ Return a boolean indicating whether the user is on the section and subsection
+ with the specified titles.
+
+ """
+ # TODO: TNL-6546: Remove if/else; always use unified_course_view version (if)
+ if self.unified_course_view:
+ # breadcrumb location of form: "SECTION_TITLE > SUBSECTION_TITLE > SEQUENTIAL_TITLE"
+ bread_crumb_current = self.q(css='.position').text
+ if len(bread_crumb_current) != 1:
+ self.warning("Could not find the current bread crumb with section and subsection.")
+ return False
+
+ return bread_crumb_current[0].strip().startswith(section_title + ' > ' + subsection_title + ' > ')
+
+ else:
+ # This assumes that the currently expanded section is the one we're on
+ # That's true right after we click the section/subsection, but not true in general
+ # (the user could go to a section, then expand another tab).
+ current_section_list = self.q(css='.course-navigation .chapter.is-open .group-heading').text
+ current_subsection_list = self.q(css='.course-navigation .chapter-content-container .menu-item.active a p').text
+
+ if len(current_section_list) == 0:
+ self.warning("Could not find the current section")
+ return False
+
+ elif len(current_subsection_list) == 0:
+ self.warning("Could not find current subsection")
+ return False
+
+ else:
+ return (
+ current_section_list[0].strip() == section_title and
+ current_subsection_list[0].strip().split('\n')[0] == subsection_title
+ )
+
+ # Regular expression to remove HTML span tags from a string
+ REMOVE_SPAN_TAG_RE = re.compile(r'(.+)
<%block name="content">
-
+
%block>
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 444778d07f..c6431043f8 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
@@ -10,7 +10,7 @@ from django.utils.translation import ugettext as _
CourseOutlineFactory('.block-tree');
%static:require_module_async>
-
+
% for section in blocks.get('children') or []:
-
% endfor
-
+
diff --git a/openedx/features/course_experience/views/course_outline.py b/openedx/features/course_experience/views/course_outline.py
index c0d42f2fc9..855e40e37c 100644
--- a/openedx/features/course_experience/views/course_outline.py
+++ b/openedx/features/course_experience/views/course_outline.py
@@ -46,7 +46,7 @@ class CourseOutlineFragmentView(FragmentView):
user=request.user,
nav_depth=3,
requested_fields=['children', 'display_name', 'type'],
- block_types_filter=['course', 'chapter', 'vertical', 'sequential']
+ block_types_filter=['course', 'chapter', 'sequential']
)
course_block_tree = all_blocks['blocks'][all_blocks['root']] # Get the root of the block tree