diff --git a/common/test/acceptance/pages/lms/course_home.py b/common/test/acceptance/pages/lms/course_home.py index 41cb4a22c5..9ff0472483 100644 --- a/common/test/acceptance/pages/lms/course_home.py +++ b/common/test/acceptance/pages/lms/course_home.py @@ -5,6 +5,8 @@ LMS Course Home page object from collections import OrderedDict from bok_choy.page_object import PageObject +from bok_choy.promise import BrokenPromise +from six import text_type from .bookmarks import BookmarksPage from .course_page import CoursePage @@ -80,15 +82,11 @@ class CourseOutlinePage(PageObject): url = None - SECTION_SELECTOR = '.outline-item.section:nth-of-type({0})' - SECTION_TITLES_SELECTOR = '.section-name h3' - SUBSECTION_SELECTOR = SECTION_SELECTOR + ' .subsection:nth-of-type({1}) .outline-item' - SUBSECTION_TITLES_SELECTOR = SECTION_SELECTOR + ' .subsection .subsection-title .subsection-title-name' - 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._section_selector = '.outline-item.section' + self._subsection_selector = '.subsection.accordion' def is_browser_on_page(self): return self.parent_page.is_browser_on_page @@ -108,28 +106,14 @@ class CourseOutlinePage(PageObject): You can use these titles in `go_to_section` to navigate to the section. """ - # Dict to store the result - outline_dict = OrderedDict() - - 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: - raise ValueError("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 + return self._get_outline_structure_as_dictionary() @property def num_sections(self): """ Return the number of sections """ - return len(self.q(css=self.SECTION_TITLES_SELECTOR)) + return len(self._get_sections_as_selenium_webelements()) @property def num_subsections(self, section_title=None): @@ -145,14 +129,19 @@ class CourseOutlinePage(PageObject): if not section_index: return else: - section_index = 1 + section_index = 0 - return len(self.q(css=self.SUBSECTION_TITLES_SELECTOR.format(section_index))) + sections = self._get_sections_as_selenium_webelements() + subsections = self._get_subsections(sections[section_index]) + return len(subsections) @property def num_units(self): """ - Return the number of units in the first subsection + Return the number of units in the first subsection. + + This method returns the number of units in the horizontal navigation + bar; not the course outline. """ return len(self.q(css='.sequence-list-wrapper ol li')) @@ -165,22 +154,23 @@ class CourseOutlinePage(PageObject): Example: go_to_section("Week 1", "Lesson 1") """ - section_index = self._section_title_to_index(section_title) - if section_index is None: - raise ValueError("Could not find section '{0}'".format(section_title)) + subsection_webelements = self._get_subsections_as_selenium_webelements() + subsection_titles = [self._get_outline_element_title(sub_webel) + for sub_webel in subsection_webelements] try: - subsection_index = self._subsection_titles(section_index + 1).index(subsection_title) + subsection_index = subsection_titles.index(text_type(subsection_title)) except ValueError: raise ValueError("Could not find subsection '{0}' in section '{1}'".format( subsection_title, section_title )) - # Convert list indices (start at zero) to CSS indices (start at 1) - subsection_css = self.SUBSECTION_SELECTOR.format(section_index + 1, subsection_index + 1) + target_subsection = subsection_webelements[subsection_index] + units = self._get_units(target_subsection) - # Click the subsection and ensure that the page finishes reloading - self.q(css=subsection_css).first.click() + # Click the subsection's first problem and ensure that the page finishes + # reloading + units[0].click() self._wait_for_course_section(section_title, subsection_title) @@ -200,7 +190,7 @@ class CourseOutlinePage(PageObject): except IndexError: raise ValueError("Section index '{0}' is out of range.".format(section_index)) try: - subsection_title = self._subsection_titles(section_index + 1)[subsection_index] + subsection_title = self._subsection_titles(section_index)[subsection_index] except IndexError: raise ValueError("Subsection index '{0}' in section index '{1}' is out of range.".format( subsection_index, section_index @@ -223,7 +213,7 @@ class CourseOutlinePage(PageObject): """ Navigate to courseware using Resume Course button in the header. """ - self.q(css=self.OUTLINE_RESUME_COURSE_SELECTOR).first.click() + self.q(css='.btn.btn-primary.action-resume-course').results[0].click() courseware_page = CoursewarePage(self.browser, self.parent_page.course_id) courseware_page.wait_for_page() @@ -231,21 +221,26 @@ class CourseOutlinePage(PageObject): """ Return a list of all section titles on the page. """ - return self.q(css=self.SECTION_TITLES_SELECTOR).map(lambda el: el.text.strip()).results + outline_sections = self._get_sections_as_selenium_webelements() + section_titles = [self._get_outline_element_title(section) for section in outline_sections] + return section_titles 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). + for the section at index `section_index` (starts at 0). """ - subsection_css = self.SUBSECTION_TITLES_SELECTOR.format(section_index) - return self.q(css=subsection_css).map( - lambda el: el.get_attribute('innerHTML').strip() - ).results + outline_sections = self._get_sections_as_selenium_webelements() + target_section = outline_sections[section_index] + target_subsections = self._get_subsections(target_section) + subsection_titles = [self._get_outline_element_title(subsection) + for subsection in target_subsections] + return subsection_titles 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. + 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() @@ -255,10 +250,78 @@ class CourseOutlinePage(PageObject): courseware_page.nav.visit_course_outline_page() self.wait_for( - 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) + 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) ) + def _get_outline_structure_as_dictionary(self): + ''' + Implements self.sections(). + ''' + outline_dict = OrderedDict() + + try: + outline_sections = self._get_sections_as_selenium_webelements() + except BrokenPromise: + outline_sections = [] + + for section in outline_sections: + subsections = self._get_subsections(section) + section_title = self._get_outline_element_title(section) + subsection_titles = [self._get_outline_element_title(subsection) + for subsection in subsections] + outline_dict[section_title] = subsection_titles + + return outline_dict + + @staticmethod + def _is_html_element_aria_expanded(html_element): + return html_element.get_attribute('aria-expanded') == u'true' + + @staticmethod + def _get_outline_element_title(outline_element): + return outline_element.text.split('\n')[0] + + def _get_subsections(self, section): + self._expand_all_outline_folds() + return section.find_elements_by_css_selector(self._subsection_selector) + + def _get_units(self, subsection): + self._expand_all_outline_folds() + return subsection.find_elements_by_tag_name('a') + + def _get_sections_as_selenium_webelements(self): + self._expand_all_outline_folds() + return self.q(css=self._section_selector).results + + def _get_subsections_as_selenium_webelements(self): + self._expand_all_outline_folds() + return self.q(css=self._subsection_selector).results + + def _expand_all_outline_folds(self): + ''' + Expands all parts of the collapsible outline. + ''' + section_button_selector = '.section-name.accordion-trigger' + subsection_button_selector = '.subsection-text.accordion-trigger' + self._expand_outline_fold(section_button_selector) + self._expand_outline_fold(subsection_button_selector) + + def _expand_outline_fold(self, fold_selector): + ''' + Ensures an outline fold is loaded, then clicks it open. + ''' + folds_as_elements = self.q(css=fold_selector) + self.wait_for_element_visibility( + fold_selector, "'{}' is visible".format(fold_selector) + ) + + for fold_element in folds_as_elements: + if not self._is_html_element_aria_expanded(fold_element): + fold_element.click() + class CourseSearchResultsPage(CoursePage): """ diff --git a/common/test/acceptance/tests/lms/test_lms_course_home.py b/common/test/acceptance/tests/lms/test_lms_course_home.py index 65b2cbec7b..56086080b2 100644 --- a/common/test/acceptance/tests/lms/test_lms_course_home.py +++ b/common/test/acceptance/tests/lms/test_lms_course_home.py @@ -91,8 +91,8 @@ class CourseHomeTest(CourseHomeBaseTest): # Check that the course navigation appears correctly EXPECTED_SECTIONS = { - 'Test Section': ['Test Subsection'], - 'Test Section 2': ['Test Subsection 2', 'Test Subsection 3'] + u'Test Section': [u'Test Subsection'], + u'Test Section 2': [u'Test Subsection 2', u'Test Subsection 3'] } actual_sections = self.course_home_page.outline.sections @@ -101,7 +101,7 @@ class CourseHomeTest(CourseHomeBaseTest): self.assertEqual(actual_sections[section], EXPECTED_SECTIONS[section]) # Navigate to a particular section - self.course_home_page.outline.go_to_section('Test Section', 'Test Subsection') + self.course_home_page.outline.go_to_section(u'Test Section', u'Test Subsection') # Check the sequence items on the courseware page EXPECTED_ITEMS = ['Test Problem 1', 'Test Problem 2', 'Test HTML'] diff --git a/common/test/acceptance/tests/lms/test_lms_courseware.py b/common/test/acceptance/tests/lms/test_lms_courseware.py index 4242dd02e6..d20e472024 100644 --- a/common/test/acceptance/tests/lms/test_lms_courseware.py +++ b/common/test/acceptance/tests/lms/test_lms_courseware.py @@ -639,24 +639,6 @@ class CoursewareMultipleVerticalsTest(CoursewareMultipleVerticalsTestBase): self.assertIn('html 2 dummy body', html2_page.get_selected_tab_content()) -@attr('a11y') -class CoursewareMultipleVerticalsA11YTest(CoursewareMultipleVerticalsTestBase): - """ - Test a11y for courseware with multiple verticals - """ - - def test_courseware_a11y(self): - """ - Run accessibility audit for the problem type. - """ - self.course_home_page.visit() - self.course_home_page.outline.go_to_section('Test Section 1', 'Test Subsection 1,1') - # Set the scope to the sequence navigation - self.courseware_page.a11y_audit.config.set_scope( - include=['div.sequence-nav']) - self.courseware_page.a11y_audit.check_for_accessibility_errors() - - @attr(shard=9) class ProblemStateOnNavigationTest(UniqueCourseTest): """ diff --git a/common/test/acceptance/tests/lms/test_lms_gating.py b/common/test/acceptance/tests/lms/test_lms_gating.py index 874984aaec..51a389c303 100644 --- a/common/test/acceptance/tests/lms/test_lms_gating.py +++ b/common/test/acceptance/tests/lms/test_lms_gating.py @@ -182,17 +182,10 @@ class GatingTest(UniqueCourseTest): Then I can see a gated subsection The gated subsection should have a lock icon and be in the format: " (Prerequisite Required)" - When I fufill the gating prerequisite - Then I can see the gated subsection (without a banner) """ self._setup_prereq() self._setup_gated_subsection() - # Fulfill prerequisites for specific student - self._auto_auth(self.STUDENT_USERNAME, self.STUDENT_EMAIL, False) - self.courseware_page.visit() - self._fulfill_prerequisite() - self._auto_auth(self.STAFF_USERNAME, self.STAFF_EMAIL, True) self.course_home_page.visit() @@ -216,11 +209,3 @@ class GatingTest(UniqueCourseTest): self.courseware_page.wait_for_page() # banner displayed informing section is a prereq self.assertTrue(self.courseware_page.has_banner()) - - self.course_home_page.visit() - self.course_home_page.preview.set_staff_view_mode_specific_student(self.STUDENT_USERNAME) - self.course_home_page.wait_for_page() - self.assertEqual(self.course_home_page.outline.num_subsections, 2) - self.course_home_page.outline.go_to_section('Test Section 1', 'Test Subsection 2') - self.courseware_page.wait_for_page() - self.assertFalse(self.courseware_page.has_banner()) diff --git a/lms/static/sass/features/_course-experience.scss b/lms/static/sass/features/_course-experience.scss index d7284c2bfe..ac4a1fc67c 100644 --- a/lms/static/sass/features/_course-experience.scss +++ b/lms/static/sass/features/_course-experience.scss @@ -302,7 +302,7 @@ } // Course outline for visual progress waffle switch -.course-outline-visualprogress { +.course-outline { .block-tree { margin: 0; padding: 0; @@ -451,111 +451,6 @@ button.accordion-trigger, button.prerequisite-button { float: right; } -// Course outline -.course-outline { - .block-tree { - margin: 0; - padding: 0; - list-style-type: none; - - .section { - margin: 0 (-1 * $baseline); - width: calc(100% + (2 * $baseline)); - padding: 0 ($baseline); - - &:not(:first-child) { - .section-name { - margin-top: $baseline; - } - } - - .section-name { - @include margin(0, 0, ($baseline / 2), ($baseline / 2)); - - padding: 0; - - .section-title { - font-weight: $font-bold; - font-size: 1.1rem; - margin: 0; - } - } - - .outline-item { - @include padding-left(0); - } - - ol.outline-item { - padding-bottom: $baseline; - border-bottom: 1px solid $border-color; - margin: 0 0 ($baseline / 2) 0; - - .subsection { - @include margin-left(10px); - - list-style-type: none; - border: 1px solid transparent; - border-radius: 3px; - margin-bottom: $baseline/4; - - a.outline-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: ($baseline / 2); - - .subsection-title { - margin: 0; - } - - &:hover, - &:focus { - background-color: palette(primary, x-back); - border-radius: $btn-border-radius; - text-decoration: none; - } - - .subsection-text { - .details { - font-size: $body-font-size; - color: theme-color("secondary"); - } - } - - .subsection-actions { - .resume-right { - position: relative; - top: calc(50% - (#{$baseline} / 2)); - } - } - } - - &.current { - border: 1px solid theme-color("primary"); - border-radius: $btn-border-radius; - - .resume-right { - @include float(right); - } - } - - &:hover { - border: 1px solid theme-color("primary"); - } - - &:last-child { - margin-bottom: 0; - } - } - - &:last-child { - border-bottom: none; - } - } - } - } -} - // date summary .date-summary-container { .date-summary { diff --git a/openedx/features/course_experience/static/course_experience/fixtures/course-outline-fragment.html b/openedx/features/course_experience/static/course_experience/fixtures/course-outline-fragment.html index 45e64be1f3..34d781c6eb 100644 --- a/openedx/features/course_experience/static/course_experience/fixtures/course-outline-fragment.html +++ b/openedx/features/course_experience/static/course_experience/fixtures/course-outline-fragment.html @@ -1,4 +1,11 @@
+
  1. diff --git a/openedx/features/course_experience/static/course_experience/js/CourseOutline.js b/openedx/features/course_experience/static/course_experience/js/CourseOutline.js index 09c6399a70..6a6a40cdb6 100644 --- a/openedx/features/course_experience/static/course_experience/js/CourseOutline.js +++ b/openedx/features/course_experience/static/course_experience/js/CourseOutline.js @@ -4,7 +4,7 @@ import { keys } from 'edx-ui-toolkit/js/utils/constants'; // @TODO: Figure out how to make webpack handle default exports when libraryTarget: 'window' export class CourseOutline { // eslint-disable-line import/prefer-default-export - constructor(newCourseOutlineEnabled) { + constructor() { const focusable = [...document.querySelectorAll('.outline-item.focusable')]; focusable.forEach(el => el.addEventListener('keydown', (event) => { @@ -54,49 +54,46 @@ export class CourseOutline { // eslint-disable-line import/prefer-default-expor sectionToggleButton.setAttribute('aria-expanded', 'false'); } - // TODO: EDUCATOR-2283 Remove check for waffle flag after it is turned on. - if (newCourseOutlineEnabled) { - [...document.querySelectorAll(('.accordion'))] - .forEach((accordion) => { - const sections = Array.prototype.slice.call(accordion.querySelectorAll('.accordion-trigger')); + [...document.querySelectorAll(('.accordion'))] + .forEach((accordion) => { + const sections = Array.prototype.slice.call(accordion.querySelectorAll('.accordion-trigger')); - sections.forEach(section => section.addEventListener('click', (event) => { - const sectionToggleButton = event.currentTarget; - if (sectionToggleButton.classList.contains('accordion-trigger')) { - const isExpanded = sectionToggleButton.getAttribute('aria-expanded') === 'true'; - if (!isExpanded) { - expandSection(sectionToggleButton); - } else if (isExpanded) { - collapseSection(sectionToggleButton); - } - event.stopImmediatePropagation(); + sections.forEach(section => section.addEventListener('click', (event) => { + const sectionToggleButton = event.currentTarget; + if (sectionToggleButton.classList.contains('accordion-trigger')) { + const isExpanded = sectionToggleButton.getAttribute('aria-expanded') === 'true'; + if (!isExpanded) { + expandSection(sectionToggleButton); + } else if (isExpanded) { + collapseSection(sectionToggleButton); } - })); - }); - - const toggleAllButton = document.querySelector('#expand-collapse-outline-all-button'); - const toggleAllSpan = document.querySelector('#expand-collapse-outline-all-span'); - const extraPaddingClass = 'expand-collapse-outline-all-extra-padding'; - toggleAllButton.addEventListener('click', (event) => { - const toggleAllExpanded = toggleAllButton.getAttribute('aria-expanded') === 'true'; - let sectionAction; - if (toggleAllExpanded) { - toggleAllButton.setAttribute('aria-expanded', 'false'); - sectionAction = collapseSection; - toggleAllSpan.classList.add(extraPaddingClass); - toggleAllSpan.innerText = 'Expand All'; - } else { - toggleAllButton.setAttribute('aria-expanded', 'true'); - sectionAction = expandSection; - toggleAllSpan.classList.remove(extraPaddingClass); - toggleAllSpan.innerText = 'Collapse All'; - } - const sections = Array.prototype.slice.call(document.querySelectorAll('.accordion-trigger')); - sections.forEach((sectionToggleButton) => { - sectionAction(sectionToggleButton); - }); - event.stopImmediatePropagation(); + event.stopImmediatePropagation(); + } + })); }); - } + + const toggleAllButton = document.querySelector('#expand-collapse-outline-all-button'); + const toggleAllSpan = document.querySelector('#expand-collapse-outline-all-span'); + const extraPaddingClass = 'expand-collapse-outline-all-extra-padding'; + toggleAllButton.addEventListener('click', (event) => { + const toggleAllExpanded = toggleAllButton.getAttribute('aria-expanded') === 'true'; + let sectionAction; + if (toggleAllExpanded) { + toggleAllButton.setAttribute('aria-expanded', 'false'); + sectionAction = collapseSection; + toggleAllSpan.classList.add(extraPaddingClass); + toggleAllSpan.innerText = 'Expand All'; + } else { + toggleAllButton.setAttribute('aria-expanded', 'true'); + sectionAction = expandSection; + toggleAllSpan.classList.remove(extraPaddingClass); + toggleAllSpan.innerText = 'Collapse All'; + } + const sections = Array.prototype.slice.call(document.querySelectorAll('.accordion-trigger')); + sections.forEach((sectionToggleButton) => { + sectionAction(sectionToggleButton); + }); + event.stopImmediatePropagation(); + }); } } diff --git a/openedx/features/course_experience/templates/course_experience/course-outline-fragment-new.html b/openedx/features/course_experience/templates/course_experience/course-outline-fragment-new.html deleted file mode 100644 index 3d596f174a..0000000000 --- a/openedx/features/course_experience/templates/course_experience/course-outline-fragment-new.html +++ /dev/null @@ -1,194 +0,0 @@ -## mako - -<%page expression_filter="h"/> - -<%namespace name='static' file='../static_content.html'/> - -<%! -from datetime import date - -from django.utils.translation import ugettext as _ - -from openedx.core.djangolib.markup import HTML, Text -%> - -
    - % if blocks.get('children'): - - - % endif -
    - -<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory"> - DateUtilFactory.transform('.localized-datetime'); - - -<%static:webpack entry="CourseOutline"> - new CourseOutline('.block-tree', true); - diff --git a/openedx/features/course_experience/templates/course_experience/course-outline-fragment-old.html b/openedx/features/course_experience/templates/course_experience/course-outline-fragment-old.html deleted file mode 100644 index 476fb3ec9a..0000000000 --- a/openedx/features/course_experience/templates/course_experience/course-outline-fragment-old.html +++ /dev/null @@ -1,153 +0,0 @@ -## mako - -<%page expression_filter="h"/> - -<%namespace name='static' file='../static_content.html'/> - -<%! -from datetime import date - -from django.utils.translation import ugettext as _ - -from openedx.core.djangolib.markup import HTML, Text -%> - -
    - % if blocks.get('children'): -
      - % for section in blocks.get('children'): -
    1. -
      -

      ${ section['display_name'] }

      -
      -
        - % for subsection in section.get('children', []): -
      1. - -
        - ## Subsection title - - % if subsection['id'] in gated_content: - % if gated_content[subsection['id']]['completed_prereqs']: - - - ${ subsection['display_name'] } - -  ${_("Unlocked")} - % else: - - - ${ subsection['display_name'] } - - - ${ _("(Prerequisite required)") } - - % endif - % else: - - ${ subsection['display_name'] } - - % endif -
        - - ## There are behavior differences between rendering of subsections which have - ## exams (timed, graded, etc) and those that do not. - ## - ## Exam subsections expose exam status message field as well as a status icon - <% - if subsection.get('due') is None: - # examples: Homework, Lab, etc. - data_string = subsection.get('format') - else: - if 'special_exam_info' in subsection: - data_string = _('due {date}') - else: - data_string = _("{subsection_format} due {{date}}").format(subsection_format=subsection.get('format')) - %> - % if subsection.get('format') or 'special_exam_info' in subsection: - - % if 'special_exam' in subsection: - ## Display the exam status icon and status message - - - ${subsection['special_exam_info'].get('short_description', '')} - - - ## completed exam statuses should not show the due date - ## since the exam has already been submitted by the user - % if not subsection['special_exam_info'].get('in_completed_state', False): - - % endif - % else: - ## non-graded section, we just show the exam format and the due date - ## this is the standard case in edx-platform - - - % if 'graded' in subsection and subsection['graded']: - -  ${_("This content is graded")} - % endif - % endif - - % endif -
        -
        -
        - ## Resume button (if last visited section) - % if subsection['resume_block']: - - ${ _("Resume Course") } - - - %endif -
        -
        -
      2. - % endfor -
      -
    2. - % endfor -
    - % endif -
    - -<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory"> - DateUtilFactory.transform('.localized-datetime'); - - -<%static:webpack entry="CourseOutline"> - new CourseOutline('.block-tree', false); - 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 new file mode 100644 index 0000000000..81aac190c3 --- /dev/null +++ b/openedx/features/course_experience/templates/course_experience/course-outline-fragment.html @@ -0,0 +1,189 @@ +## mako + +<%page expression_filter="h"/> + +<%namespace name='static' file='../static_content.html'/> + +<%! +from datetime import date + +from django.utils.translation import ugettext as _ + +from openedx.core.djangolib.markup import HTML, Text +%> + +<% +course_sections = blocks.get('children') +%> +
    + % if course_sections is not None: + + + % endif +
    + +<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory"> + DateUtilFactory.transform('.localized-datetime'); + + +<%static:webpack entry="CourseOutline"> + new CourseOutline(); + diff --git a/openedx/features/course_experience/tests/views/test_course_outline.py b/openedx/features/course_experience/tests/views/test_course_outline.py index 28d43c2044..800a917a79 100644 --- a/openedx/features/course_experience/tests/views/test_course_outline.py +++ b/openedx/features/course_experience/tests/views/test_course_outline.py @@ -131,7 +131,7 @@ class TestCourseOutlinePage(SharedModuleStoreTestCase): self.assertIn(sequential.format, response_content) self.assertTrue(sequential.children) for vertical in sequential.children: - self.assertNotIn(vertical.display_name, response_content) + self.assertIn(vertical.display_name, response_content) class TestCourseOutlinePageWithPrerequisites(SharedModuleStoreTestCase, MilestonesTestCaseMixin): @@ -230,21 +230,21 @@ class TestCourseOutlinePageWithPrerequisites(SharedModuleStoreTestCase, Mileston lock_icon = response_content('.fa-lock') self.assertTrue(lock_icon, "lock icon is not present, but should be") - subsection = lock_icon.parents('.subsection-title') + subsection = lock_icon.parents('.subsection-text') # check that subsection-title-name is the display name gated_subsection_title = self.course_blocks['gated_content'].display_name - self.assertIn(gated_subsection_title, subsection.children('.subsection-title-name').html()) + self.assertIn(gated_subsection_title, subsection.children('.subsection-title').html()) # check that it says prerequisite required - self.assertIn(self.PREREQ_REQUIRED, subsection.children('.details').html()) + self.assertIn("Prerequisite:", subsection.children('.details').html()) # check that there is not a screen reader message self.assertFalse(subsection.children('.sr')) def test_content_unlocked(self): """ - Test that a sequential/subsection with unmet prereqs correctly indicated that its content is locked + Test that a sequential/subsection with met prereqs correctly indicated that its content is unlocked """ course = self.course self.setup_gated_section(self.course_blocks['gated_content'], self.course_blocks['prerequisite']) @@ -263,24 +263,23 @@ class TestCourseOutlinePageWithPrerequisites(SharedModuleStoreTestCase, Mileston response_content = pq(response.content) - # check unlock icon is present + # check unlock icon is not present unlock_icon = response_content('.fa-unlock') - self.assertTrue(unlock_icon, "unlock icon is not present, but should be") + self.assertFalse(unlock_icon, "unlock icon is present, yet shouldn't be.") - subsection = unlock_icon.parents('.subsection-title') + gated_subsection_title = self.course_blocks['gated_content'].display_name + every_subsection_on_outline = response_content('.subsection-title') + + subsection_has_gated_text = False + says_prerequisite_required = False + + for subsection_contents in every_subsection_on_outline.contents(): + subsection_has_gated_text = gated_subsection_title in subsection_contents + says_prerequisite_required = "Prerequisite:" in subsection_contents # check that subsection-title-name is the display name of gated content section - gated_subsection_title = self.course_blocks['gated_content'].display_name - self.assertIn(gated_subsection_title, subsection.children('.subsection-title-name').html()) - - # check that it doesn't say prerequisite required - self.assertNotIn(self.PREREQ_REQUIRED, subsection.children('.subsection-title-name').html()) - - # check that there is a screen reader message - self.assertTrue(subsection.children('.sr')) - - # check that the screen reader message is correct - self.assertIn(self.UNLOCKED, subsection.children('.sr').html()) + self.assertTrue(subsection_has_gated_text) + self.assertFalse(says_prerequisite_required) class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleTestMixin): @@ -415,7 +414,7 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT self.complete_sequential(self.course, vertical1) # Test for 'resume' link - response = self.visit_course_home(course, resume_count=2) + response = self.visit_course_home(course, resume_count=1) # Test for 'resume' link URL - should be vertical 1 content = pq(response.content) @@ -423,7 +422,7 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT self.complete_sequential(self.course, vertical2) # Test for 'resume' link - response = self.visit_course_home(course, resume_count=2) + response = self.visit_course_home(course, resume_count=1) # Test for 'resume' link URL - should be vertical 2 content = pq(response.content) @@ -434,7 +433,7 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT 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) + response = self.visit_course_home(course, resume_count=1) content = pq(response.content) self.assertTrue(content('.action-resume-course').attr('href').endswith('/vertical/' + vertical2.url_name)) @@ -459,7 +458,7 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT self.store.delete_item(sequential.location, self.user.id) # check resume course buttons - response = self.visit_course_home(course, resume_count=2) + response = self.visit_course_home(course, resume_count=1) content = pq(response.content) self.assertTrue(content('.action-resume-course').attr('href').endswith('/sequential/' + sequential2.url_name)) @@ -522,22 +521,21 @@ class TestCourseOutlineResumeCourse(SharedModuleStoreTestCase, CompletionWaffleT "aria-labelledby=\"" + url + "\"" \ ">" - with patch('openedx.features.course_experience.waffle.new_course_outline_enabled', Mock(return_value=True)): - # Course tree - course = self.course - chapter = course.children[0] - sequential1 = chapter.children[0] - sequential2 = chapter.children[1] + # Course tree + course = self.course + chapter = course.children[0] + sequential1 = chapter.children[0] + sequential2 = chapter.children[1] - response_content = self.client.get(course_home_url(course)).content - stripped_response = text_type(re.sub("\\s+", "", response_content), "utf-8") + response_content = self.client.get(course_home_url(course)).content + stripped_response = text_type(re.sub("\\s+", "", response_content), "utf-8") - self.assertTrue(get_sequential_button(text_type(sequential1.location), False) in stripped_response) - self.assertTrue(get_sequential_button(text_type(sequential2.location), True) in stripped_response) + self.assertTrue(get_sequential_button(text_type(sequential1.location), False) in stripped_response) + self.assertTrue(get_sequential_button(text_type(sequential2.location), True) in stripped_response) - content = pq(response_content) - button = content('#expand-collapse-outline-all-button') - self.assertEqual('Expand All', button.children()[0].text) + content = pq(response_content) + button = content('#expand-collapse-outline-all-button') + self.assertEqual('Expand All', button.children()[0].text) def test_user_enrolled_after_completion_collection(self): """ diff --git a/openedx/features/course_experience/views/course_outline.py b/openedx/features/course_experience/views/course_outline.py index d404813659..b4ff648c69 100644 --- a/openedx/features/course_experience/views/course_outline.py +++ b/openedx/features/course_experience/views/course_outline.py @@ -15,7 +15,6 @@ from web_fragments.fragment import Fragment from courseware.courses import get_course_overview_with_access from openedx.core.djangoapps.plugin_api.views import EdxFragmentView -from openedx.features.course_experience import waffle as course_experience_waffle from student.models import CourseEnrollment from util.milestones_helpers import get_course_content_milestones @@ -50,29 +49,18 @@ class CourseOutlineFragmentView(EdxFragmentView): 'blocks': course_block_tree } - # TODO: EDUCATOR-2283 Remove this check when the waffle flag is turned on in production - if course_experience_waffle.new_course_outline_enabled(course_key=course_key): - resume_block = get_resume_block(course_block_tree) - if not resume_block: - self.mark_first_unit_to_resume(course_block_tree) + resume_block = get_resume_block(course_block_tree) + if not resume_block: + self.mark_first_unit_to_resume(course_block_tree) - xblock_display_names = self.create_xblock_id_and_name_dict(course_block_tree) - gated_content = self.get_content_milestones(request, course_key) + xblock_display_names = self.create_xblock_id_and_name_dict(course_block_tree) + gated_content = self.get_content_milestones(request, course_key) - context['gated_content'] = gated_content - context['xblock_display_names'] = xblock_display_names + context['gated_content'] = gated_content + context['xblock_display_names'] = xblock_display_names - # TODO: EDUCATOR-2283 Rename this file to course-outline-fragment.html - html = render_to_string('course_experience/course-outline-fragment-new.html', context) - return Fragment(html) - else: - content_milestones = self.get_content_milestones_old(request, course_key) - - context['gated_content'] = content_milestones - - # TODO: EDUCATOR-2283 Remove this file - html = render_to_string('course_experience/course-outline-fragment-old.html', context) - return Fragment(html) + html = render_to_string('course_experience/course-outline-fragment.html', context) + return Fragment(html) def create_xblock_id_and_name_dict(self, course_block_tree, xblock_display_names=None): """ @@ -114,28 +102,6 @@ class CourseOutlineFragmentView(EdxFragmentView): return gated_content - # TODO: EDUCATOR-2283 Remove this function when the visual progress waffle flag is turned on in production - def get_content_milestones_old(self, request, course_key): - """ - Returns dict of subsections with prerequisites and whether the prerequisite has been completed or not - """ - - all_course_prereqs = get_course_content_milestones(course_key) - - content_ids_of_unfulfilled_prereqs = { - milestone['content_id'] - for milestone in get_course_content_milestones(course_key, user_id=request.user.id) - } - - course_content_milestones = { - milestone['content_id']: { - 'completed_prereqs': milestone['content_id'] not in content_ids_of_unfulfilled_prereqs - } - for milestone in all_course_prereqs - } - - return course_content_milestones - def user_enrolled_after_completion_collection(self, user, course_key): """ Checks that the user has enrolled in the course after 01/24/2018, the date that diff --git a/openedx/features/course_experience/waffle.py b/openedx/features/course_experience/waffle.py deleted file mode 100644 index 3de0b96641..0000000000 --- a/openedx/features/course_experience/waffle.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -This module contains various configuration settings via -waffle switches for the course experience app. -""" -from __future__ import unicode_literals - -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 = 'course_experience' - -# Switches -# Full name course_experience.enable_new_course_outline -# Enables the UI changes to the course outline for all courses -ENABLE_NEW_COURSE_OUTLINE = 'enable_new_course_outline' - -# Full name course_experience.enable_new_course_outline_for_course -# Enables the UI changes to the course outline for a course -ENABLE_NEW_COURSE_OUTLINE_FOR_COURSE = 'enable_new_course_outline_for_course' - -# Full name course_experience.enable_new_course_outline_for_site -# Enables the UI changes to the course outline for a site configuration -ENABLE_NEW_COURSE_OUTLINE_FOR_SITE = 'enable_new_course_outline_for_site' - - -def waffle_switch(): - """ - Returns the namespaced, cached, audited Waffle class for course experience. - """ - return WaffleSwitchNamespace(name=WAFFLE_NAMESPACE, log_prefix='course_experience: ') - - -def waffle_flag(): - """ - Returns the namespaced, cached, audited Waffle flags dictionary for course experience. - """ - namespace = WaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'course_experience: ') - # By default, disable the new course outline. Can be enabled on a course-by-course basis. - # And overridden site-globally by ENABLE_SITE_NEW_COURSE_OUTLINE - return CourseWaffleFlag( - namespace, - ENABLE_NEW_COURSE_OUTLINE_FOR_COURSE, - flag_undefined_default=False - ) - - -def new_course_outline_enabled(course_key): - """ - Returns whether the new course outline is enabled. - """ - try: - current_site = get_current_site() - if not current_site.configuration.get_value(ENABLE_NEW_COURSE_OUTLINE_FOR_SITE, False): - return - except SiteConfiguration.DoesNotExist: - return - - if not waffle_switch().is_enabled(ENABLE_NEW_COURSE_OUTLINE): - return waffle_flag().is_enabled(course_key) - - return True