Commit 5e81eb5b by Christina Roberts Committed by GitHub

Merge pull request #15390 from edx/christina/first-react

Convert Studio course and library dashboard lists to React.
parents 679bd2c6 e1e57b5d
......@@ -11,6 +11,7 @@
},
"modules": false
}
]
],
"babel-preset-react"
]
}
......@@ -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
......
module.exports = {
extends: 'eslint-config-edx',
root: true,
settings: {
'import/resolver': 'webpack',
},
};
/* 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):
"""
......
"""
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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment