course_nav.py 7.78 KB
Newer Older
1 2 3 4 5
"""
Course navigation page object
"""

import re
6
from bok_choy.page_object import PageObject
7
from bok_choy.promise import EmptyPromise
8 9 10 11 12 13 14


class CourseNavPage(PageObject):
    """
    Navigate sections and sequences in the courseware.
    """

15
    url = None
16 17

    def is_browser_on_page(self):
18
        return self.q(css='div.course-index').present
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

    @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
41
        for sec_index, sec_title in enumerate(section_titles):
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60

            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>p'
61
        return self.q(css=seq_css).map(self._clean_seq_titles).results
62 63 64 65 66 67 68 69 70 71 72 73

    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)
74
        self.browser.execute_script("jQuery.fx.off = true;")
75 76 77 78 79 80 81 82 83 84

        # 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
85
        section_css = '.course-navigation .chapter:nth-of-type({0})'.format(sec_index + 1)
86
        self.q(css=section_css).first.click()
87 88 89 90 91 92 93 94 95 96

        # 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)
97
        subsection_css = ".course-navigation .chapter-content-container:nth-of-type({0}) .chapter-menu .menu-item:nth-of-type({1})".format(
98 99 100 101
            sec_index + 1, subsec_index + 1
        )

        # Click the subsection and ensure that the page finishes reloading
102 103 104
        self.q(css=subsection_css).first.click()
        self._on_section_promise(section_title, subsection_title).fulfill()

105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
    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)
127
            self.q(css=seq_css).first.click()
128 129 130 131 132

    def _section_titles(self):
        """
        Return a list of all section titles on the page.
        """
133
        chapter_css = '.course-navigation .chapter .group-heading'
134
        return self.q(css=chapter_css).map(lambda el: el.text.strip()).results
135 136 137 138 139 140 141 142

    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
143
        subsection_css = '.course-navigation .chapter-content-container:nth-of-type({0}) .chapter-menu .menu-item a p:nth-of-type(1)'.format(
144 145
            section_index
        )
146 147 148 149 150

        # 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.
151
        return self.q(
152 153
            css=subsection_css
        ).map(
154 155
            lambda el: el.text.strip().split('\n')[0] if el.is_displayed() else el.get_attribute('innerHTML').strip()
        ).results
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175

    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).
        """
176 177
        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
Chris Rodriguez committed
178

179 180 181 182 183 184 185 186 187 188 189 190 191
        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
            )
192 193 194 195 196 197 198 199

    # 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.
        """
200
        return self.REMOVE_SPAN_TAG_RE.sub('', element.get_attribute('innerHTML')).strip().split('\n')[0]
201 202 203 204 205 206 207 208 209 210 211

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