Convert course list to React.
EDUCATOR-625, AC-620
This commit is contained in:
@@ -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,
|
||||
'<h3 class="course-title">Robot Super Educational Course</h3>',
|
||||
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='<script>alert("course XSS")</script>'
|
||||
)
|
||||
|
||||
LibraryFactory.create(display_name='<script>alert("library XSS")</script>')
|
||||
|
||||
resp = self.client.get_html('/home/')
|
||||
for xss in ('course', 'library'):
|
||||
html = '<script>alert("{name} XSS")</script>'.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):
|
||||
|
||||
@@ -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 = "<script>alert('ESCAPE')</script>"
|
||||
|
||||
# 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
7
cms/static/js/features_jsx/.eslintrc.js
Normal file
7
cms/static/js/features_jsx/.eslintrc.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: 'eslint-config-edx',
|
||||
root: true,
|
||||
settings: {
|
||||
'import/resolver': 'webpack',
|
||||
},
|
||||
};
|
||||
131
cms/static/js/features_jsx/studio/index.jsx
Normal file
131
cms/static/js/features_jsx/studio/index.jsx
Normal file
@@ -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 (
|
||||
<ul className="list-courses">
|
||||
{
|
||||
props.items.map((item, i) =>
|
||||
(
|
||||
<li key={i} className="course-item" data-course-key={item.course_key}>
|
||||
<a className={linkClass} href={item.url}>
|
||||
<h3 className="course-title" id={`title-${idBase}-${i}`}>{item.display_name}</h3>
|
||||
<div className="course-metadata">
|
||||
<span className="course-org metadata-item">
|
||||
<span className="label">{gettext('Organization:')}</span>
|
||||
<span className="value">{item.org}</span>
|
||||
</span>
|
||||
<span className="course-num metadata-item">
|
||||
<span className="label">{gettext('Course Number:')}</span>
|
||||
<span className="value">{item.number}</span>
|
||||
</span>
|
||||
{ item.run &&
|
||||
<span className="course-run metadata-item">
|
||||
<span className="label">{gettext('Course Run:')}</span>
|
||||
<span className="value">{item.run}</span>
|
||||
</span>
|
||||
}
|
||||
{ item.can_edit === false &&
|
||||
<span className="extra-metadata">{gettext('(Read-only)')}</span>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
{ item.lms_link && item.rerun_link &&
|
||||
<ul className="item-actions course-actions">
|
||||
{ allowReruns &&
|
||||
<li className="action action-rerun">
|
||||
<a
|
||||
href={item.rerun_link}
|
||||
className="button rerun-button"
|
||||
aria-labelledby={`re-run-${idBase}-${i} title-${idBase}-${i}`}
|
||||
id={`re-run-${idBase}-${i}`}
|
||||
>{gettext('Re-run Course')}</a>
|
||||
</li>
|
||||
}
|
||||
<li className="action action-view">
|
||||
<a
|
||||
href={item.lms_link}
|
||||
rel="external"
|
||||
className="button view-button"
|
||||
aria-labelledby={`view-live-${idBase}-${i} title-${idBase}-${i}`}
|
||||
id={`view-live-${idBase}-${i}`}
|
||||
>{gettext('View Live')}</a>
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
</li>
|
||||
),
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
<CourseOrLibraryListing
|
||||
items={context}
|
||||
linkClass="course-link"
|
||||
idBase="course"
|
||||
allowReruns={allowReruns}
|
||||
/>,
|
||||
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(
|
||||
<CourseOrLibraryListing
|
||||
items={context}
|
||||
linkClass="course-link"
|
||||
idBase="archived"
|
||||
allowReruns={allowReruns}
|
||||
/>,
|
||||
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(
|
||||
<CourseOrLibraryListing
|
||||
items={context}
|
||||
linkClass="library-link"
|
||||
idBase="library"
|
||||
allowReruns={false}
|
||||
/>,
|
||||
document.querySelector(selector),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -80,7 +80,7 @@ from openedx.core.djangolib.js_utils import js_escaped_string
|
||||
</span>
|
||||
<span class="tip tip-error is-hidden"></span>
|
||||
</li>
|
||||
<li class="field text required" id="field-organization">
|
||||
<li class="field text required">
|
||||
<label for="rerun-course-org">${_("Organization")}</label>
|
||||
<input class="rerun-course-org" id="rerun-course-org" type="text" name="rerun-course-org" aria-required="true" value="${source_course_key.org}" placeholder="${_('e.g. UniversityX or OrganizationX')}" />
|
||||
<span class="tip">
|
||||
|
||||
@@ -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
|
||||
<span class="tip" id="tip-new-course-name">${_("The public display name for your course. This cannot be changed, but you can set a different display name in Advanced Settings later.")}</span>
|
||||
<span class="tip tip-error is-hiding" id="tip-error-new-course-name"></span>
|
||||
</li>
|
||||
<li class="field text required" id="field-organization">
|
||||
<li class="field text required">
|
||||
<label for="new-course-org">${_("Organization")}</label>
|
||||
## 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
|
||||
<span class="tip" id="tip-new-library-name">${_("The public display name for your library.")}</span>
|
||||
<span class="tip tip-error is-hiding" id="tip-error-new-library-name"></span>
|
||||
</li>
|
||||
<li class="field text required" id="field-organization">
|
||||
<li class="field text required">
|
||||
<label for="new-library-org">${_("Organization")}</label>
|
||||
<input class="new-library-org" id="new-library-org" type="text" name="new-library-org" required placeholder="${_('e.g. UniversityX or OrganizationX')}" aria-describedby="tip-new-library-org tip-error-new-library-org" />
|
||||
<span class="tip" id="tip-new-library-org">${_("The public organization name for your library.")} ${_("This cannot be changed.")}</span>
|
||||
@@ -321,41 +324,7 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
% endif
|
||||
|
||||
%if len(courses) > 0 or optimization_enabled:
|
||||
<div class="courses courses-tab active">
|
||||
<ul class="list-courses">
|
||||
%for course_info in sorted(courses, key=lambda s: s['display_name'].lower() if s['display_name'] is not None else ''):
|
||||
<li class="course-item" data-course-key="${course_info['course_key']}">
|
||||
<a class="course-link" href="${course_info['url']}">
|
||||
<h3 class="course-title">${course_info['display_name']}</h3>
|
||||
|
||||
<div class="course-metadata">
|
||||
<span class="course-org metadata-item">
|
||||
<span class="label">${_("Organization:")}</span> <span class="value">${course_info['org']}</span>
|
||||
</span>
|
||||
<span class="course-num metadata-item">
|
||||
<span class="label">${_("Course Number:")}</span>
|
||||
<span class="value">${course_info['number']}</span>
|
||||
</span>
|
||||
<span class="course-run metadata-item">
|
||||
<span class="label">${_("Course Run:")}</span> <span class="value">${course_info['run']}</span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<ul class="item-actions course-actions">
|
||||
% if allow_course_reruns and rerun_creator_status and course_creator_status=='granted':
|
||||
<li class="action action-rerun">
|
||||
<a href="${course_info['rerun_link']}" class="button rerun-button">${_("Re-run Course")}</a>
|
||||
</li>
|
||||
% endif
|
||||
<li class="action action-view">
|
||||
<a href="${course_info['lms_link']}" rel="external" class="button view-button">${_("View Live")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
%endfor
|
||||
</ul>
|
||||
</div>
|
||||
<div class="courses courses-tab react-course-listing active"></div>
|
||||
|
||||
%else:
|
||||
<div class="notice notice-incontext notice-instruction notice-instruction-nocourses list-notices courses-tab active">
|
||||
@@ -473,68 +442,11 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
% endif
|
||||
|
||||
%if archived_courses:
|
||||
<div class="archived-courses archived-courses-tab">
|
||||
<ul class="list-courses">
|
||||
%for course_info in sorted(archived_courses, key=lambda s: s['display_name'].lower() if s['display_name'] is not None else ''):
|
||||
<li class="course-item" data-course-key="${course_info['course_key']}">
|
||||
<a class="course-link" href="${course_info['url']}">
|
||||
<h3 class="course-title">${course_info['display_name']}</h3>
|
||||
|
||||
<div class="course-metadata">
|
||||
<span class="course-org metadata-item">
|
||||
<span class="label">${_("Organization:")}</span> <span class="value">${course_info['org']}</span>
|
||||
</span>
|
||||
<span class="course-num metadata-item">
|
||||
<span class="label">${_("Course Number:")}</span>
|
||||
<span class="value">${course_info['number']}</span>
|
||||
</span>
|
||||
<span class="course-run metadata-item">
|
||||
<span class="label">${_("Course Run:")}</span> <span class="value">${course_info['run']}</span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<ul class="item-actions course-actions">
|
||||
% if allow_course_reruns and rerun_creator_status and course_creator_status=='granted':
|
||||
<li class="action action-rerun">
|
||||
<a href="${course_info['rerun_link']}" class="button rerun-button">${_("Re-run Course")}</a>
|
||||
</li>
|
||||
% endif
|
||||
<li class="action action-view">
|
||||
<a href="${course_info['lms_link']}" rel="external" class="button view-button">${_("View Live")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
%endfor
|
||||
</ul>
|
||||
</div>
|
||||
<div class="archived-courses react-archived-course-listing archived-courses-tab"></div>
|
||||
%endif
|
||||
|
||||
%if len(libraries) > 0:
|
||||
<div class="libraries libraries-tab">
|
||||
<ul class="list-courses">
|
||||
%for library_info in sorted(libraries, key=lambda s: s['display_name'].lower() if s['display_name'] is not None else ''):
|
||||
<li class="course-item">
|
||||
<a class="library-link" href="${library_info['url']}">
|
||||
<h3 class="course-title">${library_info['display_name']}</h3>
|
||||
|
||||
<div class="course-metadata">
|
||||
<span class="course-org metadata-item">
|
||||
<span class="label">${_("Organization:")}</span> <span class="value">${library_info['org']}</span>
|
||||
</span>
|
||||
<span class="course-num metadata-item">
|
||||
<span class="label">${_("Course Number:")}</span>
|
||||
<span class="value">${library_info['number']}</span>
|
||||
</span>
|
||||
% if not library_info["can_edit"]:
|
||||
<span class="extra-metadata">${_("(Read-only)")}</span>
|
||||
% endif
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
%endfor
|
||||
</ul>
|
||||
</div>
|
||||
%if len(libraries) > 0 or optimization_enabled:
|
||||
<div class="libraries react-library-listing libraries-tab"></div>
|
||||
|
||||
%else:
|
||||
<div class="notice notice-incontext notice-instruction notice-instruction-nocourses list-notices libraries-tab">
|
||||
@@ -640,4 +552,21 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
|
||||
%endif
|
||||
</div>
|
||||
<%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>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
</span>
|
||||
<span class="tip tip-error is-hidden"></span>
|
||||
</li>
|
||||
<li class="field text required" id="field-organization">
|
||||
<li class="field text required">
|
||||
<label for="rerun-course-org">Organization</label>
|
||||
<input class="rerun-course-org" id="rerun-course-org" type="text"
|
||||
name="rerun-course-org" aria-required="true"
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<span class="tip">The public display name for your course. This cannot be changed, but you can set a different display name in Advanced Settings later.</span>
|
||||
<span class="tip tip-error is-hiding"></span>
|
||||
</li>
|
||||
<li class="field text required" id="field-organization">
|
||||
<li class="field text required">
|
||||
<label for="new-course-org">Organization</label>
|
||||
<input class="new-course-org" id="new-course-org" type="text" name="new-course-org" aria-required="true" placeholder="e.g. UniversityX or OrganizationX" />
|
||||
<span class="tip">The name of the organization sponsoring the course. <strong>Note: This is part of your course URL, so no spaces or special characters are allowed.</strong> This cannot be changed, but you can set a different display name in Advanced Settings later.</span>
|
||||
@@ -96,7 +96,7 @@
|
||||
<span class="tip">The public display name for your library.</span>
|
||||
<span class="tip tip-error is-hiding"></span>
|
||||
</li>
|
||||
<li class="field text required" id="field-organization">
|
||||
<li class="field text required">
|
||||
<label for="new-library-org">Organization</label>
|
||||
<input class="new-library-org" id="new-library-org" type="text" name="new-library-org" aria-required="true" placeholder="e.g. UniversityX or OrganizationX" />
|
||||
<span class="tip">The public organization name for your library. This cannot be changed.</span>
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:]]/}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user