From e1e57b5d093a302eb2334f0dd568c7eebd7e39fa Mon Sep 17 00:00:00 2001 From: cahrens Date: Thu, 22 Jun 2017 16:44:08 -0400 Subject: [PATCH] Convert course list to React. EDUCATOR-625, AC-620 --- .babelrc | 3 +- .../contentstore/tests/test_contentstore.py | 42 +----- .../contentstore/tests/test_course_listing.py | 28 +--- .../views/tests/test_course_index.py | 74 ++-------- cms/envs/acceptance.py | 4 + cms/envs/common.py | 4 + cms/static/js/features_jsx/.eslintrc.js | 7 + cms/static/js/features_jsx/studio/index.jsx | 131 ++++++++++++++++++ cms/static/sass/views/_dashboard.scss | 34 +---- cms/templates/course-create-rerun.html | 2 +- cms/templates/index.html | 125 ++++------------- .../mock/mock-create-course-rerun.underscore | 2 +- .../js/mock/mock-index-page.underscore | 4 +- common/test/acceptance/pages/studio/index.py | 23 ++- .../tests/studio/test_studio_course_create.py | 3 + .../tests/studio/test_studio_home.py | 47 +++++++ package.json | 4 + pavelib/quality.py | 2 +- scripts/all-tests.sh | 2 +- webpack.config.js | 5 +- 20 files changed, 277 insertions(+), 269 deletions(-) create mode 100644 cms/static/js/features_jsx/.eslintrc.js create mode 100644 cms/static/js/features_jsx/studio/index.jsx diff --git a/.babelrc b/.babelrc index 91cf362dc6bf..6c6df4b2b98f 100644 --- a/.babelrc +++ b/.babelrc @@ -11,6 +11,7 @@ }, "modules": false } - ] + ], + "babel-preset-react" ] } diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 8d4a9085fc11..f53c705dcca7 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -24,7 +24,6 @@ from opaque_keys.edx.locations import AssetLocation, CourseLocator from path import Path as path -from common.test.utils import XssTestMixin from contentstore.tests.utils import AjaxEnabledTestClient, CourseTestCase, get_url, parse_json from contentstore.utils import delete_course, reverse_course_url, reverse_url from contentstore.views.component import ADVANCED_COMPONENT_TYPES @@ -1138,7 +1137,7 @@ def _check_verticals(self, locations): @ddt.ddt -class ContentStoreTest(ContentStoreTestCase, XssTestMixin): +class ContentStoreTest(ContentStoreTestCase): """ Tests for the CMS ContentStore application. """ @@ -1473,33 +1472,6 @@ def test_item_factory(self): item = ItemFactory.create(parent_location=course.location) self.assertIsInstance(item, SequenceDescriptor) - def test_course_index_view_with_course(self): - """Test viewing the index page with an existing course""" - CourseFactory.create(display_name='Robot Super Educational Course') - resp = self.client.get_html('/home/') - self.assertContains( - resp, - '

Robot Super Educational Course

', - status_code=200, - html=True - ) - - def test_course_index_view_xss(self): - """Test that the index page correctly escapes course names with script - tags.""" - CourseFactory.create( - display_name='' - ) - - LibraryFactory.create(display_name='') - - resp = self.client.get_html('/home/') - for xss in ('course', 'library'): - html = ''.format( - name=xss - ) - self.assert_no_xss(resp, html) - def test_course_overview_view_with_course(self): """Test viewing the course overview page with an existing course""" course = CourseFactory.create() @@ -1911,30 +1883,22 @@ def post_rerun_request( destination_course_key = CourseKey.from_string(json_resp['destination_course_key']) return destination_course_key - def get_course_listing_elements(self, html, course_key): - """Returns the elements in the course listing section of html that have the given course_key""" - return html.cssselect('.course-item[data-course-key="{}"]'.format(unicode(course_key))) - def get_unsucceeded_course_action_elements(self, html, course_key): """Returns the elements in the unsucceeded course action section that have the given course_key""" return html.cssselect('.courses-processing li[data-course-key="{}"]'.format(unicode(course_key))) def assertInCourseListing(self, course_key): """ - Asserts that the given course key is in the accessible course listing section of the html - and NOT in the unsucceeded course action section of the html. + Asserts that the given course key is NOT in the unsucceeded course action section of the html. """ course_listing = lxml.html.fromstring(self.client.get_html('/home/').content) - self.assertEqual(len(self.get_course_listing_elements(course_listing, course_key)), 1) self.assertEqual(len(self.get_unsucceeded_course_action_elements(course_listing, course_key)), 0) def assertInUnsucceededCourseActions(self, course_key): """ - Asserts that the given course key is in the unsucceeded course action section of the html - and NOT in the accessible course listing section of the html. + Asserts that the given course key is in the unsucceeded course action section of the html. """ course_listing = lxml.html.fromstring(self.client.get_html('/home/').content) - self.assertEqual(len(self.get_course_listing_elements(course_listing, course_key)), 0) self.assertEqual(len(self.get_unsucceeded_course_action_elements(course_listing, course_key)), 1) def verify_rerun_course(self, source_course_key, destination_course_key, destination_display_name): diff --git a/cms/djangoapps/contentstore/tests/test_course_listing.py b/cms/djangoapps/contentstore/tests/test_course_listing.py index 8d66a64ff639..c771fbd74f2f 100644 --- a/cms/djangoapps/contentstore/tests/test_course_listing.py +++ b/cms/djangoapps/contentstore/tests/test_course_listing.py @@ -9,11 +9,9 @@ from chrono import Timer from django.conf import settings from django.test import RequestFactory -from django.test.client import Client from mock import Mock, patch from opaque_keys.edx.locations import CourseLocator -from common.test.utils import XssTestMixin from contentstore.tests.utils import AjaxEnabledTestClient from contentstore.utils import delete_course from contentstore.views.course import ( @@ -44,7 +42,7 @@ @ddt.ddt -class TestCourseListing(ModuleStoreTestCase, XssTestMixin): +class TestCourseListing(ModuleStoreTestCase): """ Unit tests for getting the list of courses for a logged in user """ @@ -88,30 +86,6 @@ def tearDown(self): self.client.logout() ModuleStoreTestCase.tearDown(self) - def test_course_listing_is_escaped(self): - """ - Tests course listing returns escaped data. - """ - escaping_content = "" - - # Make user staff to access course listing - self.user.is_staff = True - self.user.save() # pylint: disable=no-member - - self.client = Client() - self.client.login(username=self.user.username, password='test') - - # Change 'display_coursenumber' field and update the course. - course = CourseFactory.create() - course.display_coursenumber = escaping_content - course = self.store.update_item(course, self.user.id) # pylint: disable=no-member - self.assertEqual(course.display_coursenumber, escaping_content) - - # Check if response is escaped - response = self.client.get('/home') - self.assertEqual(response.status_code, 200) - self.assert_no_xss(response, escaping_content) - def test_empty_course_listing(self): """ Test on empty course listing, studio name is properly displayed diff --git a/cms/djangoapps/contentstore/views/tests/test_course_index.py b/cms/djangoapps/contentstore/views/tests/test_course_index.py index 14221e030e90..9dad8412cb96 100644 --- a/cms/djangoapps/contentstore/views/tests/test_course_index.py +++ b/cms/djangoapps/contentstore/views/tests/test_course_index.py @@ -52,55 +52,34 @@ def setUp(self): display_name='dotted.course.name-2', ) - def check_index_and_outline(self, authed_client): + def check_courses_on_index(self, authed_client): """ - Test getting the list of courses and then pulling up their outlines + Test that the React course listing is present. """ index_url = '/home/' index_response = authed_client.get(index_url, {}, HTTP_ACCEPT='text/html') parsed_html = lxml.html.fromstring(index_response.content) - course_link_eles = parsed_html.find_class('course-link') - self.assertGreaterEqual(len(course_link_eles), 2) - for link in course_link_eles: - self.assertRegexpMatches( - link.get("href"), - 'course/{}'.format(settings.COURSE_KEY_PATTERN) - ) - # now test that url - outline_response = authed_client.get(link.get("href"), {}, HTTP_ACCEPT='text/html') - # ensure it has the expected 2 self referential links - outline_parsed = lxml.html.fromstring(outline_response.content) - outline_link = outline_parsed.find_class('course-link')[0] - self.assertEqual(outline_link.get("href"), link.get("href")) - course_menu_link = outline_parsed.find_class('nav-course-courseware-outline')[0] - self.assertEqual(course_menu_link.find("a").get("href"), link.get("href")) + courses_tab = parsed_html.find_class('react-course-listing') + self.assertEqual(len(courses_tab), 1) - def test_libraries_on_course_index(self): + def test_libraries_on_index(self): """ - Test getting the list of libraries from the course listing page + Test that the library tab is present. """ - def _assert_library_link_present(response, library): + def _assert_library_tab_present(response): """ - Asserts there's a valid library link on libraries tab. + Asserts there's a library tab. """ parsed_html = lxml.html.fromstring(response.content) - library_link_elements = parsed_html.find_class('library-link') - self.assertEqual(len(library_link_elements), 1) - link = library_link_elements[0] - self.assertEqual( - link.get("href"), - reverse_library_url('library_handler', library.location.library_key), - ) - # now test that url - outline_response = self.client.get(link.get("href"), {}, HTTP_ACCEPT='text/html') - self.assertEqual(outline_response.status_code, 200) + library_tab = parsed_html.find_class('react-library-listing') + self.assertEqual(len(library_tab), 1) # Add a library: lib1 = LibraryFactory.create() index_url = '/home/' index_response = self.client.get(index_url, {}, HTTP_ACCEPT='text/html') - _assert_library_link_present(index_response, lib1) + _assert_library_tab_present(index_response) # Make sure libraries are visible to non-staff users too self.client.logout() @@ -109,13 +88,13 @@ def _assert_library_link_present(response, library): LibraryUserRole(lib2.location.library_key).add_users(non_staff_user) self.client.login(username=non_staff_user.username, password=non_staff_userpassword) index_response = self.client.get(index_url, {}, HTTP_ACCEPT='text/html') - _assert_library_link_present(index_response, lib2) + _assert_library_tab_present(index_response) def test_is_staff_access(self): """ Test that people with is_staff see the courses and can navigate into them """ - self.check_index_and_outline(self.client) + self.check_courses_on_index(self.client) def test_negative_conditions(self): """ @@ -143,7 +122,7 @@ def test_course_staff_access(self): ) # test access - self.check_index_and_outline(course_staff_client) + self.check_courses_on_index(course_staff_client) def test_json_responses(self): outline_url = reverse_course_url('course_handler', self.course.id) @@ -402,31 +381,8 @@ def check_index_page(self, separate_archived_courses, org): parsed_html = lxml.html.fromstring(index_response.content) course_tab = parsed_html.find_class('courses') self.assertEqual(len(course_tab), 1) - course_links = course_tab[0].find_class('course-link') - course_titles = course_tab[0].find_class('course-title') archived_course_tab = parsed_html.find_class('archived-courses') - - if separate_archived_courses: - # Archived courses should be separated from the main course list - self.assertEqual(len(archived_course_tab), 1) - archived_course_links = archived_course_tab[0].find_class('course-link') - archived_course_titles = archived_course_tab[0].find_class('course-title') - self.assertEqual(len(archived_course_links), 1) - self.assertEqual(len(archived_course_titles), 1) - self.assertEqual(archived_course_titles[0].text, 'Archived Course') - - self.assertEqual(len(course_links), 2) - self.assertEqual(len(course_titles), 2) - self.assertEqual(course_titles[0].text, 'Active Course 1') - self.assertEqual(course_titles[1].text, 'Active Course 2') - else: - # Archived courses should be included in the main course list - self.assertEqual(len(archived_course_tab), 0) - self.assertEqual(len(course_links), 3) - self.assertEqual(len(course_titles), 3) - self.assertEqual(course_titles[0].text, 'Active Course 1') - self.assertEqual(course_titles[1].text, 'Active Course 2') - self.assertEqual(course_titles[2].text, 'Archived Course') + self.assertEqual(len(archived_course_tab), 1 if separate_archived_courses else 0) @ddt.data( # Staff user has course staff access diff --git a/cms/envs/acceptance.py b/cms/envs/acceptance.py index 684fa2a79f8a..f31b7b0f7fc8 100644 --- a/cms/envs/acceptance.py +++ b/cms/envs/acceptance.py @@ -106,6 +106,10 @@ def seed(): # We do not yet understand why this occurs. Setting this to true is a stopgap measure USE_I18N = True +# Override the test stub webpack_loader that is installed in test.py. +INSTALLED_APPS = tuple(app for app in INSTALLED_APPS if app != 'openedx.tests.util.webpack_loader') +INSTALLED_APPS += ('webpack_loader',) + # Include the lettuce app for acceptance testing, including the 'harvest' django-admin command # django.contrib.staticfiles used to be loaded by lettuce, now we must add it ourselves # django.contrib.staticfiles is not added to lms as there is a ^/static$ route built in to the app diff --git a/cms/envs/common.py b/cms/envs/common.py index b9ca6546bf81..24e8226286f8 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -252,6 +252,10 @@ # Whether or not the dynamic EnrollmentTrackUserPartition should be registered. 'ENABLE_ENROLLMENT_TRACK_USER_PARTITION': True, + + # Whether archived courses (courses with end dates in the past) should be + # shown in Studio in a separate list. + 'ENABLE_SEPARATE_ARCHIVED_COURSES': True } ENABLE_JASMINE = False diff --git a/cms/static/js/features_jsx/.eslintrc.js b/cms/static/js/features_jsx/.eslintrc.js new file mode 100644 index 000000000000..12cb26eef91e --- /dev/null +++ b/cms/static/js/features_jsx/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: 'eslint-config-edx', + root: true, + settings: { + 'import/resolver': 'webpack', + }, +}; diff --git a/cms/static/js/features_jsx/studio/index.jsx b/cms/static/js/features_jsx/studio/index.jsx new file mode 100644 index 000000000000..0ba7b2d7052b --- /dev/null +++ b/cms/static/js/features_jsx/studio/index.jsx @@ -0,0 +1,131 @@ +/* global gettext */ +/* eslint react/no-array-index-key: 0 */ + +import PropTypes from 'prop-types'; +import React from 'react'; +import ReactDOM from 'react-dom'; + +function CourseOrLibraryListing(props) { + const allowReruns = props.allowReruns; + const linkClass = props.linkClass; + const idBase = props.idBase; + + return ( + + ); +} + +CourseOrLibraryListing.propTypes = { + allowReruns: PropTypes.bool.isRequired, + idBase: PropTypes.string.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + linkClass: PropTypes.string.isRequired, +}; + +export class StudioCourseIndex { + constructor(selector, context, allowReruns) { + // The HTML element is only conditionally shown, based on number of courses. + const element = document.querySelector(selector); + if (element) { + ReactDOM.render( + , + element, + ); + } + } +} + +export class StudioArchivedIndex { + constructor(selector, context, allowReruns) { + // The HTML element is only conditionally shown, based on number of archived courses. + const element = document.querySelector(selector); + if (element) { + ReactDOM.render( + , + element, + ); + } + } +} + +export class StudioLibraryIndex { + constructor(selector, context) { + // The HTML element is only conditionally shown, based on number of libraries. + const element = document.querySelector(selector); + if (element) { + ReactDOM.render( + , + document.querySelector(selector), + ); + } + } +} diff --git a/cms/static/sass/views/_dashboard.scss b/cms/static/sass/views/_dashboard.scss index af144c0d7564..8c9ac6b1eaa3 100644 --- a/cms/static/sass/views/_dashboard.scss +++ b/cms/static/sass/views/_dashboard.scss @@ -445,26 +445,6 @@ // STATE: hover/focus &:hover { background: $paleYellow; - - .course-actions { - opacity: 1.0; - pointer-events: auto; - } - - .view-live-button { - @extend %ui-depth3; - @extend %btn-primary-blue; - @extend %sizing; - @include transition(opacity $tmg-f2 ease-in-out 0); - @include box-sizing(border-box); - padding: ($baseline/2); - opacity: 0.0; - pointer-events: none; - } - - .course-metadata { - opacity: 1.0; - } } .course-link, .course-actions { @@ -498,8 +478,8 @@ & + .metadata-item:before { content: "/"; - margin-left: ($baseline/10); - margin-right: ($baseline/10); + margin-left: ($baseline/4); + margin-right: ($baseline/4); color: $gray-l4; } @@ -509,18 +489,15 @@ } .extra-metadata { - margin-left: ($baseline/10); + margin-left: ($baseline/4); } } .course-actions { - @include transition(opacity $tmg-f2 ease-in-out 0); @extend %ui-depth3; position: static; width: flex-grid(3, 9); @include text-align(right); - opacity: 0; - pointer-events: none; .action { display: inline-block; @@ -546,11 +523,6 @@ .action-rerun { margin-right: $baseline; } - - .rerun-button { - font-weight: 600; - // TODO: sync up button styling and add secondary style here - } } // CASE: is processing diff --git a/cms/templates/course-create-rerun.html b/cms/templates/course-create-rerun.html index a438af889513..300153bde851 100644 --- a/cms/templates/course-create-rerun.html +++ b/cms/templates/course-create-rerun.html @@ -80,7 +80,7 @@

-
  • +
  • diff --git a/cms/templates/index.html b/cms/templates/index.html index e19078b656ee..3918a67f9c38 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -3,10 +3,13 @@ from django.utils.translation import ugettext as _ from openedx.core.djangolib.markup import HTML, Text +from openedx.core.djangolib.js_utils import ( + dump_js_escaped_json + ) %> <%inherit file="base.html" /> - +<%namespace name='static' file='static_content.html'/> <%def name="online_help_token()"><% return "home" %> <%block name="title">${_("{studio_name} Home").format(studio_name=settings.STUDIO_SHORT_NAME)} <%block name="bodyclass">is-signedin index view-dashboard @@ -73,7 +76,7 @@

    ${_("Create a New Course")}

    ${_("The public display name for your course. This cannot be changed, but you can set a different display name in Advanced Settings later.")}
  • -
  • +
  • ## Translators: This is an example for the name of the organization sponsoring a course, seen when filling out the form to create a new course. The organization name cannot contain spaces. ## Translators: "e.g. UniversityX or OrganizationX" is a placeholder displayed when user put no data into this field. @@ -149,7 +152,7 @@

    ${_("Create a New Library")}

    ${_("The public display name for your library.")}
  • -
  • +
  • ${_("The public organization name for your library.")} ${_("This cannot be changed.")} @@ -321,41 +324,7 @@

    ${course_info['display_name']}

    % endif %if len(courses) > 0 or optimization_enabled: -
    - -
    +
    %else:
    @@ -473,68 +442,11 @@

    ${_('Your Course Creator Request Status:')}

    % endif %if archived_courses: -
    - -
    +
    %endif - %if len(libraries) > 0: - + %if len(libraries) > 0 or optimization_enabled: +
    %else:
    @@ -640,4 +552,21 @@

    ${_('Need help?')}

    %endif
    +<%static:webpack entry="StudioIndex"> + var enableReruns = ${allow_course_reruns and rerun_creator_status and course_creator_status=='granted' | n, dump_js_escaped_json}; + new StudioCourseIndex( + ".react-course-listing", + ${sorted(courses, key=lambda s: s['display_name'].lower() if s['display_name'] is not None else '') | n, dump_js_escaped_json}, + enableReruns + ); + new StudioArchivedIndex( + ".react-archived-course-listing", + ${sorted(archived_courses, key=lambda s: s['display_name'].lower() if s['display_name'] is not None else '') | n, dump_js_escaped_json}, + enableReruns + ); + new StudioLibraryIndex( + ".react-library-listing", + ${sorted(libraries, key=lambda s: s['display_name'].lower() if s['display_name'] is not None else '') | n, dump_js_escaped_json} + ); + diff --git a/cms/templates/js/mock/mock-create-course-rerun.underscore b/cms/templates/js/mock/mock-create-course-rerun.underscore index 1bc518655f27..ff9548b0bca6 100644 --- a/cms/templates/js/mock/mock-create-course-rerun.underscore +++ b/cms/templates/js/mock/mock-create-course-rerun.underscore @@ -59,7 +59,7 @@
  • -
  • +
  • The public display name for your course. This cannot be changed, but you can set a different display name in Advanced Settings later.
  • -
  • +
  • The name of the organization sponsoring the course. Note: This is part of your course URL, so no spaces or special characters are allowed. This cannot be changed, but you can set a different display name in Advanced Settings later. @@ -96,7 +96,7 @@ The public display name for your library.
  • -
  • +
  • The public organization name for your library. This cannot be changed. diff --git a/common/test/acceptance/pages/studio/index.py b/common/test/acceptance/pages/studio/index.py index 119c7ca7d48b..c873fc8540ef 100644 --- a/common/test/acceptance/pages/studio/index.py +++ b/common/test/acceptance/pages/studio/index.py @@ -205,12 +205,13 @@ def select_item_in_autocomplete_widget(self, item_text): ) self.q(css='.ui-autocomplete .ui-menu-item a').filter(lambda el: el.text == item_text)[0].click() - def list_courses(self): + def list_courses(self, archived=False): """ - List all the courses found on the page's list of libraries. + List all the courses found on the page's list of courses. """ # Workaround Selenium/Firefox bug: `.text` property is broken on invisible elements - course_tab_link = self.q(css='#course-index-tabs .courses-tab a') + tab_selector = '#course-index-tabs .{} a'.format('archived-courses-tab' if archived else 'courses-tab') + course_tab_link = self.q(css=tab_selector) if course_tab_link: course_tab_link.click() div2info = lambda element: { @@ -220,13 +221,14 @@ def list_courses(self): 'run': element.find_element_by_css_selector('.course-run .value').text, 'url': element.find_element_by_css_selector('a.course-link').get_attribute('href'), } - return self.q(css='.courses li.course-item').map(div2info).results + course_list_selector = '.{} li.course-item'.format('archived-courses' if archived else 'courses') + return self.q(css=course_list_selector).map(div2info).results - def has_course(self, org, number, run): + def has_course(self, org, number, run, archived=False): """ Returns `True` if course for given org, number and run exists on the page otherwise `False` """ - for course in self.list_courses(): + for course in self.list_courses(archived): if course['org'] == org and course['number'] == number and course['run'] == run: return True return False @@ -245,6 +247,7 @@ def list_libraries(self): 'name': element.find_element_by_css_selector('.course-title').text, 'org': element.find_element_by_css_selector('.course-org .value').text, 'number': element.find_element_by_css_selector('.course-num .value').text, + 'link_element': element.find_element_by_css_selector('a.library-link'), 'url': element.find_element_by_css_selector('a.library-link').get_attribute('href'), } self.wait_for_element_visibility('.libraries li.course-item', "Switch to library tab") @@ -259,6 +262,14 @@ def has_library(self, **kwargs): return True return False + def click_library(self, name): + """ + Click on the library with the given name. + """ + for lib in self.list_libraries(): + if lib['name'] == name: + lib['link_element'].click() + @property def language_selector(self): """ diff --git a/common/test/acceptance/tests/studio/test_studio_course_create.py b/common/test/acceptance/tests/studio/test_studio_course_create.py index 0e1c2ae3c2f8..a3e3a7d4d488 100644 --- a/common/test/acceptance/tests/studio/test_studio_course_create.py +++ b/common/test/acceptance/tests/studio/test_studio_course_create.py @@ -97,6 +97,9 @@ def test_create_course_with_existing_org(self): self.assertTrue(self.dashboard_page.has_course( org=self.course_org, number=self.course_number, run=self.course_run )) + # Click on the course listing and verify that the Studio course outline page opens. + self.dashboard_page.click_course_run(self.course_run) + course_outline_page.wait_for_page() def test_create_course_with_existing_org_via_autocomplete(self): """ diff --git a/common/test/acceptance/tests/studio/test_studio_home.py b/common/test/acceptance/tests/studio/test_studio_home.py index d8c93da21afc..6734bd8c371b 100644 --- a/common/test/acceptance/tests/studio/test_studio_home.py +++ b/common/test/acceptance/tests/studio/test_studio_home.py @@ -1,15 +1,18 @@ """ Acceptance tests for Home Page (My Courses / My Libraries). """ +import datetime from uuid import uuid4 from flaky import flaky from opaque_keys.edx.locator import LibraryLocator +from base_studio_test import StudioCourseTest from common.test.acceptance.pages.common.auto_auth import AutoAuthPage from common.test.acceptance.pages.lms.account_settings import AccountSettingsPage from common.test.acceptance.pages.studio.index import DashboardPage from common.test.acceptance.pages.studio.library import LibraryEditPage +from common.test.acceptance.pages.studio.overview import CourseOutlinePage from common.test.acceptance.tests.helpers import AcceptanceTest, get_selected_option_text, select_option_by_text @@ -60,6 +63,9 @@ def test_create_library(self): # Then go back to the home page and make sure the new library is listed there: self.dashboard_page.visit() self.assertTrue(self.dashboard_page.has_library(name=name, org=org, number=number)) + # Click on the library listing and verify that the library edit view loads. + self.dashboard_page.click_library(name) + lib_page.wait_for_page() class StudioLanguageTest(AcceptanceTest): @@ -95,3 +101,44 @@ def test_studio_language_change(self): get_selected_option_text(language_selector), u'Dummy Language (Esperanto)' ) + + +class ArchivedCourseTest(StudioCourseTest): + """ Tests that archived courses appear in their own list. """ + + def setUp(self, is_staff=True, test_xss=False): + """ + Load the helper for the home page (dashboard page) + """ + super(ArchivedCourseTest, self).setUp(is_staff=is_staff, test_xss=test_xss) + self.dashboard_page = DashboardPage(self.browser) + + def populate_course_fixture(self, course_fixture): + current_time = datetime.datetime.now() + course_start_date = current_time - datetime.timedelta(days=60) + course_end_date = current_time - datetime.timedelta(days=90) + + course_fixture.add_course_details({ + 'start_date': course_start_date, + 'end_date': course_end_date + }) + + def test_archived_course(self): + """ + Scenario: Ensure that an archived course displays in its own list and can be clicked on. + """ + self.dashboard_page.visit() + self.assertTrue(self.dashboard_page.has_course( + org=self.course_info['org'], number=self.course_info['number'], run=self.course_info['run'], + archived=True + )) + + # Click on the archived course and make sure that the Studio course outline appears. + self.dashboard_page.click_course_run(self.course_info['run']) + course_outline_page = CourseOutlinePage( + self.browser, + self.course_info['org'], + self.course_info['number'], + self.course_info['run'] + ) + course_outline_page.wait_for_page() diff --git a/package.json b/package.json index 26c1894f2f17..46f8207ee51c 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "babel-core": "^6.23.0", "babel-loader": "^6.4.0", "babel-preset-env": "^1.2.1", + "babel-preset-react": "^6.24.1", "backbone": "~1.3.2", "backbone.paginator": "~2.0.3", "coffee-loader": "^0.7.3", @@ -21,7 +22,10 @@ "moment": "^2.15.1", "moment-timezone": "~0.5.5", "picturefill": "~3.0.2", + "prop-types": "^15.5.10", "raw-loader": "^0.5.1", + "react": "^15.5.4", + "react-dom": "^15.5.4", "requirejs": "~2.3.2", "string-replace-webpack-plugin": "^0.1.3", "uglify-js": "2.7.0", diff --git a/pavelib/quality.py b/pavelib/quality.py index 54a8333f6a44..717db9dcb648 100644 --- a/pavelib/quality.py +++ b/pavelib/quality.py @@ -288,7 +288,7 @@ def run_eslint(options): violations_limit = int(getattr(options, 'limit', -1)) sh( - "eslint --format=compact . | tee {eslint_report}".format( + "eslint --ext .js --ext .jsx --format=compact . | tee {eslint_report}".format( eslint_report=eslint_report ), ignore_error=True diff --git a/scripts/all-tests.sh b/scripts/all-tests.sh index 4fa15f49b6af..eb93c94c9a50 100755 --- a/scripts/all-tests.sh +++ b/scripts/all-tests.sh @@ -12,7 +12,7 @@ set -e # Violations thresholds for failing the build export PYLINT_THRESHOLD=3600 -export ESLINT_THRESHOLD=9190 +export ESLINT_THRESHOLD=9134 XSSLINT_THRESHOLDS=`cat scripts/xsslint_thresholds.json` export XSSLINT_THRESHOLDS=${XSSLINT_THRESHOLDS//[[:space:]]/} diff --git a/webpack.config.js b/webpack.config.js index 9ba5257aea6f..619af0dd5100 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -24,7 +24,8 @@ var wpconfig = { CourseTalkReviews: './openedx/features/course_experience/static/course_experience/js/CourseTalkReviews.js', WelcomeMessage: './openedx/features/course_experience/static/course_experience/js/WelcomeMessage.js', Enrollment: './openedx/features/course_experience/static/course_experience/js/Enrollment.js', - Import: './cms/static/js/features/import/factories/import.js' + Import: './cms/static/js/features/import/factories/import.js', + StudioIndex: './cms/static/js/features_jsx/studio/index.jsx' }, output: { @@ -68,7 +69,7 @@ var wpconfig = { // invoke this plugin until we can upgrade karma-webpack. new webpack.optimize.CommonsChunkPlugin({ // If the value below changes, update the render_bundle call in - // common/djangoapps/pipeline_mako/templates/static_content.html + // common/djangoapps/pipeline_mako/templates/static_content.html name: 'commons', filename: 'commons.js', minChunks: 2