diff --git a/.babelrc b/.babelrc
index 91cf362dc6..6c6df4b2b9 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 8d4a9085fc..f53c705dcc 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.keys import CourseKey, UsageKey
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 @@ class MiscCourseTests(ContentStoreTestCase):
@ddt.ddt
-class ContentStoreTest(ContentStoreTestCase, XssTestMixin):
+class ContentStoreTest(ContentStoreTestCase):
"""
Tests for the CMS ContentStore application.
"""
@@ -1473,33 +1472,6 @@ class ContentStoreTest(ContentStoreTestCase, XssTestMixin):
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 @@ class RerunCourseTest(ContentStoreTestCase):
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 8d66a64ff6..c771fbd74f 100644
--- a/cms/djangoapps/contentstore/tests/test_course_listing.py
+++ b/cms/djangoapps/contentstore/tests/test_course_listing.py
@@ -9,11 +9,9 @@ from ccx_keys.locator import CCXLocator
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 @@ USER_COURSES_COUNT = 1
@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 @@ class TestCourseListing(ModuleStoreTestCase, XssTestMixin):
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 14221e030e..9dad8412cb 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 @@ class TestCourseIndex(CourseTestCase):
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 @@ class TestCourseIndex(CourseTestCase):
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 @@ class TestCourseIndex(CourseTestCase):
)
# 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 @@ class TestCourseIndexArchived(CourseTestCase):
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 684fa2a79f..f31b7b0f7f 100644
--- a/cms/envs/acceptance.py
+++ b/cms/envs/acceptance.py
@@ -106,6 +106,10 @@ FEATURES['ENABLE_DISCUSSION_SERVICE'] = False
# 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 b9ca6546bf..24e8226286 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -252,6 +252,10 @@ FEATURES = {
# 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 0000000000..12cb26eef9
--- /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 0000000000..0ba7b2d705
--- /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 af144c0d75..8c9ac6b1ea 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 a438af8895..300153bde8 100644
--- a/cms/templates/course-create-rerun.html
+++ b/cms/templates/course-create-rerun.html
@@ -80,7 +80,7 @@ from openedx.core.djangolib.js_utils import js_escaped_string
-
+
diff --git a/cms/templates/index.html b/cms/templates/index.html
index e19078b656..3918a67f9c 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" %>%def>
<%block name="title">${_("{studio_name} Home").format(studio_name=settings.STUDIO_SHORT_NAME)}%block>
<%block name="bodyclass">is-signedin index view-dashboard%block>
@@ -73,7 +76,7 @@ from openedx.core.djangolib.markup import HTML, Text
${_("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 @@ from openedx.core.djangolib.markup import HTML, Text
${_("The public display name for your library.")}
-
+
${_("The public organization name for your library.")} ${_("This cannot be changed.")}
@@ -321,41 +324,7 @@ from openedx.core.djangolib.markup import HTML, Text
% endif
%if len(courses) > 0 or optimization_enabled:
-
-
- %for course_info in sorted(courses, key=lambda s: s['display_name'].lower() if s['display_name'] is not None else ''):
-
+ %if len(libraries) > 0 or optimization_enabled:
+
%else:
@@ -640,4 +552,21 @@ from openedx.core.djangolib.markup import HTML, Text
%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}
+ );
+%static:webpack>
%block>
diff --git a/cms/templates/js/mock/mock-create-course-rerun.underscore b/cms/templates/js/mock/mock-create-course-rerun.underscore
index 1bc518655f..ff9548b0bc 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 119c7ca7d4..c873fc8540 100644
--- a/common/test/acceptance/pages/studio/index.py
+++ b/common/test/acceptance/pages/studio/index.py
@@ -205,12 +205,13 @@ class DashboardPage(PageObject, HelpMixin):
)
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 @@ class DashboardPage(PageObject, HelpMixin):
'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 @@ class DashboardPage(PageObject, HelpMixin):
'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 @@ class DashboardPage(PageObject, HelpMixin):
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 0e1c2ae3c2..a3e3a7d4d4 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 @@ class CreateCourseTest(AcceptanceTest):
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 d8c93da21a..6734bd8c37 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 @@ class CreateLibraryTest(AcceptanceTest):
# 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 @@ class StudioLanguageTest(AcceptanceTest):
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 26c1894f2f..46f8207ee5 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 54a8333f6a..717db9dcb6 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 4fa15f49b6..eb93c94c9a 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 9ba5257aea..619af0dd51 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