Merge pull request #15390 from edx/christina/first-react
Convert Studio course and library dashboard lists to React.
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>
|
||||
|
||||
Reference in New Issue
Block a user