216 lines
7.9 KiB
Python
216 lines
7.9 KiB
Python
"""
|
|
Course navigation page object
|
|
"""
|
|
|
|
import re
|
|
from bok_choy.page_object import PageObject
|
|
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>a>.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_sequential(self, sequential_title):
|
|
"""
|
|
Within a section/subsection, navigate to the sequential with `sequential_title`.
|
|
"""
|
|
|
|
# Get the index of the item in the sequence
|
|
all_items = self.sequence_items
|
|
|
|
try:
|
|
seq_index = all_items.index(sequential_title)
|
|
|
|
except ValueError:
|
|
msg = "Could not find sequential '{0}'. Available sequentials: [{1}]".format(
|
|
sequential_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})>a".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 <span> 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
|
|
)
|
|
|
|
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'</span>(.+)<span')
|
|
|
|
def _clean_seq_titles(self, element):
|
|
"""
|
|
Clean HTML of sequence titles, stripping out span tags and returning the first line.
|
|
"""
|
|
return self.REMOVE_SPAN_TAG_RE.search(element.get_attribute('innerHTML')).groups()[0].strip()
|
|
|
|
def go_to_sequential_position(self, sequential_position):
|
|
"""
|
|
Within a section/subsection navigate to the sequential position specified by `sequential_position`.
|
|
|
|
Arguments:
|
|
sequential_position (int): position in sequential bar
|
|
|
|
"""
|
|
sequential_position_css = '#tab_{0}'.format(sequential_position - 1)
|
|
self.q(css=sequential_position_css).first.click()
|